(十九)重复更新与最小二乘核对
1. 每轮保留当前损失
一次更新后继续用新参数预测,重新得到残差、梯度,再更新。把这个过程组织成 train:
# 续写同一文件:analysis_project/src/readingarray/regression.py
# 每轮用当前参数计算误差和梯度,再同时更新参数;history 保留更新前的损失。
def train(x, y, steps=400, learning_rate=0.1):
x, y = checked_xy(x, y)
if steps < 1 or not np.isfinite(learning_rate) or learning_rate <= 0:
raise ValueError("更新次数和学习率应为正数")
bias, weight = 0.0, 0.0
history = []
for _ in range(steps):
loss, grad_bias, grad_weight = loss_and_gradient(x, y, bias, weight)
if not np.isfinite([loss, grad_bias, grad_weight]).all():
raise ValueError("更新产生了非有限数,检查数据尺度与学习率")
history.append(loss)
bias -= learning_rate * grad_bias
weight -= learning_rate * grad_weight
history.append(loss_and_gradient(x, y, bias, weight)[0])
return np.array([bias, weight]), np.array(history)
def solve_least_squares(x, y):
x, y = checked_xy(x, y)
design = np.column_stack((np.ones(x.size), x))
parameters, _, _, _ = np.linalg.lstsq(design, y, rcond=None)
return parametershistory 的第 0 项对应初始参数,最后一项对应完成全部更新后的参数。因此进行 400 次更新会留下 401 个损失值。学习率增大时,一步可能走过较好的位置;实际比较每轮损失,才能看出是在稳定下降,还是不断来回跳动。
2. 用设计矩阵表达同一个模型
solve_least_squares 把常量 1 与 x 组成两列:[[1,1],[1,2],[1,3]]。乘上参数向量 [b,w],恰好得到每条 b+w*x。
linalg 是线性代数工具模块,np.linalg.lstsq(设计矩阵,y,rcond=None) 直接求最小二乘解;返回值中第一项是参数,后面还有残差、秩和奇异值等信息。这里用解构取出第一项,得到约 [0,2],再与迭代结果比较。最小二乘接口
本例的直线可以直接求解。逐步更新的写法则让我们看清参数、预测和损失之间的依赖;后面模型变复杂时,仍会沿着这些依赖计算梯度,再更新参数。