激活函数:从 Sigmoid 到 SwiGLU
大模型中的激活函数:从 Sigmoid 到 SwiGLU
刚开始看 Transformer 时,注意力机制往往最吸引眼球。可是查看模型代码,会发现每一层还有一个前馈网络,而激活函数毫无疑问在这中间扮演了重要角色。今天手搓Qwen3模型的时候就突然注意到官方配置里有 hidden_act=silu选项。一查才发现原来Qwen家族的模型和大多数开源模型一直用的都是SwiGLU这种激活函数,而不是之前学习中常见的ReLU。于是乎研究了一下各种奇形怪状的激活函数,整理一下~~
本文按 Sigmoid / tanh → ReLU → GELU / SiLU → GLU / SwiGLU 展开,简单介绍几种关键的激活函数。
1. 激活函之于 Transformer
1.1 非线性变换与模型表达能力
假设连续堆叠两个线性层,采用行向量写法:
它们可以合并成一个仿射变换。堆得再深,只要中间始终没有非线性操作,表达形式就没有跳出这类变换。
然而,加入激活函数 \(\phi\) 后,情况就不同了:
中间这次非线性变换,让模型能按输入的不同特征改变处理方式:线性投影把特征组合起来,激活函数决定这些组合以怎样的形式继续传递。
不过,也不能简单地说“删掉 FFN 激活,整个 Transformer 就变成线性模型”。注意力里的 softmax、归一化等本身也包含非线性。这里的论证更多针对的是连续线性层组成的子网络。
1.2 FFN 中的逐位置特征变换
原始 Transformer 的前馈网络可以写成:
若输入最后一维为 \(d\),中间维度为 \(m\),它的形状变化是:
\(B\) 是批大小,\(T\) 是序列长度。原始 Transformer 使用了 \(m=4d\) 的设置,但这个倍数不是所有模型必须遵守的规定。参见 Attention Is All You Need,第 3.3 节。
此时输入的 token 向量已经包含了上下文信息,同一层 FFN 会对不同 token 使用同一套参数,逐个位置进行计算。
所以,注意力负责从其他位置取信息,FFN 负责对当前表示进行特征变换。两者交替工作;激活函数主要出现在后者中。
2. 理解激活函数的几个角度
面对一个新函数,可以先关注四个问题:
- 它怎样处理正负输入? 是截断、压缩,还是保留一部分负值?
- 梯度在哪些区域容易变小? 某些输入是否很难再得到有效更新?
- 计算代价如何? 一次比较、一次指数运算,在大量 token 和多层网络中会累积成不同成本。
- 它放在什么结构里? 独立激活和门控分支里的激活,即使使用同一个函数,也不等于同一种网络。
3. Sigmoid 与 tanh的有界输出与饱和问题
先说一下,所谓“饱和”,可以先理解为:输入继续变化,输出却几乎不动了,对应的局部导数很小。训练时,梯度要经过许多这样的局部导数;如果一路都被缩小,前面的参数就难以得到足够的更新信号。
当然,真实训练还受到权重矩阵、初始化、残差连接和归一化影响,这里先不管那些。
3.1 Sigmoid 的输出与梯度
Sigmoid 的定义是:
它的输出在 \((0,1)\) 内,输入很大时接近 \(1\),很小时接近 \(0\),所以很适合表达一个平滑的开关。
它的导数也很整齐:
问题恰好藏在导数里:它最大只有 \(0.25\),而且输入进入正负两端的饱和区后,导数会接近 \(0\),层数多了,梯度逐渐消失。
另一个问题是 Sigmoid 输出总为正,缺少围绕零点的对称性。
因此,Sigmoid 逐渐不再是深层前馈网络隐藏层的默认选择,但它仍然适合门控,也常用于二分类概率输出。(顺便说一句,具体写代码时,二分类损失如果已经采用 BCEWithLogitsLoss,应直接输入 logits,不用再手动做一次 Sigmoid了😂)。
3.2 tanh 的零点对称性与梯度
双曲正切函数可以写成:
它的输出范围是 \((-1,1)\),并且是奇函数:\(\tanh(-x)=-\tanh(x)\)。相比 Sigmoid,它能自然地表达正负方向,在零附近的导数也更大:
在 \(x=0\) 时,导数是 \(1\);但 \(|x|\) 很大时,输出接近 \(\pm1\),导数仍然趋近于 \(0\)。
它改善了零附近的梯度和输出对称性,却没有完全消除两端饱和的问题。
4. 大道至简的ReLU
ReLU 的公式非常简单:
正数直接通过,负数全部变成零。它在非零点的导数是:
这解决了一个很实际的问题:正半轴不会像 Sigmoid、tanh 那样越走越平,局部梯度可以保持为 \(1\)。同时,它的计算便宜,还会产生大量精确为零的激活值。ReLU 的推广并不是近年大模型才发生的事;Deep Sparse Rectifier Neural Networks(2011) 是理解这条路线的重要早期论文。
零点虽然不可微,但训练框架可以选择一个约定的次梯度;PyTorch 使用 \(0\)。因此,“零点不可微”通常不是实践中的主要障碍。
真正需要留意的是负半轴。假如某个单元对训练中几乎所有输入都输出负的预激活值,那么它经过 ReLU 后长期输出零,沿这条通路回传的梯度也为零,这就是常说的“神经元死亡”。
Leaky ReLU 等变体会给负半轴留一点斜率,例如 \(\max(\alpha x,x)\),其中 \(0<\alpha<1\)。但对 Transformer 而言,后续更有代表性的路线,是用平滑的方式按数值大小调整信息,而不是在零点做一次硬切分,故不在此赘述。
5. GELU:更丝滑,更优雅
5.1 从 ReLU 的硬截断到平滑加权
ReLU 还可以写成:
后面的指示函数只会取 \(0\) 或 \(1\)。如果把这个突然跳变的开关换成平滑的权重,会得到什么?
GELU 给出的答案是:
其中 \(\Phi(x)\) 是标准正态分布的累积分布函数:
对负得很明显的值,\(\Phi(x)\) 接近 \(0\);对很大的正值,它接近 \(1\);零附近则是连续、平滑的过渡。因此,GELU 不会像 ReLU 那样,把所有负值一刀切成零。参见 Gaussian Error Linear Units(2016)。
比如 \(\Phi(-1)\approx0.1587\),所以 \(\operatorname{GELU}(-1)\approx-0.1587\)。它保留了一个经过缩小的负值;而 \(\operatorname{ReLU}(-1)=0\)。
5.2 GELU 的概率解释
上一节说,GELU 用 \(\Phi(x)\) 给输入加权。为什么这个权重还可以解释成“保留输入的概率”?可以先做一个思想实验:固定一个输入 \(x\),让它经过一个只会“放行”或“归零”的随机开关。
第一步:用 \(0\) 和 \(1\) 表示开关状态。
把开关记为 \(z\):\(z=1\) 表示保留输入,\(z=0\) 表示把输入变成零。因此,经过开关后的输出就是 \(xz\):
接着,让“保留”的概率由当前输入决定,取为 \(\Phi(x)\):
这种只取 \(0\)、\(1\) 两个值的随机变量,服从伯努利分布。上面的设定可以简写成 \(z\mid x\sim\operatorname{Bernoulli}(\Phi(x))\)。这里的 \(\mid x\) 表示“在输入 \(x\) 已经给定的条件下”:先确定 \(x\),再根据它计算开关的概率。
这就像抛一枚正面概率随输入变化的硬币:\(x\) 越大,\(\Phi(x)\) 越大,越容易放行;\(x\) 越小,越容易归零。比如 \(x=1\) 时,保留概率约为 \(84.13\%\);\(x=-1\) 时,保留概率约为 \(15.87\%\)。
第二步:计算这个随机开关的平均输出。
固定同一个 \(x\),如果重复很多次上述实验,输出有时是 \(x\),有时是 \(0\)。它的理论平均值,也就是期望,可以按“每种输出 × 对应概率”相加得到:
这里取平均的是固定输入时,开关随机变化产生的输出,不是把一批不同的输入求平均。这种从随机门的期望得到 GELU 的思路,来自 GELU 原论文第 2 节。
第三步:实际计算时,直接使用这个期望。
常规 GELU 实现直接计算确定性的 \(x\Phi(x)\),或者它的近似式。例如,输入 \(1\) 时,每次都得到约 \(0.8413\);上面的随机开关实验则每次只会输出 \(1\) 或 \(0\),只有长期平均才趋近 \(0.8413\)。GELU 不需要真的反复抽样、再把结果平均;公式已经把这个平均结果算出来了。 训练和推理时,GELU 本身都按这一方式计算。
5.3 GELU 的精确形式与 tanh 近似
GELU 的精确表达还可以写成:
一种常用近似是:
GPT-2 官方实现就使用了这种 tanh 近似。用 PyTorch 复现时,可以写 torch.nn.functional.gelu(x, approximate="tanh")。精确版和近似版通常很接近,但复现已有模型时仍然应当遵守原实现。参见 GPT-2 的 GELU 实现。
6. SiLU / Swish:用 Sigmoid 给输入加权
SiLU 的公式和 GELU 很像,只是把权重函数换成了 Sigmoid:
它让输入自己决定通过多少:输入大而为正,\(\sigma(x)\) 接近 \(1\),输出接近 \(x\);输入很负,\(\sigma(x)\) 接近 \(0\),输出接近零;中间区域则平滑过渡。它的导数是:
正输入很大时,导数接近 \(1\);零点导数为 \(0.5\);输入很负时,导数仍会趋近于零。SiLU 和 GELU 都没有 ReLU 那种整段负半轴完全为零的硬截断,但它们也没有完全消除小梯度问题。
Swish 常写成带参数的形式:
当 \(\beta=1\) 时,它与原始的 SiLU 相同(2016 年的 GELU 论文已讨论 SiLU,2017 年又有专门研究 SiLU 和搜索 Swish 的工作,参见 SiLU 研究、Searching for Activation Functions)。
把数值放在一起看会更直观:
| 输入 \(x\) | ReLU | GELU | SiLU |
|---|---|---|---|
| \(-2\) | \(0\) | \(-0.0455\) | \(-0.2384\) |
| \(-1\) | \(0\) | \(-0.1587\) | \(-0.2689\) |
| \(0\) | \(0\) | \(0\) | \(0\) |
| \(1\) | \(1\) | \(0.8413\) | \(0.7311\) |
| \(2\) | \(2\) | \(1.9545\) | \(1.7616\) |
注意负区间的变化:当输入从 \(-2\) 增加到 \(-1\) 时,GELU 和 SiLU 的输出反而“更负了”。这与它们在一段负区间上存在负导数的性质一致。

图 1:左侧是函数值,右侧是导数。上排展示 Sigmoid、tanh 的饱和;下排对比 ReLU、GELU、SiLU,插图放大了零附近的差异。ReLU 在零点按框架约定取导数 0,其余曲线按解析公式绘制。
7. SwiGLU:门控版的SiLU
前面讨论的 ReLU、GELU、SiLU,都是对单个数值进行变换,再逐元素应用到张量上。GLU 家族开始改变前馈网络的结构:同一份输入分别走两条投影分支,再把结果逐元素相乘。
7.1 GLU 的双分支设计
忽略偏置,GLU 可以写成:
\(\odot\) 表示对应位置逐元素相乘,两个分支的输出形状必须相同。\(W_g\) 和 \(W_u\) 是两组不同的可学习参数。
GLU 的早期代表工作是 Language Modeling with Gated Convolutional Networks(2016 预印本 / 2017 发表)。它最初并不是专门为 Transformer 提出的。
7.2 SwiGLU 与其他门控变体
在 Transformer 的 FFN 中使用门控时,还要把相乘后的结果投影回隐藏维度。常见的 SwiGLU 写法是:
这里的 \(W_g,W_u\in\mathbb R^{d\times m}\),\(W_d\in\mathbb R^{m\times d}\)。中间两条分支都是 \([B,T,m]\),相乘后仍然是这个形状,最后恢复成 \([B,T,d]\)。
同一家族还有 ReGLU 和 GEGLU,分别使用 ReLU 和 GELU 处理门控分支。Noam Shazeer 在 GLU Variants Improve Transformer(2020) 中系统比较了这些变体,GEGLU、SwiGLU 在论文的 T5 实验中表现较好。
这里有两个关键区别:
第一,SiLU 和 SwiGLU 不是同一个东西。SiLU 是 \(x\sigma(x)\) 这个函数;SwiGLU 则需要两条不同的投影分支,以及它们之间的乘法。
第二,SwiGLU 的门控系数是 \(\operatorname{SiLU}(xW_g)\),范围并不限制在 \(0\) 到 \(1\),既可能为负,也可能大于 \(1\)。
7.3 门控分支中的特征交互与梯度
令 \(g=xW_g\)、\(u=xW_u\),暂时只看一个中间维度:
与“先投影再单独激活”相比,这里增加了两种特征组合之间的乘性交互。内容分支的结果是否被放大或抑制,取决于另一条分支提取的特征。
反向传播也体现了这点:
两条通路的梯度会相互调制。
8. 算一算SwiGLU的参数量
我们当然希望新的策略不会额外增加更多参数,不然凭什么说新的策略更好,对吧?所以这里要算一下SwiGLU的门控FFN要取多大。
普通 FFN 有两个大矩阵,忽略偏置,参数量为:
而SwiGLU 有三个矩阵,因此参数量为:
假如原来的普通 FFN 使用 \(m=4d\),参数量就是 \(8d^2\)。为了让门控版本保持相近的参数和主要矩阵乘法运算量,需要:
也就是说,门控 FFN 的中间维度可以取普通 FFN 的约 \(2/3\)。这正是 GLU 变体论文采用的比较方式;LLaMA 也采用了相关设计。参见 LLaMA,第 2.2 节。
当然,实际模型还会考虑硬件对齐、额外容量和其他超参数,因此不一定严格等于 \(8d/3\)。即使参数量、主要乘法运算量相近,端到端运行速度和训练显存也可能因为算子融合、访存和保存的激活不同而有差异。
9. GELU FFN 与 SwiGLU 的代码实现
下面让两种 FFN 使用相同的输入输出维度,并选择恰好相同的参数量。为方便比较,二者都不使用偏置(实际不一定是这样,比如GPT-2 的原始 FFN 是包含偏置的)。
以下代码由GPT6.1sol实现🤣:
import torch
from torch import nn
from torch.nn import functional as F
class GELUFFN(nn.Module):
def __init__(self, d_model, intermediate_size):
super().__init__()
self.up_proj = nn.Linear(d_model, intermediate_size, bias=False)
self.down_proj = nn.Linear(intermediate_size, d_model, bias=False)
def forward(self, x):
return self.down_proj(F.gelu(self.up_proj(x), approximate="tanh"))
class SwiGLUFFN(nn.Module):
def __init__(self, d_model, intermediate_size):
super().__init__()
self.gate_proj = nn.Linear(d_model, intermediate_size, bias=False)
self.up_proj = nn.Linear(d_model, intermediate_size, bias=False)
self.down_proj = nn.Linear(intermediate_size, d_model, bias=False)
def forward(self, x):
gate = F.silu(self.gate_proj(x))
content = self.up_proj(x)
return self.down_proj(gate * content)
x = torch.randn(2, 4, 96)
gelu_ffn = GELUFFN(d_model=96, intermediate_size=384)
swiglu_ffn = SwiGLUFFN(d_model=96, intermediate_size=256)
print(gelu_ffn(x).shape, swiglu_ffn(x).shape)
# torch.Size([2, 4, 96]) torch.Size([2, 4, 96])
print(sum(p.numel() for p in gelu_ffn.parameters())) # 73728
print(sum(p.numel() for p in swiglu_ffn.parameters())) # 73728
这段代码展示了结构和参数量之间的关系。性能对比实验就不搞了,也不是我这种层次能搞得了的😅。两者好不好,需要在一致的数据、训练安排和评测任务上比较,不能用随机初始化后的输出来判断。
10. 激活函数与门控结构的演进
| 阶段或时间 | 代表方案 | 主要设计动机 |
|---|---|---|
| 早期神经网络 | Sigmoid、tanh | 平滑可微,输出有界;深层传播会受到饱和影响 |
| 2010 年代初广泛推广 | ReLU | 简化计算,让正半轴保持不饱和的局部梯度 |
| 2016 起 | GELU、SiLU | 用连续权重调节输入,软化零附近的硬截断 |
| 2016 / 2017 | GLU | 用独立投影分支产生输入相关的门控 |
| 2017 | 原始 Transformer | 在逐位置 FFN 中使用 ReLU |
| 2017 | Swish 研究 | 搜索并评估 \(x\sigma(\beta x)\) 形式的激活 |
| 2019 | GPT-2 | 使用带 GELU 近似的普通 FFN |
| 2020 | GLU 变体研究 | 将 ReGLU、GEGLU、SwiGLU 放进 Transformer 做预算相近的比较 |
| 2023 及后续模型实践 | LLaMA 等 | 采用 SwiGLU 作为整体架构设计的一部分 |
几种常见方案也可以这样对照:
| 方案 | 关键特征 | 需要留意的限制 |
|---|---|---|
| Sigmoid | \((0,1)\) 内的平滑门 | 两端饱和,单独作隐藏层激活时梯度容易缩小 |
| tanh | 输出正负对称,零附近导数较大 | 两端依然饱和,不保证实际输出均值为零 |
| ReLU | 便宜、正半轴线性、产生精确零值 | 负区间局部梯度为零,可能长期失活 |
| GELU | 用正态 CDF 平滑加权输入 | 可保留负值;精确式和近似式需要区分 |
| SiLU | \(x\sigma(x)\),正半轴渐近线性 | 负区间仍可能有小梯度或负导数 |
| SwiGLU | 两路投影、SiLU 门控、逐元素相乘 | 是结构变化;比较时要控制参数和计算预算 |
继续往深处学,值得关注的就不只是某个函数有没有一个更漂亮的导数,而是它怎样与归一化、残差路径、训练精度和计算预算共同工作,让训练更稳定、模型更有效。现阶段就还是当成黑箱罢,记住就好辣~
参考资料与延伸阅读
- Deep Sparse Rectifier Neural Networks(2011):理解 ReLU 的训练特点和稀疏激活。
- Gaussian Error Linear Units(2016):阅读 GELU 的定义、概率解释、近似形式及 SiLU 的早期讨论。
- Sigmoid-Weighted Linear Units(2017):阅读 SiLU 的专门研究。
- Searching for Activation Functions(2017):理解 Swish 的定义与实验背景。
- Language Modeling with Gated Convolutional Networks(2016 预印本 / 2017 发表):了解 GLU 的门控思想。
- GLU Variants Improve Transformer(2020):重点看 FFN 公式与保持参数预算的设计。
- GPT-2 官方实现:把 GELU 近似与普通 FFN 对应起来。
- LLaMA(2023):结合模型架构理解 SwiGLU 和中间维度的选择。
- Qwen3 实现(Transformers v5.3.0):对照
Qwen3MLP阅读三组投影矩阵。 - PyTorch GELU 文档、SiLU 文档:确认函数定义、近似选项与接口用法。
浙公网安备 33010602011771号