碎碎念念

小学期的大作业(为什么暑假要上课),再也不想写这种令人心情不愉快的作业了口牙!

本文结合小规模示例介绍以下内容:

  • 最小二乘线性回归
  • 决策树回归
  • 决策树分类
  • K-Means 聚类与 K-Means++ 初始化

图形界面使用 EasyX 完成。EasyX 在项目中主要负责绘制散点、曲线、聚类中心和处理按钮点击,本文不对其接口展开介绍。

回归、分类和聚类有什么区别

机器学习任务可以先从“有没有标签”和“要预测什么”两个角度理解。

任务 数据特点 输出结果 简单例子
回归 有标签 连续数值 根据学习时间预测成绩
分类 有标签 离散类别 判断鸢尾花属于哪个品种
聚类 无标签 自动形成的分组 将位置相近的数据点分成若干组

回归和分类都需要已知答案来训练,因此属于监督学习;聚类没有预先提供类别,因此属于无监督学习。

最小二乘线性回归

线性回归希望用一条直线描述输入 $x$ 与目标值 $y$ 之间的关系:

$$ y=ax+b $$

其中,$a$ 是斜率,$b$ 是截距。最小二乘法会选择一组 $a$ 和 $b$,使所有样本的残差平方和尽可能小:

$$ \sum_{i=1}^{n}\left[y_i-(ax_i+b)\right]^2 $$

平方可以避免正误差和负误差相互抵消,同时会让较大的误差受到更明显的惩罚。

斜率和截距可以直接计算:

$$ a=\frac{n\sum x_i y_i-\sum x_i\sum y_i} {n\sum x_i^2-(\sum x_i)^2} $$

$$ b=\frac{\sum y_i-a\sum x_i}{n} $$

一个简单例子

假设有三个样本:

$x$ $y$
1 2
2 3
3 5

可以得到:

$$ \sum x_i=6,\quad \sum y_i=10,\quad \sum x_i y_i=23,\quad \sum x_i^2=14 $$

代入公式:

$$ a=\frac{3\times23-6\times10}{3\times14-6^2}=1.5 $$

$$ b=\frac{10-1.5\times6}{3}=\frac{1}{3} $$

最终拟合直线约为:

$$ y=1.5x+0.33 $$

当输入 $x=4$ 时,模型预测:

$$ y=1.5\times4+0.33=6.33 $$

C++ 实现思路

程序只需遍历一次样本,累计四个统计量,再代入公式即可:

void LeastSquares(const std::vector<DataPoint>& points,
                  double& a, double& b) {
    if (points.empty()) {
        a = b = 0.0;
        return;
    }

    double sumX = 0.0;
    double sumY = 0.0;
    double sumXY = 0.0;
    double sumX2 = 0.0;

    for (const auto& point : points) {
        sumX += point.x;
        sumY += point.y;
        sumXY += point.x * point.y;
        sumX2 += point.x * point.x;
    }

    const double n = static_cast<double>(points.size());
    const double denominator = n * sumX2 - sumX * sumX;
    if (std::abs(denominator) < 1e-12) {
        a = b = 0.0;
        return;
    }

    a = (n * sumXY - sumX * sumY) / denominator;
    b = (sumY - a * sumX) / n;
}

该算法的时间复杂度为 $O(n)$,额外空间复杂度为 $O(1)$。如果所有样本的 $x$ 都相同,公式分母为零,此时无法求出唯一斜率,需要单独处理。

决策树回归

线性回归只能拟合一条直线。当数据关系不是线性的,可以使用决策树回归。

决策树回归会不断选择阈值,把数据分成左右两部分。例如:

x <= 3.5 进入左子树
x > 3.5 进入右子树

每个叶节点保存该区域内所有目标值的平均值。预测时,从根节点出发,根据输入与阈值的关系向左或向右移动,最后返回叶节点的均值。

如何选择划分位置

回归树通常用均方误差衡量一组数据是否集中:

$$ \operatorname{MSE}(D)=\frac{1}{|D|} \sum_{i=1}^{|D|}(y_i-\bar y)^2 $$

对一个候选阈值,需要计算左右子集的加权误差:

$$ L=\frac{|D_L|}{|D|}\operatorname{MSE}(D_L) +\frac{|D_R|}{|D|}\operatorname{MSE}(D_R) $$

算法选择 $L$ 最小的阈值。

一个简单例子

假设数据如下:

$x$ $y$
1 2
2 2
3 8
4 8

候选阈值可以取相邻 $x$ 的中点,即 $1.5$、$2.5$ 和 $3.5$。

如果按 $x=2.5$ 划分:

  • 左侧目标值为 $\lbrace 2,2\rbrace$,均值为 $2$,MSE 为 $0$;
  • 右侧目标值为 $\lbrace 8,8\rbrace$,均值为 $8$,MSE 为 $0$;
  • 加权误差为 $0$。

因此,$2.5$ 是一个非常合适的阈值。模型得到的规则是:

x <= 2.5 时预测 2
x > 2.5 时预测 8

这也解释了为什么回归树的预测图像通常是阶梯形:同一个叶节点对应的输入区间会得到相同预测值。

前缀和优化

如果每测试一个阈值都重新遍历左右数据,计算开销会比较大。项目在排序后建立 $y$ 和 $y^2$ 的前缀和。

对任意区间,可以快速得到:

$$ \operatorname{MSE}=E[y^2]-E[y]^2 $$

这样每个候选切分位置的误差可以在 $O(1)$ 时间内计算。单个节点中,排序复杂度为 $O(n\log n)$,扫描候选阈值为 $O(n)$。

树深对结果的影响

树的最大深度决定模型复杂度:

  • 深度较小,划分次数少,曲线简单,可能欠拟合;
  • 深度较大,模型可以描述更多局部变化,但可能过拟合;
  • 设置最小分裂样本数,可以避免节点在样本很少时继续划分。

项目同时绘制浅层树和较深的树,可以直观看到预测曲线从粗略到细致的变化。

决策树分类

决策树分类与决策树回归的整体结构相似,区别主要在于:

  • 回归树预测连续数值,叶节点保存平均值;
  • 分类树预测离散类别,叶节点保存多数类别;
  • 回归树可以用 MSE 选择阈值;
  • 分类树可以用信息增益选择阈值。

项目使用 Iris 鸢尾花数据集。每个样本包含四个特征:

  • 花萼长度
  • 花萼宽度
  • 花瓣长度
  • 花瓣宽度

目标类别包括山鸢尾、变色鸢尾和维吉尼亚鸢尾。

信息熵

信息熵用于描述类别的混乱程度:

$$ H(D)=-\sum_{k=1}^{K}p_k\log_2p_k $$

其中,$p_k$ 是第 $k$ 类样本所占比例。

如果节点中的样本全部属于同一类,熵为 $0$;如果各类别数量接近,数据更加混乱,熵会更大。

一个简单例子

假设某节点中有四个样本:

A, A, B, B

两类比例都为 $\frac{1}{2}$,因此:

$$ H(D)=-\frac{1}{2}\log_2\frac{1}{2} -\frac{1}{2}\log_2\frac{1}{2}=1 $$

现在用某个特征进行划分,得到:

左侧:A, A
右侧:B, B

左右子集的熵都是 $0$。信息增益为:

$$ Gain(D)=H(D)-\frac{|D_L|}{|D|}H(D_L) -\frac{|D_R|}{|D|}H(D_R)=1 $$

这次划分完全分开了两个类别,因此效果很好。

建树过程

分类树的训练过程可以概括为:

  • 遍历每个特征;
  • 对特征值排序并去重;
  • 将相邻特征值的中点作为候选阈值;
  • 计算每个候选划分的信息增益;
  • 选择信息增益最大的特征和阈值;
  • 对左右子集递归执行相同操作。

当样本已经属于同一类别、达到最大深度或找不到有效划分时停止递归,并将当前节点设置为叶节点。

如何评价分类结果

最常用的基础指标是准确率:

$$ Accuracy=\frac{\text{预测正确的样本数}} {\text{样本总数}}\times100% $$

项目先使用固定随机种子打乱 Iris 数据,再按照 $8:2$ 划分训练集和测试集。固定随机种子可以让每次运行得到相同的数据划分,方便比较和复现实验结果。

训练准确率很高并不一定代表模型效果好,还需要观察测试准确率。如果训练准确率明显高于测试准确率,模型可能出现了过拟合。

K-Means 聚类

K-Means 用于在没有标签的情况下,将数据自动分成 $K$ 个簇。它希望同一簇内的样本彼此接近,不同簇的中心尽量分开。

算法的基本过程是:

  • 选择 $K$ 个初始聚类中心;
  • 将每个样本分配给距离最近的中心;
  • 对每个簇中的样本求平均,得到新中心;
  • 重复分配和更新,直到中心几乎不再移动。

二维空间中常用欧几里得距离:

$$ d(\mathbf{x},\boldsymbol{\mu})= \sqrt{\sum_{j=1}^{m}(x_j-\mu_j)^2} $$

一个简单例子

假设有六个二维点:

(1, 1), (1, 2), (2, 1)
(8, 8), (8, 9), (9, 8)

设置 $K=2$,初始中心选择 $(1,1)$ 和 $(8,8)$。

第一次分配后,前三个点更接近 $(1,1)$,后三个点更接近 $(8,8)$。随后计算两个簇的新中心:

$$ \boldsymbol{\mu}_1= \left(\frac{1+1+2}{3},\frac{1+2+1}{3}\right) =(1.33,1.33) $$

$$ \boldsymbol{\mu}_2= \left(\frac{8+8+9}{3},\frac{8+9+8}{3}\right) =(8.33,8.33) $$

再次分配时,各点所属簇不会改变,中心也保持稳定,算法便可以停止。

为什么需要 K-Means++

普通 K-Means 如果随机选出的初始中心过于接近,可能需要更多次迭代,也可能得到较差结果。

K-Means++ 的初始化方式是:

  • 随机选择第一个中心;
  • 计算每个样本到最近已有中心的距离平方;
  • 按距离平方作为权重选择下一个中心;
  • 重复以上过程,直到得到 $K$ 个中心。

距离现有中心越远的点,被选为新中心的概率越大。因此,初始中心通常会分布得更分散。

需要注意,这里是“按概率选择”,而不是每次直接选择最远点。这样可以降低异常值被固定选中的风险。

收敛与空簇处理

项目计算新旧中心之间的位移。如果最大位移不超过容差,就认为算法已经收敛:

$$ \max_k\left\lVert\boldsymbol{\mu}_k^{new} -\boldsymbol{\mu}_k^{old}\right\rVert\leq\varepsilon $$

如果某个簇没有分配到任何样本,就无法计算平均值。一个简单的处理方式是暂时保留该簇原来的中心。也可以重新选择距离其他中心较远的样本作为中心。

K-Means 的局限也比较明显:需要提前指定 $K$,对初始中心敏感,更适合近似球状、大小接近的簇,而且容易受到异常值影响。

可视化如何帮助理解算法

图形界面不改变算法本身,但能把抽象结果转换为更直观的图像:

  • 在线性回归中,同时绘制原始散点和拟合直线;
  • 在决策树回归中,用阶梯线展示分段常数预测;
  • 在决策树分类中,用不同颜色表示不同类别;
  • 在 K-Means 中,用颜色表示聚类标签,用特殊标记显示中心。

从数据坐标转换到屏幕坐标时,横坐标可以按比例映射:

$$ x_s=x_{left}+\frac{x-x_{min}}{x_{max}-x_{min}} (x_{right}-x_{left}) $$

由于屏幕纵坐标通常向下增大,纵坐标映射需要反向:

$$ y_s=y_{bottom}-\frac{y-y_{min}}{y_{max}-y_{min}} (y_{bottom}-y_{top}) $$

项目使用 EasyX 完成这些基础绘制和交互。对于理解算法而言,重点仍然是数据如何训练、模型如何预测以及结果为什么呈现出当前形态。

几种算法的对比

算法 任务 主要依据 结果特点
最小二乘线性回归 回归 残差平方和最小 一条连续直线
决策树回归 回归 加权 MSE 最小 分段常数、阶梯形
决策树分类 分类 信息增益最大 一组可解释的判断规则
K-Means 聚类 样本到中心的距离 自动形成若干数据簇

线性回归结构简单、计算速度快,但只能描述近似线性关系。决策树能够表达非线性规则,解释也比较直观,但树过深时容易过拟合。K-Means 不需要标签,实现简单,不过需要提前指定簇数,并且会受到数据分布和初始中心影响。

总结

通过手动实现这些基础算法,可以更清楚地看到机器学习的核心过程:

  • 回归是在寻找输入到连续数值的映射;
  • 分类是在学习区分不同类别的规则;
  • 聚类是在没有标签时发现数据内部的分组;
  • 损失函数或评价指标决定了算法如何选择更好的参数;
  • 停止条件和边界处理决定了程序是否可靠。

相比只调用现成接口,小规模示例更适合观察每一步计算。把算法结果绘制出来后,拟合直线、树的阶梯预测和聚类中心都会变得容易理解,这也是该 C++ 可视化项目最主要的学习价值。