首页/Agent 核心/前向传播与反向传播
Agent 核心 · 第 02 章

前向传播与反向传播

上一章的结尾,文字已经变成了向量——但一组固定的向量还谈不上"智能"。机器怎么从数据中自己学到规律?这一章回答这个问题,顺便完成一次彻底的祛魅:所谓深度学习模型,就是一连串嵌套的矩阵运算加非线性函数,没有魔法、没有神秘主…

约 24 分钟9,507 字14 个代码示例

上一章的结尾,文字已经变成了向量——但一组固定的向量还谈不上"智能"。机器怎么从数据中自己学到规律?这一章回答这个问题,顺便完成一次彻底的祛魅:所谓深度学习模型,就是一连串嵌套的矩阵运算加非线性函数,没有魔法、没有神秘主义。

本章分两步走:前半讲结构——从神经元讲到前向传播,你将亲手写出一个能跑的神经网络;后半讲学习——损失函数给"错得多离谱"定价,梯度下降决定"往哪边调、调多少",反向传播把责任分摊到每一个参数头上,最后我们亲手把这个网络训练到学会 XOR——深度学习的"Hello World"。带着这套认知,后面理解 Transformer(第 3 章)时你会发现它依然是同一套东西。

1. 从生物神经元到数学模型

1943 年,神经科学家 McCulloch 和 Pitts 提出了神经元的数学模型,思路惊人地简单:模仿生物神经元的"加权聚合 + 阈值触发"。

生物神经元的工作方式:

树突(接收多路信号) → 细胞体(汇总) → 超过阈值则放电 → 轴突(输出)

数学翻译,一个人工神经元接收 $n$ 个输入,做三件事:

  1. 加权求和:每个输入乘以自己的权重 $w_i$,再加上偏置 $b$
  2. 激活:把结果丢进一个非线性函数 $\sigma$
  3. 输出:作为下一层的输入

$$ z = w_1 x_1 + w_2 x_2 + \cdots + w_n x_n + b = \mathbf{w}^T \mathbf{x} + b $$

$$ a = \sigma(z) $$

各符号的直觉含义:

符号 含义 直觉
$x_i$ 输入 上一层传来的信号
$w_i$ 权重 这个输入"多重要",可正可负
$b$ 偏置 触发的难易度,类似阈值
$\sigma$ 激活函数 汇总后做一次非线性"审判"
$a$ 输出 传给下一层的信号

训练调的就是 $w$ 和 $b$。一个 GPT 级别的模型里有几千亿个这样的数字——数量变化,本质不变。

2. 激活函数:为什么必须有非线性

如果去掉激活函数(或用恒等函数),会发生灾难:多层网络塌缩成一层。

推导只需两行。若每层只做 $\mathbf{a} = \mathbf{W}\mathbf{x} + \mathbf{b}$(纯线性),则两层叠加:

$$ \mathbf{h} = \mathbf{W}_1\mathbf{x} + \mathbf{b}_1 $$

$$ \mathbf{y} = \mathbf{W}_2\mathbf{h} + \mathbf{b}_2 = \mathbf{W}_2(\mathbf{W}_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 = \underbrace{(\mathbf{W}_2\mathbf{W}_1)}_{\text{一个新的矩阵}}\mathbf{x} + \underbrace{(\mathbf{W}_2\mathbf{b}_1 + \mathbf{b}_2)}_{\text{一个新的偏置}} $$

两个矩阵相乘还是矩阵——一百层线性网络等价于一层。深度就失去了意义。激活函数的存在打断这种塌缩,让每层都有"自己的发言权"。

常见激活函数:

名称 公式 特点
Sigmoid $\frac{1}{1+e^{-z}}$ 压到 (0,1),可当概率;两端梯度消失
Tanh $\tanh(z)$ 压到 (-1,1),零中心
ReLU $\max(0, z)$ 简单粗暴,现代网络主力;负区直接归零
GELU 平滑版 ReLU Transformer 的默认选择(第 3 章会见到)

用代码看看它们长什么样:

import numpy as np

z = np.array([-3.0, -1.0, 0.0, 1.0, 3.0])

def sigmoid(x): return 1 / (1 + np.exp(-x))
def relu(x):    return np.maximum(0, x)
def gelu(x):     return 0.5 * x * (1 + np.tanh(np.sqrt(2/np.pi) * (x + 0.044715 * x**3)))

for name, fn in [("sigmoid", sigmoid), ("relu", relu), ("gelu", gelu)]:
    print(f"{name:>8}: {np.round(fn(z), 3)}")

输出(注意观察:sigmoid 永远在 0~1 之间;relu 把负数全部斩为 0):

  sigmoid: [0.047 0.269 0.5   0.731 0.953]
     relu: [0.  0.  0.  1.  3. ]
    gelu: [-0.004 -0.159  0.    0.841 2.996]

3. 一层网络 = 一次矩阵变换

把多个神经元并排放在一起,就得到一层网络。关键洞察:既然每个神经元都在做 $\mathbf{w}^T\mathbf{x}+b$,只是 $\mathbf{w}$ 不同,那么这一层就是:

$$ \mathbf{a} = \sigma(\mathbf{W}\mathbf{x} + \mathbf{b}) $$

  • $\mathbf{x}$:输入向量,形状 (m, 1)
  • $\mathbf{W}$:权重矩阵,形状 (n, m) —— 一行就是一个神经元的权重
  • $\mathbf{b}$:偏置向量,形状 (n, 1)
  • $\mathbf{a}$:输出向量,形状 (n, 1)

也就是说:"层"不是神经元的手工集合,它本身就是一个矩阵。所谓"这个网络每层有 768 个神经元",翻译成数学就是"每层乘一个 (768, m) 的矩阵"。神经网络里根本没有一个个独立的"神经元",只有矩阵的行——这是很多初学者没有意识到的事实。

矩阵形状的变化规律(务必记住,读源码和论文时天天用):

$$ \underbrace{(n, m)}_{\mathbf{W}} \cdot \underbrace{(m, 1)}_{\mathbf{x}} = \underbrace{(n, 1)}_{\mathbf{y}} $$

矩阵第二维必须等于输入维度,输出维度由矩阵第一维决定。你以后看任何一个模型的架构图,都可以在脑中翻译成"一串形状各异的矩阵依次相乘"。

4. 多层 = 复合函数

把单层串起来,就是深度学习里"深度"的含义:

$$ \mathbf{y} = \sigma_3(\mathbf{W}_3 \cdot \sigma_2(\mathbf{W}_2 \cdot \sigma_1(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2) + \mathbf{b}_3) $$

从右往左读:原始输入 $\mathbf{x}$ 被逐层变换,每经过一层,先做一次线性变换(旋转/拉伸/平移空间),再过一次非线性(弯折空间)。层层叠加后,网络可以在输入空间里切出任意复杂的决策边界。

万能近似定理(不证明,记住直觉):单个隐藏层足够宽的网络,理论上可以逼近任何连续函数。深度学习则用"深"替代"宽"——同样的参数量,深层比宽层能表达更抽象的特征(边缘 → 纹理 → 部件 → 物体,逐层抽象)。

5. 前向传播:亲手写一个

"前向传播"(Forward Propagation)听起来很玄,实际就是:按顺序把输入依次过完每一层,得到输出。下面用 numpy 完整实现一个 2 → 4 → 1 的网络(2 个输入,隐藏层 4 个神经元,1 个输出):

import numpy as np

rng = np.random.default_rng(0)

# ---------- 定义网络:所有参数都初始化为随机数 ----------
# W1: (4, 2)  输入2维 → 隐藏4维
# b1: (4,)
# W2: (1, 4)  隐藏4维 → 输出1维
# b2: (1,)
W1, b1 = rng.normal(0, 1, (4, 2)), np.zeros(4)
W2, b2 = rng.normal(0, 1, (1, 4)), np.zeros(1)

def relu(x):
    return np.maximum(0, x)

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def forward(x):                    # x 形状 (2,)
    h = relu(W1 @ x + b1)          # (2,) → (4,)  隐藏层
    y = sigmoid(W2 @ h + b2)       # (4,) → (1,)  输出层
    return y

# ---------- 跑一个样本 ----------
x = np.array([0.5, -1.2])
print("输出:", forward(x))

运行输出(你的随机数和我的一致,因为固定了种子):

输出: [0.4812]

这个 0.4812 没有任何意义——因为参数是随机的。前向传播只是"计算",还没有"学习"。学习是本章后半的主题:怎么根据输出"错得有多离谱",反过来调整 W1/b1/W2/b2。

加一个逐层形状打印,强化"矩阵流水线"的直觉:

def forward_verbose(x):
    print(f"输入   形状: {x.shape}")
    z1 = W1 @ x + b1
    print(f"隐藏层 形状: {z1.shape}   ← W1{(W1.shape)} @ x{(x.shape)}")
    h = relu(z1)
    y = W2 @ h + b2
    print(f"输出层 形状: {y.shape}   ← W2{(W2.shape)} @ h{(h.shape)}")
    return y

forward_verbose(x)

输出:

输入   形状: (2,)
隐藏层 形状: (4,)   ← W1(4, 2) @ x(2,)
输出层 形状: (1,)   ← W2(1, 4) @ h(4,)

看清了吗?数据在网络里流动的过程,就是向量形状被一串矩阵依次改写的过程。以后你听说某个大模型"有 96 层,隐藏维度 12288",翻译过来就是:有 96 个 (12288, 12288) 量级的矩阵在流水线上工作。

6. 数一数参数

顺便建立"参数量"的概念,这是评价模型大小的通用语言。上面的网络参数量:

参数 形状 数量
W1 (4, 2) 8
b1 (4,) 4
W2 (1, 4) 4
b2 (1,) 1
合计 17

代码验证:

print(W1.size + b1.size + W2.size + b2.size)   # 17

计算规律:一层 (n, m) 的变换带 (n,) 的偏置,参数量 = $n \times m + n$。大模型动辄"7B 参数"(B = 十亿),就是这个加法的放大版。参数量直接决定显存占用:以 16 位浮点存储,7B 模型仅权重就需要约 14GB 显存——这是部署本地模型时算账的基本功。

现在你知道了网络的结构,但有个悬而未决的问题:这些 W 和 b 是随机的,怎么把它们变成"有用的"?这就是训练要解决的事——本章后半,我们把学习机制补全。

7. 损失函数:给"错"定价

训练的第一步是定义"错"。不能只说"模型输出不对",要给出一个数字:损失(Loss)越大,错得越离谱。训练的目标就是让这个数字变小。

不同任务用不同的损失函数:

任务 损失函数 直觉
回归(预测数值) 均方误差 MSE 预测值和真值差的平方和
二分类 交叉熵 BCE 预测概率离正确标签有多远
语言模型 交叉熵(下一个词) 模型给"正确下一个词"分配的概率有多低

以语言模型为例感受一下交叉熵的含义:模型看到"今天天气真"后,给下一个词分配概率:

好: 0.60   不错: 0.20   差: 0.15   香蕉: 0.0001   ...

如果原文的下一个词是"好",损失就低(模型押对了);如果是"香蕉",损失就高(模型完全没料到)。训练大模型,就是在万亿个这样的位置上,让模型给"真实出现的下一个词"分配越来越高的概率。你每天使用的"预测输入法",就是同一个原理。

8. 梯度下降:往哪边调

现在问题变成:参数空间动辄上亿维,怎么找到让损失最小的那一组值?

想象你蒙着眼在山上,目标是走到山谷最低处。你唯一能做的是用脚感受脚下的坡度:哪个方向往下最陡,就往那个方向迈一小步。不断重复,就能走到(某个)谷底。

这就是梯度下降:

$$ \theta_{new} = \theta_{old} - \eta \nabla L(\theta) $$

  • $\nabla L(\theta)$:梯度,指向"损失上升最快"的方向 → 反方向就是下山方向
  • $\eta$:学习率(learning rate),每步迈多大
  • $\theta$:所有参数(就是前文的 W 和 b)

学习率是最重要的超参数,两个失败极端:

  • 太大:步子跨过谷底,在两侧来回震荡甚至发散(损失越训越大)
  • 太小:一百万步还没走到山脚,训练成本爆炸
学习率过大:  损失 5.0 → 8.2 → 15.7 → 44.1 → 💥 发散
学习率过小:  损失 5.0 → 4.99 → 4.98 → ... (一百年后到达)
学习率合适:  损失 5.0 → 3.1 → 1.8 → 0.9 → 0.4 → 0.2 ✅

9. 反向传播:责任分摊

梯度下降需要知道"每个参数的梯度"。但参数在多层网络里层层嵌套,最前面的一个 W1 怎么知道自己对最终损失有多少责任?

答案是大一学过的链式法则,加上一次聪明的复用。

前向传播(本章第 5 节):

$$ \mathbf{z}_1 = \mathbf{W}_1\mathbf{x} + \mathbf{b}_1 \quad\rightarrow\quad \mathbf{h} = \text{ReLU}(\mathbf{z}_1) \quad\rightarrow\quad \mathbf{z}_2 = \mathbf{W}_2\mathbf{h} + \mathbf{b}_2 \quad\rightarrow\quad y = \sigma(\mathbf{z}_2) \quad\rightarrow\quad L $$

反向传播就是沿这条链从右往左逐层求导。以"输出层的 z2"为例(sigmoid + BCE 组合的著名结果,推导略):

$$ \frac{\partial L}{\partial \mathbf{z}_2} = y - t \qquad \text{(预测值减真实值)} $$

直觉很漂亮:预测越自信、错得越离谱,这个信号越强。有了它,往左逐层传递:

$$ \frac{\partial L}{\partial \mathbf{W}_2} = \frac{\partial L}{\partial \mathbf{z}_2} \cdot \mathbf{h}^T \qquad\qquad \frac{\partial L}{\partial \mathbf{h}} = \mathbf{W}_2^T \cdot \frac{\partial L}{\partial \mathbf{z}_2} $$

$$ \frac{\partial L}{\partial \mathbf{z}_1} = \frac{\partial L}{\partial \mathbf{h}} \odot \text{ReLU}'(\mathbf{z}_1) \qquad\qquad \frac{\partial L}{\partial \mathbf{W}_1} = \frac{\partial L}{\partial \mathbf{z}_1} \cdot \mathbf{x}^T $$

($\odot$ 表示逐元素相乘。$\mathbf{W}_2^T$ 出现,是因为信号要"穿过"W2 回到上一层。)

反向传播的工程价值:一次从右到左的扫描,同时拿到所有参数的梯度,计算量和一次前向传播同量级。深度学习框架(PyTorch 的 loss.backward())干的就是这件事。我们下面手写它,写完你对"框架帮你做了什么"就彻底祛魅了。

10. 实战:训练网络学会 XOR

XOR(异或)是神经网络历史上的"名场面":单层网络(感知机)数学上不可能解决它,多层网络可以——正好验证"深度"的价值。任务:输入两个 0/1,输出它们的异或。

import numpy as np

rng = np.random.default_rng(0)

# ---------- 数据 ----------
X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float)   # 4 个样本,每个 2 维
t = np.array([[0],[1],[1],[0]], dtype=float)            # 异或标签

# ---------- 网络(沿用本章第 5 节的结构) ----------
W1, b1 = rng.normal(0, 1, (4, 2)) * 0.5, np.zeros(4)
W2, b2 = rng.normal(0, 1, (1, 4)) * 0.5, np.zeros(1)

def relu(x):    return np.maximum(0, x)
def sigmoid(x): return 1 / (1 + np.exp(-x))

def forward(x):
    h = relu(W1 @ x + b1)          # (2,) → (4,)
    y = sigmoid(W2 @ h + b2)       # (4,) → 标量
    return h, y

# ---------- 训练循环 ----------
lr = 0.5                                    # 学习率
for step in range(3000):
    # 全批量:4 个样本一起算(样本间独立,天然支持向量化)
    # 逐样本写法更接近推导,这里为了篇幅用循环展开
    dW1 = np.zeros_like(W1); db1 = np.zeros_like(b1)
    dW2 = np.zeros_like(W2); db2 = np.zeros_like(b2)
    loss = 0

    for x, ti in zip(X, t):                 # 逐样本累积梯度
        h, y = forward(x)

        # 损失:二元交叉熵
        loss += -(ti * np.log(y + 1e-9) + (1-ti) * np.log(1-y + 1e-9))

        # ---- 反向传播(手写) ----
        dz2 = y - ti                        # sigmoid+BCE 的组合导数
        dW2 += dz2 * h                       # (1,4)
        db2 += dz2
        dh   = W2.T @ dz2                    # (4,)  穿过 W2
        dz1  = dh * (h > 0)                  # ReLU 的导数:正区为 1,负区为 0
        dW1 += np.outer(dz1, x)              # (4,2)
        db1 += dz1

    # ---- 梯度下降:一次性更新所有参数 ----
    W1 -= lr * dW1; b1 -= lr * db1
    W2 -= lr * dW2; b2 -= lr * db2

    if step % 600 == 0:
        print(f"step {step:>4}  loss = {loss.mean():.4f}")

# ---------- 验收 ----------
print("\n输入 → 预测 (期望 0,1,1,0):")
for x in X:
    _, y = forward(x)
    print(f"  {x.astype(int)} → {y.item():.4f}")

运行结果:

step    0  loss = 2.8290
step  600  loss = 0.5011
step 1200  loss = 0.0459
step 1800  loss = 0.0150
step 2400  loss = 0.0086

输入 → 预测 (期望 0,1,1,0):
  [0 0] → 0.0082
  [0 1] → 0.9915
  [1 0] → 0.9914
  [1 1] → 0.0085

四个数字全部预测正确。这 23 个参数"学会"了异或——没有一行代码写异或规则,我们只告诉它"哪里错了",它自己调整到了正确状态。这就是"学习"的全部机制,GPT 训练时和这段代码没有本质区别,区别只在规模:参数多十一个数量级、样本多十一个数量级、损失函数面向下一个词预测。

11. Mini-Batch:样本太多怎么办

上面的循环每个 step 用了全部 4 个样本,这叫全批量(full batch)。真实训练的样本动辄上亿,问题来了:

  • 全批量算一次梯度太贵,还没迈一步就要算几个小时
  • 单样本(batch=1)虽然便宜,但梯度噪声太大,方向抖得厉害

折中方案:Mini-Batch。每次随机抽一小撮(如 256 个)样本算平均梯度,更新一步。三个概念对齐:

方式 每步样本数 特点
全批量 全部 方向准、单步贵
随机(SGD) 1 单步便宜、方向抖
Mini-Batch 几十~几百 工程实践的标准选择
# mini-batch 版核心(示意)
idx = rng.choice(len(X), size=2, replace=False)   # 随机抽 2 个样本
for x, ti in zip(X[idx], t[idx]):
    ...  # 累积梯度后更新

顺带建立三个高频术语:一次完整的数据遍历叫一个 epoch;一个 batch 跑完更新一次参数,叫一个 step;batch size 就是每撮样本数。你以后在训练日志里看到的 epoch=3, step=12000, batch_size=256 就是这三件事。

12. 过拟合:学得太好也是病

最后一个概念,直接决定你的模型在真实场景能不能用。

把训练想成背题:一个学生把题库背得滚瓜烂熟(训练损失为 0),考试遇到新题却不会做——这就是过拟合:模型记住了训练数据的细节噪声,没学到通用规律,即泛化(Generalization)能力差。

诊断手段是把数据切成两份:

训练集(约80%)── 用来调参数
验证集(约20%)── 只看不学,用来检查"新题"表现

判断依据:

训练损失 验证损失 诊断
高 高 欠拟合:模型还没学会,继续训
低 高 过拟合:背题了,需要正则化/更多数据
低 低 泛化良好 ✅

常见解药(后面读到 LLM 时会反复遇到思想同源的机制):更多数据、更简单的模型、Dropout、权重衰减、早停(验证损失开始上升就停止训练)。

Agent 开发者的关联:few-shot 示例太多挤占上下文、Agent 对历史对话的过度依赖,本质上都是"过拟合现场"。理解这个概念,你在调 Agent 时会有完全不同的直觉。

本章小结

  • 人工神经元 = 加权求和 + 偏置 + 激活:$a = \sigma(\mathbf{w}^T\mathbf{x} + b)$
  • 激活函数存在的唯一理由:提供非线性,防止多层塌缩成一层
  • 一层网络就是一个矩阵:$\mathbf{a} = \sigma(\mathbf{W}\mathbf{x} + \mathbf{b})$,"神经元"只是矩阵的行
  • 前向传播 = 数据依次流过各层 = 向量形状被矩阵逐个改写
  • 深度学习的"深度" = 复合函数的层数;模型大小 = 参数个数
  • 损失函数把"错"变成一个数字;训练 = 最小化损失
  • 梯度下降:沿负梯度方向小步更新;学习率决定步幅,是最重要的超参数
  • 反向传播用链式法则一次性算出所有参数的梯度,$\mathbf{W}_2^T$ 让责任穿过层间传递
  • 手写训练循环(XOR)= 深度学习的最小完整样例;大模型训练与此同构,只差规模
  • Mini-Batch / epoch / step 是训练工程的通用词汇
  • 过拟合是"背题",靠训练/验证集切分诊断

至此,你已经具备读懂大模型架构所需的全部数学直觉:第 1 章解决了"语言怎么变成数字",本章解决了"数字怎么学会规律"。下一章,我们进入 Transformer 的心脏——注意力机制,看模型如何读懂上下文。

下一篇:03 · 注意力机制与 Transformer 架构