1.3 激活函数:非线性从哪来
1.2 节反复用到一个事实:网络的表达能力全部来自 。但「提供非线性」只是激活函数的必要条件,不是它全部的工作。在真实训练里,激活函数还要同时满足三件事:导数在多数区域不接近零(否则深层连乘后梯度消失)、计算便宜(每个元素每个 batch 都要算一次)、数值范围可控(不放大到溢出、也不塌缩到零)。
这三条要求之间有冲突。Sigmoid 的导数上限只有 ;tanh 更好地做到零均值但依然两端饱和;ReLU 在正半区导数恒为 ,一举解决饱和问题,却在负半区把梯度直接置零。现代大模型用的又是另一类东西——带门控的 GLU 家族。这一节按「饱和的定量分析 → ReLU 与它的缺陷 → 平滑化 → 门控」的顺序把这些选择讲清楚,最后给出一张可以照着查的对比表。
1.3.1 为什么非线性是唯一的开关
激活函数是逐元素作用于预激活 的标量函数 ,即 。按它在网络中的位置分两类:
- 隐藏层激活:作用在中间层,要求非线性且在训练中梯度可传;
- 输出层激活:把网络输出映射到任务需要的取值域(分类用 softmax、二分类用 Sigmoid、回归通常不加)。
按导数结构分,常见激活可归为三族:饱和型(Sigmoid、tanh,两端导数趋零)、分段线性型(ReLU 及 LeakyReLU、ELU 等变体)、平滑非饱和型(GELU、SiLU),另有以乘积形式构造的门控型(GLU 家族,见 1.3.5)。
把激活函数想成给每个单元装了一个音量旋钮: 就是当前音量。旋钮太小( 落在饱和区或 ReLU 的负半区),这个单元对损失的变化几乎无反应,梯度也传不上去;旋钮是 ,梯度原样通过。于是「选激活」这个问题的工程版表述是:希望尽可能多的单元在尽可能长的时间里保持一个接近 的旋钮,同时不要因为旋钮太大而让信号爆炸。
1.3.2 对比表:从 Sigmoid 到 SwiGLU
| 激活 | 定义 | 值域 | 导数(关键区间) | 主要优点 | 主要代价 |
|---|---|---|---|---|---|
| Sigmoid | 输出可解释为概率;光滑 | 两端饱和→梯度消失;输出非零均值;含指数运算 | |||
| tanh | 零均值,比 Sigmoid 好传梯度 | 仍两端饱和( 后导数降至 量级) | |||
| ReLU | ()/() | 正区不饱和、计算极便宜、激活稀疏 | 负区梯度为 (可死亡);输出非零均值 | ||
| LeakyReLU | , 常取 | / | 负区保留小梯度,缓解死亡 | 多一个超参;收益不稳定 | |
| ELU | ()/() | / | 负区饱和到 ,输出接近零均值 | 含指数运算,稍贵 | |
| GELU | , 为标准正态 CDF;常用近似 | 平滑、非单调(在 附近有极小值) | 平滑、可导、经验上优于 ReLU(在若干任务上) | 比 ReLU 贵;理论优势与任务强相关 | |
| SiLU / Swish | 平滑、自门控形式、无饱和上界 | 含指数运算 | |||
| GLU / SwiGLU / GeGLU | , 是同一输入的两个线性投影 | 视门控而定 | 门控项决定局部增益 | 表达力强,现代 LLM 的默认前馈层选择 | 参数量与计算量比单线性层更多(需宽幅缩放,见下) |
| Softmax | 单纯形 | 把 logits 变成分布 | 只用于输出层(或权重归一化),不是隐藏层激活 |
边界是「导数会不会在某个区间塌到零」。饱和型(Sigmoid/tanh)在两端塌,于是深层网络的梯度链断了;分段线型(ReLU)在负半区塌,于是部分单元死掉;平滑型(GELU/SiLU)通过「负半区导数很小但不恒为零」在两者之间取折中;门控型(GLU)干脆把「增益」交给另一个学出来的线性投影决定,让网络自己选择每个位置该放大还是该关闭。条条大路都在回答同一个问题:梯度怎么活着穿过很多层。
1.3.3 饱和与梯度消失:一次定量计算
设一个 层网络全部使用 Sigmoid 激活,忽略权重的缩放效应,则反向传播中跨过 层的梯度至少被乘上因子
即层数每增加一层,梯度量级至少再乘 : 时上界为 , 时约为 。
一层的输入输出关系为 、。损失对第 层预激活的梯度由链式法则写成连乘:
关键在对角矩阵里的 :(在 取等号),且当 时 。把它代回连乘,并注意每一层的对角因子在每个元素上都不超过 ,即得
因此梯度随深度呈几何衰减;权重的谱范数若大于 可以暂时抵消,但那会导致逐层放大,训练中表现为震荡或溢出——这是早期深层网络难以训练的经典困境。
∎反向传播把损失的变化量从最后一层传到第一层,每一层都要乘以本层的「传导率」。传导率小于 时,多次相乘等于反复打折:打九折十次是 ,打二五折十次只剩百万分之一。而且这不是「平均意义上」的折扣——Sigmoid 在 大时折扣到千分之一,而大 恰恰是「网络很自信」的样本,也就是说网络越自信,越学不动。tanh 把上限从 提到 ,缓解了一个数量级,但没有解决「大输入时塌到零」这个结构性问题。
由定理 1.3.1 与 1.2.5的方差守恒推导可得两条配对规则:Sigmoid / tanh 配 Xavier(增益 ),ReLU 族配 Kaiming(增益 )。用错配对的症状不同:Sigmoid 配 Kaiming 会因增益过大而让 变大、快速进入饱和区(表现为训练初期损失几乎不降);ReLU 配 Xavier 会因增益偏小而逐层衰减(表现为深层网络的有效深度变小)。
1.3.4 ReLU 的死亡问题与缓解
ReLU 在 时输出为 且导数为 :如果一个单元对训练集里的所有样本都落在负半区,它的梯度永远是 ,参数永远不会被更新(包括偏置)——这就是「死亡 ReLU」(dying ReLU)。
一次过大的梯度更新(通常来自过大的学习率、或某个批里的离群样本)可能把某个单元的偏置推到很负的位置,使它对所有样本都输出 ;此后它的梯度恒为 ,于是它再也不能把自己救回来。与「饱和」的区别是:Sigmoid 的饱和单元只要输入分布变化还会恢复(导数小但非零),死亡 ReLU 的单元是永久失效的。
缓解手段有五种,从弱到强:
| 现象 | 症状 | 判据 | 首选处置 |
|---|---|---|---|
| 死亡 ReLU | 某层激活大量恒为 ,损失下降到平台 | 统计每层「激活为 0 的比例」持续接近 1 | 换 GELU/SiLU,加 warmup |
| 梯度饱和(Sigmoid/tanh) | 首层梯度范数比末层小若干数量级 | 打印每层梯度范数,比值随深度指数下降 | 换 ReLU 族;改 Xavier 初始化 |
| 梯度爆炸 | 损失出现 NaN / 大幅震荡 | 梯度范数逐层放大 | 梯度裁剪(1.6.6)、降学习率 |
1.3.5 平滑化与门控:GELU、SiLU 与 GLU 家族
平滑化路线。 GELU 与 SiLU 都把 ReLU 的硬折角换成光滑曲线:
两者都可以写成「输入 × 一个门」的形式:GELU 的门是标准正态 CDF,SiLU 的门是 Sigmoid。它们与原 ReLU 的差别有三点:在 附近有非零的负值区间(允许小幅度反向响应)、在负半区导数为小正数(不会永久死亡)、处处可导(对二阶方法、平滑分析更友好)。代价是需要指数或误差函数的计算(GELU 的 tanh 近似缓解了一部分),以及在部分任务上相对 ReLU 并没有稳定优势——需要以「实证结果」而非「数学更漂亮」来判断。
门控路线。 GLU 家族换了一个思路:不再对每个坐标单独施加一个固定形状的弯曲,而是用同一输入的另一个线性投影生成一个逐元素的门:
给定输入 ,线性门控单元定义为
其中 为逐元素积, 为门控激活。替换门控激活即得变体:GeGLU(用 GELU)、SwiGLU(用 SiLU / Swish)、ReGLU(用 ReLU)。在现代 Transformer 的前馈层里,通常写成 。
ReLU 的开关是固定的: 就放行。GLU 的开关是学出来的: 可以依赖输入的任意方向,网络因此能表达「在特征 A 出现时放大特征 B,在特征 A 消失时关闭特征 B」这类条件性关系。这就是为什么门控在语言模型里特别有效——语言里的依赖本来就是条件性的(下一个词是什么,取决于前面出现的是什么)。
- GLU 不是「激活函数」,而是一个含参数的层。它自带两个线性投影 (以及后续的 ),因此参数量比「一个线性层 + 一个逐元素激活」更多。工程上为了保持总参数量可比,通常会把隐藏宽度按比例缩小(文献中常见做法是缩到原来的约 ,具体比例与实现相关,需核实你所参考的具体实现)。
- 「SwiGLU 一定优于 GELU」是过强的说法。它在若干公开的语言模型对比中被报告为更好,但结论依赖模型规模、数据与训练配置,不能无条件外推。稳妥的表述是:截至 2025 年,多数开源大模型的前馈层默认采用 SwiGLU 类门控(如 LLaMA 系列的公开配置),这是一个社区默认做法,而它的收益幅度需要在你自己的配置上验证。
顺带说清 RMSNorm。 现代大模型在归一化上普遍从 LayerNorm 换成了 RMSNorm:它只做「按均方根缩放」,不减均值、不学偏置,计算更省且效果相当。这是与激活选择同时发生的另一处默认变更,原理与对比放在 1.7.4 统一处理。
某隐藏层使用 tanh 激活,某个单元在训练集上的预激活集中在 附近(例如该单元已学到一个较大的偏置)。批大小 。
(1) 求该单元的 ;(2) 若这一层还有 4 个同量级的 tanh 层在它下面(即梯度要再连乘 4 次),第一层收到的梯度是被衰减了多少倍?(3) 把激活换成 ReLU,在 处这个衰减是多少?(4) 为什么这说明「饱和」不是「梯度为零」而是「梯度被系统性打折」?
(1) ,故 。注意它不是零——这正是「饱和」而非「死亡」。
(2) 单层衰减 ,五层连乘(本层 + 下方 4 层)为 。也就是说,这一路的梯度在传到第一层时已经剩下不到百亿分之一。
(3) ReLU 在 处的导数是 ,五层连乘为 。同样的深度、同样的预激活量级,仅因激活不同,梯度相差 倍。
(4) 因为 :如果输入分布改变(比如归一化层把 拉回 附近,或数据重新标准化),导数会立刻回升到接近 ——单元可以恢复。而死亡 ReLU 的导数是精确的 ,参数永不更新,也就没有任何机制能把它救回来。判据:打印每层梯度范数,若随深度呈几何衰减(比值稳定),是饱和;若某一层直接变成 且此后不再变化,是死亡。
∎某 Transformer 的模型维度 ,基线前馈层是「两层线性 + 激活」:d_model → 4·d_model → d_model(不含偏置)。若改用 SwiGLU(三个投影:gate / up / down),并要求总参数量与基线一致,中间宽度 应取多少?
基线参数量(不含偏置):。
SwiGLU 参数量(不含偏置):gate、up、down 三个投影,输入输出分别是 、、,合计
令二者相等:。
代入 :。实际工程中通常取一个便于并行切分的整数(如 11008 —— 这是公开的 LLaMA 系列配置中常见的取值,具体数字以你使用的模型配置为准),并把结果与「 量级」核对。
读法: 这个倍数是「三个矩阵对两个矩阵」的必然结果;它也解释了为什么改用 GLU 家族时必须重算前馈层宽度,而不是保留 不动(那会让参数量变成原来的 倍)。
∎1.3.6 形状、参数量与显存:激活选择改变了什么
| 组件 | 参数量(隐藏宽 、前馈宽 ) | 相对代价 | 说明 |
|---|---|---|---|
| 线性层 + 逐元素激活 | (d→4d 与 4d→d 两块) |
GELU / ReLU / SiLU 属于此类 | |
| SwiGLU(未缩放宽度) | 三个投影:gate / up / down | ||
| SwiGLU(宽幅缩至约 ) | 量级 | 约 | 让参数量与原方案接近的常见做法(比例需核实具体实现) |
逐元素激活不增加参数量,但增加激活显存:反向传播需要保存前向的中间结果,ReLU 只需保存一个 0/1 掩码(甚至可用输入符号重算),而 GELU / SiLU / GLU 的门值需要保存浮点结果。对宽度 、批 、序列长 的前馈层,掩码方案约 比特量级,门控方案约 个浮点数——两者相差一个数据格式的倍数( 倍)。这就是「换激活会让显存涨一点」的算术依据,也是激活重计算(activation checkpointing)要处理的量级(见 第 9 讲)。
1.3.7 代码:激活的导数、死亡比例与门控层
import torch, torch.nn as nn, torch.nn.functional as F
# ① 用自动微分核对 1.3.2 表里的导数结论(Sigmoid 上限 1/4、tanh 上限 1、ReLU 为 0/1)
z = torch.linspace(-6, 6, 13, requires_grad=True)
for name, fn in [("sigmoid", torch.sigmoid), ("tanh", torch.tanh),
("silu", F.silu), ("gelu", F.gelu), ("relu", F.relu)]:
g = torch.autograd.grad(fn(z).sum(), z, create_graph=False)[0]
print(f"{name:8s} max|grad| = {g.abs().max():.4f} min|grad| = {g.abs().min():.4f}")
# ② 死亡 ReLU 侦察:统计每层激活为 0 的比例(判据来自 1.3.4 的表)
def dead_ratio(model, x, threshold=1e-8):
ratios, h = [], x
for layer in model:
h = layer(h)
if isinstance(layer, nn.ReLU):
# 只看 ReLU 输出:逐样本、逐通道统计“恒为零”的比例
ratios.append((h.abs() < threshold).float().mean().item())
return ratios
net = nn.Sequential(nn.Linear(64, 256), nn.ReLU(),
nn.Linear(256, 256), nn.ReLU(),
nn.Linear(256, 64), nn.ReLU())
# 极端初始化(偏置全设为很负的数):人为制造大面积死亡,观察比例飙升
for m in net.modules():
if isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, nonlinearity="relu")
nn.init.constant_(m.bias, -3.0) # 反例:把单元整体推到负半区
print("dead ratio (bad init):", dead_ratio(net, torch.randn(512, 64)))
for m in net.modules():
if isinstance(m, nn.Linear):
nn.init.zeros_(m.bias) # 修正:偏置归零,死亡比例显著回落
print("dead ratio (fixed ):", dead_ratio(net, torch.randn(512, 64)))
# ③ 门控前馈层的形状与参数量:对照 1.3.6 的表
class SwiGLUFeedForward(nn.Module):
def __init__(self, d_model, d_ff):
super().__init__()
self.gate = nn.Linear(d_model, d_ff, bias=False) # 门控投影
self.up = nn.Linear(d_model, d_ff, bias=False) # 值投影
self.down = nn.Linear(d_ff, d_model, bias=False) # 回到模型维度
def forward(self, x): # x: (B, T, d_model)
# 门与值逐元素相乘:门控制每个特征位置放行多少
return self.down(F.silu(self.gate(x)) * self.up(x)) # -> (B, T, d_model)
ff = SwiGLUFeedForward(512, 1368) # 4d=2048 时的常见缩放取整(比例需核实具体实现)
print("SwiGLU params:", sum(p.numel() for p in ff.parameters())) # 3*d*d_ff
plain = nn.Sequential(nn.Linear(512, 2048, bias=False), nn.SiLU(),
nn.Linear(2048, 512, bias=False))
print("plain params:", sum(p.numel() for p in plain.parameters())) # 2*d*2048
1.3.8 陷阱与勘误
- 「隐藏层用 Sigmoid 更『神经』」:直接落进定理 1.3.1 的 陷阱。隐藏层默认用 ReLU 族;Sigmoid 只用在需要 输出的位置(二分类输出层、门控里的门)。
- 「ReLU 的稀疏性天然更好」:稀疏本身不是优点。负区输出为零意味着该路径没有学到任何东西;只有当网络宽度足够、且负区分布是数据驱动的(而非初始化偏差导致)时,稀疏才是无害的。
- 「换更好的激活就能提点」:激活的收益与初始化、学习率、归一化层强耦合。只换激活而不动其他超参,常常看不到差别——这也是「GELU 比 ReLU 好」的实验结论在不同配置下不一致的原因之一。
- 「输出层也可以用 ReLU / GELU」:输出层要做的是「映射到任务取值域」。回归输出若有明确非负约束才考虑 ReLU / Softplus;分类必须用 softmax(且通常与交叉熵一起由框架的
CrossEntropyLoss处理,见 1.4.6)。 - 「GLU 里的 与 Sigmoid 输出层是一回事」:形式相同,语义不同。门控里的 是逐元素的乘性权重,不是概率分布;不同的门控激活(Sigmoid / GELU / SiLU)会得到不同变体,不能随意替换。
- 「激活函数与归一化层可以互相替代」:不能。激活提供逐元素非线性(改变的是「函数形状」),归一化提供跨样本或跨特征的统计标准化(改变的是「分布的尺度与位置」)。两者作用维度不同,缺一不可。
框架里的 nn.ReLU(inplace=True) 会就地覆盖输入张量以省内存,但它会破坏反向传播所需的中间值(反向时要用到预激活的符号)。在残差连接、激活检查点(activation checkpointing)等场景里,原地操作会导致梯度错误或直接报错。默认建议用 inplace=False,只在显存确实吃紧且该激活的输出不再被其他分支使用时才打开。
另一个是精度问题:SiLU / GELU 在 很负时输出极小(SiLU 在 处约 ),在 FP16 中仍可表示,但在 BF16 中靠近 量级时会损失有效位;当这些值还要经过多次连乘(门控 × 投影)时,低精度下的误差会被放大。工程上的做法是:门控乘积通常在更高精度或使用融合算子中计算,而不是逐元素拆开做。具体实现细节依赖框架版本,遇到低精度下的异常发散时,这是应当检查的一处(需核实具体框架的融合策略)。
Q1 写出 Sigmoid 及其导数,说明为什么「Sigmoid 深层网络训不动」可以用一个数字(导数上限)概括。
Q2 死亡 ReLU 与 Sigmoid 饱和这两种「梯度为零」的现象,在机制上有何不同?各自的首选处置是什么?
Q3 GELU 与 ReLU 在 时的行为差别是什么?这个差别为什么有助于缓解死亡问题?
Q4 为什么把前馈层的 ReLU 换成 SwiGLU 时,往往要同时把隐藏宽度从 调小?
参考要点
Q1 ,,在 取等。反向传播要连乘各层导数,于是 层网络至少把梯度乘上 : 时约 。这个上限是结构性的,与数据、学习率无关。
Q2 机制不同:Sigmoid 的饱和是当前输入落在饱和区导致导数很小但非零,输入分布变化后单元可以恢复;死亡 ReLU 是导数恒为 0,且因为梯度为零,参数永不更新,单元无法自行恢复(自我强化)。处置:饱和问题换 ReLU 族并配对 Xavier / Kaiming 初始化;死亡问题换 GELU/SiLU、降低学习率、加 warmup、或加归一化层。
Q3 时 ReLU 恒为 、导数恒为 ;GELU 渐近为 ,但导数是一个很小但严格为正的值( 项贡献 量级的负向修正,整体趋向 但过程中不为零);更准确地说,SiLU 在负区的导数在 附近达到约 的极小值,而不是恒零。这意味着负区单元仍能收到(反向的)梯度信号,从而有机会回到正区。
Q4 因为 SwiGLU 有三个权重矩阵(gate / up / down)而不是两个,若不改宽度,参数量与计算量约变为原来的 倍( 个矩阵对 个矩阵)。为保持与基线可比的总参数量与显存占用,常见的做法是把隐藏宽度按比例缩小(缩到约 ),具体比例与实现和模型规模有关。
- Hendrycks & Gimpel, Gaussian Error Linear Units (GELUs)(2016,arXiv:1606.08415)——GELU 原始论文,含 tanh 近似。
- Elfwing, Uchibe & Doya, Sigmoid-Weighted Linear Units for Neural Network Function Approximation in Reinforcement Learning(2017,arXiv:1702.03118)——SiLU / Swish 的原始来源之一。
- Ramachandran, Zoph & Le, Searching for Activation Functions(2017,arXiv:1710.05941)——把 SiLU 作为搜索结果的报告,含与 ReLU 的对照实验。
- Dauphin, Fan, Auli & Grangier, Language Modeling with Gated Convolutional Networks(2017,arXiv:1612.08083)——GLU 的提出与门控动机。
- Shazeer, GLU Variants Improve Transformer(2020,arXiv:2002.05202)——GeGLU / SwiGLU 在 Transformer 上的对比与宽幅缩放。
- Zhang & Sennrich, Root Mean Square Layer Normalization(2019,arXiv:1910.07467)——RMSNorm 原始论文(细节见 1.7.4)。
本节小结:激活函数是网络里唯一的非线性来源,它的选择由「导数会不会塌到零」这一条主线决定:Sigmoid 在两端塌(定量上界 ),ReLU 在负半区塌(死亡单元不可自愈),GELU / SiLU 用平滑与小斜率折中,GLU 家族把门控交给学出来的投影。激活必须与初始化(Xavier / Kaiming)、归一化层配合使用,单独换激活通常看不到稳定收益。下一节回答一个被激活问题掩盖的、更根本的问题:什么叫「好」——损失函数是怎么把任务翻译成可优化的目标的。