2.1 现场四:手写线性回归梯度下降


2.1 现场四:手写线性回归梯度下降

本节摘要:面试官收走所有机器学习库,要求用纯 numpy 实现线性回归的训练循环:前向预测、均方误差、解析梯度、参数更新。追问链压向学习率行为、特征缩放与闭式解对比。这道题是全册所有训练代码的母版,第 3 章的两层网络就是把它加一层非线性放大出来的。

承接第 2 章开篇"收走框架"的设定,这是机器学习现场的第一题。它考的不是线性回归本身,而是你有没有把"训练"这件事拆成可手写的机械步骤。

面试官提问

"用 numpy 从零实现线性回归:数据是一维特征加上截距项,要求写完整训练循环,打印损失随轮次下降的过程。写完告诉我,学习率设成一百倍会看到什么。"

三个要求都有用意:完整循环是骨架分;打印损失是验证分——不能证明损失在降,等于没写对;学习率问题是下一层追问的预埋。

现场推演

候选人先在白板上写公式再翻译成代码。模型是 y = w·x + b,损失取均方误差 MSE = 平均 of (预测 − 真值) 的平方。对 w 与 b 的偏导分别是 2·平均 of (预测 − 真值)·x 与 2·平均 of (预测 − 真值)。口述完这两行,面试官点头,开始落笔。

import numpy as np # 构造可复现数据:y = 2x + 1 加小噪声 rng = np.random.default_rng(42) X = rng.uniform(-1, 1, size=(50, 1)) y = 2.0 * X + 1.0 + 0.05 * rng.standard_normal((50, 1)) w, b = 0.0, 0.0 # 参数初始化为标量,演示用 lr = 0.1 # 学习率 for epoch in range(5): pred = w * X + b # 前向 err = pred - y # 残差 loss = float(np.mean(err ** 2)) # 均方误差 grad_w = float(np.mean(2 * err * X)) # 对 w 的梯度 grad_b = float(np.mean(2 * err)) # 对 b 的梯度 w -= lr * grad_w # 参数更新 b -= lr * grad_b print(f"epoch {epoch} loss {loss:.4f}") print("w =", round(w, 4), " b =", round(b, 4))
epoch 0 loss 1.8441 epoch 1 loss 1.2877 epoch 2 loss 0.9103 epoch 3 loss 0.6525 epoch 4 loss 0.4740 w = 0.8244 b = 0.4124

损失单调下降,方向正确。候选人主动说明:"五轮远远没收敛,真要收敛要几百轮,这里打印五轮只为展示下降趋势。"面试官认可这种"知道自己在演示什么"的表述。

追问链

第一问:向量化版本。 标量参数只是教学玩具,真实写法把截距并进权重矩阵,一步矩阵乘法完成前向。

Xb = np.hstack([X, np.ones((50, 1))]) # 增广一列 1,吸收截距 W = np.zeros((2, 1)) # [w; b] for epoch in range(300): grad = 2 * Xb.T @ (Xb @ W - y) / len(y) # 解析梯度:形状 (2,1) W -= 0.1 * grad print("收敛到 w, b =", W.ravel().round(3).tolist())
收敛到 w, b = [1.996, 0.999]

与构造数据的真值 2.0 与 1.0 几乎重合——这是面试里最有力的一句验证:"构造时用了什么真值,收敛后就该回到什么真值附近。"

第二问:学习率乘一百会怎样? 候选人没有背答案,而是现场跑给你看:

W2 = np.zeros((2, 1)) for epoch in range(12): grad = 2 * Xb.T @ (Xb @ W2 - y) / len(y) W2 -= 10.0 * grad # 学习率放大一百倍 if epoch % 4 == 0: print(epoch, float(np.mean((Xb @ W2 - y) ** 2)).__round__(1))
0 9.4 4 50612769476436.0 8 3.720636339229644e+31 12 inf

损失爆到天文数字再溢出为 inf——"每步跨过谷底到对面更高的坡上,越荡越高"这就是发散的现场证据。用实验回答追问,比背"会不收敛"强得多。

第三问:线性回归有闭式解,为什么还要梯度下降? 候选人答:正规方程 W = (X 的转置乘 X) 的逆乘 X 转置乘 y,样本和特征小的时候一步到位;但特征上万时矩阵求逆是立方复杂度,内存与算力都吃不消,这时随机梯度下降反而是唯一选择。他顺手验证了闭式解与迭代解一致:

W_closed = np.linalg.inv(Xb.T @ Xb) @ Xb.T @ y print("闭式解 =", W_closed.ravel().round(3).tolist())
闭式解 = [1.997, 1.0]

第四问:批梯度与随机梯度怎么选? 候选人把逐样本版写出来对比——每轮打乱样本顺序,每步只用一个样本更新:

rng = np.random.default_rng(42) X = rng.uniform(-1, 1, size=(50, 1)) y = 2.0 * X + 1.0 + 0.05 * rng.standard_normal((50, 1)) Xb = np.hstack([X, np.ones((50, 1))]) W = np.zeros((2, 1)) for epoch in range(50): for i in rng.permutation(len(y)): # 每步只看一个样本 xi, yi = Xb[i:i + 1], y[i:i + 1] g = 2 * xi.T @ (xi @ W - yi) W -= 0.05 * g print('SGD 收敛到 w, b =', W.ravel().round(3).tolist()) print('最终损失 =', round(float(np.mean((Xb @ W - y) ** 2)), 4))
SGD 收敛到 w, b = [2.001, 0.992] 最终损失 = 0.0014

同样收敛到真值附近,但逐样本的更新路径带着抖动——每步只朝"当前这一个点"的坡往下走,方向噪声大。他总结取舍:"批梯度稳但每步贵,随机梯度便宜但抖,折中是小批量——深度学习训练的默认形态,骨架与本节的四段循环完全同款,只是批大小从全量变成几十。"

失误复盘

高频翻车点:梯度公式里忘了除以样本数,学习率等效放大了几十倍,损失当场发散还以为是代码写错;前向用了 y 而残差符号弄反,损失上升却查不出原因;打印损失放在参数更新之后,序列错位误导判断。还有一个隐蔽错误:把噪声方差设得太大(比如 0.5),收敛值偏离真值明显,候选人误以为算法有 bug——那是数据本身的信息量问题,不是优化问题。

主线候选人这一场写得很顺,但学习率追问时先背了句"太大会不收敛",被面试官要求"跑给我看"才补上实验。他的复盘笔记写着:"结论要先能演示,再配解释,顺序反了就显得是背的。"

关键直觉:训练循环只有四段——前向、损失、梯度、更新。后面所有花哨的网络,改的只是前向与梯度内部,骨架一寸不变。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U