4、Word2Vec入门:CBOW与Skip-gram架构、负采样与层次Softmax、Gensim库快速上手

说实话,Word2Vec 是我入行 NLP 时第一个让我觉得「哇,原来词还能这么玩」的技术。2013 年 Google 放出这篇的时候,整个圈子都震了一下——一个两层的神经网络,居然能把「国王 - 男人 + 女人 ≈ 女王」这种语义关系给学出来。

这一章,我们就把它拆开看看。

4.1 核心思想:用上下文学词义

Word2Vec 的直觉其实很简单:一个词的意思,由它周围的词决定。你想想看,如果我说「我今天去____买了杯咖啡」,你大概率会填「星巴克」或者「瑞幸」,而不是「冰箱」或「轮胎」。这就是分布语义学的核心假设。

Word2Vec 有两种训练模式,它们正好是互逆的:

  • CBOW(Continuous Bag of Words):用上下文词预测中心词
  • Skip-gram:用中心词预测上下文词

我个人习惯这样记:CBOW 适合高频词,训练快;Skip-gram 对低频词更友好,但慢一些。选哪个?看你的数据量和任务场景。

一句话总结:CBOW 是「看周围猜中间」,Skip-gram 是「看中间猜周围」。

4.2 CBOW 与 Skip-gram 架构详解

4.2.1 CBOW 架构

CBOW 的输入是上下文词的 one-hot 向量,输出是中心词的概率分布。中间只有一层隐藏层——嗯,就是那个我们最终要拿来做词向量的矩阵。

流程是这样的:

  1. 取窗口大小(比如 2),拿到中心词前后各 2 个词
  2. 把这些词的 one-hot 向量分别乘以输入权重矩阵 W(形状:V×N)
  3. 得到 N 维向量后取平均
  4. 再乘以输出权重矩阵 W'(形状:N×V)
  5. 经过 softmax 得到 V 个词的概率分布
  6. 用交叉熵损失更新 W 和 W'

我在项目中遇到过一个问题:窗口大小怎么选?太小了学不到全局语义,太大了又引入噪声。一般经验是 5-10,但如果你做的是专业领域(比如医疗、法律),窗口可以适当缩小,因为专业术语的上下文通常更紧凑。

4.2.2 Skip-gram 架构

Skip-gram 正好反过来。输入是中心词,输出是它周围每个词的概率。注意,这里每个上下文词是独立预测的,所以损失是多个交叉熵的和。

Skip-gram 的优点是:对低频词更友好。为什么?因为一个低频词只要出现一次,它就会强迫模型去预测它周围的多个词,相当于一次训练产生了多个样本。CBOW 则相反,多个上下文词取平均后,低频词的信号容易被稀释。

对比维度 CBOW Skip-gram
输入 → 输出 上下文 → 中心词 中心词 → 上下文
训练速度 快 慢(约 2-3 倍)
低频词效果 一般 好
高频词效果 好 一般
适用场景 大规模语料、高频词为主 小规模语料、低频词重要
我的建议:如果你刚开始做 Embedding,数据量在百万级以下,直接用 Skip-gram。数据量上亿了,再考虑 CBOW 提速。

4.3 负采样与层次 Softmax

好,现在问题来了。上面说的 softmax 要对 V 个词(通常是几万到几十万)做归一化,每次更新都要算一遍,太慢了。怎么办?

Word2Vec 给了两个优化方案:负采样和层次 Softmax。

4.3.1 负采样(Negative Sampling)

负采样的思路很巧妙:我不去算所有词的概率,而是只挑几个「负样本」来算。

具体来说:

  • 正样本:中心词和它真实的上下文词
  • 负样本:中心词和随机挑的几个「非上下文词」
  • 目标:让正样本的得分高,负样本的得分低

负样本怎么挑?不是均匀采样,而是按词频的 3/4 次方加权。为什么是 3/4?实验出来的,能让低频词被采到的概率稍微高一点。

我曾经踩过一个坑:负样本数量设太大。默认是 5-10,我试过 20,结果训练慢了一倍,效果反而没提升。后来我养成了习惯:小数据集用 5,大数据集用 10,再多就没必要了。

注意:负采样只优化了输出层的计算,输入层的矩阵更新还是全量的。不过输入层更新只涉及非零的 one-hot 位置,所以实际计算量不大。

4.3.2 层次 Softmax(Hierarchical Softmax)

层次 Softmax 是另一种思路。它用 Huffman 树把 V 分类问题变成了 log₂(V) 次二分类问题。

怎么做的?

  1. 根据词频构建 Huffman 树,高频词路径短
  2. 每个叶子节点对应一个词
  3. 每个内部节点是一个二分类器(sigmoid)
  4. 预测时,从根节点走到目标词,路径上的概率相乘

这样,计算复杂度从 O(V) 降到了 O(log V)。对于 10 万词的词表,从 10 万次计算降到约 17 次,效果非常明显。

不过层次 Softmax 有个限制:它假设词之间是互斥的(一个位置只能有一个词),这在某些场景下不太自然。负采样就没有这个假设。

优化方法 核心思想 复杂度 适用场景
负采样 只更新少量负样本 O(k),k 为负样本数 通用,低频词友好
层次 Softmax 用 Huffman 树做二分类 O(log V) 高频词多、词表大
我的选择:Gensim 默认用负采样。除非你的词表超过 50 万且高频词集中,否则不用换。

4.4 架构对比图

下面这张图把 CBOW 和 Skip-gram 的流程放在一起对比,方便你理解它们的差异。

CBOW vs Skip-gram 架构对比 CBOW w(t-2) w(t-1) w(t+1) w(t+2) 投影层(平均) 输出 w(t) Skip-gram w(t) 投影层 w(t-2) w(t-1) w(t+1) CBOW:多个上下文词 → 平均 → 预测中心词 Skip-gram:一个中心词 → 分别预测每个上下文词

4.5 Gensim 库快速上手

理论说完了,来点实际的。Gensim 是 Python 里最成熟的 Word2Vec 实现,API 设计得很干净。

4.5.1 安装与基本使用

pip install gensim

训练一个最简单的模型:

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

# 假设你有一些句子
sentences = [
    "我 今天 去 星巴克 买 了 一杯 拿铁",
    "咖啡 的 味道 很 香",
    "我 喜欢 喝 美式 咖啡"
]

# 分词(这里用空格预分好,实际要用 jieba 等工具)
tokenized = [s.split() for s in sentences]

# 训练模型
model = Word2Vec(
    sentences=tokenized,
    vector_size=100,      # 词向量维度
    window=5,             # 窗口大小
    min_count=1,          # 忽略出现次数少于该值的词
    sg=0,                 # 0=CBOW, 1=Skip-gram
    hs=0,                 # 0=负采样, 1=层次Softmax
    negative=5,           # 负样本数
    epochs=5              # 迭代次数
)

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

# 找相似词
print(model.wv.most_similar('咖啡', topn=3))
小技巧:Gensim 的 simple_preprocess 函数可以帮你做基础的分词和清洗,但中文场景下效果一般。建议用 jieba 或 pkuseg 先分好词,再喂给 Gensim。

4.5.2 关键参数详解

参数 默认值 说明 我的建议
vector_size 100 词向量维度 小数据用 100,大数据用 300
window 5 上下文窗口大小 5-10,专业领域可缩小
min_count 5 最低词频过滤 数据量大时设 5-10,小数据设 1
sg 0 0=CBOW, 1=Skip-gram 默认 CBOW,低频词多时用 Skip-gram
hs 0 0=负采样, 1=层次Softmax 保持默认 0
negative 5 负样本数量 5-10,不要超过 20
epochs 5 训练轮数 5-20,看 loss 收敛情况

4.5.3 模型保存与加载

# 保存
model.save("word2vec.model")

# 加载
model = Word2Vec.load("word2vec.model")

# 只保存词向量(轻量版)
model.wv.save_word2vec_format("vectors.txt", binary=False)
踩坑提醒:我曾经在保存模型时用了 save_word2vec_format,结果加载后没法继续训练了。如果你还要继续训练,一定要用 model.save() 保存完整模型。

4.5.4 常用操作

# 找相似词
model.wv.most_similar("咖啡", topn=10)

# 找不匹配的词
model.wv.doesnt_match(["星巴克", "瑞幸", "咖啡", "冰箱"])

# 词向量运算(经典例子)
result = model.wv.most_similar(positive=["国王", "女人"], negative=["男人"])
print(result)  # 应该接近「女王」

# 获取词向量
vec = model.wv["咖啡"]

# 计算两个词的相似度
sim = model.wv.similarity("咖啡", "拿铁")

4.6 本章小结

这一章我们走通了 Word2Vec 的核心脉络:

  • CBOW 用上下文预测中心词,适合高频词和大数据
  • Skip-gram 用中心词预测上下文,对低频词更友好
  • 负采样 通过只更新少量负样本大幅加速训练
  • 层次 Softmax 用 Huffman 树把复杂度从 O(V) 降到 O(log V)
  • Gensim 提供了开箱即用的实现,参数调优是关键