在第一节课中,基于Dogs vs. Cats
数据集,设置了一个ResNet34的网络,并通过学习速率选取方法,以及设置数据遍历次数为2,获得了一个准确率如下的网络:
Epoch | trn_loss | val_loss | accuracy |
---|---|---|---|
0 | 0.052014 | 0.028396 | 0.99 |
1 | 0.049761 | 0.028705 | 0.9885 |
本节将在上一节的基础上,通过若干参数的设定,提高所构造的分类网络的准确率。本节的主要内容有:
由于我们要在已有的图片卷积网络的基础上构建新网络,而已有的图片卷积网络通常是在尺寸为224×224或是299×299的图片集上进行训练的,因此,Fast.AI会对输入图片进行裁剪,以使之满足网络输入层的要求。这一操作是通过在获取图片分类器时,传入参数tfms
来实现的。如上节所示:
data = ImageClassfierData.from_paths(PATH, tfms=tfms_from_model(arch, sz))
事实上,裁剪后的图像可能会丢失关键特征区域(如猫或狗的脑袋),因此需要对输入图片进行额外的修饰,使得所构建的网络尽可能地“看全图片”。本节所采取的修饰如下:
tfms = tfms_from_model(resnet34, sz, aug_tfms=transforms_side_on, max_zoom=1.1)
其中transforms_side_on
表明图片是从侧向拍摄,即是一般的摄影图像,区别于卫星图像;对这种图像会进行轻微平移、轻微旋转、左右翻转等变换。参数max_zoom
指定了对图像进行缩放的幅度,即最大缩放不超过1.1倍。
由于输入图片被改变,由已有的卷积核所生成的结果应发生相应改变,因此设置precompute=False
。
通过数据修饰所获得准确率如下:
Epoch | trn_loss | val_loss | accuracy |
---|---|---|---|
0 | 0.044965 | 0.025717 | 0.992 |
1 | 0.039476 | 0.025723 | 0.9925 |
在随机梯度下降算法中,我们选取某一点做出发点,然后针对损失函数逐步优化相应参数。由于开始时网络对最优解并无信息,因此会随机的选择参数值做为出发点。此时我们期望优化算法能够有效地进行梯度下降,从而尽快的找到最优解。但随着数据的投喂,网络对最优解也有了一定的认知,而此时的参数估计值也越来越接近最优解,我们则期望优化算法不要跨太大的步子,从而避免在最优解附近震荡。基于这样的理念,随机梯度下降算法将使用逐步减小学习速率(Learning Rate Annealing
)的方法。
另一方面,我们期望最终训练出的网络,有稳定的泛化能力,即输入数据有微小变化时,其输出结果不至于变化的太离谱。这就要求训练所得的参数落在损失函数的较为平稳的区域。为达到这一目的,与逐步减小学习速率相反,我们会定期增大学习速率,以跳出底部尖锐(不稳定)的低损失函数区域。这就是随机梯度算法中的学习速率重置技巧(SGDR,Stochastic Gradient Descent with Restarts
)。如下图所示。
learn.fit(1e-2, 3, cycle_len=1)
其中cycle_len指定了学习速率重置的周期,本例中即在遍历一次图片集后,重置学习速率。现在更新一下参数3
的说明(原说明参见上一篇博文):3
表示学习速率重置次数。使用语句learn.sched.plot_lr()
可以画出学习速率的变化曲线:
Epoch | trn_loss | val_loss | accuracy |
---|---|---|---|
0 | 0.03602 | 0.024977 | 0.989 |
1 | 0.03687 | 0.022677 | 0.99 |
2 | 0.039753 | 0.022224 | 0.9925 |
整个网络是在arch
参数指定的架构的基础上搭建起来的。而原架构中的参数是在另外的数据集上训练得到的。比如在本节的示例中,采用的ResNet34卷积网络,其卷积核是在ImageNet数据集上训练的。若要使这些参数更契合Dog vs. Cat
的数据,可以考虑进行现有模型的微调(Fine Tuning
)。这一概念可通过下述代码实现:
learn.unfreeze()
lr=np.array([1e-4,1e-3,1e-2])
learn.fit(lr, 3, cycle_len=1, cycle_mult=2)
其中unfreeze()
表示对ResNet34的卷积层参数解锁。而设置学习速率为向量lr,则是不同的隐含层采用不同的学习速率。其值的设定遵从如下理念:在深度神经网络中,越靠近输入层的隐含层学到的特征越通用化,比如第一隐含层可能只是通过卷积核获取了图像的边缘、形状信息等特征;而越靠近输出层的隐含层越贴近应用场景越,比如ImageNet数据集可能划分有上千类,那么最贴近输出层的隐含层就要想办法识别出足够划分上千类别的特征。对应于Dog vs. Cat
问题,通用特征如边缘、特定形状等信息在这个问题中也可使用,因此获取这些信息的隐含层所需进行的微调幅度应该很小;另一方面,由于本例仅需分别两类,因此,ResNet34网络的靠近输出层的参数需要的微调幅度应该大些。基于这些考量,学习速率向量设定才如代码所示。
在learn.fit()
中,又出现了一个新参数cycle_mult=2
,其表示学习速率每次重置时,遍历数据集的次数就翻倍。这是基于如下理念:在训练之初,由于所获得的信息有限,我们不指望一开始就能通过梯度下降进入泛化能力强的参数所在区域。这一阶段,我们较为频繁地改变学习速率,就能检验更大的参范围,尽快地找到理想的参数区域。但随着训练的进行,网络获得了越来越多的信息,优化算法基本限定在了理想参数所在的区域,此时我们希望能够做足够的训练以提高输出准确率,因此需要遍历多次数据集。cycle_mult
的作用如下图所示。
Epoch | trn_loss | val_loss | accuracy |
---|---|---|---|
4 | 0.02953 | 0.021555 | 0.993 |
5 | 0.024118 | 0.019837 | 0.9935 |
6 | 0.015768 | 0.017175 | 0.993 |
正如训练数据需要修饰一样,由于网络输入层要求图片长宽一致,因此传入测试数据给网络时,也需裁剪,这样也会导致关键区域的缺失。此时需对测试数据进行修饰(TTA, Test Time Augmentation
)。与在训练数据上所采用的策略不同,在此对测试数据随机进行4组变换,然后取对4组变换的检测结果的平均值。实现代码如下:
log_preds,y = learn.TTA()
probs = np.mean(np.exp(log_preds),0)
其中log_preds
为预测的概率值的对数,因此在取平均时需要做指数运算。
precompute=True
。lr_find()
找到损失函数仍在明显降低的较大的学习速率。precompute=False
,设定cycle_len=1
,遍历数据集2~3次,训练附加在已有模型后的全连接层。lr_find()
函数,找到合适的学习速率。设置cycle_mult=2
,训练所有参数,直至出现overfitting现象。为实践上述步骤,课程中选取Kaggle的狗类分辨比赛Dog Breed Challenge为例进行演示。
训练数据包含120类狗的10223张图片,其说明以CSV文件存储,格式如下:
首先需要从训练数据中分割出验证集:
val_idxs = get_cv_idxs(n)
其中n
为数据样本数,get_cv_indx()
默认会挑出20%的数据索引。
tfms = tfms_from_model(arch, sz,
aug_tfms=transforms_side_on, max_zoom=1.1)
data = ImageClassifierData.from_csv(PATH, 'train',
f'{PATH}labels.csv', test_name='test',
val_idxs=val_idxs, suffix='.jpg', tfms=tfms, bs=bs)
其中from_csv()
指定了数据组织形式是csv文件;'train'
为训练数据集所在目录,'f'{PATH}labels.csv''
为csv文件,val_idxs
为验证数据的索引,suffix
为图片文件扩展名。出于显存的考虑,bz
设置为24(不同于课程中的值,这也导致了后续训练参数的变化)。
lr_find()
确定学习速率本例数据与猫狗分类的的数据类似,因此学习速率直接设定为0.01。
learn = ConvLearner.pretrained(arch, data, precompute=True)
learn.fit(1e-2, 3)
所得准确率如下
Epoch | trn_loss | val_loss | accuracy |
---|---|---|---|
0 | 0.616665 | 0.32665 | 0.893346 |
1 | 0.392434 | 0.278094 | 0.905088 |
2 | 0.29207 | 0.262963 | 0.913405 |
课程中设定遍历次数为5,因为其bz=16
。而在本博文bz=24
的情况下,5次遍历大概率会过拟合。后续参数不同于课程中所设的原因也是如此,不再赘述。
precompute=False
,设定cycle_len=1
,重新训练learn.precompute = False
learn.fit(1e-2, 3, cycle_len=1)
所得准确率为:
Epoch | trn_loss | val_loss | accuracy |
---|---|---|---|
0 | 0.305328 | 0.249781 | 0.919765 |
1 | 0.288835 | 0.253377 | 0.923679 |
2 | 0.261483 | 0.255604 | 0.91683 |
cycle_mult=2
,训练所有参数learn.unfreeze()
lr=np.array([1e-4,1e-3,1e-2])
learn.fit(lr, 3, cycle_len=1, cycle_mult=2
所得准确率为:
Epoch | trn_loss | val_loss | accuracy |
---|---|---|---|
0 | 0.615331 | 0.433425 | 0.866928 |
1 | 0.899787 | 0.655809 | 0.799413 |
2 | 0.264583 | 0.402661 | 0.886497 |
3 | 0.951714 | 0.964435 | 0.729941 |
4 | 0.419752 | 0.618164 | 0.822407 |
5 | 0.172857 | 0.466143 | 0.871331 |
6 | 0.104304 | 0.443973 | 0.876223 |
其实效果不是太好。因此课程中其实并未用这一步骤。主要原因是本例所用数据集应该是ImageNet数据集的子集,因此由ImageNet数据集训练所得的网络参数更为有效。而若根据本数据集进行微调,反而会使网络性能下降。这也从另一方面说明了数据集的重要性。
课程中实际采用了另一种策略:若数据集为图像,则从小尺寸的图像开始训练,然后转到大尺寸图像继续训练,是一种有效避免过拟合的手段。代码如下:
learn.set_data(get_data(299, bs))
learn.freeze()
learn.fit(1e-2, 3, cycle_len=1)
所得准确率如下:
Epoch | trn_loss | val_loss | accuracy |
---|---|---|---|
0 | 0.265264 | 0.238493 | 0.921233 |
1 | 0.294865 | 0.245732 | 0.920744 |
2 | 0.241306 | 0.235098 | 0.925636 |
上述结果中训练数据集的损失值和验证集的损失值已经非常接近,再训练会出现过拟合现象,因此,博主训到这儿就结束了。课程中没有这个现象,因此还会设置cycle_mut=2
,继续训一轮。
最后采用测试集数据修饰,代码如下:
log_preds, y = learn.TTA()
probs = np.mean(np.exp(log_preds), 0)
accuracy_np(probs,y)
metrics.log_loss(y, probs)
得到在测试集上的分类准确度为0.928,所得对数损失函数值为0.219。而课程中得到的值为0.941,0.1998。
learn.save(filename)
,会被存储在{PATH}/tmp/{arch}/models
目录下。learn.unfreeze_to(n)
。{PATH}/fastai
路径下。