标准回答
一、先把结论讲清楚
可以先说整体思路:前向要缓存中间量,反向从输出层开始按链式法则一层层传回去。
一、前向怎么走?
两层 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 的前向与反向传播 对训练稳定性、泛化、收敛速度或显存/算力的影响。再说明一个常见调参或排错场景,面试官会更容易判断你真的用过这些方法。
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,更能保持前向激活和反向梯度的尺度稳定。
🔗 相似问题
同一考点的不同问法,换着练更稳
延伸学习
按主题分类的相关资源,便于系统复习
