📘 第7章 统计机器学习(无监督学习和有监督学习)
📄 一句话概括

把答案加回去,问题立刻变得可算:猜对了就是对了,猜错了就是错了。但代价跟着来—— 你得先有一批标好的数据,还得留出一部分自己不看,专门用来考试。 这一节的四件事:怎么用邻居投票(k 近邻)、怎么用问题切空间(决策树)、 怎么把「对不对」拆成四个格子(混淆矩阵)、以及为什么维度一高这些方法就失灵。

📝 本节和前面几节的关系

这一节是 7.1 的镜像:上一节连答案都没有,这一节答案多到能算分数。 两节合起来把「从数据里学」的两种基本姿势摆完——剩下的事,都是在这两种姿势上加复杂度。 它跟第 6 章的关系是「评价」。6.2 讲的后验、6.4 讲的解码,都在回答「哪个更可能」; 这一节把它变成一个工程问题:在有限的数据上,「更可能」能不能兑现成「更准」。 答案是不能保证,只能留出测试集去看。

一、有标签之后,问题变成什么

标签有两种样子,对应两类任务。这个区分不是学究式的分类,它决定了你用哪个数衡量好坏:

标签长什么样 任务 输出什么 好坏怎么量
一个类别 分类 类别或各类概率 准确率、精确率、召回率
一个数 回归 一个实数 均方误差、R²
Demo 先做了一遍回归:真关系是 y = 2.4x − 1,加噪声造 80 个点,
用最小二乘拟合出来 y = 2.326x − 0.979,R² = 0.9332。
R² 的意思是「模型解释了 93.32% 的波动」,剩下 6.68% 是噪声和自己没抓住的部分。
然后换成分类。造 400 个点,两团各在一个位置,故意让它们重叠:中心隔 1.87,
各自标准差 0.62。这一步是故意的——如果两团分得干干净净,什么算法都能拿 100%,就没什么可看的了。
260 个当训练集(带标签),140 个当测试集。测试集的标签先藏起来,只在最后打分时用一次。
这条规矩看着简单,是整个领域的底线,
因为一旦你反复用测试集调参数,测试集就变成了训练集的一部分,分数立刻虚高。

二、k 近邻:拿训练集当模型

最简单的有监督算法:来了新点,找最近的 k 个训练点,少数服从多数。它没有「训练」这一步, 模型就是训练集本身——这类方法叫惰性学习,代价是预测时要把所有距离算一遍。

k 训练集准确率 测试集准确率
1 1.0000 0.9143
3 0.9462 0.9071
5 0.9308 0.9143
9 0.9308 0.9429
15 0.9308 0.9429
25 0.9231 0.9357
45 0.9269 0.9429
75 0.9269 0.9429
k=1 那行的训练集准确率是 1.0000,这是必然的——每个训练点最近的邻居就是它自己。
所以「k 近邻的训练集准确率」这个数几乎没有信息量。
k=1 的测试集是 0.9143,k=9 之后升到 0.9429。这个差距看着不大,
但它说明了一件事:k=1 的边界在绕噪声走。每个训练点都被当成自己家门口,
边界上凸出一块只为包住那一个点,换个点来问就答错。
不过也别把 0.9429 这个「最高」太当真。测试集只有 140 个点,一个点就是 0.0071,
表里 9 到 75 那几行差两三个点,换算下来就是一两次运气。
能稳的结论只有一句:k 大了偏差大、k 小了方差大。
k 近邻还有个隐性的成本:每次预测都要算「新点到所有训练点」的距离。训练集一百万条,
预测一条就要算一百万次。这就是为什么工业界很少直接用它,除非先把向量建好索引(近似最近邻)。

三、决策树:一串「是/否」把空间切成方块

决策树一次只问一个问题:「坐标 1 是不是小于 0.85?」问法不是随便挑的, 挑的是「切完之后两边最纯」的那个阈值,纯度的尺子这里用基尼不纯度。 Demo 深度限到 3 层,长出来是这样的(每个叶子标了落进去的点数):

坐标 1 <= 0.85 ?
    是→ 坐标 2 <= 0.93 ?
        是→ 坐标 2 <= 0.67 ?
            是→ 判为第 0 类(109 个点)
            否→ 判为第 0 类(12 个点)
        否→ 坐标 1 <= 0.07 ?
            是→ 判为第 0 类(2 个点)
            否→ 判为第 1 类(16 个点)
    否→ 坐标 2 <= 0.13 ?
        是→ 坐标 1 <= 1.26 ?
            是→ 判为第 0 类(6 个点)
            否→ 判为第 1 类(7 个点)
        否→ 坐标 1 <= 0.86 ?
            是→ 判为第 0 类(1 个点)
            否→ 判为第 1 类(107 个点)

一眼就能看出它在干什么:用横一刀、竖一刀,把平面切成若干个矩形。 这也是它的局限——真正的边界是斜的,它只能用很多个小方块去凑。 深度是它唯一的刹车。把深度放开,看训练集和测试集怎么走:

最大深度 叶子数 训练集准确率 测试集准确率
1 2 0.8769 0.9000
2 4 0.9115 0.8786
3 8 0.9423 0.9214
4 13 0.9538 0.9143
6 25 0.9846 0.8929
8 30 1.0000 0.8786
12 30 1.0000 0.8786
20 30 1.0000 0.8786
训练集从 0.8769 一路爬到 1.0000,测试集反而从 0.9000 掉到 0.8786。这是过拟合最直白的样子:
树越深,它越能把训练集里的每个点都背下来,可它背下来的那部分不是规律,是这批点的运气。
为什么深度 8 之后就不再变?因为 260 个点已经被切到每片叶子都纯了,
再深也没有可切的东西。所以「叶子数停在 30」不是收敛,是数据用完了。
工程上的办法是剪枝:限制深度、限制一个叶子最少几个点、或者先长满再往回砍。
思路都一样——用「叶子太多」当惩罚,换测试集上的稳定。
还有一句得说清:这题最简单的树(只问一个问题)就已经 0.9000 了。两团重叠这么多,
再精巧的结构也换不来多少分。这是数据的上限,不是模型的锅。认不清这一点,就会一直换模型、
一直失望。

四、混淆矩阵:把「对不对」拆成四个格子

准确率只有一个数,但它把两种错当成了一种错。把四格摊开就看清了:

真实\预测 预测第 0 类 预测第 1 类
真实第 0 类 74(对) 5(错)
真实第 1 类 6(错) 55(对)
从四格能算出三个率,它们回答的是三个不同的问题:
指标 算法 回答的问题 Demo 里
准确率 (对的对数) / 总数 整体有多少猜对 0.9214
精确率 真阳 / (真阳 + 假阳) 说它是的时候,有多可信 0.9167
召回率 真阳 / (真阳 + 假阴) 真的里面,抓到了多少 0.9016
F1 两者的调和平均 一个兼顾的折中 0.9091
这三个数经常打架。想少误报,就把标准抬高(精确率升、召回率降);
想少漏报,就把标准压低(召回率升、精确率降)。
它们不是水平不同的同一个东西,是方向不同的三个东西。
准确率最容易骗人的场景是「正例稀少」。Demo 举了个极端例子:
1000 个样本里只有 30 个是真的(3%),一个什么都不看、全判「不是」的模型,准确率 0.9700。
做法 准确率 召回率 有用吗
全判「不是」 0.9700 0.0000 一个真的都没抓到
按 3% 的比例瞎猜 0.9418 0.0300 比全判「不是」还差
0.9700 的准确率听着挺高,可它一个正例都没找出来。而瞎猜那一行的准确率 0.9418 反而更低——
因为乱猜会把本来对的那部分也搞错。
所以正例稀少的时候(欺诈、故障、罕见病),先看召回率,再看准确率。这句话不是技巧,
是常识的翻译:「漏掉一个坏人的代价」和「冤枉一个好人的代价」本来就不该用同一个权重去平均。

五、维度灾难:距离不再像距离

k 近邻全靠「离得近」这三个字。 可维度一高,这三个字会松动。在 d 维单位超立方体里撒 400 个点,量两两距离,看两个比值:

维度 最近/最远 第 10 近/最近 还能不能用
2 0.023 3.845 好用
4 0.108 2.106 好用
8 0.252 1.498 好用
16 0.406 1.269 好用
64 0.657 1.095 勉强
256 0.813 1.041 失效
1024 0.904 1.018 失效
二维的时候,最近的邻居只有最远那个的 2.3%,距离分得很开;
1024 维时这个比值涨到 0.904,最近的邻居和最远的只差 10%。
更狠的是右边那一列:1024 维时,第 10 近的邻居是最近那个的 1.02 倍。
十个邻居几乎一样远——「最近的那个」这句话就没了分量。这叫维度灾难。
为什么会这样,直觉上很好懂。高维空间几乎所有的体积都挤在「壳」上:边长为 1 的立方体,
内切球的体积占比随维度指数级趋近于 0。点全被推到角落里,谁离谁都差不多远。
这一条不是理论洁癖,它直接决定了做法。特征从几十个加到几千个(文本、图像),
k 近邻会慢慢失效,甚至不如「全猜多数类」。这时候要么降维(7.1 的 PCA),
要么换成能自己学表示的模型(第 9 章的神经网络)。
「k 近邻在二维图上讲得通」这句话,在千维特征上未必成立。

六、动手 Demo:有人告诉你答案,怎么学着分

五段:回归与分类的区别、k 近邻扫 k、决策树扫深度、混淆矩阵、维度灾难。 这五段合起来就是有监督学习的完整闭环:给标签、选模型、调旋钮、评分数、 以及知道这套东西什么时候不管用。 代码里 knn 只有五行,build 也就十几行。这两个算法的力量不在复杂度, 在它们把「像不像」和「怎么切」翻译成了可以算的东西。

def knn(Xtr, ytr, Xte, k):         # 没有训练,只有「查邻居」
    d = ((Xte[:, None, :] - Xtr[None, :, :]) ** 2).sum(2)
    idx = np.argsort(d, 1)[:, :k]
    v = ytr[idx].mean(1)           # 邻居里第 1 类的比例
    return (v >= 0.5).astype(int)

def build(X, y, dep, maxd):        # 决策树:每次挑一个「切完最纯」的阈值
    lab = int(y.mean() >= 0.5)
    if dep >= maxd or len(y) < 2 or y.min() == y.max():
        return ('leaf', lab, len(y))
    _, f, t = best_split(X, y)     # best_split 用基尼不纯度扫阈值
    m = X[:, f] <= t
    return ('node', f, t,
            build(X[m], y[m], dep + 1, maxd),
            build(X[~m], y[~m], dep + 1, maxd))

def cm(yt, yp):                    # 混淆矩阵的四个格子
    tp_ = int(((yp == 1) & (yt == 1)).sum())
    fp_ = int(((yp == 1) & (yt == 0)).sum())
    tn_ = int(((yp == 0) & (yt == 0)).sum())
    fn_ = int(((yp == 0) & (yt == 1)).sum())
    return tp_, fp_, tn_, fn_

def dmat(P):                       # 两两距离,用矩阵乘法算,别用三维广播
    s = (P ** 2).sum(1)
    D = s[:, None] + s[None, :] - 2.0 * P.dot(P.T)
    np.maximum(D, 0.0, out=D)
    return np.sqrt(D)

实跑输出:

--------------------------------------------------------------------
有人告诉你答案,怎么学着分
--------------------------------------------------------------------
==============================================================

[1] 有标签和没标签,差在哪儿

  7.1 那批点只有坐标,没有答案,所以算法只能告诉你「哪几个像一家」。
  **有监督学习多了一样东西:每个样本都带一个标签。**标签有两种样子:

    标签长什么样            任务叫法        例子
    ------------------------------------------------------------------
    一个类别(猫/狗)      分类            这封邮件是不是垃圾邮件
    一个数(3.7 万)        回归            这套房子明年卖多少钱

  两种都能用最小二乘的骨架先摸一遍。造 80 个点,真关系是 y = 2.4x − 1:

    拟合出来:y = 2.326 x -0.979,R² = 0.9332
    真关系是:y = 2.400 x -1.000,噪声标准差 0.350
  **回归输出一个数,好坏用「差多少」量(R²、均方误差)。**
  **分类输出一个类别,好坏用「对几个」量(准确率)。**量法不一样,这是两件事。

  分类这边造 400 个点:两团各在一个位置,但**故意让它们重叠**——
  两团中心隔 1.87,各自标准差 0.62,中间那一小块谁也不比谁更像。
  260 个当训练集(带标签),140 个当测试集(**标签先藏起来**)。

[2] k 近邻:不训练,考试的时候现查邻居

  它没有「训练」这一步,模型就是训练集本身。来一个新点,就找最近的 k 个
  训练点,**少数服从多数**。k 取多少,边界的光滑程度差很多:

    k     训练集准确率      测试集准确率      训练集上的样子
    --------------------------------------------------------------------------
    1     1.0000            0.9143            ####################
    3     0.9462            0.9071            ###################.
    5     0.9308            0.9143            ###################.
    9     0.9308            0.9429            ###################.
    15    0.9308            0.9429            ###################.
    25    0.9231            0.9357            ##################..
    45    0.9269            0.9429            ###################.
    75    0.9269            0.9429            ###################.

  **k=1 在训练集上是 1.0000,在测试集上只有 0.9143。**这就是过拟合的样子:
  它把每个训练点都当成自己家门口,边界绕着噪声走,换个点来问就答错。
  k 加到 9 时测试集最高(0.9429),但**别把这个「最高」太当真**:
  测试集只有 140 个点,一个点就是 0.0071,表里 9 到 75 那几行差两三个点,
  换算下来就是一两次运气。**k 大了偏差大、k 小了方差大,这才是稳的结论。**
  **另外,k 近邻的「训练集准确率」几乎没用**——k=1 时它按定义就等于 1.0000。

[3] 决策树:一串「是/否」把空间切成方块

  它一次只问一个问题,问法由「切完之后两边有多纯」决定,这把尺子叫基尼不纯度。
  深度限到 3 层,长出来是这样:

    坐标 1 <= 0.85 ?
        是→ 坐标 2 <= 0.93 ?
            是→ 坐标 2 <= 0.67 ?
                是→ 判为第 0 类(109 个点)
                否→ 判为第 0 类(12 个点)
            否→ 坐标 1 <= 0.07 ?
                是→ 判为第 0 类(2 个点)
                否→ 判为第 1 类(16 个点)
        否→ 坐标 2 <= 0.13 ?
            是→ 坐标 1 <= 1.26 ?
                是→ 判为第 0 类(6 个点)
                否→ 判为第 1 类(7 个点)
            否→ 坐标 1 <= 0.86 ?
                是→ 判为第 0 类(1 个点)
                否→ 判为第 1 类(107 个点)

    最大深度    叶子数    训练集准确率      测试集准确率
    --------------------------------------------------------------
    1           2         0.8769            0.9000
    2           4         0.9115            0.8786
    3           8         0.9423            0.9214
    4           13        0.9538            0.9143
    6           25        0.9846            0.8929
    8           30        1.0000            0.8786
    12          30        1.0000            0.8786
    20          30        1.0000            0.8786

  **训练集准确率从 0.8769 一路爬到 1.0000,测试集反而从 0.9000 掉到 0.8786。**
  这是过拟合最直白的样子:树越深,它越能把训练集里的每个点都背下来,
  **可它背下来的那部分不是规律,是这批点的运气。**
  工程上的办法是**剪枝**:限制深度、限制一个叶子最少几个点、或者长完再往回砍。
  顺便说一句,这题最简单的树(只问一个问题)就已经 0.9000 了——
  **两团重叠这么多,再精巧的结构也换不来多少分。这是数据的上限,不是模型的锅。**

[4] 光看准确率会骗人:混淆矩阵

  拿 3 层那棵树在测试集 140 个点上打分,把四种结果数清楚:

                    预测第 0 类     预测第 1 类
    ------------------------------------------------
    真实第 0 类     74(对)        5(错)
    真实第 1 类     6(错)         55(对)

    准确率    0.9214      (对的比例,129 / 140)
    精确率    0.9167      (判成第 1 类的里,真有多少是)
    召回率    0.9016      (真正第 1 类的里面,找出来多少)
    F1        0.9091      (精确率和召回率的调和平均)

  **准确率高不代表有用,因为四种错误的分量不一样。**举个极端的例子:

  1000 个样本里只有 30 个是真的(3%)。如果一个模型**什么都不看,全判「不是」**:

    做法                  准确率      召回率      有用吗
    ----------------------------------------------------------------
    全判「不是」          0.9700      0.0000      一个真的都没抓到
    按 3% 的比例瞎猜      0.9418      0.0300      比全判「不是」还差

  **0.9700 的准确率听着挺高,可它一个正例都没找出来。**
  瞎猜那一行的准确率 0.9418 还更低——**乱猜会把本来对的那部分也搞错。**
  所以正例稀少的时候(欺诈、故障、罕见病),**先看召回率,再看准确率。**

[5] 维度一高,距离就不像距离了

  k 近邻全靠「离得近」这三个字。可维度一上去,这个前提会松动:
  在 d 维单位超立方体里撒 400 个点,量两两距离,看两个比值:

    维度    最近/最远      第 10 近/最近    还能不能用
    ------------------------------------------------------------------
    2       0.023           3.845             好用
    4       0.108           2.106             好用
    8       0.252           1.498             好用
    16      0.406           1.269             好用
    64      0.657           1.095             勉强
    256     0.813           1.041             失效
    1024    0.904           1.018             失效

  **1024 维时,最近那个邻居和最远那个只差 10%——**
  而第 10 近的邻居是最近那个的 1.02 倍。**十个邻居几乎一样远,**
  「最近的那个」这句话就没了分量。这叫维度灾难。
  **降维不是为了画图好看,是把 k 近邻这类方法从失效拉回能用的前提。**
  **k 近邻在二维图上讲得通,在千维特征上就未必讲得通了。**

  这一节走下来,落到一句话:
  **有标签只说明你能开始学;学得好不好,得另找一批没见过的点来量。**

第二段那张表最该盯的是第一行。k=1 的训练集 1.0000、测试集 0.9143, 这个 1.0000 是「邻居就是自己」的必然结果,不是模型好。 凡是「训练集分数高得离谱」的模型,第一个要怀疑的就是这个。 第三段里训练集一路涨到 1.0000、测试集一路掉到 0.8786,这是过拟合的教科书图景。 注意中间 3 到 4 那一档:训练集只涨了 0.01,测试集掉了 0.007。 过拟合不是「突然坏掉」,是一点点换来的。 第四段那个 0.9700 是整节最值得记住的数。它提醒你: 准确率是「平均」,而平均会把少数类的痛苦摊平。30 个正例全错,在 1000 个样本里只值 3 个百分点, 看上去像「还不错」。这就是为什么罕见病的筛查模型、欺诈检测模型,从来不用准确率当主指标。 第五段那两个比值是「维度灾难」最干净的量化。2 维时最近/最远是 0.023,1024 维时是 0.904。 这不是算法变笨了,是「近」这个字在高维里失去了区分度。 任何基于距离的方法(k 近邻、k-means、RBF 核)都会撞上这堵墙。 最后说一句这段代码没做的事:它没有交叉验证,也没调超参。 真实项目里 k 和深度不能直接在测试集上挑—— 挑完了,测试集就被用过了。正确做法是从训练集里再切一块「验证集」,或者做 k 折交叉验证。 Demo 为了看清趋势省掉了这一层,但结论不能省。

30 秒实验

  1. 把两团中心的距离从 (1.55, 1.05) 改成 (2.60, 1.80),重跑。 k=1 和深度 8 的测试集都会跳到 0.97 以上——因为重叠少了,边界好画了。 这正好说明「模型分数」里有一大半是数据给的,不是模型挣的。
  2. 把 build 里的 maxd 那一路放开到 30,并去掉「叶子最少两个点」的限制,重跑第三段。 训练集会精确到 1.0000,测试集会掉到 0.85 附近——这是把过拟合推到极限的样子。
  3. 把第 5 段的维度从 1024 改成 16 和 64 各跑一次。 64 维时「第 10 近/最近」还有 1.095,16 维是 1.269。 这个下降速度就是「多少维还算低维」的答案,它取决于样本量,不是固定的 10 或 100。

常见误区

  1. 用训练集准确率当成绩。k 近邻 k=1 时训练集恒为 1.0000,决策树长满也是 1.0000。 训练集分数只说明「背下来了没有」,不说明「学会了没有」。
  2. 反复用测试集调参。每挑一次超参就看一眼测试集, 测试集就悄悄变成了训练集。规范做法是从训练集里再切验证集,或者做交叉验证, 测试集只在最后用一次。
  3. 在正例稀少的数据上看准确率。Demo 里「全判不是」的准确率 0.9700,召回率 0。 准确率是平均值,它会把少数类的错误摊平。先看召回率,再看精确率,最后才看准确率。
  4. 以为特征越多越好。维度一高,距离失去区分度(1024 维时最近和最远只差 10%), 基于距离的方法会先崩。特征不是免费的,加之前先想清楚它有没有信息量。
自测三问(点开对答案) 1. 为什么必须留一个模型「没见过」的测试集? —— 因为**训练集上的分数衡量的是「记住」,不是「学会」。**Decision tree 长满时训练集 1.0000, 测试集只有 0.8786,**这中间的差就是过拟合的量。**而且**测试集只能用一次**:反复拿它调参数, 它就变成了训练集的一部分,分数会虚高。**要调超参,得再切一块验证集出来。** 2. 召回率和精确率,什么时候该先看哪个? —— **看漏掉和误报哪个更贵。**漏掉一个癌症病灶很贵,就先看召回率(宁可多报,别漏); 误报一次会让人白做一次穿刺,就得把精确率抬上去。 **Demo 里「全判不是」的准确率 0.9700、召回率 0**, 正是「只看准确率会放过一个废模型」的极端例子。**两个率是一对,不能只提一个。** 3. 维度灾难到底坏了什么?为什么它会让 k 近邻失效? —— **坏的是「近」这个字的区分度。**二维时最近的邻居只有最远那个的 2.3%, **1024 维时这个比值是 0.904——谁离谁都差不多远。** k 近邻的整个逻辑建立在「近的那些更有参考价值」上,**这个前提一塌,投票就没意义了。** **同理,k-means(用距离)、RBF 核(用距离)都会撞上同一堵墙。**

到这里,第 7 章的两条路都走完了:没有答案怎么找结构,有答案怎么学规律。 下一章换一个更难的问题:没有标准答案,只有「做了之后拿到的分数」。那就是强化学习; 而在那之前,先看怎么把很多个弱模型拼成一个强的——集成学习。

相关笔记