第六周复现了《机器学习实战》第 8 章中能够离线运行的核心代码,包括标准线性回归、局部加权线性回归、岭回归、前向逐步回归和岭回归交叉验证。相比前几周一直处理的分类问题,这一章第一次把目标换成连续数值,也让我开始关注预测值与真实值之间到底差了多少。

这篇复盘会讲清楚

  • 回归和分类到底有什么不同
  • 标准线性回归怎样找到一条合适的直线
  • 局部加权回归中的 k 为什么不能只看训练误差
  • 岭回归、逐步回归和交叉验证分别解决什么问题

一、回归要回答的是“具体有多少”

分类的输出通常是有限的类别。例如病马预测会回答“存活”或“死亡”,垃圾邮件过滤会回答“垃圾邮件”或“正常邮件”。回归的输出则是一个连续数值,例如房价、温度、销量或者鲍鱼年龄。

分类

像判断一道题属于单选题中的哪一个选项,结果是预先规定好的类别。

回归

像根据面积、地段和楼层估计房价,需要给出一个可以连续变化的数值。

这也让评价方式发生变化。分类常看错误率,回归则要计算预测值与真实值之间的距离。本章主要使用 RSS(残差平方和)

RSS = Σ(真实值 − 预测值)²

先求每个样本的误差,再平方并相加。平方可以避免正误差和负误差互相抵消,也会让较大的错误受到更明显的惩罚。

二、标准线性回归:用一条直线概括数据

标准线性回归希望找到一组权重 w,让所有样本的预测误差平方和尽量小:

ŷ = Xw

w = (XᵀX)⁻¹Xᵀy

可以把它想成在一堆散点中放一把直尺。直尺不一定穿过每个点,但要让它与所有点的总体偏差尽可能小。书中的实现先计算 XᵀX,确认行列式不为 0 后再求逆:

def standRegres(xArray, yArray):
    xMatrix = np.matrix(xArray)
    yMatrix = np.matrix(yArray).T
    xTx = xMatrix.T * xMatrix

    if np.linalg.det(xTx) == 0.0:
        print("This matrix is singular, cannot do inverse")
        return None

    return xTx.I * (xMatrix.T * yMatrix)

作者配套的 ex0.txt 一共有 200 个样本。实际运行得到:

回归权重:[3.00774324, 1.69532264]
训练集 RSS:1.355249
前 5 个预测值:
[3.12257084, 3.73301922, 4.69582855, 4.25946098, 4.67099547]

这里的 RSS 适合在同一批数据、同一目标尺度下比较模型,不能脱离数据直接说“1.35 一定很好”。样本数量或者目标值量级变化后,RSS 的大小也会跟着变化。

三、局部加权线性回归:预测时更相信附近样本

标准线性回归只生成一个全局模型。如果数据在不同区域有不同趋势,一条直线可能过于简单。局部加权线性回归在预测每个点时临时建立模型,并让附近样本拥有较大权重,远处样本拥有较小权重。

估计某套房子的价格时,同一小区、面积相近的房子通常比几十公里外的房子更值得参考。

书中使用高斯核计算权重:

w(i,i) = exp(‖x(i) − x‖² ÷ (−2k²))

k 控制“附近”到底有多大。k 大时,较远的样本仍有一定影响,曲线比较平滑;k 小时,模型主要听取非常靠近预测点的样本,曲线会更贴近训练数据。

k训练集 RSS曲线特点
1.01.354961较平滑,接近标准线性回归
0.010.148829明显贴近训练点
0.0030.068660弯折更多,最贴近训练数据
标准线性回归与三个不同 k 值的局部加权线性回归曲线对比
图 1:k 越小,局部模型越重视距离预测点很近的样本,训练曲线也越容易追随数据的细小波动。

四、普通最小二乘为什么会不稳定

标准线性回归需要对 XᵀX 求逆。当两个特征高度相关、特征数很多或者样本不足时,矩阵可能不可逆;即使勉强能够求逆,得到的权重也可能因为数据的一点变化而大幅波动。

例如房屋的“建筑面积”和“使用面积”通常高度相关。模型同时使用这两个特征时,很难稳定区分究竟应该把多少作用分给哪一列。

这不是 Python 独有的问题,而是公式本身对数据条件有要求。书中遇到矩阵不可逆时直接返回,岭回归则通过在矩阵上增加一个限制来改善这个问题。

五、岭回归:不要让某个权重过于激进

岭回归在原来的矩阵中加入 lambda I

w = (XᵀX + λI)⁻¹Xᵀy

I 是单位矩阵,lambda 决定限制强度。可以把它理解成给模型加了一条规则:每个特征都可以参与预测,但不要轻易把某一个权重放得特别大。

本次使用 4177 条鲍鱼数据测试了 30 个不同的 lambda。最大值约为 178482300.96,此时八个权重都已经非常接近 0。这个实验说明了权重收缩现象,但不能把“权重绝对值最小”误称为最佳模型。

岭回归负责提供一组从宽松到严格的候选限制;哪一个 lambda 更合适,要由新数据上的误差来判断。

六、前向逐步回归:一次只调整一个方向

前向逐步回归从全部权重为 0 开始。每一轮依次尝试让某个特征的权重增加或减少一个很小的步长,计算 RSS,并保留误差最低的调整。

像在黑暗中下山:每次只朝一个方向迈一小步,比较哪一步下降最多,再决定真正往哪里走。
for iteration in range(numberOfIterations):
    lowestError = float("inf")

    for featureIndex in range(featureCount):
        for direction in (-1, 1):
            trialWeights = weights.copy()
            trialWeights[featureIndex] += eps * direction
            prediction = xMatrix * trialWeights
            currentError = rssError(yMatrix.A, prediction.A)

            if currentError < lowestError:
                lowestError = currentError
                bestWeights = trialWeights.copy()

    weights = bestWeights.copy()

按照作者 Notebook 的参数,在鲍鱼数据上设置 eps=0.01,运行 200 轮后得到:

[0.05, 0.00, 0.09, 0.03, 0.31, -0.64, 0.00, 0.36]

有些权重仍然为 0,表示在当前步长和轮数下,它们没有被优先选择。这个算法速度不快,但每次只改一个特征,能够清楚展示权重是怎样一步步进入模型的。

七、交叉验证:不要拿练习题给自己打分

如果使用训练数据选择 lambda,就像做完练习题后仍拿同一套题给自己考试,容易高估效果。交叉验证会多次随机划分训练部分和测试部分:

  1. 使用训练部分计算 30 个 lambda 对应的岭回归模型。
  2. 在没有参与本轮训练的测试部分上计算 RSS。
  3. 重复多次,比较每个 lambda 的平均测试误差。

本次固定随机种子并进行 10 次验证,得到:

平均错误最小的 lambda 下标:0
对应 lambda:0.00004540
最小平均 RSS:2046.026659

固定随机种子是为了让每次运行都能复现同一结果,不代表换一组随机划分仍会得到完全一样的数字。更稳妥的做法是继续比较不同划分、不同评价指标,并保留真正独立的测试集。

八、源码复现中遇到的兼容问题

书中的 regularize 函数会让每一列减去均值,再除以该列方差。如果一列的所有值都相同,方差就是 0,直接相除会出现 NaN 和运行警告。

这次没有改变书中正常特征的算法,只对常数列作了兼容处理:

centered = xMatrix.copy()
columnMeans = np.mean(centered, 0)
columnVariance = np.var(centered, 0)

safeVariance = np.asarray(columnVariance, dtype=float).copy()
safeVariance[safeVariance == 0] = 1.0

return (centered - columnMeans) / safeVariance

常数列减去自己的均值后本来就是全 0,把除数从 0 改为 1 后结果仍为 0;其他有变化的列继续除以原方差,与书中公式一致。回归测试还会检查没有 RuntimeWarning、结果全部有限,并确认正常列没有被错误清零。

九、本周没有写成“已完成”的内容

作者第 8 章后半部分包含乐高商品网页抓取和旧版 Google Shopping API。相关服务已经失效,无法按照原流程获得数据,因此本周只复现了使用作者配套数据、能够离线验证的回归核心部分。

我没有为了让目录看起来更完整而编造一个新的商品接口,也没有在周报中写成“已经运行成功”。源码复现除了知道完成了什么,也应该把无法验证的边界记录清楚。

十、目前还没有真正弄懂的问题

  1. 最小二乘公式的推导。现在会使用公式,也知道目标是让 RSS 最小,但还不能从求导开始完整推到闭式解。
  2. 局部加权回归的高斯权重。能够解释距离越近权重越大,却还需要理解指数形式和 k 的具体影响。
  3. 岭回归为何更稳定。已经看到权重随 lambda 收缩,但对特征值、矩阵条件数和不可逆问题之间的关系还不熟。
  4. 参数选择。训练 RSS 较小不代表泛化更好,还需要通过测试误差理解过拟合与欠拟合。

十一、下周计划

  1. 01
    手算一个最小二乘例子

    从 RSS 写起,补一遍对权重求导并令导数为 0 的过程。

  2. 02
    比较训练误差与测试误差

    把局部加权回归的 k 放到独立测试数据上比较,而不是只看训练 RSS。

  3. 03
    补做上周遗留实验

    继续整理 AdaBoost 轮数和 SVM 核参数对结果的影响。

  4. 04
    进入第 9 章树回归

    学习用树结构处理回归问题,并比较它与本章线性模型的差别。

十二、第六周最值得保留的认识

数值误差局部约束验证

回归预测的是连续数值,RSS 负责描述总体误差;局部加权回归让模型在不同位置使用不同的局部参考,岭回归通过约束降低权重的不稳定,最后还要用交叉验证判断参数能否应对未见数据。

如果只用一句话总结第六周,那就是:模型把训练数据拟合得更紧,并不自动等于预测得更好;真正重要的是它面对新数据时还能不能保持可靠。