DataWhale-天池街景数字识别竞赛-task5-模型集成
背景
2020年5月的DW组队学习选择了天池的街景字符编码识别,在这个入门竞赛中,数据集来自Google街景图像中的门牌号数据集(The Street View House Numbers Dataset, SVHN),并根据一定方式采样得到比赛数据集。评测标准为测试集预测结果的准确率,即编码识别正确的数量占测试集图片数量的比率。
组队学习的第五个任务是学会使用集成学习的方法来提高预测的精度。
本章学习手册内容由 安晟 编写,而本篇博客则是这章内容的笔记,在这里对作者表示感谢,受益匪浅!
常用集成学习方法
1.简介
深度学习模型的集成学习方法大致可以参考机器学习的集成学习方法,这些方法能在一定程度上提高预测精度,通常是对完成调参的多个模型所得的预测结果进行综合,以不同的方法进行结果的融合(如stacking、voting、bagging等),以提升模型整体的性能。
在进行模型融合之前,各个基学习器不能够太差,他们的效果最好是接近的。其次,它们之间要有区分度,模型的相关性不能够太高。要满足这两点,把多个学习器结合在一起,它们的效果才能比原先的各个基学习器要好。
2.方法
在Datawhale的学习手册中,ML67介绍了以下三大类的方法:
a)Voting:
由于我们解决的问题是字符的分类问题,对于分类问题,可以采用最简单的投票(Voting)法进行集成。具体方法就是根据不同模型的结果对某一位置的字符进行投票,规则是少数服从多数。
投票法分为硬投票与软投票。硬投票就是 对多个模型进行投票,不区分重要程度(没有权重),投票数最多的类为最终被预测的类。软投票和硬投票原理相同,增加了设置权重的功能,可以为不同模型设置不同权重,进而区别模型不同的重要度。这里直接自定义模型的权重。
投票法在多个模型相关性较低的时候才会起作用,不然多个模型预测出来的结果都是差不多的,投票的意义也就不大了。
b)stacking/blending:
这两种方法有着相似之处,都是构建多层模型,并利用预测结果再拟合预测。它的具体方法比较复杂,建议大家看一下这篇图解法的博客,可以有助于理解:
c)bagging/boosting:
bagging是用随机取样的方式从数据集中抽取一部分进行训练,从而根据取样的次数训练出多个不同的模型。boosting则是一个迭代的过程,用于自适应地改变训练样本的分布,使得基分类器聚焦在那些很难分的样本上。由于boosting的需要迭代生成,而bagging可以并行训练,节省时间,因此深度学习上采用bagging可能会更加方便。更多的内容可以参考这篇博客:
Dropout
Dropout机制是训练神经网络的一种技巧。在每个训练批次中,随机让一部分节点停止工作,同时在预测过程中让所有节点都起作用。这可以有效缓解过拟合的情况,在PyTorch中,加入dropout的语句为:torch.nn.
Dropout
(p=0.5, inplace=False).
官方文档中的解释为:
During training, randomly zeroes some of the elements of the input tensor with probability
p
using samples from a Bernoulli distribution. Each channel will be zeroed out independently on every forward call.
假如执行下列语句:
m = nn.Dropout(p=0.2)
input = torch.randn(4, 3)
output = m(input)
则输出:
input
tensor([[ 2.0818, -2.2274, 2.1362],
[ 0.3226, -0.4531, -1.8721],
[ 0.3396, -0.5100, -1.7158],
[ 0.5772, 0.2640, -0.4812]])
output
tensor([[ 0.0000, -0.0000, 2.6702],
[ 0.4032, -0.5664, -2.3402],
[ 0.4245, -0.6375, -2.1448],
[ 0.0000, 0.0000, -0.6015]])
可见输出中一些元素被置0了。
TTA
测试集数据扩增(Test time augmentation)即在测试集上预测时也用上数据扩增,简单来说就是对测试集预测多次,由于Dataloader每次读取的样本处理都不一样(如随机旋转、裁剪等),相当于每次预测都扩增一次,那么预测n次后再取个平均(其实不取平均也可以,类别就是直接取累加值最大的索引),就可以作为最后预测的结果。
代码的实现也十分的简单,在测试集预测时多加一个for循环以及最后加几条累加的语句即可:
def predict(test_loader, model, tta=10):
model.eval()
test_pred_tta = None
# TTA 次数
for _ in range(tta):
test_pred = []
with torch.no_grad():
for i, (input, target) in enumerate(test_loader):
c0, c1, c2, c3, c4, c5 = model(data[0])
output = np.concatenate([c0.data.numpy(), c1.data.numpy(),
c2.data.numpy(), c3.data.numpy(),
c4.data.numpy(), c5.data.numpy()], axis=1)
test_pred.append(output)
test_pred = np.vstack(test_pred)
if test_pred_tta is None:
test_pred_tta = test_pred
else:
test_pred_tta += test_pred
return test_pred_tta
预测字符的获取
在最后一小节补一个坑。
由于模型结果返回的只是一些数字,我们还需要将模型计算出来的结果转化为具体的字符,以计算预测的准确率。所以这里的处理值得我们仔细品味一下。首先看一下每个字符位置的模型返回值,这里以
c0, c1, c2, c3, c4, c5 = model(data[0])
中的c0为例:
它的shape为(batch_size * category_num),如果batch_size为128,那么在本例中形状为128*11,也就是说每行都是一个样本,每一列是它的softmax函数值。那下一步如何处理呢?注意到手册执行了这一句:
output = np.concatenate([c0.data.numpy(), c1.data.numpy(),
c2.data.numpy(), c3.data.numpy(),
c4.data.numpy(), c5.data.numpy()], axis=1)
这个拼接将每个样本的softmax函数值都集中在同一行中,前11个是第一个位置的softmax函数值,以此类推。
为了帮助理解,可以举一个例子:
input:
x = np.concatenate([[[1,2,2],[2,3,2]],[[3,4,2],[4,5,2]]], axis=1)
output:
x = array([[1, 2, 2, 3, 4, 2],
[2, 3, 2, 4, 5, 2]])
接着是一个列表的append操作,即将每个batch的样本拼接起来,用上述例子,可以得到这样的结果:
after append 3 times:
output =
[array([[1, 2, 2, 3, 4, 2],
[2, 3, 2, 4, 5, 2]]),
array([[1, 2, 2, 3, 4, 2],
[2, 3, 2, 4, 5, 2]]),
array([[1, 2, 2, 3, 4, 2],
[2, 3, 2, 4, 5, 2]])]
但是每一个元素都是一个ndarray,这样并不好,于是用这条语句合并一下:
test_pred = np.vstack(test_pred)
用上述例子,可以得到如下的输出:
after vstack:
array([[1, 2, 2, 3, 4, 2],
[2, 3, 2, 4, 5, 2],
[1, 2, 2, 3, 4, 2],
[2, 3, 2, 4, 5, 2],
[1, 2, 2, 3, 4, 2],
[2, 3, 2, 4, 5, 2]])
由此可见,该函数的返回值有num行,其中num为样本个数,有66列,分别代表每个位置的11个softmax函数值(使用tta后则是累加值)。
后续操作如下:
val_predict_label = np.vstack([
val_predict_label[:, :11].argmax(1),
val_predict_label[:, 11:22].argmax(1),
val_predict_label[:, 22:33].argmax(1),
val_predict_label[:, 33:44].argmax(1),
val_predict_label[:, 44:55].argmax(1),
]).T
首先是将softmax函数值最大的索引作为预测的类别,然后一个vstack将其合并,行数为字符的位置数,即6行,列数为样本数,最后一个T将其转置。得到一个样本数*6列的矩阵,列的值为类别序号。
最后:
for x in val_predict_label:
val_label_pred.append(''.join(map(str, x[x!=10])))
将类别10转化为空字符串(map函数对元素不为10的才起作用)。从而得到预测出来的字符。
最后
此次学习的教程由Datawhale提供,学习手册的链接为:点这里。