深度神经网络训练全攻略:从梯度消失到Adam优化器,一篇搞懂所有技巧

张开发
2026/4/17 21:49:48 15 分钟阅读

分享文章

深度神经网络训练全攻略:从梯度消失到Adam优化器,一篇搞懂所有技巧
训练深度神经网络就像调教一匹烈马——既要选对方向优化器又要控制好缰绳学习率还得给它戴好马鞍正则化。本文将带你系统掌握这些核心技巧从此告别“训练不收敛”的噩梦。一、为什么深度网络这么难训练1.1 深度学习是什么简单来说深度学习就是层数很多的神经网络。层数越多网络就越“深”它能学习到更抽象、更高级的特征。比如识别一张猫的图片浅层学到边缘和颜色中间层学到眼睛和耳朵深层就能认出“这是猫”。自从2012年深度学习在ImageNet竞赛中大放异彩后它就成为了人工智能领域的主流方向。1.2 梯度消失和梯度爆炸深度网络的“死穴”想象一下你在一座很长的队伍里传话。梯度消失就像声音越传越小到最后一个人完全听不见——靠近输入层的网络层学不到东西参数几乎不更新。梯度爆炸则相反声音越传越大最后变成噪音——网络变得极不稳定参数乱跳。为什么会出现这种情况因为反向传播时每一层都会对梯度乘以一个系数比如激活函数的导数或权重。如果这个系数总是小于1层层相乘后前面的梯度就会趋近于0消失如果大于1就会爆炸。这也就是为什么早期的深度网络很难训练。解决这个问题需要从三个方面入手改进参数更新方法、合理的初始化、有效的正则化。二、优化器进化史从SGD到Adam优化器就是用来更新网络参数权重和偏置的算法。最基础的是SGD随机梯度下降但它有不少毛病后来者一个个地给它“打补丁”。2.1 SGD最朴素的起点SGD的核心思想沿着梯度下降的反方向走一小步。class SGD: 最朴素的梯度下降法。 每一步都按照“当前梯度 × 学习率”来更新参数。 缺点容易在峡谷中震荡、容易被困在局部最优点或鞍点。 def __init__(self, lr0.01): self.lr lr # 学习率控制步长大小 def update(self, params, grads): # params: 参数字典比如 {W1: 权重矩阵, b1: 偏置} # grads: 梯度字典结构同params for key in params.keys(): params[key] - self.lr * grads[key]SGD的问题很直观容易陷入局部最优或鞍点梯度为0的地方没法继续走收敛慢尤其在崎岖的损失面上对学习率极其敏感大了震荡小了太慢2.2 Momentum给SGD装上“惯性”Momentum动量法借鉴了物理中小球滚下坡的思想——小球会积累速度即使遇到小坑也能冲过去。它保留了过去梯度的加权和让更新方向不仅取决于当前梯度还取决于历史“势头”。class Momentum: 动量法保留历史梯度让更新有“惯性”。 参数momentum控制历史梯度的保留程度通常取0.9。 def __init__(self, lr0.01, momentum0.9): self.lr lr self.momentum momentum self.v None # 用来存放历史梯度的加权和 def update(self, params, grads): if self.v is None: self.v {} for key in params.keys(): self.v[key] np.zeros_like(params[key]) for key in params.keys(): # 更新规则 # v momentum * v - lr * grad # param param v self.v[key] self.momentum * self.v[key] - self.lr * grads[key] params[key] self.v[key]Momentum有效抑制了震荡还能冲过小坑避免陷入局部极小值训练速度明显提升。2.3 学习率衰减先大步快跑再小步慢走学习率是训练中最重要的超参数。初期用大学习率快速靠近最优解后期用小学习率精细调整——这就是学习率衰减的核心思想。# 等间隔衰减每20轮衰减为原来的0.7 def step_decay(epoch, lr0.1, drop0.7, epochs_drop20): return lr * (drop ** (epoch // epochs_drop)) # 指数衰减每轮都乘0.99 def exp_decay(epoch, lr0.1, decay0.99): return lr * (decay ** epoch)1等间隔衰减每隔固定的训练周期epoch学习率按一定的比例下降也称为“步长衰减”。例如使学习率每隔20 epoch衰减为之前的0.72指定间隔衰减在指定的epoch让学习率按照一定的系数衰减。例如使学习率在epoch达到[10,50,200]时衰减为之前的0.73指数衰减学习率按照指数函数进行衰减。例如使学习率以0.99为底数epoch为指数衰减2.4 AdaGrad给每个参数单独配一个学习率AdaGrad的想法很巧妙频繁更新的参数学习率应该小一点罕见更新的参数学习率应该大一点。它通过累加每个参数的梯度平方和来实现。class AdaGrad: 自适应学习率每个参数有自己的学习率。 频繁更新的参数梯度平方累加变大学习率自动变小。 缺点累加只会增加不会减少最终学习率趋近于0训练停止。 def __init__(self, lr0.01): self.lr lr self.h None # 存放梯度平方的累加和 def update(self, params, grads): if self.h is None: self.h {} for key in params.keys(): self.h[key] np.zeros_like(params[key]) for key in params.keys(): # h h grad^2 self.h[key] grads[key] * grads[key] # param param - lr * grad / (sqrt(h) eps) params[key] - self.lr * grads[key] / (np.sqrt(self.h[key]) 1e-7)AdaGrad在稀疏数据如自然语言处理上表现不错但它有个致命问题h只增不减学习率最终会小到零。2.5 RMSProp解决AdaGrad的“早衰”RMSProp给AdaGrad打了一个补丁不再简单累加而是用指数移动平均让过去的影响逐渐“遗忘”。class RMSProp: 均方根传播用指数加权平均代替累加避免学习率过早衰减。 def __init__(self, lr0.01, decay_rate0.99): self.lr lr self.decay_rate decay_rate # 衰减系数越大记住的历史越长 self.h None def update(self, params, grads): if self.h is None: self.h {} for key in params.keys(): self.h[key] np.zeros_like(params[key]) for key in params.keys(): # h decay_rate * h (1-decay_rate) * grad^2 self.h[key] self.decay_rate * self.h[key] (1 - self.decay_rate) * grads[key] * grads[key] params[key] - self.lr * grads[key] / (np.sqrt(self.h[key]) 1e-7)2.6 Adam集大成者当今最流行Adam自适应矩估计同时拥有Momentum和RMSProp的优点它既维护了梯度的一阶矩动量又维护了二阶矩自适应学习率还加了偏差校正来避免初始阶段估计不准确。class Adam: Adam Momentum RMSProp 偏差校正。 是目前最常用、最稳定的优化器。 def __init__(self, lr0.001, beta10.9, beta20.999): self.lr lr self.beta1 beta1 # 一阶矩的衰减率 self.beta2 beta2 # 二阶矩的衰减率 self.t 0 # 迭代次数 self.v None # 一阶矩动量 self.h None # 二阶矩自适应学习率 def update(self, params, grads): if self.v is None: self.v {} self.h {} for key in params.keys(): self.v[key] np.zeros_like(params[key]) self.h[key] np.zeros_like(params[key]) self.t 1 for key in params.keys(): # 更新一阶矩动量 self.v[key] self.beta1 * self.v[key] (1 - self.beta1) * grads[key] # 更新二阶矩自适应 self.h[key] self.beta2 * self.h[key] (1 - self.beta2) * grads[key] * grads[key] # 偏差校正解决初始时刻估计偏低 v_hat self.v[key] / (1 - self.beta1 ** self.t) h_hat self.h[key] / (1 - self.beta2 ** self.t) # 参数更新 params[key] - self.lr * v_hat / (np.sqrt(h_hat) 1e-8)总结优化器新手直接上Adam想省显存可以用Momentum处理稀疏数据可以用AdaGrad或RMSProp。三、初始化别让网络“输在起跑线上”参数初始值选不好梯度消失或爆炸就找上门。随机初始化是打破对称性的关键千万不要把所有权重设成一样的值。3.1 Xavier初始化适合Sigmoid/TanhXavier让每一层的输入和输出方差保持一致有效缓解梯度消失。正态分布均值0标准差 sqrt(2 / (n_in n_out))均匀分布区间 [-sqrt(6/(n_inn_out)), sqrt(6/(n_inn_out))]3.2 He初始化适合ReLUReLU会把一半的神经元置零所以需要更大的方差。正态分布均值0标准差 sqrt(2 / n_in)均匀分布区间 [-sqrt(6/n_in), sqrt(6/n_in)]实际建议用ReLU激活函数就用He初始化用Sigmoid/Tanh就用Xavier初始化。四、正则化对抗过拟合的五种武器过拟合就是模型把训练数据背下来了但见到新数据就懵了。正则化的目的就是防止模型“死记硬背”。4.1 Batch NormalizationBNBN放在全连接层或卷积层之后、激活函数之前。它对每个小批量的数据做标准化让数据分布保持稳定。BN的作用允许用更大的学习率训练更快不那么依赖初始化本身就有轻微的正则化效果def batch_norm(x, gamma, beta, eps1e-5): # x: 输入数据shape (batch_size, features) mu np.mean(x, axis0) # 均值 var np.var(x, axis0) # 方差 x_hat (x - mu) / np.sqrt(var eps) # 标准化 out gamma * x_hat beta # 缩放平移gamma, beta是可学习的 return out4.2 权值衰减Weight Decay大权重容易导致过拟合。权值衰减在损失函数里加上权重的平方和作为惩罚项让权重尽量小。# 原始损失 L加上正则项后变为 L L 0.5 * λ * ||W||^2 # 梯度更新时相当于在原梯度上加上 λ * W for key in params.keys(): grads[key] lambda_reg * params[key] # 加上衰减项 params[key] - lr * grads[key]4.3 Dropout随机失活训练时以概率p随机“关掉”一些神经元让网络不能依赖某几个特定的神经元从而学习到更鲁棒的特征。class Dropout: def __init__(self, p0.5): self.p p # 神经元保留的概率 def forward(self, x, trainingTrue): if not training: return x # 生成随机掩码保留概率为p然后除以p保持期望值不变 mask np.random.binomial(1, self.p, sizex.shape) / self.p return x * maskDropout可以理解为一种隐式的模型集成——每次迭代训练一个不同的子网络最后综合起来。总结一张表记住核心要点主题推荐做法关键参数优化器先用Adam有问题再换SGDMomentumlr0.001学习率用指数衰减或余弦衰减初始0.01~0.1初始化ReLU用HeSigmoid/Tanh用Xavier-正则化Dropout(0.5) 权值衰减(1e-4)p0.5, λ1e-4BN放在全连接/卷积后、激活前momentum0.9训练深度网络就像烹饪——优化器是火候学习率是调味初始化是食材正则化是摆盘。每一环都做到位才能端出一道色香味俱全的“大餐”。希望这篇文章能帮你理清深度神经网络训练的核心脉络。实践出真知赶紧动手跑几个模型试试吧

更多文章