WARNING
🧪 Beta公测版本提示:教程主体已完成,正在优化细节,欢迎大家提Issue反馈问题或建议。
s07 多层网络的矩阵反向传播 — exercise.py 练习指南
练习目标
通过亲手实现单隐藏层的反向传播(
预备知识
建议先阅读 index.md 并运行 demo.py,确保理解以下核心公式:
| 公式 | 含义 |
|---|---|
| 输出层误差信号 | |
| 隐藏层误差递推 | |
| 权重梯度(外积) | |
| 偏置梯度(求和) |
任务清单
任务1:实现单隐藏层的反向传播(δ 递推计算)
描述:补全 single_hidden_backward() 函数。网络结构为 输入 → 隐藏层(ReLU) → 输出层(Sigmoid),损失函数使用 MSE。
代码骨架:
python
def single_hidden_backward(W1, b1, W2, b2, X, Y):
m = X.shape[1]
# ---- 前向传播 ----
Z1 = W1 @ X + b1 # 隐藏层线性变换
A1 = relu(Z1) # 隐藏层 ReLU 激活
Z2 = W2 @ A1 + b2 # 输出层线性变换
A2 = sigmoid(Z2) # 输出层 Sigmoid 激活 → 预测值
# ---- 反向传播 ----
dA2 = (1.0 / m) * (A2 - Y) # ∂L/∂A2 — MSE 损失的梯度
dZ2 = dA2 * sigmoid_derivative(Z2) # δ[2] = ∇_A L ⊙ φ'(Z2)
dW2 = dZ2 @ A1.T # ∂L/∂W2 = δ[2] @ (A1)^T
db2 = np.sum(dZ2, axis=1, keepdims=True) # ∂L/∂b2
dZ1 = (W2.T @ dZ2) * relu_derivative(Z1) # δ[1] = W2^T @ δ[2] ⊙ ReLU'(Z1)
dW1 = dZ1 @ X.T # ∂L/∂W1 = δ[1] @ X^T
db1 = np.sum(dZ1, axis=1, keepdims=True) # ∂L/∂b1关键提示:
- 前向顺序:先
,再 ,然后 ,最后 。不要搞反! - 输出层 δ:MSE 损失对
输出的梯度是 ,再乘以 。 - δ₁ 递推:这是整个练习的核心——把输出层的误差通过
传回隐藏层,再经过 ReLU 导数门控。注意: 。 - 权重梯度:
, 。这是误差信号与输入的外积(矩阵乘法)。 - 偏置梯度:对
按 axis=1 求和,并用 keepdims=True保持形状。
维度检查表(以 2 输入 → 3 隐藏 → 1 输出,2 个样本为例):
| 变量 | 形状 | 说明 |
|---|---|---|
| X | (2, 2) | 2特征 × 2样本 |
| W1 | (3, 2) | 3神经元 × 2输入 |
| b1 | (3, 1) | 广播到 (3, 2) |
| Z1, A1 | (3, 2) | 3神经元 × 2样本 |
| W2 | (1, 3) | 1神经元 × 3输入 |
| dZ2 | (1, 2) | 输出层 δ |
| dZ1 | (3, 2) | 隐藏层 δ(W2^T @ dZ2) |
| dW1 | (3, 2) | 必须与 W1 shape 一致 |
期望输出:所有梯度的形状与对应参数完全一致。
任务2:实现梯度裁剪
描述:补全 clip_gradients() 函数。当梯度的全局 L2 范数超过 max_norm 时,按比例缩小所有梯度。
数学公式:
全局 L2 范数:
缩放因子:
裁剪:
提示:
- 计算
total_norm_sq:遍历所有梯度矩阵,累加np.sum(grad ** 2) - 总范数:
total_norm = np.sqrt(total_norm_sq) - 如果
total_norm > max_norm,缩放因子scale = max_norm / total_norm;否则scale = 1.0 - 每个梯度
grad * scale
核心思想:梯度裁剪不改变梯度的方向,只限制长度——防止某一步的梯度过大导致参数跳到不稳定的区域。这在 RNN 和 Transformer 训练中是标配手段。
任务3:实现数值梯度检查
描述:补全 numerical_gradient_check() 函数。对每个参数的前 N 个元素(如 10 个),用双边有限差分验证解析梯度的正确性。
数学公式:
算法步骤:
- 对
params中的每个参数:- 将其展平(
flatten()) - 取前
n_check个元素检查
- 将其展平(
- 对每个元素:
- 保存原始值
- 构造
θ + ε:修改该元素 → 调用forward_fn计算loss_plus - 构造
θ - ε:修改该元素 → 计算loss_minus - 数值梯度
- 恢复原始值
- 计算相对误差:
abs(grad_analytic - grad_numeric) / max(abs(grad_analytic) + abs(grad_numeric), 1e-10) - 如果相对误差 >
,标记失败
为什么只检查前 10 个元素? 梯度检查极其缓慢——每个参数元素需要 2 次额外前向传播。只随机抽查 10 个元素足以发现 bug,又能保证检查在合理时间内完成。
参考判断标准:
- 相对误差
:实现大概率正确 - 相对误差
:可能有小错误 - 相对误差
:几乎肯定有 bug
关键概念速查
| 任务 | 核心公式 | 最容易错的地方 |
|---|---|---|
| TODO 1: δ 递推 | 忘记 ReLU 导数 (Z1 > 0) | |
| TODO 1: 权重梯度 | 忘记除以 | |
| TODO 2: 梯度裁剪 | 忘记 | |
| TODO 3: 梯度检查 | 忘记恢复参数原始值 |
源码位置
clone 后打开(相对仓库根目录):
docs/nn-decision/dl/matrix-backprop/code/exercise.py