(十八)从损失变化推导一次参数更新
1. 先定义一条直线的预测
用自拟时长单位 x 为 [1,2,3],完成页数单位 y 为 [2,4,6]。希望用直线 预测=b+w*x 表达关系:w 为斜率,b 为截距。先把两者都设为 0,预测全是 0,残差为 [−2,−4,−6]。
于是平方为 [4,16,36],损失 J=56/3。现在需要知道:b 或 w 往哪边小幅移动,会让损失先减小。
2. 展开一个小变化产生的差
先看第 i 条记录的残差 e。给 b 加上小量 δ,残差变为 e+δ,平方从 e² 变成 e²+2eδ+δ²。对所有记录求平均后,损失变化为:
δ 越接近 0,决定变化方向的主要是一次项系数 2*mean(e)。它就是对 b 的导数,记作 。
给 w 加 δ 时,第 i 条预测增加的是 x_i*δ,因此残差变为 e_i+x_i*δ。展开平方并求平均,得到:
对应的一次项系数为 2*mean(e*x)。这里的 x 来自“斜率改变时,每条预测按各自 x 放大了多少”,不是额外补入的乘数。
3. 代入数字再同时更新
# 项目文件:analysis_project/src/readingarray/regression.py
# 一维输入对应一维目标;先由同一份旧参数算梯度,再同时更新斜率和截距。
import numpy as np
def checked_xy(x, y):
x, y = np.asarray(x, dtype=np.float64), np.asarray(y, dtype=np.float64)
if x.ndim != 1 or y.ndim != 1 or x.shape != y.shape or x.size == 0:
raise ValueError("x、y 应为等长且非空的一维数组")
if not np.isfinite(x).all() or not np.isfinite(y).all():
raise ValueError("x、y 中存在缺失值或无穷值")
return x, y
def loss_and_gradient(x, y, bias, weight):
error = bias + weight * x - y
loss = float(np.mean(error**2))
grad_bias = float(2 * error.mean())
grad_weight = float(2 * np.mean(error * x))
return loss, grad_bias, grad_weight这份计算把两项梯度都从同一份旧残差得到。初始时:
| 对象 | 手算 | 梯度 |
|---|---|---|
| b | 2×(−2−4−6)/3 |
−8 |
| w | 2×(−2×1−4×2−6×3)/3 |
−56/3 |
两者都为负,说明往正方向小幅移动会先让损失下降。学习率 η 决定本次移动多少,更新写为“旧参数减去学习率乘梯度”:
取 η=0.1,得到新 b=0.8,新 w=28/15≈1.866667。更新之前先把两项梯度算好,再一起用来更新,避免先改 b、又拿新 b 去计算另一项旧参数的梯度。
# 项目文件:demos/gradient_step.py
# 自拟三条时长与页数,旧参数都为零;先算两份梯度,再同时更新。
import numpy as np
from readingarray.regression import loss_and_gradient
x, y = np.array([1.0, 2.0, 3.0]), np.array([2.0, 4.0, 6.0])
bias, weight, rate = 0.0, 0.0, 0.1
loss, grad_bias, grad_weight = loss_and_gradient(x, y, bias, weight)
print("旧损失与梯度", round(loss, 6), round(grad_bias, 6), round(grad_weight, 6))
bias -= rate * grad_bias
weight -= rate * grad_weight
print("新参数", round(bias, 6), round(weight, 6))
print("新预测", (bias + weight * x).round(6).tolist())
print("新损失", round(loss_and_gradient(x, y, bias, weight)[0], 6))新预测约为 [2.666667,4.533333,6.4],新损失约为 0.296296。可以对照每条预测、残差和平方,看到一次更新怎样从 18.666667 降到这个值。