Word Embedding(词嵌入) 是自然语言处理(NLP)中的一项基础技术,它的核心目的是将人类语言中的词语转换成计算机能够理解和计算的数字形式------即向量(Vectors)。
简单来说,就是把字典里的每一个词映射到一个高维向量空间中的一个点。
为什么需要它?
在词嵌入出现之前,计算机通常使用 One-Hot 编码(独热编码) 来表示词。
- One-Hot 的问题: 假设你有 10,000 个词,每个词都是一个长为 10,000 的向量,其中只有一位是 1,其他全是 0。这种表示法既浪费空间(稀疏),又无法体现词与词之间的关系(例如,"苹果"和"橘子"在数学上是正交的,计算机看不出它们都是水果)。
- Word Embedding 的解决: 它将词压缩到一个低维(如 50 到 1000 维)、稠密的实数向量中。更重要的是,它能捕捉语义关系。
- 例子: 在向量空间中,King⃗−Man⃗+Woman⃗≈Queen⃗\vec{King} - \vec{Man} + \vec{Woman} \approx \vec{Queen}King −Man +Woman ≈Queen 。
Word2Vec 的两种训练方法
Word2Vec 有两种模型结构:
- CBOW(Continuous Bag of Words)
- Skip-gram
二者的区别核心在于:
| 模型 | 输入 | 输出 |
|---|---|---|
| CBOW | 上下文 | 预测中心词 |
| Skip-gram | 中心词 | 预测上下文 |
我们一步一步拆解。
CBOW(Continuous Bag-of-Words,连续词袋模型) 的训练过程就是 "用周围的词猜中心词" 。它在做的事情很像我们在做英语阅读理解时的完形填空。
第一阶段:构造训练数据(完形填空)
假设我们的语料库中有一句话:"The quick brown fox jumps over the lazy dog"。
设定窗口大小(Window Size)为 2(即取目标词前后各 2 个词)。
当我们的目标是预测中心词 "fox" 时:
- 上下文(输入 Input):
quick,brown(前两个),jumps,over(后两个)。 - 中心词(输出/标签 Target):
fox。
这样,我们就构造了一个训练样本:
([quick,brown,jumps,over],fox)([\text{quick}, \text{brown}, \text{jumps}, \text{over}], \text{fox})([quick,brown,jumps,over],fox)
第二阶段:网络前向传播(CBOW 的核心特征)
CBOW 的网络同样只有三层:输入层、投影层(隐藏层)和输出层。假设词表大小 ,词向量维度 。
1. 输入层 (Input Layer)
输入是 CCC 个上下文单词(这里 C=4C=4C=4 )。在计算机内部,它们实际上是 4 个 One-Hot 向量,或者直接是这 4 个词在词表中的索引 ID。
2. 投影层 (Projection Layer) ------ 这是与 Skip-gram 最大的区别!
我们有一个输入权重矩阵WinW_{in}Win (维度是 V×NV \times NV×N ),这就是我们要训练的词向量矩阵。
- 查表提取: 首先,根据输入的 4 个词的 ID,去矩阵 WinW_{in}Win 中把它们对应的 4 个 300 维的词向量全部"拎"出来。假设它们分别是 v⃗1,v⃗2,v⃗3,v⃗4\vec{v}_1, \vec{v}_2, \vec{v}_3, \vec{v}_4v 1,v 2,v 3,v 4 。
- 求和 / 求平均 (Averaging): CBOW 会将这 4 个向量相加并求平均 (或者直接求和),融合成一个单一的 300 维向量 h⃗\vec{h}h 。
h⃗=1C∑i=1Cv⃗i\vec{h} = \frac{1}{C} \sum_{i=1}^{C} \vec{v}_ih =C1i=1∑Cv i
- 为什么叫"词袋 (Bag-of-Words)"? 当你把向量加在一起时,词的顺序信息就完全丢失了 。不论输入是
[quick, brown]还是[brown, quick],加起来的平均向量 h⃗\vec{h}h 是一模一样的。就像把词扔进了一个袋子里搅匀了一样。
3. 输出层 (Output Layer)
现在,我们有了一个代表上下文综合语义的向量 h⃗\vec{h}h 。
我们将 与输出权重矩阵 WoutW_{out}Wout(维度是 N×VN \times VN×V)相乘,得到一个长度为 的得分向量(Logits)。这个得分表示了词表中每一个词成为那个被挖空的中心词的概率。
第三阶段:计算误差与优化
和 Skip-gram 一样,如果对输出层的 10000 个词做标准的 Softmax,计算量太大了。因此,CBOW 同样会使用负采样(Negative Sampling)或层级 Softmax 来加速。
负采样:
简单来说,负采样的核心思想是:把一个极其庞大的"多分类问题"(1万选1),强行降维打击,变成几个小巧的"二分类问题"(判断是或不是)。
我们继续用那句 The quick brown fox jumps over the lazy dog 为例,假设现在的中心词是 fox,真实的上下文词是 quick。
以下是负采样的完整通关过程:
第一步:锁定"正样本" (Positive Sample)
模型知道 fox 和 quick 是一对真实的上下文(因为它们在句子里挨着)。
- 任务: 我们要让机器判断 (
fox,quick) 是一对好朋友。 - 计算: 取出
fox的向量(输入矩阵的 300 维)和quick的向量(输出矩阵的 300 维),计算它们的点积 (Dot Product)。 - 目标: 我们希望这个点积越大越好。经过 Sigmoid 函数后,概率要无限接近于 1(Yes)。
第二步:随机抓壮丁,制造"负样本" (Negative Samples)
如果只给模型看好朋友,模型会变成傻子(它会觉得所有词都是好朋友)。所以必须给它看一些"不是朋友"的反例。
- 动作: 从包含 10000 个词的字典里,随机抽取 KKK 个 (通常是 5 到 20 个)跟
fox八竿子打不着的词。 - 假设: 我们抽到了
apple,car,math,sky,piano这 5 个词。 - 制造样本: 我们硬凑出 5 对关系:(
fox,apple), (fox,car), (fox,math), (fox,sky), (fox,piano)。这些就是负样本。 - 目标: 计算
fox分别与这 5 个词的点积。我们希望点积越小(或者负得越多)越好。经过 Sigmoid 函数后,这 5 个结果都要无限接近于 0(No)。
第三步:计算与参数更新
在没有负采样之前,你需要计算 10000 次才能更新一次参数(因为要算那个庞大的 Softmax 分母)。
用了负采样之后,计算量断崖式下降:
- 算 1 次正样本的 Sigmoid 误差。
- 算 5 次负样本的 Sigmoid 误差。
- 总共只算 6 次!
参数更新(梯度下降):
- 模型根据误差,稍微调整
fox和quick的向量,让它们在多维空间里互相靠近。 - 同时,模型调整
fox和apple,car等 5 个词的向量,把它们在空间里互相推开。
注意: 这一次更新,只有这 1+5+1=71 + 5 + 1 = 71+5+1=7 个词的向量发生了改变,字典里剩下的 9993 个词的向量在这一轮完全不参与运算,直接挂机休息!
进阶小细节:负样本是怎么"随机"抽的?
在真正的 Word2Vec 实现中,这 5 个负样本并不是绝对的等概率盲抽,而是根据词频来抽的。
- 高频词更容易当选负样本: 像
the,is,a这种词在语料库里出现极多,它们被抽作负样本的概率远大于生僻词(比如hippopotamus河马)。 - 为什么? 因为高频词几乎可以出现在任何词的旁边。模型需要更多次地被警告:"虽然
the经常出现,但它并不能代表fox的特殊语义特征,把它推开!" - (学术界通常使用词频的 次方来作为抽样概率,这是一个经验公式,能适当提高低频词被抽中的概率,防止它们永远被冷落)。
层级softmax
这是一个非常经典且优雅的算法设计!如果你对数据结构与算法 比较熟悉的话,理解层级 Softmax(Hierarchical Softmax,简称 HS)会非常有亲切感,因为它的核心本质就是把一个 O(N) 的线性查找问题,通过构建二叉树,优化成了 O(log N) 的树形查找问题。
如果说负采样是"随机抽查",那么层级 Softmax 就是一场极其高效的 "20个问题"猜词游戏。
1. 核心痛点回顾与思路转换
- 标准 Softmax 的痛点: 扁平化的一层结构。为了找出一个中心词
fox,需要计算上下文均值向量 与词表中所有 10000 个词的相似度得分,然后求和做分母。复杂度是 O(V)。 - 层级 Softmax 的思路: 既然直接从 10000 个词里选 1 个太慢,那我能不能每次只做 "二选一"(二分类)?经过有限次"二选一",最终精准定位到那个词?
2. 第一步:构建哈夫曼树 (Huffman Tree)
在正式开始训练之前,算法会根据语料库中所有单词的出现频率,构建一棵哈夫曼树。
- 叶子节点(Leaf Nodes): 树的每一个叶子节点,代表词表中的一个真实单词(共 10000 个)。
- 非叶子节点(Internal Nodes): 树的内节点(共有 9999 个)。注意!在 HS 中,原来那个庞大的输出矩阵 消失了!取而代之的是,这 9999 个内节点,每个节点都拥有一个自己的向量(我们暂称之为"路径向量")。
- 哈夫曼树的特性: 词频越高的词(如
the,is),离根节点越近,路径越短;词频越低的词(如fox,hippopotamus),离根节点越远,路径越长。
3. 第二步:猜词游戏(计算前向概率)
现在,模型拿到了上下文特征向量 h⃗\vec{h}h (比如基于 quick, brown, jumps, over 算出来的),它要如何预测目标词 fox 呢?
模型从这棵树的根节点开始,往下走:
- 在根节点(假设是节点 A): 模型要决定向左走还是向右走。
- 它用 h⃗\vec{h}h 和节点 A 的"路径向量"做点积,然后套用 Sigmoid 函数,算出一个概率(比如 0.7)。
- 规定:向左走的概率是 0.7,那么向右走的概率就是 1 - 0.7 = 0.3。
- 假设通往
fox的正确路径是向左,模型记录下这一步的概率:0.7。
- 来到下一个节点(假设是节点 B): 模型再次面临左右抉择。
- 它用 h⃗\vec{h}h 和节点 B 的"路径向量"做点积,再过 Sigmoid,算出向左的概率是 0.2(向右是 0.8)。
- 假设通往
fox的正确路径是向右,模型记录下这一步的概率:0.8。
- 走到叶子节点: 就这样一路做二元判断,直到走到叶子节点
fox。
最终 fox 的概率怎么算?
就是把一路上所有做出的正确选择的概率乘起来 !
P(fox)=0.7×0.8×⋯×最后一步的概率P(\text{fox}) = 0.7 \times 0.8 \times \dots \times \text{最后一步的概率}P(fox)=0.7×0.8×⋯×最后一步的概率
为什么这很神奇?
在这个过程中,我们根本没有计算 apple, car 等其他 9999 个单词的得分!我们只计算了从根节点走到 fox 所经过的几个内节点。对于 10000 个词的词表,树的深度大约是 log2(10000)≈14\log_2(10000) \approx 14log2(10000)≈14。。
计算量从 10000 次点积,骤降到了 14 次点积!
4. 第三步:反向传播与参数更新
训练的时候,模型依然是"拿着答案做题"。
- 我们知道目标词是
fox,也知道从根节点到fox的正确路径(比如:左 -> 右 -> 左 -> 右...)。 - 模型在沿途的每一个内节点做判断。如果它在某个节点本该向左(概率应趋近于 1),但它算出来向左的概率只有 0.2,误差就产生了。
- 梯度下降触发:
- 调整沿途经过的那些内节点的"路径向量" ,让它们下次面临同样的 h⃗\vec{h}h 时,能给出更准确的左右引导。
- 沿着路径把误差传导回去,调整输入层的 WinW_{in}Win,微调上下文那几个词的词向量。
5. 总结:层级 Softmax vs 负采样
虽然它们都是为了解决 Softmax 计算量过大的问题,但思路截然不同,在实际应用中也各有千秋:
| 维度 | 负采样 (Negative Sampling) | 层级 Softmax (Hierarchical Softmax) |
|---|---|---|
| 底层逻辑 | 采样思维:把多分类变成 1个正例 + K个负例的二分类。 | 树形搜索:把 O(N) 的线性查找变成 O(log N) 的二叉树路径决策。 |
| 生僻词处理 | 对生僻词不太友好(因为抽不到它们做负样本,也很少作为正样本出现)。 | 对生僻词更友好(只要它在树上,总有一条确定的路径可以精准地更新到它,不会被彻底忽略)。 |
| 代码实现 | 相对简单,矩阵运算容易并行化。 | 需要构建和维护一棵庞大的二叉树,逻辑较复杂。 |
在早期的 Word2Vec 源码中,这两个选项都是提供的。如果你追求极致的速度和对高频词的优良语义,负采样是首选;如果你非常看重低频词、长尾词的表达,并且想体验一下将数据结构之美(哈夫曼树)融入神经网络的快感,层级 Softmax 是非常完美的范例。
第四阶段:反向传播与参数更新
计算出误差后,利用梯度下降算法往回更新参数:
- 更新 : 稍微调整目标词
fox和几个负样本词的输出向量。 - 更新 : 这是关键!误差会反向传递给那个平均向量 。然后,这个梯度会被等分地传递给当初参与求平均的那 4 个上下文词向量 。
换句话说,quick,brown,jumps,over这 4 个词的词向量都会根据这次预测的结果被微微修改,让它们在空间中的组合位置更符合语言规律。
如果说 CBOW 是"给你周围的同学,猜你是谁"(完形填空),那么 Skip-gram 就是"给你一个人,猜他周围坐的都是谁"(发散联想)。
句子依然是:"The quick brown fox jumps over the lazy dog",当前聚焦的中心词是 fox,窗口大小为 2。
第一阶段:准备"开卷考题"(
在 CBOW 中,老师把 4 个上下文词打包成 1 道题:( [quick, brown, jumps, over] -> fox )。
而在 Skip-gram 中,老师的策略变了。他把这 1 个中心词,拆分成了 4 道独立的单选题:
- 考题 1:已知
fox,猜它左边第二个词是不是quick? - 考题 2:已知
fox,猜它左边第一个词是不是brown? - 考题 3:已知
fox,猜它右边第一个词是不是jumps? - 考题 4:已知
fox,猜它右边第二个词是不是over?
差异点 1: Skip-gram 的训练样本对是(中心词,上下文词)(中心词, 上下文词)(中心词,上下文词) 。对于同一个中心词,它会生成多个独立的训练样本。
第二阶段:学生审题与特征提取(前向传播)
模型手里依然拿着那个正在被打磨的输入字典 。
- 查表 (Lookup): 模型看到输入是
fox,直接去 里把fox的 300 维词向量 抽出来。 - 没有求平均! 因为输入只有一个词,所以这个 300 维的 直接就作为了这一轮的"特征向量" 。
差异点 2: CBOW 需要把多个词向量相加求平均(丢失了词的具体细节);而 Skip-gram 始终保留着单一中心词原汁原味的向量。
第三阶段:老师"对答案"(前向传播 - 多次负采样)
我们以考题 1 (fox -> quick) 为例,采用负采样模式:
老师拿着输出矩阵 :
- 正样本: 拔出
quick的输出向量 ,要求模型计算它和 的点积,尽量接近 1。 - 负样本: 随机抽出 5 个错词(如
apple,car等),拔出它们的输出向量,要求模型计算它们和 的点积,尽量接近 0。 - 计算误差。
注意: 这仅仅是考题 1!接下来,针对考题 2 (brown)、考题 3 (jumps)、考题 4 (over),模型都要分别重复一遍上述的"1 正 + 5 负"的打分和计算误差过程。
第四阶段:强行纠错,更新大脑(反向传播)
随着每一道考题的误差产生,梯度下降开始疯狂更新参数:
- 修改 (老师的辅助工具):
quick,brown,jumps,over这 4 个正确答案的输出向量被依次拉近 ;同时那 20 个(4题 5个)无辜被抽中的负样本错词,被狠狠推远。 - 核心更新 (学生的字典):
误差信号顺着网络反向传回。谁是输入,谁就挨打(被修改)!
在 Skip-gram 中,输入始终是fox。
因此, 里的fox向量,会因为quick的误差被微调一次,接着又因为brown的误差被微调一次,以此类推...... 在这一个窗口的滑动中,fox的词向量被连续精雕细琢了 4 次!
总结:Skip-gram 与 CBOW 的巅峰对决
把整个流程走完,我们就可以非常清晰地梳理出两者的核心区别了:
| 维度 | CBOW (连续词袋) | Skip-gram (跳字模型) |
|---|---|---|
| 预测方向 | 上下文 中心词 (多对一) | 中心词 上下文 (一对多) |
| 特征处理 | 输入多个词,需要求平均 (抹平了细节) | 输入一个词,无需融合 (保留了独立特征) |
| 更新频率 (针对同一窗口) | 中心词(作为WoutW_{out}Wout )被更新 1 次; 上下文(作为WinW_{in}Win )共同被更新 1 次。 | 中心词(作为 WinW_{in}Win)被连续更新 CCC 次 (CCC为上下文数量); 上下文(作为WoutW_{out}Wout )各被更新 1 次。 |
| 训练速度 | 快! 几个词一锅端,算一次就完事了。 | 慢! 1 个词要分别去和好几个词算误差,计算量成倍增加。 |
| 对生僻词(低频词)的态度 | 不友好。 生僻词经常被高频词(如 the, a)"平均"掉,声音被淹没在词袋里。 | 极其友好! 哪怕 fox 是个生僻词,只要它作为中心词出现一次,它就会获得多次一对一的、不受干扰的精细更新。 |
| 整体效果 | 适合小型语料库,对高频词表达较好。 | 目前的主流选择! 在大型语料库上,能捕捉到更细腻的语义关系。 |
结语
简单来说:
- CBOW 喜欢"吃大锅饭",上下文揉在一起算,速度快,但细节容易丢。
- Skip-gram 喜欢"开小灶",中心词和周围的词一对一地建立联系,虽然计算量大、训练慢,但雕琢出来的词向量质量极高,尤其是对那些不常出现的词。