资讯动态

激活函数设计取舍:梯度消失、ReLU家族与Swish/Mish选型

发布时间:2026/10/26 12:00:10 来源:尧图企业网站定制
上周有位刚入行的朋友拿着他的第一个分类网络来找我说训练了三十轮准确率卡在 0.5 一动不动学习率从 1e-2 调到 1e-5 全试过最后发现是他在隐藏层里连用了两层sigmoid梯度早就衰减到小数点后十几位去了。这种问题在教程里很少被点破——大家讲激活函数的时候习惯把 sigmoid、tanh、ReLU、Leaky ReLU、P-ReLU、R-ReLU、ELU、Swish、Mish、Maxout、softmax 排成一列每个给个公式和一张曲线图然后就没了。可真到了自己搭网络、自己调参数的时候你面对的问题不是这个函数长什么样而是我这个场景该挑哪个、为什么、换了之后会出现什么新问题。这篇东西想做的事是把这十来个激活函数从公式列表还原成设计取舍。我会讲清楚每个函数是在给谁打补丁、补丁本身又引入了什么代价、以及在什么网络结构里它才是正确的选择。读完之后你至少能做到两件事看到一个新模型用了某个激活函数能猜出作者为什么这么选自己动手时不靠碰运气也能选出合适的那个。全文的代码都基于常见的深度学习框架写法思路是通用的换个库照样能搬。1. 先想清楚激活函数在补什么再谈选哪个1.1 没有非线性堆一百层也只是一次矩阵乘法很多人对这个问题的理解停留在加个非线性让网络更强但具体强在哪说不清楚。其实道理非常朴素两个线性变换的复合还是线性变换。你写 $y W_2(W_1x b_1) b_2$展开就是 $y (W_2W_1)x (W_2b_1 b_2)$等价于一个单层网络。也就是说如果中间不插入非线性函数你叠一百层和叠一层在表达能力上没有任何区别只是多烧了几百倍的算力。激活函数的作用就是在这条线性链条上凿出一个弯让网络能拟合曲线而不只是直线。理解这一点之后你就明白了一个判断标准一个激活函数的价值不在于它本身多复杂而在于它凿出的这个弯是否容易被优化器找到。这也是为什么 ReLU 这种看起来极其简陋、连导数都只有两个取值的函数能统治深度学习十几年——它凿的弯简单、粗暴但梯度传得动。1.2 衡量激活函数的四把尺子我平时评估一个激活函数会从四个维度去看这四个维度基本能覆盖绝大多数实际问题。第一把尺子是饱和性。什么叫饱和就是输入跑到很大或很小的区间时函数输出趋近一个常数导数趋近于零。导数趋于零意味着反向传播时这一层的梯度被乘上一个接近零的系数梯度就这么没了。sigmoid 和 tanh 两端都饱和ReLU 只在负半轴硬饱和导数为零而非趋近零ELU、Swish、Mish 则是负半轴渐近饱和。梯度能不能穿过深网络主要看这把尺子。第二把尺子是是否零中心。如果激活函数的输出恒为正那么下一层权重的梯度方向会被整体约束在同一侧优化路径会呈现锯齿状的震荡。这不是致命问题但会让收敛变慢。sigmoid 输出恒正tanh 输出以零为中心ReLU 输出非负这就是 tanh 在很多场景下比 sigmoid 收敛更快的原因之一。第三把尺子是计算开销。别小看这一点。ReLU 是一次比较运算sigmoid 要算一次指数加一次除法Mish 要算 softplus含 exp 和 log再算一次 tanh 再乘一次单次前向的开销能差出好几倍。在参数量不大、激活调用次数不多的网络上这无所谓但在移动端或者大批量推理的场景里这个差距是能实测出来的。第四把尺子是死区风险。ReLU 在负半轴梯度严格为零一旦某个神经元的输入长期落在负区它的权重就再也拿不到梯度更新彻底死掉。这个现象在小学习率、大学习率、初始化不当等情况下都可能触发后面会单独讲。提醒一句这四把尺子不是打分表而是权衡表。没有哪个激活函数四项全优你要做的是搞清楚自己的场景最不能容忍哪一项。2. Sigmoid 与 Tanh教科书里的元老今天该放在哪2.1 从导数上限 0.25 算出梯度消失的速度sigmoid 的定义是 $\sigma(x) \frac{1}{1e^{-x}}$它的导数有个非常漂亮的闭式$\sigma(x) \sigma(x)(1-\sigma(x))$。这个式子本身没什么但如果你把它的最大值算出来问题就一目了然了$\sigma(x)$ 的取值范围是 $(0,1)$令 $p \sigma(x)$则导数为 $p(1-p)$这是一个开口向下的抛物线在 $p0.5$ 时取最大值 $0.25$。也就是说sigmoid 的导数在任何输入下都不会超过 0.25。反向传播时梯度每穿过一层 sigmoid就要被乘上一个不超过 0.25 的系数。假设你有 10 层这样的结构梯度幅值最多剩 $0.25^{10} \approx 9.5 \times 10^{-7}$实际上因为大部分输入并不落在 0 附近衰减比这还要惨烈得多。这就是那位朋友训练不动的原因——不是学习率的问题是信号在传回去的路上已经没了。配套的还有另一个麻烦sigmoid 输出恒为正均值大约 0.5。这会导致下一层权重的梯度符号高度一致优化时走的是之字形路线。两个问题叠加深网络里用 sigmoid 基本等于自废武功。2.2 tanh 的零中心优势与它没解决的问题tanh 常被说成sigmoid 的改进版这个说法对了一半。从形式上看$\tanh(x) 2\sigma(2x) - 1$它就是 sigmoid 拉伸平移后的结果输出范围变成 $(-1, 1)$均值落在零附近零中心这一项拿到了满分。导数也变成 $1 - \tanh^2(x)$最大值是 1比 sigmoid 的 0.25 好太多。但它没解决饱和问题。输入绝对值超过 3 之后tanh 的输出就贴着 $\pm 1$ 不动了导数掉到 0.01 以下。所以深网络里堆 tanh梯度照样会消失只是消失得慢一些。我自己做过一个粗糙的对比实验同样 8 层全连接、同样的 Xavier 初始化用 sigmoid 的网络在第 3 层左右梯度就已经降到 $10^{-5}$ 量级用 tanh 能撑到第 5 层但第 8 层一样是 $10^{-6}$ 级别。结论很明确——tanh 是缓解不是解决。2.3 这两个函数今天仍然不可替代的三个位置虽说隐藏层里它们已经退居二线但有几个位置的活儿别的函数真干不了。第一个位置是二分类的输出层。你需要一个 $(0,1)$ 区间内的概率值sigmoid 天然符合而且配合二元交叉熵损失时梯度形式会被化简得非常干净不存在饱和导致的梯度消失——因为损失函数里的 $\log$ 和 sigmoid 的 $\exp$ 会相互抵消。这一点很多人不知道以为输出层用 sigmoid 也会梯度消失实际上只要损失函数配对正确输出层的梯度是线性的。第二个位置是门控结构。LSTM、GRU 里的输入门、遗忘门、输出门本质上要做的是按比例保留信息需要一个 $(0,1)$ 的软开关sigmoid 正好。注意力机制里的门控权重、某些混合专家结构的路由权重也是同理。第三个位置是需要严格有界输出的场景。比如某些强化学习里的动作概率、某些归一化系数的预测你需要输出被硬性限制在某个区间内tanh 的 $(-1,1)$ 就比 ReLU 的无界输出好用。import torch # sigmoid 的朴素写法在极端输入下会溢出看这个例子 x torch.tensor([-200.0], dtypetorch.float32) naive 1.0 / (1.0 torch.exp(-x)) # torch.exp(200) 直接变成 inf结果是 0虽然答案碰巧对但中间过程已经炸了 # 按符号分支的稳定写法 def sigmoid_stable(x): out torch.empty_like(x) pos x 0 neg ~pos out[pos] 1.0 / (1.0 torch.exp(-x[pos])) ex torch.exp(x[neg]) out[neg] ex / (1.0 ex) return out print(sigmoid_stable(x)) # tensor([0.])这段代码不是炫技。用 float32 时输入到 -100 左右才会溢出容易被忽略一旦切到 float16 做混合精度训练输入到 -12 附近exp就爆了直接给你返回inf或者nan。所以自己实现激活函数时数值稳定性必须是第一考虑框架自带的torch.sigmoid已经做了这个分支处理能用内置的就别手写。3. ReLU 家族的三种补丁Leaky、P-ReLU、R-ReLU 分别堵哪个洞3.1 Dying ReLU 是怎么发生的一次调参事故的复盘ReLU 的定义简单到不能再简单$f(x) \max(0, x)$。正区间导数恒为 1梯度原封不动地传回去这就绕开了 sigmoid 的连乘衰减负区间导数恒为 0输出稀疏计算极快。它的问题也都写在这个定义里负半轴梯度为零一旦进去就出不来。我见过一次很典型的事故。某个开发者把学习率从 1e-3 直接拉到 1e-1 做快速试验跑了两百个 step 之后 loss 突然不动了。他以为是学习率太大导致震荡又降回 1e-4结果还是不动。后来我让他统计每一层激活后的零值比例发现第三层有 87% 的通道输出恒为零——这一层的权重被一次大梯度更新推到了让所有输入都落在负半轴的位置然后梯度永远是零权重再也回不来了。这就是Dying ReLU神经元不是学得慢是彻底死了。排查这个东西有个简单办法拿一个 batch 的数据过一遍网络统计每层激活输出的全零通道占比。健康的值大概在 30% 到 60% 之间ReLU 本身就会让约一半的输入变零这是设计意图如果某层超过 80% 且训练过程中还在涨基本可以确定出问题了。def dead_channel_ratio(act): # act: [batch, channels, ...]统计每个通道在整个 batch 上是否全零 flat act.flatten(2) # [B, C, N] per_channel_max flat.abs().amax(dim(0, 2)) return (per_channel_max 0).float().mean().item()3.2 Leaky ReLU 的斜率取值与它的实际收益边界Leaky ReLU 的思路非常直接负半轴不给零给一个很小的斜率。$f(x) x$ 当 $x0$$f(x) \alpha x$ 当 $x \le 0$$\alpha$ 通常取 0.01。这样负区间的导数恒为 $\alpha$虽然小但不是零梯度能漏过去神经元就不会彻底死掉。这个补丁的收益有多大说实话取决于你的网络有没有真的在死。如果 ReLU 跑得好好的换成 Leaky ReLU 大概率提升在噪声范围内。它的真正价值在于两种情况一是学习率不得不设得比较大比如你用的是大 batch 训练学习率线性放大二是网络很深、初始化不太讲究。这两种情况下 Leaky ReLU 的容错性明显更好。$\alpha$ 的取值也值得说一句。0.01 是历史默认值但工程上更常见的是 0.1 到 0.2。$\alpha$ 太小比如 1e-4等于没加负区间的梯度微弱到在浮点精度下和零没区别$\alpha$ 太大比如 0.5就退化成近似线性的函数非线性表达能力被削弱。我个人的经验是先用 0.01 做基线如果发现死亡通道比例还是偏高往 0.1 调别超过 0.3。3.3 P-ReLU 的可学习斜率参数量、初始化与过拟合风险P-ReLU 是 Leaky ReLU 的参数化版本——它不让你手工指定 $\alpha$而是把 $\alpha$ 变成一个可学习的参数让网络自己决定负半轴该有多斜。听起来很美实际使用时有两个细节必须注意。第一个是参数共享粒度。框架里通常有个num_parameters的选项设成 1 表示整个层共用一个 $\alpha$设成通道数 $C$ 表示每个通道一个 $\alpha$。后者表达能力强得多但参数量也跟着涨。在你网络很宽比如 512 通道而数据量不大的情况下每个通道一个可学习斜率是有过拟合风险的。我一般默认用 1只有在数据量足够、且确实是深层视觉任务时才用 $C$。第二个是初始值。$\alpha$ 的默认初始化通常是 0.25这个值比 Leaky ReLU 常用的 0.01 大不少。如果你的任务对负区间的信息不敏感这个偏大的初值可能会让训练前期负半轴的贡献过多。我遇到过一个语义分割的活儿换成 P-ReLU 后 mIoU 掉了两个点最后发现就是把 $\alpha$ 初值从 0.25 改成 0.01 就恢复了。import torch.nn as nn # 每通道一个可学习斜率 prelu_c nn.PReLU(num_parameters64, init0.01) print(prelu_c.weight.shape) # torch.Size([64]) # 全局共享一个斜率 prelu_s nn.PReLU(num_parameters1, init0.01) print(prelu_s.weight.shape) # torch.Size([1])还有一点容易踩P-ReLU 的权重参与正则化。如果你用了权重衰减这个 $\alpha$ 会一起被往零拉等于在训练过程中逐渐把 P-ReLU 退化成 ReLU。要么把 $\alpha$ 从 weight decay 里排除要么接受这个退化行为——但至少你得知道它在发生。3.4 R-ReLU 的随机斜率与训练/推理模式切换的坑R-ReLU 的想法挺有意思既然 $\alpha$ 不好定那就在训练时给每次前向随机采样一个 $\alpha$从均匀分布 $U(\alpha_{low}, \alpha_{high})$ 里抽推理时用期望值 $\frac{\alpha_{low} \alpha_{high}}{2}$ 固定住。等效于给激活函数本身加了一层随机扰动起到轻微的正则化作用。这个设计在小数据集上确实能带来一点泛化收益但它有个非常隐蔽的坑训练和推理的行为不一致。如果你自己实现 R-ReLU 却忘了在验证模式下切换到固定斜率那么同一个样本两次推理可能得到不同的输出你的验证指标就会莫名其妙地抖动而且抖动量不大不小特别容易被误判成模型不稳定或者数据有问题。框架里的实现会自动处理train()和eval()的切换但你还是要知道底层发生了什么。默认的 $\alpha_{low}1/8$、$\alpha_{high}1/3$推理时用的是 $\approx 0.229$。这个值比 Leaky ReLU 的 0.01 大一个数量级意味着 R-ReLU 在负半轴的信息保留要比 Leaky ReLU 激进得多。坦白讲这个函数我在实际项目里用得极少。它的收益太依赖数据规模而在大数据集上随机斜率带来的正则化效果基本被海量数据稀释掉了性能上和 Leaky ReLU 拉不开差距。它的价值更多体现在数据少、又想少调一个超参的场景。4. ELU、Swish、Mish用计算换平滑值不值4.1 ELU 的负饱和设计为什么没能取代 ReLUELU 的全称是指数线性单元负半轴用 $f(x) \alpha(e^x - 1)$正半轴保持 $x$。这个设计的巧思在于负半轴不是线性地漏而是平滑地饱和到 $-\alpha$。带来的好处有两个——输出均值更接近零负半轴有下界不会像 Leaky ReLU 那样一直往下走以及负区间的导数连续$f(x) f(x) \alpha$在 $x0$ 处两侧导数都是 1。理论上听起来比 ReLU 优雅得多但实际落地时它一直没能取代 ReLU。原因说穿了很现实它要算指数。ReLU 是一次比较ELU 负半轴要算 $\exp$在 GPU 上这个差距在批量大的时候是能测出来的。而它换来的收敛优势在配上批量归一化之后就基本消失了——批量归一化已经把激活前的分布拉回零均值附近ELU 输出均值接近零的优势被提前实现了。还有个更微妙的坑$\alpha$ 的取值。默认 $\alpha1$ 时负半轴饱和到 $-1$这个下界其实挺深的。有些任务里改小到 0.5 甚至 0.1 效果更好。这个参数在大多数框架里是可以配置的但很多教程不提导致大家以为它是固定的。4.2 Swish 的自门控结构与 β 的调节作用Swish 的公式是 $f(x) x \cdot \sigma(\beta x)$$\beta1$ 时一般叫 SiLU。它的结构很有意思——用输入自己乘上自己经过 sigmoid 后的门控值。这相当于让每个神经元自己决定我这条信息该保留多少是一种自门控机制。这个函数有三个和 ReLU 家族不一样的性质。第一它是平滑且非单调的在 $x \approx -1.278$ 处有个浅浅的负值低谷这意味着它允许小幅度负输出信息保留比 ReLU 丰富。第二它是有下界无上界的负方向渐近趋零正方向线性增长梯度不会在正区间饱和。第三$\beta$ 这个参数可以调节门控的锐利程度——$\beta \to 0$ 时函数趋近线性 $x/2$$\beta \to \infty$ 时趋近 ReLU。$\beta$ 可学习是 Swish 的一个进阶用法有些实现会让你把它设成可训练参数。但我的经验是除非你有明确的理由别动 $\beta$就固定在 1。因为这个参数的梯度尺度和其他权重完全不同很容易在训练后期引发不稳定收益却未必明显。4.3 Mish 的四次指数运算代价与实测差异Mish 的公式是 $f(x) x \cdot \tanh(\text{softplus}(x))$其中 softplus 是 $\ln(1e^x)$。展开看它要算一次 exp、一次 log、一次 tanh、两次乘法。相比之下 ReLU 只要一次比较。这个计算量的差距在理论上是四到五倍。那它换来了什么Mish 的曲线比 Swish 更平滑负区间的低谷更深一些大约在 $-0.3$ 附近梯度流在整个定义域上都连续可导。有些视觉任务上的实测显示 Mish 比 Swish 略好尤其是在目标检测这类需要精细特征的任务上。但我自己的实测结论要保守得多。在同一个骨干网络上分别换 ReLU、Swish、Mish 跑对比差距通常在 0.3 到 0.8 个百分点之间而且这个差距在不同随机种子下的波动能达到 0.5 个点。也就是说除非你跑多次取平均、严格控制变量否则很难说清这点提升到底来自激活函数还是来自随机性。而推理耗时上Mish 带来的增幅是实打实的两位数百分比。所以我的建议是Swish 和 Mish 适合用在精度已经卡住、愿意拿推理速度换一点提升的场景。如果你的模型还要部署到端侧老老实实用 ReLU 或者 Leaky ReLU。import torch import torch.nn as nn import torch.nn.functional as F class Swish(nn.Module): def __init__(self, beta1.0): super().__init__() self.beta beta def forward(self, x): return x * torch.sigmoid(self.beta * x) class Mish(nn.Module): def forward(self, x): # softplus 内部默认会对大输入做阈值截断避免 exp 溢出 return x * torch.tanh(F.softplus(x)) # 快速对比一下三者在前向耗时上的差距 x torch.randn(64, 512, 56, 56, devicecuda) for name, fn in [(relu, F.relu), (swish, Swish()), (mish, Mish())]: torch.cuda.synchronize() import time; t0 time.time() for _ in range(200): fn(x) torch.cuda.synchronize() print(name, round((time.time() - t0) / 200 * 1000, 3), ms)这段对比代码跑出来的绝对数值取决于你的卡但相对关系很稳定Mish 大概是 ReLU 的三到四倍Swish 在两倍左右。这个数据在你决定要不要为了零点几个点换激活函数时比任何论文里的曲线都更有参考价值。5. Maxout 与 Softmax两个常被归错类的东西5.1 Maxout 是分段线性逼近器代价是参数翻倍Maxout 和前面所有函数都不是一个路数。它的形式是 $f(x) \max_{i \in [1,k]}(w_i^T x b_i)$也就是说它对输入做 $k$ 组不同的线性变换然后取最大值。这个操作本身没有固定的函数形状——它的形状是由学出来的权重决定的。理解 Maxout 的关键在于任意凸函数都可以用足够多的线性片段逼近。$k$ 组线性变换取最大等于用 $k$ 个超平面拼出一个分段线性的凸函数。当 $k$ 足够大时它能逼近 ReLU、Leaky ReLU、绝对值函数等等。所以它不是一个激活函数更像是一个激活函数的生成器。代价也很直接参数量变成 $k$ 倍。原来一个 $512 \to 512$ 的线性层有 26 万参数用了 $k2$ 的 Maxout 就变成 52 万。这还没算显存和计算量。所以在参数量敏感的场景里Maxout 基本没有生存空间。不过它在某些特定结构里有奇效比如需要极强非线性、层数又不敢堆太深的网络。我见过有人在小规模语音任务上用 $k2$ 的 Maxout 替代 ReLU在层数相同的情况下拟合能力确实更强。但这种收益很快会被多堆两层抵消掉所以它的实际使用场景很窄。class Maxout(nn.Module): def __init__(self, in_features, out_features, k2): super().__init__() self.k k self.out_features out_features self.lin nn.Linear(in_features, out_features * k) def forward(self, x): y self.lin(x) y y.view(*y.shape[:-1], self.out_features, self.k) return y.max(dim-1).values写这段代码时有个细节容易错view的维度顺序。如果你把k放在out_features前面取 max 的维度就错了得到的结果形状虽然一样语义完全变了而且不会报错。我第一次写的时候就栽在这训练 loss 一直比 ReLU 版本高排查了半天才发现是维度切错了。5.2 Softmax 是输出层的归一化不该出现在隐藏层把 softmax 和前面那些函数放在一起讲其实是历史原因——它确实满足非线性的定义形式上也是逐元素的指数运算加归一化。但它和隐藏层激活函数的功能完全不同softmax 的作用是把一组实数转成一个概率分布它跨维度耦合不是逐元素的。这个区别很重要。ReLU、sigmoid 这些是逐元素操作的每个输入独立算自己的输出维度之间互不影响。softmax 不是——它要对整个维度求和做分母任何一个元素变了所有元素的输出都跟着变。这意味着它不能随便塞在隐藏层里你会强行让隐藏层的每个位置去竞争总和为 1的配额等于给网络加了一个莫名其妙的约束。那为什么早期有些网络在隐藏层里用 softmax因为在没有批量归一化的年代softmax 的输出有界、均值稳定能起到一点类似归一化的作用。但这个功能现在被批量归一化、层归一化接替了没必要再用 softmax 干这活。5.3 Softmax 与交叉熵合并计算数值稳定性与重复计算Softmax 真正的用武之地是多分类的输出层。它的定义是 $\text{softmax}(z)_i \frac{e^{z_i}}{\sum_j e^{z_j}}$问题在于 $e^{z_i}$ 很容易溢出——$z_i$ 到 90 左右float32 的 exp 就爆了。标准做法是先减去最大值$\text{softmax}(z)_i \frac{e^{z_i - \max(z)}}{\sum_j e^{z_j - \max(z)}}$。因为分子分母同乘一个常数不影响结果减掉最大值后所有指数都不超过 0不会溢出而且分母至少有一项是 $e^0 1$不会出现除以零。真正的坑在这里绝大多数框架的交叉熵损失内部已经包含了 softmax。如果你在网络最后手动加了一层 softmax再喂给交叉熵损失等于做了两次 softmax。结果不是报错而是 loss 变小、梯度变小、收敛变慢你很难从现象上判断出原因。这个错误我见过太多次尤其是从教程代码改过来的人因为教程里经常把 softmax 显式写出来方便展示概率。import torch import torch.nn.functional as F logits torch.randn(8, 10) # 正确做法直接把 logits 传给交叉熵内部会做 log_softmax loss_ok F.cross_entropy(logits, torch.randint(0, 10, (8,))) # 错误做法先 softmax 再交叉熵等于双重归一化 probs F.softmax(logits, dim-1) loss_bad F.cross_entropy(probs, torch.randint(0, 10, (8,))) print(loss_ok.item(), loss_bad.item()) # 后者明显更小但不是好事如果你确实需要输出概率比如做阈值判断或者集成用F.softmax单独算别把它和损失函数串起来。另一个稳妥的写法是用F.log_softmax配合F.nll_loss效果和cross_entropy完全等价但中间结果更稳定。6. 一套可以直接照抄的选型与验证流程6.1 按任务类型和网络结构挑函数的决策表讲了这么多最后落到我该选哪个上。下面这张表是我自己项目里总结出来的覆盖了大部分常见情况。场景特征首选备选理由通用 CNN / 大模型隐藏层ReLUSwish计算最省配合 BN 收敛稳定深网络、学习率偏大Leaky ReLU ($\alpha$0.1)ELU避免死亡神经元容错性好数据量小、易过拟合R-ReLULeaky ReLU随机斜率自带正则效果需要每通道自适应P-ReLU ($C$ 维)Leaky ReLU表达能力强注意权重衰减精度卡住、可牺牲推理速度Mish / SwishELU平滑梯度实测小幅提升端侧部署、算力受限ReLULeaky ReLU单次比较运算无超越函数循环网络隐藏层tanhsigmoid门控零中心梯度比 sigmoid 好传二分类输出层sigmoid—配合 BCE 损失梯度不消失多分类输出层softmax或直接 logits—配交叉熵别手动加 softmax参数量不敏感的浅层网络Maxout ($k$2)—拟合能力强但参数翻倍这张表不是圣旨有几条边界值得补充。如果你的网络用了批量归一化激活函数的选择空间会大幅收窄——归一化已经处理了分布问题这时候 ReLU 和 Leaky ReLU 的差距往往被抹平选谁都能跑。反过来如果你的网络因为某些原因不能用归一化比如很小的 batch、序列长度差异极大激活函数的选择就变得非常关键这时候 Leaky ReLU 或者 ELU 明显更稳。另外还有一类情况值得单独提Transformer 类结构。这类模型里通常用 GELU 或者 Swish 而不是 ReLU原因和残差连接、层归一化的配合有关。我上面没把 GELU 单独列出来但它和 Swish 属于同一族都是平滑、非单调的自门控类理解了 Swish 就理解了 GELU。6.2 换激活函数之前必须做的三项自检很多人在换激活函数之后发现效果变差就直接放弃了其实是没做验证。换之前把这三项做一遍能省掉大量返工。第一项梯度检查。如果你自己实现了激活函数而不是用框架内置的一定要用数值梯度验证一遍解析梯度。框架通常提供gradcheck之类的工具思路是拿一个很小的扰动 $\epsilon$ 去逼近导数和解析导数对比。这个检查必须用双精度浮点做用单精度会因为舍入误差误报。from torch.autograd import gradcheck import torch class MyAct(torch.autograd.Function): staticmethod def forward(ctx, x): ctx.save_for_backward(x) return torch.where(x 0, x, torch.exp(x) - 1) staticmethod def backward(ctx, grad_out): (x,) ctx.saved_tensors grad torch.where(x 0, torch.ones_like(x), torch.exp(x)) return grad_out * grad x torch.randn(20, dtypetorch.double, requires_gradTrue) print(gradcheck(MyAct.apply, (x,), eps1e-6, atol1e-4)) # True 才算过第二项死亡神经元统计。前面给过代码。训练几个 epoch 之后统计每层的零通道比例如果某一层的比例随着训练单调上升并且超过 80%说明这一层正在死。这时候的处理方式不是立刻换激活函数而是先看学习率是不是偏大、初始化是不是没配好。Kaiming 初始化是给 ReLU 设计的用在 tanh 或者 sigmoid 上会出问题同理 Xavier 初始化配 ReLU 也不合适。这个对应关系经常被忽略。第三项混合精度下的数值溢出。如果你的训练用 float16指数类激活函数是重点排查对象。sigmoid 的朴素实现在输入到 -12 左右就溢出前面解释过ELU 的正半轴虽然是线性的不涉及指数但如果你的实现写成了对所有元素统一算 exp照样会爆。Mish 的 softplus 内部有阈值截断默认阈值 20超过就退化成线性这是框架帮你处理的自己实现时别漏掉。排查方法很简单在训练循环里加一个钩子定期检查激活输出的isinf().any()和isnan().any()。用 float16 训练时这两个检查应该成为默认动作成本极低但能救你一命。6.3 一个容易被忽略的细节激活函数和归一化的顺序最后说个和激活函数强相关但经常被搞混的点——卷积、归一化、激活三者的顺序。绝大多数情况下的标准顺序是卷积 → 归一化 → 激活。原因在于归一化需要看到卷积输出的原始分布如果你先做激活再做归一化等于把已经截断过的分布拿去统计均值和方差的估计会偏。但这个顺序也不是铁律。有些结构里会看到卷积 → 激活 → 归一化典型的是某些轻量化网络的变体目的是让激活后的稀疏性参与统计。这种写法能用但换结构时一定要重新调学习率因为归一化的输入分布变了原来那套超参不再适用。还有个更实际的坑如果你用的是带inplace参数的激活函数ReLU 系列都有在残差连接处会出问题。因为残差需要保留激活前的张量做加法而inplaceTrue会把它原地改掉反向传播时要么报错要么算出错误梯度。报错还好最怕的是某些框架版本静默通过结果精度莫名偏低。所以只要是残差结构inplace一律设成False别为了省那点显存给自己挖坑。我个人在实际操作中的体会是激活函数这一层的选择八成的功夫在选对大类上——先判断你的场景要不要平滑要就 Swish/Mish不要就 ReLU 家族、要不要避免死区要就 Leaky/P-ReLU剩下两成才是调 $\alpha$、$\beta$ 这些细节。把大类选错再怎么调参数也救不回来大类选对参数差一点影响有限。这也是为什么我更愿意花时间讲每个函数在补什么洞而不是给一堆公式让你背。

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

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

免费获取报价 →
↑