第 46 课 · 进阶 · 19 分钟

Python 线性回归:从直线到误差

拟合一条直线,做预测,再衡量它错了多少。

线性回归斜率截距均方误差

天气越热,冰咖啡卖得越多——小黑板一直这么写。穿过销售数据的一条直线可以验证这句话。斜率是每升高一度多卖几杯?

线性回归是最小的有用模型:一个输入、一个输出和一条直线。亲手实现它能看清“拟合”到底在做什么、预测如何产生,以及为什么训练点上的低误差不等于模型能够泛化。 你还会用 R² 把直线与均值比较,并检查异常值的影响。

直线就是一条预测规则

斜率表示输入每增加 1 时预测值变化多少,截距表示输入为 0 时的预测值。两者合起来把输入 x 变成一个预测输出。

def predict(x, slope, intercept):
    return slope * x + intercept

print(predict(3, 2.0, 1.0))

两个数字定义一条直线

用中心化求和拟合

只有一个输入时,最小二乘斜率等于 x 与 y 的协方差除以 x 的方差。先把两组数减去均值,可以让算式更小。截距再让直线穿过均值点。

xs = [1, 2, 3]
ys = [2, 4, 6]
mean_x = sum(xs) / len(xs)
mean_y = sum(ys) / len(ys)
numerator = sum((x - mean_x) * (y - mean_y) for x, y in zip(xs, ys))
denominator = sum((x - mean_x) ** 2 for x in xs)
slope = numerator / denominator
intercept = mean_y - slope * mean_x
print(round(slope, 2), round(intercept, 2))

直线穿过均值点

先预测,再衡量误差

均方误差把每个真实值与预测值之差的平方取平均。平方会放大严重偏差,结果单位也是输出单位的平方。只要可能,就应在未参与拟合的数据上报告指标。

xs = [1, 2, 3]
ys = [2, 4, 5]
mean_x = sum(xs) / len(xs)
mean_y = sum(ys) / len(ys)
slope = sum((x - mean_x) * (y - mean_y) for x, y in zip(xs, ys)) / sum((x - mean_x) ** 2 for x in xs)
intercept = mean_y - slope * mean_x
mse = sum((y - (slope * x + intercept)) ** 2 for x, y in zip(xs, ys)) / len(xs)
print(round(slope * 4 + intercept, 2))
print(round(mse, 2))

预测和误差是两个问题

守住输入范围

直线对任何 x 都能算出数字,但这不代表观察范围之外的结果可信。记录训练范围,在越界时给出明确警告或回退,而不是悄悄外推。

def within_range(x, xs):
    return min(xs) <= x <= max(xs)

print(within_range(4, [1, 2, 3]))
print(within_range(2, [1, 2, 3]))

有数字不等于有保证

R² 将直线与均值比较

R² 等于 1 减去残差平方和与围绕均值的总平方和之比。接近 1 说明直线解释了大部分变化;等于 0 说明它不比始终预测均值更好。在新数据上,如果直线比这个基线更差,R² 可以为负。

xs = [1, 2, 3, 4]
ys = [2, 4, 5, 8]
mean_y = sum(ys) / len(ys)
mean_x = sum(xs) / len(xs)
slope = sum((x - mean_x) * (y - mean_y) for x, y in zip(xs, ys)) / sum((x - mean_x) ** 2 for x in xs)
intercept = mean_y - slope * mean_x
ss_res = sum((y - (slope * x + intercept)) ** 2 for x, y in zip(xs, ys))
ss_tot = sum((y - mean_y) ** 2 for y in ys)
print(round(1 - ss_res / ss_tot, 2))

相对常数预测的改进

一个异常值就能拉动直线

最小二乘法让平方残差总和最小,因此远离模式的点会把直线拉向自己。比较包含和不包含该点的拟合,检查残差,并报告这一决定。不要悄悄删掉不方便的观测。

def fit(xs, ys):
    mx = sum(xs) / len(xs)
    my = sum(ys) / len(ys)
    slope = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / sum((x - mx) ** 2 for x in xs)
    return slope, my - slope * mx

for ys in ([2, 4, 6, 8], [2, 4, 6, 20]):
    slope, intercept = fit([1, 2, 3, 4], ys)
    print(round(slope, 2), round(intercept, 2))

敏感性也是结果的一部分

本课要点

  • 简单线性模型只保存一个斜率和一个截距。
  • 最小二乘选择平方误差最小的直线。
  • MSE 适合比较模型,但单位是输出单位的平方。
  • 误差应报告在留出数据上,而不只是拟合数据。
  • 信任外推之前,先检查输入是否在训练范围内。
  • R² 衡量相对均值的改进;异常值可能主导最小二乘拟合。

常见错误

  • 把训练集完美拟合当成泛化能力的证据。
  • 所有 x 相同时仍然直接除以零。
  • 直接比较不同输出单位的 MSE。
  • 悄悄删掉异常值,再把更干净的拟合当成没有做过任何决定。
轮到你

拟合点 (1, 5)、(2, 8)、(3, 11),然后打印 x = 4 的预测值,保留一位小数。

动手试试

改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。

浏览器内 Python本地运行 · 无需账号
首次运行会从 CDN 下载 Pyodide。修改后点击“运行代码”。

练习本课

12 道题,作答后立即看到解释。

查看 12 道题答案开始练习
← 提示词校验梯度下降 →