核心要点

  • 前向:z1=XW1+b1 → a1=ReLU(z1) → z2=a1W2+b2 → p=Softmax(z2)

  • 反向从输出起:dz2 = p − y(Softmax+交叉熵合并梯度),逐层用链式法则回传

  • 权重梯度 dW = 前一层激活ᵀ @ 本层 dz;偏置梯度 db = dz 按样本维求和

  • ReLU 的反向就是把 z≤0 处的梯度置零(乘以掩码),缓存前向中间量供反向使用

标准回答

一、先把结论讲清楚

可以先说整体思路:前向要缓存中间量,反向从输出层开始按链式法则一层层传回去

一、前向怎么走?

两层 MLP 可以写成:z1 = XW1 + b1,a1 = ReLU(z1),z2 = a1W2 + b2,最后 p = softmax(z2)。

二、拆开机制和判断点

反向要用到 X、z1、a1、p,所以前向时要缓存这些量。

二、反向从哪里开始?

如果损失是 Softmax + Cross Entropy,输出层梯度可以合并成 dz2 = p - y。然后 dW2 = a1ᵀ @ dz2,db2 = dz2 按 batch 求和。

三、补上例子、边界和取舍

三、怎么传回第一层?

先 da1 = dz2 @ W2ᵀ,再过 ReLU 的掩码:z1 <= 0 的地方梯度置零,得到 dz1。最后 dW1 = Xᵀ @ dz1,db1 = dz1 求和。完整实现如下:

面试里不要只停在公式或名词,可以补 手撕代码:实现一个两层 MLP 的前向与反向传播 对训练稳定性、泛化、收敛速度或显存/算力的影响。再说明一个常见调参或排错场景,面试官会更容易判断你真的用过这些方法。

python
import numpy as np

def softmax(z):
    z = z - z.max(axis=1, keepdims=True)
    ez = np.exp(z)
    return ez / ez.sum(axis=1, keepdims=True)

class TwoLayerMLP:
    def __init__(self, d_in, d_hidden, d_out, seed=0):
        rng = np.random.default_rng(seed)
        # He 初始化(配合 ReLU)
        self.W1 = rng.normal(0, np.sqrt(2 / d_in), (d_in, d_hidden))
        self.b1 = np.zeros(d_hidden)
        self.W2 = rng.normal(0, np.sqrt(2 / d_hidden), (d_hidden, d_out))
        self.b2 = np.zeros(d_out)

    def forward(self, X):
        self.X = X
        self.z1 = X @ self.W1 + self.b1
        self.a1 = np.maximum(0, self.z1)        # ReLU
        self.z2 = self.a1 @ self.W2 + self.b2
        self.p = softmax(self.z2)
        return self.p

    def backward(self, y):
        N = self.X.shape[0]
        # 输出层:Softmax+交叉熵合并梯度 dz2 = p - y
        dz2 = self.p.copy()
        dz2[np.arange(N), y] -= 1.0
        dz2 /= N
        dW2 = self.a1.T @ dz2                    # (H, C)
        db2 = dz2.sum(axis=0)
        # 回传到隐藏层,过 ReLU 掩码
        da1 = dz2 @ self.W2.T
        dz1 = da1 * (self.z1 > 0)               # ReLU 导数:z>0 处为 1
        dW1 = self.X.T @ dz1                     # (D, H)
        db1 = dz1.sum(axis=0)
        return {'W1': dW1, 'b1': db1, 'W2': dW2, 'b2': db2}

    def step(self, grads, lr):
        for k in ('W1', 'b1', 'W2', 'b2'):
            setattr(self, k, getattr(self, k) - lr * grads[k])

if __name__ == '__main__':
    rng = np.random.default_rng(1)
    X = rng.normal(0, 1, (200, 4))
    y = (X[:, 0] + X[:, 1] > 0).astype(int)     # 二分类标签
    net = TwoLayerMLP(4, 16, 2)
    for epoch in range(300):
        p = net.forward(X)
        loss = -np.mean(np.log(p[np.arange(len(y)), y] + 1e-12))
        net.step(net.backward(y), lr=0.5)
    acc = (net.forward(X).argmax(1) == y).mean()
    print('loss=', round(float(loss), 4), 'acc=', round(float(acc), 3))

常见误区

⚠️ 常见踩坑

误区一:ReLU 反向不用前向缓存。 应该用 z1 或 a1 的掩码,不能凭感觉重新算。

误区二:矩阵乘顺序写反。 权重梯度一般是“输入激活的转置 @ 上游梯度”。

误区三:忘记除以 batch size 这样梯度尺度会随 batch 变化,学习率不好调。

追问

追问 1复杂度是多少?如何验证梯度正确?

前向和反向主要成本都在矩阵乘,单层大约是 O(N·d_in·d_out)。验证梯度可以做数值梯度检查:用 (L(θ+ε)-L(θ-ε))/(2ε) 和解析梯度比,相对误差通常应在 1e-5 左右。

追问 2为什么用 He 初始化而不是全零或 Xavier?

全零初始化会让同层神经元完全对称,学不出差异。ReLU 会截掉一半负值,所以常用 He 初始化,方差取 2/d_in,更能保持前向激活和反向梯度的尺度稳定。

🔗 相似问题

同一考点的不同问法,换着练更稳

延伸学习

按主题分类的相关资源,便于系统复习