神经网络训练的第一课,很多时候决定你后面所有模型能不能真正跑起来。很多人花大量时间搭模型结构,结果一到训练就翻车:loss不降、梯度爆炸、过拟合、训练集准确率都上不去。我自己刚开始训练模型时也踩过不少坑,后来回头看,核心问题大多集中在数据怎么预处理、参数怎么初始化、损失函数怎么选、正则化怎么加、学习率怎么调这几个环节。这次就以“训练神经网络I”为主线,把这些基础但关键的环节拆开揉碎讲一遍,既有理论层面的“为什么”,也有可直接上手的实操细节,适合刚入门深度学习的同学,也适合已经跑过几个模型但总被训练问题卡住的人。
1. 内容整体设计与思路拆解
1.1 这门课到底在解决什么问题
训练神经网络,本质上是在解决一个“参数怎么更新”的问题。网络层数一多、参数一多,梯度的计算和传播就会变得非常复杂。Lecture 6 作为训练神经网络系列的第一部分,并没有一开始就讲复杂的分布式训练或者超大规模模型并行,而是把注意力集中在单机单卡环境下最基础、最常用的一套训练流程:从数据预处理开始,到网络初始化,再确定损失函数和正则化,最后用梯度下降类算法去迭代更新参数。
这个设计是有讲究的。很多人习惯先把模型结构堆起来,再随便选个损失函数和优化器就跑,结果发现问题一堆:loss震荡、训练不收敛、验证集过拟合。这些问题其实在训练流程的最前端就已经埋下隐患了。数据没归一化、初始化不合适、正则化缺失,都会直接表现为训练过程失控。所以这门课的核心思路,就是教你从“源头”去控制训练的稳定性和泛化能力,而不是等模型跑坏了再去打补丁。
1.2 为什么训练技巧比网络结构更能决定成败
我见过不少同学在刷论文时,看到一个新的网络模块就迫不及待往自己的模型里塞,结果效果反而变差。问题不在模块本身,而在于训练策略没有跟上。同样的ResNet结构,不同的数据预处理方式、不同的初始化方法、不同的学习率调度,最终性能可以差好几个点。这背后其实是一个“训练动态”的问题:神经网络的损失曲面是非凸的,参数更新路径非常敏感,任何一处的微小改变都可能让模型走向完全不同的局部最优。
这也是为什么Lecture 6 把训练细节放在网络结构之前来讲。先掌握了损失怎么算、梯度怎么走、正则化怎么限制、学习率怎么调,再去折腾网络结构,你会发现自己写的模型开始“听话”了,训练曲线也开始符合预期。换句话说,训练技巧是基本功,基本功扎实了,结构创新才可能发挥出应有的效果。
1.3 适合谁来学,能解决什么痛点
这部分的定位,我觉得是“深度学习入门的第二道坎”。第一道坎是理解前向传播和反向传播,第二道坎就是训练。如果你已经能用框架搭出一个简单CNN或者MLP,但训练时经常遇到loss不降、准确率忽高忽低的情况,那么这部分内容就是直接对症的。它不会教你造火箭,但能帮你把最常见的训练问题归纳清楚,并给出可复现的解决方案。
如果你已经在训练模型方面有一定经验,这部分也能帮你重新梳理一遍知识框架。我到现在每次训练新模型时,还会下意识地按照这套流程检查一遍数据和初始化,很大程度上就是得益于这类系统性讲解。
2. 数据预处理与网络初始化:训练的第一步
2.1 数据预处理的“零均值化”与“归一化”
训练开始前,第一件事不是构建模型,而是处理数据。很多人忽视了这一步,直接把原始像素值或者原始特征丢给模型。这种做法会让梯度更新变得非常别扭。
以图像数据为例,通常的做法是先做“零均值化”,也就是把每个通道的像素值减去对应通道的均值,让数据的中心落在0附近。然后再做“归一化”,把标准差缩放到1附近。做了这一步之后,数据分布整体变成以0为中心、方差为1的标准分布。这样做的原因,本质上是让损失函数对每个参数的梯度处于一个相对均衡的量级,避免某些维度的特征因为数值范围过大而主导梯度,其他维度则被“淹没”。
我实际测试过,同样一个分类模型,对输入做零均值化和不做,收敛速度差距非常明显。做了预处理之后,损失曲线下降得更平滑,震荡也更小。如果不做,往往需要额外小心地调整学习率,否则很容易出现loss爆炸。
还有一个细节是:测试集的数据预处理必须和训练集完全一致,也就是说,均值、标准差这些统计量只能在训练集上计算,然后应用到训练集和测试集上,而不是分别在训练集和测试集上各算各的。这一点在实际工程中经常被忽略,一旦平均值算法不一致,做模型评估时结果会严重失真。
2.2 权重初始化的底层逻辑
当数据被预处理好了,接下来就要初始化网络参数。随机初始化看起来是个小操作,但选择的分布范围直接决定了模型的训练命运。
如果权重全部初始化为0,后果非常清楚:正向传播时所有神经元输出相同,反向传播时梯度也相同,所有神经元完全对称,模型退化成一个线性层。所以必须用随机数来打破对称性。但随机初始化的尺度也有讲究。权重太小,信号在层层传播中逐渐衰减,梯度可能趋近于0;权重太大,信号会不断放大,梯度可能爆炸。
这里就必须提Xavier初始化。它根据输入维度和输出维度来设置权重的方差,目标是让每一层的输出方差在传播过程中保持稳定。推导过程不复杂,核心是假设激活函数为线性时,各层输出方差不会逐层放大或缩小。对于使用Sigmoid、Tanh这类饱和激活函数的网络,Xavier初始化效果很好。不过后面深度学习发展出了ReLU等非饱和激活函数,Xavier初始化会导致输出方差在深层网络中仍然偏小,于是就有了一种更适配ReLU的初始化方式,在论文中通常被称为Kaiming初始化。它的核心思想是结合ReLU的特性,把方差乘以2,补偿被ReLU置为0的一半神经元。
这里我给你一个实际选择建议:如果网络使用ReLU及其变体,优先使用Kaiming初始化;如果使用Tanh或Sigmoid,优先使用Xavier初始化。主流深度学习框架里,这两类初始化都内置了,直接调用即可,关键是理解背后的匹配逻辑。
2.3 Batch Normalization能缓解初始化的压力吗
Batch Normalization(简称BN)在训练中出现的频率很高,它本质上是在每一层激活函数之前,对一批数据的输出做标准化,让每个维度均值为0、方差为1,然后再通过两个可学习的缩放和平移参数恢复必要的表达能力。
BN的一个显著好处是:它允许我们在一定程度上使用更大胆的初始化,甚至对初始化尺度的敏感度降低。因为BN强制把每层的输入分布拉回标准分布,即使前面的权重初始化不太理想,经过BN之后也能得到一个相对稳定的输入分布。这也是为什么现代很多网络即便用了较为随意的初始化,依然能训练起来的原因之一。
但BN不是万能药,它对batch size有较强的依赖。当batch size很小时,BN计算出的均值方差噪声很大,反而会干扰训练。所以在小batch场景下,有些模型会退而使用Layer Normalization或Group Normalization。这里要提醒一下:不要盲目在任何网络里都加BN,要结合你的数据规模和batch size来取舍。
3. 损失函数与正则化:让训练有明确目标
3.1 分类问题中的损失函数选择
训练神经网络的过程中,损失函数是模型优化的“指挥棒”。对于分类问题,最常用的损失函数是交叉熵损失(Cross-Entropy Loss)。很多人一开始搞不清它和均方误差(MSE)的区别,容易选错。
交叉熵之所以更适合分类问题,关键在于它和Softmax的配合。Softmax把网络的原始输出(logits)转换成概率分布,之后交叉熵直接衡量预测概率分布和真实标签分布之间的差异。从梯度的角度看,交叉熵配合Softmax的梯度形式非常简洁,梯度大小与预测概率和真实标签之间的差成正比。当预测错得很离谱时梯度大,预测接近正确时梯度小,更新方向非常合理。
而如果分类问题使用MSE,则会遇到梯度消失的隐患。因为Softmax输出通常是0到1之间的值,经过MSE求导后,在输出接近0或1时导数会非常小,导致梯度更新缓慢,训练速度明显变慢。这个差异我实测过:同样的网络结构,交叉熵损失可能几十步就能看到损失明显下降,MSE则可能需要几百步甚至上千步。
当然,如果你的任务是回归任务,MSE或者MAE依然是主流选择。这里要强调一个原则:损失函数必须和任务类型强匹配,分类问题不要用MSE,回归问题不要硬套交叉熵。
3.2 L1与L2正则化的差异与感悟
有了损失函数之后,下一步就是防止过拟合。正则化是训练过程中不可缺少的一环。
L2正则化是最直观的一种方式。它通过在原始损失上增加所有权重平方和的一项,驱使权重向零值的方向整体收缩。但L2并不会真正把权重变成严格的0,而是让较大的权重受到更大的惩罚,从而让模型尽量使用全体特征而非依赖少数突出特征。这种约束会降低模型在训练集上的拟合程度,但往往能提升在测试集上的泛化能力。
L1正则化则是增加权重绝对值之和,更倾向于让部分权重变成严格的0,产生稀疏解。如果特征维度很高,且你怀疑大部分特征是无用的,L1能帮你做特征选择。但是L1的导数在0点不光滑,优化过程会稍微麻烦一点。
实际操作中,我更建议在大多数深度学习任务里优先考虑L2正则化,因为它的优化稳定性更好,超参也比较好调。如果你的模型已经明显过拟合,我个人的经验是先尝试调整L2正则化系数,比如从1e-4量级开始尝试,而不是着急改变网络结构。很多时候,一个合适的L2系数就能让验证集准确率提升不少。
3.3 Dropout的正确理解与使用
Dropout是另一种古典但有效的正则化手段。它的核心思想是在训练时随机“丢弃”一部分神经元,即让这部分神经元的输出置为0,从而迫使网络不依赖于某些特定的神经元组合。这样做可以理解为在训练过程中同时训练了很多个共享权重的子网络,最后在测试时用全部神经元并做权重缩放,相当于对这些子网络的集成。
这里有一个很常见的误区:很多人使用Dropout时,在测试阶段忘记关闭。实际上测试时必须关闭Dropout,并且如果实现正确,测试时的权重应该乘上保留概率,或者训练时对输出除以保留概率(这就是通常说的Inverted Dropout)。现代框架里,Dropout层的默认行为已经做了正确的缩放和测试时关闭,不必手动处理,但你理解它背后的机制非常重要。
Dropout也有适用场景的限制。在卷积网络中,直接对卷积层特征图使用Dropout的效果往往一般,因为相邻像素高度相关,随机丢弃像素并不能有效打破神经元的协同适应。这时更常用的是Dropout的变体,比如对特征通道进行随机丢弃的Dropout2d或者Spatial Dropout。我自己在项目里用过,对某些图像分类任务确实更有效,但在Transformer类的结构中使用Dropout时,通常作用在attention层的输出上,位置和比例都需要专门设计。
4. 优化算法与超参数调整:让训练稳定走高
4.1 从SGD到Momentum的改进逻辑
训练神经网络的核心是参数更新,而最基础的更新方式是随机梯度下降(SGD)。SGD每次根据当前batch的梯度方向更新权重。但实际使用中,SGD有一个明显的短板:当损失曲面在某个方向很陡,在另一个方向很平缓时,SGD的更新轨迹会呈锯齿状,收敛速度很慢。
为了缓解这个问题,Momentum方法被引入。Momentum的思想可以类比成一个小球从山坡上滚下来:小球不会只沿当前梯度的瞬时方向运动,而是会累计之前的“动量”,当遇到局部陡峭区域时,动量能帮助它平滑地穿过;当进入平坦区域时,动量又能帮助它加速前进。实际上,Momentum在梯度计算中引入了一个“速度项”,每次更新的方向不仅依赖于当前梯度,还依赖于之前的速度,通过一个动量系数来控制历史梯度的影响。
我实际体验是,加上Momentum之后,训练收敛速度会有明显提升,尤其是对于深层网络,loss曲线的震荡也会小很多。在深度学习框架中,SGD+Momentum的组合通常通过设置优化器的momentum参数来实现,比如PyTorch中optim.SGD(model.parameters(), lr=0.01, momentum=0.9)。这里的0.9是一个比较常用的默认值,大多数情况下不需要做大幅度调整。
这里要注意一点:Momentum并不等同于Adam,Adam是在Momentum的基础上,进一步引入了梯度平方的指数加权平均,从而对不同参数维度的学习率进行自适应调整。理解这个区别对后面选择优化器很有帮助。
4.2 Adam优化器的适用场景与局限
Adam是目前训练深度模型时使用最广泛的优化器之一。它的全称是Adaptive Moment Estimation,也就是自适应矩估计。Adam同时维护了一阶动量(梯度的均值)和二阶动量(梯度平方的均值),然后利用这两个量分别调整更新方向和每个参数的学习率。从实际效果来看,Adam对学习率的敏感性明显小于SGD,默认学习率甚至可以直接使用0.001,很多模型都能训起来。
但这并不代表Adam在所有场景下都是最优选择。我注意到在很多计算机视觉任务中,尤其是一些需要精细调优的视觉模型,很多团队最后还是偏好使用SGD+Momentum。原因在于Adam的自适应学习率可能会导致模型在某类特征上更新过强,而在另一类特征上更新过弱,最终影响了模型的泛化能力。对于目标检测、语义分割这类复杂任务,SGD+Momentum调好参数后往往能够达到更高的精度上限,而Adam的优势是前期的收敛速度非常快。
一个比较务实的策略是:初期用Adam快速跑通流程、验证模型可行性,等模型结构确定后,再切换为SGD+Momentum进行精细调参。这种方式在很多实际项目中被证明是稳妥有效的。当然,如果任务对收敛速度要求非常高,而硬件资源又有限,那Adam也是一个合理的选择,并且可以通过调整权重衰减(Weight Decay)来平衡正则化效果。
4.3 学习率的重要性与调整策略
在所有超参数中,学习率大概是最影响训练结果的一个。学习率过大,模型不断在损失函数的低谷附近震荡,无法收敛;学习率过小,模型训练速度极慢,甚至陷入某个局部区域停滞。
识别学习率是否合适,可以从损失曲线直观判断。如果训练损失一开始就剧烈震荡、甚至出现NaN,基本可以判定是学习率过大了。如果损失下降非常缓慢,经过几百步后几乎没有变化,但又不是因为局部极小,那可能是学习率太小了。
除了固定学习率,更常用的是学习率调整策略,比如阶梯衰减、指数衰减,以及最近比较流行的余弦退火。阶梯衰减是指每隔一定训练轮数,学习率乘以一个小于1的因子;指数衰减则是让学习率按照指数函数持续减小;余弦退火则是让学习率按照余弦曲线逐渐降低,并且有时还配合周期性的上升,帮助模型跨越局部最优。
我自己的习惯是先使用一个相对合适的学习率训练几十个batch,观察loss下降速度,再决定是否调整学习率或调度策略。对于大部分图像分类任务,初始学习率设置0.1(配合batch size 256)或者0.01,通常是一个不错的起点。如果模型是大Transformer,初始学习率一般会进一步降低。总之,学习率没有绝对的最佳值,需要结合模型规模、数据量、batch size和优化器来综合调试。
5. 常见训练问题与排查技巧实录
5.1 Loss不下降或下降极慢
面对这个问题,排查方向通常集中在数据、预训练模型、学习率、网络结构这几个层面。首先检查数据标签和预处理是否正常,比如图像归一化后数值范围是否合理,标签有没有错乱。然后检查损失函数和输出层的匹配性,例如多分类任务是否用了Softmax+交叉熵。再检查学习率是否过小,可以尝试把学习率调大一个数量级看看loss是否有反应。最后,如果采用了深度网络,检查是否有梯度消失现象,比如观察梯度范数是否长时间接近于0。很多时候,换用更好的初始化或增加BN就能解决。
这里分享一个我踩过的坑:有一次我训练一个回归模型,loss始终维持在同一个值不怎么变化,检查来检查去最后发现是输入数据包含了大量NaN,导致梯度计算无效。所以遇到loss异常,先检查数据是否有异常值。
5.2 梯度爆炸与NaN的出现
梯度爆炸是训练过程中比较让人抓狂的问题,具体表现为loss突然变成NaN,或者梯度范数异常大。根本原因通常是网络过深、权重初始化过大、学习率设置过大,或者数据中有异常样本。
解决梯度爆炸有一个非常有效的手段——梯度裁剪(Gradient Clipping)。它的思路是在反向传播计算出梯度后,对整个梯度向量的范数设定一个上限,如果超过上限就按比例缩放,保证更新步长不会过大。在PyTorch中,torch.nn.utils.clip_grad_norm_就可以实现这个功能。我通常在训练RNN或Transformer时都会加上梯度裁剪,经验上限值设置在1.0左右比较安全。
另外,降低学习率、使用更稳定的初始化、在数据预处理阶段做更严格的异常值过滤也能规避这个问题。如果你在训练过程中发现loss突然变大然后又恢复,也可以考虑降低学习率并加入梯度裁剪。
5.3 过拟合的早期判断与应对
过拟合的本质是模型在训练集上表现很好,但在测试集或验证集上表现差。通常可以通过对比训练集loss和验证集loss来判断,如果训练集loss持续下降,验证集loss在某个点开始回升,那基本就是过拟合了。
应对过拟合的手段很多,最简单的是增加数据量或数据增强。图像类的任务中,随机裁剪、水平翻转、色彩扰动都是非常有效的增强方式。如果数据增强效果不够,可以调大L2正则化系数或Dropout比例。还有一种办法是早停(Early Stopping),也就是监控验证集loss,一旦它开始连续几个epoch不降或者上升,就提前结束训练并回滚到验证集最好的那个权重。
在实际项目中,我经常是先跑一个较小规模的模型来验证数据增强和正则化的基本效果,等验证集指标稳定后再逐步放大模型规模。这样做能大大减少浪费在无效训练上的时间。
5.4 训练集准确率也上不去的问题
如果模型在训练集上的准确率都不高,那问题往往不是过拟合,而是欠拟合。欠拟合可能是模型表达能力不够,即网络太浅或太窄,不足以捕获数据中的模式;也可能是训练不充分,比如epoch数太少、学习率太小;还可能是特征工程没做好,输入数据无法反映出有效的模式。
这时可以检查一下模型在训练集上的loss曲线,是否已经收敛到平原区域。如果是,考虑增加模型的宽度或深度。如果曲线还在下降,说明训练尚未结束,可以加大训练轮数或提高学习率。还有一个容易忽略的点是标签是否做了正确的编码,比如类别顺序是否与输出层一致。这类问题排查起来不算难,但需要细心。
6. 实操总结与技巧分享
6.1 一套可复用的训练配置清单
基于课程内容和我的实际经验,我整理了一份可用于大多数中小型视觉分类任务的训练配置清单:
- 数据预处理:零均值化 + 标准差归一化;注意测试集使用训练集统计量。
- 权重初始化:ReLU类网络使用Kaiming初始化;Tanh/Sigmoid使用Xavier初始化。
- 损失函数:多分类使用Cross-Entropy Loss;回归使用MSE或MAE。
- 优化器:优先SGD+Momentum(momentum=0.9),或Adam(初始lr=0.001)。
- 学习率调整:阶梯衰减或余弦退火,具体根据验证集表现调整。
- 正则化:L2权重衰减系数从1e-4开始调整;Dropout根据过拟合情况从0.5附近调整。
- Batch Size:大多数情况下,图像分类取32到128之间,视觉Transformer可适当增大。
- 梯度裁剪:当模型较深或使用RNN/Transformer时,clip_grad_norm设为1.0左右。
这套配置并不是绝对的,但它能帮你快速跑通一个训练管线,后续所有调参都是在这些基础上逐步改变。
6.2 一个实用的训练流程模板
我习惯把训练流程按阶段拆分:
第一阶段是快速验证阶段。用相对较小的模型、较大的学习率、较短的时间,验证数据和模型管线是否正确。这个阶段的目标不是拿最高准确率,而是确保loss能下降、验证指标能计算、没有NaN或异常曲线。
第二阶段是稳定收敛阶段。在管线跑通后,将学习率降低到合适范围,调大训练轮数,增加数据增强和正则化,观察验证集指标的变化。
第三阶段是精细调优阶段。在验证集指标接近饱和时,对小范围超参做随机搜索或网格搜索,比如尝试不同的学习率、不同的权重衰减系数、不同的Dropout比例。
每一阶段都可以固定其他变量,只改变一个目标变量,从而清楚看到每个改动带来的实际影响。这种做法看似笨拙,但非常可靠。
6.3 我对训练调试的几点碎碎念
调试训练过程最忌讳的事情之一,就是同时修改多个超参数。我见过有同学一次改了学习率、优化器、batch size和网络结构,结果模型效果变差了,根本不清楚是哪个改动导致的。这个方法非常容易被问题淹没,反而浪费时间。
第二个经验是:多关注验证集loss的变化趋势,而不是单纯看训练集loss的绝对值。训练集loss低不一定代表模型好,尤其对于有过拟合倾向的模型。验证集loss能够更真实地反映泛化能力。
第三个经验是:对于训练中遇到的一切异常,先怀疑数据,再怀疑代码,最后再怀疑模型。数据集中的标签错乱、缺失值、异常数值都比模型层的问题常见得多。
第四个经验是:不要忽视随机种子。同样的代码,不同随机种子可能得到的结果会有波动。建议在实验初期固定随机种子,以保证结果可对比。等到最终确定参数以后,再用多个种子跑多次,取平均结果来评估模型真实水平。
最后还有一点,就是我踩过很多次坑之后体会到的:训练神经网络更像是一门实验科学,理论给你指明方向,但最终效果还是需要通过实验验证。不要迷信某一种初始化或者某一种优化器的“玄学”,带着“假设-实验-结论”的思维方式去做每一轮训练,你的模型效果和调参速度都会逐步提升。
如果真的遇到连续很多次实验效果都很差,不妨退回最小配置,比如一个小型网络、少部分数据、简单任务,重新验证训练管线是否正常。多数时候,问题会在这个过程中暴露出来。训练神经网络就是这样,把基本功打扎实,后面的模型创新才能真正落地。