DenseNet训练实践

在Cifar10上训练DenseNet,复现DenseNet论文的结果是后面两篇分析工作的基础,因此本系列的第一篇,将首先讨论深度学习在训练过程中的几个方面:

1. 数据集

1.1 Cifar10数据集

Cifar10数据集有6万张32X32的图片,是一个小型的数据集,但数据的复杂度足够,下图来自DenseNet论文,可以看出,不用数据增强的话,ResNet的正确率也很难超过90%,因而足以用来验证一个模型的性能。
DenseNet训练实践_第1张图片

1.2 数据预处理

DenseNet论文对输入图像做了归一化,这样更容易训练。因为色彩饱和度的绝对量对人眼理解有意义,所以色彩的信息会有所损失。另外,如果训练出的模型用于迁移学习,目标数据集很可能和训练集的均值方差不一样,也会有一定的问题。但为了取得和论文相同的正确率,我仍旧采用和论文中相同的归一化。

1.3 验证集和测试集

无论是Keras还是TensorFlow打包提供的Cifar10数据集,都分成5万的一个训练集和一个1万的验证集,有的网上的测试数据也是验证集上的正确率,这样在调优的过程中会对验证集过拟合,因而验证集上的错误率就不能准确的反映/包含泛化误差。所以我把1万的验证集随机分成两部分:5000验证集和5000测试集。(参见函数generate_rand_index())在整个训练过程中,始终使用一次生成的固定索引访问5000验证集。最终训练结束时,验证集上的错误率和DenseNet论文的结果(见之前的论文截图)基本一致。然后用训练过程中保存的最优模型对5000测试集进行evaluate,结果没有发现在验证集上有明显的过拟合,参见函数test(model, model_file)。当然,由于5000比10000少了一半的数据,方差会大一点(5000上的标准差是10000上的 2 \sqrt{2} 2 倍),另外,由于数据划分的随机性,同一个模型在测试集上的正确率和验证集上的正确率还是有点不同。

2 优化算法

2.1 优化算法

DenseNet论文给出了训练过程和超参数的细节,优化算法使用SGD。实话说我觉得Adam明显更rational,但深度学习的训练很大程度上还没有完善精确的理论,SGD可能正是因为没有Adam那么理性,所以能在高维空间中更加放飞自我,跳跃到更好的点。在Cifar10上使用数据增强不用dropout训练k=12,depth=40的DenseNet,Adam算法在少量的测试中得到的正确率,比SGD差不到1%。Adam的优势主要在开始时收敛的会比较快,方便快速的进行验证和训练。而SGD在一开始收敛得没有那么好:
DenseNet训练实践_第2张图片
可以看出,使用SGD的话验证集的error和loss震荡得比较剧烈。这幅图来自[Keras] by tdeboissiere ,因为他的github项目里没有LICENSE文件,我就直接拷贝过来了:P 我的训练曲线和他的一样,在我的代码里注释掉的TensorBoard的调用能生成这些图。

2.2 学习率

经验表明,在训练过程中,当正确率和损失值达到极限陷入震荡时,如果指数性的减少学习率,会使正确率立即大幅跳跃提升。前一节的图展示了断崖式下降学习率带来的正确率的大幅变化。DenseNet论文在Cifar10上训练300 epochs,初始学习率0.1,训练到一半时(150 epochs)学习率除10,再到剩下的一半时(225 epochs)再除10。一开始我没这么办,而是尝试了一些自适应学习率的算法,结果发现自适应学习率算法的表现令人失望,并且尝试所花的时间更长。最后我还是老实的按论文实现了。代码中使用了Keras的LearningRateScheduler,这样实现更抽象一些。每次除10的下降只能进行3次,再多的话,学习率就太小起不到什么作用了。

如果使用Adam算法,初始学习率就不能用0.1了,我的尝试认为0.001比较好(尽管达不到SGD的正确率)。某些被广泛采用的经验性的学习率值之所以好用,应该和数据归一化(包括隐层使用的BatchNorm)有关,它使学习率的经验数值可以应用到不同的数据集和网络。

3 权值初始化

合理的权值初始化有助于快速收敛,有的情况下,不合理的初始化甚至造成无法收敛(这里我不是说权值初始化为0,这个问题已经广为人知了)。对于本文的实验,缺省的初始化函数,或者其他一些合理的初始化函数都可以比较快速的收敛到较高的正确率,基本不影响训练的结果。

4 正则化和Dropout

DenseNet论文里,不使用数据增强的话,需要开0.2的dropout,使用数据增强则不用dropout(用了也得不到更好的结果),无论哪种情况,都需要打开1-e4的weight decay,需要注意的是weight decay不只用在卷积核上,所有的BatchNorm和最后一层的Dense都要用。并且我的测试中发现weight decay对最终结果的影响还很大。
正则化相当于对网络施加一个很强的先验,如果这个先验对了,对网络的性能就会有很大贡献。1e-4这个数值应该也和数据归一化有关。
而Dropout有点自适应的意味,如果已经使用了数据增强和BatchNorm,它起的作用可能就不大了。

5 Reproducible

有时候你需要让你的训练结果可复现,总的思路是设定随机种子,并且不使用多线程进行并行操作,参见http://keras-cn.readthedocs.io/en/latest/for_beginners/FAQ/#keras_5。其实针对第二点,并非必须。

导致训练结果随机有三个原因:

  • 一是权值初始化时使用随机函数,如果使用的深度学习框架在模型建立的时候就确定了输入shape的话,那么所有的参数的shape就可以确定,比如Keras就强制要求提供一个输入shape,这种情况深度学习框架应该不会在多个线程里对不同的权值进行初始化,所以只要设定随机种子,网络里的权值就会顺序被初始化成固定的伪随机值。有的框架可能允许延后提供输入shape,这样就会使用延迟初始化,这时候也许不能保证初始化的顺序,所以需要在一开始就给出输入shape以避免这种情况发生。

  • 二是和训练的数据集有关,这里假设你想要复现结果时数据集本身是固定的,batch size也是固定的,这两者固定下来,你就必须保证数据按批次顺序依次进行训练,因为数据一般都需要shuffle,如果用了shuffle buffer,你的buffer大小也需要固定,shuffle一般在一个汇合点进行,所以实际上应该也无需担心多线程的问题,训练数据最终也会按固定的伪随机顺序feed进学习系统。如果你使用分布式训练,在数据分发时,遵循前述的原则就不会有问题。

  • 第三个使用随机函数的地方是dropout,这时网络前向传播已经在GPU里并行运行了。如果能不使用dropout可以避免这个问题。如果是分布式训练的情况,就更不可能保证了,除非你自己写代码预先的集中生成伪随机序列,然后分发给各个节点。

由于舍入的原因,浮点运算的结果可能会因为数值计算的顺序不同有所不同,这一点无法避免,比如框架或者编译器优化,我们只能假定其影响到的精度不会对最终训练的结果产生大的影响。

下一篇 DenseNet性能分析

你可能感兴趣的:(人工智能,深度学习与人工智能)