共计 1718 个字符,预计需要花费 5 分钟才能阅读完成。
为什么梯度会消失?
2006 年之前,当研究者尝试训练超过 3 层的神经网络时,常常发现模型完全无法学习。这就像教小孩认字时,如果每教一个笔画就忘记前一个笔画,最后永远学不会完整的字。背后的数学原因是:传统 Sigmoid 激活函数的导数最大只有 0.25,当误差反向传播时,每经过一层就要乘以这个小于 1 的数,经过多层后梯度就会指数级衰减。

激活函数进化史
- Sigmoid/Tanh 的局限性:
- 像老式收音机的旋钮,输入值太大或太小时就卡在 ” 饱和区 ”(导数接近 0)
-
举例:当输入为 5 时,Sigmoid 输出 0.993,导数仅剩 0.007
-
ReLU 的革命性:
- 简单到令人惊讶:max(0,x),正数区域导数为 1 彻底解决梯度衰减
- 副作用是可能出现 ” 神经元死亡 ”(负值永远不激活)
- 改进版 LeakyReLU 给负数区留了 0.01 的 ” 小缝 ”
两大救星技术
-
批归一化(BatchNorm)
把每层输入数据强行拉到均值为 0、方差 1 的分布,就像给每层都配了个自动调音器:# PyTorch 示例 self.bn = nn.BatchNorm1d(hidden_size) x = F.relu(self.bn(self.fc(x))) # 通常放在激活函数前 -
残差连接(ResNet)
让网络可以 ” 抄近路 ”,2015 年微软研究院提出的跳跃连接:# 残差块实现 class ResidualBlock(nn.Module): def __init__(self, in_channels): super().__init__() self.conv1 = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1) def forward(self, x): residual = x x = F.relu(self.conv1(x)) x = self.conv2(x) return F.relu(x + residual) # 关键加法操作
完整训练示例
import torch
import torch.nn as nn
import torch.optim as optim
# 定义带防护措施的深度网络
class SafeDeepNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.bn1 = nn.BatchNorm1d(256)
self.fc2 = nn.Linear(256, 256)
self.bn2 = nn.BatchNorm1d(256)
self.fc3 = nn.Linear(256, 10)
def forward(self, x):
x = F.leaky_relu(self.bn1(self.fc1(x)), 0.01)
x = F.leaky_relu(self.bn2(self.fc2(x)), 0.01)
return self.fc3(x)
# 训练技巧三件套
model = SafeDeepNet()
optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam 比 SGD 更稳
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1) # 动态调整学习率
实战效果对比
在 MNIST 数据集上的测试结果:
– 传统 Sigmoid 网络(5 层):测试准确率 82%
– ReLU+ 批归一化(同深度):测试准确率 97%
– 加入残差连接后:训练速度提升 3 倍
避坑指南
- 学习率不要超过 0.01(建议从 0.001 开始试)
- 批量大小 (Batch Size) 最好在 32-256 之间
- 遇到验证集准确率波动时,适当增加 BatchNorm 层
- 文本任务中,LayerNorm 通常比 BatchNorm 效果更好
这些方法已经成为现代深度学习的标准配置。有趣的是,在解决梯度消失的过程中,研究者意外发现了更深的网络往往表现更好——这直接导致了后来 1000 层超深度网络的出现。技术发展有时就是这样,解决一个问题时,往往打开了新的可能性大门。
正文完
发表至: 未分类
近一天内
