土法炼钢兴趣小组的算法知识备份

【Transformer 与注意力机制】05. 激活函数:让网络「弯下来」的非线性魔法

文章导航

分类入口
transformer
标签入口
#transformer#激活函数#ReLU#GELU#SwiGLU#非线性

目录

一、上一篇留下的悬念

上一篇我们论证了一件事——纯线性的网络再深,也只是一个线性变换。把 \(W_2(W_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2\) 展开就是 \(W'\mathbf{x} + \mathbf{b}'\)。线性的复合还是线性,这是线性代数的铁律。

所以神经网络要想拟合「弯曲的」关系,必须引入非线性。引入的方式很巧妙——不是改 \(W\mathbf{x} + \mathbf{b}\) 的形状(那样就不再是线性变换了),而是在两个线性变换之间插入一个逐元素的非线性函数。这个函数叫激活函数(activation function)。

形式化地,一个典型的「线性 + 非线性」三明治长这样:

\[\mathbf{h} = \sigma(W_1 \mathbf{x} + \mathbf{b}_1)\] \[\mathbf{y} = W_2 \mathbf{h} + \mathbf{b}_2\]

其中 \(\sigma\) 是激活函数,逐元素作用——也就是说 \(\sigma(\mathbf{z})\) 表示对 \(\mathbf{z}\) 的每一个元素分别应用 \(\sigma\)

这一篇我们就来仔细看看 \(\sigma\) 这个角色。它看起来微不足道——一个一维标量函数嘛,能有多复杂?但偏偏,激活函数的选择直接决定了深度网络能不能训得动、训得好。一个不合适的激活函数,再大的模型再多的数据都救不了;一个合适的激活函数,能让原本训不动的网络突然焕发生机。

我们会按时间顺序讲:Sigmoid(最早)→ Tanh(改进)→ ReLU(革命)→ Leaky/ELU/SELU(修补)→ Swish/GELU(现代标配)→ SwiGLU/GeGLU(大模型时代)。每一个的诞生都对应着一个时代的痛点,每一次迭代都解决了前一代的某个核心问题。我希望你读完之后,不仅记住每个函数长什么样,更能体会到激活函数演化的逻辑——从生物启发到数值优化,从局部小改到大模型时代的全新设计。

二、为什么必须是「逐元素」的?

这个细节很多教材会一笔带过,但其实值得停下来想一下。

激活函数作用在向量上,但它是「逐元素」作用——也就是说,对向量 \(\mathbf{z} = (z_1, z_2, \cdots, z_h)\),我们有:

\[\sigma(\mathbf{z}) = (\sigma(z_1), \sigma(z_2), \cdots, \sigma(z_h))\]

每一个分量独立处理,互不干扰。这意味着 \(\sigma\) 本质上只是一个一维函数 \(\mathbb{R} \to \mathbb{R}\),被「广播」到向量上。

为什么要这样设计?为什么不用一个「真·向量到向量」的非线性函数(比如对向量做一些复杂的混合)?

有两个原因。

第一,计算简单。逐元素操作可以完美并行——每个元素分配到 GPU 的一个线程,独立计算。如果用复杂的混合非线性,并行度会被破坏。

第二,线性变换已经在做混合了\(W\) 矩阵的每一行都是对输入向量的线性组合。也就是说,「不同维度间的相互作用」这件事,已经由 \(W\) 解决了。激活函数不需要再重复做。

所以神经网络的分工是:\(W\) 负责「不同维度间的混合」(线性,跨元素),\(\sigma\) 负责「单个维度上的弯曲」(非线性,元素内)。两者各司其职,组合出强大的表达能力。

后面我们会看到,这个分工有一个例外——Softmax。Softmax 是非逐元素的非线性(它需要对整个向量归一化),所以它有特殊的地位。但常规激活函数都是逐元素的。

三、Sigmoid:第一代主角

最早的激活函数是 Sigmoid(也叫 Logistic 函数):

\[\sigma(z) = \frac{1}{1 + e^{-z}}\]

它的形状是一条「S」曲线:当 \(z \to -\infty\) 时趋于 0,当 \(z \to +\infty\) 时趋于 1,在 \(z = 0\) 处等于 0.5,左右对称。

Sigmoid 为什么是第一代?因为它有几个让人喜欢的性质。

性质一:输出在 \((0, 1)\) 之间。这个区间天然像「概率」。所以 Sigmoid 在二分类的输出层用得很多——「这是猫的概率」之类。

性质二:处处可导。Sigmoid 处处光滑,没有任何尖角。它的导数还有一个漂亮的形式:

\[\sigma'(z) = \sigma(z)(1 - \sigma(z))\]

也就是说,导数可以由函数值本身算出来——前向算了 \(\sigma(z)\),反向算梯度时直接乘 \(\sigma(z)(1-\sigma(z))\),不用重新算 exp。这在没有自动微分的早期是个工程优势。

性质三:受生物启发。神经元有「激发阈值」——刺激不够就不发放,刺激够了就饱和发放。Sigmoid 的形状刚好像「阈值激发函数的光滑版」。早期人们觉得这种「生物味」是好事。

但 Sigmoid 也有几个致命的缺点。这些缺点直到 2010 年代才被深度学习社区集体抛弃。

四、Sigmoid 的致命问题:梯度消失

Sigmoid 的导数 \(\sigma'(z) = \sigma(z)(1 - \sigma(z))\),最大值出现在 \(z = 0\) 处,等于 \(0.5 \times 0.5 = 0.25\)最大值只有 0.25。当 \(|z|\) 大一点(比如 \(z = 5\)),\(\sigma(z) \approx 0.993\),导数 \(\approx 0.993 \times 0.007 \approx 0.007\)——基本上等于 0。

这意味着什么?意味着 Sigmoid 在「饱和区」(\(|z|\) 较大)的梯度极小

而梯度在反向传播时是「逐层相乘」的。链式法则告诉我们:

\[\frac{\partial L}{\partial W_1} = \frac{\partial L}{\partial \mathbf{h}_L} \cdot \prod_{l} \frac{\partial \mathbf{h}_{l+1}}{\partial \mathbf{h}_l}\]

每一层都要乘一次激活函数的导数。如果每层导数最大才 0.25,10 层之后梯度被乘了 \(0.25^{10} \approx 10^{-6}\)。再乘点权重矩阵,基本就是 0 了。

这就是著名的梯度消失问题(vanishing gradients)。早期网络的浅层(靠近输入)几乎学不到东西——梯度还没传到那里就消失了。1990 年代到 2000 年代,整个深度学习领域被这个问题困住了十几年。

我以前读这段历史,觉得不可思议——一个看起来这么显然的问题,居然困住了一代人。但仔细想想,「显然」是事后的视角。当时人们没有 PyTorch 的可视化工具,没有 TensorBoard 的梯度直方图。他们只能盯着训练曲线发呆——「为什么深网络比浅网络还差?」直到 Hinton 等人在 2006 年用「逐层预训练」绕开这个问题,再到 2012 年 ReLU 直接消解这个问题,整个领域才打开局面。

五、Sigmoid 的另一个问题:输出不以 0 为中心

Sigmoid 的输出范围是 \((0, 1)\)全是正数。这看起来无害,其实有一个微妙的问题——梯度更新会有 zigzag

简单解释:假设第 \(l\) 层的激活是 \(\mathbf{h}_l = \sigma(\cdot)\),全是正数。那么下一层 \(W\) 的梯度更新方向,会受 \(\mathbf{h}_l\) 的符号约束——所有 \(W\) 的元素的梯度同号(要么都正、要么都负)。这意味着参数只能朝某些「象限」移动,要去其他象限就得绕路,形成 zigzag 的更新轨迹。

这听起来抽象,但实际表现就是收敛慢、训练不稳定。

这个问题被叫做「非零中心化」(not zero-centered)问题。要解决它,激活函数的输出最好以 0 为中心,左右对称分布。

六、Tanh:Sigmoid 的孪生兄弟

为了解决「非零中心化」,人们找到了 Tanh:

\[\tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}}\]

它和 Sigmoid 是孪生兄弟——形状几乎一模一样,只是范围从 \((0, 1)\) 拉到了 \((-1, 1)\),并且关于原点对称。事实上:

\[\tanh(z) = 2\sigma(2z) - 1\]

Tanh 解决了 Sigmoid 的「非零中心化」问题。RNN 时代(90 年代到 2010 年代初),Tanh 是 LSTM 和 GRU 内部的标准激活——你今天打开任何一个 RNN 实现,里面到处都是 tanh。

但 Tanh 没有解决梯度消失问题。它的导数 \(\tanh'(z) = 1 - \tanh^2(z)\),最大值是 1(在 \(z = 0\) 处),但两端饱和时仍然趋于 0。所以 Tanh 比 Sigmoid 好一点,但深度网络仍然训不深。

到这里,「梯度消失」已经像一座大山压在深度学习的头上。整个领域亟需一个全新的激活函数。

七、ReLU:一记神来之笔

2010 年前后,一个看似「大道至简」的激活函数横空出世——

\[\text{ReLU}(z) = \max(0, z)\]

「整流线性单元」(Rectified Linear Unit)。它的形状简单到不像「现代神经网络」该有的东西:负数砍成零,正数原样保留。一条折线,仅此而已。

但就是这样一个朴素的函数,彻底改变了深度学习

ReLU 的优势:

第一,正区间梯度恒为 1。在 \(z > 0\) 时,\(\text{ReLU}'(z) = 1\)梯度不会衰减。这意味着深层网络的梯度可以原样传到底层。一记神来之笔

第二,计算极快max(0, z) 是一个 if-else 分支,比 exp 快几十倍。这在 GPU 上是巨大优势。

第三,稀疏性。负输入直接归零,意味着每次前向时,约一半神经元处于「不激活」状态。稀疏激活对生物神经元而言是常态,对网络的容量也有帮助。

第四,不饱和。Sigmoid/Tanh 在 \(|z|\) 大时饱和(输出贴近边界,梯度趋零);ReLU 在正区间永不饱和。

ReLU 不是 2010 年才被发明的。最早 Hahnloser 在 2000 年的论文里就提到过它。但真正让它流行起来的,是 2010 年 Glorot & Bengio 的《Deep Sparse Rectifier Neural Networks》,以及 2012 年 Krizhevsky 用 ReLU 训出的 AlexNet——一举打破了 ImageNet 的所有纪录,深度学习从此进入「ReLU 时代」。

八、ReLU 的「副作用」:Dying ReLU

ReLU 这么好,有缺点吗?有。最有名的叫Dying ReLU(死亡 ReLU)。

如果某个神经元的输入 \(z\) 始终为负,那么 ReLU 输出始终为 0,对应的梯度也是 0。梯度为 0 意味着参数不会更新这个神经元就「死了」——再也不会激活,再也不会学习

死亡 ReLU 在训练初期最容易发生:参数初始化不好,或者学习率太大,导致某些神经元的输入被推到一直为负,从此再也救不回来。研究发现,一个训练完的 ReLU 网络里,可能有 10%-40% 的神经元已经死了。

这是 ReLU 的「副作用」,但通常不致命——其他 60%-90% 的神经元仍然在工作,整体性能仍然很好。但在一些极端情况下,死亡 ReLU 会显著降低模型容量。

为了缓解这个问题,人们提出了一系列「ReLU 变种」。

九、Leaky ReLU 与 PReLU:让负数也有一点点导数

第一个补丁是 Leaky ReLU

\[\text{LeakyReLU}(z) = \begin{cases} z & z > 0 \\ \alpha z & z \le 0 \end{cases}\]

这里 \(\alpha\) 是个小常数,通常取 0.01。负数区间也有一个小斜率,梯度不再为零。即使神经元的输入长期为负,参数也仍然在更新——它有机会「复活」。

PReLU(Parametric ReLU)把 \(\alpha\) 也变成可学习参数,每个神经元自己学一个最合适的负斜率。He 等人的工作(2015)显示这在某些任务上略有提升。

Leaky/PReLU 实战中的表现……不算特别好。它们解决了 Dying ReLU 的理论问题,但实际收益不太明显。多数大模型仍然用普通的 ReLU 或后面要讲的 GELU,而不是 Leaky 系。

十、ELU 与 SELU:试图同时解决多个问题

ELU(Exponential Linear Unit, Clevert et al. 2015)走得更远:

\[\text{ELU}(z) = \begin{cases} z & z > 0 \\ \alpha(e^z - 1) & z \le 0 \end{cases}\]

ELU 在负区间用 \(\alpha(e^z - 1)\),让输出能取到接近 \(-\alpha\) 的负值。这有两个好处:

  1. 零中心化——输出可以是负数,整体均值可以接近 0。
  2. 平滑——在 \(z = 0\) 处可导(一阶导连续),不像 ReLU/Leaky 有尖角。

SELU(Scaled ELU, Klambauer et al. 2017)在 ELU 基础上加了一个缩放因子 \(\lambda\)

\[\text{SELU}(z) = \lambda \cdot \text{ELU}(z)\]

并精心选取 \(\lambda \approx 1.0507\)\(\alpha \approx 1.6733\),使得网络在前向传播时自归一化——激活的均值和方差自动保持在 0 和 1 附近。理论很漂亮,工程上也省了 BatchNorm。

但 SELU 的限制很多(要求特定权重初始化、特定层结构),使用门槛高。最终它没能成为主流。

ELU/SELU 的故事告诉我们一件事——激活函数的「数学优雅」不一定等于「工程实用」。一个函数能不能流行,要看它在大量真实任务上的综合表现,不只是看它有多少漂亮的性质。

十一、Swish 与 SiLU:从神经架构搜索来的「光滑 ReLU」

2017 年,Google 用神经架构搜索(NAS)寻找更好的激活函数。算法跑了几天,吐出了一个让人意外的结果:

\[\text{Swish}(z) = z \cdot \sigma(z)\]

其中 \(\sigma\) 是 Sigmoid。这个函数也叫 SiLU(Sigmoid Linear Unit)。

它的形状像「光滑版的 ReLU」——大于 0 时趋近 \(z\),小于 0 时趋近 0,中间过渡平滑、非单调(在某个负值附近有个小凹陷)。

Swish 的优势是「」——不像 ReLU 在 0 处有尖角,Swish 处处光滑。这对优化曲面有微妙的好处,让训练更稳。

更妙的是,Swish 的形式非常简单——只是 \(z\)\(\sigma(z)\) 的乘积,几乎不增加计算量。

Swish 在 EfficientNet 等模型里效果不错,逐渐流行起来。但更广为人知的是它的「亲戚」——GELU。

十二、GELU:Transformer 的标准激活

GELU(Gaussian Error Linear Unit, Hendrycks & Gimpel, 2016)的形式是:

\[\text{GELU}(z) = z \cdot \Phi(z)\]

其中 \(\Phi(z)\) 是标准正态分布的累积分布函数(CDF)。

直观地讲,GELU 把 \(z\) 乘以「\(z\) 是正的概率」(在标准正态假设下)。如果 \(z\) 很大,概率接近 1,GELU 输出 \(\approx z\);如果 \(z\) 很负,概率接近 0,GELU 输出 \(\approx 0\)。它和 ReLU 的差别在中间——ReLU 是「硬开关」(要么 0 要么 1),GELU 是「软概率」(连续从 0 过渡到 1)。

GELU 的精确公式比较麻烦,所以工程实现常用近似:

\[\text{GELU}(z) \approx 0.5 z \left(1 + \tanh\left(\sqrt{\frac{2}{\pi}}(z + 0.044715 z^3)\right)\right)\]

PyTorch 提供了精确版(用 erf 函数)和 tanh 近似版两种。

GELU 的真正分量在哪里?——它是 Transformer 的标准激活函数。从原始的 BERT 论文(2018)开始,几乎所有后续 Transformer 模型(GPT-2、T5、ViT、CLIP 等)都用 GELU。它和 Swish/SiLU 形状几乎一样,效果也接近,但在 Transformer 上 GELU 的实证表现略好,于是成为 de facto 标准。

我自己的理解:GELU 在 ReLU 和 Swish 中间找到一个甜蜜点——它是「带概率解释的 Swish」「光滑版的 ReLU」。它没有 ReLU 的尖角问题,又有 ReLU 的稀疏激活倾向,还多了一个「概率」的诠释(虽然这个诠释主要是事后归因)。

十三、SwiGLU:LLaMA 时代的新王

Transformer 的 FFN 部分原本是这样:

\[\text{FFN}(x) = W_2 \cdot \text{GELU}(W_1 x + b_1) + b_2\]

这是一个「线性 → 激活 → 线性」的标准结构。

LLaMA(2023)和它的后继者用了一个不一样的结构——SwiGLU(Shazeer 2020 提出,LLaMA 2 普及):

\[\text{SwiGLU}(x) = (\text{Swish}(W_1 x) \odot (W_3 x)) W_2\]

这里 \(\odot\) 是逐元素相乘。

注意它有三个权重矩阵 \(W_1, W_2, W_3\),比标准 FFN 多了一个 \(W_3\)。整体结构是:

  1. \(W_1\) 投影一次,过 Swish(其实就是 SiLU)。
  2. \(W_3\) 投影一次,不过激活
  3. 把两个结果逐元素相乘——这是「门控」(gating)。
  4. 再过 \(W_2\) 投影回去。

这种「用一路当门控乘另一路」的设计叫门控线性单元(GLU, Gated Linear Unit)。最早出现在 Dauphin 等人的 LSTM 改造里,2017 年被引入 Transformer,2020 年 Shazeer 系统比较了各种 GLU 变种(GeGLU、ReGLU、SwiGLU 等),SwiGLU 综合表现最好。

LLaMA 把 SwiGLU 用了之后,开源社区基本一边倒地跟上——Mistral、Qwen、DeepSeek、Yi 都用 SwiGLU。它已经替代 GELU 成为现代 LLM 的 FFN 标准

为什么 SwiGLU 比 GELU 好?没有非常完整的理论解释。Shazeer 在论文最后一段写了一句很有名的话:「我们没有理由解释为什么这些架构有效。我们把它归因于神圣的恩赐(divine benevolence)」——这是激活函数研究里的一句神回复,意思是「就是 work,问就是玄学」。

但有一些直觉可以建立:

  1. 门控带来更强的表达能力。SwiGLU 比 GELU 多一倍的参数量(多了一个 \(W_3\)),所以表达力更强。
  2. 门控让网络能动态决定信息流——某些维度被门「关上」,某些被「打开」。这种数据相关的稀疏性比 ReLU 的固定稀疏更灵活。
  3. 乘法操作引入二阶相互作用——比单层 MLP 的线性 + 一元激活表达更复杂。

我的看法:SwiGLU 的成功很可能是「容量增加」+「门控形式」+「Swish 激活」三者的合力。其中哪个占主因,研究界还没共识。

十四、激活函数家谱

整理一下整个家族:

时代 激活函数 主要优势 主要问题
1990s Sigmoid 概率解释、光滑 梯度消失、非零中心
1990s-2010s Tanh 零中心 仍然梯度消失
2012- ReLU 简单、不饱和、稀疏 Dying ReLU、非光滑
2014- Leaky/PReLU 缓解 Dying ReLU 实证收益小
2015- ELU/SELU 自归一化(SELU) 限制太多
2017- Swish/SiLU 光滑、轻微非单调 略有计算开销
2018- GELU Transformer 标配 和 Swish 实质等价
2020- SwiGLU/GeGLU LLM 时代标配 参数量多 50%

这条演化线有几个有意思的观察:

第一前期演化由「梯度消失」驱动。从 Sigmoid 到 Tanh 到 ReLU,每一步都在让梯度更好传播。

第二中期演化由「Dying ReLU」驱动。从 ReLU 到 Leaky/ELU/Swish,每一步都在让负区间也有梯度。

第三后期演化由「Transformer + LLM」驱动。从 GELU 到 SwiGLU,是为了在大模型上挤出最后一点性能。这个阶段的进步不是「修补已知问题」,而是「在已经很好的基础上找更好的」——所以提升幅度小、解释难。

第四最简单的方案不是最优的。ReLU 简单,但不是终点;GELU/SwiGLU 比 ReLU 复杂得多,但确实更好。这驳斥了「最简单的就是最好的」这种朴素观点——在工程上,一点复杂换一点性能经常是值得的。

十五、推导:为什么 n 层线性等价于 1 层线性

上一篇我们口头说「线性的复合还是线性」。这里给一个严格推导,让结论无可辩驳。

定义两层无激活的全连接:

\[\mathbf{h} = W_1 \mathbf{x} + \mathbf{b}_1\] \[\mathbf{y} = W_2 \mathbf{h} + \mathbf{b}_2\]

代入:

\[\mathbf{y} = W_2 (W_1 \mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 = W_2 W_1 \mathbf{x} + W_2 \mathbf{b}_1 + \mathbf{b}_2\]

\(W' = W_2 W_1\)\(\mathbf{b}' = W_2 \mathbf{b}_1 + \mathbf{b}_2\),则:

\[\mathbf{y} = W' \mathbf{x} + \mathbf{b}'\]

这就是单层线性。归纳法可以推出 n 层无激活的复合也等价于单层。

数学上严谨多了。插入非线性 \(\sigma\) 之后,因为 \(\sigma\) 不可交换出来\(\sigma(W\mathbf{x}) \ne W \sigma(\mathbf{x})\) 一般情况下),整个表达就不再能折叠成单层了。这就是非线性的本质作用——打破线性变换的「可交换+可吸收」性质

十六、激活函数与「初始化」的纠缠

激活函数和参数初始化是耦合的。同一个网络,初始化变了,激活函数的合适选择也会变。

具体说,初始化的目标是让前向传播时每层激活的方差稳定(不爆炸、不消失),同时让反向传播时梯度方差稳定。不同激活函数对这两个目标的影响不同:

这些是经典论文里推出来的。有兴趣可以读 He et al. 2015《Delving Deep into Rectifiers》——里面把 ReLU 初始化的方差推导得很清楚。

初始化错了,再好的激活函数也救不了你。这是新手常踩的坑——下载了别人代码,把 ReLU 换成 Sigmoid 就完蛋,因为初始化没跟着换。

十七、激活函数与「归一化」的关系

激活函数还和归一化(Normalization)层紧密相关。

BatchNorm(2015)和 LayerNorm(2016)的出现,让激活函数饱和的问题得到了缓解——归一化把每一层的输入拉到一个合理范围,避免输入过大导致 Sigmoid/Tanh 饱和。

有趣的是,归一化层的存在让一些「老」激活函数重新焕发生机——比如在 Transformer 里,Pre-LayerNorm + GELU 工作良好;在 ViT 里,Pre-LayerNorm + GELU 也是标配。如果没有 LayerNorm,单纯的 GELU 也很难训稳大型 Transformer。

激活 + 归一化是「深度学习训练稳定性的双轮」。讨论激活函数时,不能脱离归一化谈。

十八、梯度爆炸怎么办?激活函数能管吗?

激活函数主要解决梯度消失。但爆炸呢?

爆炸通常不是激活函数的问题,是权重过大循环结构(RNN)的问题。激活函数有界(Sigmoid/Tanh)能从源头限制爆炸,但代价是梯度消失。无界激活(ReLU)不限制爆炸,但配合归一化和权重初始化通常没事。

实战中对抗梯度爆炸的工具:

激活函数本身能做的有限。它的主战场仍然是梯度消失

十九、Softmax:一个特殊的「广义激活」

我们之前说「激活函数都是逐元素的」。唯一的例外是 Softmax

\[\text{Softmax}(\mathbf{z})_i = \frac{e^{z_i}}{\sum_j e^{z_j}}\]

它把一个向量映射到「概率分布」——所有输出非负、和为 1。它是非逐元素的(每个输出依赖整个输入向量)。

Softmax 在 Transformer 里有两个关键位置:

  1. 注意力里,对 attention scores 做 Softmax 转成权重分布。
  2. 输出层,把 logits 转成下一个 token 的概率分布。

Softmax 我们留到「第八篇·Softmax 与概率分布」专门讲。这里只是提一句它的特殊地位——它不算狭义的「激活函数」,但它做的事情类似(引入非线性、把一个向量「整形」成有特定语义的输出)。

二十、激活函数的导数和「梯度流」

激活函数的导数决定了梯度怎么流。简单整理:

激活 函数 导数
Sigmoid \(\sigma(z) = 1/(1+e^{-z})\) \(\sigma'(z) = \sigma(z)(1-\sigma(z))\)
Tanh \(\tanh(z)\) \(1 - \tanh^2(z)\)
ReLU \(\max(0, z)\) \(\mathbb{1}(z > 0)\)
Leaky ReLU \(\max(\alpha z, z)\) \(\mathbb{1}(z > 0) + \alpha \mathbb{1}(z \le 0)\)
ELU \(z\)\(\alpha(e^z - 1)\) \(1\)\(\alpha e^z\)
Swish \(z \cdot \sigma(z)\) \(\sigma(z) + z \sigma(z)(1-\sigma(z))\)
GELU \(z \cdot \Phi(z)\) \(\Phi(z) + z \phi(z)\)

注意 Swish/GELU 的导数都不是「函数本身的简单变换」——它们包含 \(z\)\(\sigma(z)/\Phi(z)\) 的乘积。这意味着反向传播时既要算 \(\sigma\) 又要算 \(\sigma'\),比 ReLU 贵一点。但在 GPU 上这点开销几乎可忽略。

梯度流」(gradient flow)是分析深度网络训练的一个视角。我们关心:每过一层,梯度的「典型大小」会变成多少?如果大于 1,可能爆炸;小于 1,可能消失;接近 1,最稳。激活函数选得好,能让梯度流尽量保持「接近 1」。这就是从 Sigmoid 到 ReLU 到 GELU 演化的内在逻辑。

二十一、动手实验:在 PyTorch 里换激活

文字讲多了,回到代码感受一下。一个最小的两层 MLP:

import torch
import torch.nn as nn

class MLP(nn.Module):
    def __init__(self, in_dim, hidden, out_dim, act='relu'):
        super().__init__()
        self.fc1 = nn.Linear(in_dim, hidden)
        self.fc2 = nn.Linear(hidden, out_dim)
        self.act = {
            'sigmoid': nn.Sigmoid(),
            'tanh': nn.Tanh(),
            'relu': nn.ReLU(),
            'leaky': nn.LeakyReLU(0.01),
            'elu': nn.ELU(),
            'gelu': nn.GELU(),
            'silu': nn.SiLU(),
        }[act]

    def forward(self, x):
        return self.fc2(self.act(self.fc1(x)))

用同一份训练代码、同一个数据集(比如 MNIST 或 fashion-MNIST),跑 Sigmoid / Tanh / ReLU / GELU 四个版本——你会清楚地看到:

我建议每个学深度学习的人都自己跑一遍这个实验。理论看得再多,不如自己跑一次的肌肉记忆深刻。Karpathy 的 makemore 系列有一节专门讲激活函数的实证对比,强烈推荐。

二十二、为什么大模型几乎都用 SwiGLU 而不是 GELU?

这是最近(2023 年开始)一个有趣的现象。GPT-3 还用 GELU,但 LLaMA 系列、Mistral、Qwen、DeepSeek、Yi 全用了 SwiGLU。这不是巧合。

主要原因有几条。

第一,效果实测稍好。Shazeer 2020 的论文《GLU Variants Improve Transformer》对比了多种 GLU 变体在 T5 上的表现,SwiGLU 略胜 GeLU。LLaMA 团队复现了这个结论,于是采用。

第二,参数效率。SwiGLU 多一个 \(W_3\) 矩阵,但 LLaMA 把 hidden_dim 略微缩小,使总参数量保持原样。同样参数预算下 SwiGLU 略好。

第三,惯性效应。LLaMA 是开源大模型的标杆,它怎么做后面的人就跟着做。这种「事实标准的传递」是开源社区的常见现象——一旦某个实践被证明有效且开源可见,其他人不会冒险换。

但 SwiGLU 也不是没缺点:

我的判断:SwiGLU 不是终点。未来可能有更好的 FFN 结构(比如 Mixture of Experts、LayerScale 等替代)。但短期内 SwiGLU 是大模型的实战标配。

二十三、激活函数的「死信」与「活档」

说一段题外话。

我喜欢把激活函数想象成「邮局窗口」——输入是一封信,激活函数是窗口工作人员,决定这封信是「寄出去」还是「扔掉」。

这是个调皮的比喻,但帮我记住了不同激活的「精神」。读者可以自己创造比喻——让抽象概念在脑子里有形象,是学习深度学习的关键习惯。

二十四、从激活函数看深度学习的「大道」

激活函数这个领域,给我最大的启发是:

深度学习的进步,往往来自「把一个简单的细节做到极致」

激活函数有什么大不了?不就是一个一维标量函数嘛。但从 Sigmoid 到 ReLU,整个深度学习领域换了几代——这是一个看似微小的细节,却带动了整个学科的变革。

类似的「小细节大革命」还有很多:

每一个细节,单独看都不起眼;合起来,把深度学习从「学术玩具」变成了「工业级技术」。

这给我们一个研究方法论:关注「不起眼」的小问题,往里面深挖。激活函数研究了三十年,仍然有人在 ICLR/NeurIPS 发新激活函数的论文。这不是「卷」,这是「重要的小事」。

二十五、关键概念回顾

回顾一下本篇的脉络。

为什么需要激活函数?因为线性的复合还是线性,单纯堆叠 \(W\mathbf{x} + \mathbf{b}\) 表达力极有限。激活函数引入非线性,把「分段线性」「光滑曲线」等表达能力带给神经网络。每一层的激活函数都是「弯曲」的来源。

激活函数家谱怎么看?早期 Sigmoid(光滑、概率解释),改进 Tanh(零中心),革命 ReLU(不饱和、训得动深),修补 Leaky/ELU(缓解 Dying),现代 Swish/GELU(光滑 ReLU),LLM 时代 SwiGLU(门控 + Swish)。每一代都解决了上一代的某个核心问题。

激活函数的「形状」决定什么?决定梯度怎么流。两端饱和会梯度消失;中段陡峭会梯度爆炸;恒等斜率(ReLU 正半轴)让梯度原样传递。深网络能不能训,根本上是「梯度流稳不稳」的问题。

激活函数与其他组件的耦合:和初始化耦合(不同激活配不同初始化);和归一化耦合(LayerNorm 让饱和问题缓解);和优化器耦合(不同激活的 loss 曲面不同,最佳学习率也不同)。孤立讨论激活函数没有意义,要把它当作整个系统的一部分

Transformer 用什么?GELU 是经典选择;SwiGLU 是最新事实标准。两者实质都属于「光滑、近似 ReLU」家族。

把这五点串起来,你对激活函数的理解就完整了。

二十六、常见误解

误解一:激活函数就是「神经元的开关」。

部分对。Sigmoid 和 Tanh 像「软开关」,ReLU 像「硬开关」。但 Swish/GELU 的「开关」是平滑过渡的,没有明显的「关」状态。激活函数本质是非线性函数,不必都按「开关」理解

误解二:越复杂的激活函数效果越好。

错。ReLU 是最简单的之一,却是十年来最成功的激活函数。SwiGLU 比 GELU 复杂,但提升幅度有限。复杂不等于好——简单 + 工程实用,往往胜过复杂 + 数学优雅。

误解三:训练时用哪个激活函数差别不大。

大错。激活函数能让训练快 10 倍或慢 10 倍。Sigmoid 和 ReLU 在深网络上的差距是「能训」与「训不动」的差距。激活函数是深度学习能 work 的关键之一。

误解四:激活函数只在隐藏层有用,输出层不需要。

要看任务。回归任务输出层通常不加激活(让输出可以是任意实数);分类任务输出层加 Softmax(变概率);二分类输出层加 Sigmoid。所谓「输出层不需要激活」是回归的特例,不是通则。

误解五:Sigmoid 已经被淘汰了。

不完全。Sigmoid 仍然在二分类输出层、注意力门控(如 LSTM)、某些特殊场景里活跃。它在隐藏层确实被 ReLU 系替代,但不等于全面淘汰。

误解六:所有 Transformer 都用 GELU。

不对。原始 Transformer(2017)用 ReLU;BERT/GPT 系列用 GELU;LLaMA 系列用 SwiGLU。激活函数选择随时代演进,不要被「Transformer 用 X」一概而论

误解七:激活函数「越接近 ReLU」越好。

部分对。形状像 ReLU(一边趋零、另一边趋恒等)的激活,确实在深网络上表现普遍较好。但「光滑」「零中心」「门控」等其他属性也重要。不要只盯一个维度优化

误解八:激活函数是网络的「主要参数」。

错。激活函数本身是固定的非线性函数,几乎没有可学习参数(除了 PReLU 等少数变种)。可学习参数都在 \(W\)\(\mathbf{b}\) 里。

误解九:换一个激活函数就能让模型大幅提升。

实际中,从 ReLU 换 GELU 的提升通常 < 1%。从 GELU 换 SwiGLU 类似。激活函数的边际收益有限——但如果你的模型在百亿参数级别,这 1% 就是值钱的。

误解十:激活函数研究已经做完了。

未必。每年仍有新的激活函数论文。比如 Mish、Hardswish、SquaredReLU、Geglu 等等。虽然「大革命」可能不会再有,但「小优化」一直在继续。

二十七、下一步

下一篇(第六篇)我们讲梯度下降与反向传播

我们会从最朴素的梯度下降开始,逐步讲到 SGD、Momentum、Adam,看看「怎么用数据找好参数」这件事的工程实现。然后我们会讲反向传播——链式法则的工程化版本,让自动微分成为可能。

之后第七篇是 Softmax 与概率分布——它是注意力机制的核心齿轮之一。第八篇之后我们就开始接触向量空间、嵌入、位置编码,正式踏上注意力机制的核心。

激活函数这一篇,是「神经网络的非线性来源」。Softmax 那一篇,是「注意力的概率化齿轮」。这两个非线性合起来,构成了 Transformer 的全部非线性来源。记住一句话:Transformer 的非线性,在 FFN 的激活(GELU/SwiGLU)和 Attention 的 Softmax 这两个地方

二十八、参考文献


上一篇04. 从函数到神经网络  下一篇06. 梯度下降与反向传播

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .