第 6 章那一套都要「先假设一个分布,再用数据调它」。 这一节把假设拆掉:连答案都没有,只有一堆坐标。 能问的只剩三件事: 哪些点像一家(聚类)、能不能用更少的数描述它们(降维)、哪些地方常见(密度估计)。代价是—— 没有标准答案,也就没有干净的分数。
这一节是第 7 章的前半,也是全书第一次「没有标签」地面对数据。前面 6 章要么在算一个已知量, 要么在用标签校正;从这里开始,算法要自己在数据里找出结构。 它跟第 5 章有直接血缘:k-means 是 5.2 那种「反复挪一步、直到不再变好」的局部搜索, 层次聚类是 5.1 那个「图」上的合并过程。 PCA 用的特征值分解,是第 4 章矩阵那套东西的直接后果。
一、没有答案时,能问的三个问题
有监督学习的输入是「数据 + 答案」,无监督的输入只有数据。少了答案,问题反而分成三种, 因为「结构」这个词可以指三样东西:
| 问什么 | 要找的结构 | 典型方法 | 结果长什么样 |
|---|---|---|---|
| 哪些点像一家 | 分组 | k-means、层次聚类 | 每个点一个簇号 |
| 能不能少用几个数 | 低维表示 | PCA、t-SNE | 每个点一组新坐标 |
| 哪些地方常见 | 密度形状 | 核密度、混合模型 | 一条密度曲线 |
| 这三种问法经常连着用。比如先把一千维的图压到二维(降维),再在二维上分组(聚类), | |||
| 最后看每组落在密度图的哪个峰上。PCA 那一段末尾会看到,降维常常是聚类能不能工作的前提。 | |||
| 最麻烦的地方在评价。有监督时错了就是错了, | |||
| 无监督时——你怎么知道这堆分法比那堆分法好?没有标签当裁判,就只能用「簇内紧不紧、 | |||
| 簇间开不开」这类内部指标,而这些指标衡量的只是「分组干不干净」,不是「分组有没有意义」。 | |||
| 这句话是整节的地基:算法能给你一个划分,给不了你一个意义。意义的判断只能来自这批数据之外—— | |||
| 业务上说得通吗?换个时间段还稳定吗?拿去做下游任务有没有变好?这三问,代码答不了。 |
二、k-means:把「像」定义成「离得近」
想法简单到几乎没有余地:先随便挑 k 个点当中心,然后重复两件事——每个点归给最近的中心; 每个中心挪到自己那一堆点的平均位置。挪到中心不再动,就停。 它一定停得下来,因为每一步都不会让「簇内平方和」变大。归队这一步,每个点都选了更近的中心, 平方和只会小;挪中心这一步,均值是一个簇里「平方和最小」的那个位置,所以也只会小。 一个单调下降又有下界的量,只能收敛。 但它收敛到哪儿,跟它从哪儿出发有关。Demo 拿十个随机种子在 150 个点上各跑一遍:
| 跑出来的簇内平方和 | 出现次数 | 跟真实三组的一致率 |
|---|---|---|
| 28.818 | 6 次 | 1.0000 |
| 227.874 | 1 次 | 0.7333 |
| 265.200 | 2 次 | 0.6667 |
| 272.740 | 1 次 | 0.7333 |
| 同样数据、同样算法,只是初值不同,最好和最差差了近十倍。差的那种是怎么来的? | ||
| 两个初值中心同时落在同一团里,于是一个真实的团被硬拆成两半, | ||
| 另一个团和它的一半合伙——一旦错到这一步,后面的「挪中心」只会把错误固化,不会自己跳出来。 | ||
| 这叫局部最优。k-means 保证的是「不再变好」,不是「最好」。 | ||
| 工程上的补救很朴素: | ||
| 多跑几个初值,挑簇内平方和最小的那个。Demo 里十次有六次落在最好的 28.818 上, | ||
| 所以跑十次基本稳。 | ||
| 还有一层更根本的东西:k-means 用欧氏距离和均值,等于默认「簇是圆滚滚、大小差不多、 | ||
| 各方向散得差不多」的球。数据要是一条弯月、一个长条、或者一大一小两个团, | ||
| 它必然切错——不是初值的问题,是模型的样子跟数据的样子不合。 |
三、k 该取几:肘部是个手势,不是判据
k 越大,簇内平方和一定越小。 极端情况每个点自己一簇,平方和就是 0。所以「挑最小的」等于挑「k 等于点数」,毫无意义。 能看的是再增加一簇还能降多少:
| k | 簇内平方和 | 比上一档降了多少 |
|---|---|---|
| 1 | 491.299 | — |
| 2 | 232.853 | 258.447 |
| 3 | 28.818 | 204.035 |
| 4 | 23.839 | 4.979 |
| 5 | 21.379 | 2.460 |
| 6 | 18.170 | 3.209 |
| 从 2 到 3 降了 204,从 3 到 4 只降了 5。这个断崖看着就像胳膊肘,所以叫肘部法。 | ||
| 它确实指向了 3,而这批数据本来就是三团。 | ||
| 但别把它当判据。它是「一条曲线拐弯的地方」,拐弯的陡缓取决于数据的尺度、点的密度、 | ||
| 甚至随机种子。 | ||
| 换一批数据,同一个算法给出的肘部可能根本不存在——曲线一路平滑地下去,你得自己决定在哪儿停。 | ||
| 更根本的一句:k 不是数据里的真相,是你要的分辨率。一所学校按年级分是 6 类,按班级分是 30 类, | ||
| 两个都对。问「到底该分几类」之前,先问「分出来要干什么」。 |
四、层次聚类:一次给出所有层级的切法
另一种思路是自底向上:一开始每个点自己一簇,然后反复把最近的两簇合起来,一路合到只剩一簇。 整个过程记下来就是一棵树,你想要几类,就在树上切一刀。 关键在于「两簇的距离怎么算」。两种常见定义,性格完全相反:
| 链接方式 | 两簇距离 = | 性格 |
|---|---|---|
| 单链接 | 两簇之间最近的一对点 | 顺着链子爬,容易串成一长条 |
| 全链接 | 两簇之间最远的一对点 | 要整块靠近才合,偏好紧凑的团 |
| Demo 造了三组点:一条横着摊开的长条(30 个)+ 两个紧凑团(各 30 个)。先量距离, | ||
| 这是两种链接各自要看的数: |
| 两组之间 | 最近距离 | 最远距离 |
|---|---|---|
| 长条 与 右边那个团 | 0.323 | 3.405 |
| 长条 与 上面那个团 | 2.318 | 3.441 |
| 两个团之间 | 1.993 | 3.290 |
| 长条自己内部 | — | 2.920 |
| 链接方式 | 三簇的大小 | 跟真实三组的一致率 |
|---|---|---|
| 单链 | 30/30/30 | 1.0000 |
| 全链 | 40/20/30 | 0.8889 |
| 结果跟教科书给的预期反了:这批数据上,单链全对,全链反而切错。原因就在上面那张距离表里。 | ||
| 单链接看「最近」那一对:0.323 这道缝,比长条内部相邻点的间距大得多, | ||
| 所以它一路都在长条内部下手,把长条合成一整块就停了,正好停在三个簇上。 | ||
| 教科书警告的链式效应没发作——因为两团之间没有一条点挨点的细链,那道缝还不够小。 | ||
| 全链接看「最远」那一对:长条跟右边那个团最远 3.405,比长条自己内部最远的 2.920 还大。 | ||
| 于是它宁愿从长条中间切一刀:右边那 10 个点并进了右边那个团,剩下 20 个自成一簇。 | ||
| 这一刀切掉了 10 个点的正确归属,一致率从 1.0000 掉到 0.8889。 | ||
| 所以「哪种链接更好」没有答案。你怕的是「两团被一条链串成一坨」(那就用全链), | ||
| 还是怕「一个大团被切成两半」(那就用单链)。先说清怕什么,再选链接方式。 | ||
| 层次聚类还有个实际的好处:它一次把所有 k 都算出来了。代价是慢——每合并一次都要重算两两距离, | ||
| 朴素实现是立方级,几万个点就跑不动了,这是它只在中小数据上常见的原因。 |
五、PCA:换一副坐标轴
降维的想法可以一句话说清: 原来的坐标轴是随手选的(比如「横坐标」),换一副轴,让数据在新轴上散得最开。 「散得最开」就是方差最大。第一个新轴让投影后的方差最大, 第二个轴在与第一个垂直的前提下方差最大……一直算下去,就得到一串互相垂直的轴。 这些轴正好是协方差矩阵的特征向量,轴上的方差就是对应的特征值。这是线代和统计的接口, 记住结论就够用:求主轴,就是求特征向量。 Demo 拿一批二维数据(两个变量高度相关,大致沿 45 度方向排开)算了一遍:
| 量 | 算出来 |
|---|---|
| 协方差矩阵 | [[0.850, 0.810], [0.810, 0.943]] |
| 两个特征值 | 1.7075 和 0.0857 |
| 第一主方向跟横轴的夹角 | 46.7 度 |
| 第一主成分解释的方差比例 | 0.9522 |
| 投影后的方差 | 1.6989 |
| 注意 46.7 度这个数:两个变量相关系数约 0.91,主方向就落在 45 度附近。这不是巧合—— | |
| 相关性越强,数据越像躺在一根斜线上,那根斜线就是第一主方向。 | |
| 投影之后只用一列数,方差 1.6989;原来两个变量各自的方差是 0.8456 和 0.9386。 | |
| 一个数顶两个数,还保住了 95.2% 的波动。这就是降维的全部好处。 | |
| 但它也扔掉了一点东西:8.57% 的方差被当成噪声丢了。如果那点方向上恰好有你要的信号,就一起丢了。 | |
| PCA 挑的是「方差大」,不是「有用」——这两个词经常不是一回事。 | |
| (一根方差极小但和标签完全对应的轴,PCA 会毫不犹豫地丢掉它。) | |
| 还有两个必须交代的前提。第一,它只做线性变换,数据躺在一条弯线上时,一条直线概括不了, | |
| 得换核方法。 | |
| 第二,量纲不同必须先标准化:把「身高(厘米)」和「收入(元)」放一起, | |
| 第一主方向会被收入一个变量独占——因为它的数字大,不是因为它重要。 |
六、动手 Demo:没有标签,怎么把它们分开
五段:散点图、k-means 的初值敏感、肘部、层次聚类的两种链接、PCA。
每段都在回答同一个问题的不同侧面:没有答案的时候,怎么判断「分对了」。
注意 kmeans 里那句 if np.allclose(new, C): break——停下来的条件是「中心不再动」,
不是「结果最好」。这一句就是局部最优的来源。
def kmeans(X, k, seed, iters=30):
r2 = np.random.default_rng(seed)
C = X[r2.choice(len(X), k, replace=False)].copy() # 初值:随便挑 k 个点
for _ in range(iters):
d = ((X[:, None, :] - C[None, :, :]) ** 2).sum(2)
lab = d.argmin(1) # 1. 每个点归给最近的中心
new = np.array([X[lab == j].mean(0) for j in range(k)])
if np.allclose(new, C): # 2. 中心不再动就停
break
C = new # 3. 中心挪到自家点的平均位置
inertia = float(((X - C[lab]) ** 2).sum()) # 簇内平方和
return C, lab, inertia
def agglom(X, mode, k=3): # 层次聚类:每次合并「距离最近的两簇」
cls = [[i] for i in range(len(X))]
while len(cls) > k:
best = None
for i in range(len(cls)):
for j in range(i + 1, len(cls)):
dd = dist(X[cls[i]], X[cls[j]], mode) # 单链取最近,全链取最远
if best is None or dd < best[0]:
best = (dd, i, j)
_, i, j = best
cls[i] = cls[i] + cls[j]
cls.pop(j)
lab = np.zeros(len(X), dtype=int) # 把簇编号摊回每个点
for t, idxs in enumerate(cls):
lab[idxs] = t
return lab
# PCA:协方差矩阵的特征向量就是「新的坐标轴」,特征值就是「轴上的方差」
cov = np.cov(Zc.T)
w, V = np.linalg.eigh(cov)
order = np.argsort(-w)
w, V = w[order], V[:, order]
share = w[0] / w.sum() # 第一主成分解释的方差比例
实跑输出:
--------------------------------------------------------------------
没有标签,怎么把它们分开
--------------------------------------------------------------------
==============================================================
[1] 先看数据:三团点,但没有答案
150 个点,其实是三团(左下一团 60 个、右边一团 50 个、上边一团 40 个)。
**可如果没人告诉你这一点,你手上只有 150 个坐标。**
| |
| ooo o |
| oooo oo o o |
| o ooooooooo |
| o oooo |
| |
| |
| |
| o o |
| o o oooo o |
| o oooooo oo ooooooooo o |
| o o oooooooo o ooooooo |
| o oooo o oo o o o oo |
| o oooo o o |
**同一批点,三种算法会给出三种不同的「分法」。**这一节就是看它们各自凭什么分。
[2] k-means:先猜 k 个中心,再反复「归队、挪中心」
想法很朴素:**先随便挑 k 个点当中心,然后重复两件事——**
**每个点归给最近的中心;每个中心挪到自己那一堆点的平均位置。**
挪到中心不再动为止。k=3,先拿十个随机种子各跑一遍:
十个种子跑出来的结果,一共 4 种:
簇内平方和 28.818——出现 6 次,一致率 1.0000
簇内平方和 227.874——出现 1 次,一致率 0.7333
簇内平方和 265.200——出现 2 次,一致率 0.6667
簇内平方和 272.740——出现 1 次,一致率 0.7333
**同样的数据、同样的算法,只是初值不同,结果差了近十倍。**
**这就是初值敏感:k-means 只保证「不再变好」,不保证「最好」。**
它停在哪儿,取决于它从哪儿出发。
工程上的补救很简单:**多跑几次、挑簇内平方和最小的那个。**
这题十次里有 6 次落在最好的 28.818 上,所以跑十次基本稳。
**但也别以为这是 k-means 的固有毛病。**如果三团分得再开一点,十次可能全都落到同一个答案。
**「跑几次才稳」取决于数据,不取决于算法。**
[3] k 该取几:看「肘部」
k 越大,簇内平方和一定越小(极端情况每个点自己一簇,就是 0)。
所以不能挑最小的,要看「再增加一簇还能降多少」:
k 簇内平方和 比上一档降了 形状
----------------------------------------------------------
1 491.299 — ######################################
2 232.853 258.447 ##################
3 28.818 204.035 ##
4 23.839 4.979 ##
5 21.379 2.460 ##
6 18.170 3.209 #
**从 1 到 3 降得很快,从 3 到 4 之后每档只降一点点。**那个拐弯的地方就是「肘部」。
**注意这只是个经验办法,没有数学保证。**数据本身没有「正确的 k」——
**k 是你要的分辨率,不是数据里的真相。**
[4] 层次聚类:一步步合并,看「先合谁」
另一种思路:**一开始每个点自己一簇,然后反复把「最近的两簇」合起来。**
「最近」怎么定义,结果会差很远。两种常见定义:
定义 两簇距离 = 倾向
------------------------------------------------------------
单链接 最近的一对点 顺着链子爬,容易串成一长条
全链接 最远的一对点 要整块靠近才合,偏好紧凑的团
造三组点:**一条横着摊开的长条(30 个)+ 两个紧凑团(各 30 个)。**
先把三组之间的最近、最远距离量出来,这正是两种链接各自要看的数:
两组之间 最近距离 最远距离
--------------------------------------------------------
长条 与 右边那个团 0.323 3.405
长条 与 上面那个团 2.318 3.441
两个团之间 1.993 3.290
长条自己内部 — 2.920
长条右端离右边那个团最近的一对只隔 0.323,最容易出事的地方就在这儿。
链接方式 三簇的大小 跟真实三组的一致率
--------------------------------------------------------------
单链 30/30/30 1.0000
全链 40/20/30 0.8889
**单链接看的是「最近」那一对:0.323 这道缝比长条内部相邻点的间距大得多。**
所以它动手的顺序全在长条内部,把长条自己合成一整块就停了,正好认出三组。
**全链接看的是「最远」那一对:长条跟右边那个团最远 3.405。**
这比长条自己内部最远的 2.920 还大——**它宁愿从长条中间切一刀。**
**全链那一刀切在 x 约 2.0:长条右边那 10 个点被并进了右边那个团。**
长条剩下的 20 个自成一簇,**10 个点因此判错,一致率掉到 0.8889。**
**单链的经典毛病叫链式效应:两团之间只要有一条点挨点的细链,它就会顺着链子串成一坨。**
这批数据里它没发作,纯粹因为 0.323 那道缝还不够小。
**所以「哪种链接更好」没有答案,得看你怕的是「串成一长条」还是「把大团切开」。**
[5] PCA:换一副坐标轴,把「散得最开」的方向找出来
一批二维数据,两个变量高度相关(大致沿 45 度方向排开)。
**PCA 做的事是:找一个新的坐标轴方向,让投影上去之后方差最大。**
协方差矩阵:[[0.850, 0.810], [0.810, 0.943]]
两个特征值:1.7075 和 0.0857
第一主方向跟横轴的夹角:46.7 度
第一主成分解释的方差比例:**0.9522**
把 200 个点全投到第一主方向上,只剩一列数。
**这一列数的方差 1.6989,而原始两个变量各自的方差是 0.8456 和 0.9386。**
**用一个数代替两个数,还保住了 95.2% 的波动信息**——这就是降维的意思。
**但它也扔掉了一点东西。**剩下的 4.8% 被当成噪声丢掉了。
**要是那点方向上恰好有你要的信号,就一起丢了。**
**PCA 挑的是「方差大」,不是「有用」。**这两个词经常不是一回事。
第二段那张表是这一节最该记住的数。四种结果、簇内平方和差近十倍, 而它们来自同一个算法、同一批数据。换初值就换答案, 所以「跑一次 k-means 就交差」在真实项目里是很危险的习惯。 第三段那张肘部表里,2 到 3 降 204、3 到 4 只降 5。这个断崖之所以这么干净, 是因为这批数据本来就是三团、而且三团分得开。 真实数据的肘部经常是一条平滑的斜坡,看不出拐点——那时候你要靠业务上「分几类有用」来决定, 不是靠这条曲线。 第四段的结果跟很多教材的说法相反,值得多看一眼。教材说单链容易串成长条、全链更稳; 这批数据上单链全对、全链切错。 原因不在算法,在数据:这里没有那条「点挨点的细链」,而长条本身又特别长(内部最远 2.920)。 这就是为什么算法选择要看数据形状,不能背结论。 第五段那个 0.9522 和 8.57% 要一起看。「保住了 95%」听着很好, 但剩下的 5% 里有没有你要的东西,PCA 不知道。它只保证「丢的是方差小的方向」, 不保证「丢的是不重要的方向」。降维之后一定要回头验:拿降维后的数据做原来的任务,看效果掉没掉。
30 秒实验
- 把
kmeans里挑初值那一行改成「取前 k 个点」而不是随机挑,重跑第二段。 十个种子会变成同一个结果——因为初值不再随机, 这正好反证了「结果差近十倍」是初值造成的,不是数据造成的。 - 把第 4 段的长条从
uniform(0.0, 3.0)改成uniform(0.0, 4.5),重跑。 长条更长,全链会更狠地把它切开,而单链那道 0.323 的缝会变大,两边的一致率都会变。 看哪种链接先崩,比背结论有用。 - 把 PCA 那段的数据乘上不同尺度(第一列乘 10),重跑。 主方向会从 46.7 度明显偏向横轴——因为方差被尺度放大了。 这就是「先标准化」这条规矩的由来。
常见误区
- 以为无监督的结果「客观」。算法只保证「按这个目标函数最优」, 目标函数是你选的,k 是你给的,初值还是随机的。换个种子就可能换个划分,哪有什么客观。
- 以为簇内平方和越小越好。它随 k 单调下降,k 等于点数时是 0。 该看的是「降幅」而不是「绝对值」,而肘部本身也只是个手势。
- 以为单链一定串、全链一定稳。Demo 里正好反过来。 链接方式的优劣取决于数据的形状:有没有细链、团有多长、两团之间那道缝多宽。
- 以为 PCA 挑出来的就是「重要特征」。它挑的是方差大。 一个方差极小但和标签完全对应的方向,PCA 会当噪声丢掉。想做「有监督的降维」, 得用 LDA 或者直接用模型自己学表示。
自测三问(点开对答案)
1. k-means 为什么一定收敛,却不一定收敛到最好的结果? —— 因为**每一步都不会让簇内平方和变大**(归队选更近的中心、挪中心取均值), 一个单调下降又有下界的量必然收敛。 **但它只往下走、不往上跳,**一旦两个初值中心落在同一团里,那个错误划分就成了「局部最低点」, **后面的迭代只会把错误固化。**Demo 里同样的数据跑出四种结果, 簇内平方和从 28.818 到 272.740,**差近十倍。** 2. 为什么层次聚类能在同一棵树上拿到任意 k 的结果,k-means 不行? —— 因为**层次聚类算的是「合并的顺序」,这个顺序与 k 无关**——它一路从 n 簇合到 1 簇, 整个过程记录成一棵树,**你想要几类就在树上切一刀。**k-means 相反,**k 是输入**, 换一个 k 就得从头再跑一遍,而且不同 k 的结果之间没有包含关系。 **代价是层次聚类慢**:朴素实现每合并一次都要重算两两距离,立方级。 3. PCA 保住了 95% 的方差,为什么还可能把关键信息丢掉? —— 因为**「方差大」和「有用」是两件事**。 PCA 只看数据的二阶统计量,**它不知道你要预测什么。**一个方差极小、但和标签完全对应的方向, 在它眼里就是噪声。**所以降维之后必须回头验**:拿降维后的数据做原来的任务,看效果掉没掉; **掉了就说明你丢掉的那 5% 里有东西。**这一节全程没有答案,所以最后只能靠「内部指标 + 人来解读」。 下一节把答案加回去:每个样本都带标签,于是「对不对」变成一个能算的数。 代价是你得先有一批标好的数据,还得留出一部分不许看。
- ☐ (本节完,下一节 7.2 有监督学习)