把答案加回去,问题立刻变得可算:猜对了就是对了,猜错了就是错了。但代价跟着来—— 你得先有一批标好的数据,还得留出一部分自己不看,专门用来考试。 这一节的四件事:怎么用邻居投票(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.55, 1.05) 改成 (2.60, 1.80),重跑。 k=1 和深度 8 的测试集都会跳到 0.97 以上——因为重叠少了,边界好画了。 这正好说明「模型分数」里有一大半是数据给的,不是模型挣的。
- 把
build里的maxd那一路放开到 30,并去掉「叶子最少两个点」的限制,重跑第三段。 训练集会精确到 1.0000,测试集会掉到 0.85 附近——这是把过拟合推到极限的样子。 - 把第 5 段的维度从 1024 改成 16 和 64 各跑一次。 64 维时「第 10 近/最近」还有 1.095,16 维是 1.269。 这个下降速度就是「多少维还算低维」的答案,它取决于样本量,不是固定的 10 或 100。
常见误区
- 用训练集准确率当成绩。k 近邻 k=1 时训练集恒为 1.0000,决策树长满也是 1.0000。 训练集分数只说明「背下来了没有」,不说明「学会了没有」。
- 反复用测试集调参。每挑一次超参就看一眼测试集, 测试集就悄悄变成了训练集。规范做法是从训练集里再切验证集,或者做交叉验证, 测试集只在最后用一次。
- 在正例稀少的数据上看准确率。Demo 里「全判不是」的准确率 0.9700,召回率 0。 准确率是平均值,它会把少数类的错误摊平。先看召回率,再看精确率,最后才看准确率。
- 以为特征越多越好。维度一高,距离失去区分度(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 章的两条路都走完了:没有答案怎么找结构,有答案怎么学规律。 下一章换一个更难的问题:没有标准答案,只有「做了之后拿到的分数」。那就是强化学习; 而在那之前,先看怎么把很多个弱模型拼成一个强的——集成学习。
- ☐ (本章完,下一节 8.1 集成学习)