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要做的,就是根据这四个词,猜出中间那个词是「吃」。

具体怎么做的呢?

  1. 每个上下文词都有一个向量表示(随机初始化)
  2. 把这些向量加起来(或者取平均),得到一个上下文向量
  3. 用这个向量去预测中心词

我在项目中遇到过一个问题: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-15
  • epochs:默认5。语料小的话可以多跑几轮,但别超过20,容易过拟合

一个小技巧:

训练完成后,记得调用model.save()保存模型。下次直接用Word2Vec.load()加载,不用重新训练。我当年第一次跑Word2Vec时忘了保存,结果语料太大重新跑了3个小时……嗯,从那以后我再也没忘过。

2.4 总结

Word2Vec虽然已经是「老技术」了,但它的核心思想——用上下文预测词、用词预测上下文、负采样、层次Softmax——至今仍然影响着NLP的发展。你想想看,BERT里的Masked Language Model,本质上不就是CBOW的升级版吗?

所以,把Word2Vec吃透,对你理解后续的ELMo、BERT、GPT都有很大帮助。下一章我们会讲GloVe,它从另一个角度(共现矩阵)来解决词向量问题。到时候你会发现,不同的方法其实殊途同归。

好,今天就到这儿。有什么问题欢迎在评论区留言,咱们一起讨论。