Python 现代开发学习笔记

(十八)从损失变化推导一次参数更新

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δ+δ²。对所有记录求平均后,损失变化为:

J(b+δ,w)−J(b,w)=2mean⁡(e)δ+δ2.J(b+\delta,w)-J(b,w)=2\operatorname{mean}(e)\delta+\delta^2.

δ 越接近 0,决定变化方向的主要是一次项系数 2*mean(e)。它就是对 b 的导数,记作 ∂J/∂b\partial J/\partial b。

给 w 加 δ 时,第 i 条预测增加的是 x_i*δ,因此残差变为 e_i+x_i*δ。展开平方并求平均,得到:

J(b,w+δ)−J(b,w)=2mean⁡(e⋅x)δ+mean⁡(x2)δ2.J(b,w+\delta)-J(b,w)=2\operatorname{mean}(e\cdot x)\delta+\operatorname{mean}(x^2)\delta^2.

对应的一次项系数为 2*mean(e*x)。这里的 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

两者都为负,说明往正方向小幅移动会先让损失下降。学习率 η 决定本次移动多少,更新写为“旧参数减去学习率乘梯度”:

bnew=b−η∂J∂b,wnew=w−η∂J∂w.b_{new}=b-\eta\frac{\partial J}{\partial b},\qquad w_{new}=w-\eta\frac{\partial J}{\partial w}.

取 η=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 降到这个值。

旧参数零产生残差负二负四负六,两项梯度分别累计并乘学习率,新参数同时进入下一次预测

笔记正文与全部题解 · Ctrl / ⌘ K