共计 2173 个字符,预计需要花费 6 分钟才能阅读完成。
为什么需要手动计算 CBOW 词嵌入?
对于刚接触 NLP 的新手来说,词嵌入模型往往像是一个黑箱。虽然我们可以直接调用现成的工具包(如 Gensim 或 TensorFlow),但如果不了解其内部运作机制,在遇到以下场景时会非常被动:

- 模型输出异常时无法定位问题根源
- 需要自定义词向量维度或优化策略时
- 想要结合业务特性调整损失函数时
手动计算一个微型 CBOW 模型,就像拆解钟表观察齿轮运动,能让我们真正理解词向量如何从原始语料中诞生。
数学推导全过程
1. 输入层:one-hot 编码
假设我们的迷你语料只有 3 个单词:["自然", "语言", "处理"],其 one-hot 编码为:
$$
\begin{aligned}
自然 &= [1,0,0] \
语言 &= [0,1,0] \
处理 &= [0,0,1]
\end{aligned}
$$
2. 隐藏层向量计算
设定词向量维度 $d=2$,初始化权重矩阵 $W_{V\times d}$(V 为词汇表大小):
$$
W = \begin{bmatrix}
0.1 & 0.3 \
0.2 & 0.5 \
0.4 & 0.6
\end{bmatrix}
$$
以窗口大小 2 为例,计算 ” 语言 ” 的上下文向量(前后各 1 个词):
$$
h = \frac{W[ 自然] + W[处理]}{2} = \frac{[0.1,0.3] + [0.4,0.6]}{2} = [0.25, 0.45]
$$
3. 输出层 softmax 计算
定义输出权重矩阵 $W’_{d\times V}$:
$$
W’ = \begin{bmatrix}
0.2 & 0.3 & 0.1 \
0.4 & 0.1 & 0.9
\end{bmatrix}
$$
计算未归一化得分:
$$
u = h \cdot W’ = [0.25\times0.2+0.45\times0.4, …, 0.25\times0.1+0.45\times0.9] = [0.23, 0.08, 0.46]
$$
softmax 概率:
$$
p_i = \frac{e^{u_i}}{\sum e^{u_j}} \Rightarrow [0.30, 0.24, 0.46]
$$
4. 反向传播更新
计算交叉熵损失对 $u$ 的梯度(目标词为 ” 语言 ”):
$$
\frac{\partial L}{\partial u} = p – y = [0.30, -0.76, 0.46]
$$
更新输出层权重:
$$
\Delta W’ = h^T \cdot \frac{\partial L}{\partial u} = \begin{bmatrix}
0.25 \ 0.45
\end{bmatrix}
[0.30, -0.76, 0.46]
$$
Python 实现代码
import numpy as np
# 超参数设置
V, d = 3, 2 # 词汇表大小和词向量维度
lr = 0.01 # 学习率
# 初始化权重
W = np.random.rand(V, d) # 输入权重
W_prime = np.random.rand(d, V) # 输出权重
# 语料准备
word_to_idx = {"自然":0, "语言":1, "处理":2}
corpus = ["自然", "语言", "处理"]
# 训练步骤
def train(target_pos):
# 获取上下文
context = [corpus[target_pos-1], corpus[(target_pos+1)%3]]
# 前向传播
h = np.mean([W[word_to_idx[w]] for w in context], axis=0)
u = np.dot(h, W_prime)
p = np.exp(u) / np.sum(np.exp(u))
# 计算梯度
y = np.zeros(V)
y[word_to_idx[corpus[target_pos]]] = 1
grad_u = p - y
# 反向传播
grad_W_prime = np.outer(h, grad_u)
grad_h = np.dot(W_prime, grad_u)
# 更新权重
W_prime -= lr * grad_W_prime
for w in context:
W[word_to_idx[w]] -= lr * grad_h / len(context)
return W.copy(), W_prime.copy()
# 训练演示
for epoch in range(3):
print(f"Epoch {epoch+1}:")
for i in range(3):
new_W, new_W_prime = train(i)
print(f"Target'{corpus[i]}'| W:\n{new_W}\n W':\n{new_W_prime}\n")
常见问题与解决方案
词向量维度选择
- 小语料(<1MB 文本):50-100 维
- 中等语料:200-300 维
- 大型语料:300-500 维
低频词处理
- 采用 subword 信息(如 FastText)
- 对低频词适当增大学习率
- 设置最低词频阈值(如 min_count=5)
梯度消失对策
- 使用负采样替代 softmax 全计算
- 梯度裁剪(gradient clipping)
- 改用 Adam 等自适应优化器
延伸思考
与 Skip-gram 的区别
- CBOW:用上下文预测中心词,适合高频词
- Skip-gram:用中心词预测上下文,对低频词表现更好
- 计算量上,Skip-gram 需要更多训练步数
验证方法
- 检查损失函数是否单调下降
- 可视化词向量(TSNE 降维)观察聚类效果
- 人工检查近义词是否相邻
手动实现 CBOW 虽然繁琐,但能建立对词嵌入本质的直觉理解。当你在实际项目中遇到 OOV(未登录词)或语义异常时,这种底层认知会带来关键的调试视角。
