第2章:Word2Vec原理:CBOW与Skip-gram模型架构详解、负采样与层次Softmax

好,咱们接着聊。上一章我们把Embedding的来龙去脉理了一遍,这一章,我带你深入Word2Vec这个经典模型。说实话,Word2Vec虽然已经是2013年的老技术了,但它的思想至今影响着NLP的方方面面。我个人习惯把它当作理解“分布式表示”的入门必修课。

2.1 从语言模型到词向量

先问个问题:为什么要有Word2Vec?

其实很简单。在它之前,我们做词向量主要靠共现矩阵+SVD分解。这种方法的问题是:矩阵太大了,而且训练起来慢得要命。我记得2015年刚入行时,用SVD做一个中等规模的语料,服务器跑了整整两天。嗯,那时候我就想,一定有更聪明的办法。

Word2Vec的思路很直接:用预测任务来学词向量。说白了,就是让模型去猜某个词在上下文里出现的概率。猜对了,词向量就学好了。这个想法,现在看很自然,但在当时确实是个突破。

2.2 CBOW模型:用上下文预测中心词

CBOW,全称Continuous Bag of Words,连续词袋模型。名字挺唬人,其实逻辑很简单:给你一堆上下文词,让你猜中间那个词是什么

举个例子:

句子: "我 喜欢 吃 苹果"
窗口大小 = 2
上下文: ["我", "喜欢", "吃", "苹果"] → 预测中心词?不对
实际: 上下文 ["我", "喜欢", "苹果", "吗"] → 预测中心词 "吃"

模型结构也不复杂:

  • 输入层:上下文词的one-hot向量
  • 隐藏层:其实就是Embedding层,把one-hot映射成稠密向量
  • 输出层:Softmax分类器,预测中心词的概率

这里有个关键点:CBOW把多个上下文词的向量做了平均。你想想看,这样做的结果是,每个上下文词对中心词的贡献是平等的。我在项目中遇到过一个问题:如果语料里有很多停用词,比如“的”、“了”,它们会拉低重要词的信号。所以后来我做预处理时,都会先过滤掉这些词。

核心公式

隐藏层输出 h = (1/C) * Σ(v_w_i),其中C是上下文词数量,v_w_i是第i个上下文词的向量

输出层概率 P(w_t | context) = softmax(h · W_out)

2.3 Skip-gram模型:用中心词预测上下文

Skip-gram和CBOW正好反过来:给你中心词,让你猜它周围的词

还是那个例子:

句子: "我 喜欢 吃 苹果"
中心词: "吃"
预测: ["我", "喜欢", "苹果", "吗"] 中的每一个

模型结构:

  • 输入层:中心词的one-hot向量
  • 隐藏层:Embedding层,和CBOW共享
  • 输出层:多个Softmax,每个预测一个上下文词

Skip-gram有个特点:它对低频词更友好。为什么?因为每个中心词都要预测多个上下文词,相当于低频词被反复训练。我个人的经验是,如果语料里有很多专业术语或生僻词,Skip-gram的效果通常比CBOW好。

我的建议

  • 语料量大、高频词多 → 用CBOW,训练更快
  • 语料量小、低频词多 → 用Skip-gram,效果更好

2.4 负采样:让训练快10倍的技巧

好,模型结构讲完了。但你有没有想过一个问题:输出层的Softmax要对整个词表做归一化,如果词表有10万个词,每次训练都要算10万次指数运算。这谁受得了?

负采样(Negative Sampling)就是来解决这个问题的。

它的思路很巧妙:不计算所有词的概率,只计算正样本和少量负样本

具体怎么做?

  1. 从语料中取一个正样本对(中心词,上下文词)
  2. 随机采样k个负样本对(中心词,随机词)
  3. 让模型学会区分正负样本

k值怎么选?我一般这样设置:

语料规模 k值 说明
小语料(<100万词) 5-10 负样本太少,模型学不好
大语料(>1亿词) 2-5 负样本太多,反而浪费

避坑指南

我曾经在某个项目里把k值设成了20,结果训练时间翻了一倍,效果却没提升。后来才发现,负采样不是越多越好。嗯,这个坑我替你们踩过了。

2.5 层次Softmax:用树结构加速

除了负采样,还有另一种加速方法:层次Softmax(Hierarchical Softmax)。

它的想法是:把词表组织成一棵二叉树,每个叶子节点对应一个词。这样,计算一个词的概率,只需要沿着树根走到叶子节点,走log(V)步就行。

举个例子:

词表大小 V = 10000
普通Softmax:计算10000次
层次Softmax:计算log2(10000) ≈ 14次

效率提升很明显,对吧?

但层次Softmax有个缺点:树的构建方式会影响效果。常用的方法是Huffman树,把高频词放在靠近根节点的位置。这样高频词的计算路径更短,整体效率更高。

我个人更倾向于用负采样。原因很简单:层次Softmax的实现比较复杂,而且对词表变化不灵活。如果你要动态添加新词,树结构就得重建。而负采样就灵活多了。

2.6 实战:用Gensim训练Word2Vec

理论讲完了,咱们来点实际的。用Gensim训练Word2Vec,代码其实很简单:

from gensim.models import Word2Vec
from gensim.utils import simple_preprocess

# 准备语料
sentences = [
    "我 喜欢 吃 苹果",
    "苹果 是 一种 水果",
    "我 每天 都 吃 一个 苹果"
]

# 分词
corpus = [simple_preprocess(sent) for sent in sentences]

# 训练模型
model = Word2Vec(
    sentences=corpus,
    vector_size=100,      # 词向量维度
    window=5,             # 上下文窗口大小
    min_count=1,          # 最小词频
    sg=1,                 # 0=CBOW, 1=Skip-gram
    negative=5,           # 负采样数量,0表示使用层次Softmax
    epochs=10
)

# 查看词向量
print(model.wv['苹果'])  # 输出100维向量

# 找相似词
print(model.wv.most_similar('苹果'))

这里有几个参数值得注意:

  • sg:选0还是1?我建议先试Skip-gram,效果不好再换CBOW
  • negative:设成5-10通常就够用了
  • min_count:这个参数很关键。设得太小,低频词会引入噪声;设得太大,稀有词就学不到

经验之谈

我在做中文词向量时,发现分词质量直接影响结果。如果分词错了,比如把“机器学习”分成“机器”和“学习”,那学出来的向量就不太对。所以,预处理阶段多花点时间,后面会省很多事。

2.7 小结

这一章我们聊了:

  • CBOW:用上下文预测中心词,适合大语料
  • Skip-gram:用中心词预测上下文,对低频词更友好
  • 负采样:用少量负样本加速训练,灵活实用
  • 层次Softmax:用树结构加速,适合静态词表

下一章,我们会讲GloVe和FastText,看看它们是怎么在Word2Vec基础上做改进的。到时候你会发现,很多思路其实都是相通的。

嗯,今天就到这儿。如果你在训练过程中遇到什么问题,欢迎随时交流。