01 / 本课学习路线
本课学习路线
阅读与推演约 118 分钟,练习约 85 分钟,进阶练习另需约 95 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
训练类题目只有一条循环:前向算预测、损失衡量差距、梯度给方向、沿负梯度更新。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 写出均方误差的定义,并说出为什么用平方而不用误差本身 | 第 04 节 | 自查第 2 条 |
| 不看参考写出批量梯度下降的一轮:累加梯度、乘 2/n、同步更新 | 第 04、05 节 | 自查第 3 条、练习 4 |
| 用「改变一点参数、观察损失变化」验证梯度公式的符号和大小 | 第 04 节 | 自查第 4 条、代码自测 |
| 说出学习率过大和过小各自的症状 | 第 04 节 | 自查第 5 条、练习 3 |
| 说出正规方程和梯度下降各自什么时候选,λ 加在哪些对角元上 | 第 06 节 | 自查第 6 条、练习 2 |
| 通过 AI002 与 AI003 | 第 05、06、08 节 | 自查第 1 条 |
先修自测:下面 5 题请先自己写答案,再展开对照。答不出的按括号里的位置补看再回来。本课假定你已完成上一课「特征归一化、KNN 与 K-Means」(Min-Max 的拟合与转换(fit / transform)在那里讲过)。
| 题号 | 题目 | 补看位置 |
|---|---|---|
| 自测 1 | sum((b + w * x - y) ** 2 for x, y in [(0, 1), (1, 3)]) 在 w=b=0 时是多少? | 常用内置函数 |
| 自测 2 | 训练集 [[0,0],[10,20],[5,10]]:每列的最小值、最大值各是多少?第三行归一化后是什么? | 特征归一化、KNN 与 K-Means第 04 节 |
| 自测 3 | (b + w·x − y)² 对 b 求导是多少?对 w 求导是多少? | 本课第 04 节(推导写在正文里) |
| 自测 4 | 三个数 −1、−3、−6 的和乘 2/3 是多少? | 数字与运算符 |
| 自测 5 | 解方程组 3b + 3w = 9、3b + 5w = 13。 | 本课第 06 节(消元过程写在正文里) |
展开先修自测答案
自测 1:(0 − 1)² + (0 − 3)² = 10——除以样本数 2 得均方误差 5.0,就是第 04 节的起点。
自测 2:最小值 [0, 0]、最大值 [10, 20];第三行 (5, 10) 归一化后是 (0.5, 0.5)。第 05 节第二个例子用到它。
自测 3:对 b 求导得 2(b + w·x − y),对 w 求导多乘一个 x:2(b + w·x − y)·x。n 条样本取平均后再求导,就是「误差之和乘 2/n」。
自测 4:(−1 − 3 − 6) × 2/3 = −20/3 ≈ −6.667。第 05 节 AI002 的梯度就是这种形状。
自测 5:两式相减得 2w = 4,w = 2;代回 3b + 6 = 9,b = 1。这就是第 06 节 λ=0 时的正规方程。
03 / 概念与术语
模型、损失、梯度、学习率、同步更新与正规方程
本课的词都能落到代码的某一行。下表第三列给出它们在第 08 节完整程序里的位置。
| 术语 | 含义 | 代码里的位置 |
|---|---|---|
| 线性模型 ŷ = w·x + b | 预测值 = 各列权重乘特征再加偏置;w 有 d 个分量,b 一个 | b + sum(w[j] * row[j] for j in range(d)) |
| 均方误差(MSE) | 每条样本误差 ŷ − y 平方后取平均;平方让正负不抵消、大误差罚更重 | loss(w, b)(代码自测) |
| 梯度 | 损失对每个参数的导数;对 b 是误差之和乘 2/n,对 w 多乘一个特征值 | gb += err、gw[j] += err * row[j]、scale = 2.0 / n |
学习率 eta | 每一步沿负梯度走多远:参数 −= eta × 梯度 | b -= eta * scale * gb |
| 同步更新 | 一轮内先把全部样本的梯度累加完,w 和 b 再一起换 | 累加循环结束后才出现 -= |
| fit / transform | 只用训练集算每列最小值最大值;常数列归 0;查询超出范围不截断 | mins / maxs / transform |
| 岭回归与正规方程 | 损失取平方误差之和 Σ(ŷ−y)² 再加 λΣw²(等价于 MSE + (λ/n)Σw²,AI003 按前者);令每个偏导为 0 得 (ZᵀZ + λI′)·w = Zᵀy,一次解出全部参数;I′ 的第 0 个对角元为 0(偏置不正则) | a[j][j] += lam(j 从 1 起)、solve_linear |
| 数值梯度 | 把一个参数增加 0.001,损失变化量除以 0.001;用来验证梯度公式 | (loss(0, 0.001) - loss(0, 0)) / 0.001 |
04 / 损失与梯度
梯度下降过程中损失值的变化
两条训练样本:x=0 时 y=1,x=10 时 y=3(归一化后 x 变成 0 和 1)。从 w=0、b=0 出发,eta=0.1,每轮按公式更新一步:
| 样本 | 预测 ŷ | 误差 ŷ − y | 误差² | 对 b 的贡献 | 对 w 的贡献(误差 × x) |
|---|---|---|---|---|---|
| (x=0, y=1) | 0 | −1 | 1 | −1 | 0 |
| (x=1, y=3) | 0 | −3 | 9 | −3 | −3 |
| 合计 | 10 → 损失 = 10/2 = 5.0 | −4 | −3 |
本节把损失写成 Σ(ŷ−y)²/2(除以样本数 n=2)——此时对 b 的梯度就是误差之和 −4、对 w 是 −3,乘 2/n 的系数在 n=2 时恰好是 1。AI002 的题面用同一个约定:梯度乘 2/n。沿负梯度走一步:b = 0 − 0.1×(−4) = 0.4,w = 0 − 0.1×(−3) = 0.3。
| 轮次 | w | b | 损失值 |
|---|---|---|---|
| 0(初始) | 0.000 | 0.000 | 5.0000 |
| 1 | 0.300 | 0.400 | 2.8250 |
| 2 | 0.530 | 0.690 | 1.6322 |
| 3 | 0.708 | 0.899 | 0.9753 |
第 2 轮:误差变成 0.4−1 = −0.6 与 0.7−3 = −2.3,对 b 的梯度 −2.9、对 w 的梯度 −2.3,所以 b = 0.4 + 0.29 = 0.69、w = 0.3 + 0.23 = 0.53。损失函数的最小值在 w=2、b=1(损失值=0);30 轮后走到损失值=0.0255,还在继续逼近。
批量梯度下降的一轮
Pythonw = [0.0] * d
b = 0.0
for _ in range(epochs):
gw = [0.0] * d
gb = 0.0
for i in range(n): # 先把全部样本的梯度累加完
pred = b + sum(w[j] * x[i][j] for j in range(d))
err = pred - y[i]
gb += err
for j in range(d):
gw[j] += err * x[i][j]
scale = 2 / n # MSE 的求导系数
b -= eta * scale * gb # 再一起更新(同步更新)
for j in range(d):
w[j] -= eta * scale * gw[j]公式可以当场验证:把 b 从 0 增加到 0.001,损失值的变化量除以 0.001 得 −3.999,和公式算出的 −4 对上;w 那边是 −3。改变一点参数、观察损失,就能验证梯度。
上面这一轮代码对应训练循环的四个阶段:前向计算(用当前参数算出预测值 ŷ=w·x+b)→ 损失(用均方误差衡量 ŷ 与 y 的差距)→ 反向(按链式法则,把损失对 ŷ 的导数乘以 ŷ 对各参数的导数,得到每个参数的梯度;线性模型只有一层,链式法则只用一步,多层网络则逐层相乘、从输出层传回输入层)→ 更新(每个参数沿负梯度方向移动 eta × 梯度)。线性回归、逻辑回归和神经网络共享这四个阶段,但各自的前向结构、损失函数和梯度的具体形式并不相同。
| eta | 第 1 轮损失值 | 第 10 轮损失值 | 第 30 轮损失值 | 诊断 |
|---|---|---|---|---|
| 0.01 | 4.7533 | 3.0305 | 1.1739 | 过慢:方向对,步长太小 |
| 0.1 | 2.8250 | 0.1321 | 0.0255 | 合适:稳步逼近最小值 |
| 1.2 | 21.8 | 约 1951 万 | 3.3×10²⁰ | 发散:跨过最小值,来回放大 |
训练损失是最直接的信号:变大了就调小 eta;早期几乎不动、数据和梯度都正常时再考虑调大。
AI002 的三个易错规则
常数列归一化为 0;e=0(零轮训练)时参数保持全零;查询值用训练集参数归一化、不做区间截断。时限 3 秒、n≤500、e≤200,双重循环 O(n·d·e) 最多 80 万次乘加,纯循环够用——容易出错的是规则细节,不是速度。
补充学习(选学)学习率过大为什么会发散约 4 分钟更新步长跨过最小值,落到损失更高的位置,来回放大
这个例子的损失函数是凸函数,更新是沿负梯度方向把参数移动 eta × 梯度 这么远。eta=1.2 时第一步就跨过了最小值,落到损失更高的位置;那里梯度的绝对值更大,下一步移动得更远——参数在最小值两侧来回振荡,损失越来越大。数字就是证据:第 1 轮损失值从 5.0 涨到 21.8,第 10 轮 1951 万,第 30 轮 3.3×10²⁰。eta=0.01 的问题正相反:30 轮后还停在 1.17,离最小值还很远。
补充学习(选学)梯度下降前为什么先做 Min-Max 归一化约 4 分钟不缩放时,梯度被大量级特征主导
同一份数据、同一个 eta=0.1,只差归一化
归一化后 x∈{0, 1}: grad_w = -3 -> 一轮后 w=0.3, 损失值 5.0 -> 2.825
原始尺度 x∈{0, 10}: grad_w = -30 -> 一轮后 w=3.0, 损失值 5.0 -> 375.56梯度里有一项「误差 × 特征值」:特征值越大,这列的梯度天生越大。上一课「特征归一化、KNN 与 K-Means」里词元(token)列主导的是距离,本课它主导梯度。多列特征时问题更明显——不同量级的列需要不同的有效步长,单一学习率难以同时适合所有列。先把每列缩放到 [0,1],就可以用统一的学习率更新,这就是 AI002 把归一化放在梯度下降前面的原因。
补充学习(选学)L2 正则与正规方程:AI003 的两个关键约 5 分钟λ 压谁不压谁;解方程和梯度下降各自什么时候用
L2 正则在损失里加 λ×Σw²:权重越大罚得越重,模型想增大权重就得拿「损失下降」来换——这是对「通过某个过大的权重过度拟合训练数据」的约束。偏置 b 不参与,所以岭回归的正则项不包含它。λ 越大权重被压得越小,太大连有用的权重也会被压没。
正规方程(也称法方程)是另一条路:把「每个偏导都等于 0」列成方程组,高斯消元一次解出全部参数——不挑学习率、不担心收敛。代价在规模:构造 O(n·d²)、求解 O(d³);梯度下降每轮只要 O(n·d)。选择时的一般原则:特征维度较小、数据可以整体装入内存时可用正规方程;数据或特征规模较大时通常使用梯度下降这类迭代优化,并结合内存、稀疏性和数值稳定性综合选择。需要注意:λ>0 通常让正规方程更容易求解,但 AI003 无论 λ 是否为 0,题目数据都保证方程可解——不要把「λ>0 才可逆」当成普适结论。
补充学习(选学)从批量梯度下降到 Adam:优化器谱系约 5 分钟SGD、小批量、动量与 Adam 各自改了哪一件事
本课写的批量梯度下降每一轮都用全部样本算一次梯度,是神经网络训练循环(算损失、算梯度、更新参数)的基础形态。工程上常见的几种优化器都建立在这三步之上,每一种只改其中一件事。
| 名称 | 每步用多少样本 | 更新方式的改动 | 带来的影响 |
|---|---|---|---|
| 批量梯度下降 | 全部样本 | 无改动,就是本课的写法 | 每一步的方向最准,但每步的开销随样本数增长 |
| SGD(随机梯度下降) | 1 条样本 | 用单条样本的梯度近似整体梯度 | 单步很快,方向抖动大,需要更小的学习率 |
| mini-batch(小批量) | 一小批样本(如 32、128 条) | 用一批样本的平均梯度 | 工程上最常用的折中:方向比 SGD 稳,开销比全量小 |
| momentum(动量法) | 同小批量 | 把历史更新方向按比例累加进本次更新 | 在长而窄的谷地里减少来回摆动,穿过平坦区域更快 |
| Adam | 同小批量 | 按梯度的一阶矩与二阶矩为每个参数分别调整更新幅度 | 对学习率的初值不那么敏感,但仍替代不了特征归一化 |
从上到下是逐层叠加的关系:小批量改的是「每步看多少数据」,动量法(momentum)和 Adam 改的是「拿到梯度之后怎么更新」,两类改动可以同时使用。
无论用哪一种,学习率都是第一个要调的超参数;训练不收敛时,先检查的也仍是本课的两件事:学习率是否合适、特征有没有归一化。
补充学习(选学)进阶练习涉及的模型与损失函数:sigmoid、交叉熵与同形梯度约 6 分钟AI009 / AI010 / AI012 各考什么,梯度为什么同形
sigmoid 把分数 z=w·x+b 映射到 (0,1):σ(−2)=0.1192、σ(0)=0.5、σ(2)=0.8808。损失换交叉熵:y=1 而 p=0.01 时均方误差最多罚 0.98,交叉熵罚 4.61 且随偏离程度一直增大——这正是想要的惩罚形状。梯度方面:sigmoid 和交叉熵在求导时互相抵消,最后只剩 (p−y)·x,与线性回归同形。
逻辑回归一步梯度下降的完整计算(x=[1,2,3,4]、y=[0,0,1,1]、w=b=0、lr=0.1)
四个 p 全是 0.5 (z = 0) 误差 p-y = [0.5, 0.5, -0.5, -0.5] grad_w = (0.5 + 1 - 1.5 - 2) / 4 = -0.5 grad_b = 0 更新: w 0 -> 0.05, b 不动 平均损失 0.6931 -> 0.6705, 下降 ✓
sigmoid、交叉熵与一步梯度下降的参考实现
Pythonimport math
EPS = 1e-12
def sigmoid(z):
# 按 z 的符号分支:两条路都不会让 exp 溢出
if z >= 0:
return 1.0 / (1.0 + math.exp(-z))
e = math.exp(z)
return e / (1.0 + e)
def cross_entropy(p, y):
# 二分类:y 是 0 或 1,p 是预测为 1 的概率
p = min(max(p, EPS), 1.0 - EPS) # 裁剪,护住 log(0)
return -(y * math.log(p) + (1 - y) * math.log(1.0 - p))
def gd_step(xs, ys, w, b, lr):
# 一步批量梯度下降:先把两个梯度都算完,再一起更新
n = len(xs)
grad_w = 0.0
grad_b = 0.0
for x, y in zip(xs, ys):
p = sigmoid(w * x + b)
grad_w += (p - y) * x # 与线性回归同形
grad_b += (p - y)
return w - lr * grad_w / n, b - lr * grad_b / n
assert abs(sigmoid(1000) - 1.0) < 1e-12 # 大分数不溢出
assert sigmoid(-1000) < 1e-12 # 小分数不溢出
assert abs(sigmoid(0) - 0.5) < 1e-12
assert abs(sigmoid(-2) - 0.119203) < 1e-6
assert abs(cross_entropy(0.01, 1) - 4.6052) < 1e-4 # 均方误差同一情形最多罚 0.9801
assert cross_entropy(0.0, 1) > 25 # 裁剪后仍是有限值,不会得到无穷大
xs, ys = [1, 2, 3, 4], [0, 0, 1, 1]
w1, b1 = gd_step(xs, ys, 0.0, 0.0, 0.1)
assert abs(w1 - 0.05) < 1e-12 and abs(b1 - 0.0) < 1e-12
loss0 = sum(cross_entropy(sigmoid(0.0 * x + 0.0), y) for x, y in zip(xs, ys)) / 4
loss1 = sum(cross_entropy(sigmoid(w1 * x + b1), y) for x, y in zip(xs, ys)) / 4
assert abs(loss0 - 0.6931) < 1e-4 and abs(loss1 - 0.6705) < 1e-4 and loss1 < loss0三个函数各守一处数值边界:sigmoid 按 z 的符号分支,|z| 很大时两条路都不会让 exp 溢出;cross_entropy 先把概率裁剪到 [EPS, 1−EPS],p 取到 0 或 1 时也不会算出无穷大;gd_step 把两个梯度都算完再一起更新,不能边算边改。断言里的 w 从 0 变成 0.05、平均损失 0.6931 降到 0.6705,与上面的推演逐项对应。
三道进阶练习各有侧重:AI009 是完整的逻辑回归训练器,有一组用例的 |z| 达到 10⁵,sigmoid 必须按 z 的符号分支才不会溢出;AI010 练稳定 softmax,再从 logits(模型输出的原始分数)直接算交叉熵——就是模块 6 · 第 2 课(浮点精度与稳定 softmax)讲过的 log-sum-exp;AI012 是两层多层感知机(MLP)的前向计算:ReLU(负数置 0)隐层加线性输出,按下标从小到大求和。
05 / AI002 手动计算
归一化、同步更新与三条规则:题面示例逐步
AI002「归一化与线性预测」:第一行 n d m e,第二行学习率 eta;接下来 n 行「d 个整数特征 + 一个实数标签」,再 m 行查询。先用训练集每列最小值最大值做 Min-Max(常数列归 0),从零参数跑 e 轮批量梯度下降(梯度乘 2/n、同步更新),查询用训练集参数归一化、不截断;输出 m 行四位小数,绝对值小于 0.00005 输出 0.0000。
| 步骤 | 计算 | 结果 |
|---|---|---|
| fit | 第 1 列最小 0、最大 10 | mins=[0]、maxs=[10] |
| transform | (0−0)/10、(10−0)/10 | 训练集 x 变成 0、1 |
| 前向 | w=b=0,两条预测都是 0 | 误差 −1、−3 |
| 梯度 | gb = −4;gw = (−1)×0 + (−3)×1 = −3;scale = 2/2 = 1 | gb=−4、gw=−3 |
| 同步更新 | b = 0 − 0.1×1×(−4);w = 0 − 0.1×1×(−3) | b=0.4、w=0.3 |
| 查询 | 0 → 0 → 0.4 + 0.3×0;10 → 1 → 0.4 + 0.3×1 | 0.4000 / 0.7000 |
e=2 时再走一轮得 w=0.53、b=0.69,输出 0.6900 / 1.2200(练习 1)。
| 步骤 | 计算 | 结果 |
|---|---|---|
| fit | 两列最小 [0,0]、最大 [10,20] | 常数列不存在 |
| transform | 三行变成 (0,0)、(1,1)、(0.5,0.5) | 自测 2 |
| 误差 | 0−2、0−10、0−6 | −2、−10、−6 |
| 梯度 | gb = −18;gw₁ = gw₂ = (−10)×1 + (−6)×0.5 = −13;scale = 2/3 | |
| 同步更新 | b = 0.15×(2/3)×18;w = 0.15×(2/3)×13 | b=1.8、w=(1.3, 1.3) |
| 查询 | (2,4) → (0.2,0.2) → 1.8 + 1.3×0.4;(8,16) → (0.8,0.8) → 1.8 + 1.3×1.6 | 2.3200 / 3.8800 |
两个 w 分量相等不是巧合:第二列恰好是第一列的两倍,归一化后两列完全一样。若先更新 b 再用新 b 算 w 的梯度(边算边更新),第二个数变成 3.4480——第 09 节错误表第一行。
三条规则各对应一个题库用例
常数列归 0:用例 3 2 2 5 / 0.1 / 5 0 1 / 5 10 3 / 5 20 5 / 5 5 / 999 15 第一列全是 5,直接除会得到除以零的运行错误;正确输出 2.0926 / 2.8023。零轮训练:2 2 1 0 / … 参数全零,任何查询都输出 0.0000。不截断:2 1 2 2 / 0.1 / 0 0 / 10 10 / 20 / -10 查询 20 归一化成 2.0、−10 成 −1.0,正确输出 5.3000 / −0.1000;把它们截到 [0,1] 会得到 3.5000 / 1.7000。
拟合(fit)只读训练集:把查询也放进最小值最大值的计算(fit 混入查询),同一份 20 / -10 用例会输出 2.9296 / 1.7333——训练集的坐标被查询改写了,与上一课「数据泄漏的表现」是同一件事。
06 / AI003 手动计算
正规方程的构造、λ 加在哪里与 NA 规则
AI003「缺失值恢复」:第一行 n d m,第二行 λ;接下来 n 行「d 个特征 + 标签」;再 m 行每行 d 个特征加目标值,其中恰好一个特征是 ?。先用带偏置的岭回归解出 w,再由目标值反推缺失特征;缺失列权重绝对值小于 1e-10 输出 NA;输出四位小数。
构造:AI003 的目标是 Σ(ŷ−y)² + λΣw²——注意第一项是平方误差之和,不是第 04 节的均方误差(若沿用 MSE,方程里的 λ 要换成 nλ)。把偏置当成恒为 1 的第 0 列,每条样本扩成 z = (1, x₁, …, x_d)。正规方程是 (ZᵀZ + λI′)·w = Zᵀy:矩阵 ZᵀZ 的第 r 行第 c 列是 Σ z_r·z_c,右边第 r 项是 Σ z_r·y——两者都能在读入时逐条样本累加,不必存下整张表;I′ 是对角线上第 0 项为 0、其余为 1 的矩阵,所以 λ 只加在权重的对角元上,偏置不正则。
| 量 | λ=0 | λ=1 |
|---|---|---|
| ZᵀZ | [[3, 3], [3, 5]] | 同左 |
| + λI′ | 不变 | [[3, 3], [3, 6]] |
| Zᵀy | [9, 13] | 同左 |
| 方程 | 3b + 3w = 9;3b + 5w = 13 | 3b + 3w = 9;3b + 6w = 13 |
| 消元 | 相减:2w = 4 | 相减:3w = 4 |
| 解 | w = 2、b = 1(直线 y = 2x + 1 恰好过三点) | w = 4/3 ≈ 1.3333、b = 5/3 ≈ 1.6667 |
恢复 ? 7 | x = (7 − 1)/2 = 3.0000 | x = (7 − 5/3)/(4/3) = 4.0000 |
ZᵀZ 的四个数:左上是样本数 3,右上和左下是 Σx = 0+1+2 = 3,右下是 Σx² = 0+1+4 = 5;Zᵀy 是 Σy = 9 与 Σx·y = 0+3+10 = 13。λ 越大权重被压得越小(2 → 4/3),同一个目标值反推出的 x 就越大。把 λ 也加到偏置上([[4,3],[3,6]])会解出 x = 3.6000——第 09 节错误表。
求解:d ≤ 8 时方程组最多 9 元,用列主元高斯消元(每一列先换到绝对值最大的行再消,减少浮点误差)——第 08 节 solve_linear。恢复:缺失列 j 的权重记作 w_j,其余已知项之和记作 known = b + Σ(其他列 w·x),则 x_j = (目标值 − known)/w_j。NA 规则:题面示例 2 的第二列全为 0,ZᵀZ 里这一列的行与列全是 0、右边对应项也是 0,加上 λ=0.1 后解出 w₂ 恰好为 0——目标值不受这一列影响,无法反推,输出 NA;漏判会得到除以零的运行错误。题面示例 1 解出 b ≈ 2.002、w ≈ (2.9943, −0.9997):? 1 7 得 (7 − 2.002 + 0.9997)/2.9943 = 2.0030,2 ? 5 得 (5 − 2.002 − 5.9886)/(−0.9997) = 2.9917。
07 / 多层网络的梯度
一个 2→2→1 小网络的前向计算与反向传播
没有非线性激活时,两层线性运算可以合并成一次线性运算,多层没有意义;加上 ReLU(负数置 0)之后,网络才能表示折线。下面固定一个 2 输入、2 个 ReLU 隐藏单元、1 输出的网络,参数顺序固定为 [w11, w12, b1, w21, w22, b2, v1, v2, c](隐藏层两组权重与偏置,再接输出层两个权重与偏置 c),输入 x=(1.0, 2.0)、目标 t=1.0,参数 [0.5, 0.25, 0, −1, 0.25, 0, 1, −0.5, 0.5],损失取平方误差 (y−t)²。
| 量 | 算式 | 结果 |
|---|---|---|
| z1 | 0.5×1 + 0.25×2 + 0 | 1.0 |
| h1 | ReLU(1.0) | 1.0 |
| z2 | −1×1 + 0.25×2 + 0 | −0.5 |
| h2 | ReLU(−0.5),未激活 | 0 |
| y | 1×1.0 + (−0.5)×0 + 0.5 | 1.5 |
| L | (1.5 − 1)² | 0.25 |
第二个隐藏单元的 z2 为负,经 ReLU 后输出 0——它在这条样本上未被激活。
| 梯度 | 链式来源 | 结果 |
|---|---|---|
| dL/dy | 2×(y − t) = 2×0.5 | 1.0 |
| dL/dv1 | dL/dy × h1 = 1.0×1.0 | 1.0 |
| dL/dv2 | dL/dy × h2 = 1.0×0 | 0 |
| dL/dc | dL/dy | 1.0 |
| dL/dh1 | dL/dy × v1 = 1.0×1 | 1.0 |
| dL/dh2 | dL/dy × v2 = 1.0×(−0.5) | −0.5 |
| dL/dz1 | dL/dh1 × ReLU′(z1) = 1.0×1 | 1.0 |
| dL/dz2 | dL/dh2 × ReLU′(z2) = −0.5×0 | 0 |
| dL/dw11、dL/dw12、dL/db1 | dL/dz1 × (x1、x2、1) | 1.0、2.0、1.0 |
| dL/dw21、dL/dw22、dL/db2 | dL/dz2 × (x1、x2、1) = 0×… | 全为 0 |
9 个梯度按参数顺序排列为 [1.0, 2.0, 1.0, 0, 0, 0, 1.0, 0, 1.0]。本课程约定 ReLU′(0) = 0;z2 = −0.5 时第二个隐藏单元未激活,因此它的输入权重和偏置梯度全为 0。
前向、损失、反向、数值梯度与一次更新
PythonEPS = 1e-5
# 参数顺序固定:w11, w12, b1, w21, w22, b2, v1, v2, c
def relu(z):
return z if z > 0 else 0.0
def relu_grad(z):
# 本课程约定 ReLU 在 z = 0 处的导数取 0
return 1.0 if z > 0 else 0.0
def forward(params, x):
w11, w12, b1, w21, w22, b2, v1, v2, c = params
x1, x2 = x
z1 = w11 * x1 + w12 * x2 + b1
z2 = w21 * x1 + w22 * x2 + b2
h1, h2 = relu(z1), relu(z2)
y = v1 * h1 + v2 * h2 + c
return z1, z2, h1, h2, y
def loss(params, x, t):
y = forward(params, x)[-1]
return (y - t) ** 2
def backward(params, x, t):
v1, v2 = params[6], params[7]
x1, x2 = x
z1, z2, h1, h2, y = forward(params, x)
dy = 2 * (y - t) # 损失对输出的导数
dv1, dv2, dc = dy * h1, dy * h2, dy # 输出层三个参数
dh1, dh2 = dy * v1, dy * v2 # 传回隐藏层输出
dz1 = dh1 * relu_grad(z1) # 经过 ReLU:未激活时局部导数为 0
dz2 = dh2 * relu_grad(z2)
return [dz1 * x1, dz1 * x2, dz1,
dz2 * x1, dz2 * x2, dz2,
dv1, dv2, dc]
def numeric_grad(params, x, t, i):
up, down = params[:], params[:]
up[i] += EPS
down[i] -= EPS
return (loss(up, x, t) - loss(down, x, t)) / (2 * EPS)
def train_step(params, x, t, lr):
grads = backward(params, x, t)
return [p - lr * g for p, g in zip(params, grads)]梯度校验(gradient check):对每个参数用中心差分 (L(wᵢ+ε) − L(wᵢ−ε)) / (2ε)(ε = 1e−5)核对解析梯度,本例 9 项误差都在 1e−6 以内(自测断言按 < 1e−6 判定,不把某台机器上更小的误差当成普遍保证)。学习率 0.1 同步更新一次后,预测从 1.5 变为 0.76,损失从 0.25 降到 0.0576;学习率 1.0 时同一步更新会把损失推高到 2.25。
两个边界:未激活的 ReLU 与全零初始化
未激活的 ReLU:该路径上的局部导数为 0,流经它的梯度全部为 0——若某个隐藏单元对所有样本都未激活,它名下的参数就不再更新。全零初始化:两个隐藏单元完全对称,隐藏层 6 个参数的梯度全为 0,整个网络只有输出偏置在更新;因此实际训练用小随机数初始化打破对称。更新必须同步:9 个参数都用同一轮的梯度,边算边改会互相污染。
补充学习(选学)梯度消失与 ReLU 的取舍约 5 分钟sigmoid 导数最大 0.25,连乘 10 层只剩百万分之一量级
sigmoid 的导数是 σ×(1−σ),在 σ=0.5 处取到最大值 0.25。反向传播每经过一层就要乘一次这个导数:10 层 sigmoid 网络只看激活函数导数这一项,最底层的衰减因子就是 0.25¹⁰ ≈ 9.5×10⁻⁷——实际梯度还要乘每层权重等因素,但这一项足以把信号缩到百万分之一量级,底层权重几乎不再更新。这就是梯度消失。
ReLU 的局部导数要么是 1 要么是 0:激活的路径上这一项恒为 1、不随深度衰减(整条链的梯度仍要乘每层权重),所以许多深度网络采用 ReLU 或其变体(现代架构也常用 GELU、SiLU(两种平滑的激活函数))。代价在正文的推演里已经出现——z2 = −0.5 的未激活单元把梯度变成 0;若某单元对所有样本都长期未激活(称为「失活的 ReLU」),它名下的参数就不再更新。最常见的补救是给未激活的一侧也保留一个很小的斜率(例如 0.01),即带泄漏的 ReLU。
补充学习(选学)过拟合的第三类缓解方法:随机失活、早停与批归一化约 4 分钟三个作用在训练过程上的技巧各一句话
过拟合前面处理过两次:本课用 L2 正则给大权重加罚,模块 6 · 第 5 课 用限制深度给树剪枝。神经网络参数多,更容易记住训练集,常用的三种方法都作用在训练过程上。随机失活(dropout):训练时按比例随机把一部分神经元临时置零,迫使其余神经元学出冗余表示,推理时全部神经元参与。早停:验证集损失开始回升就停止训练,把训练轮数当成复杂度旋钮——实践中通常容忍若干轮再停,并回滚到验证集最优的那份参数。批归一化(BN):把每层输入拉回稳定的分布再向下传,自带可学习的缩放平移参数与运行统计,训练更稳,并带有轻微的正则效果。
08 / 从公式到程序
参考实现与五份完整程序,每一步落在哪几行
先用断言验证梯度函数,再组合成 AI002 的读入、归一化、训练与输出;AI003 换成累加矩阵、消元、反推三步。
| 步骤 | AI002 | AI003 |
|---|---|---|
| 读入 | n d m e、eta、n 行特征+标签、m 行查询 | n d m、λ、n 行特征+标签、m 行含 ? 的查询 |
| 准备 | fit:mins / maxs;训练集 transform 一次 | 逐条样本累加 a(ZᵀZ)与 rhs(Zᵀy) |
| 求参数 | e 轮:累加 gb / gw → 乘 2/n → 同步 -= | a[j][j] += lam(j ≥ 1)→ solve_linear |
| 查询 | transform(不截断)→ b + Σ w·z | 找 ? 的列 → 判 NA → (target − known) / coef |
| 输出 | fmt:四位小数,|v| < 0.00005 → 0.0000 | 同左,或 NA |
展开参考实现:梯度函数(grad)与数值梯度断言(先自己写完再对照)
grad 的参考实现(自带断言)
Python# 两条样本 (x=0, y=1)、(x=1, y=3),损失 = Σ(ŷ−y)²/2,w=b=0
SAMPLES = [(0, 1), (1, 3)]
def grad(w, b):
dw = 0.0
db = 0.0
for x, y in SAMPLES:
err = b + w * x - y # ŷ − y
dw += err * x # 对 w:误差乘特征值
db += err # 对 b:误差本身
return dw, db
def loss(w, b):
return sum((b + w * x - y) ** 2 for x, y in SAMPLES) / 2
dw, db = grad(0, 0)
assert dw == -3 and db == -4
assert abs((loss(0, 0.001) - loss(0, 0)) / 0.001 - db) < 0.01 # 数值梯度 −3.999
assert abs((loss(0.001, 0) - loss(0, 0)) / 0.001 - dw) < 0.01 # 数值梯度 −2.9995
# 沿负梯度走一步(eta=0.1):w=0.3、b=0.4,损失 5.0 → 2.825(浮点乘法有尾差,用容差比较)
w1, b1 = 0 - 0.1 * dw, 0 - 0.1 * db
assert abs(w1 - 0.3) < 1e-12 and abs(b1 - 0.4) < 1e-12
assert abs(loss(w1, b1) - 2.825) < 1e-12断言覆盖第 04 节第一张表(−3、−4)、两个数值梯度(−2.9995、−3.999)和走一步后的 w=0.3、b=0.4、损失 2.825。
展开完整参考程序 1:AI002 归一化与线性预测
完整程序:AI002(标准输入 → 标准输出)
Pythonimport sys
def fmt(v):
if abs(v) < 0.00005: # 避免打出 -0.0000
v = 0.0
return f"{v:.4f}"
def main():
data = sys.stdin.read().split()
if not data:
return
pos = 0
n, d, m, epochs = (int(data[i]) for i in range(4))
pos = 4
eta = float(data[pos]); pos += 1
xs, ys = [], []
for _ in range(n):
xs.append([float(v) for v in data[pos:pos + d]]); pos += d
ys.append(float(data[pos])); pos += 1
queries = []
for _ in range(m):
queries.append([float(v) for v in data[pos:pos + d]]); pos += d
# fit:只用训练集算每列最小值与最大值
mins = [min(row[j] for row in xs) for j in range(d)]
maxs = [max(row[j] for row in xs) for j in range(d)]
def transform(row): # 常数列归 0;超出范围不截断
return [0.0 if maxs[j] == mins[j] else (row[j] - mins[j]) / (maxs[j] - mins[j])
for j in range(d)]
train = [transform(row) for row in xs] # 训练集只归一化一次
w = [0.0] * d
b = 0.0
for _ in range(epochs):
gw = [0.0] * d
gb = 0.0
for row, y in zip(train, ys): # 先把全部样本的梯度累加完
err = b + sum(w[j] * row[j] for j in range(d)) - y
gb += err
for j in range(d):
gw[j] += err * row[j]
scale = 2.0 / n # 均方误差的求导系数
b -= eta * scale * gb # 再一起更新(同步更新)
for j in range(d):
w[j] -= eta * scale * gw[j]
out = []
for q in queries:
z = transform(q)
out.append(fmt(b + sum(w[j] * z[j] for j in range(d))))
print("\n".join(out))
main()transform 同时服务训练集和查询:常数列归 0、超出范围不截断都在这一个函数里。用题面示例 → 0.4000 / 0.7000 和第 05 节的三条规则用例核对。
展开完整参考程序 2:AI003 缺失值恢复
完整程序:AI003(标准输入 → 标准输出)
Pythonimport sys
def solve_linear(a, rhs):
"""高斯消元(列主元)解 a·x = rhs,返回 x。"""
size = len(rhs)
for col in range(size):
pivot = max(range(col, size), key=lambda r: abs(a[r][col]))
a[col], a[pivot] = a[pivot], a[col]
rhs[col], rhs[pivot] = rhs[pivot], rhs[col]
div = a[col][col]
for j in range(col, size):
a[col][j] /= div
rhs[col] /= div
for r in range(size):
if r == col or a[r][col] == 0:
continue
factor = a[r][col]
for j in range(col, size):
a[r][j] -= factor * a[col][j]
rhs[r] -= factor * rhs[col]
return rhs
def fmt(v):
if abs(v) < 0.00005:
v = 0.0
return f"{v:.4f}"
def main():
data = sys.stdin.read().split()
if not data:
return
n, d, m = int(data[0]), int(data[1]), int(data[2])
lam = float(data[3])
pos = 4
size = d + 1 # 第 0 列是恒为 1 的偏置列
a = [[0.0] * size for _ in range(size)]
rhs = [0.0] * size
for _ in range(n):
z = [1.0] + [float(v) for v in data[pos:pos + d]]
pos += d
y = float(data[pos]); pos += 1
for r in range(size): # 累加 ZᵀZ 与 Zᵀy
rhs[r] += z[r] * y
for c in range(size):
a[r][c] += z[r] * z[c]
for j in range(1, size): # λ 只加在权重的对角元上,偏置不加
a[j][j] += lam
w = solve_linear(a, rhs) # w[0] 是偏置,w[1..d] 是各列权重
out = []
for _ in range(m):
row = []
missing = -1
for j in range(d):
token = data[pos]; pos += 1
if token == "?":
missing = j
row.append(0.0)
else:
row.append(float(token))
target = float(data[pos]); pos += 1
coef = w[missing + 1]
if abs(coef) < 1e-10:
out.append("NA")
continue
known = w[0] + sum(w[j + 1] * row[j] for j in range(d) if j != missing)
out.append(fmt((target - known) / coef))
print("\n".join(out))
main()solve_linear 是列主元高斯消元,返回的 w[0] 是偏置。用题面两组示例(2.0030 / 2.9917、NA)核对。
展开完整参考程序 3:AI009 逻辑回归训练器(进阶练习)
完整程序:AI009(标准输入 → 标准输出)
Pythonimport math
import sys
def sigmoid(z):
if z >= 0: # 按符号分支:exp 的参数始终非正,不会溢出
return 1.0 / (1.0 + math.exp(-z))
e = math.exp(z)
return e / (1.0 + e)
def fmt(v):
if abs(v) < 0.00005:
v = 0.0
return f"{v:.4f}"
def main():
data = sys.stdin.read().split()
if not data:
return
n, d, m, epochs = (int(data[i]) for i in range(4))
eta = float(data[4])
pos = 5
xs, ys = [], []
for _ in range(n):
xs.append([float(v) for v in data[pos:pos + d]]); pos += d
ys.append(float(data[pos])); pos += 1
queries = []
for _ in range(m):
queries.append([float(v) for v in data[pos:pos + d]]); pos += d
mins = [min(row[j] for row in xs) for j in range(d)]
maxs = [max(row[j] for row in xs) for j in range(d)]
def transform(row):
return [0.0 if maxs[j] == mins[j] else (row[j] - mins[j]) / (maxs[j] - mins[j])
for j in range(d)]
train = [transform(row) for row in xs]
w = [0.0] * d
b = 0.0
for _ in range(epochs):
gw = [0.0] * d
gb = 0.0
for row, y in zip(train, ys):
err = sigmoid(b + sum(w[j] * row[j] for j in range(d))) - y # p − y
gb += err
for j in range(d):
gw[j] += err * row[j]
b -= eta * gb / n # 平均梯度,同步更新
for j in range(d):
w[j] -= eta * gw[j] / n
out = [" ".join(fmt(v) for v in [b] + w)]
for q in queries:
z = transform(q)
out.append(fmt(sigmoid(b + sum(w[j] * z[j] for j in range(d)))))
print("\n".join(out))
main()与 AI002 只差三处:预测值套 sigmoid、梯度乘 1/n 而不是 2/n、第一行先输出 b 与 w。sigmoid 按符号分支——|z| 达到 10⁵ 时直接写 1/(1+exp(-z)) 会溢出(第 09 节错误表)。
展开完整参考程序 4:AI010 稳定 softmax 与交叉熵(进阶练习)
完整程序:AI010(标准输入 → 标准输出)
Pythonimport math
import sys
def fmt(v, digits):
if abs(v) < 0.5 * 10 ** (-digits):
v = 0.0
return f"{v:.{digits}f}"
def main():
data = sys.stdin.read().split()
if not data:
return
r, c = int(data[0]), int(data[1])
pos = 2
rows = []
for _ in range(r):
rows.append([float(v) for v in data[pos:pos + c]]); pos += c
labels = [int(v) for v in data[pos:pos + r]]
out = []
total = 0.0
for row, label in zip(rows, labels):
mx = max(row)
exps = [math.exp(z - mx) for z in row] # 减最大值:最大项 exp(0)=1,不会上溢
s = sum(exps)
out.append(" ".join(fmt(e / s, 4) for e in exps))
total += math.log(s) - (row[label] - mx) # −ln p = ln Σexp(z−max) − (z_label − max)
out.append(fmt(total / r, 6))
print("\n".join(out))
main()交叉熵按 ln Σexp(z−max) − (z_label − max) 计算:1 3 / 1000 0 -1000 / 2 真实类别的概率下溢成 0,对它取对数会报数学域错误,正确答案是 2000.000000。
展开完整参考程序 5:AI012 MLP 前向传播(进阶练习)
完整程序:AI012(标准输入 → 标准输出)
Pythonimport sys
def fmt(v):
if abs(v) < 0.00005:
v = 0.0
return f"{v:.4f}"
def main():
data = sys.stdin.read().split()
if not data:
return
d, h, m = int(data[0]), int(data[1]), int(data[2])
pos = 3
hidden = [] # 每项:(d 个权重, 偏置)
for _ in range(h):
ws = [float(v) for v in data[pos:pos + d]]
pos += d
hidden.append((ws, float(data[pos]))); pos += 1
vs = [float(v) for v in data[pos:pos + h]]; pos += h
c = float(data[pos]); pos += 1
out = []
for _ in range(m):
x = [float(v) for v in data[pos:pos + d]]; pos += d
y = c
for (ws, bias), v in zip(hidden, vs): # 隐藏神经元按下标从小到大
z = bias
for wj, xj in zip(ws, x): # 求和按下标从小到大
z += wj * xj
a = z if z > 0 else 0.0 # ReLU
y += v * a
out.append(fmt(y))
print("\n".join(out))
main()两层循环都按下标从小到大求和,与题面一致。用第 10 节练习 5 的题面示例(1.5000 / 0.5000)核对。
09 / 边界、反例与复杂度
错误做法在题库用例上各输出什么
下面每一行都是一个具体的错误程序,给出输入、错误输出与正确输出;把参考程序改成对应写法就能复现。
| 错误做法 | 输入 | 错误输出 | 正确输出 | 判题结果 |
|---|---|---|---|---|
| AI002 先更新 b 再算 w 的梯度(边算边更新) | 3 2 2 1 / 0.15 / 0 0 2 / 10 20 10 / 5 10 6 / 2 4 / 8 16 | 2.2120 / 3.4480 | 2.3200 / 3.8800 | 答案错误(WA) |
| AI002 漏乘 2/n | 同上 | 3.4800 / 5.8200 | 2.3200 / 3.8800 | 答案错误(WA) |
| AI002 乘 1/n 而不是 2/n | 同上 | 1.1600 / 1.9400 | 2.3200 / 3.8800 | 答案错误(WA) |
| AI002 查询截到 [0,1] | 2 1 2 2 / 0.1 / 0 0 / 10 10 / 20 / -10 | 3.5000 / 1.7000 | 5.3000 / -0.1000 | 答案错误(WA) |
| AI002 fit 混入查询 | 同上 | 2.9296 / 1.7333 | 5.3000 / -0.1000 | 答案错误(WA) |
| AI002 常数列直接除 | 3 2 2 5 / 0.1 / 5 0 1 / 5 10 3 / 5 20 5 / 5 5 / 999 15 | 除以零的运行错误 | 2.0926 / 2.8023 | 运行错误(RE) |
| AI002 梯度累加器不按轮归零 | 3 1 6 10 / 0.07 / 0 1 / 5 4 / 10 9 / 0 / 2 / … | 2.0661 / 3.5896 / … | 2.8640 / 3.4477 / … | 答案错误(WA) |
| AI003 λ 也加到偏置 | 题面示例 1 | 2.0017 / 3.0116 | 2.0030 / 2.9917 | 答案错误(WA) |
AI003 缺失列取 w[missing](忘了偏置占第 0 位) | 题面示例 1 | 2.9959 / -0.9988 | 2.0030 / 2.9917 | 答案错误(WA) |
| AI003 known 漏加偏置 | 题面示例 1 | 2.6716 / 0.9890 | 2.0030 / 2.9917 | 答案错误(WA) |
| AI003 漏判 NA | 题面示例 2 | 除以零的运行错误 | NA | 运行错误(RE) |
AI009 sigmoid 不按符号分支 | 2 1 2 300 / 20 / 0 0 / 10 1 / 10000 / -10000 | exp 溢出的运行错误 | -6.9950 14.3958 / 1.0000 / 0.0000 | 运行错误(RE) |
| AI010 对下溢后的概率取对数 | 1 3 / 1000 0 -1000 / 2 | 数学域错误 | 1.0000 0.0000 0.0000 / 2000.000000 | 运行错误(RE) |
样例 2 1 2 1 上漏乘 2/n 看不出来(n=2 时 2/n=1),所以表里换用 n=3 的用例——只靠题面示例验证不出这一类错误。
| 做法 | 时间 | 本课规模下 |
|---|---|---|
| AI002 批量梯度下降 | O(e·n·d) | 200 × 500 × 8 = 8×10⁵ 次乘加,可通过;训练集只归一化一次 |
| AI003 正规方程 | 构造 O(n·d²) + 消元 O(d³) | 200 × 81 + 729,远小于时限 |
| AI002 每轮重算最小值最大值 | O(e·n·d) 再翻倍 | 仍可通过,但是无意义的重复;性能压力大的题会因此超时 |
10 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。
练习 1(跟做):按第 05 节第一张表的格式,把题面示例的第 2 轮(从 w=0.3、b=0.4 出发)算完,并写出两个查询的输出。
展开练习 1 答案
误差:0.4 − 1 = −0.6、0.7 − 3 = −2.3;gb = −2.9、gw = −2.3;b = 0.4 + 0.29 = 0.69、w = 0.3 + 0.23 = 0.53;查询 0 → 0.6900,10 → 0.69 + 0.53 = 1.2200。与第 04 节第 2 轮一行一致。
练习 2(改一个条件):第 06 节三个点的例子把 λ 改成 3,解出的 b、w 是多少?? 7 恢复出的 x 是多少?
展开练习 2 答案
方程变成 3b + 3w = 9、3b + 8w = 13:相减 5w = 4,w = 0.8,b = 3 − 0.8 = 2.2。x = (7 − 2.2)/0.8 = 6.0000。λ 从 0、1 到 3,w 从 2、4/3 降到 0.8,恢复出的 x 从 3、4 升到 6——权重被压得越小,同一目标值要靠越大的 x 去凑。
练习 3(改一个条件):题面示例把学习率改成 0.2,一轮后 w、b 与两个输出各是多少?改成 1.2 时第一轮损失是多少?
展开练习 3 答案
eta=0.2:b = 0.8、w = 0.6,输出 0.8000 / 1.4000,损失 (0.8−1)² + (1.4−3)² 再除以 2 = 1.3。eta=1.2:b = 4.8、w = 3.6,损失 (4.8−1)² + (8.4−3)² 再除以 2 = (14.44 + 29.16)/2 = 21.8——第 04 节第三张表第 1 轮那一格,已经比起点 5.0 大,之后每轮继续放大。
练习 4(独立实现):完成「代码自测」的 grad,再加一条断言:沿负梯度走一步(eta=0.1)后损失是 2.825。
展开练习 4 答案
grad 的参考实现(自带断言)
Python# 两条样本 (x=0, y=1)、(x=1, y=3),损失 = Σ(ŷ−y)²/2,w=b=0
SAMPLES = [(0, 1), (1, 3)]
def grad(w, b):
dw = 0.0
db = 0.0
for x, y in SAMPLES:
err = b + w * x - y # ŷ − y
dw += err * x # 对 w:误差乘特征值
db += err # 对 b:误差本身
return dw, db
def loss(w, b):
return sum((b + w * x - y) ** 2 for x, y in SAMPLES) / 2
dw, db = grad(0, 0)
assert dw == -3 and db == -4
assert abs((loss(0, 0.001) - loss(0, 0)) / 0.001 - db) < 0.01 # 数值梯度 −3.999
assert abs((loss(0.001, 0) - loss(0, 0)) / 0.001 - dw) < 0.01 # 数值梯度 −2.9995
# 沿负梯度走一步(eta=0.1):w=0.3、b=0.4,损失 5.0 → 2.825(浮点乘法有尾差,用容差比较)
w1, b1 = 0 - 0.1 * dw, 0 - 0.1 * db
assert abs(w1 - 0.3) < 1e-12 and abs(b1 - 0.4) < 1e-12
assert abs(loss(w1, b1) - 2.825) < 1e-12见第 08 节展开区(同一份代码)。
练习 5(迁移):AI012 题面示例 2 2 2 / 1 -1 0.5 / 0.5 0.5 -2 / 2 1 -0.5 / 1 0.5 / 0 0:手推两个输入的前向结果,指出哪个隐藏神经元未激活。
展开练习 5 答案
隐藏 1:权重 (1, −1)、偏置 0.5;隐藏 2:权重 (0.5, 0.5)、偏置 −2;输出层权重 (2, 1)、偏置 −0.5。输入 (1, 0.5):z₁ = 1 − 0.5 + 0.5 = 1 → 1;z₂ = 0.5 + 0.25 − 2 = −1.25 → 0(未激活);y = −0.5 + 2×1 + 1×0 = 1.5000。输入 (0, 0):z₁ = 0.5 → 0.5;z₂ = −2 → 0;y = −0.5 + 1 = 0.5000。与第 07 节的推演同一套步骤,只是参数不同。
11 / 读题要求与复习自评
五道题的要求对照,以及完成本课之后怎么复习
提交前把下表过一遍;题目页的题面与样例是最终依据。
| 项目 | AI002 线性回归 | AI003 缺失值恢复 | AI009 / AI010 / AI012 |
|---|---|---|---|
| 输入顺序 | n d m e、eta、n 行、m 行 | n d m、λ、n 行、m 行含 ? | AI009 同 AI002;AI010 r c、r 行 logits、一行标签;AI012 d h m、h 行、一行、m 行 |
| 参数 | 从零出发,e 轮,梯度乘 2/n,同步更新 | 正规方程一次解出,λ 不加偏置 | AI009 梯度乘 1/n;AI010 / AI012 无训练 |
| 归一化 | 训练集 fit 一次;常数列 0;查询不截断 | 不归一化 | AI009 同 AI002;其余不归一化 |
| 特殊规则 | e=0 输出全零参数的预测 | |w_j| < 1e-10 → NA | AI009 sigmoid 分支;AI010 log-sum-exp;AI012 ReLU、按下标求和 |
| 输出 | m 行四位小数,|v| < 0.00005 → 0.0000 | 同左或 NA | AI009 首行 b 与 w;AI010 概率四位 + 交叉熵六位;AI012 四位 |
需要对照解法时,展开本页第 08 节的五份完整参考程序。复习与自评:本课的「学习完成检查」六条是自评,不改变题目的通过(AC)状态,本课算完成的条件以页面下方「学习完成检查」处的说明为准。复习时用三个问题自测:① 不看正文,写出一轮批量梯度下降的四个阶段和 2/n 出现的位置;② 不看表格,从 w=0.3、b=0.4 算出第 2 轮;③ 说出 λ 加在哪些对角元上、为什么偏置不加。答不出哪一条,就回到对应的节重读,再做第 10 节对应的练习。
12 / 练习
按顺序完成本课的任务
必做题已通过 0/2 道;进阶练习已通过 0/3 道
实现梯度函数(grad),再用数值梯度验证
代码自测自主练习练习重点:按正文公式写 grad(w, b),再把 b 增加 0.001 看损失变化,和 −4 核对;预计用时:10 分钟
完成标准:公式梯度与「改变一点参数、观察损失变化」得到的数值梯度在断言容差内一致
需要时查看提示
先按正文公式写 grad:dL/dw = Σ(ŷ−y)·x,dL/db = Σ(ŷ−y)。损失变化量除以步长就是数值梯度——之后任何训练题怀疑梯度推错,都可以用这个方法当场验证。第 04 节第一张表是逐样本的数字,参考实现在第 08 节。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
# 两条样本 (x=0, y=1)、(x=1, y=3),损失 = Σ(ŷ−y)²/2,w=b=0
SAMPLES = [(0, 1), (1, 3)]
def grad(w, b):
# 待完成:按正文公式返回 (dL/dw, dL/db)
...
def loss(w, b):
return sum((b + w * x - y) ** 2 for x, y in SAMPLES) / 2
# 按公式计算:dL/dw = Σ(ŷ−y)·x = -3,dL/db = Σ(ŷ−y) = -4
dw, db = grad(0, 0)
assert dw == -3 and db == -4
# 差分验证:把 b、w 各增加 0.001,损失变化量 / 0.001 应与公式值接近
assert abs((loss(0, 0.001) - loss(0, 0)) / 0.001 - db) < 0.01
assert abs((loss(0.001, 0) - loss(0, 0)) / 0.001 - dw) < 0.01AI002 · 归一化与线性预测
必做任务 1练习重点:先用训练集的最小值与最大值做 Min-Max 缩放,再从零参数跑固定轮数批量梯度下降;预计用时:40 分钟
完成标准:能解释为什么一轮的梯度要全部累加完,w 和 b 才一起更新
需要时查看提示
需要正确处理三种情况:常数列归一化为 0;e=0 时参数保持全零;查询值超出训练范围时不做区间截断。输出保留四位小数,绝对值小于 0.00005 时输出 0.0000。第 05 节有题面示例和一个二维用例的逐步表。
AI003 · 缺失值恢复
必做任务 2练习重点:带偏置的岭回归正规方程一次求解,再由目标值反推缺失特征;预计用时:35 分钟
完成标准:能说出 λ 加在正规方程矩阵的哪些对角元上、偏置为什么不加
需要时查看提示
把偏置当成恒为 1 的第 0 列,建 (d+1) 元正规方程;对角线除第 0 项外都加 λ,高斯消元求解;缺失列权重绝对值小于 1e-10 时输出 NA。λ=0 也可解——题目数据保证。第 06 节有一个能手解的 2×2 例子。
AI009 · 逻辑回归训练器
进阶练习 1进阶练习练习重点:Min-Max + 固定轮数批量梯度下降,梯度 (1/n)·Σ(p−y)·x̂,同步更新;预计用时:25 分钟
完成标准:能解释 (p−y)·x 为什么和线性回归同形
需要时查看提示
有一组用例的 |z| 达到 10⁵——sigmoid 按 z 的符号分支,保证传给指数函数(exp)的值始终为非正数。查询样本用训练集参数归一化、不截断。输出 4 位小数。
AI010 · 稳定 softmax 与交叉熵
进阶练习 2进阶练习练习重点:逐行减最大值的 softmax + 平均交叉熵,大数上溢与下溢都要处理;预计用时:25 分钟
完成标准:能解释交叉熵为什么要用 log-sum-exp 的写法
需要时查看提示
有一行 logits 绝对值到 1000:概率可以下溢成 0.0000,但交叉熵必须照常算对——先取对数再相减,不要对下溢后的 p 取对数。概率 4 位、平均交叉熵 6 位小数。
AI012 · MLP 前向传播
进阶练习 3进阶练习练习重点:ReLU 隐层 + 线性输出的两层前向,按下标从小到大求和;预计用时:25 分钟
完成标准:能手动计算一个 2×2 小网络的前向结果
需要时查看提示
逐神经元算 z = b + Σw·x、取 max(z,0),输出层不加激活。输出保留四位小数、绝对值小于 0.00005 时输出 0.0000——与 AI002 使用同一个格式化函数(fmt)。第 10 节练习 5 有题面示例的手推。
进阶自测:2→2→1 小网络的反向传播与梯度校验
进阶练习 4进阶练习自主练习练习重点:独立实现前向(forward)、损失(loss)、反向(backward)、数值梯度(numeric_grad)与一次更新(train_step),参数顺序与正文一致;预计用时:20 分钟
完成标准:9 个解析梯度与手算表逐格一致,且逐参数中心差分误差都小于 1e-6;能说出未激活 ReLU 与全零初始化各把梯度挡在哪里
需要时查看提示
先让 forward 把 z1、z2、h1、h2、y 全部返回,反向才有中间量可用;ReLU 的判断统一用 z > 0,z 恰好为 0 时导数按 0 处理。数值梯度每次只改一个参数,改完记得还原;全零初始化后打印前 6 个梯度,应该全是 0。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
params = [0.5, 0.25, 0.0, -1.0, 0.25, 0.0, 1.0, -0.5, 0.5]
x, t = (1.0, 2.0), 1.0
# 前向:与正文推演表逐格一致
assert forward(params, x) == (1.0, -0.5, 1.0, 0.0, 1.5)
assert loss(params, x, t) == 0.25
# 反向:9 个梯度与手算一致;未激活神经元(z2 = -0.5)名下的三个参数梯度为 0
assert backward(params, x, t) == [1.0, 2.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0]
# 梯度校验(gradient check):逐参数用中心差分核对解析梯度
grads = backward(params, x, t)
for i in range(9):
assert abs(grads[i] - numeric_grad(params, x, t, i)) < 1e-6
# 学习率 0.1 同步更新一次:预测 1.5 -> 0.76,损失 0.25 -> 0.0576
stepped = train_step(params, x, t, 0.1)
assert abs(forward(stepped, x)[-1] - 0.76) < 1e-9
assert abs(loss(stepped, x, t) - 0.0576) < 1e-9
assert loss(stepped, x, t) < loss(params, x, t)
# 学习率 1.0 时同一样本更新一次,损失反而从 0.25 增至 2.25(学习率过大)
assert abs(loss(train_step(params, x, t, 1.0), x, t) - 2.25) < 1e-9
# 全零初始化:隐藏层 6 个参数的梯度全为 0,只有输出偏置在更新
zero = [0.0] * 9
assert backward(zero, x, t)[:6] == [0.0] * 6提交结果
提交结果说明与处理方法
- WA
答案错误
查询样本要用训练集的最小值与最大值归一化;梯度记得乘 2/n;先算完全部梯度再一起更新;AI009 的 sigmoid 按符号分支。第 09 节的表给出了每种错误在题库用例上的输出
- PE
格式错误
每行保留四位小数;绝对值小于 0.00005 时输出 0.0000,不能出现 -0.0000
- RE
运行错误
按空白整体读入再切分;每行训练数据是 d 个特征加 1 个标签,若每行列数解析错误,后续字段会整体偏移;常数列直接除、漏判 NA、exp 溢出也都是运行错误
- TLE
超时
训练集只归一化一次,存下来复用;n≤500、e≤200 双重循环足够,不要在每轮里重算最小值与最大值
- AC
通过
用样例手动计算一轮 w 和 b 核对;再想想 λ 加大后 AI003 解出的权重会怎么变(练习 2)
13 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。