🧮 第3章 无监督学习
📄 一句话概括

LSA 就是把词频矩阵做一次奇异值分解,取前几个奇异向量当「主题」。 Demo 用 12 个词、6 篇小文档试了一次: 没人告诉算法哪几篇在讲开会、哪几篇在讲促销,它自己从共现模式里分出了两路, 六篇文档的归属和人工标注完全一致。 代价是压到 2 维相对误差 36.5%。

📝 本节和前面几节的关系

这一节和上一节是同一件事的两个名字。 PCA 输入的是中心化后的数据矩阵,LSA 输入的是词频矩阵,两者都是奇异值分解。 所以有人说「LSA 就是文本上的 PCA」,这话基本没错。 区别在于:文本矩阵是非负的、稀疏的、量纲统一的, 这些性质让奇异值分解在它身上有了「主题」这个解释。 第 4 章的「文本数据的转换处理」会接着讲怎么把一段文字变成这张表。

一、先把文字变成一张表

算法不认识字。要让算法处理文本,第一步永远是把文本变成数字矩阵。 最朴素的办法叫词袋模型:先收集所有出现过的词做成词典,然后每一篇文档数一遍这些词出现了几次, 填成一行。 Demo 里就是这么干的:12 个词的词典、6 篇文档,得到一个 12 行 6 列的表。 表里第 i 行第 j 列的数,就是「第 i 个词在第 j 篇文档里出现了几次」。 这一步扔掉了语序。「狗咬人」和「人咬狗」在词袋里完全一样。 对主题分类这种任务,扔掉语序损失不大;但对情感分析、问答这类任务就是灾难。 这是词袋模型的天花板,也是后来词向量、Transformer 出现的动机。 还有个细节:要不要用词频本身? 「的」「是」这种词每篇都出现几十次, 按词频算它们会主导整个矩阵。 所以工程上常用 TF-IDF 压一下:一个词在很多文档里都出现,说明它没有区分度,权重就该降下来。 这一节为了看清原理,只用了原始词频。

二、奇异值分解:比特征值分解更通用

上一节的 PCA 用的是特征值分解,它只能用在方阵(协方差矩阵)上。 而词频矩阵是 12 行 6 列的长方形,特征值分解用不了。 这时要用奇异值分解(SVD):任何一个 m 行 n 列的矩阵 A,都能写成三个矩阵相乘——A = U · S · Vᵀ。 U 是 m 行 k 列(词的方向),S 是对角阵(每个方向有多重要),Vᵀ 是 k 行 n 列(文档的方向), k 等于矩阵的秩。 S 对角线上那些数叫奇异值,它们就是「每个主题有多重」。 奇异值按从大到小排,第一个永远最大。 这和 PCA 的特征值是同一个东西——事实上奇异值的平方就是 AᵀA 的特征值。 只保留前几个奇异值,就得到原矩阵的一个低秩近似。 而且这个近似在所有同秩矩阵里误差最小(Eckart–Young 定理)。 这句话是 LSA 能成立的数学依据:你要的「只要两个主题」,正好就是最优的秩 2 近似。

三、两个主题是怎么自己冒出来的

Demo 的六篇文档是这样写的:前三篇讲开会报销(预算、会议、审批、项目、报告、纪要), 后三篇讲促销中奖(优惠、券、抢购、点击、领取、中奖)。 两组词几乎不交叉,但算法完全不知道这件事。 做完奇异值分解,取前两个方向,词就落到了二维平面上:「优惠」「抢购」「点击」挤在主题 1 那头, 「审批」「项目」「会议」挤在主题 2 那头。没有负数、没有混杂,两拨词泾渭分明。 文档也一样:前三篇在主题 2 上都是 +2.0817,主题 1 上恰好是 0;后三篇反过来。 这个 0 不是巧合,是两组词完全不交叉的必然结果。 真实语料里会有交叠,那时的值就不会这么干净。 主题的名字是人看了词之后起的。 算法只给你两个方向,方向里哪些词权重大、你把它们念一遍, 主题名就有了。这一步永远需要人,也永远带主观性——所以「主题模型」出来的结果,一定要人来验一遍。

四、压到几维才够:看误差曲线的拐点

Demo 里把 1 到 6 维的误差都算了一遍:75.3%、36.5%、31.6%、25.8%、18.3%、0。 前两维吃掉一大半,第 3 维只再降 5 个点,最后一维把误差清零。 这里有个容易误读的地方:误差到 6 维变成 0,是因为矩阵本来只有 6 个奇异值(6 篇文档)。 6 维等于没压,当然没有误差。真正该看的是「加一维能降多少」——从 2 到 3 只降 5 个点, 说明第 3 维基本没用。 存储上也划算:原始矩阵 12 × 6 = 72 个数,两个主题的近似只要 24 + 2 + 12 = 38 个,压缩比 1.9 倍。 注意这个压缩比不高,因为矩阵本来就小。 真实语料动辄几万词乘几万文档,那时压到几百维,压缩比是几百倍。 误差 36.5% 这个数要正视。 它不小。 原因是六篇文档两两不同,真实秩接近 6,硬压到 2 维必然丢东西。 LSA 不是「无损压缩」,它是「用少量主题近似表达一大堆文档」,丢掉的正是「每篇文档的个性」。

五、它的三个老毛病

第一,一个词只有一个坐标,所以分不清多义词。 「苹果」在水果文档和手机文档里会落到同一个位置, 算法没法知道这两处的「苹果」不是一回事。 第二,同义词不会自动合并。 「电脑」和「计算机」在矩阵里是两行,彼此正交。 除非它们在语料里总是同时出现(共现),LSA 才会把它们拉到一起。 这一步靠的是统计数据,不是语义知识。 第三,它只能抓线性结构。 和 PCA 一样,奇异值分解是线性的, 抓不出「否定」这种结构——「好」和「不好」在词袋里是两个不同的词,但在语义上它们是相反的。 这三个毛病后来分别被主题模型(LDA,见 3.4 节末尾的辨析)和词向量解决了。 但 LSA 依然是理解「文本 → 矩阵 → 分解」这条主线最干净的入口。

六、动手 Demo:没人告诉它两个主题,它自己分出来了

12 个词、6 篇文档,刻意写成两组:前三篇讲开会报销,后三篇讲促销中奖,两组词几乎不交叉。 算法只看得到那张 12 乘 6 的表,完全不知道哪几篇是一类。 代码就是一次奇异值分解,加两个切片——词看 U,文档看 Vᵀ,再各自乘上奇异值把尺度还原。

import numpy as np

# A 是词频矩阵:行是词,列是文档(12 个词 × 6 篇)
U, s, Vt = np.linalg.svd(A, full_matrices=False)

k = 2
U2 = U[:, :k] * s[:k]        # 词在「主题空间」里的坐标
D2 = Vt[:k, :].T * s[:k]     # 文档在「主题空间」里的坐标

# 留几维的误差:丢掉的那些奇异值的平方和,占全部的多少
for kk in range(1, 7):
    Ak = (U[:, :kk] * s[:kk]) @ Vt[:kk, :]
    rel = np.sqrt(((A - Ak) * (A - Ak)).sum() / (A * A).sum())
    print("留 %d 维  相对误差 %.4f" % (kk, rel))

实跑输出:

--------------------------------------------------------------------
LSA:把词-文档矩阵做一次奇异值分解
--------------------------------------------------------------------

  6 篇小文档、12 个词的词频矩阵。前 3 篇是「开会报销」的事,后 3 篇是「促销中奖」的事——
  **但算法不知道这件事,它只看到一张表。**

  【一】奇异值:每个「潜在主题」有多重
  序号    奇异值      平方占比    累计
  ----------------------------------------
  1       3.6056      0.4333      0.4333
  2       3.6056      0.4333      0.8667
  3       1.0000      0.0333      0.9000
  4       1.0000      0.0333      0.9333

  【二】只留两个维度:词在「主题空间」里的坐标
  词      主题 1 权重   主题 2 权重
  ----------------------------------
  优惠     +1.7321       -0.0000
  抢购     +1.7321       -0.0000
  点击     +1.7321       -0.0000
  中奖     +1.1547       -0.0000
  审批     +0.0000       +1.7321
  项目     +0.0000       +1.7321
  会议     -0.0000       +1.7321
  纪要     -0.0000       +1.1547
  表格分成上下两半:上半是主题 1 权重最高的四个词(优惠、抢购、点击 这一路),
  下半是主题 2 权重最高的四个词(审批、项目、会议 这一路)。
  **没有人告诉算法这两个主题,它自己从共现模式里分出来的。**

  【三】文档在主题空间里的坐标
  文档    主题 1      主题 2      归属
  ----------------------------------------
  第 1 篇  -0.0000     +2.0817    开会
  第 2 篇  -0.0000     +2.0817    开会
  第 3 篇  +0.0000     +2.0817    开会
  第 4 篇  +2.0817     +0.0000    促销
  第 5 篇  +2.0817     +0.0000    促销
  第 6 篇  +2.0817     +0.0000    促销
  六篇文档被清清楚楚地分成两组,**和人工标注完全一致。**
   这就是 LSA(潜在语义分析)在做的事:把「词」和「文档」同时映射到一个低维空间,
  在这个空间里,同主题的东西自然靠近。

  【四】压到几维才够:留几维就丢多少信息
  保留维数    相对重构误差
  --------------------------
  1           75.3%
  2           36.5%
  3           31.6%
  4           25.8%
  5           18.3%
  6           0.0%
  从 12 维压到 2 维,误差 36.5%。再往上加一维只降到 31.6%——
  **这说明真正有信息的就是前两个方向,后面几维基本是在拟合噪声。**
  存储上:原始矩阵 72 个数,两个主题的近似只要 24 + 2 + 12 个数(左向量、奇异值、右向量)。
  **压缩比 1.9 倍。**
  LSA 和 3.1 节的 PCA 是同一件事:**都对矩阵做奇异值分解**。
  差别只在输入——PCA 输入是中心化后的数据矩阵,LSA 输入是词频矩阵。
  所以有人说「LSA 就是文本上的 PCA」,这话基本没错。

奇异值那张表:3.6056、3.6056,然后断到 1.0000。 前两个一样大, 因为它们对应两个势均力敌的主题(各三篇文档);后四个都是 1.0000,是「每篇文档各自的个性」。 这个 1.0000 反复出现,说明剩下的维度全是同一量级的残余,没有主次之分。 词表要注意那些 −0.0000 和 +0.0000。它们不是「很小的数」,就是精确的 0——两组词完全不交叉, 数学上必然如此。 真实语料里每个词都在两边带一点值,那时才需要看「谁大谁小」,而不是看「有没有」。 文档表六行全对。前三篇在主题 2 上 +2.0817、主题 1 上 0;后三篇反过来。 同一个数字 2.0817 出现六次,因为每篇文档都是 5 个词各一次,权重完全一样。 这也是「刻意凑出来的干净」,真实数据不会这么整齐。 误差曲线那张表最后一行是 0,别当成「算法很准」。留 6 维等于不压缩,当然没误差。 要看的是从 2 到 3 只降 5 个点——那才说明第 3 维没用。

30 秒实验

  1. 把第 3 篇改成「项目 进度 优惠 点击 抢购」(一半开会一半促销),它的归属会变成什么?
  2. 把 6 篇文档的每篇都加上一个共同词「公司」,两个主题的坐标会怎么变?
  3. 只留 1 维(k = 1)时,六篇文档的归属还有意义吗?为什么?

常见误区

  1. 以为 LSA 认出了「同义词」。它只是发现两个词总在同样的文档里出现,所以把它们放到相近的位置。 这是统计数据,不是语义理解。同义词如果从不同时出现,LSA 一点办法都没有。
  2. 忘了先去掉停用词。「的」「是」「和」每篇都有,它们的权重会压过所有有信息量的词, 主题就变成「所有文档都差不多」。做文本任务,停用词表和 TF-IDF 几乎是必做项。
  3. 把主题数当准确率来调。主题数不是越多越好——主题数等于文档数时误差是 0, 但那时每个主题只是一篇文档,毫无意义。主题数要按「能不能解释」来定,不能按误差来定。
自测三问(点开对答案) 1. LSA 和 PCA 的数学操作有区别吗? —— 没有本质区别,都是一次奇异值分解。区别在输入:PCA 输入中心化后的数据矩阵,LSA 输入词频矩阵。 所以 LSA 常被叫做「文本上的 PCA」。 2. 为什么 Demo 里词的坐标会出现精确的 0? —— 因为两组词完全不交叉,词频矩阵是分块对角的结构,两个主题的支撑集(非零位置)不重叠, 所以每个词在一个主题上必然是 0。 3. 留 6 维时误差是 0,这能说明 LSA 很准吗? —— 不能。留 6 维等于不压缩(矩阵本来只有 6 个奇异值),当然没误差。 要看的是「每加一维能降多少」,降得少就说明那一维没用。

LSA 的主题向量里可以出现负数,读起来别扭。 3.3 节加一条限制——所有元素必须非负——看看结果会变成什么样。

相关笔记