激活函数:从 Sigmoid 到 SwiGLU

大模型中的激活函数:从 Sigmoid 到 SwiGLU

刚开始看 Transformer 时,注意力机制往往最吸引眼球。可是查看模型代码,会发现每一层还有一个前馈网络,而激活函数毫无疑问在这中间扮演了重要角色。今天手搓Qwen3模型的时候就突然注意到官方配置里有 hidden_act=silu选项。一查才发现原来Qwen家族的模型和大多数开源模型一直用的都是SwiGLU这种激活函数,而不是之前学习中常见的ReLU。于是乎研究了一下各种奇形怪状的激活函数,整理一下~~

本文按 Sigmoid / tanh → ReLU → GELU / SiLU → GLU / SwiGLU 展开,简单介绍几种关键的激活函数。

1. 激活函之于 Transformer

1.1 非线性变换与模型表达能力

假设连续堆叠两个线性层,采用行向量写法:

\[y=(xW_1+b_1)W_2+b_2 =x(W_1W_2)+(b_1W_2+b_2) \]

它们可以合并成一个仿射变换。堆得再深,只要中间始终没有非线性操作,表达形式就没有跳出这类变换。

然而,加入激活函数 \(\phi\) 后,情况就不同了:

\[y=\phi(xW_1+b_1)W_2+b_2 \]

中间这次非线性变换,让模型能按输入的不同特征改变处理方式:线性投影把特征组合起来,激活函数决定这些组合以怎样的形式继续传递。

不过,也不能简单地说“删掉 FFN 激活,整个 Transformer 就变成线性模型”。注意力里的 softmax、归一化等本身也包含非线性。这里的论证更多针对的是连续线性层组成的子网络。

1.2 FFN 中的逐位置特征变换

原始 Transformer 的前馈网络可以写成:

\[\operatorname{FFN}(x) =\operatorname{ReLU}(xW_1+b_1)W_2+b_2 \]

若输入最后一维为 \(d\),中间维度为 \(m\),它的形状变化是:

\[[B,T,d]\ \longrightarrow\ [B,T,m] \ \longrightarrow\ [B,T,d] \]

\(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 的定义是:

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

它的输出在 \((0,1)\) 内,输入很大时接近 \(1\),很小时接近 \(0\),所以很适合表达一个平滑的开关。

它的导数也很整齐:

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

问题恰好藏在导数里:它最大只有 \(0.25\),而且输入进入正负两端的饱和区后,导数会接近 \(0\),层数多了,梯度逐渐消失。

另一个问题是 Sigmoid 输出总为正,缺少围绕零点的对称性。

因此,Sigmoid 逐渐不再是深层前馈网络隐藏层的默认选择,但它仍然适合门控,也常用于二分类概率输出。(顺便说一句,具体写代码时,二分类损失如果已经采用 BCEWithLogitsLoss,应直接输入 logits,不用再手动做一次 Sigmoid了😂)。

3.2 tanh 的零点对称性与梯度

双曲正切函数可以写成:

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

它的输出范围是 \((-1,1)\),并且是奇函数:\(\tanh(-x)=-\tanh(x)\)。相比 Sigmoid,它能自然地表达正负方向,在零附近的导数也更大:

\[\tanh'(x)=1-\tanh^2(x) \]

在 \(x=0\) 时,导数是 \(1\);但 \(|x|\) 很大时,输出接近 \(\pm1\),导数仍然趋近于 \(0\)。

它改善了零附近的梯度和输出对称性,却没有完全消除两端饱和的问题。

4. 大道至简的ReLU

ReLU 的公式非常简单:

\[\operatorname{ReLU}(x)=\max(0,x) \]

正数直接通过,负数全部变成零。它在非零点的导数是:

\[\operatorname{ReLU}'(x)= \begin{cases} 0,&x<0\\ 1,&x>0 \end{cases} \]

这解决了一个很实际的问题:正半轴不会像 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 还可以写成:

\[\operatorname{ReLU}(x)=x\mathbf{1}_{x>0} \]

后面的指示函数只会取 \(0\) 或 \(1\)。如果把这个突然跳变的开关换成平滑的权重,会得到什么?

GELU 给出的答案是:

\[\operatorname{GELU}(x)=x\Phi(x) \]

其中 \(\Phi(x)\) 是标准正态分布的累积分布函数:

\[\Phi(x)=P(Z\le x),\qquad Z\sim\mathcal N(0,1) \]

对负得很明显的值,\(\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\):

\[xz= \begin{cases} x,&z=1\quad\text{(保留)}\\ 0,&z=0\quad\text{(归零)} \end{cases} \]

接着,让“保留”的概率由当前输入决定,取为 \(\Phi(x)\):

\[P(z=1\mid x)=\Phi(x),\qquad P(z=0\mid x)=1-\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\)。它的理论平均值,也就是期望,可以按“每种输出 × 对应概率”相加得到:

\[\begin{aligned} \mathbb E[xz\mid x] &=x\cdot P(z=1\mid x)+0\cdot P(z=0\mid x)\\ &=x\Phi(x)+0\cdot(1-\Phi(x))\\ &=x\Phi(x)\\ &=\operatorname{GELU}(x) \end{aligned} \]

这里取平均的是固定输入时,开关随机变化产生的输出,不是把一批不同的输入求平均。这种从随机门的期望得到 GELU 的思路,来自 GELU 原论文第 2 节。

第三步:实际计算时,直接使用这个期望。

常规 GELU 实现直接计算确定性的 \(x\Phi(x)\),或者它的近似式。例如,输入 \(1\) 时,每次都得到约 \(0.8413\);上面的随机开关实验则每次只会输出 \(1\) 或 \(0\),只有长期平均才趋近 \(0.8413\)。GELU 不需要真的反复抽样、再把结果平均;公式已经把这个平均结果算出来了。 训练和推理时,GELU 本身都按这一方式计算。

5.3 GELU 的精确形式与 tanh 近似

GELU 的精确表达还可以写成:

\[\operatorname{GELU}(x) =\frac{x}{2}\left[1+\operatorname{erf}\left(\frac{x}{\sqrt2}\right)\right] \]

一种常用近似是:

\[\operatorname{GELU}(x)\approx \frac{x}{2}\left[1+\tanh\left(\sqrt{\frac2\pi} (x+0.044715x^3)\right)\right] \]

GPT-2 官方实现就使用了这种 tanh 近似。用 PyTorch 复现时,可以写 torch.nn.functional.gelu(x, approximate="tanh")。精确版和近似版通常很接近,但复现已有模型时仍然应当遵守原实现。参见 GPT-2 的 GELU 实现。

6. SiLU / Swish:用 Sigmoid 给输入加权

SiLU 的公式和 GELU 很像,只是把权重函数换成了 Sigmoid:

\[\operatorname{SiLU}(x)=x\sigma(x) \]

它让输入自己决定通过多少:输入大而为正,\(\sigma(x)\) 接近 \(1\),输出接近 \(x\);输入很负,\(\sigma(x)\) 接近 \(0\),输出接近零;中间区域则平滑过渡。它的导数是:

\[\operatorname{SiLU}'(x) =\sigma(x)+x\sigma(x)(1-\sigma(x)) \]

正输入很大时,导数接近 \(1\);零点导数为 \(0.5\);输入很负时,导数仍会趋近于零。SiLU 和 GELU 都没有 ReLU 那种整段负半轴完全为零的硬截断,但它们也没有完全消除小梯度问题。

Swish 常写成带参数的形式:

\[\operatorname{Swish}_\beta(x)=x\sigma(\beta x) \]

当 \(\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 的输出反而“更负了”。这与它们在一段负区间上存在负导数的性质一致。

Sigmoid、tanh、ReLU、GELU、SiLU 的函数值与导数对比

图 1:左侧是函数值,右侧是导数。上排展示 Sigmoid、tanh 的饱和;下排对比 ReLU、GELU、SiLU,插图放大了零附近的差异。ReLU 在零点按框架约定取导数 0,其余曲线按解析公式绘制。

7. SwiGLU:门控版的SiLU

前面讨论的 ReLU、GELU、SiLU,都是对单个数值进行变换,再逐元素应用到张量上。GLU 家族开始改变前馈网络的结构:同一份输入分别走两条投影分支,再把结果逐元素相乘。

7.1 GLU 的双分支设计

忽略偏置,GLU 可以写成:

\[\operatorname{GLU}(x) =\underbrace{\sigma(xW_g)}_{\text{门控分支}} \odot\underbrace{(xW_u)}_{\text{内容分支}} \]

\(\odot\) 表示对应位置逐元素相乘,两个分支的输出形状必须相同。\(W_g\) 和 \(W_u\) 是两组不同的可学习参数。

GLU 的早期代表工作是 Language Modeling with Gated Convolutional Networks(2016 预印本 / 2017 发表)。它最初并不是专门为 Transformer 提出的。

7.2 SwiGLU 与其他门控变体

在 Transformer 的 FFN 中使用门控时,还要把相乘后的结果投影回隐藏维度。常见的 SwiGLU 写法是:

\[\operatorname{FFN}_{\text{SwiGLU}}(x) =\left[\operatorname{SiLU}(xW_g)\odot(xW_u)\right]W_d \]

这里的 \(W_g,W_u\in\mathbb R^{d\times m}\),\(W_d\in\mathbb R^{m\times d}\)。中间两条分支都是 \([B,T,m]\),相乘后仍然是这个形状,最后恢复成 \([B,T,d]\)。

flowchart LR X["输入 x"] --> G["门控投影 Wg"] G --> S["SiLU"] X --> U["内容投影 Wu"] S --> MUL["逐元素相乘"] U --> MUL MUL --> D["输出投影 Wd"] D --> Y["FFN 输出"]

同一家族还有 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\),暂时只看一个中间维度:

\[h=\operatorname{SiLU}(g)u \]

与“先投影再单独激活”相比,这里增加了两种特征组合之间的乘性交互。内容分支的结果是否被放大或抑制,取决于另一条分支提取的特征。

反向传播也体现了这点:

\[\frac{\partial h}{\partial u}=\operatorname{SiLU}(g),\qquad \frac{\partial h}{\partial g}=u\operatorname{SiLU}'(g) \]

两条通路的梯度会相互调制。

8. 算一算SwiGLU的参数量

我们当然希望新的策略不会额外增加更多参数,不然凭什么说新的策略更好,对吧?所以这里要算一下SwiGLU的门控FFN要取多大。

普通 FFN 有两个大矩阵,忽略偏置,参数量为:

\[\underbrace{dm}_{W_1}+\underbrace{md}_{W_2}=2dm \]

而SwiGLU 有三个矩阵,因此参数量为:

\[\underbrace{dm}_{W_g}+\underbrace{dm}_{W_u} +\underbrace{md}_{W_d}=3dm \]

假如原来的普通 FFN 使用 \(m=4d\),参数量就是 \(8d^2\)。为了让门控版本保持相近的参数和主要矩阵乘法运算量,需要:

\[3dm_{\text{gated}}\approx8d^2 \quad\Longrightarrow\quad m_{\text{gated}}\approx\frac83d \]

也就是说,门控 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 门控、逐元素相乘 是结构变化;比较时要控制参数和计算预算

继续往深处学,值得关注的就不只是某个函数有没有一个更漂亮的导数,而是它怎样与归一化、残差路径、训练精度和计算预算共同工作,让训练更稳定、模型更有效。现阶段就还是当成黑箱罢,记住就好辣~

参考资料与延伸阅读

posted @ 2026-10-16 21:00  JaaaLmg  阅读(15)  评论(0)    收藏  举报