天气越热,冰咖啡卖得越多——小黑板一直这么写。穿过销售数据的一条直线可以验证这句话。斜率是每升高一度多卖几杯?
线性回归是最小的有用模型:一个输入、一个输出和一条直线。亲手实现它能看清“拟合”到底在做什么、预测如何产生,以及为什么训练点上的低误差不等于模型能够泛化。 你还会用 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 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。