神经网络
理解前馈网络和反向传播,是掌握 Transformer 的必经之路。Transformer 中的 FFN 层就是两层 MLP,注意力层的输出也要经过线性投影——神经网络的基本功贯穿始终。
在大模型体系中的位置
神经网络 ◄── 你在这里
├── MLP → Transformer FFN 层的核心组件
├── 激活函数 → GELU (GPT), SwiGLU (Llama) 的选择依据
├── 反向传播 → 模型训练的核心算法
├── 损失函数 → 交叉熵驱动 next-token prediction
├── 优化器 → AdamW 是大模型训练的标配
└── 正则化 → LayerNorm, Dropout 的取舍感知机到多层网络
单个神经元
一个神经元执行两步操作:线性变换 + 激活函数:
其中
没有激活函数的话,多层网络等价于单层(线性函数的组合仍是线性函数)。激活函数引入非线性,是网络能拟合复杂函数的关键。
多层感知机 (MLP)
多层感知机由多个全连接层堆叠而成。下面用一个两层线性网络演示前向与反向传播的完整流程:
import torch
batch_size = 4 # 样本数
in_features = 8 # 输入维度
hidden_features = 16 # 隐藏层维度
out_features = 3 # 输出维度
x = torch.randn(batch_size, in_features)
W1 = torch.randn(in_features, hidden_features, requires_grad=True)
W2 = torch.randn(hidden_features, out_features)
target = torch.randn(batch_size, out_features)
# 前向传播
h = x @ W1 # 隐藏层: [batch, in] @ [in, hidden] -> [batch, hidden]
h.retain_grad()
logits = h @ W2 # 输出层: [batch, hidden] @ [hidden, out] -> [batch, out]
logits.retain_grad()
loss = ((target - logits) ** 2).mean()
# 反向传播
loss.backward()手动验证梯度计算——理解反向传播的核心逻辑:
# 输出层梯度:手动推导 vs PyTorch autograd
N = batch_size * out_features
grad_out = 2.0 * (logits - target) / N # d(loss)/d(logits)
W2_grad_manual = h.t() @ grad_out # d(loss)/d(W2) = h^T @ grad_out
h_grad_manual = grad_out @ W2.t() # d(loss)/d(h) = grad_out @ W2^T
print(torch.allclose(W2_grad_manual, W2.grad)) # True
print(torch.allclose(h_grad_manual, h.grad)) # True
# 隐藏层梯度
W1_grad_manual = x.t() @ h_grad_manual # d(loss)/d(W1) = x^T @ d(loss)/d(h)
print(torch.allclose(W1_grad_manual, W1.grad)) # True关键结论:梯度计算是前向计算的两倍计算量。 单层网络中两者计算量相同(不需要对输入
万能逼近定理
Universal Approximation Theorem: 一个具有足够宽度的单隐藏层前馈网络,可以以任意精度逼近任何连续函数。
但"足够宽"可能意味着指数级的神经元数量。实践中我们用更深的网络(更多层、更少宽度)来高效逼近复杂函数。
激活函数
ReLU 及其变体
优点:计算简单、缓解梯度消失。缺点:负值区域梯度为 0("死神经元"问题)。
Leaky ReLU 给负值区域一个小斜率:
Sigmoid 与 Tanh
两者的输出都是有界的(sigmoid:
GELU (GPT 使用)
GELU 是 ReLU 的平滑版本,可以看作对输入的"软门控"——根据输入值的大小,以一定概率保留或丢弃。GPT 系列模型使用 GELU。
SwiGLU (Llama 使用)
其中
SwiGLU 引入了门控机制(Gate),用一个分支控制另一个分支的信息流。Llama、Mistral 等模型的 FFN 层使用 SwiGLU,需要三个权重矩阵(
损失函数
MSE (均方误差)
适用于回归任务。在分类任务中,MSE 的梯度在预测接近 0 或 1 时非常小(梯度消失),训练效率低。
交叉熵损失
交叉熵是分类任务和语言模型的标准损失函数:
在分类问题中,真实分布
完整的实现流程:
import torch
import torch.nn as nn
import torch.nn.functional as F
vocab = 50 # 特征维度
num_classes = 5 # 分类数
batch = 8 # batch size
x = torch.randn(batch, vocab)
label = torch.randint(high=num_classes, size=(batch,))
proj = nn.Linear(vocab, num_classes)
logits = proj(x)
# 方式 1:手动实现——从 softmax 概率出发
def manual_cross_entropy(labels, logits):
n = logits.size(0)
probs = F.softmax(logits, dim=-1)
# 只取正确类别的概率,再求负对数均值
correct_probs = probs[torch.arange(n), labels]
return -correct_probs.log().mean()
# 方式 2:用 log_softmax 避免数值溢出(PyTorch 内部做法)
def ce_from_logits(labels, logits):
n = logits.size(0)
log_probs = F.log_softmax(logits, dim=-1)
return -log_probs[torch.arange(n), labels].mean()
# 方式 3:PyTorch 官方接口
loss_fn = nn.CrossEntropyLoss()
print(manual_cross_entropy(label, logits))
print(ce_from_logits(label, logits))
print(loss_fn(logits, label)) # 三者结果一致交叉熵的梯度推导
下面给出完整推导。结论先行:
即 Softmax + 交叉熵 对 logits 的梯度 = 预测概率 - 真实概率。极其优雅简洁。
推导过程:
- 交叉熵对 softmax 输出
的梯度:
- Softmax 对 logits
的雅可比矩阵:
矩阵形式:
- 链式法则组合(以第
个 logit 为例):
代码验证:
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(42)
C = 7 # 类别数
z = torch.randn(1, C, requires_grad=True)
y = torch.randint(high=C, size=(1,))
# 1. PyTorch autograd
criterion = nn.CrossEntropyLoss()
loss = criterion(z, y)
loss.backward()
print("autograd 梯度:", z.grad)
# 2. 用 q - p 直接验证
one_hot = torch.zeros(1, C)
one_hot[0, y] = 1.0
q = F.softmax(z, dim=1)
grad_check = q - one_hot
print("q - p 梯度: ", grad_check.detach())
# 两者完全一致实践意义: 这就是为什么 nn.CrossEntropyLoss 接收 logits 而非 softmax 后的概率——直接用
反向传播
计算图
神经网络的每一步运算构成一个有向无环图(DAG)。前向传播沿着图从输入到输出计算结果;反向传播沿着图从输出到输入计算梯度。
链式法则在计算图上的应用
下面手动实现一个微型自动微分引擎,思路参考 Karpathy 的 micrograd,但变量命名和实现细节做了重新组织:
import math
class Scalar:
"""标量计算图节点,支持自动反向传播"""
def __init__(self, data, _inputs=(), _operator='', label=''):
self.data = data
self.grad = 0.0
self._grad_fn = lambda: None
self._inputs = set(_inputs)
self._operator = _operator
self.label = label
def __add__(self, other):
result = Scalar(self.data + other.data, (self, other), '+')
def _grad_fn():
self.grad += 1.0 * result.grad # d(a+b)/da = 1
other.grad += 1.0 * result.grad # d(a+b)/db = 1
result._grad_fn = _grad_fn
return result
def __mul__(self, other):
result = Scalar(self.data * other.data, (self, other), '*')
def _grad_fn():
self.grad += other.data * result.grad # d(a*b)/da = b
other.grad += self.data * result.grad # d(a*b)/db = a
result._grad_fn = _grad_fn
return result
def tanh(self):
t = math.tanh(self.data)
result = Scalar(t, (self,), 'tanh')
def _grad_fn():
self.grad += (1.0 - t ** 2) * result.grad # d(tanh)/dx = 1 - tanh^2
result._grad_fn = _grad_fn
return result
def backward(self):
# 拓扑排序,确保上游节点先计算梯度
order = []
seen = set()
def _topological_sort(node):
if node not in seen:
seen.add(node)
for inp in node._inputs:
_topological_sort(inp)
order.append(node)
_topological_sort(self)
self.grad = 1.0
for node in reversed(order):
node._grad_fn()使用这个引擎模拟一个简单的神经元:
# 构建计算图: out = tanh(x1*w1 + x2*w2 + b)
x1 = Scalar(3.0, label='x1')
x2 = Scalar(-1.0, label='x2')
w1 = Scalar(0.5, label='w1')
w2 = Scalar(-1.5, label='w2')
b = Scalar(2.0, label='b')
h1 = x1 * w1 # 1.5
h2 = x2 * w2 # 1.5
pre_act = h1 + h2 + b # 5.0
out = pre_act.tanh() # ≈ 0.9999
# 反向传播
out.backward()
# 现在每个节点的 .grad 都已计算完毕PyTorch 的自动微分
PyTorch 的 autograd 就是上述思想的工业级实现。设置 requires_grad=True 的张量会自动构建计算图,调用 .backward() 自动计算梯度。
import torch
x = torch.tensor(2.0, requires_grad=True)
y = x ** 2 + 3 * x + 1 # y = x^2 + 3x + 1
y.backward()
print(x.grad) # dy/dx = 2x + 3 = 7.0优化器
SGD 与动量
随机梯度下降(SGD):
带动量的 SGD: 引入"惯性",平滑梯度更新方向:
Adam 的数学推导
Adam 同时使用一阶矩(梯度的均值)和二阶矩(梯度的方差)来自适应调整每个参数的学习率:
偏差校正(训练初期
参数更新:
常用超参数:
直觉: 一阶矩提供动量(平滑方向),二阶矩提供自适应学习率(梯度大的参数步长小,梯度小的参数步长大)。
AdamW (解耦权重衰减)
标准 Adam 中,权重衰减(L2 正则化)和自适应学习率耦合在一起,效果不理想。AdamW 将权重衰减从梯度更新中解耦出来:
AdamW 是目前大模型预训练的标配优化器。
学习率调度
大模型通常使用 Warmup + Cosine Decay 策略:
- Warmup 阶段: 学习率从 0 线性增长到峰值(如 1000 步),避免训练初期梯度不稳定
- Cosine Decay 阶段: 学习率按余弦曲线从峰值衰减到接近 0
正则化
Dropout
训练时随机将一部分神经元输出置零(概率
除以
大模型的实践: GPT-3 等大模型在预训练阶段通常不使用 Dropout,因为训练数据足够大,过拟合风险低。但在微调阶段,尤其是数据量少时,会加入 Dropout。
Layer Normalization
其中
为什么 Transformer 选择 LayerNorm 而非 BatchNorm?
- BatchNorm 在 batch 维度上归一化,依赖 batch size,在序列长度不一致时不适用
- LayerNorm 在特征维度上归一化,与 batch size 和序列长度无关
RMSNorm(Llama 使用): LayerNorm 的简化版,去掉了均值中心化:
计算更快,效果相当。
Weight Decay
权重衰减等价于对参数施加 L2 正则化,鼓励参数值保持较小,防止过拟合:
在 AdamW 中以解耦形式实现(见优化器部分)。
前沿优化器:Muon
核心思想
AdamW 是当前大模型训练的标配,但它是 element-wise 的优化器——每个参数独立维护动量和方差。Muon(Matrix-based Update Optimization via Newton-schulz) 则利用了权重矩阵的整体结构信息,通过矩阵符号函数 (msign) 对梯度进行正交化处理,实现更高效的参数更新。
Muon 算法的核心更新规则:
其中
(
直觉理解: msign 是 sign 函数从标量到矩阵的推广。标量 sign(x) 只保留符号、丢弃幅度;msign(M) 保留矩阵的"方向"(正交结构
Newton-Schulz 迭代:高效计算 msign
直接对大矩阵做 SVD 计算量为
Muon 官方使用的系数为
简化版 Muon 实现
以下是简化版 Muon 优化器实现:
import torch
class SimpleMuon:
"""简化版 Muon 优化器"""
def __init__(self, params, lr=1e-3, momentum=0.95, ns_steps=5):
self.lr = lr
self.momentum = momentum
self.ns_steps = ns_steps
self.state = {id(p): torch.zeros_like(p) for p in params}
def _newton_schulz(self, M, steps=5):
"""Newton-Schulz 迭代求 msign"""
a, b, c = 3.4445, -4.7750, 2.0315
X = M / (M.norm(dim=(-2, -1), keepdim=True) + 1e-7)
for _ in range(steps):
A = X @ X.mT
B = b * A + c * A @ A
X = a * X + B @ X
return X
@torch.no_grad()
def step(self, params, weight_decay=1e-2):
for p in params:
if p.grad is None:
continue
buf = self.state[id(p)]
# 动量累积
buf.mul_(self.momentum).add_(p.grad, alpha=1 - self.momentum)
# Nesterov 动量
M = p.grad * (1 - self.momentum) + buf * self.momentum
# msign 变换(仅对 2D 权重矩阵生效)
dW = self._newton_schulz(M, self.ns_steps) if M.dim() == 2 else M
# 权重衰减 + 更新
p.data.mul_(1 - self.lr * weight_decay).add_(dW, alpha=-self.lr)Muon vs AdamW 对比
| 特性 | AdamW | Muon |
|---|---|---|
| 更新粒度 | 逐元素 (element-wise) | 矩阵级 (matrix-wise) |
| 状态变量 | 2 组(一阶矩 + 二阶矩) | 1 组(动量) |
| 显存开销 | 参数量 x2 | 参数量 x1,更低 |
| 核心操作 | 逐元素除法 | 矩阵乘法(Newton-Schulz) |
| 适用参数 | 所有参数 | 2D 权重矩阵(Attention, FFN) |
| 收敛速度 | 基准 | 部分场景更快 |
混合训练策略
实践中,Muon 通常与 Adam 混合使用:Attention 和 FFN 的权重矩阵用 Muon,而 Embedding、RMSNorm、LM Head 等非矩阵参数仍用 Adam。
苏格拉底时刻
- 如果所有激活函数都换成线性函数,多层网络和单层网络有什么区别?这说明了激活函数的什么作用?
- 手动推导:对于
, 和 分别是什么?(提示: ) - 为什么交叉熵 + Softmax 的梯度公式如此简洁(
)?这是巧合还是设计? - Adam 优化器同时使用一阶矩和二阶矩估计,这比普通 SGD 带来了什么优势?又引入了什么额外开销?
- 为什么大模型预训练通常不使用 Dropout,但在微调阶段有时会加入?
- Layer Normalization 和 Batch Normalization 的核心区别是什么?为什么 Transformer 选择了前者?
- 梯度的计算量为什么是前向传播的两倍?(提示:每层需要对权重和输入分别求导)
常见问题 & 面试考点
| 问题 | 要点 |
|---|---|
| 反向传播的本质是什么? | 链式法则在计算图上的应用,从输出到输入逐节点计算梯度 |
| 为什么 ReLU 比 Sigmoid 好? | 梯度不饱和、计算简单、缓解梯度消失 |
| Adam vs SGD 选哪个? | Adam 收敛快但可能泛化差;SGD+动量泛化好但调参难。大模型用 AdamW |
| CrossEntropyLoss 为什么接收 logits? | 内部用 log_softmax 保证数值稳定,且梯度公式 |
| 什么是梯度裁剪? | 限制梯度范数不超过阈值,防止梯度爆炸。大模型常设 max_norm=1.0 |
| Pre-LN vs Post-LN? | Pre-LN(先 norm 再 attention/FFN)训练更稳定,是现代大模型的标准选择 |
推荐资源
- Neural Networks and Deep Learning - Michael Nielsen 经典教程
- Andrej Karpathy: Neural Networks: Zero to Hero - 从零实现 micrograd 和 GPT
- CS231n: Convolutional Neural Networks - Stanford 视觉识别课程(基础部分通用)
- Deep Learning Book - Goodfellow 等人的权威教材
- The Annotated Transformer - Transformer 逐行注释实现