Word2Vec:从词向量训练到神经网络分类器
前面学习文本处理时,我们已经知道:自然语言不能直接送进模型,必须先变成数字。
最朴素的方法是 One-hot。比如词表里有 10000 个词,每个词就用一个长度为 10000 的向量表示,属于自己的位置为 1,其他位置为 0。
但 One-hot 有两个明显问题:
- 向量太稀疏,绝大多数位置都是 0。
- 词和词之间没有语义关系,“国王”和“王后”在 One-hot 空间里并不比“国王”和“香蕉”更接近。
word2vec 要解决的就是这个问题:
把词表示成低维、稠密、带有语义关系的向量。
例如:
国王 -> [0.32, -0.18, 0.71, ...]
王后 -> [0.29, -0.15, 0.69, ...]
香蕉 -> [-0.44, 0.83, 0.12, ...]
如果训练得好,“国王”和“王后”的向量会更接近,而“国王”和“香蕉”的向量会更远。
这篇文章就从 word2vec 出发,理解词向量是怎么训练出来的、怎么改进和评价词向量,并进一步过渡到神经网络文本分类器。
1. 词向量到底是什么
词向量,也叫 word embedding,本质上就是给每个词分配一个可学习的向量。
如果词表大小是 $V$,词向量维度是 $d$,那么所有词向量可以组成一个矩阵:
$$
E \in \mathbb{R}^{V \times d}
$$
其中:
- $V$ 是词表大小。
- $d$ 是词向量维度,例如 50、100、300。
- 矩阵第 $i$ 行就是第 $i$ 个词的向量。
如果“喜欢”在词表中的编号是 12,那么:
embedding[12]
就是“喜欢”这个词的向量。
在神经网络里,Embedding 层看起来像一层网络,但它本质上更像查表:
词 id -> 去词向量矩阵中取对应行 -> 得到词向量
例如:
import torch
import torch.nn as nn
embedding = nn.Embedding(num_embeddings=10000, embedding_dim=100)
word_ids = torch.tensor([12, 35, 87])
vectors = embedding(word_ids)
print(vectors.shape) # torch.Size([3, 100])
这里输入 3 个词 id,输出 3 个 100 维词向量。
2. word2vec 的核心思想
word2vec 的核心假设来自语言学中的分布式假说:
一个词的含义,可以从它经常出现的上下文中推断出来。
比如:
我 喜欢 自然语言 处理
我 喜欢 机器 学习
我 喜欢 深度 学习
“自然语言”“机器”“深度”经常和“学习”“处理”“模型”等词一起出现。
随着语料越来越多,模型会逐渐发现:
- 经常出现在相似上下文中的词,语义可能相近。
- 经常互相搭配的词,向量应该具有较高相似度。
- 很少一起出现、上下文差异很大的词,向量应该相对远一些。
word2vec 并不是靠人工告诉模型“国王”和“王后”相似。
它是通过大量文本中的共现关系,把这种相似性训练出来。
3. 训练样本从哪里来
word2vec 不需要人工标注情感类别、主题类别。
它可以直接从普通文本中构造训练样本。
假设有一句话:
我 喜欢 自然语言 处理
如果窗口大小为 1,那么每个词只看左右各 1 个词。
对于中心词“喜欢”:
上下文:我,自然语言
中心词:喜欢
对于中心词“自然语言”:
上下文:喜欢,处理
中心词:自然语言
也就是说,word2vec 会用滑动窗口从文本中自动构造很多训练样本。
4. CBOW 和 Skip-gram
word2vec 常见有两种训练方式:
- CBOW:Continuous Bag of Words
- Skip-gram
它们的区别在于预测方向不同。
4.1 CBOW:用上下文预测中心词
CBOW 的训练目标是:
给定上下文词,预测中心词
例如:
我 ___ 自然语言 处理
模型看到“我”“自然语言”“处理”,要预测中间缺失的词可能是“喜欢”。
如果用公式表达,可以写成:
$$
P(w_t \mid w_{t-k}, \cdots, w_{t-1}, w_{t+1}, \cdots, w_{t+k})
$$
其中:
- $w_t$ 是中心词。
- $k$ 是窗口大小。
- 周围的词是上下文。
CBOW 通常训练速度较快,因为它把多个上下文词的信息平均或相加后,用来预测一个中心词。
4.2 Skip-gram:用中心词预测上下文
Skip-gram 的训练目标刚好相反:
给定中心词,预测它周围可能出现哪些上下文词
例如中心词是“喜欢”,模型要预测:
我
自然语言
处理
机器
学习
也就是学习:
$$
P(w_{t+j} \mid w_t),\quad -k \le j \le k,\ j \ne 0
$$
Skip-gram 对低频词通常更友好,因为每个中心词会产生多个训练样本。
可以简单记:
| 方法 | 输入 | 输出 | 特点 |
|---|---|---|---|
| CBOW | 上下文词 | 中心词 | 训练较快,对高频模式稳定 |
| Skip-gram | 中心词 | 上下文词 | 更适合学习低频词表示 |
5. 一个小例子:Skip-gram 如何构造样本
句子:
我 喜欢 自然语言 处理
窗口大小设为 1。
Skip-gram 会构造:
| 中心词 | 上下文词 |
|---|---|
| 我 | 喜欢 |
| 喜欢 | 我 |
| 喜欢 | 自然语言 |
| 自然语言 | 喜欢 |
| 自然语言 | 处理 |
| 处理 | 自然语言 |
如果词表是:
我:0, 喜欢:1, 自然语言:2, 处理:3
那么训练样本可以写成:
(0, 1)
(1, 0)
(1, 2)
(2, 1)
(2, 3)
(3, 2)
模型要做的事情是:
输入中心词 id
-> 查中心词向量
-> 和候选上下文词向量计算相似度
-> 让真实上下文词得分更高
6. word2vec 的神经网络结构
以 Skip-gram 为例,一个简化结构如下:
中心词 one-hot
-> 输入词向量矩阵 W_in
-> 得到中心词向量 v
-> 输出词向量矩阵 W_out
-> 预测上下文词概率
如果用 One-hot 输入,假设中心词编号是 1:
[0, 1, 0, 0]
乘上词向量矩阵:
$$
W_{in} \in \mathbb{R}^{V \times d}
$$
得到的其实就是第 1 行词向量。
所以从实现角度看:
Embedding 查表 等价于 One-hot 向量乘词向量矩阵,但查表更高效。
训练过程中,模型会不断调整 $W_{in}$ 和 $W_{out}$。
训练完成后,通常会取 $W_{in}$ 作为最终词向量。
7. 为什么不能直接对整个词表做 softmax
如果词表很小,模型可以直接预测整个词表的概率:
$$
P(w_o \mid w_i)=
\frac{\exp(v_{w_o}^{\top}v_{w_i})}
{\sum_{w=1}^{V}\exp(v_w^{\top}v_{w_i})}
$$
问题是,真实 NLP 任务中的词表可能非常大。
如果词表有 100000 个词,每个训练样本都要对 100000 个词计算 softmax,成本会很高。
因此 word2vec 常用一些近似训练方法来加速,比如:
- Hierarchical Softmax
- Negative Sampling
其中负采样最常见,也最容易理解。
8. 负采样:让真实词对更像,随机词对更不像
负采样的思路是:
不再让模型从整个词表中选正确答案,而是让模型判断“中心词和上下文词是否真的一起出现过”。
例如真实语料中出现过:
喜欢 -> 自然语言
这就是正样本,标签为 1。
然后随机采几个词作为负样本:
喜欢 -> 冰箱
喜欢 -> 火车
喜欢 -> 香蕉
这些大概率不是当前中心词的真实上下文,标签为 0。
模型计算中心词向量和上下文词向量的点积:
$$
score = v_c^\top v_o
$$
再通过 Sigmoid 得到概率:
$$
\sigma(score)=\frac{1}{1+e^{-score}}
$$
训练目标是:
- 正样本的 $\sigma(score)$ 越接近 1 越好。
- 负样本的 $\sigma(score)$ 越接近 0 越好。
这会推动:
- 经常共现的词向量更接近。
- 随机无关的词向量更远。
9. 手工理解一次负采样更新
假设中心词是“喜欢”,真实上下文是“学习”。
它们当前向量点积为:
$$
v_{\text{喜欢}}^\top v_{\text{学习}} = 0.2
$$
经过 Sigmoid:
$$
\sigma(0.2) \approx 0.55
$$
对于正样本来说,目标是 1,当前只有 0.55,说明模型还不够确信这两个词应该共现。
训练会把它们的向量往更相似的方向推。
再看一个负样本:
喜欢 -> 冰箱
假设点积也是 0.2,Sigmoid 也是 0.55。
但负样本的目标是 0,模型现在却给了 0.55,说明它错误地认为“喜欢”和“冰箱”比较相关。
训练会把这两个词的向量往更不相似的方向推。
所以负采样不是直接告诉模型“每个词是什么意思”,而是通过大量这样的正负样本,让向量空间慢慢变得有结构。
10. 高频词和低频词的问题
真实语料中有很多高频词,比如:
的、了、是、在、the、of、and
这些词出现次数很多,但语义区分度不一定强。
如果不处理,它们会在训练样本中占据太大比例,影响词向量质量。
word2vec 常见改进包括:
10.1 高频词下采样
对于特别高频的词,可以按一定概率丢弃一部分训练样本。
直觉是:
“的”少看几次,模型仍然知道它很常见;但给低频词更多机会,可能更有价值。
这样可以减少高频虚词对训练的干扰,也能提升训练速度。
10.2 负样本分布调整
负样本不是完全均匀随机采样。
常见做法是按照词频的 $3/4$ 次方构造采样分布:
$$
P(w)=\frac{f(w)^{3/4}}{\sum_i f(w_i)^{3/4}}
$$
这样既会更常抽到高频词,又不会让最高频词压倒一切。
10.3 调整窗口大小
窗口大小会影响词向量捕捉的信息:
| 窗口大小 | 更容易学到 |
|---|---|
| 小窗口 | 语法关系、局部搭配 |
| 大窗口 | 主题关系、语义相关 |
例如小窗口可能让“吃”和“苹果”靠近,大窗口可能让“电影”“导演”“演员”靠近。
10.4 增加语料和清洗质量
词向量非常依赖语料。
如果语料太少,模型学不到稳定的共现关系。
如果语料领域不匹配,词向量也会偏向错误语义。
例如:
- 医学语料训练出的“阳性”“阴性”和普通语料不同。
- 编程语料中的“类”“对象”“继承”有特定含义。
- 电商评论中的“苹果”可能更多指手机品牌,而不是水果。
所以训练词向量时要注意语料领域。
11. 怎么评价词向量
词向量训练好以后,不能只看训练 loss。
更重要的是看它是否真的学到了语义关系,以及能不能帮助下游任务。
11.1 最近邻词
最简单的方法是找某个词最相似的词。
常用余弦相似度:
$$
\cos(a,b)=\frac{a^\top b}{|a||b|}
$$
如果输入“国王”,比较好的结果可能是:
王后、王子、皇帝、君主
如果出现大量无关词,说明词向量质量可能不够好。
11.2 词类比
word2vec 最经典的现象之一是词向量可以表达某些方向关系。
例如:
国王 - 男人 + 女人 ≈ 王后
或者:
巴黎 - 法国 + 日本 ≈ 东京
这说明向量空间中不只是“相似词靠近”,还可能存在某些可迁移的语义方向。
但要注意,这类例子不能神化。
它能说明词向量有一定结构,但不代表模型真正理解了人类意义上的知识。
11.3 可视化
词向量通常是 100 维、300 维,无法直接画出来。
可以用 PCA、t-SNE 或 UMAP 降到二维,再观察相近词是否聚在一起。
例如:
猫、狗、兔子
北京、上海、广州
篮球、足球、网球
如果同类词能形成相对清晰的簇,说明向量空间有一定语义结构。
11.4 下游任务评价
最终最可靠的评价还是下游任务。
例如把词向量用于:
- 文本情感分类。
- 新闻主题分类。
- 搜索召回。
- 推荐系统。
- 命名实体识别。
如果使用训练好的词向量后,验证集准确率、F1、召回率等指标提升了,说明这些词向量对任务有帮助。
12. word2vec 的局限
word2vec 很重要,但它也有局限。
12.1 一词多义问题
word2vec 通常给每个词一个固定向量。
例如“苹果”:
我吃了一个苹果
我买了一台苹果手机
这两个句子里的“苹果”含义不同,但 word2vec 里通常只有一个“苹果”向量。
这会导致多义词表示不够精确。
后来 BERT、GPT 这类上下文相关模型会根据句子动态生成词表示,从而缓解这个问题。
12.2 无法直接处理未登录词
如果训练词表里没有某个词,word2vec 就很难直接给它一个好向量。
例如训练时没见过:
超导量子芯片
模型就无法直接查到它的词向量。
后来 fastText 通过子词 n-gram 改进了这一点。
它不仅学习整个词的向量,也学习词内部片段的向量,因此对未登录词更友好。
12.3 只利用局部窗口
word2vec 主要基于局部上下文窗口训练。
它能学到很多共现关系,但对长距离依赖、句法结构、篇章关系的建模能力有限。
这也是后来 RNN、CNN、Transformer 在 NLP 中继续发展的原因。
13. 从词向量过渡到神经网络分类器
训练好词向量以后,下一步通常是把它用于下游任务。
以情感分类为例:
输入:这部电影很好看
输出:正面
模型流程可以是:
具体步骤:
- 分词。
- 把每个词转成词 id。
- 用 Embedding 层把词 id 转成词向量。
- 把多个词向量合成一个句子向量。
- 用全连接层输出类别 logits。
- 使用交叉熵损失训练分类器。
一个最简单的句子分类器可以写成:
句子 token ids
-> Embedding
-> Mean Pooling
-> Linear
-> 分类 logits
14. PyTorch 实现一个简单文本分类器
下面是一个极简版本,重点看结构。
import torch
import torch.nn as nn
class TextClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, num_classes):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.classifier = nn.Linear(embed_dim, num_classes)
def forward(self, input_ids):
# input_ids: [batch_size, seq_len]
x = self.embedding(input_ids)
# x: [batch_size, seq_len, embed_dim]
sentence_vec = x.mean(dim=1)
# sentence_vec: [batch_size, embed_dim]
logits = self.classifier(sentence_vec)
# logits: [batch_size, num_classes]
return logits
假设:
model = TextClassifier(
vocab_size=10000,
embed_dim=100,
num_classes=2
)
input_ids = torch.tensor([
[12, 35, 87, 0, 0],
[44, 19, 63, 91, 0]
])
logits = model(input_ids)
print(logits.shape) # torch.Size([2, 2])
这里:
batch_size = 2seq_len = 5embed_dim = 100num_classes = 2
输出 [2, 2] 表示每个句子对应两个类别的 logits。
15. 如何使用预训练 word2vec 初始化 Embedding
如果已经有训练好的 word2vec 向量,可以用它初始化 nn.Embedding。
假设预训练矩阵为:
pretrained_weight.shape # [vocab_size, embed_dim]
可以这样写:
embedding = nn.Embedding.from_pretrained(
embeddings=pretrained_weight,
freeze=False
)
其中:
freeze=True:训练分类器时固定词向量,不更新。freeze=False:训练分类器时允许词向量继续微调。
如果数据量较小,可以先固定词向量,避免过拟合。
如果下游任务数据较多,通常可以允许词向量微调,让它更适合当前任务。
16. 为什么需要 padding 和 mask
真实句子的长度通常不一样:
很好看
这部电影真的非常好看
为了组成 batch,通常会 padding 到相同长度:
[很好看, PAD, PAD, PAD]
[这部, 电影, 真的, 好看]
如果直接对所有位置做平均,PAD 的向量也会被算进去,影响句子表示。
所以更严谨的 mean pooling 应该排除 PAD:
class TextClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, num_classes, pad_id=0):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=pad_id)
self.classifier = nn.Linear(embed_dim, num_classes)
self.pad_id = pad_id
def forward(self, input_ids):
x = self.embedding(input_ids)
# x: [batch_size, seq_len, embed_dim]
mask = (input_ids != self.pad_id).unsqueeze(-1)
# mask: [batch_size, seq_len, 1]
x = x * mask
lengths = mask.sum(dim=1).clamp(min=1)
sentence_vec = x.sum(dim=1) / lengths
logits = self.classifier(sentence_vec)
return logits
这里的 padding_idx=pad_id 会让 PAD 对应的 embedding 不参与更新。mask 则保证池化时不把 PAD 算进有效词。
17. word2vec 和现代 Embedding 的关系
word2vec 是理解词向量非常好的入口。
它让我们明白:
- 词向量不是人工规定的,而是训练出来的。
- 训练目标可以来自文本自身,不一定需要人工标签。
- 向量空间可以编码相似性、共现关系和某些语义方向。
- Embedding 层本质上是可学习的查表矩阵。
后来的很多模型仍然保留了这个核心思想:
离散 token -> 连续向量 -> 神经网络处理
区别在于:
- word2vec 给每个词一个固定向量。
- RNN/CNN 会在词向量之上继续建模句子结构。
- Transformer 会根据上下文动态生成 token 表示。
- BERT/GPT 里的 embedding 不只是词向量,还包含位置、上下文和深层语义信息。
所以学 word2vec 并不是过时,而是在理解现代 NLP 模型的地基。
18. 总结
word2vec 可以用一句话概括:
通过预测上下文或中心词,让经常出现在相似上下文中的词拥有相似的向量表示。
这篇文章的主线是:
One-hot 太稀疏
-> 需要稠密词向量
-> word2vec 用上下文预测训练词向量
-> CBOW 和 Skip-gram 是两种训练方向
-> 负采样让训练更高效
-> 词向量可以通过相似词、类比、可视化和下游任务评价
-> 词向量可以进一步接入神经网络分类器
理解 word2vec 后,再看文本分类、RNN、CNN、Transformer,会更容易抓住核心:
先把离散词变成连续向量,
再让神经网络在这些向量上学习任务需要的表示。
参考资料
- Tomas Mikolov 等,Efficient Estimation of Word Representations in Vector Space。
- Tomas Mikolov 等,Distributed Representations of Words and Phrases and their Compositionality。
- Yoav Goldberg, Omer Levy,word2vec Explained: deriving Mikolov et al.’s negative-sampling word-embedding method。
