真实面经题目 · 原创解析

手写单层 MLP 做回归或二分类时,如何实现 forward、loss、反向传播和参数更新?

这题考的是能否从零写出一个最小神经网络训练闭环:线性层、激活、任务损失、链式法则、梯度形状和参数更新。回答要同时覆盖回归和二分类,并能解释为什么回归常用 MSE,二分类常用 sigmoid + BCE,以及如何验证梯度和训练是否正确。

出现于:MiniMax · 算法

60 秒回答模板

手写单层 MLP 时,我会先明确结构:输入 X 形状是 N×D,隐藏层或输出层权重 W、偏置 b,前向先算 z=XW+b,再根据任务选择输出。严格说 MLP 通常至少有一层非线性隐藏层,但面试里的单层 MLP 常指一个线性层加可选激活;如果是回归,可以直接输出 y_hat=z,用 MSE loss;如果是二分类,可以输出 logit z,再过 sigmoid 得到概率 p,用 BCE loss。反向传播按链式法则从 loss 对输出的梯度开始。回归 MSE 下,若 loss=mean((y_hat-y)^2),则 dL/dz=2*(y_hat-y)/N;梯度 dW=X^T dZ,db=sum(dZ)。二分类 sigmoid+BCE 的组合会简化为 dL/dz=(p-y)/N,比先分别写 sigmoid 导数和 BCE 导数更稳定。更新就是 W -= lr*dW, b -= lr*db。完整回答还要补充初始化不能全零隐藏层,输入最好标准化,BCE 要做数值稳定,检查张量形状,训练时看 loss 是否下降,并用有限差分做梯度检查。

考点 先定形状
难度 真实面经题
回答目标 让面试官看到你能手写最小神经网络训练闭环,并能解释每个公式背后的任务假设、梯度形状、数值稳定和验证方法。

深入解析

01

定义输入和参数

设 X 是 N×D 的 batch,y 是 N×1。单输出回归或二分类都可以用 W 形状 D×1、b 形状 1。若面试官要求真正的单隐层 MLP,可以扩展为 XW1+b1 经过 ReLU,再接 W2+b2;但如果题目说单层 MLP 手写回归/二分类,最小可讲清的是线性层加任务输出。关键是先把形状说清,后面梯度才不会乱。

02

回归 forward 和 loss

回归任务常直接用线性输出 y_hat=XW+b。MSE 可以写成 mean((y_hat-y)^2),也可以带 1/2 系数简化梯度。MSE 适合连续值预测且对大误差更敏感;如果异常值很多,可以提到 MAE 或 Huber,但手写题优先把 MSE 的梯度写对。

03

二分类 forward 和 loss

二分类最好保留 logit z=XW+b,再通过 sigmoid 得到 p=1/(1+exp(-z))。损失用 BCE:-mean(y log p + (1-y) log(1-p))。实现时要 clip p 或用 logits 形式避免 log(0) 和 exp 溢出。面试中要强调标签 y 通常是 0/1,预测概率 p 用阈值 0.5 或按业务调阈值转成类别。

04

反向传播

回归 MSE 的梯度从 dZ=2*(y_hat-y)/N 开始;二分类 sigmoid+BCE 合并后 dZ=(p-y)/N。然后线性层梯度都是 dW=X^T dZ,db=sum(dZ, axis=0)。如果有隐藏层,还要继续乘 W2^T 并乘激活函数导数,例如 ReLU 的导数是 z>0 时为 1,否则为 0。核心是每一步梯度形状都要和参数形状一致。

05

参数更新

最基础更新是 SGD:W -= lr*dW,b -= lr*db。实际训练会用 mini-batch、学习率衰减、动量或 Adam,但手写题不要一开始堆优化器,先把梯度和更新闭环写正确。每轮训练前向、算 loss、反向、更新,循环若干 epoch,并监控训练集 loss 和验证集指标。

06

验证和故障排查

正确性可以从三方面验证:第一,打印各张量 shape,确保广播没有误伤;第二,小数据集上 loss 应该下降,线性可分二分类应能接近高准确率;第三,用有限差分对 W 的某个元素做 gradient check。常见故障是 sigmoid 溢出、BCE log(0)、忘记除以 batch size、db 维度错误、学习率太大导致 loss 爆炸、输入未归一化导致训练慢。

python

单层 MLP 的 forward、loss、backward 和参数更新

import numpy as np

class OneLayerMLP:
    def __init__(self, in_dim, task="binary", seed=0):
        rng = np.random.default_rng(seed)
        self.W = rng.normal(0.0, 0.01, size=(in_dim, 1))
        self.b = np.zeros((1,))
        self.task = task

    def forward(self, X):
        z = X @ self.W + self.b
        if self.task == "regression":
            return z, {"X": X, "z": z, "y_hat": z}
        p = 1.0 / (1.0 + np.exp(-np.clip(z, -50, 50)))
        return p, {"X": X, "z": z, "p": p}

    def loss_and_backward(self, cache, y):
        X = cache["X"]
        y = y.reshape(-1, 1)
        n = X.shape[0]
        if self.task == "regression":
            y_hat = cache["y_hat"]
            loss = np.mean((y_hat - y) ** 2)
            dz = 2.0 * (y_hat - y) / n
        else:
            p = np.clip(cache["p"], 1e-12, 1.0 - 1e-12)
            loss = -np.mean(y * np.log(p) + (1.0 - y) * np.log(1.0 - p))
            dz = (p - y) / n
        dW = X.T @ dz
        db = dz.sum(axis=0)
        return loss, dW, db

    def step(self, dW, db, lr=0.1):
        self.W -= lr * dW
        self.b -= lr * db

# Training loop
model = OneLayerMLP(in_dim=3, task="binary")
X = np.array([[0.0, 1.0, 2.0], [1.0, 0.0, 1.0], [2.0, 1.0, 0.0]])
y = np.array([1, 0, 1])
for _ in range(200):
    pred, cache = model.forward(X)
    loss, dW, db = model.loss_and_backward(cache, y)
    model.step(dW, db, lr=0.5)
print(loss, pred.ravel())
  • 回归任务使用线性输出和 MSE,二分类任务使用 sigmoid 输出和 BCE。
  • BCE + sigmoid 的梯度可化简为 p - y,但要除以 batch size。
  • 真实面试中要补充 shape、数值稳定、学习率和梯度检查。

易错点

  • 不先说明张量形状,导致 dW、db 和输出维度混乱。
  • 二分类把 sigmoid 导数和 BCE 导数写错,或忘记 sigmoid+BCE 可简化为 p-y。
  • BCE 直接 log(p) 且不处理 p=0 或 p=1,出现 NaN。
  • 忘记对 batch 求平均,学习率对 batch size 极其敏感。
  • 只写 forward 不写 loss、backward、update,缺少完整训练闭环。
  • 把单层线性模型说成能拟合任意非线性边界,忽略模型容量限制。

面试官追问

为什么二分类不用 MSE 而常用 BCE?

二分类建模的是伯努利分布概率,BCE 对应最大似然,更符合概率输出;sigmoid+BCE 的梯度对 logit 是 p-y,优化更直接。MSE 也能训练,但在概率接近 0 或 1 时梯度形态不理想,分类收敛和校准通常不如 BCE。

如果要加一个隐藏层,反向传播怎么改?

设 h=ReLU(XW1+b1),z=hW2+b2。先按任务得到 dZ2,然后 dW2=h^T dZ2,db2=sum(dZ2)。再传回隐藏层 dH=dZ2 W2^T,dZ1=dH*(pre_hidden>0),最后 dW1=X^T dZ1,db1=sum(dZ1)。本质还是链式法则。

如何做梯度检查?

选 W 的一个元素,把它加 epsilon 和减 epsilon 分别算 loss,用 (loss_plus-loss_minus)/(2*epsilon) 近似数值梯度,再和反向传播算出的 dW 对应元素比较。误差很小说明梯度大概率正确;如果差很多,通常是 batch 平均、激活导数或 shape 广播出错。

训练 loss 不下降可能是什么原因?

常见原因包括学习率过大或过小,输入未标准化,标签形状或取值错误,梯度忘记除以 batch size,BCE 数值溢出,参数没有更新,或者模型表达能力不足。如果是单层线性模型,非线性不可分数据本来就无法拟合很好。