共计 3266 个字符,预计需要花费 9 分钟才能阅读完成。
背景痛点:从 one-hot 到分布式表示
传统 NLP 处理文本时常用 one-hot 编码表示词汇,这种方法存在两个显著缺陷:

- 维度灾难:词表越大向量维度越高,百万级词表需要百万维向量
- 语义缺失:所有词向量相互正交,无法体现 ” 猫 ” 与 ” 狗 ” 比 ” 桌子 ” 更相似的关系
词嵌入技术通过低维稠密向量(通常 50-300 维)解决这些问题。其中 CBOW(Continuous Bag-of-Words)模型通过上下文预测中心词,是生成词嵌入的经典方法。
技术对比:CBOW vs Skip-gram
两种模型选择取决于实际场景:
- CBOW:适合小型数据集,对高频词表现更好,训练速度较快
- Skip-gram:在大型数据集上表现更优,能更好处理低频词
本文聚焦 CBOW 实现,假设我们有句子 ”the quick brown fox jumps”,设置窗口大小为 2,以下演示预测 ”brown” 的过程。
前向传播手算过程
1. 初始化参数
设词向量维度 $d=3$,词表 V =[“the”, “quick”, “brown”, “fox”, “jumps”],则:
- 输入矩阵 $W_{in} \in \mathbb{R}^{5×3}$(5 个词×3 维)
- 输出矩阵 $W_{out} \in \mathbb{R}^{3×5}$(3 维×5 个词)
随机初始化值示例:
W_in = np.array([[0.1, 0.2, 0.3], # the
[0.4, 0.5, 0.6], # quick
[0.7, 0.8, 0.9], # brown (目标词,暂不参与计算)
[0.2, 0.4, 0.6], # fox
[0.3, 0.5, 0.7] # jumps
])
2. 计算隐藏层
上下文词为 [“the”, “quick”, “fox”, “jumps”],取平均:
$$
\begin{aligned}
h &= \frac{1}{4}(v_{the} + v_{quick} + v_{fox} + v_{jumps}) \
&= \frac{1}{4}\left(
\begin{bmatrix}0.1\0.2\0.3\end{bmatrix} +
\begin{bmatrix}0.4\0.5\0.6\end{bmatrix} +
\begin{bmatrix}0.2\0.4\0.6\end{bmatrix} +
\begin{bmatrix}0.3\0.5\0.7\end{bmatrix}
\right) = \begin{bmatrix}0.25\0.4\0.55\end{bmatrix}
\end{aligned}
$$
3. 输出层计算
通过 $W_{out}$ 计算未归一化得分:
$$
u = W_{out}^T h = \begin{bmatrix}0.1&0.4&0.2&0.3\0.2&0.5&0.4&0.5\0.3&0.6&0.6&0.7\end{bmatrix}^T \begin{bmatrix}0.25\0.4\0.55\end{bmatrix}
$$
假设 $W_{out}$ 初始化为:
W_out = np.random.randn(3,5)*0.01
最终通过 softmax 得到概率分布:
$$
p_i = \frac{e^{u_i}}{\sum_{j=1}^V e^{u_j}}
$$
反向传播与权重更新
定义损失函数为交叉熵:
$$
L = -\log p_{brown}
$$
通过链式法则计算梯度(推导过程略),得到更新规则:
$$
W_{out}^{(new)} = W_{out}^{(old)} – \eta \cdot \frac{\partial L}{\partial W_{out}}
$$
$$
W_{in}^{(new)} = W_{in}^{(old)} – \eta \cdot \frac{\partial L}{\partial W_{in}}
$$
Python 实现核心代码
import numpy as np
from collections import defaultdict
# 数据预处理
def build_vocab(sentences):
word_counts = defaultdict(int)
for sent in sentences:
for word in sent.split():
word_counts[word] += 1
vocab = {w:i for i,w in enumerate(word_counts.keys())}
return vocab
# CBOW 模型类
class CBOW:
def __init__(self, vocab_size, embedding_dim):
self.W_in = np.random.randn(vocab_size, embedding_dim) * 0.01
self.W_out = np.random.randn(embedding_dim, vocab_size) * 0.01
def forward(self, context_words, target_word_idx):
# 平均上下文词向量 [embedding_dim,]
h = np.mean([self.W_in[i] for i in context_words], axis=0)
# 输出层得分 [vocab_size,]
u = np.dot(self.W_out.T, h)
# softmax 概率
exp_u = np.exp(u - np.max(u)) # 数值稳定性处理
probs = exp_u / np.sum(exp_u)
# 计算损失
loss = -np.log(probs[target_word_idx])
return h, probs, loss
def backward(self, context_words, target_word_idx, h, probs, learning_rate):
# 输出层梯度 [vocab_size,]
grad_out = probs.copy()
grad_out[target_word_idx] -= 1
# 更新 W_out [embedding_dim, vocab_size]
self.W_out -= learning_rate * np.outer(h, grad_out)
# 输入层梯度 [embedding_dim,]
grad_in = np.dot(self.W_out, grad_out) / len(context_words)
# 更新 W_in 中上下文词的向量 [len(context_words), embedding_dim]
for i in context_words:
self.W_in[i] -= learning_rate * grad_in
避坑指南
- 学习率设置
- 典型初始值 0.025,随训练逐渐衰减
-
过大导致震荡,过小收敛缓慢
-
负采样技术
- 当词表超过 1 万词时建议启用
-
修改输出层为 sigmoid,随机采样 5 -20 个负例
-
维度选择
- 50-100 维:小型数据集或语义简单的任务
- 200-300 维:大型语料或复杂语义关系
- 超过 300 维可能出现过拟合
延伸应用:文本分类
训练好的词向量可通过以下方式用于分类:
- 求文档向量:平均所有词的嵌入
- 作为 LSTM/CNN 的输入特征
- 冻结词向量或微调取决于数据量
示例代码片段:
# 加载预训练词向量
def load_embeddings(emb_file):
embeddings = {}
with open(emb_file) as f:
for line in f:
values = line.split()
word = values[0]
vector = np.array(values[1:], dtype='float32')
embeddings[word] = vector
return embeddings
# 文本转为向量序列
def text_to_sequence(text, embeddings, max_len=100):
seq = []
for word in text.split()[:max_len]:
if word in embeddings:
seq.append(embeddings[word])
return np.array(seq)
通过理解 CBOW 的手算过程,我们能更深入地把握词嵌入的本质,为后续复杂模型打下坚实基础。建议读者尝试在小规模数据(如电影评论)上从头训练词向量,观察不同参数下的效果差异。
