1. 梳理数据
训练神经网络的第一步是不要碰代码,先彻底检查自己的数据。这一步非常关键。我喜欢用大量时间浏览数千个样本,理解它们的分布,寻找其中的模式。有一次,我发现数据中包含重复的样本,还有一次我发现了损坏的图像/标签。我会查找数据不均衡和偏差。我通常还会注意自己的数据分类过程,它会揭示我们最终探索的架构。比如,只需要局部特征就够了还是需要全局语境?标签噪声多大?
2. 配置端到端训练/评估架构、获取基线结果
- 固定随机 seed:始终使用固定的随机 seed 能保证很多属性,例如在我们两次运行相同代码时能得到相同的输出。这能消除变化因子,从进行合理的判断。
- 简化:确保禁用不必要的技巧。例如,在这个阶段肯定需要关闭数据增强。数据增强可以在后期引入,并作为一种强大的正则化策略。不过在这个阶段引入的话,它就有机会带来一些愚蠢的 bug。
- 使用多数据、少次数的验证评估:当我们在绘制测试损失时,我们需要在整个比较大的测试集中执行评估。不要过几个批量就绘制一次测试损失,然后再依赖 TensorBoard 的平滑处理。我们虽然追求的是准确率,但也要防止犯这些低级错误。
- 在初始化中验证损失:验证你的损失函数在初始化中有比较合理的损失值。例如,如果你正确地初始化最终层,那么你应该通过-log(1/n_classes) 度量初始化的 Softmax 值。L2 回归和 Huber 损失函数等都有相同的默认值。
- 优秀的初始化:正确地初始化最终层。例如,如果你正在对均值为 50 的一些数据做回归处理,那么初始化的最终偏置项就应该为 50。如果你有一个非平衡数据集(两类样本数 1:10),那么就需要在 logits 上设置偏置项,令模型在初始化时预测概率为 0.1。正确配置这些偏置项将加快收敛速度,因为网络在前面几次迭代中基本上只在学习偏置。
- 人类基线结果:监控损失值等其他度量指标(例如准确度),这些指标应该是人类能解释并检查的。尽可能评估你自己(人类)获得的准确率,并与构建的模型做对比。或者对测试数据进行两次标注,其中一次为预测值,另一次为标注值。
- 独立于输入的基线结果:训练一个独立于输入的基线模型,例如最简单的方法就是将所有输入都设置为 0。这样的模型应该比实际输入数据表现更差,你的模型是否准备好从任何输入中抽取任何信息?
- 在批数据上过拟合:在单个批数据上使得过拟合(两个或多个少样本)。为此,我们需要增加模型拟合能力,并验证我们能达到的最低损失值(即 0)。我还想在同一张图中显示标签和预测值,并确保损失值一旦达到最小,它们就能完美地对齐了。
- 验证训练损失的下降:在这一阶段,你可能希望在数据集上实现欠拟合,该阶段的模型应该是极简的。然后我们尝试增加一点模型的拟合能力,再看看训练损失是否稍微下降了一些。
- 在输入网络前可视化:在运行模型之前,我们需要可视化数据。也就是说,我们需要可视化输入到网络的具体数据,即可视化原始张量的数据和标签。这是唯一的「真实来源」,我有很多次都是因为这个过程而节省了大量时间,并揭示了数据预处理和数据增强过程中的问题。
- 可视化预测过程:我喜欢在训练过程中对一个固定的测试批数据进行模型预测的可视化。这展示了预测值如何变化的过程,能为我们提供关于训练过程的优秀直觉。很多时候,如果网络以某种方式小幅度波动,那么模型最可能在尝试拟合数据,这也展示了一些不稳定性。太低或太高的学习率也很容易注意到,因为抖动量比较大。
- 使用反向传播绘制依赖性:你的深度学习代码通常包括复杂的、矢量化的、Boardcast 操作。一个常见的 bug 是,人们会无意间使用 view 而不是 transpose/permute,从而混合了批量数据中的维度信息。然而,你的网络仍然可以正常训练,只不过它们学会忽略了其它样本中的数据。一种 debug 的方法是将某些样本 i 的损失设置为 1.0,然后运行反向传播一直到输入,并确保第 i 个样本的梯度不为零。更一般的,梯度为我们提供了网络中的依赖性关系,它们在 debug 中非常有用。
- 一般化特殊案例:这是一种更为通用的代码技巧,但是我经常看到人们在使用这些技巧时会新产生 Bug,尤其是在从头构建一般函数时。相反,我喜欢直接写非常具体的函数,它只包含我现在需要做的事情。我会先让这个函数能 work,然后再一般化好函数,并确保能取得相同的结果。通常这个过程会体现在向量化代码中,我会先用循环编写某个过程,然后再一次一个循环地将它们转化为向量化化代码。
3. 过拟合
- 选择模型:为了达到理想的训练损失,我们可能希望为数据选择一个合适的架构。当我们在挑选模型时,我的第一个建议即别好高骛远。我看到很多人都非常渴望一开始就堆叠一些新的模块,或创造性地用于各种异质架构,从而想一步到位做好。我建议可以找最相关的论文,并直接利用它们的简单架构,从而获得良好性能。后面再基于这个架构做修改和改进,并将我们的想法加进去就行了。
- Adam 是一般选择:在配置基线模型地早期阶段,我喜欢使用 Adam 算法(学习率为 3e-4)。在我的经验中,Adam 对超参数的容忍度更高,不太好的学习率也能获得一般的效果。对于卷积网络来说,一般经过仔细调整的 SGD 几乎总会略优于 Adam,但最佳学习率的可能区域要窄得多。
- 一次复杂化一个:如果你有多个特性插入分类器,我建议你一个个插入,从而确保能获得期待的性能提升。不要在最开始时就一次性全加上,这样你会弄不清楚性能提升到底是哪个特性带来的。还有其它增加复杂性的方法,例如你可以先尝试插入较小的图像,然后再慢慢地加大。
- 别相信默认的学习率衰减:如果你修改来自其它领域的代码,你应该小心使用学习率衰减方法。对于不同问题,你不仅希望使用不同的衰减策略,同时因为 Epoch 的数量不同,衰减过程也会不一样。例如数据集的大小,会影响 Epoch 的数量,而很多学习率衰减策略是直接与 Epoch 相关的。在我自己的工作中,我经常整个地关闭学习率衰减,即使用常数学习率。
4. 正则化
- 更多数据:首先,在当前任何实际环境中正则化模型的最好方式是增加更多真实的训练数据。在你能收集更多数据时,花费大量工程时间试图从小数据集上取得更好结果是很常见的一个错误。我认为增加更多数据是单调提升一个较好配置神经网络性能的唯一可靠方式。
- 数据增强:比真实数据较次的方法是半假数据,试验下更激进的数据增强。
- 创造性增强:如果半假数据也没有,假数据也还可以。人们在寻求扩展数据集的创造性方法。例如,域随机化、使用模拟数据、把数据插入场景这样机智的混合方法,甚至可以用 GAN。
- 预训练:即使你有足够的数据,你也可以使用预训练网络,基本没什么损失。
- 坚持监督式学习:不要对无监督学习过于激动。据我所知,没有什么无监督学习方法在当前计算机视觉任务上有很强的结果(尽管 NLP 领域现在有了 BERT 和其他类似模型,但这更多归功于文本更成熟的本质以及对噪声比更好的信号)。
- 更小的输入维度:移除可能包含假信号的特征。如果你的数据集很小,任何加入的假输入只会增加过拟合的可能。类似地,如果低级细节作用不大,试试输入更小的图像。
- 更小的模型:在许多情况下,你可以在网络上使用域知识约束来降低模型大小。例如,在 ImageNet 主干网络顶部使用全连接层一度很流行,但它们后来被简单的平均池化取代,消除了这一过程中大量的参数。
- 减小批大小:由于 BN 基于批量大小来做归一化,较小的批量大小具有更强的正则化效果。这主要因为一个批量的统计均值与标准差是实际均值和标准差的近似,所以缩放量和偏移量在小批量内波动地更大。
- drop:增加 dropout。在卷积网络上使用 dropout2d(空间 dropout)。保守谨慎的使用 dropout,因为它对 batch 归一化好像不太友好。
- 权重衰减:增加权重衰减惩罚。
- 早停(early stopping):基于你得到的验证损失停止训练,从而在即将过拟合之前获取模型。
- 尝试更大的模型:我过去多次发现更大模型最终都会很大程度的过拟合,但它们「早停」后的性能要比小模型好得多。
最后,为了更加确保网络是个合理的分类器,我喜欢可视化网络第一层的权重,确保自己获得了有意义的边缘。如果第一层的滤波器看起来像噪声,那需要去掉些东西。类似地,网络内的激活函数有时候也会揭示出一些问题。
5. 精调
- 随机网格搜索:在同时精调多个超参数时,使用网格搜索听起来更诱惑,能够确保覆盖到所有环境。但记住,使用随机搜索反而是最佳方式。直观上,因为神经网络对一些参数更为敏感。在极限情况下,如果参数 a 很重要,改变 b 却没有影响,然后相比于多次在固定点采样,你宁可彻底采样 a。
- 超参数优化:如今社区内有大量好的贝叶斯超参数优化工具箱,我的一些朋友用过后觉得很成功。但我的个人经验是,探索好的、宽的模型空间和超参数的最佳方法是找个实习生。开玩笑而已,哈哈哈。
6. 最后的压榨
一旦你找到最好的架构类型和超参数,依然可以使用更多的技巧让系统变得更好:
- 集成:模型集成是能将准确率稳定提升 2% 的一种好方式。如果你承担不起测试阶段的计算成本,试着使用《Distilling the Knowledge in a Neural Network》中的方法把你的模型蒸馏到一个网络。
- 一直训练:我经常看到一些人在验证损失趋平时会中断模型训练,以我的经验来看,网络会长时间保持非直观的训练。寒假时有一次我忘了关掉模型训练,一月回来后发现它取得了 SOTA 结果。
本文转载自[深度学习工坊]。