AI/ML 详解:激活函数与归一化

Day 32 · 2026-06-18
面向:有编程经验的非 AI 方向工程师

激活函数与 ReLUActivation Functions / ReLU

非线性基石
一句话类比

激活函数是神经网络里的非线性开关。没有它,叠 100 层和叠 1 层完全等价——就像你串联了 100 个只做线性变换的中间件(纯转发、纯缩放),编译器一优化全 collapse 成一个矩阵。ReLU = max(0, x),等价于 SQL 的 GREATEST(0, x):负数清零、正数放行,一个最朴素的门。

它解决什么问题 + 工作机制

核心痛点是线性可折叠:神经网络一层做的是 y = Wx + b(矩阵乘 + 偏置)。两层线性叠加 W₂(W₁x) = (W₂W₁)x 还是一个线性变换——无论多深,表达力等于单层,只能拟合直线/超平面。激活函数在每层之间插入非线性,让网络能逼近任意复杂函数(这就是「通用逼近定理」的直觉)。

为什么必须有非线性

无激活:W₁W₂W₃ 单个 W (白堆层)
有激活:W₁ReLUW₂ReLUW₃ 单层 (深度有效)

早期用 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)
常见误区 + 实践场景
「激活函数是为了模拟生物神经元放电」——这是事后类比,不是设计动机。真正的动机是纯数学:打破线性可折叠性。把它理解成「让深度有意义的非线性算子」比「模拟大脑」准确得多——后者会让你在调参时产生错误的直觉。
📌 BigCat 场景:当你读到某个模型「换了激活函数就涨点」时,别当玄学。它本质是在改变梯度如何流动哪些神经元被激活——和你调分布式系统里的「限流阈值/熔断曲线」是同一类「改变信号传递特性」的决策。
Takeaway + 思考题
💡 激活函数不是装饰,是深度学习「深」字成立的前提——没有它,万层网络等于一层。
🤔 如果非线性是表达力的来源,那「更强的非线性」是否总更好?为什么实践中大家偏爱 ReLU 这种近乎线性的简单函数,而不是高度弯曲的复杂曲线?

GELU 与 SwiGLUGELU / SwiGLU

现代激活门控LLM 标配
一句话类比

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 那样按符号硬性门控。负值区也留了一点点信号(不会完全死),梯度更平滑。

三种激活在负区/正区的行为
输入 x: -3 -1 0 1 3
ReLU   0 0 0 1 3 硬截断、有尖角
GELU  ≈0 -.16 0 .84 2.99 平滑、负区微漏
↑ GELU 在负区保留少量信号,0 点处处可导

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);* 是逐元素门控相乘
常见误区 + 实践场景
「SwiGLU 比 ReLU『更非线性』所以更强」——不准确。SwiGLU 的增益主要来自门控带来的乘法交互(数据依赖地放大/抑制特征),不是非线性更陡。注意:GLU 类把单层拆成两路投影,参数变多,所以实践中会把隐藏维 hidden 缩到约 2/3 来保持总参数量持平——这是论文里的细节,否则比较不公平。
📌 BigCat 场景:理解「门控」这个机制本身比记住名字重要——它和你熟悉的 feature flag / 动态路由 / 按请求内容做的条件放行是同构的。LLM 内部到处是这种「让数据自己决定信号通路」的设计,attention 也是其一。
Takeaway + 思考题
💡 从 ReLU 到 GELU 到 SwiGLU,主线是「硬开关 → 软加权 → 可学习门控」——越来越让数据自己决定信号怎么流。
🤔 门控(乘法交互)和加深网络(堆更多层)都是增强表达力的手段。在固定参数预算下,你会怎么权衡「更宽的门控 FFN」和「更深的简单层」?

批归一化 vs 层归一化BatchNorm vs LayerNorm

归一化训练稳定
一句话类比

归一化 = 在数据进入下一层前做一次标准化(z-score),把激活值拉回「均值 0、方差 1」的统一量纲——就像查询前对特征做 normalize,避免某个数量级巨大的字段淹没其他字段。区别在沿哪个维度统计BatchNorm 依赖整个 batch 的全局统计(像依赖全局计数的限流器,batch 小就抖);LayerNorm 每个样本自己算(像 per-request 的本地归一化,不看邻居)。

它解决什么问题 + 工作机制

痛点:深层网络训练时,每层的输入分布会随前面层参数更新而剧烈漂移,导致后面层要不断「追着移动靶子」学,训练慢且不稳。归一化把每层激活强制拉回稳定分布。公式核心是 y = γ · (x − μ) / √(σ² + ε) + β,逐项解释:μ 是均值、σ² 是方差(先把数据标准化到 0 均值 1 方差);ε 是防除零的小常数γ、β 是可学习的缩放和平移——这一步很关键:先归一化再让模型自己学回它需要的尺度,所以归一化不会丧失表达力。

关键区别:沿哪个轴算 μ 和 σ
张量 [batch=N, features=D]

BatchNorm:对每个特征,跨整个 batch 统计 (↓ 竖着切)
  样本1样本2样本3 → 同一列一起归一化

LayerNorm:对每个样本,跨它自己的全部特征 (→ 横着切)
  样本1 的 D 个特征 → 这一行内部归一化,与别人无关

为什么 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)
常见误区 + 实践场景
「归一化是为了消除『内部协变量偏移』」——这是原论文的原始解释,后来被质疑。Santurkar 等 2018 的研究指出,归一化真正的作用更可能是平滑了损失曲面(让梯度更稳、可用更大学习率),而非减少分布漂移。所以别把「内部协变量偏移」当成铁律——它是一个有争议的动机假说。
📌 BigCat 场景:看到模型卡里写「BatchNorm」基本能推断是视觉模型,写「LayerNorm/RMSNorm」基本是 Transformer/LLM——这是快速判断架构家族的可靠信号,和你看到「Raft/Paxos」就知道是共识系统一样。
Takeaway + 思考题
💡 归一化不改变信息、只稳定尺度;选 BatchNorm 还是 LayerNorm,本质是问「统计该跨样本算还是样本内算」——由任务的 batch/序列特性决定。
🤔 BatchNorm 在训练时偷偷引入了样本间的耦合(一个样本的归一化依赖同 batch 其他样本)。这种耦合既是正则化的来源,也是 bug 的温床——你能想到它在哪些场景会出问题吗?

均方根归一化RMSNorm

归一化精简现代 LLM
一句话类比

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 vs RMSNorm:省了哪几步

LayerNorm算均值 μ减 μ算方差除 σ×γ +β

RMSNorm(跳过居中)算 RMS除 RMS×γ 少 1 次均值、1 次减法、1 个 β

收益:计算更少、更快(论文报告可观的加速),在大模型规模上效果与 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),生产直接用官方实现
常见误区 + 实践场景
「RMSNorm 是 LayerNorm 的近似/退化版,所以更弱」——反了。它是有意的简化,建立在「re-centering 冗余」的假设上,实测效果不输还更快。误区根源是把「步骤更少」等同于「能力更弱」——在深度学习里,去掉被验证为冗余的部分恰恰是进步的常见形态(Occam 剃刀)。
📌 BigCat 场景:RMSNorm 的故事是一个漂亮的工程哲学样本——「质疑一个被默认沿用的步骤是否真的必要」。这和你在分布式系统里砍掉一个习惯性的同步屏障、发现系统更快且依然正确是同一种直觉。值得迁移到你自己的技术决策里。
Takeaway + 思考题
💡 RMSNorm = LayerNorm 砍掉「减均值」;它的广泛采用证明了:在巨大规模下,每步省下的小成本会被放大成决定性优势
🤔 「re-centering 冗余」是个经验假设而非定理。如果换一种架构或数据分布,这个假设会不会失效?你怎么设计实验去验证「某个被默认的步骤其实可以删」?

深入资源Further Reading

深入思考Deep Questions

1. 激活函数(引入非线性)和归一化(稳定尺度)看似两件事,为什么它们总是成对出现、紧挨着排在一起?
因为它们处理的是同一条数据流上的互补问题。激活函数引入非线性来获得表达力,但非线性算子(尤其饱和型)对输入尺度非常敏感——输入过大过小都会落进梯度趋零的区域,让深网难训。归一化恰好把进入激活前/后的数据尺度拉回到激活函数「工作良好」的区间,两者配合才能让深层网络既有表达力又能稳定训练。所以典型层就是 Norm → Linear → 激活 的三明治反复堆叠。换个角度:激活决定「信号怎么变换」,归一化决定「信号以什么尺度进入下一次变换」——前者管形状,后者管量纲。理解这一点,你就明白为什么换激活函数常常要连带调归一化策略:它们在同一个「梯度健康」的目标上耦合。
2. ReLU、GELU、SwiGLU 的演进里,有一条「让数据自己决定信号通路」的暗线。Attention 机制是不是同一个思想的极致版本?
是,而且 attention 是这条线的「顶点」。把这几个机制按「数据依赖程度」排序:ReLU 的门完全由符号决定(x>0 开、否则关),参数无关、跨样本固定;GELU 的门由输入大小平滑决定,仍是逐元素、无交互;SwiGLU 的门由一个可学习投影算出,开合依赖输入内容,且引入了维度间的乘法调制;Attention 则把「门」泛化成由 token 之间的相似度动态计算出的权重矩阵——每个位置看什么、看多少,完全由当前输入内容决定。一脉相承的思想是:从「固定计算图」走向「输入条件化的计算图」(input-conditioned computation)。这也解释了为什么现代架构表达力强——它们让网络在推理时根据数据动态重组信息通路,而非走死板的固定变换。BigCat 你可以把它类比成从「静态路由表」到「根据请求内容实时计算的动态路由」的演化。
3. RMSNorm 删掉「减均值」后依然 work——这件事对「深度学习里有多少『习以为常的步骤』其实是冗余的」有什么启示?
启示很深:深度学习的许多默认配置是历史惯性 + 一次性经验的产物,而非第一性原理推导的必然。RMSNorm、Pre-LN 取代 Post-LN、去掉 bias 项、用更简单的位置编码替代复杂方案——近年一连串改进的共同模式都是「质疑某个被默认沿用的组件是否必要,删掉它,发现更简单且不更差」。这背后是一个可迁移的研究方法论:(a) 消融(ablation)——系统性地逐个删除组件,观察哪些删了无害;(b) 警惕「因为大家都这么做」的设计;(c) 在大规模下重新评估小规模时代的结论(很多默认是小模型时代定的,规模变了结论可能翻转)。但要诚实:这不意味着「越简单越好」是铁律——简化有效是因为那一步恰好冗余,换个数据分布/架构未必成立。真正的能力是知道如何验证某步骤是否冗余,而不是盲目崇拜简洁。这种「敢于删、且能严谨验证删得对不对」的判断力,正是资深工程师与新手的分水岭——无论在 AI 还是分布式系统里。
4. BatchNorm 让一个样本的输出依赖同 batch 的其他样本,这种「样本间耦合」既是正则化也是 bug 源。它和分布式系统里的「共享状态」有什么共通的工程教训?
本质上是同一个问题:引入隐式共享状态会带来你没预期的耦合。BatchNorm 的「副作用正则化」很像分布式系统里「碰巧因为某个共享缓存而获得的性能」——好用,但脆弱且难推理。它的 bug 模式高度类比共享状态的经典坑:(a) 训练/推理不一致——训练用 batch 统计、推理用全局滑动平均,等价于「测试环境和生产环境状态来源不同」导致的诡异 bug;(b) 小 batch 噪声大——共享状态的样本量不足时统计失真,类比限流器在低流量时阈值乱跳;(c) 分布式训练下要跨卡同步统计(SyncBatchNorm),这就是字面意义的「分布式共享状态一致性」问题,带来通信开销和复杂度;(d) 对比学习/小 batch 任务里直接出错,因为样本间耦合污染了本应独立的表示。共通教训:共享状态是「免费的午餐」的假象——它常常带来短期收益但长期债务。LayerNorm/RMSNorm 之所以在 Transformer 时代胜出,部分正是因为它们无样本间耦合、无隐藏全局状态,训练推理行为一致、易于推理和分布式扩展——这恰是「无状态设计更可组合」这一分布式智慧在深度学习里的回声。