激活函数是神经网络里的非线性开关。没有它,叠 100 层和叠 1 层完全等价——就像你串联了 100 个只做线性变换的中间件(纯转发、纯缩放),编译器一优化全 collapse 成一个矩阵。ReLU = max(0, x),等价于 SQL 的 GREATEST(0, x):负数清零、正数放行,一个最朴素的门。
核心痛点是线性可折叠:神经网络一层做的是 y = Wx + b(矩阵乘 + 偏置)。两层线性叠加 W₂(W₁x) = (W₂W₁)x 还是一个线性变换——无论多深,表达力等于单层,只能拟合直线/超平面。激活函数在每层之间插入非线性,让网络能逼近任意复杂函数(这就是「通用逼近定理」的直觉)。
早期用 Sigmoid / Tanh(S 形曲线),但它们在两端「饱和」——输入很大或很小时梯度趋近 0,反向传播时梯度逐层相乘指数衰减,深层网络学不动(梯度消失)。ReLU(2010 年代普及)的革命在于:正区间梯度恒为 1,不衰减,让深网训练成为可能。代价是 Dying ReLU:若某神经元的输入长期为负,输出恒 0、梯度恒 0,永久死亡——这是它最著名的失效模式。
import torch, torch.nn as nn # 直观证明:两层"无激活"线性网 == 一层线性网 x = torch.randn(4, 8) lin = nn.Sequential(nn.Linear(8, 16), nn.Linear(16, 8)) # 两个 Linear 串联仍是线性映射,可被一个等效矩阵替代 relu_net = nn.Sequential( nn.Linear(8, 16), nn.ReLU(), # ← 插入非线性,深度才"算数" nn.Linear(16, 8), ) print(relu_net(x).shape) # torch.Size([4, 8]) # ReLU 本体就这么简单: def relu(x): return torch.clamp(x, min=0) # max(0, x)
ReLU 是硬性 if-else 路由:负数一刀切归零。GELU 是带概率的软路由:按输入「有多大概率值得保留」平滑加权放行。SwiGLU 更进一步,是可学习的动态门控——像一个根据请求内容自己决定放行多少流量的智能负载均衡器,门开多大由数据和参数共同决定。
ReLU 的硬截断有两个毛病:① 在 0 点不可导(有个尖角);② Dying ReLU。GELU(Hendrycks & Gimpel, 2016)用一条平滑曲线替代尖角。它的定义是 GELU(x) = x · Φ(x),其中 Φ(x) 是标准正态分布的累积分布函数(CDF)——直觉是:Φ(x) 表示「一个标准正态随机数小于 x 的概率」,x 越大这个概率越接近 1(几乎全放行),x 越负越接近 0(几乎全挡掉),中间平滑过渡。所以 GELU 是「按输入的相对大小概率性加权」,而非 ReLU 那样按符号硬性门控。负值区也留了一点点信号(不会完全死),梯度更平滑。
SwiGLU(来自 Shazeer 2020《GLU Variants Improve Transformer》)不是单个激活,而是改造了 Transformer 的前馈层(FFN)结构。普通 FFN:W₂ · act(W₁x)。GLU 类结构引入门控:用两个线性投影,一路当「内容」、一路过激活当「门」,再逐元素相乘 (W_v·x) ⊙ Swish(W_g·x)。这里 ⊙ 是逐元素乘法,门的开合由输入动态决定——比固定激活多了一层数据依赖的调制能力。这是 LLaMA、PaLM 等现代 LLM 的 FFN 标配。
import torch, torch.nn as nn, torch.nn.functional as F # GELU:PyTorch 内置,一行即可 x = torch.randn(2, 8) y = F.gelu(x) # x * Φ(x),平滑版 ReLU # SwiGLU 风格的 FFN(LLaMA 同款思路) class SwiGLU_FFN(nn.Module): def __init__(self, dim, hidden): super().__init__() self.w_gate = nn.Linear(dim, hidden, bias=False) # 门 self.w_val = nn.Linear(dim, hidden, bias=False) # 内容 self.w_out = nn.Linear(hidden, dim, bias=False) def forward(self, x): # Swish(gate) 决定每个维度放行多少 value return self.w_out(F.silu(self.w_gate(x)) * self.w_val(x)) # F.silu == Swish == x*sigmoid(x);* 是逐元素门控相乘
归一化 = 在数据进入下一层前做一次标准化(z-score),把激活值拉回「均值 0、方差 1」的统一量纲——就像查询前对特征做 normalize,避免某个数量级巨大的字段淹没其他字段。区别在沿哪个维度统计:BatchNorm 依赖整个 batch 的全局统计(像依赖全局计数的限流器,batch 小就抖);LayerNorm 每个样本自己算(像 per-request 的本地归一化,不看邻居)。
痛点:深层网络训练时,每层的输入分布会随前面层参数更新而剧烈漂移,导致后面层要不断「追着移动靶子」学,训练慢且不稳。归一化把每层激活强制拉回稳定分布。公式核心是 y = γ · (x − μ) / √(σ² + ε) + β,逐项解释:μ 是均值、σ² 是方差(先把数据标准化到 0 均值 1 方差);ε 是防除零的小常数;γ、β 是可学习的缩放和平移——这一步很关键:先归一化再让模型自己学回它需要的尺度,所以归一化不会丧失表达力。
为什么 Transformer / LLM 用 LayerNorm 而非 BatchNorm?三个硬原因:① 序列长度可变、batch 内样本不齐,跨 batch 统计意义模糊;② BatchNorm 在小 batch 下统计噪声大、训练推理行为不一致(推理用滑动平均的全局统计);③ 推理时 batch 可能是 1,BatchNorm 的 batch 统计直接失效。LayerNorm 每个样本独立计算,训练和推理完全一致,与 batch 大小、序列长度全部解耦——天然适配 NLP。BatchNorm 则在 CNN 视觉里依然是主力。
import torch, torch.nn as nn x = torch.randn(32, 512) # [batch=32, features=512] bn = nn.BatchNorm1d(512) # 跨 batch:对 512 个特征各自统计 ln = nn.LayerNorm(512) # 跨特征:每个样本独立统计 # 验证统计维度的差别 # BatchNorm: 每"列"(特征)被拉成 0 均值 → mean(dim=0)≈0 print(bn(x).mean(dim=0).abs().max()) # ≈ 0 # LayerNorm: 每"行"(样本)被拉成 0 均值 → mean(dim=1)≈0 print(ln(x).mean(dim=1).abs().max()) # ≈ 0 # 推理时 batch=1:BatchNorm 需 eval() 切到全局统计,LayerNorm 无所谓 bn.eval(); print(bn(torch.randn(1, 512)).shape)
RMSNorm 是 LayerNorm 的精简版:作者发现 LayerNorm 里「减均值(re-centering)」这一步其实可有可无,于是直接砍掉,只保留「除以幅度(re-scaling)」。就像你在 RPC 协议里删掉一个分析后发现没人真正用到的字段——省了序列化开销,准确率不掉。LLaMA 全系、现代主流 LLM 都换成了它。
LayerNorm 做两件事:re-centering(减去均值 μ,让数据居中)和 re-scaling(除以标准差,统一幅度)。Zhang & Sennrich(2019)的核心假设是:真正起作用的是 re-scaling,re-centering 是冗余的。RMSNorm 据此只除以均方根(RMS, Root Mean Square):y = γ · x / RMS(x),其中 RMS(x) = √( (1/D)·Σxᵢ² )。直觉上 RMS 就是「这个向量整体有多大幅度」(各分量平方平均再开根),不需要先算均值、也不需要减法和 β 偏置。
收益:计算更少、更快(论文报告可观的加速),在大模型规模上效果与 LayerNorm 相当甚至更好。在千亿参数、每一层都要归一化无数次的 LLM 里,这点单步节省被放大成显著的总训练/推理成本下降——这是它被广泛采用的现实理由。
顺带一个相关机制:归一化放在哪。早期 Transformer 是 Post-LN(归一化在残差相加之后),深层时训练不稳、需要 warmup。现代普遍改成 Pre-LN(归一化在子层之前),梯度更稳、可去掉繁琐的 warmup——这也是「为什么现在的 Transformer 更好训」的关键改动之一。
import torch, torch.nn as nn class RMSNorm(nn.Module): def __init__(self, dim, eps=1e-6): super().__init__() self.weight = nn.Parameter(torch.ones(dim)) # 只有 γ,没有 β self.eps = eps def forward(self, x): # RMS = sqrt(mean(x^2));注意:不减均值 rms = x.pow(2).mean(dim=-1, keepdim=True).add(self.eps).rsqrt() return x * rms * self.weight # 除以幅度再缩放 x = torch.randn(2, 512) print(RMSNorm(512)(x).shape) # torch.Size([2, 512]) # torch 2.4+ 也有内置 nn.RMSNorm(512),生产直接用官方实现
Norm → Linear → 激活 的三明治反复堆叠。换个角度:激活决定「信号怎么变换」,归一化决定「信号以什么尺度进入下一次变换」——前者管形状,后者管量纲。理解这一点,你就明白为什么换激活函数常常要连带调归一化策略:它们在同一个「梯度健康」的目标上耦合。