文本表示与词向量
第一节 初级分词技术
要让计算机开始理解人类语言,首先要做的就是把连续不断的文本“敲碎”,变成一块块有意义的“积木”——这个过程就是分词(Tokenization)。如果说文本是一座宏伟的建筑,那么“词”就是构成这座建筑的砖瓦。分词任务的质量,将决定上层建筑(如信息检索、机器翻译、情感分析等)的稳固程度。
一、分词的定义与重要性
分词的任务,是把连续的文本序列切分成具有独立语义的基本单元(即“词”或“词元”)。
- 对于英文等天然有空格作为分隔符的语言,分词相对简单。
- 但对于中文、日文、泰文等语言,文本是连续的字符流,词之间没有明确的边界。例如,
"给阿姨倒一杯卡布奇诺",计算机需要依据算法将其正确地切分为["给", "阿姨", "倒", "一杯", "卡布奇诺"]。
在传统的 NLP 处理流程中,分词是后续所有任务的基础。其处理方式通常是将分词作为一个独立且“硬性”的预处理步骤,这就导致一个微小的分词错误就可能造成语义信息的丢失。这种错误会在后续的处理链条中被不断放大,产生“差之毫厘,谬以千里”的级联效应(Cascading Effect)。例如,在传统的搜索引擎里,一旦“南京市长江大桥”被错分为 [“南京”, “市长”, “江大桥”],系统就很难再从这三个错误的词块中还原出原始的、正确的地理位置含义,导致搜索结果完全跑偏。现代的 NLP 方法则通过更灵活的切分策略,在很大程度上缓解了这个问题。
二、通过 jieba 认识分词
诚然,在以 Transformer 为基础的大语言模型兴起后,传统意义上“语言学分词”的应用场景在逐步减少,现代大语言模型更多采用子词(Subword)等切分策略。但 jieba 作为一个经典、简单、代码开源的中文分词库,仍是理解传统分词思想、学习 NLP 基础工程实践的重要工具 [^1]。
2.1 jieba安装
jieba 是目前流行的 Python 中文分词库之一,对初学者较为友好。可以使用如下 pip 命令安装。
| |
安装完成后,输入以下命令查看 jieba 的版本等信息,以确认是否安装成功。
| |
2.2 基于规则与词典
这是最早期也是最符合直觉的分词方法,它的核心是一部大型词典和一套匹配规则。jieba 的默认模式实现的就是这种方式。它首先基于一个前缀词典(Trie树),高效地构建出一个包含句子中所有可能词语组合的有向无环图(DAG)。接着,通过动态规划算法寻找一条概率最大的路径,作为最终分词结果。
这个过程可以被量化为一个概率计算问题。假设一个分词路径由一个词语序列组成,将其表示为 $w_1, w_2, …, w_n$,其中 $w_i$ 代表序列中的第 i 个词。那么这条路径的概率可以近似为:
$$ P(w_1, w_2, ..., w_n) \approx P(w_1) \times P(w_2) \times ... \times P(w_n) $$其中,每个词 $w_i$ 的概率 $P(w_i)$ 可以通过其在词典(语料库)中的频率来估算:
$$ P(w_i) = \frac{\text{词 } w_i \text{ 的词频}}{\text{词典中所有词的总词频}} $$jieba的目标就是找到一条路径,使得这个累乘的概率值最大。
2.2.1 Log概率与动态规划
在实际工程中,将大量小于1的概率值直接相乘,很容易导致结果趋近于0,造成浮点数下溢,从而无法比较路径优劣。
jieba 采用了两种关键技术来解决这个问题:
(1)对数概率:利用对数函数 log 的性质,将概率的累乘转换为 log 概率的累加。寻找概率最大值就等价于寻找 log 概率之和的最大值,有效避免了下溢问题。
(2)动态规划:暴力计算所有可能路径概率和的计算量是很大的。jieba 使用动态规划的思想,从句子的末尾开始,从后向前递推计算到每个位置的最优切分路径及其 log 概率之和,并记录下来。最终,从句子开头出发,根据记录好的最优路径信息,就能反推出整个句子的最优分词结果。
怎么又是数学公式🥹?我知道你很急,但你先别急
下面通过一个实际的例子来理解一下
例如,对于句子“给阿姨倒一杯卡布奇诺”,在构建好 DAG 之后,jieba 在“一/一杯”这个位置附近,可能会存在两条候选路径:
- 路径 A:
给 阿姨 倒 一杯 卡布奇诺 - 路径 B:
给 阿姨 倒 一 杯 卡布奇诺
如果直接从“概率乘积”的角度看,两条路径的概率近似为:
$P(A) \approx P(\text{给}) \times P(\text{阿姨}) \times P(\text{倒}) \times P(\text{一杯}) \times P(\text{卡布奇诺})$
$P(B) \approx P(\text{给}) \times P(\text{阿姨}) \times P(\text{倒}) \times P(\text{一}) \times P(\text{杯}) \times P(\text{卡布奇诺})$
在真实实现中,jieba 会把上面的乘积全部换成 log 概率的加和 来计算:
$\log P(A) \approx \log P(\text{给}) + \log P(\text{阿姨}) + \log P(\text{倒}) + \log P(\text{一杯}) + \log P(\text{卡布奇诺})$
$\log P(B) \approx \log P(\text{给}) + \log P(\text{阿姨}) + \log P(\text{倒}) + \log P(\text{一}) + \log P(\text{杯}) + \log P(\text{卡布奇诺})$
由于对数函数是单调递增的,谁的 log 概率和更大,谁对应的原始概率也更大。假设“一杯”是一个高频词,它单独出现的概率 $P(\text{一杯})$ 会远大于两个单字“一”和“杯”的概率之积 $P(\text{一}) \times P(\text{杯})$,于是就有:
$$\log P(\text{一杯}) \gg \log P(\text{一}) + \log P(\text{杯})$$因此:
$$\log P(A) > \log P(B)$$动态规划要做的,就是**在句子的每一个位置上,自动完成这种“在所有可能后续切分中,挑出 log 概率和最大的那条路径”**的比较,而不是像例子里只人工枚举两条路径。最终,它就会把路径 A 选为“全句最优分词结果”。
2.2.2 Jieba 实践
jieba的精确模式正是前文所述**“基于词典和动态规划寻找最大概率路径”**这一方法的典型应用,它会力图将句子尽可能精确地切开。
| |
输出:
| |
上面的例子之所以能切得比较理想,是因为“清华大学”“通知书”等词在词典中已有较稳定的统计信息。反过来,精确模式的核心仍然是“基于词典找最优路径”。一旦遇到词典里没有收录的新词(例如“奔波儿灞”),就容易被拆成更小的片段,出现不符合预期的切分。这时,基于词典的分词方法的优势就体现出来了。我们可以进行人工干预,通过自定义词典把新词加入词表,“教会”jieba认识它。
下面创建一个自定义字典user_dict.txt:
| |
jieba自定义词典的格式为:词语 [词频] [词性]。词频和词性是可选的,用空格隔开。这里先不添加这两个选项。
| |
| |
通过自定义词典,能很方便地解决 OOV 的问题,让分词结果符合预期,这在处理特定业务领域文本时尤其有用。
未登录词(OOV, Out-Of-Vocabulary)是一个“相对概念”——相对某个词典/词表未被收录的词。对词典法来说,就是词典里没有;常见表现是被切成多个字或更小片段,需要通过维护词典(或引入其它模型)来修正。
当向jieba中添加自定义词(如“奔波儿灞”)但不指定词频时,jieba会采取一种自动的方式来估算其词频。它会首先尝试对这个新词进行分词(例如,默认会切成 ["奔", "波", "儿", "灞"]),然后基于这些组成部分的基础词频计算出一个概率。最后,给“奔波儿灞”这个整体赋予一个略高于其组成部分概率之积的词频。这样,在后续的分词计算中,模型就会更倾向于将“奔波儿灞”视为一个完整的词,从而实现了“强制”分词的效果。
2.2.3 精确模式工作流程
图 2-1 jieba 精确模式工作流程
分析底层源码可以发现,jieba 精确模式的分词过程主要是下面四步(简单了解):
文本预处理与分块 (
cut方法)cut函数是总调度。它首先通过正则表达式re_han_default将整个句子切分成连续的汉字区块和非汉字部分(如英文、数字、标点)。非汉字部分被直接输出,而每个汉字区块则被送入核心分词流程。构建有向无环图(DAG) (
get_DAG方法)这一步为每个汉字区块生成一个记录所有可能分词路径的图。
get_DAG的逻辑是:- 从句子的第
k个字开始,向后扫描,形成词语frag(fragment)。 - 只要
frag存在于self.FREQ这个前缀词典(由主词典与用户词典加载后生成)中,就继续向后扫描。 - 在扫描过程中,如果
self.FREQ[frag]的值不为0,说明frag是一个能独立成词的词语,就将它的结束位置i记录下来。词频为0的词条仅被当作前缀,不会被记录为成词路径。 - 最终,
get_DAG返回一个字典DAG。
- 从句子的第
计算最优路径 (
calc方法)这是动态规划算法的核心实现,用于从DAG中寻找最优路径。
calc的计算方向很重要:它是从句子的末尾向前反向计算的 (for idx in xrange(N - 1, -1, -1))。- 对于句中的每个位置
idx,它会考察所有从idx出发的可能词语(由DAG[idx]提供)。 - 对于每个可能的词
sentence[idx:x + 1],它会计算一个路径“分数”,这个分数由两部分相加而成:- 当前词的log概率:源码中为
log(self.FREQ.get(...) or 1) - logtotal。 - 该词之后剩余句子的最优log概率:这部分已经在之前的迭代中计算好并储存在
route[x + 1][0]中。
- 当前词的log概率:源码中为
calc会选择使这个总分数最高的那个词语作为从idx出发的“最佳下一步”,并将这个最高分和“下一步”的起始位置x记录在route[idx]中。
- 对于句中的每个位置
从路由表中重建结果 (
__cut_DAG_NO_HMM方法)当
calc计算完成后,route字典中已经储存了从每个位置出发的最优选择。__cut_DAG_NO_HMM的工作就相对简单:- 它从句子的第
0位开始 (x=0)。 - 通过
route[x][1]查找下一步应该跳到哪里,从而得到第一个最优的词。 - 然后将
x更新为这个词的结束位置,继续循环查找,直到句子末尾。 - 最终,通过
yield逐个返回最优路径上的所有词语。
- 它从句子的第
2.3 统计学习时代的方法
为了解决对人工词典的过度依赖,研究者们转向了统计学习。其核心思想是把分词看作一个序列标注问题。它会为每个字标注其在词中的位置(B-Begin, M-Middle, E-End, S-Single),然后利用隐马尔可夫模型(HMM) 等模型来预测每个字最可能的位置标签序列。也就是为句子中的每个字打上一个位置标签,例如:
B(Begin):词的开始M(Middle):词的中间E(End):词的结束S(Single):单字成词
这样,“我爱北京”就会被标注为 S S B E。分词任务就变成了为字序列寻找最合理的标签序列。
隐马尔可夫模型是解决这类问题的经典生成式模型。它能学习到字与标签之间的对应关系(发射概率)以及标签与标签之间的转移关系(转移概率)。jieba 就利用了HMM来识别词典中不存在的 OOV [^2]。当基于词典的图算法在句子中遇到一个无法切分的、连续的未登录字串时,就会调用HMM模块对这个局部子句进行分词。
- 优点:能够发现词典外的新词,一定程度上解决了 OOV 问题。
- 缺点:HMM的两个核心假设(观测独立和齐次马尔可夫)过于严格,限制了其利用更丰富的上下文特征的能力,因此在处理复杂的歧义场景时效果不如后续的CRF等模型。
2.3.1 Jieba 实践:HMM对未登录词的识别
前面分析了 __cut_DAG_NO_HMM 的源码,它直接使用动态规划(calc 方法)计算出的最优路径。如果最优路径将“直聘”切分为两个单字,它就会直接输出两个单字。
而当 HMM=True(默认情况)时,jieba 调用的是 __cut_DAG 方法,它的源码展现了混合策略:
- 设立缓冲区:函数内有一个缓冲区
buf。 - 收集单字:当
calc计算出的最优路径是一个单字时,该函数并不立即输出,而是将这个单字存入buf。 - HMM介入:当遇到一个多字词,或者整个句子遍历结束时,函数会检查
buf。如果buf中积累了连续的单字(如"直聘"),它并不会直接输出这些单字,而是调用finalseg.cut(buf)来处理。finalseg正是jieba内置的HMM模型。 - 二次分词:HMM模型会对
buf中的字符串进行一次基于统计模型的“二次分词”,尝试从中识别出完整的未登录词。
下面的例子直观地展示了 __cut_DAG 中HMM模块的作用:
| |
| |
在这个例子中,当HMM开启时,__cut_DAG 方法将动态规划产生的 ['直', '聘'] 序列在缓冲区内拼接成 "直聘",然后交由HMM模型(finalseg.cut)处理,最终HMM通过其学到的统计规律,“猜”出这两个字是一个完整的词语,从而弥补了词典法的不足。
2.3.2 词性标注
除了分词,jieba 还提供了词性标注功能。采用了词典查询与隐马尔可夫模型相结合的混合策略,来识别出每个词语的语法属性(名词、动词、形容词等)。这需要使用jieba.posseg模块。
由于在前面已经通过 jieba.load_userdict() 加载了包含“奔波儿灞”的词典,因此 jieba 已经能够正确地将其切分出来。但是,因为初始词典未提供词性,jieba 会给它一个默认的、不一定准确的词性(如下面的x)。
| |
此时的输出,分词是正确的,但“九头虫”和“奔波儿灞”的词性是x(非语素字):
| |
接下来,尝试通过调整词频来“干预”分词路径。如果希望将‘九头虫’完全切分为‘九’、‘头’、‘虫’三个单字,可以修改词典如下。为单个字“九”和“头”赋予了很高的词频,这会使jieba在进行动态规划计算时,认为 九+头 这条路径的概率远大于 九头 这条路径,从而优先选择前者。
将user_pos_dict.txt修改为:
| |
为了让修改生效,需要重新加载词典:
| |
可以看到,jieba根据词典的“指示”,成功地将“九头”拆分:
| |
jieba 使用的是兼容ICTCLAS的词性标记集,常见的标签如下:
| 标签 | 含义 | 标签 | 含义 |
|---|---|---|---|
| n | 名词 | nr | 人名 |
| ns | 地名 | nt | 机构团体 |
| nz | 其他专名 | v | 动词 |
| a | 形容词 | d | 副词 |
| m | 数词 | q | 量词 |
| r | 代词 | p | 介词 |
| c | 连词 | u | 助词 |
| t | 时间词 | x | 非语素字 |
| w | 标点符号 | un | 未知词 |
2.4 从“分词”到“分块”
随着深度学习,特别是BERT和GPT等大规模预训练模型的兴起,传统意义上“将句子切分成标准词语”的分词范式有了重大改变。现代 NLP 模型更倾向于采用 “无分词”或“弱分词” 的策略,将文本处理成更基础的、数据驱动的单元,主要分为以下两种流派:
2.4.1 字粒度分词(Character-level)
以 BERT 模型为代表,在处理中文时,其最基础的分词策略就是字粒度,即直接将每个汉字视为一个独立的Token。
- 优点 :
- 有效解决了 OOV 问题:常用汉字的数量是有限的(几千个),模型可以构建一个全覆盖的“字表”。任何由标准汉字组成的词语都不会“未登录”,因为构成它的每个字都在字表里。
- 无需维护庞大词典:摆脱了对词典的依赖。
- 缺点 :
- 丢失词汇语义:像“博物馆”这样的词,其整体语义在输入层面被人为地拆散为三个独立的字,模型需要消耗更多的计算资源在内部重新学习这些字的组合关系。
- 输入序列更长:相较于词,字的序列长度会显著增加,加大了模型的处理负担。
2.4.2 子词分词(Subword)
以 GPT 系列为代表的大语言模型,则采用了更灵活的子词(Subword) 切分方案,其中最主流的算法是 BPE(BytePair Encoding) [^3]。
BPE的核心思想是:在原始的字符语料库上,迭代地将高频的相邻字节对(或字符对)合并成一个新的、更大的单元,并将其加入词表。
例如,对于语料中频繁出现的 “deeper”,BPE可能会先学习到 “er”,然后是 “deep”,最终可能将 “deeper” 作为一个整体或"deep"+“er” 的组合加入词表。
优点 :
- 有效平衡 :它在“词”和“字”之间取得了较好的平衡。高频词(如“机器学习”)可以被完整保留,低频词(如“擘画”)可以被拆分为更小的有意义的子词或单字(
擘+画),而OOV(如一个新造的网络词)则可以被拆解成字符或字节组合,从而在保持信息完整性的前提下有效解决了OOV问题。 - 词表大小可控:可以通过控制合并次数,将词表大小有效控制在预设范围内。
- 有效平衡 :它在“词”和“字”之间取得了较好的平衡。高频词(如“机器学习”)可以被完整保留,低频词(如“擘画”)可以被拆分为更小的有意义的子词或单字(
缺点 :
- 语言学可解释性不强:BPE等算法产生的子词通常是基于统计频率,而非语言学上的词根、词缀等有意义的语素。这使得分词结果的可解释性变差。
- 对训练语料的强依赖性:子词词表是根据特定语料训练的,对于领域外的文本,其分词效果可能会退化,将很多词切成单字,导致处理效率降低。
子词分词是当前大语言模型处理文本的标准方案,既保留了词的语义信息,又具备了字的灵活性。
练习
- 尝试修改 user_pos_dict.txt 中九和头的词频看看结果有什么不同
- 自行更换代码中使用的 text 短句看看会得到什么输出
参考文献1
[1]: Sun, F. (2012). jieba Chinese segmentation. GitHub
第二节 词向量表示
在上一章中,简单学习了如何将连续的文本切分成一个个独立的词元(Token)。然而,无论是jieba分出的词语,还是BERT模型中的单个汉字,它们本质上仍然是计算机无法直接理解的"字符串"。
机器学习和深度学习模型,无论结构多么复杂,其处理的输入都必须是数值形式——具体来说,是由数字组成的特征向量或矩阵。因此,在分词之后,必须将这些词元转换为模型可以"消化"的数字形式。这个过程称为词向量表示 (Word Representation) 。**词嵌入 (Word Embedding)**通常特指通过神经网络学习得到的稠密向量表示,是词向量表示的一个重要子集。
一、为什么需要词向量?
模型无法直接处理文本,它需要数字化的特征。以最基础的 NLP 任务——文本分类为例,需要判断一段文本“国足爱吃海参”属于“负面”类别。模型无法直接理解这串汉字,它只能处理数字。
词向量的核心任务就是弥合自然语言(符号世界)与数学模型(向量空间)之间的鸿沟。需要一种系统性的方法,将分词后得到的["国足", "爱", "吃", "海参"]这个词元序列,整体转换成一个或一组有意义的数字(即向量),然后才能将其输入给分类模型进行训练和预测。一个好的词向量表示,不仅要能唯一地标识一个词,更理想的情况是,向量本身能够蕴含词语的语义信息。例如,我们希望“国王”和“女王”的向量在空间中的距离,会比“国王”和“香蕉”的向量距离更近。实现这一目标,也是 NLP 领域的重要发展方向之一。
二、离散表示
在深度学习普及之前,研究者们提出了多种将词语表示为固定向量的方法。这些方法通常将每个词视为一个独立的、不可再分的单元,生成的向量因此也被称为离散向量(Discrete Vector)。其特点是维度高且稀疏,在机器学习领域应用广泛。
2.1 独热编码(One-Hot Encoding)
这是最直观、最基础的词元级别表示方法。它将每个词元都看作一个独立的类别,其思想与机器学习中的类别特征处理一致。
2.1.1 编码原理
独热编码,也称"哑编码",其核心步骤如下:
- 构建词典:首先,从整个语料库中收集所有出现过的唯一词语,构成一个词典。
- 分配索引:为词典中的每个词语分配一个从 0 开始的唯一整数索引。
- 创建向量:用一个长度等于词典大小的向量来表示每个词。向量中,该词对应索引的位置为
1,其余所有位置均为0。
2.1.2 编码示例
假设词典是从句子"我先挣它一个亿"构建的,分词后为["我", "先", "挣", "它", "一个", "亿"]。
我->[1, 0, 0, 0, 0, 0]先->[0, 1, 0, 0, 0, 0]挣->[0, 0, 1, 0, 0, 0]它->[0, 0, 0, 1, 0, 0]一个->[0, 0, 0, 0, 1, 0]亿->[0, 0, 0, 0, 0, 1]
2.1.3 优点与缺陷
优点:实现简单,能够清晰地将词语区分开。
缺点:
维度灾难:如果词典中有数万个词,那么每个词的向量维度就高达数万,造成数据极其稀疏,浪费计算和存储资源。
语义鸿沟:任意两个不同词的独热向量都是正交的(它们的点积为 0)。
为什么点积为0?
点积是通过将两个向量的对应元素相乘再求和来计算的。在独热编码中,每个向量只有一个位置是
1,其余都是0。对于任意两个不同的词,它们值为1的位置必然是错开的。因此,在计算点积时,
1总是与0相乘,导致所有乘积项都为0,最终的和(点积)也为 0。在几何上,点积为 0 意味着向量正交(互相垂直),这在语义上表示所有词都被视为同等的不相似。意味着模型无法从向量层面得知词与词之间的任何相似关系。在模型看来,“国王"与"女王"的距离,和"国王"与"香蕉"的距离是完全一样的,这严重丢失了语义信息。
2.2 词袋模型(Bag-of-Words, BoW)
哑编码表示的是单个词,但实际通常需要表示整个句子或文档。词袋模型正是为此而生,它是表示文档级别特征最常用的方法之一。这种方法的理论基础可以追溯到向量空间模型 [^1]的提出。
2.2.1 基本思想
词袋模型的基本思想是忽略文本中的词序和语法,将其仅仅视作一个装满词的"袋子”,用袋子中每个词出现的统计量来表示整个文档。
它的实现过程可以理解为:将文档中所有词的独热向量相加,得到一个最终的向量。这个向量的维度等于词典大小,每一维的值代表了对应词语在文档中的出现频次。实际实现时,通常直接统计每个词的出现次数,而不需要真正构造和相加 One-Hot 向量。
2.2.2 实现示例
假设词典与上文相同(基于"我先挣它一个亿"),有两个文档:
- 文档1:
我 先 挣 一个 亿 - 文档2:
我 挣 它 一个 亿
它们的词袋表示为:
vec(文档1)=vec(我)+vec(先)+vec(挣)+vec(一个)+vec(亿)=[1, 1, 1, 0, 1, 1]vec(文档2)=vec(我)+vec(挣)+vec(它)+vec(一个)+vec(亿)=[1, 0, 1, 1, 1, 1]
这个结果向量的每一维,代表了对应词典中的词在该文档中出现的次数。通过计算两个向量的距离(如余弦相似度),可以发现这两个文档是比较相似的,因为它们共享了"我"、“挣”、“一个”、“亿"这几个词。
2.2.3 余弦相似度计算
**余弦相似度(Cosine Similarity)**通过计算两个向量夹角的余弦值来衡量它们的相似性。对于非负向量(如词袋模型产生的向量),其值范围在 [0, 1] 之间,值越接近1,表示两个向量越相似。
公式:
$$\text{similarity} = \cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{||\mathbf{A}|| \cdot ||\mathbf{B}||} = \frac{\sum_{i=1}^{n} A_i B_i}{\sqrt{\sum_{i=1}^{n} A_i^2} \cdot \sqrt{\sum_{i=1}^{n} B_i^2}}$$令 A = vec(文档1) = [1, 1, 1, 0, 1, 1],B = vec(文档2) = [1, 0, 1, 1, 1, 1]
计算点积 A · B: $(1 \times 1) + (1 \times 0) + (1 \times 1) + (0 \times 1) + (1 \times 1) + (1 \times 1) = 1 + 0 + 1 + 0 + 1 + 1 = 4$
计算模长 ||A|| 和 ||B||:
||A||= $\sqrt{1^2+1^2+1^2+0^2+1^2+1^2} = \sqrt{5}$||B||= $\sqrt{1^2+0^2+1^2+1^2+1^2+1^2} = \sqrt{5}$计算相似度:
$$\cos(\theta) = \frac{4}{\sqrt{5} \cdot \sqrt{5}} = \frac{4}{5} = 0.8$$
结果为0.8,这是一个非常接近1的值,这从数学上证明了这两个文档是高度相似的。
2.2.4 不同统计方式
词袋模型向量中每一维的值,可以根据不同策略来确定:
- 频数: 直接使用单词在文档中出现的次数。这是最简单直接的方式,但会受到文章长度影响,长文章的计数值会普遍偏高。
- 频率: 使用单词在文档中出现的次数除以文档的总词数,即词频(TF)。这在一定程度上缓解了文档长度不同带来的问题。
- 二进制: 只关心单词是否出现,出现即为
1,不出现为0,不关心出现的次数。
2.2.5 优点与局限
- 优点:实现简单,并且在文本分类等任务上,因为这类任务的核心在于判断"文档里有什么词”,而非"词与词之间如何关联",所以即使丢失了词序,也常常能取得不错的效果。
- 缺点:
- 丢失词序:
"我 爱 你"和"你 爱 我"的词袋表示完全相同,无法区分语义差异。 - 未考虑词的重要性:像"的"、“是"这类在所有文档中都频繁出现的停用词,会获得很高的频次,但它们对区分文档主题几乎没有贡献,反而会形成干扰。
- 丢失词序:
2.3 N-gram 模型
词袋模型最大的局限在于丢失了词序信息,而 N-gram(N元语法) 通过统计连续词组的方式弥补了这一短板。它不仅仅是对传统方法的改进,更是现代大语言模型(LLM)的鼻祖,因为它最早引入了预测下一个词的思想 [^2]。
2.3.1 预测下一个词
与词袋模型只关心“有什么词”不同,N-gram(1-gram 是特例)关心的是“词的顺序”。其核心基于马尔可夫假设,也就是认为一个词出现的概率只取决于它前面 N-1 个词。虽然这是一种简化,但它极大地降低了建模的复杂度。
这正是生成式 AI 的雏形。根据依赖的前文长度不同,常见的 N-gram 模型包括:
- Unigram (1-gram): 即词袋模型。假设每个词都是独立的,完全不看前面。
- Bigram (2-gram): 只看前 1 个词。例如看到“喜欢”,预测下一个词是“玩”的概率。
- Trigram (3-gram): 看前 2 个词。例如看到“喜欢 玩”,预测下一个词是“GTA6”的概率。
现在的 GPT 等大模型,本质上可以被视为一个 N 非常非常大(比如 N=128000)的超级 N-gram 模型。它们都在执行根据上文预测下一个词这一核心任务。
2.3.2 示例
对于句子 "我 喜欢 玩 GTA6":
- Bigram 特征:
{"我 喜欢", "喜欢 玩", "玩 GTA6"} - Trigram 特征:
{"我 喜欢 玩", "喜欢 玩 GTA6"}
通过这种方式,模型就能区分 "我 喜欢" 和 "喜欢 我" 了,因为它捕捉到了局部的序列信息。
2.3.3 维度灾难
虽然 N-gram 找回了词序,但它付出了巨大的代价,这也是它被神经网络取代的原因:
- 指数级爆炸:如果词典有 10,000 个词,Bigram 就有 $10^8$ 种组合,Trigram 有 $10^{12}$ 种… 这种维度灾难是传统计算机无法承受的。
- 数据稀疏:绝大多数词的组合(如“大象 驾驶 飞机”)在语料中永远不会出现,导致概率为 0。为了解决这个问题,传统 NLP 发展出了复杂的平滑技术,以及通过将词聚类来减少参数的基于类的 N-gram 模型 [^3]。
正是为了从根本上解决这些问题,后续诞生了词向量和神经网络语言模型。
2.4 TF-IDF
为了提升文档在向量空间中的区分度,解决"常见词权重过高"导致文档混淆的问题,TF-IDF(Term Frequency-Inverse Document Frequency) 被提出。正如 Salton 等人所指出的,优秀的索引项应该能降低文档空间的密度,使语义不同的文档在空间中距离更远 [^1]。TF-IDF 就是实现这一目标的经典加权技术。
TF-IDF 指出:一个词的重要性,与其在当前文档中出现的次数成正比,与其在整个语料库中出现的频率成反比。一个词在当前文档里越常见,但在其他文档里越罕见,其权重就越高。
2.4.1 计算公式
它由两部分组成:
词频(Term Frequency, TF):衡量一个词在当前文档中出现的频繁程度。常见的计算方式有:
- 原始频数: $TF(t, d) = f_{t,d}$
- 归一化频率: $TF(t, d) = \frac{f_{t,d}}{\sum_{t’ \in d} f_{t’,d}}$
其中 $f_{t,d}$ 表示词 $t$ 在文档 $d$ 中出现的次数。
逆文档频率(Inverse Document Frequency, IDF):衡量一个词的"稀有"程度或"信息量”。这一概念由 Karen Sparck Jones 在 1972 年提出 [^4]。
$$ IDF(t, D) = \log \frac{|D|}{|\{d \in D : t \in d\}|} $$其中 $|D|$ 是语料库中的总文档数, $|{d \in D : t \in d}|$ 是包含词 $t$ 的文档数。
为避免除零错误,实际应用中常使用平滑版本:
$$ IDF(t, D) = \log \frac{|D|}{1 + |\{d \in D : t \in d\}|} $$
最终, 一个词的 TF-IDF 权重就是这两者的乘积:
$$ TF-IDF(t, d, D) = TF(t, d) \times IDF(t, D) $$一个文档的 TF-IDF 向量, 就是由该文档中每个词的 TF-IDF 值构成的向量。
2.4.2 实际应用
- 关键词提取:计算一篇文章中每个词的 TF-IDF 值,并按降序排列,排在最前面的通常就是这篇文章的关键词。
jieba的关键词提取也内置了这种算法。 - 文本相似度计算:计算两篇文档的 TF-IDF 向量,再通过余弦相似度等方法判断它们的相似性。
- 传统搜索引擎:在早期的搜索引擎中,TF-IDF 是衡量查询词与网页相关性的核心指标之一。
三、序号化表示
虽然上述方法在传统机器学习时代扮演了重要角色,但在深度学习时代,它们已不再是主流。现代深度学习模型,尤其是大语言模型,采用的是一种更简洁、更灵活的输入方式——序号化(Sequentialization)。
3.1 核心思想转变
在深度学习中,通常只进行最少的预处理。不再像传统方法那样,费尽心思地设计复杂的特征工程(如计算 TF-IDF)来告诉模型哪些词重要。相反,只把文本转换成最基础的整数 ID 序列,然后把"学习词语的含义和重要性"这个更复杂的任务,交给模型自己去完成。
3.2 序号化过程
序号化,也称"整数编码",是将分词后的词元序列转换为深度学习模型能够处理的整数序列的核心步骤。其过程如下:
- 构建词典:与 One-Hot 类似,首先从训练语料中构建一个词典。但在深度学习中,这个词典通常是字级别的(如BERT),或是子词级别的(如GPT),而不是词级别的。
- 增加特殊词元:在词典中加入一些有特殊功能的 Token,至少包括:
[PAD](Padding):用于填充。因为模型通常需要批处理(Batch Processing),一个批次内的所有句子必须长度相同。短句子会用[PAD]填充到与最长句子一致的长度。其对应的 ID 通常是0。[UNK](Unknown):用于表示所有在词典中未出现过的词。其对应的 ID 通常是1。- 此外,还可能有
[CLS](Classification),[SEP](Separator) 等用于特定任务的特殊词元。
- ID 映射:将文本序列中的每个词元(字/子词)直接映射为其在词典中的整数 ID。
预训练模型的词典:
在实践中,很少从零开始为自己的小数据集构建词典。更常见的做法是,直接使用像 BERT、GPT 这类预训练模型官方提供的 词典文件(vocab.txt) 。这些词典通常包含了数万个字、子词、符号等,是在海量通用语料上构建的,覆盖面非常广。
例如,Google 的中文 BERT 模型词典 vocab.txt 中就包含了约 21128 个词元,其中不仅有常用汉字,还包括了英文字母、数字、标点及 [PAD], [UNK] 等特殊符号。
3.3 序号化实例
假设有一个精简词典:
{'[PAD]': 0, '[UNK]': 1, '比': 2, '方': 3, '说': 4, '我': 5, '先': 6, '挣': 7, '它': 8, '一': 9, '个': 10, '亿': 11}
现在有三个句子需要处理:
我挣一个亿比方说我我先挣钱
第一步:分词 & 查找ID
- 句子1 (
我挣一个亿):我(5),挣(7),一(9),个(10),亿(11) ->[5, 7, 9, 10, 11] - 句子2 (
比方说我):比(2),方(3),说(4),我(5) ->[2, 3, 4, 5] - 句子3 (
我先挣钱):我(5),先(6),挣(7),钱(不在词典中) ->[5, 6, 7, 1]
第二步:填充 (Padding)
为了将这三个长短不一的序列组成一个矩阵,需要以最长的序列(句子1,长度为5)为基准,对其他短序列用[PAD]的ID 0进行填充。
- 序列1 (长度5):
[5, 7, 9, 10, 11] - 序列2 (长度4→5):
[2, 3, 4, 5, 0] - 序列3 (长度4→5):
[5, 6, 7, 1, 0]
最终,我们得到一个 3x5 的整数矩阵。这个矩阵,就是喂给深度学习模型的最终输入。
| |
序号化本身并未解决语义鸿沟,其整数ID(如
2和3)不具备数学意义。它的真正价值是作为后续嵌入层的输入。嵌入层会将这些ID查询并映射为低维、稠密的浮点数向量(即词向量),而这个映射关系本身是在模型训练中学习出来的 [^5]。
参考文献2
第三节 从主题模型到 Word2Vec
无论是哑编码还是序号化,它们本身都存在一个根本性的缺陷:无法表达词与词之间的语义关系。在这些表示方法中,不同词的向量通常是正交的(如One-Hot编码中点积为0),或者其ID大小关系是随机的,这导致模型无法从输入层面理解“国王”与“女王”的语义比“国王”与“苹果”更近。
为了解决这个问题,分布式表示(Distributed Representation) 被提出,目的是将词语映射到一个低维、稠密、且蕴含丰富语义信息的连续向量空间中。本节将首先介绍一种基于传统机器学习思想的分布式表示方法——主题模型,并在此基础上,引出真正开启了 NLP 新时代的经典算法——Word2Vec。
一、寻找理想的词向量
理想中的词向量需要同时满足两个主要目标:
- 语义蕴含:向量之间的距离(如余弦相似度或欧氏距离)能够度量词语之间的语义相似度。这背后的原理是 分布式假设 的朴素应用:如果两个词经常在相似的上下文中共同出现(Co-occurrence),那么它们的向量在空间上应该是彼此靠近的。例如,“被子"和"床铺"经常一起出现,它们的向量就应该接近;而"椰子"和"企鹅"则应该相互远离。语义的相似性,正是这种"上下文共现"关系在向量空间中的自然体现。
- 低维稠密:摆脱“维度灾难”。词向量的维度应该是一个较小的、可控的超参数,而不是动辄数万的词典大小。并且,向量中的每一维都应是有意义的浮点数,而非绝大部分为0的稀疏表示。
为实现这一目标,研究者们探索了不同的技术路径。其中,一条是基于全局文档统计的主题模型;另一条则是后来居上、并成为主流的、基于局部上下文预测的神经网络模型。
二、主题模型
主题模型是基于机器学习和传统数学思想的经典方法。它尝试从宏观的视角,通过分析大量文档的词语共现统计,来发现词语间的潜在语义关联。其关键假设是:一篇文档由多个"主题"按一定比例混合而成,而一个主题又由多个"词语"按一定概率组成。词语之所以会一同出现在某篇文档中,是因为它们都在共同描述这篇文章所包含的某个或某些潜在主题。
例如,一篇关于"人工智能"的文档,会高频出现"深度学习”、“Transformer”、“注意力机制"等词。正是因为这些词都强关联于"AI技术"这个主题,它们才频繁地共现在一起。所以,一个词的向量,就可以用它与各个主题的关联强度来表示。这其中最核心的技术,就是 矩阵分解(Matrix Factorization)。
2.1 SVD 矩阵分解
该方法将获取词向量的过程,巧妙地转化成了一个矩阵分解问题。

图 2-2 矩阵分解图示
构建“词-文档”矩阵
首先,以整个语料库为基础,构建一个巨大的词-文档矩阵 $X$。这个矩阵的每一行代表一个词,每一列代表一篇文档,矩阵中 $X(i, j)$ 的值是词 $i$ 在文档 $j$ 中的重要性权重,可以使用 TF-IDF 值来填充。这个矩阵通常是巨大且高度稀疏的。
矩阵分解
从线性代数的角度看,这个巨大的稀疏矩阵 $X$ 可以被近似分解为两个更小的、更稠密的矩阵的乘积。最常用的分解技术之一是 奇异值分解(SVD)。
$$X_{m \times n} \approx W_{topic\, m \times k} \times H_{topic\, k \times n}$$- $X_{m \times n}$ 是原始的词-文档矩阵, $m$ 是词典大小, $n$ 是文档数量。
- $k$ 是一个远小于 $m$ 和 $n$ 的超参数,代表期望发现的潜在主题数量。
- $W_{topic}$ 表示 “词-主题矩阵”。它的每一行,都是一个 $k$ 维的稠密向量,表示一个词与 $k$ 个主题的关联度。
- $H_{topic}$ 表示 “文档-主题矩阵”。它的每一列,都是一个 $k$ 维的稠密向量,表示一篇文档在 $k$ 个主题上的分布。
获取词向量
分解完成后,我们真正关心的是 “词-主题”矩阵 $W_{topic}$。这个矩阵的每一行,正是我们需要的词向量。
- 它将原来 $m$ 维的One-Hot编码,降维到了 $k$ 维。
- 它是一个稠密向量,每个维度都代表了与某个主题的关联强度。
- 它蕴含了语义信息。如果两个词(如"CPU"和"GPU”)经常在描述"硬件"这个主题的文档中共同出现,那么SVD分解的结果会使它们在对应"硬件"主题的那个维度上都有很高的值,从而使它们的最终词向量在空间上非常接近。
2.2 聚类视角理解主题模型
从机器学习的角度看,主题模型本质上是一个聚类算法:
文档聚类:文档主题矩阵 $H_{topic}$ 将 $n$ 篇文档聚成 $k$ 个主题类别。每篇文档都有一个 $k$ 维向量,表示其属于各个主题的 置信度 或 软分配。例如,一篇文档可能70%属于"AI技术"主题,30%属于"数学理论"主题。
词语倾向性:单词主题矩阵 $W_{topic}$ 揭示了词语的主题倾向。有些词语(如"深度学习"、“Transformer”)更倾向于描述"AI技术"主题,而另一些词语(如"坦克"、“导弹”)则更倾向于描述"军事"主题。这在一定程度上减弱了“同义词”问题。虽然“番茄”和“西红柿”写法完全不同,但因为它们都高频出现在“烹饪”或“蔬菜”相关的主题中,它们在 $W_{topic}$ 矩阵中的向量表示就会非常相似。
语义相关性的来源:正因为描述同一主题的词语会在相同的主题维度上有较高的权重,它们的词向量才会在空间中彼此靠近,从而实现了语义信息的捕捉。
2.3 总结与局限
主题模型(如其更广为人知的名字 LSA, Latent Semantic Analysis [^1])通过对全局的"词-文档"共现矩阵进行分解,成功地将词语映射到了一个低维的"主题空间",从而得到了能够表达语义的稠密词向量。相关的技术还包括 PCA 和 NMF(非负矩阵分解)。
不过,这种方法也存在明显的局限性:
- 计算代价高昂:对一个大型语料库进行SVD分解,计算量和内存开销都极大。
- 依赖全局统计:它依赖的是全局的、粗粒度的文档级别共现信息,忽略了词语在句子中的局部上下文和词序信息,这使得它难以捕捉更精细的语义关系。
- 难以集成:这种"先统计,再分解"的流程,很难与现代的深度学习模型进行端到端的联合训练。
三、Word2Vec
与主题模型从全局文档统计中挖掘主题不同,由 Google 在 2013 年提出的 Word2Vec 算法 [^2],将视角聚焦于词语的局部上下文。它的思想来源于语言学中的分布式假设,即一个词的含义,由其上下文中的词语所决定 [^3]。
换言之,如果两个词的上下文经常是相似的,那么这两个词的语义就是相近的。Word2Vec正是这一思想的数学实现。
3.1 Word2Vec概述
Word2Vec 通常被认为是一种浅层神经网络模型(Shallow Neural Network)。其"浅层"体现在网络结构的简单性上,它移除了传统神经概率语言模型(NNLM)中计算昂贵的非线性隐藏层,直接将投影层与输出层相连。这种简洁的设计使得 Word2Vec 的计算非常高效,从而能够在大规模语料库上进行训练。
目标与手段的分离:
理解Word2Vec的关键在于区分其最终目标与实现手段。神经网络结构本身只是获取词向量的一种方式,并非模型的最终目的。
- 最终目标:获取一个高质量的 词向量查询表。这本质上是一个巨大的矩阵 $W_{in}$ , 矩阵的每一行就是对应单词的稠密向量。
- 实现手段:为了学习到这个查询表,Word2Vec设计了一个巧妙的"伪任务"——根据上下文预测中心词(或反之),并在这个过程中,将词向量查询表作为模型参数进行训练和优化。
训练结束后,用于执行预测任务的神经网络本身会被丢弃。不会使用它的输出,真正需要和保留的,只有作为其内部参数的那个 词向量查询表。
3.2 可学习的词向量矩阵
从数学上看,将一个单词的ID转换为其稠密向量的过程,在概念上可以分解为两步:
- 输入:一个代表单词的ID,例如 $3$ 。
- 哑编码:将ID $3$ 转换为一个维度等于词典大小 $|V|$ 的高维稀疏向量,例如 $[0, 0, 0, 1, 0, \ldots]$ , 其中只有第 3 个位置为 1。
- 矩阵乘法:用这个One-Hot向量去乘以一个巨大的、可学习的参数矩阵 $W_{in}$(尺寸为 $|V|\times D$)。这个矩阵 $W_{in}$ 就是最终想要得到的词向量查询表。
由于One-Hot向量只有一个位置是1,这个矩阵乘法的结果,等效于直接从矩阵 $W_{in}$ 中 “抽取”出索引为 3 的那一行。
$$ \begin{bmatrix} 0 & 0 & 0 & \color{#42b983}{1} & 0 & 0 \end{bmatrix} \times \begin{bmatrix} 2 & 8 & 5 & 3 & 1 \ 9 & 4 & 7 & 2 & 6 \ 3 & 1 & 8 & 5 & 0 \ 5 & 6 & 2 & 9 & 4 \ 8 & 0 & 3 & 7 & 2 \ 4 & 2 & 9 & 6 & 1 \end{bmatrix}
\begin{bmatrix} \color{#42b983}{5} & \color{#42b983}{6} & \color{#42b983}{2} & \color{#42b983}{9} & \color{#42b983}{4} \end{bmatrix} $$
在实践中,为了极大地提升效率,程序并不会真的执行稀疏的矩阵乘法,而是直接实现一个 查询 操作:根据输入的单词ID,直接从 $W_{in}$ 矩阵中获取对应的行向量。理解这里的关键在于,这个参数矩阵 $W_{in}$ 本身就是学习的目标。它被随机初始化,并在后续的训练过程中,通过CBOW或Skip-gram这样的 预测任务 不断地被优化和调整。
在 PyTorch 等深度学习框架中,
nn.Embedding层本质上就是维护了这个 $W_{in}$ 矩阵(Lookup Table)。当我们在后续章节中搭建模型时,第一层通常都是 Embedding 层。它接收输入序列的整数 ID,直接通过查表将其映射为稠密的词向量,而这个矩阵的参数会随着整个模型的训练(反向传播)而被自动更新和学习。
3.3 两种经典模型
Word2Vec 包含 CBOW 和 Skip-gram 两种具体的实现模型。两者在任务设计上恰好相反,但最终都实现了相同的目标,即通过训练过程得到一个高质量的词向量查询表。
3.3.1 CBOW 模型详解
如图 2-3,CBOW(Continuous Bag-of-Words)的任务是 “根据上下文预测中心词”。
图 2-3 CBOW
数据流与维度变化:
假设 $B$ 是批大小, $S$ 是上下文单词数量, $|V|$ 是词典大小, $D$ 是词向量维度。
输入层:上下文窗口内的所有词对应的ID。
- 数据形状: $(B, S)$ , 例如输入为 $[[1, 8, 10, 13, 14, 16], [8, 5, 14, 16, 18, 10]]$ 。
词向量转换:
- 操作:将每个单词ID转换为对应的 $D$ 维词向量。这通过输入矩阵 $W_{in}$ (大小为 $|V|\times D$ )实现,相当于公式中的 $v_{c-k} = W_{in}w_{c-k}$ 。
- 数据形状变化: $(B, S)$ -> $(B, S, D)$ 。
上下文向量聚合:
- 操作:将一个样本中 $S$ 个上下文单词的 $D$ 维向量进行聚合(通常是求和或平均),得到上下文向量 $h$ 。原论文中使用的是平均,但在部分实现中也会采用求和。
- 数据形状变化: $(B, S, D)$ -> $(B, D)$ 。
输出得分计算:
- 操作:将 $D$ 维的上下文向量与输出矩阵 $W_{out}$ (大小为 $D\times |V|$ )相乘,得到 $|V|$ 维的得分向量,对应公式中的 $z_c = W_{out}^T, h$ (或按实现以右乘形式表示)。
- 数据形状变化: $(B, D)$ -> $(B, |V|)$ 。
损失计算:
- 操作:使用 Softmax 将得分转换为概率分布,然后计算与真实中心词之间的交叉熵损失。
- 优化:通过反向传播更新输入矩阵 $W_{in}$ 和输出矩阵 $W_{out}$ 。最终需要的是训练好的输入矩阵 $W_{in}$ 。
公式:
词向量转换:对于上下文中的每个词 $w_{c-k}$ ,从输入矩阵 $W_{in}$ 中获取对应的词向量: $v_{c-k} = W_{in}w_{c-k}$
上下文向量:将上下文窗口中所有词的词向量聚合(求和或平均),得到 $h = \frac{1}{S} \sum (v_{c-m} + \cdots + v_{c+m})$ (此处以平均为例)
输出得分:将上下文向量与输出矩阵 $W_{out}$ 相乘: $z_c = W_{out}^T h$
损失函数:模型的优化目标是最小化负对数似然:
$$ \begin{aligned} \text{minimize } J &= -\log P(w_c | w_{c-m}, \ldots, w_{c-1}, w_{c+1}, \ldots, w_{c+m}) \\ &= -\log P(u_c | h) \\ &= -\log \frac{\exp(u_c^T h)}{\sum_{j=1}^{|V|} \exp(u_j^T h)} \\ &= -u_c^T h + \log \sum_{j=1}^{|V|} \exp(u_j^T h) \end{aligned} $$其中 $u_c$ 是目标中心词的输出向量, $h$ 是上下文向量。
3.3.2 Skip-gram 模型详解
与 CBOW 恰好相反,Skip-gram 的任务是 “根据中心词预测上下文”。
Skip-gram 与 N-gram 的关系:
- 名字渊源:Skip-gram 这个名字确实源于传统的 k-skip-n-gram 模型(即允许跳过中间词的 N-gram)。
- 核心区别:虽然借用了“跳跃”的思想,但 Word2Vec 的 Skip-gram 是一种预测模型,而非统计模型。它并不是为了“修复”N-gram,而是为了更高效地学习稠密词向量。它通过“用中心词预测上下文”这一任务,强迫模型学习到词语的语义特征,从而彻底解决了传统 N-gram 面临的稀疏性和维度灾难问题。
在具体实现上,它将一个预测任务,分解成了多个独立的子任务。
数据流与维度变化:
- 输入层:中心词的 ID 。
- 数据形状: $(B, 1)$ 。
- 词向量转换:
- 操作:将中心词 ID 通过查表(输入矩阵 $W_{in}$ )转换为其 $D$ 维词向量 $v_{w_c}$ 。
- 数据形状变化: $(B, 1)$ -> $(B, 1, D)$ 。
- 输出得分计算:
- 操作:将 $D$ 维的中心词向量 $v_{w_c}$ , 与输出矩阵 $W_{out}$ 相乘,得到一个 $|V|$ 维的得分向量。
- 数据形状变化: $(B, 1, D)$ -> $(B, |V|)$ 。
- 损失计算(多标签问题):
- 操作:与 CBOW 不同,这里的目标是预测多个上下文单词(例如, $S$ 个)。因此,这一个 $|V|$ 维的得分向量将被 复用 $S$ 次,分别与 $S$ 个真实的上下文单词计算损失。
- 优化:将 $S$ 个位置的损失全部相加,然后进行反向传播,同时更新 $W_{in}$ 和 $W_{out}$ 。因为一个输入对应多个输出标签,这本质上是一个多标签分类问题。在实际运用中,通常将其分解为多个独立的单标签分类任务:对每个上下文词位置,都使用 Softmax 进行一次独立的预测,然后将所有位置的损失相加。
公式:
词向量转换:对于中心词 $w_c$,从输入矩阵 $W_{in}$ 中获取对应的词向量: $v_c = W_{in}w_c$
输出得分计算:将中心词向量与输出矩阵 $W_{out}$ 相乘: $z = W_{out}^T v_c$
损失函数:模型的优化目标是最小化负对数似然。完整的数学推导过程如下:
$$ \begin{aligned} \text{minimize } J &= -\log P(w_{c-m}, \ldots, w_{c-1}, w_{c+1}, \ldots, w_{c+m} | w_c) \\ &= -\log \prod_{j=0, j \neq m}^{2m} P(w_{c-m+j} | w_c) \\ &= -\log \prod_{j=0, j \neq m}^{2m} P(u_{c-m+j} | v_c) \\ &= -\log \prod_{j=0, j \neq m}^{2m} \frac{\exp(u_{c-m+j}^T v_c)}{\sum_{k=1}^{|V|} \exp(u_k^T v_c)} \\ &= -\sum_{j=0, j \neq m}^{2m} u_{c-m+j}^T v_c + 2m \log \sum_{k=1}^{|V|} \exp(u_k^T v_c) \end{aligned} $$其中 $v_c$ 是中心词的输入向量, $u_{c-m+j}$ 是上下文词的输出向量。
Skip-gram为每个"中心词-上下文词"对都创建了一个独立的学习任务,这使得它能够更好地学习到词与词之间更精细的关系。在处理低频词和大数据集时,通常能得到质量更高的词向量,但由于其任务量是CBOW的 $S$ 倍,训练速度相对较慢。
3.3.3 滑动窗口的直观理解
在了解了模型的基本构造后,可以深入探讨Word2Vec是如何真正捕捉到语义的。以CBOW模型为例,其关键在于滑动窗口机制如何生成大量高度重叠的训练样本。
假设有一个很长的句子,并设窗口大小为 $k=7$(中心词左右各 7 个词)。通过在文本上滑动该窗口可以生成大量训练样本:对于 CBOW 任务,当窗口中心位于第 8 个单词时,模型使用上下文 $[w_1, \ldots, w_7]$ 与 $[w_9, \ldots, w_{15}]$ 预测 $w_8$;随后窗口右移一格,中心变为第 9 个单词,模型使用新的上下文 $[w_2, \ldots, w_8]$ 与 $[w_{10}, \ldots, w_{16}]$ 预测 $w_9$。比较这两次样本可见,它们有 12 个上下文词完全相同( $w_2 \sim w_7$ 与 $w_{10} \sim w_{15}$),因此两者的 上下文向量(所有上下文词向量之和)在初始时就非常相似。
模型的目标是:
- 对于第一个样本,它需要调整参数,使得相似的上下文向量能够成功预测出 $w_8$。
- 对于第二个样本,它需要让这个极其相似的上下文向量,又能成功预测出 $w_9$。
为了同时满足这两个看似矛盾的目标,优化算法(如梯度下降)会找到一个“捷径”:如果 $w_8$ 和 $w_9$ 的词向量本身就足够接近,那么模型就能用一个相似的上下文向量同时很好地预测出它们俩。这一现象在整个语料库中不断重复:当两个不同的词(如“笔记本”和“电脑”)因语言习惯而频繁出现在相似的上下文(如与“键盘”“屏幕”“CPU”等共现)时,为了降低总体损失,模型会将它们的词向量在空间中推向彼此靠近的位置,从而形成语义相似性。
训练目标与余弦相似度:
从数学角度看,模型的最终目标是让 $|V|$ 维得分向量中,对应真实目标词的那个维度的值最大化。这个得分值,是由上下文向量 $x$(CBOW)或中心词向量 $v_c$(Skip-gram)与输出矩阵 $W_{out}$ 中对应目标词的行向量 $u_{target}$ 进行 点积 得到的。
$$ \text{score} = x \cdot u_{target} $$两个向量的点积是余弦相似度公式的分子部分。因此,最大化这个点积得分,在几何上就是在促使上下文向量 $x$ 和目标词向量 $u_{target}$ 的夹角尽可能小,即让它们在空间上更接近。这为前述的滑动窗口机制,提供了数学解释。实际训练中,为避免对整个词表进行 Softmax 归一化带来的高开销,可以用 Hierarchical Softmax 与负采样等近似方法加速训练 [^4]。
4. Word2Vec的局限性
尽管Word2Vec是里程碑式的算法,但存在一个根本性的局限性——它产生的是静态词向量。
上下文无关
- 对于词典中的任意一个词,Word2Vec 只会生成一个 固定 的向量表示。这个向量是在整个语料库上训练得到的“平均”语义,与该词出现的具体上下文无关。
- 这直接导致了 Word2Vec 无法解决一词多义的问题。例如,“小米”这个词,无论是在“农民伯伯正在收割小米”的语境中,还是在“小米公司发布了新手机”的语境中,Word2Vec 赋予它的词向量都是完全相同的。
静态的本质
- Word2Vec 的输出是一个巨大的查询表。训练完成后,这个表就固定下来了。在使用时,只是根据单词 ID 去查找对应的行向量,整个过程不涉及对上下文的动态分析。
练习
- 根据已经学过的内容使用
20newsgroups数据(from sklearn.datasets import fetch_20newsgroups)实现基于全连接的文本分类模型训练和推理代码(若自行实现困难,可以参考基于 LSTM 的文本分类)
参考文献3
[3]: Harris, Z. S. (1954). Distributional structure. Word, 10(2-3), 146-162
第四节 基于Gensim的词向量实战
前面已经学习了多种词向量表示,接下来尝试将这些理论转化为可运行的代码。本节将使用Gensim进行实践,通过简洁的代码示例来应用前几章介绍的算法,来加深对模型工作原理的理解,并掌握其基本使用方法。
一、Gensim 简介
Gensim (Generate Similar) 是一个功能强大且高效的Python库,专门用于处理原始的、非结构化的纯文本文档。它内置了多种主流的词向量和主题模型算法,如 Word2Vec、TF-IDF、LSA、LDA 等。
1.1 核心概念
使用 Gensim 时,会遇到几个概念:
语料库:这是 Gensim 处理的主要对象,可以简单理解为训练数据集。分词后的文档通常表示为
list[list[str]];用于 TF-IDF、LDA 等模型的标准 BoW 语料库是包含稀疏向量的可迭代对象,每篇文档表示为[(token_id, frequency), ...]。例如[["我", "爱", "吃", "海参"], ["国足", "惨败", "泰国"]]中每个子列表代表一篇独立的文档。词典:这是一个将词语(token)映射到唯一整数ID的词汇表。在使用词袋模型之前,必须先根据整个语料库构建一个词典。
向量:在Gensim中,一篇文档最终会被转换成一个数学向量。例如,使用词袋模型时,一篇文档
["我", "爱", "我"]可能会被表示为[(0, 2), (1, 1)]。稀疏向量:这是 Gensim 为了节省内存而采用的一种高效表示法。对于像 One-Hot 或词袋模型这样维度巨大且绝大多数值为0的向量,Gensim 不会存储所有0。例如,一个词袋向量
[2, 1, 0, 0, ... , 0]会被表示成[(0, 2), (1, 1)],仅记录非零项的索引和值,极大地减少了存储开销。模型:在 Gensim 中,模型是一个用于实现向量转换的算法。例如,
TfidfModel可以将一个由词频构成的词袋向量,转换为一个由TF-IDF权重构成的向量。
1.2 内置模型
Gensim 几乎涵盖了前面章节中讨论过的所有经典算法:
- TF-IDF:
models.TfidfModel - 主题模型 / 矩阵分解:
- LSA (Latent Semantic Analysis):
models.LsiModel - LDA (Latent Dirichlet Allocation):
models.LdaModel - NMF (Non-negative Matrix Factorization):
models.Nmf
- LSA (Latent Semantic Analysis):
- 神经网络词向量:
- Word2Vec:
models.Word2Vec - FastText:
models.FastText - Doc2Vec:
models.Doc2Vec
- Word2Vec:
1.3 安装gensim
直接使用pip即可:
| |
二、Gensim 工作流
在 Gensim 中,将原始文本转换为TF-IDF或主题模型向量,通常遵循一个标准的三步流程。这个流程是后续应用的基础。
准备语料:将原始的文本文档进行分词,并整理成Gensim要求的格式——一个由列表构成的列表
list[list[str]],其中每个子列表代表一篇独立的文档。创建词典:遍历所有分词后的文档,创建一个词典,将每个唯一的词元(Token)映射到一个从 0 开始的整数 ID。
词袋化:使用创建好的词典,将每一篇文档转换为其稀疏的词袋(BoW)向量。这个向量只记录文档中出现的词的 ID 及其频次,格式为
[(token_id, frequency), ...]。
这个最终生成的 BoW语料库,就是训练 TF-IDF、LDA 等模型的标准输入。
以上三步适用于 TF-IDF、LSA、LDA、NMF 等基于 BoW 的模型;不适用于 Word2Vec/FastText/Doc2Vec 等神经网络词向量模型。后者直接以分词后的句子序列(
list[list[str]])为输入,无需词袋化。详见下文“Word2Vec模型实战”。
| |
示例输出:
| |
三、TF-IDF与关键词权重
TF-IDF 是衡量一个词在文档中重要性的经典加权方法。下面将继续使用新闻标题的例子,演示如何计算其 TF-IDF 向量。
| |
输出:
| |
结果分析
- 原始的BoW向量只包含词频(整数),而 TF-IDF 向量则包含浮点数权重。
- 像“,”这样在多篇文档中都出现的词,其 TF-IDF 权重会较低;而在特定财经新闻中出现的“股市”、“降息”等词,权重会相对较高。
- 这个 TF-IDF 向量后续可用于计算文档相似度或作为机器学习模型的输入特征。
- 词典外的新词会被忽略,新文本的向量仅由词典中已有词构成。
- 本示例中标点“,”进入了词典并具有非零权重;如不希望其影响权重或相似度,建议在构建词典前移除标点/停用词。
- 你会看到新标题的TF-IDF仅包含“股市”和“,”两项,这是因为其他词为OOV被忽略。
四、LDA 与文档主题挖掘
主题模型(如 LDA)能从大量文档中自动发现隐藏的、无监督的主题。它的输入同样是词典和 BoW 语料库。
| |
输出:
| |
通过 LDA,不仅可以将一篇文档表示为一个主题概率分布(Gensim 默认以稀疏列表返回;例如 90% 体育、10% 财经),还能清晰地看到每个主题由哪些核心词构成。若新文本在词典中几乎无重叠词(doc2bow 为空),推断出的主题分布可能接近均匀(例如 2 个主题时约为 0.5/0.5)。
五、Word2Vec 模型实战
与前两者不同,Word2Vec 的输入直接是 分词后的句子列表 。它的目标不是加权或寻找主题,而是根据上下文学习每个词语本身内在的、稠密的语义向量。
目标与手段 需要强调的核心观点是:Word2Vec训练结束后,神经网络本身通常会被丢弃。其 最终目标 是获得那个高质量的 词向量查询表 ,它存储在
model.wv属性中。后续所有的应用,都是围绕这个查询表展开的。
5.1 模型训练与核心参数
训练 Word2Vec 模型非常直接,关键在于理解其核心参数的设置。继续使用新闻标题的例子。
| |
参数解析:
sentences: 输入的语料库,必须是list[list[str]]格式。vector_size: 词向量的维度。维度越高,能表达的语义信息越丰富,但计算量也越大。通常在50-300之间选择。window: 上下文窗口大小。表示在预测一个词时,需要考虑其前后多少个词。min_count: 最小词频过滤。任何在整个语料库中出现次数低于此值的词将被直接忽略。这是非常关键的一步,可以过滤掉大量噪音(如错别字、罕见词),并显著减小模型体积。sg: 选择训练算法。0表示 CBOW (根据上下文预测中心词);1表示 Skip-gram (根据中心词预测上下文)。hs: 选择优化策略。0表示使用 Negative Sampling (负采样);1表示使用 Hierarchical Softmax。当hs=0时,下面的negative参数才会生效。negative: 当使用负采样时,为每个正样本随机选择多少个负样本。通常在5-20之间。sample: 高频词二次重采样阈值。这是一个控制高频词(如“的”、“是”)被随机跳过的机制,目的是减少它们对模型训练的过多影响,并加快训练速度。值越小,高频词被跳过的概率越大。
5.2 使用词向量
模型训练完成后,所有的操作都围绕 model.wv 展开,用于探索词语间的语义关系。小语料示例下,相似度数值通常较低且不稳定,仅作演示参考。
| |
输出:
| |
5.3 模型的持久化
在实际项目中,通常会把训练好的词向量保存下来,避免重复训练。推荐只保存 KeyedVectors 对象,它更轻量、高效。
| |
示例输出:
| |
通过 Gensim,可以非常方便地训练自己的 Word2Vec 模型,并利用其强大的语义捕捉能力进行相似度计算、语义类比等 NLP 任务。