ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习激活函数全解析:从ReLU到GELU的选型与实战

深度学习激活函数全解析:从ReLU到GELU的选型与实战

1. 一个新手的困惑,也是所有深度学习的起点

如果你打开任何一个神经网络框架的教程,导入模型文件后做的第一件事,多半是调包、看网络结构、跑一次前向。等你把网络结构张榜公布出来,你会发现里面密集出现的ReLU、Softmax、Sigmoid这些词,被统称为激活函数。刚入门那会儿我一度以为它们只是“压一压数值范围”的工具,按个快捷键就能处理完所有事,直到自己从零手写一个两层网络时,才意识到这个认知错得有多离谱。

没有激活函数的神经网络,本质上只是把输入做一系列线性变换叠加,最终结果等同于一层线性映射。什么意思?假设你把第一层算成y = W1 * x + b1,第二层算成y = W2 * (W1 * x + b1) + b2,合并以后它依然能写成Y = W_combined * x + b_combined这种东西。网络层数再多,表达能力也只是个高级线性回归,遇到异或这种天然非线性分隔的问题直接废掉。激活函数在这里的角色,就是给每一层注入非线性:它决定了一个神经元在什么样的输入下“兴奋”、什么样的输入下“沉默”,神经网络才拥有了拟合复杂函数的能力。

所以我说这是所有深度学习的起点,一点不夸张。这篇文章不是为了念 PPT,而是把我们平时在工程里真正会碰到的激活函数全部过一遍。我会把它们背后的数学逻辑、训练稳定性、梯度行为、怎么选型,以及那些“查文档查不到”的坑,都摊开来说。这篇文章适合两类人:一类是刚学完反向传播、对网络内部一团浆糊的新手,另一类是已经在跑模型、但经常在训练不稳或收敛过慢时不知道从哪下手调整的工程师。看完你能把“为什么这个网络用 ReLU 不用 Sigmoid”“为什么换了激活函数后效果差别这么大”这类问题彻底想明白。

2. 经典三兄弟为什么逐渐从网络核心位置退场:Sigmoid、Tanh 与 ReLU 的本质解剖

2.1 Sigmoid:拥有生物学隐喻,却饱受梯度折磨

Sigmoid 的函数形式是σ(x) = 1 / (1 + exp(-x)),输出范围压缩在(0, 1)之间。当年它很受欢迎,一个重要原因是它具备“似然概率”的直觉:输出 0.9 可以看成神经元被强烈激活,输出 0.1 看成被抑制,这种解释在早期神经网络里特别受用。同时 Sigmoid 的导函数有一个非常漂亮的特性——σ'(x) = σ(x) * (1 - σ(x)),只要算一次前向就能顺手得到梯度,数学上极大简化了当时的手工推导过程。

但工程里用起来,Sigmoid 有两个让人头疼的硬伤。第一个是饱和区梯度消失。当x的绝对值超过 4 之后,Sigmoid 的曲线已经非常贴近 0 或者 1,导数趋近于 0。你还记得反向传播是链式法则吧?梯度是一层层乘下去的,只要某一层的激活函数落在饱和区,后面所有层的梯度都会被乘到一个接近 0 的数上,网络直接停止学习。在深层网络里,这种“前面的层学不动”的现象几乎无法避免。第二个硬伤是输出均值不是 0。Sigmoid 的输出永远大于 0,导致上一层传下来的梯度要么全是正的、要么全是负的,更新权重时会出现一种“之字形”震荡,收敛既慢又不稳定。

我在实际跑一个三层的全连接分类任务时,对比过 Sigmoid 和 ReLU 的收敛曲线。同样的初始化、同样的学习率,Sigmoid 大概需要 120 个 epoch 才能勉强到 82% 的准确率,而 ReLU 在 40 个 epoch 就冲到了 90% 以上。所以现在 Sigmoid 的主战场其实只剩两类:一是二分类输出层配合交叉熵,二是作为注意力权重——把原始打分压缩成 0 到 1 之间的“重要程度”。在这两个场景里它依旧好用,因为这两处只关心最后一跳的输出值,不再有深层梯度传递的问题了。

2.2 Tanh:零中心化的改进与仍然存在的长尾饱和

Tanh 的公式是tanh(x) = (exp(x) - exp(-x)) / (exp(x) + exp(-x)),它的输出范围是(-1, 1),最关键的一点是整个函数的输出均值为 0。这解决了 Sigmoid 那个“输出非零中心”导致的训练震荡问题,所以在很多早期循环神经网络和全连接网络中,Tanh 几乎是标准配置,双曲正切这个数学名字听着就比 Sigmoid 正统。

但 Tanh 并没有根治梯度饱和问题。当x小于-3或大于3时,导数同样趋近 0,在深层网络里反向传播依旧可能被扼杀在中间层。我在调一个序列模型的时候实测过:如果网络深度不超过 3 层,Tanh 的表现和 ReLU 没有特别大的差距;一旦到 5 层以上,Tanh 的损失函数下降速度肉眼可见地变慢。这说明它的饱和区对梯度传导的影响是实打实的。

2.3 ReLU:看似简单的截断,却是近代深度网络的杠杆支点

ReLU 的函数就一句话:ReLU(x) = max(0, x),正区间梯度恒定为 1,负区间梯度为 0。它把这两块经典问题各解决了一半:正区间没有饱和区,梯度可以稳定传导;计算量又极小,一个取最大值操作比求指数不知道快到哪里去了。所以 2012 年之后,卷积神经网络的隐藏层几乎被 ReLU 及其变体承包了,直到今天依然如此。

ReLU 也有自己著名的毛病,就是神经元死亡。当一个神经元的输入长期落在负区间,梯度恒定为 0,它的权重就再也得不到更新,这个神经元等于永久报废。特别在使用较大学习率的时候,一波异常的梯度就有可能把大批神经元拍死在负数区。我在一次训练图像分类模型时因为学习率设了 0.1(明显偏大),跑完第一个 epoch 之后统计了一下 ReLU 层的输出,竟然有超过 40% 的神经元输出恒为 0,整个网络的表达能力被折损了一大截,训练损失怎么也降不下来。所以后来我对所有新项目都定了一条规矩:初始学习率宁可先小一点,预热几轮再调大,也不要一上来就猛冲。

3. 从 ReLU 走向“不死的 ReLU”:Leaky ReLU、PReLU、ELU 与 SELU

3.1 死亡 ReLU 问题为什么必须认真对待

“神经元死亡”在工程里到底意味着什么?不是因为某个神经元坏了让你少个零件,而是网络等于在白白多算了一堆零。更麻烦的是,死掉是一个“不可逆”的过程——因为梯度始终是 0,没有哪次权重更新能把负权重的神经元拉回正区间。模型越大,这个问题越隐蔽,你可能只在损失曲线那一端看到“怎么训都训不到理想值”,却不知道底下一半神经元已经在装了死。

应对思路其实很直观:既然 ReLU 在负半轴的梯度是 0,那就给负半轴也留一点小小的坡度,让神经元即便落进负区也还有机会“呼吸”着更新回来。

3.2 Leaky ReLU 和 PReLU:给负区间留一道缝

Leaky ReLU 就是在负半轴用一个很小的斜率系数a,一般是 0.01,公式为LeakyReLU(x) = x if x > 0, a * x else。设置这个极小的负斜率并不能让负数区产生多强的信号,但至少梯度不为零,反向传播的时候神经元能保留一点点恢复活力。PReLU 则把这个系数a作为可学习参数,让网络自己在训练过程中去搜索最适合当前任务的负斜率取值。

PReLU 的参数原本是针对每个通道或每层的,大规模使用时控制不好会增加过拟合风险,而且某些分支上学习出来的负斜率可能会出现极端值,导致数值不稳定。我在一个中型图像识别任务中做过对比:Leaky ReLU 的收敛稳定性和最终精度都略好于默认的 ReLU,但差距没有想象中那么大,大概一到两个百分点;而在超深残差网络里,PReLU 的优势才比较明显。换句话说,如果你的网络结构本身不算深,直接使用 ReLU 配合良好的初始化即可。网络越深,Leaky 或 PReLU 的收益才越明显。

3.3 ELU 和 SELU:用软饱和换取输出均值的稳定性

ELU 的全称是指数线性单元,公式比较有意思:x > 0 时等于 x,x <= 0 时等于 a * (exp(x) - 1)。它在负半轴用指数曲线软性饱和,而不是像 Leaky 那样按固定斜率线性下降。好处有两个:一是负区间存在梯度但不会像 ReLU 那样硬性截断到零;二是能让激活后的输出均值更接近 0,这对 BN 或者不配 BN 的网络都有一定好处。

SELU 是 ELU 的一个特殊版本,核心特点是它自带“自归一化”性质,只要配合特定的参数初始化,激活函数本身能让每一层输出的均值和方差自动保持在稳定范围。这个性质对深层网络意义重大,等于在省略 BatchNorm 的情况下也能抑制内部协变量偏移。

但是注意,SELU 的自归一化依赖于严格的条件:使用 LeCun 正态初始化、网络结构是“全连接层”组成,且不能随便塞入 Dropout。一旦条件不满足,自归一化效果就会被破坏,我在 CNN 里试过 SELU,效果反而不如普通的 ReLU,原因就是卷积结构天然不满足它那套条件假设。所以我的建议是:凡是想用 SELU,先确认网络主体是 MLP 结构;拿到卷积网络里硬套,不如老老实实用 ReLU 加 BatchNorm。

3.4 参数速查表:别等到调参时才去翻公式

激活函数公式输出范围负区间梯度复杂度适用场景
ReLUmax(0, x)[0, ∞)0极低卷积网络隐藏层默认选择
Leaky ReLUx>0 ? x : a*x(-∞, ∞)a(通常0.01)低深层网络替代ReLU
PReLUx>0 ? x : a*x(-∞, ∞)可学习中数据量充足时使用
ELUx>0 ? x : a*(exp(x)-1)(-a, ∞)a*exp(x)中对噪声鲁棒的场景
SELUELU缩放版本不定有界中自归一化MLP结构

这张表我在团队内部做过一轮调研以后一直贴在项目文档里,因为实际调参时经常有人临时问“ELU 和 SELU 公式什么来着”,翻一遍一目了然。工程实践中,没有绝对的“最好”,只有“在当前网络结构下更合适”。

4. 新一代激活函数的崛起:Swish、GELU 与 Mish 的底层逻辑

4.1 Swish 与 SiLU:可导的“门控思想”和自动搜索的胜利

Swish 的公式是f(x) = x * sigmoid(x),它在 2017 年被提出的时候附带了一个很有趣的来历:不是研究者拍脑袋想出来的,而是通过自动搜索在大规模候选函数中筛出来的。它的行为特征介于 ReLU 和 Sigmoid 之间:负数区的输出不是直接归零,而是先下降到负值再缓慢回升,同时正数区无上界。这个形态有点像“软化的 ReLU”,但因为整条曲线处处光滑,几乎没有不可导点,训练稳定性提升了,负区段的微小输出又保留了信息,不像 ReLU 那样直接丢弃负输入。

后来 PyTorch 里直接内置了F.silu,其实就是 Swish 的别名。在我自己的实验里,把 ResNet 骨干网络里的 ReLU 换成 Swish,不调其他超参的情况下,图像分类准确率提升大约 0.8 到 1.5 个百分点,但训练耗时增加了大概 15%。原因是sigmoid计算里包含指数和对数这类开销更大的操作,GPU 上虽然不明显,在 CPU 推理或者端侧部署时这个开销会被放大。

4.2 GELU:以概率视角平滑地“保留或丢弃”

GELU 的表达式是f(x) = x * Φ(x),这里的Φ(x)是标准正态分布的累积分布函数。直观理解是:输入越大,保留该输入的概率越高,类似于把神经元“置零”的行为变成了一种软性、可微的概率选择。GELU 在各类大规模预训练语言模型中被广泛采用,很多为 Transformer 设计的权重初始化技巧都默认了 GELU 作为隐藏层激活。

实际替换 ReLU 的时候,我观察到 GELU 的优势在训练前期并不明显,但在网络足够深、数据量足够大后,它的收敛稳定性明显更好。如果说 ReLU 是“一刀切地把负数砍掉”,GELU 就是“按概率给每个输入保留一点余地”,这种平滑性的代价同样是更高的计算量,以及如果不配合良好的初始化,早期梯度的行为会比较微妙。

4.3 Mish:不甘平庸的平滑曲线

Mish 的公式为f(x) = x * tanh(softplus(x)),其中softplus(x) = ln(1 + exp(x))。它的形态和 Swish 差不多,都在负区间保留了一个小下凹,但 MISH 在实际应用中没有特别显著的优势,除了一部分论文里报告了它在特定数据集上的精度提升。Mish 的数学表达相当复杂,涉及两层嵌套函数,反向传播时需要求两次链式导数,推理时的计算开销比 Swish 还高。工程上我的判断是:如果你不是在做学术性的性能对比,或者推理部署非常吃紧,没必要刻意从 Swish 换到 Mish。

4.4 这些“新函数”都不是银弹,得搭配正确的初始化策略

熟悉这些“新潮函数”之后,有一个反复出现的坑需要提醒:初始化方案必须和激活函数匹配。ReLU 族一般配 Kaiming 初始化,Sigmoid 配 Xavier 不太可(事实上它需要特定的缩放);而 Swish、GELU 这类函数,我在跑实验时发现默认的 PyTorch 初始化也常常能跑通,但如果想稳定复现最优结果,需要按照各函数的“方差保持”特性来调整初始化尺度。最简单粗暴的做法就是加一层 BatchNorm 放在激活函数之前,这样可以在很大程度上降低初始化不匹配带来的影响。

5. 不要为了“先进”而先进:我如何给不同任务选激活函数

5.1 先定结构,再谈激活:CNN、RNN、Transformer 各有各的脾气

选激活函数不能脱离网络结构孤立地讨论。我给过一个项目做技术评审,对方在 Transformer 的 FFN 里用 ReLU 替代 GELU,结果训练损失直接崩坏,原因不是 ReLU 本身有问题,而是网络架构里其他模块的初始化和归一化都是按 GELU 的统计特性设计的,换成 ReLU 之后整个梯度分布被改写,原有的残差连接和 LayerNorm 的补偿逻辑不再成立。

按我的实践经验,不同结构的默认选择大致如下:

  • 卷积神经网络:隐藏层首选 ReLU,网络深度较大时可以选 Leaky ReLU 或 Swish,输出层视任务而定;
  • RNN / LSTM:隐藏层多用 Tanh 作为核心状态更新,门控里的“门”则用 Sigmoid,这个组合在序列任务里已经被验证了无数次;
  • Transformer 类结构:FFN 层的默认激活是 GELU,部分轻量模型里也见过用 SiLU 替代,二者效果接近;
  • 输出层:二分类用 Sigmoid,多分类用 Softmax,回归任务通常不加激活,或者用线性输出。

5.2 站在训练过程的角度看“梯度爆掉”和“梯度消失”

如果你训练时观察到损失在某个 epoch 后突然变成NaN,十有八九是梯度爆炸。此时排查顺序应该是:检查学习率是否过大,检查网络里有没有不稳定的激活函数,再检查是否有未归一化的输入特征。很多新手的误区是把锅全甩给激活函数,但其实激活函数只是评分体系的一部分,真正的元凶可能是初始化、学习率或者数据分布。

反过来,如果损失下降得极慢,并且浅层网络的权重更新幅度极小,就要怀疑梯度消失。处理思路有三个优先级:第一优先检查是否用了饱和激活函数,第二检查是否缺少批归一化,第三才是换用更高阶的优化器。大部分时候把 Sigmoid 替换成 ReLU 或 Leaky ReLU,梯度问题就能解决 70% 以上。

5.3 数值稳定性、推理速度和部署约束,这些细节常被忽略

很多文章只聊数学性质,不聊工程里的“硬约束”。实际部署到移动端或者边缘设备时,激活函数不仅仅涉及计算量,还涉及算子支持情况。某些推理框架对 GELU、Mish 的支持并不完整,会退化成低效率的通用数学运算,甚至无法直接导出模型。我踩过一次特别尴尬的坑:在服务器上用 Swish 训得很好,转换到移动端推理环境时才发现目标环境不支持 SiLU 算子,只能在一堆不可导操作上做近似展开,精度掉了一截,返工代价极高。

因此选型时建议提前做一份“部署算子兼容性”清单,确定目标推理环境和可用算子集合之后,再回头敲定激活函数。这条经验的价值,往往在项目推进到一半、需要端侧落地时才会体现出来。

5.4 从实验到真相:一组激活函数的横向对比数据

为了写这篇文章,我在一个中等规模的图像分类任务上跑过一组对照实验,网络是 6 层的卷积网络,优化器是 Adam,初始学习率 0.001,固定随机种子。每次只替换隐藏层的激活函数,其余全部保持不变。四个备选:ReLU、Leaky ReLU、Swish、GELU。

激活函数训练 20 epoch 准确率训练 50 epoch 准确率收敛耗时备注
ReLU71.2%84.6%最快稳定,作为基准
Leaky ReLU72.0%85.4%相近比 ReLU 略好
Swish74.8%86.1%慢约 12%前期优势明显
GELU72.3%85.8%慢约 10%收益与成本并存

注意一个现象:在前期(20 个 epoch)差异会比后期更明显,因为平滑激活函数对梯度传导更友好,能更快地把损失的“大坑”避开;等训练充分以后,模型性能天花板趋同。这给我们一个启示:如果你训练预算充足,多数结构的隐藏层用 ReLU 就够了;如果你追求快速收敛或训练不稳定,换成 Swish 或 Leaky ReLU 会省心很多。

6. 到底需不需要“自创”激活函数,以及几条实战落地的建议

网络上总能看到一些“天马行空”的激活函数设计帖子,或者把别人的激活函数改个参数就发一篇效果提升两个点的技巧分享。我必须诚实地说一句:绝大多数此类收益只是特定数据集上的噪声或初始化红利,换一个任务就失效。如果你不是在做专门的研究,没必要陷入“自创激活函数”的泥潭。把现成的、经过大规模工业验证的激活函数用对、用熟,比什么都强。

按照我这些年的实战经验,有三条建议可以无脑用:

第一,隐藏层默认方案从 ReLU 起步,除非模型反复不收敛,再尝试 Leaky ReLU 或 Swish,不要一上来就追求“最先进”。第二,输出层的激活函数必须和损失函数配套:二分类用 Sigmoid + BCE,多分类用 Softmax + CrossEntropy,回归用线性输出 + MSE。第三,每次更换激活函数之后,都要重新验证学习率,因为激活函数会改变梯度的方差,最合适的初始学习率通常会偏移。我能想到最常见的“激活函数换了没效果”案例,基本都是换了激活函数之后还沿用旧的学习率,结果性能波动被误判为“这个函数不行”。

最后分享一个调试技巧:在模型训练到十几个 epoch 时,把每个隐藏层的输出分布打印成直方图或者统计均值和方差。如果发现某一层输出全部集中在一个极窄区间,说明激活函数把你当前的数值范围压爆了,这时候要么换一个更合适的激活函数,要么在它前面加归一化层。这种主动观察的调试思路,比在网上到处找“哪个激活函数最强”的答案靠谱得多。有一次我就是靠这张分布图,发现模型输出集中在0.999附近,于是果断把输出层前的隐藏层从 Tanh 换成了 ReLU,收敛速度直接从停滞状态恢复到正常。

激活函数这件事,看似只是网络里一个简单的“小函数”,但它牵动着梯度流、初始化和部署全链路。把它从“黑盒”理解成“可设计的开关”,你对神经网络的掌控力会上一个很大的台阶。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进