ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深层网络越叠越蠢?十种激活函数对比与PyTorch实验指南

深层网络越叠越蠢?十种激活函数对比与PyTorch实验指南 神经网络越叠越蠢这个说法听起来有些夸张但它描述的是一个真实存在的工程现象。模型层数从几层加深到几十层甚至上百层参数量明显增加训练误差却可能不降反升或者在网络加深后前面的层几乎学不到有效特征后面层则在随机初始化的状态下输出无意义结果。堆叠层数必须同时解决两个问题信息能否有效前向传播梯度能否顺利反向回传。激活函数在这两条链路里都处于关键位置它决定每一层非线性变换的方式也决定反向传播时梯度的大小和走向。下面围绕 10 种常见激活函数分析它们如何改善深层网络的梯度传递、非线性表达和训练稳定性并给出可运行的 PyTorch 对比实验和排查路径。1. 深层网络为什么越叠越蠢退化、梯度消失与激活饱和1.1 训练误差不降反升优化困难不等于过拟合很多时候网络加深后训练误差反而变高第一反应容易误判为“过拟合”。但两者的外在表现完全不同。过拟合通常是训练集效果很好、验证集效果变差而“越叠越蠢”的场景往往是训练集本身的误差就很高模型根本没有把训练数据拟合到位。这类现象更接近优化退化。模型容量虽然在增加但优化器在参数空间中找不到一个足够好的解。一个典型的例子是不加残差连接时把一组全连接层从 10 层增加到 20 层甚至 50 层训练损失可能先下降后反弹。如果每层权重初始化不合理前向传播到深层时输出方差会不断放大反向传播时梯度又难以回到浅层网络参数几乎停在初始值附近。这时模型表达力并不弱只是训练过程没有把表达能力发挥出来。这个判断对排查问题很关键。如果训练集 loss 都降不下去应该先怀疑“优化链路是否通”也就是梯度传播是否正常、激活函数是否饱和、初始化是否匹配而不是急着加正则化或者增大数据集。1.2 反向传播链式法则下的梯度消失与梯度爆炸反向传播的核心是链式法则。对于某个浅层参数它的梯度等于后面每一层局部导数的连乘结果grad_prev grad_next * w * activation_gradient如果每一层的梯度范数都小于 1多层连乘后就会指数级趋近于 0这叫梯度消失。如果每一层的梯度范数都大于 1连乘后会指数级增大到天文数字这叫梯度爆炸。激活函数在其中是人为可控的一环。Sigmoid 的导数最大只有 0.25在深层网络中连乘很容易让前面几层梯度接近 0。ReLU 在正半区导数恒为 1至少不会主动缩小梯度这就是深层网络通常默认使用 ReLU 的数学原因。这里要区分梯度消失的两种触发方式一种是激活函数饱和导致导数接近 0另一种是权重初始化太小导致每层缩放因子都在缩小梯度。两者叠加时网络会更快失去学习能力。1.3 激活函数的饱和区会放大深层问题“饱和”指的是激活函数输入落在某个范围后输出变化变得非常缓慢导数趋近于 0。Sigmoid 在输入很大或很小时输出接近 0 或 1Tanh 在输入很大或很小时输出接近 -1 或 1。输入一旦进入饱和区神经元输出对输入几乎不敏感反向传播时这个神经元贡献的梯度也会归零。多个饱和神经元串联起来浅层参数很难获得有效更新。更麻烦的是饱和不只会因为输入本身过大而触发。权重初始化太大、批归一化没有跟上、学习率过高都可能让激活层的输入很快落入饱和区。所以实际项目中换激活函数通常会伴随初始化策略一起调整。Sigmoid/Tanh 这类中心对称激活更适合配合 Xavier 初始化ReLU 系列更适合配合 Kaiming 初始化。初始化做得好能让激活输入落在梯度较大的区间这是后续一切训练稳定的基础。2. 激活函数究竟改变了什么从非线性到梯度通路2.1 前向传播中的非线性多个线性层等价于一个线性层如果不加激活函数一个多层网络的每一层只是线性变换h1 W1 * x b1 h2 W2 * h1 b2把两步合并本质还是线性变换h2 (W2 * W1) * x (W2 * b1 b2)无论网络叠多少层表达能力都不会超过一个线性模型。这样的网络无法拟合异或这类非线性关系更处理不了图像、语音和自然语言中的复杂模式。激活函数的加入让每个线性变换之后多了一层非线性映射z Wx b a activation(z)当激活函数是非线性函数时多层网络才能逐步组合低级特征形成更抽象的表示。这也是“深度”真正有意义的条件之一。在这条链路里bias 的作用也不可忽视。即使某一层的输入全为 0bias 也能让加权和不为 0从而避免激活函数输入长期停留在极端位置。实际网络初始化时bias 通常初始化为 0 或很小的值这个选择也需要和激活函数配合。2.2 反向传播中的导数激活函数决定梯度是放大还是缩小反向传播只关心局部梯度。每一层的梯度要乘上当前层的权重再乘上激活函数的导数delta_prev delta_next * W^T * activation_gradientactivation_gradient是一个标量如果它小于 1经过多层连乘就会缩小梯度如果它大于 1就可能放大梯度。理想的激活函数应该在常见输入范围内保持非饱和、平滑让梯度能稳定回传。ReLU 正半区导数为 1这是一个非常简洁的设计。它既不会放大梯度也不会缩小梯度。但它在负半区导数为 0一旦神经元进入负区间这个神经元就不会再更新也就是常说的 Dead ReLU。Leaky ReLU、PReLU、ELU、SELU 等改进版本核心思路都是在负半区保留一点梯度通道避免神经元完全死亡。2.3 仅换激活函数还不够初始化、归一化和残差需要一起设计深层网络是否容易训练是由一个组合机制决定的。激活函数是其中一个关键组件但不是全部。比较常见的组合是权重初始化匹配激活函数ReLU 系列用 Kaiming 初始化Sigmoid/Tanh 系列用 Xavier 初始化在线性层之后加入批归一化或层归一化让每层输入分布更稳定深度到达几十层以上时加入残差连接让梯度可以跨层抄近路。这里的关键是激活函数解决“非线性表达”和“局部导数”问题但不负责全局梯度路径。如果网络太深还是需要残差这类结构来缩短梯度回传链条。理解这一点就不会把激活函数当作解决所有深层训练问题的万能药。3. 十种激活函数逐个拆解公式、导数与适用场景3.1 Sigmoid适合理解历史但不建议用于深层隐藏层Sigmoid 的公式是sigmoid(x) 1 / (1 exp(-x))输出范围是 (0, 1)导数范围是 (0, 0.25)最大值为 0.25。优点是输出可以解释为概率特别适合二分类输出层。缺点是输出不是零中心化的在深层网络中一方面饱和问题严重另一方面导数最大值只有 0.25多层连乘很容易造成梯度消失。工程建议隐藏层不要用 Sigmoid。二分类任务中通常最后一层用 Sigmoid或者更推荐在训练时使用带 logits 的BCEWithLogitsLoss避免数值不稳定。3.2 Tanh零中心化改进版但仍然会饱和Tanh 的公式是tanh(x) (exp(x) - exp(-x)) / (exp(x) exp(-x))输出范围是 (-1, 1)导数最大为 1但在输入绝对值较大时快速接近 0。和 Sigmoid 相比Tanh 输出是零中心化的优化效果通常更好。但在层数较深时饱和问题依然存在输入过大或过小都会让梯度趋近 0。适用场景包括 RNN 中的某些门控计算、浅层网络隐藏层。在深层全连接网络里Tanh 不是首选。3.3 ReLU深层网络默认主力ReLU 的公式是relu(x) max(0, x)正半区导数为 1负半区导数为 0。优点非常明显计算简单、正区间梯度恒定、输出有稀疏性。它不会主动缩小梯度所以比 Sigmoid/Tanh 更适合深层网络。缺点是 Dead ReLU神经元一旦激活输出为 0 且梯度为 0就可能长期不更新。工程建议大多数 CNN 和 MLP 的隐藏层可以从 ReLU 开始。搭配 Kaiming 初始化和适量学习率能避开大部分 Dead ReLU 问题。3.4 Leaky ReLU给负区间留一条小梯度通道Leaky ReLU 的公式是leaky_relu(x) x, x 0 leaky_relu(x) alpha * x, x 0alpha通常取 0.01。它解决的是 ReLU 负区间完全死亡的缺陷。即使输入落在负区间依然有一个很小的梯度可以让神经元有机会回到活跃状态。相比 ReLU它在处理大量负特征时更稳定。缺点是alpha是超参数不同任务的最优值可能不同。不过大多数情况下0.01 的默认值就能工作得不错。3.5 PReLU把泄漏系数变成可学习参数PReLU 的公式和 Leaky ReLU 类似prelu(x) x, x 0 prelu(x) alpha * x, x 0不同点在于alpha不是固定值而是网络在训练过程中自己学习的参数。优点是可以针对任务自适应负区间斜率理论上更灵活。缺点是多了一部分可学习参数增加了过拟合风险也需要更细致的正则化和调参。在大规模图像任务中PReLU 有应用价值。但在小数据集上Leaky ReLU 往往足够不需要额外引入学习参数。3.6 ELU平滑负区间输出均值更接近 0ELU 的公式是elu(x) x, x 0 elu(x) alpha * (exp(x) - 1), x 0alpha通常取 1.0。和 ReLU 相比ELU 在负区间是平滑曲线而不是硬截断。它避免了 0 点处的突变输出均值更接近 0对下一层训练更友好。缺点是负区间使用了指数运算计算成本更高。另外输入极度为负时导数依然接近 0只是不如 ReLU 那样完全截断。需要更高计算精度和更多算力的场景要评估性能损耗。3.7 SELU让网络具备自我归一化能力SELU 的公式是selu(x) scale * x, x 0 selu(x) scale * alpha * (exp(x) - 1), x 0在原始论文的设计中scale约为 1.0507alpha约为 1.67326。SELU 的特别之处在于当网络结构满足特定条件时输出能自动保持均值为 0、方差为 1 的分布相当于内建了一部分归一化能力可以替代某些场景下的批归一化。但它的适用条件比较严格。原始设定下网络需要以顺序结构堆叠权重初始化也需要匹配复杂结构中并不一定生效。生产环境使用前一定要先在目标数据上做验证不要因为“自归一化”这个特性就盲目替换原有激活。3.8 GELUTransformer 和预训练模型中的高频选择GELU 的公式是gelu(x) x * Phi(x)其中Phi(x)是标准正态分布的累积分布函数。工程实现常用近似公式gelu(x) ≈ 0.5 * x * (1 tanh(sqrt(2 / pi) * (x 0.044715 * x^3)))GELU 的设计思路是根据输入值大小施加不同权重输入越大保留程度越高输入为负时保留程度越低。它比 ReLU 更平滑在 Transformer、BERT 等预训练模型中非常常见。缺点是计算量比 ReLU 高。实际落地时训练和推理必须使用相同的近似实现否则模型输出会有细微偏差。3.9 Swish / SiLU无上界有下界平滑非单调Swish 的公式是swish(x) x * sigmoid(x)它也叫 SiLU。输出无上界有下界在负区间呈现非单调特性。Swish 的优点是平滑、非单调在部分 CNN 和 Transformer 任务中表现优于 ReLU。MobileNetV3 等网络结构中就使用了 Swish 类似设计。缺点是计算成本比 ReLU 稍高。很多推理框架对 ReLU 有专门优化对 Swish 的支持取决于算子实现情况。3.10 Mish平滑性更强的探索型激活Mish 的公式是mish(x) x * tanh(softplus(x)) softplus(x) ln(1 exp(x))Mish 同样具有无上界、有下界、平滑、非单调的特点。原始论文中在部分图像任务上报告了优于 ReLU/Swish 的结果。但它的计算复杂度更高涉及指数、对数、双曲正切多个算子。很多推理框架对 Mish 的支持不如 ReLU 成熟部署阶段需要额外关注算子兼容性和性能。下面是十种激活函数的速查表。激活函数表达式要点导数特点主要风险典型场景Sigmoid1 / (1 exp(-x))最大 0.25容易饱和深层梯度消失二分类输出层Tanh(exp(x) - exp(-x)) / (exp(x) exp(-x))最大 1仍会饱和深层梯度消失浅层网络、RNN 门控ReLUmax(0, x)正区间 1负区间 0Dead ReLUCNN、MLP 隐藏层默认Leaky ReLU负区间乘以 alpha负区间小梯度alpha 需要调整出现 Dead ReLU 时PReLU负区间斜率可学习自适应负梯度参数变多易过拟合大数据量图像任务ELU负区间指数平滑负区间平滑但不恒为 1计算成本高需要更稳定训练时SELU带 scale 的 ELU自归一化条件严格顺序全连接网络GELUx * Phi(x)平滑非饱和计算成本略高Transformer、BERTSwish/SiLUx * sigmoid(x)平滑非单调算子支持待确认CNN、部分 TransformerMishx * tanh(softplus(x))平滑非单调计算成本高图像探索实验4. 用 PyTorch 写一个深层网络实验实测不同激活函数4.1 准备环境与依赖这个实验只需要 Python 和 PyTorch。建议 Python 3.9 以上PyTorch 2.x 都可以。安装方式会根据操作系统和 CUDA 环境不同而不同在只做 CPU 实验时直接安装 CPU 版即可。pip install torch如果是 GPU 环境需要先到 PyTorch 官网确认与 CUDA 版本匹配的安装命令。实验本身不依赖 GPUCPU 完全可以跑通。4.2 构造等深度 MLP核心思路是保持网络深度、宽度、优化器完全一致只替换隐藏层激活函数。这样能比较公平地观察激活函数对训练过程的影响。import torch import torch.nn as nn import torch.optim as optim class DeepMLP(nn.Module): def __init__( self, activation_factory, in_features32, hidden_features64, out_features1, depth12, use_bnFalse, ): super().__init__() layers [nn.Linear(in_features, hidden_features)] if use_bn: layers.append(nn.BatchNorm1d(hidden_features)) layers.append(activation_factory()) for _ in range(depth - 1): layers.append(nn.Linear(hidden_features, hidden_features)) if use_bn: layers.append(nn.BatchNorm1d(hidden_features)) layers.append(activation_factory()) layers.append(nn.Linear(hidden_features, out_features)) self.net nn.Sequential(*layers) def forward(self, x): return self.net(x)这里使用activation_factory而不是直接传入一个激活函数实例原因是要为每一层创建独立的激活模块。如果复用同一个 PReLU 实例它的可学习参数会在多层之间共享这通常不是实验期望的行为。构造一个回归任务输入是 32 维随机向量输出是sin(x w)这样数据本身存在非线性关系适合检验网络拟合能力。torch.manual_seed(0) x torch.randn(1024, 32) weight_true torch.randn(32, 1) y torch.sin(x weight_true).squeeze() def train_activation(activation_factory, depth12, epochs300): model DeepMLP(activation_factoryactivation_factory, depthdepth) optimizer optim.Adam(model.parameters(), lr1e-3) loss_fn nn.MSELoss() model.train() for epoch in range(1, epochs 1): optimizer.zero_grad() pred model(x) loss loss_fn(pred.squeeze(), y) loss.backward() total_grad_norm 0.0 for param in model.parameters(): if param.grad is not None: total_grad_norm param.grad.norm().item() ** 2 grad_norm total_grad_norm ** 0.5 optimizer.step() if epoch % 50 0: print( fepoch{epoch}, floss{loss.item():.6f}, fgrad_norm{grad_norm:.6f} ) return loss.item()运行方式很简单python compare_activation.py在相同深度下Sigmoid 很可能出现 loss 下降极慢、梯度范数极小的情况ReLU、GELU、Swish 这类激活函数的收敛速度通常更快。但要注意这个结果和随机种子、深度、学习率密切相关不能直接当作普适结论。做对比实验时应该固定所有其他变量只改激活函数。4.3 输出层激活函数必须与任务匹配隐藏层激活函数和输出层激活函数是两套逻辑。隐藏层负责非线性表示输出层负责把网络输出映射到任务目标空间回归任务输出层通常不加激活函数直接输出连续值二分类输出层可用 Sigmoid或更推荐直接输出 logits 配合BCEWithLogitsLoss多分类输出层可用 Softmax但训练时直接使用CrossEntropyLoss时最后一层保持原始 logits 即可不需要手动加 Softmax。不要在隐藏层使用 Softmax。Softmax 会把输出归一化成一组和为 1 的概率这会让梯度在多层之间传递时出现明显抑制训练稳定性反而更差。5. 验证一套指标怎么判断换激活函数确实有用5.1 梯度范数检验换完激活函数后第一步要看梯度范数而不是只看最终损失。梯度范数能直接反映反向传播链路是否通畅。def print_layer_grad_norm(model): for name, param in model.named_parameters(): if param.grad is not None and weight in name: print(name, param.grad.norm().item())观察两个阶段训练初期和训练中期。训练初期前面层的梯度如果比后面层小几个数量级说明梯度过早衰减。比如某层梯度是 1e-12 级别那这层参数基本不会更新网络再深也没有意义。梯度范数也不宜过大。如果训练初期就出现 1e10 以上说明存在梯度爆炸风险需要先做梯度裁剪或降低学习率。5.2 训练曲线和验证曲线激活函数的效果不能只看训练集上的最终 loss还要看训练曲线形态。同一个 epoch 数下loss 下降更快说明优化效率提升loss 波动更小说明训练更稳定。验证集曲线用来判断泛化能力。如果训练 loss 很低但验证 loss 很高问题是过拟合和激活函数关系不大。如果训练 loss 本身不降问题更可能是优化链路不通这时候换激活函数才可能有效。建议每轮训练都记录 train loss、val loss、grad_norm三个指标一起观察。单独看其中一个很难定位问题。5.3 稳定性和可复现性清单做激活函数 A/B 实验时最容易出现的问题是结果不可复现。同一个实验跑两遍损失曲线差异很大结论自然不可靠。可复现实验至少需要满足固定torch.manual_seed固定数据划分训练集验证集不要每次随机生成固定优化器、学习率、batch size、深度和宽度同一组配置至少跑 3 个不同随机种子记录均值或中位数不要把一次偶然的好结果当成确定结论。这套清单在调试深层网络时非常实用。不管是换激活函数、换初始化还是换归一化层都应该按这个标准执行。6. 常见问题与排查路径换完激活函数网络还是不行怎么办6.1 大量神经元输出恒为 0Dead ReLU现象是网络中间层输出大量 0loss 长时间不下降。常见原因是 ReLU 负区间导数始终为 0学习率过大或权重初始化不合适导致激活输入长期落在负区间。一旦神经元进入死亡状态没有梯度能把它拉回来。检查方式可以统计激活输出中 0 的比例def dead_ratio(module, x): with torch.no_grad(): out module(x) return (out 0).float().mean().item()处理方式换 Leaky ReLU、PReLU 或 ELU降低学习率改用 Kaiming 初始化必要时增加批归一化。6.2 深层网络输出全 0 或全部相同现象是不同样本的输出非常接近甚至完全相同。常见原因是梯度消失导致前面层几乎没有更新也可能是 batch size 过小导致批归一化统计不稳定还可能最后一个线性层输出被某些极端值压到了几乎不变的位置。检查方式是打印每层激活输出的均值和方差def print_activation_stats(model, x): with torch.no_grad(): h x for i, layer in enumerate(model.net): h layer(h) if hasattr(h, mean) and hasattr(h, std): print(i, layer.__class__.__name__, h.mean().item(), h.std().item())处理方式减少网络深度加入残差连接加入批归一化换非饱和激活函数检查输入数据是否需要标准化。6.3 训练中途出现 NaN 或梯度爆炸现象是 loss 在某一步突然变成nan或者梯度范数在训练过程中不断暴涨。常见原因是学习率过大、数据没有标准化、激活函数和权重初始化组合不当。检查方式是在optimizer.step()之前打印梯度范数torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)如果梯度范数已经达到 1e8 以上需要先处理梯度爆炸问题。推荐做法是配合梯度裁剪同时降低学习率并检查输入数据是否做了标准化。6.4 训练不收敛但梯度正常梯度范数正常不代表网络一定能收敛。可能原因还包括损失函数和输出层不匹配学习率过大导致 loss 震荡网络深度过大信息在前向传播中已经失真数据本身没有归一化特征尺度差异太大。排查时可以按这个顺序走问题现象检查顺序常用处理loss 波动大学习率是否过大降低学习率或使用学习率调度loss 长期不降输出层和损失函数是否匹配二分类用 BCE多分类用 CE回归用 MSE加深后变差梯度是否有效回传加残差、BN或更换激活函数训练结果不稳定随机种子和数据划分是否固定固定 seed多次实验取中位数7. 实践选型清单不同任务和环境下怎么选激活函数7.1 快速选型表大多数场景不需要追求复杂激活函数。下面这张表偏工程向可以作为起点场景推荐隐藏层激活理由注意点通用 MLP/CNN 分类ReLU简单高效默认可用防止 Dead ReLU深层网络出现大量死亡神经元Leaky ReLU 或 ELU负区间保留梯度通道参数需要验证Transformer / 预训练模型GELU 或 SwiGLU平滑性好主流验证充分训练推理实现要一致回归任务ReLU 或 GELU中间层保持非线性输出层不加激活二分类输出层Sigmoid输出可解释为概率训练时推荐 BCEWithLogitsLoss多分类输出层Softmax输出概率分布训练时推荐 CrossEntropyLoss需要说明的是从传统神经网络到 Transformer激活函数从 ReLU 演进为 GELU、SwiGLU本质仍然是解决深层训练中的梯度流动和表达能力问题并没有脱离本文讨论的核心逻辑。7.2 学习环境与生产环境的差异学习环境里激活函数可以随意更换重点看训练曲线和梯度。生产环境则不同激活函数会影响推理算子、量化、内存占用和延迟。ReLU 在绝大多数推理框架中都有专门优化。GELU 在不同框架中可能存在近似实现差异模型从 PyTorch 导出到 ONNX、TensorRT 时要检查算子是否被正确映射。Mish 这类计算较复杂的激活函数在部分推理设备上支持度有限性能可能是瓶颈。另一个容易忽略的点是训练和推理的一致性。训练时使用了某个自定义近似实现导出模型时也必须保持一致否则线上推理结果会和离线测试有偏差。7.3 组合使用激活函数无法单独解决所有深度问题深层网络要达到稳定可训练的状态通常需要把多个机制组合在一起残差连接给梯度提供跨层通路批归一化或层归一化稳定每层输入分布合理初始化避免激活输入落在饱和区学习率调度和梯度裁剪控制更新步长监控梯度范数和激活分布把优化问题暴露在早期。这几件事做得越稳激活函数的效果越能体现出来。与其在多个激活函数之间频繁切换不如固定一种主流激活函数先把归一化、初始化和残差结构搭好再针对具体瓶颈替换激活函数做实验。8. 从激活函数开始继续深入网络可训练性8.1 下一步可以做什么实验如果希望把激活函数的理解落到代码上可以按下面的顺序实验手动实现 ReLU 和它的导数理解 forward 和 backward 的关系用 10
返回列表