资讯动态

手写神经网络训练实战:从超参数、Dropout到优化器的调参指南

发布时间:2026/10/14 8:17:45 来源:尧图企业网站定制
我自己手写神经网络的时候最难熬的往往不是把反向传播的代码调通而是把网络“训练好”。loss明明在降却总是差那么一点换个学习率可能直接起飞到NaN模型在训练集上猛如虎一到验证集就现原形。这篇是这个手写深度学习网络系列的第七篇主题锁定在最关键的一环如何寻找最优权重参数。我会把超参数设定、dropout、权值衰减、以及SGD、Momentum、AdaGrad、Adam这些优化器逐个拆开讲清楚它们背后在解决什么问题再给出可以直接“抄作业”的实现思路和避坑经验。如果你也在手写网络或者做小型实验这篇应该能帮你减少大半无效调参的时间。1. 为什么说“找最优权重参数”是整个训练过程的核心1.1 损失函数的地形决定了优化的难度训练神经网络本质上是在参数空间里找一个点使得损失函数的值尽可能小。这个“找点”的过程就是最优化问题。但神经网络的损失函数不是一个光滑的碗状曲面它更像一片连绵起伏的山区有平坦的谷地、有陡峭的悬崖、有无数个局部低洼处还有大片大片的鞍点。梯度下降能做的只是根据当前位置的斜率迈出下一步并不知道全局哪里最低。所以同一个模型用不同的初始化、不同的学习率、不同优化器最终走到的位置可能完全不同。这也是为什么很多人把“训练技巧”看得和“模型结构”一样重要。1.2 手写网络的意义能看见每一步发生了什么用主流深度学习框架训练模型时优化器、正则化、学习率调整都是一个参数就搞定了它们被封装成黑盒。换到手写神经网络的情境下每一行更新参数的代码都是自己写的你会被迫去理解权重更新时到底加上了什么、减去了什么、为什么不能直接把梯度乘个0.5就往回走。这个过程虽然慢但价值远超过“跑通一个模型”。因为当你亲手把Momentum的速度项、AdaGrad的累积梯度、Adam的一阶二阶动量写出来的时候那些优化器之间的区别就不再是论文里的公式而是你能实实在在感受到的行为差异。2. 超参数设定先把最基础的几个旋钮拧对2.1 学习率所有超参数里的“大哥大”学习率对训练结果的影响怎么强调都不过分。它直接决定了每次参数更新的步长如果学习率太大参数会在损失函数的陡峭区域来回震荡loss曲线表现为快速下降后突然跳变甚至直接变成NaN。如果学习率太小loss会下降得像蜗牛爬训练几百个epoch仍然没走到最优区域。我习惯的做法是先把学习率粗粗扫一遍0.1、0.01、0.001、0.0001每个跑50个epoch看loss曲线的下降趋势。注意这里不是看最终精度而是看曲线的“形状”0.1通常会让loss剧烈抖动0.01在很多小数据集上表现最稳0.001适合更深或更复杂的网络。提示学习率的选择和网络深度、激活函数强相关。手写的多层全连接网络如果激活函数用的是ReLU常见安全起步值是0.01如果是sigmoid/tanh这类容易饱和的激活函数建议从0.001起步。2.2 批次大小和隐藏层宽度批次大小batch size影响梯度的稳定性。批次越小每次估计的梯度噪声越大但有时这种噪声反而能帮模型跳出局部极小值批次越大梯度估计越接近真实梯度收敛更平滑但计算成本也更高。手写网络时我通常从32或者64起步不会一上来就塞进全部样本。全批量梯度下降在MNIST级别的数据集上虽然也能收敛但曲线会很“犟”一旦陷入鞍点附近难以自然逃逸。隐藏层宽度则要遵守“够用就好”的原则。过宽的隐藏层会让模型容量过剩在小数据集上几乎必然过拟合过窄的隐藏层则可能表示能力不足loss怎么调都下不去。一个稳妥的起点是在输入输出维度之间取几何中间值然后向两边翻倍或减半做对比。2.3 超参数搜索网格搜索和随机搜索的实际差异网格搜索看起来最“全面”但实际体验很差假设学习率、批次大小、隐藏层宽度各有4个候选值组合起来就是64组实验每组还要跑几十上百个epoch。在数据集稍大时这就是灾难。随机搜索反而更高效。它的思路是给每个超参数设定一个合理范围在这个范围内随机取值跑N组实验。原因是不同超参数对结果的影响并不是等权的学习率的影响远远大于批次大小随机搜索在同样的预算下能覆盖更多“有效维度”的变化。用这种思路我常常用30组随机配置就能找到比上百组网格搜索更好的结果。实际操作时我会把超参数搜索写成两层循环外层随机生成配置内层做一次完整的训练和验证记录验证集准确率。最后不只看最优的那组而是看表现最好的前几组有没有共性比如是不是都是小学习率、是不是都用到了dropout这些共性往往会给你比“最优值”更有价值的提示。3. 权值衰减Weight Decay给参数“踩刹车”3.1 L2正则化与权值衰减的同一件事权值衰减这个名字一听像是在“削弱权重”实际上它的做法是每次更新参数时都把权重往零的方向拉一点。数学上它等价于在损失函数后面加一个L2正则项L_new L_original (lambda / 2) * sum(w^2)然后求梯度时会多出一项 lambda * w更新规则就变成w w - lr * grad_w - lr * lambda * w后面那一项就是“衰减项”。它逼迫模型不要依赖任何单个过大的权重而是把信息分散到更多参数上。直观理解就是一个权重很大的特征组合往往是在死记训练样本的特点把参数范数压住之后模型的决策边界会变得更平滑泛化能力通常更好。3.2 衰减系数的选择与手写实现手写权值衰减时我最常踩的坑是忘记把衰减项写进梯度而是直接改loss。如果你是用数值法验证梯度会发现这样没问题但如果你在手动实现反向传播要特别注意weight decay的梯度要叠加到对应权重项的梯度上。衰减系数lambda的经验值通常在1e-4到1e-2之间。对于卷积网络或大参数量的模型常用1e-4或5e-4对于全连接小网络1e-3也算常见。我建议从1e-4开始观察验证集和训练集的差距。如果验证集比训练集明显差一截就增大lambda如果训练集本身已经欠拟合就减小或者去掉它。def update_weights_with_decay(params, grads, lr0.01, weight_decay1e-4): for param, grad in zip(params, grads): # 权值衰减项直接作用于参数 decay_term weight_decay * param param - lr * (grad decay_term) return params这里有一个容易混淆的点PyTorch的weight_decay参数实际上是加在更新公式里的而不是在loss里再加一个正则项。很多初学者把两者混为一谈导致梯度校验对不上。记住一个原则权值衰减就是在更新时额外减去一个与当前权值成比例的量。4. Dropout用“随机失活”对抗过拟合4.1 Dropout为什么有效过拟合的本质是模型把训练集里的噪声也学进去了。Dropout的做法简单粗暴训练时每次前向传播过程中按照一定概率让隐藏层神经元随机失活也就是把它的输出置为0。这样一来每个batch训练的网络结构都不一样相当于同时训练了很多个共享权重的“子网络”最终使用的又是这些子网络的集成结果。从经验上看Dropout能强迫网络不要把宝押在某几个特定神经元上。它类似于一种“多样化”手段让每个神经元都单独承担一部分有效特征而不是互相依赖、共同犯错。4.2 手写实现与测试时的缩放问题手写dropout的关键细节是“缩放”。在训练时如果我们以概率p保留一个神经元那么这一层输出的期望值会变小。为了保证训练和测试行为一致必须在训练时对输出做反向缩放也就是除以保留概率p或者在测试时乘以保留概率p。两种方式最终等价但实现位置不同。我习惯在训练阶段做“保留/scaling”def dropout_forward(x, keep_prob0.5, trainingTrue): if not training: return x mask (np.random.rand(*x.shape) keep_prob).astype(np.float32) return x * mask / keep_prob这里除以keep_prob是为了让训练时每个神经元的输出期望仍然等于x。如果不做这步模型的测试结果会出现系统性的偏差表现为训练集和验证集行为不一致。注意测试阶段一定不要做dropout也不要做mask。很多人在推理时忘了关闭dropout导致输出变得不稳定尤其在自定义实现里这是个非常隐蔽的bug。检查方法很简单固定输入跑两次推理结果应该完全一致。4.3 Dropout与权值衰减如何配合很多人会问有了dropout是不是就不需要权值衰减了我的经验是可以同时使用但要注意它们的作用维度不太一样。权值衰减直接约束参数范数让模型整体偏向更简单的解dropout则通过随机失活增加训练过程中的噪声防止共适应。两者作用于不同的环节配合使用时通常效果更好。在一些参数量很大的全连接网络里我常用的组合是dropout保留概率0.5配合weight_decay取1e-4或3e-4。如果网络比较小dropout可以放宽到0.7~0.8甚至不用因为小网络本身欠拟合风险更大dropout反而可能让loss下不去。对于卷积网络或后续要接批归一化的网络dropout的使用场景会少很多但全连接层后加上依然有效。5. 优化器横向对比SGD、Momentum、AdaGrad、Adam5.1 几类优化器背后的直觉这一部分是整个系列的重头戏。很多人用优化器只看名字知道Adam好使但不知道为什么好使。我逐个讲一遍它们到底在做什么。最朴素的SGD就是沿着梯度的反方向走一步w w - lr * grad。它的优点是简单、稳缺点是容易陷入峡谷地区反复震荡收敛速度慢。Momentum在SGD的基础上增加了一个“速度项”更新公式是v alpha * v lr * gradw w - v这里的alpha通常取0.9。可以想象一个小球从山坡滚下速度会累积遇到局部平坦区域时凭借“惯性能冲过去”。因此Momentum收敛更快震荡更小。AdaGrad的思路是“每个参数自适应学习率”那些梯度很大的参数累积平方梯度大学习率自动变小梯度小的参数学习率自动变大。更新公式是h h grad^2w w - lr * grad / sqrt(h eps)eps一般取1e-8防止除零。AdaGrad对稀疏特征很友好但它的累积量会一直增大导致学习率后期变得过小训练提前停滞。这一点在深层网络里格外明显。Adam可以看作Momentum和AdaGrad的融合它同时维护一阶动量梯度均值和二阶动量梯度平方均值并且做了偏差校正。m beta1 * m (1 - beta1) * gradv beta2 * v (1 - beta2) * grad^2m_hat m / (1 - beta1^t)v_hat v / (1 - beta2^t)w w - lr * m_hat / sqrt(v_hat eps)默认取beta10.9beta20.999。偏差校正让训练初期动量估计更准不至于因为一开始的梯度方向偏了就冲出太远。5.2 手写实现时的几个关键细节我在手写这些优化器时踩过几个很典型的坑。第一个是Momentum的更新方向。有的实现写成v alpha * v - lr * grad然后w w v这和刘军手写网络系列的表示习惯一样其实和前一个写法等价。但如果你混着用就容易错。第二个是AdaGrad更新时开根号的位置。我遇到过把sqrt写在整个公式外面结果数值小到无法训练因为当梯度累积量很大时整体被压得太厉害。正确写法是对二阶累积量开根号后再取倒数乘到梯度上。第三个是Adam里的偏差校正。最初手写时为了省事我没做这一步结果训练前期loss下降特别慢因为一阶动量和二阶动量都被初始值0拉偏了。后来才发现偏差校正是Adam收敛速度的重要保障。尤其在训练的前几百步有没有偏差校正曲线的差距肉眼可见。5.3 优化器横向实验与选型经验在手写网络时我建议把优化器作为可插拔模块设计每个优化器只需要实现一个update方法输入参数和梯度输出更新后的参数。这样就能在同一个网络结构下做横向对比。我实测下来在小规模全连接网络、手写数字集这类任务上SGD收敛最慢但收敛后的泛化能力往往不错。Momentum能把收敛速度提升30%以上而且没有明显副作用。AdaGrad在小数据上反而容易过早饱和适合特征维度极高、数据稀疏的场景。Adam几乎不需要调学习率就能达到不错的结果是最省心的起点。但如果追求极致的泛化能力学习率衰减加Momentum的组合有时候能超过Adam。这里要特别提醒一点优化器的选择要和学习率配合起来看。同一个学习率0.01SGD可能还在“爬坡”Adam可能已经震荡了。我一般会给Adam设置比SGD小一到两个数量级的学习率0.001起步。Momentum则可以直接沿用SGD的学习率。6. 常见问题与排查技巧实录6.1 loss 不降或者降得太慢先检查学习率。把学习率往上调10倍如果loss反而更差说明当前是学习率过大如果loss变化不明显则继续往上调。这个操作能帮你快速定位“梯度太小”还是“学习率太低”。如果学习率怎么调都无效再检查输入数据有没有归一化。手写网络时最容易忘记对输入做标准化特征尺度差距大的情况下梯度就会在不同维度上失衡导致训练非常缓慢。把输入缩放到均值为0、方差为1通常能解决一半的“loss不降”问题。6.2 训练loss降了验证loss却反弹这就是标准的过拟合信号。处理优先级上我一般先做三件事加dropout调大weight_decay减少隐藏层神经元数量。加dropout时要注意从0.5开始试如果网络很小或者过拟合不明显0.7也许更合适。权值衰减同理先1e-4不够再往上加。另一个常见迷惑点dropout加得过大比如0.2保留概率会导致训练loss比原来高很多。这不是实现错误而是因为模型容量被“抹掉”了一大部分。你需要观察的是验证集的表现而不是训练集曲线。6.3 loss出现NaNNaN问题九成是学习率过大导致梯度爆炸。把学习率除以10如果NaN停止出现就确认是这个原因。如果学习率已经很小仍然出现NaN检查梯度计算过程中有没有除以接近零的数或者有没有对log、sqrt之类的函数输入了非正数。还有一种隐蔽情况在自定义实现里某些权重初始化为零导致反向传播中梯度也被清零某些层的更新永远不会发生。观察参数更新前后是否变化能帮你快速定位这类bug。6.4 随机种子与可复现性手写网络的随机性来自三处权重初始化、数据打乱、dropout的mask。做实验对比时如果不同优化器之间固定了同一套初始化结果才有可比性。我每次实验都会设置随机种子并且在记录结果时把当前网络结构、学习率、优化器参数、batch size、seed全部记下来。看起来麻烦但之后排查问题时这套记录能帮你省下大半天时间。提示比较不同的正则化方法和优化器时种子最好固定成同一个值。否则你看到的性能差异可能来自随机波动而不是方法本身的优劣。确定种子之后再用多个不同种子跑几遍取平均结果才是一个可信的结论。6.5 多元调试策略可视化loss曲线调试深度学习代码最直观的工具就是loss曲线。但很多人只看最终准确率错过了曲线里的大量信息。loss曲线缓慢下降且起伏很大说明学习率太高曲线一开始下降很快然后彻底平坦说明可能陷入鞍点或学习率过小训练loss和验证loss的间距逐步拉大就是过拟合的开始信号。还有一个技巧把SGD、Momentum、Adam的loss曲线画在同一张图上如果某个优化器在前期下降特别猛后期却明显跑偏你就知道它的“脾气”和当前网络结构匹配不匹配了。手写网络系列说到底训练过程就是一场和曲线反复对话的过程。我自己在这些实验里最大的体会是没有“最好”的优化器也没有绝对正确的超参数组合一切都是和具体数据、具体网络结构纠缠在一起的。手写网络是个极好的放大镜它让你看清每一次权重更新背后发生了什么也逼着你去理解每个选择背后的代价。如果你正在尝试类似项目建议你从最简单的小网络和少量数据开始把SGD、Momentum、AdaGrad、Adam逐一实现和对比再引入weight decay和dropout。这样踩出来的经验会比任何现成框架的自动调参都来得扎实。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑