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 秒实验
- 把第 3 篇改成「项目 进度 优惠 点击 抢购」(一半开会一半促销),它的归属会变成什么?
- 把 6 篇文档的每篇都加上一个共同词「公司」,两个主题的坐标会怎么变?
- 只留 1 维(k = 1)时,六篇文档的归属还有意义吗?为什么?
常见误区
- 以为 LSA 认出了「同义词」。它只是发现两个词总在同样的文档里出现,所以把它们放到相近的位置。 这是统计数据,不是语义理解。同义词如果从不同时出现,LSA 一点办法都没有。
- 忘了先去掉停用词。「的」「是」「和」每篇都有,它们的权重会压过所有有信息量的词, 主题就变成「所有文档都差不多」。做文本任务,停用词表和 TF-IDF 几乎是必做项。
- 把主题数当准确率来调。主题数不是越多越好——主题数等于文档数时误差是 0, 但那时每个主题只是一篇文档,毫无意义。主题数要按「能不能解释」来定,不能按误差来定。
自测三问(点开对答案)
1. LSA 和 PCA 的数学操作有区别吗? —— 没有本质区别,都是一次奇异值分解。区别在输入:PCA 输入中心化后的数据矩阵,LSA 输入词频矩阵。 所以 LSA 常被叫做「文本上的 PCA」。 2. 为什么 Demo 里词的坐标会出现精确的 0? —— 因为两组词完全不交叉,词频矩阵是分块对角的结构,两个主题的支撑集(非零位置)不重叠, 所以每个词在一个主题上必然是 0。 3. 留 6 维时误差是 0,这能说明 LSA 很准吗? —— 不能。留 6 维等于不压缩(矩阵本来只有 6 个奇异值),当然没误差。 要看的是「每加一维能降多少」,降得少就说明那一维没用。LSA 的主题向量里可以出现负数,读起来别扭。 3.3 节加一条限制——所有元素必须非负——看看结果会变成什么样。
- ☐ (本节完,下一节 3.3 算法 12:NMF)