2、Word2Vec详解:CBOW与Skip-gram模型架构,负采样与层次Softmax
好,咱们今天来聊聊Word2Vec。说实话,在2013年之前,做词向量基本就是靠SVD分解共现矩阵,那玩意儿算起来慢得要命,而且效果嘛……嗯,只能说差强人意。直到Mikolov他们搞出了Word2Vec,整个NLP领域才算真正找到了一个高效又好用的词表示方法。
我个人习惯把Word2Vec看作是「词向量时代的开山之作」。虽然现在大家都用BERT了,但Word2Vec里的很多思想——比如负采样、层次Softmax——至今还在各种模型里发光发热。你想想看,一个2013年的技术能活到现在,本身就说明它有多经典。
2.1 两种核心架构:CBOW与Skip-gram
Word2Vec其实就两个模型:CBOW和Skip-gram。它们的目标正好相反。
一句话总结:
- CBOW:用上下文词预测中心词
- Skip-gram:用中心词预测上下文词
2.1.1 CBOW(Continuous Bag of Words)
CBOW的思路很直接。假设我们有一个句子:「我 喜欢 吃 苹果」。窗口大小设为2,那么对于中心词「吃」,它的上下文就是「我」「喜欢」「苹果」这四个词(左右各两个)。CBOW要做的,就是根据这四个词,猜出中间那个词是「吃」。
具体怎么做的呢?
- 每个上下文词都有一个向量表示(随机初始化)
- 把这些向量加起来(或者取平均),得到一个上下文向量
- 用这个向量去预测中心词
我在项目中遇到过一个问题:CBOW对高频词的效果特别好,但对低频词就有点力不从心。为什么呢?因为低频词出现的次数少,上下文信息不够丰富,模型很难学准。所以如果你的语料里有很多低频词,我建议优先考虑Skip-gram。
2.1.2 Skip-gram
Skip-gram正好反过来。还是那个句子,给定中心词「吃」,模型要预测它周围的词「我」「喜欢」「苹果」。
你可能会问:「这俩有啥本质区别?」
嗯,区别大了。CBOW是把多个词的信息压缩成一个向量去预测一个词,相当于「多对一」。Skip-gram是一个词去预测多个词,相当于「一对多」。从信息论的角度看,Skip-gram的任务更难,所以它学到的词向量质量也更高——尤其是对低频词。
我曾经做过一个实验:用同样的语料训练CBOW和Skip-gram,然后做词类比任务(比如「国王-男人+女人=女王」)。Skip-gram的准确率比CBOW高了将近10个百分点。当然,代价就是训练时间更长。
我的选择建议:
- 语料大、计算资源有限 → 用CBOW(快,效果也不错)
- 语料小、对低频词有要求 → 用Skip-gram(慢但效果好)
2.2 训练加速:负采样与层次Softmax
好,现在问题来了。不管是CBOW还是Skip-gram,最后一步都是做Softmax。如果词表有10万个词,每次训练都要算10万个词的Softmax概率——这谁顶得住啊?
所以Word2Vec用了两个技巧来加速:负采样和层次Softmax。
2.2.1 负采样(Negative Sampling)
负采样的思路特别巧妙。它把原来的多分类问题,变成了二分类问题。
什么意思呢?
原来我们要从10万个词里找出正确的那个词。现在呢,我们只判断「这个词是不是正确的」。每次训练,我们拿一个正样本(正确的词)和几个负样本(随机选的词),让模型学会区分它们。
比如在Skip-gram里,给定中心词「吃」,正样本是「苹果」,负样本是随机抽的几个词,比如「跑步」「电脑」「天空」。模型只需要学会:
- 「吃」和「苹果」应该靠近
- 「吃」和「跑步」应该远离
这就简单多了,对吧?
负样本怎么选?不是均匀随机抽,而是按照词频的3/4次方来抽。这个3/4次方是我觉得Word2Vec里最精妙的设计之一——它让高频词被抽到的概率降低了一些,低频词被抽到的概率提高了一些,这样模型就不会只盯着高频词学。
避坑指南:
我曾经在训练一个中文语料时,负样本数量设成了5(默认值)。结果发现模型对「的」「了」这类高频虚词的区分能力很差。后来我把负样本数调到了15,效果明显改善。所以我的建议是:如果语料里高频词很多,适当增加负样本数量。
2.2.2 层次Softmax(Hierarchical Softmax)
层次Softmax是另一种思路。它用一棵哈夫曼树来代替原来的Softmax。
这棵树长什么样呢?叶子节点是词表中的每个词,内部节点是二分类器。从根节点到某个叶子节点的路径上,每个内部节点都要做一次二分类判断:向左走还是向右走?
这样,原来需要计算10万个词的概率,现在只需要计算log2(10万) ≈ 17次二分类。速度提升了将近6000倍。
哈夫曼树的构造有个特点:高频词的路径短,低频词的路径长。这意味着高频词只需要很少的几次判断就能找到,而低频词需要更多次。这其实挺合理的——高频词我们经常遇到,当然要优先优化它们的速度。
| 方法 | 计算复杂度 | 适用场景 |
|---|---|---|
| 原始Softmax | O(V) | 词表小(V < 1000) |
| 负采样 | O(K) | 词表大,对低频词有要求 |
| 层次Softmax | O(log V) | 词表大,高频词多 |
(V是词表大小,K是负样本数量)
2.3 代码实战:用Gensim训练Word2Vec
说了这么多理论,咱们来点实际的。用Gensim训练Word2Vec其实特别简单,几行代码就搞定。
from gensim.models import Word2Vec
from gensim.utils import simple_preprocess
# 准备语料(假设已经分好词)
sentences = [
['我', '喜欢', '吃', '苹果'],
['他', '喜欢', '打', '篮球'],
['她', '喜欢', '看', '电影'],
# ... 更多句子
]
# 训练CBOW模型
model_cbow = Word2Vec(
sentences,
vector_size=100, # 词向量维度
window=5, # 窗口大小
min_count=5, # 忽略出现次数少于5的词
sg=0, # 0表示CBOW,1表示Skip-gram
hs=0, # 0表示负采样,1表示层次Softmax
negative=5, # 负样本数量
epochs=5, # 训练轮数
workers=4 # 并行线程数
)
# 训练Skip-gram模型
model_sg = Word2Vec(
sentences,
vector_size=100,
window=5,
min_count=5,
sg=1, # 使用Skip-gram
hs=0,
negative=5,
epochs=5,
workers=4
)
# 查看词向量
print(model_cbow.wv['苹果']) # 输出100维的向量
# 找相似词
print(model_cbow.wv.most_similar('苹果', topn=10))
你看,就这么简单。但要注意几个参数:
min_count:我一般设5-10。太小了低频词学不好,太大了会丢失信息negative:默认5。如果语料大,可以试试10-15epochs:默认5。语料小的话可以多跑几轮,但别超过20,容易过拟合
一个小技巧:
训练完成后,记得调用model.save()保存模型。下次直接用Word2Vec.load()加载,不用重新训练。我当年第一次跑Word2Vec时忘了保存,结果语料太大重新跑了3个小时……嗯,从那以后我再也没忘过。
2.4 总结
Word2Vec虽然已经是「老技术」了,但它的核心思想——用上下文预测词、用词预测上下文、负采样、层次Softmax——至今仍然影响着NLP的发展。你想想看,BERT里的Masked Language Model,本质上不就是CBOW的升级版吗?
所以,把Word2Vec吃透,对你理解后续的ELMo、BERT、GPT都有很大帮助。下一章我们会讲GloVe,它从另一个角度(共现矩阵)来解决词向量问题。到时候你会发现,不同的方法其实殊途同归。
好,今天就到这儿。有什么问题欢迎在评论区留言,咱们一起讨论。