共计 3495 个字符,预计需要花费 9 分钟才能阅读完成。
为什么需要词嵌入
在自然语言处理中,传统 one-hot 编码存在明显的局限性。假设我们有一个包含 1 万个单词的词汇表,每个单词将被表示为一个 1 万维的向量,其中只有对应位置的元素为 1,其余为 0。这种表示方式导致:

- 向量维度灾难(维度过高)
- 无法表达单词之间的语义关系(所有向量正交)
- 难以捕捉 ” 猫 ” 与 ” 狗 ” 比 ” 猫 ” 与 ” 石头 ” 更相似的常识
分布式表示(词嵌入)通过将单词映射到低维连续空间(通常 50-300 维),解决了这些问题。在嵌入空间中,语义相似的单词会聚集在一起,且可以通过向量运算发现有趣的关系(如:国王 – 男 + 女 ≈ 女王)。
CBOW vs Skip-gram
Word2Vec 提供了两种模型架构:
- CBOW(Continuous Bag-of-Words):通过上下文预测中心词。训练更快,对高频词效果更好,适合小型数据集。
- Skip-gram:通过中心词预测上下文。能更好处理低频词,适合大型数据集。
本次我们聚焦 CBOW 的实现,因为它的计算过程更直观适合教学演示。假设窗口大小为 2(即使用前后各 2 个词预测中心词),下面拆解具体步骤。
CBOW 前向传播手算示例
1. 数据准备
假设我们有一个微型语料库和词汇表:
corpus = ["the", "cat", "sat", "on", "the", "mat"]
vocab = {"the":0, "cat":1, "sat":2, "on":3, "mat":4}
vocab_size = len(vocab) # 5
embedding_dim = 3 # 为演示方便设小值
2. 初始化参数矩阵
需要创建两个权重矩阵:
- 输入矩阵 $W_{in}$(形状:vocab_size × embedding_dim)
- 输出矩阵 $W_{out}$(形状:embedding_dim × vocab_size)
import numpy as np
np.random.seed(42)
W_in = np.random.randn(5, 3) # 输入矩阵
W_out = np.random.randn(3, 5) # 输出矩阵
3. 处理训练样本
以 ([“cat”, “sat”, “on”, “the”], “the”) 为例:
- 将上下文词转换为 one-hot 向量:
- cat: [0,1,0,0,0]
- sat: [0,0,1,0,0]
- on: [0,0,0,1,0]
-
the: [1,0,0,0,0]
-
计算隐藏层(平均词向量):
$$ h = \frac{1}{4}(W_{in}^\top x_{cat} + W_{in}^\top x_{sat} + W_{in}^\top x_{on} + W_{in}^\top x_{the}) $$
实际计算过程:
context_indices = [1, 2, 3, 0] # cat, sat, on, the
h = np.mean([W_in[i] for i in context_indices], axis=0)
# 示例值: array([-0.138, -0.272, 0.464])
-
计算输出层得分:
$$ u = W_{out}^\top h $$u = W_out.T @ h # 形状 (5,) -
应用 softmax 得到概率分布:
$$ p(w_j|context) = \frac{e^{u_j}}{\sum_{k=1}^V e^{u_k}} $$def softmax(x): e_x = np.exp(x - np.max(x)) # 数值稳定 return e_x / e_x.sum() y_pred = softmax(u) # 示例输出: [0.12, 0.23, 0.18, 0.27, 0.20]
反向传播与权重更新
定义交叉熵损失函数:
$$ L = -\sum_{j=1}^V y_j \log p_j $$
其中 $y$ 是真实标签的 one-hot 向量。
通过链式法则计算梯度:
-
输出层梯度:
$$ \frac{\partial L}{\partial W_{out}} = h (p – y)^\top $$ -
隐藏层梯度:
$$ \frac{\partial L}{\partial h} = W_{out}(p – y) $$ -
输入层梯度(对每个上下文词):
$$ \frac{\partial L}{\partial W_{in}^{(k)}} = \frac{1}{C} \frac{\partial L}{\partial h} $$
其中 $C$ 是上下文词数量(本例为 4)
更新公式($\eta$ 为学习率):
$$ W_{out} := W_{out} – \eta \frac{\partial L}{\partial W_{out}} $$
$$ W_{in} := W_{in} – \eta \frac{\partial L}{\partial W_{in}} $$
Python 完整实现
import numpy as np
from collections import defaultdict
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_indices):
"""上下文词索引列表 -> 预测概率"""
h = np.mean([self.W_in[i] for i in context_indices], axis=0)
u = self.W_out.T @ h
return softmax(u), h
def backward(self, context_indices, target_idx, learning_rate=0.01):
"""执行一次参数更新"""
# 前向传播
y_pred, h = self.forward(context_indices)
# 构造 one-hot 标签
y_true = np.zeros_like(y_pred)
y_true[target_idx] = 1
# 计算梯度
dL_du = y_pred - y_true
dL_dW_out = np.outer(h, dL_du) # (embed_dim, vocab_size)
dL_dh = self.W_out @ dL_du # (embed_dim,)
# 更新参数
self.W_out -= learning_rate * dL_dW_out
# 平均梯度分配给每个上下文词
dL_dWin_avg = dL_dh / len(context_indices)
for i in context_indices:
self.W_in[i] -= learning_rate * dL_dWin_avg
# 返回当前样本的损失
return -np.log(y_pred[target_idx] + 1e-8)
# 训练示例
model = CBOW(vocab_size=5, embedding_dim=3)
losses = []
for epoch in range(100):
# 模拟一个训练样本
context = [1, 2, 3, 0] # cat, sat, on, the
target = 4 # 预测 "mat"
loss = model.backward(context, target, 0.1)
losses.append(loss)
实践建议与优化
- 学习率选择 :
- 典型值在 0.001 到 0.1 之间
- 过大导致震荡,过小收敛慢
-
可以使用学习率衰减策略
-
负采样加速 :
- 当词汇量很大时(>1 万),softmax 计算成本高
- 可改用负采样(Negative Sampling),只更新少数负样本的权重
-
在训练中期引入效果更好
-
维度选择经验 :
- 小型数据集:50-100 维
- 中型数据集:200-300 维
- 超 300 维通常收益递减
词向量应用示例
训练好的词向量可以用于文本分类:
- 对文档中所有词的向量取平均
- 将平均向量输入分类器(如 SVM、神经网络)
# 伪代码示例
def document_vector(doc, word_vectors):
"""将文档转换为向量表示"""
vectors = [word_vectors[w] for w in doc if w in word_vectors]
return np.mean(vectors, axis=0) if vectors else None
# 假设已训练好词向量
X_train = [document_vector(doc, model.W_in) for doc in train_docs]
y_train = [...] # 对应标签
# 训练分类器
from sklearn.svm import SVC
clf = SVC().fit(X_train, y_train)
通过这次手算和实现,我们应该对 CBOW 如何生成词向量有了直观理解。虽然实际生产环境会使用优化过的实现(如 Gensim 库),但理解底层原理对调试模型和解决实际问题至关重要。
