共计 1874 个字符,预计需要花费 5 分钟才能阅读完成。
CBOW(Continuous Bag-of-Words)模型是词嵌入技术的基石之一,它通过上下文词预测中心词的方式学习词向量。对于 NLP 初学者来说,理解 CBOW 的梯度计算过程往往是掌握词嵌入原理的第一道门槛。

为什么梯度计算容易卡壳?
初学者常在这些地方陷入困惑:
- 矩阵维度变化不直观(如从
[vocab_size, dim]到[dim, 1]) - softmax 求导时对
i=j和i≠j的情况处理混乱 - 反向传播时梯度分配的逻辑(谁该对谁的误差负责)
手把手拆解计算流程
1. 前向传播三步走
假设我们的词表大小 V=5,词向量维度 d=3,上下文窗口为 2:
- 输入层→隐藏层:
- 上下文词索引
[2,4](假设是 one-hot 编码) - 词向量矩阵
W_in.shape=(5,3) -
计算:
h = (W_in[2] + W_in[4]) / 2→ 得到(3,)向量 -
隐藏层→输出层:
- 输出矩阵
W_out.shape=(3,5) -
计算
u = h @ W_out→ 得到(5,)分数向量 -
softmax 归一化:
p_j = \frac{e^{u_j}}{\sum_{k=1}^V e^{u_k}}
2. 梯度推导关键步骤
损失函数采用交叉熵 L = -log(p_true),求导时注意:
- softmax 导数特性:
∂p_i/∂u_j = p_i(1-p_j)当i=j,否则-p_ip_j - 对真实词
t的梯度:\frac{\partial L}{\partial u_j} = \begin{cases} p_j - 1 & j = t \\ p_j & j \neq t \end{cases} - 反向传播到输入词向量:
\frac{\partial L}{\partial W_{in}} = \frac{1}{2} \cdot W_{out} \cdot (\mathbf{p} - \mathbf{y})(
y是真实词的 one-hot 向量)
Python 实现核心代码
import numpy as np
# 超参数
V, d = 5, 3
lr = 0.01
# 初始化(注意均匀分布范围)W_in = np.random.uniform(-0.5, 0.5, (V, d))
W_out = np.random.uniform(-0.5, 0.5, (d, V))
# 前向传播
def forward(context_words, true_word):
h = np.mean(W_in[context_words], axis=0) # (d,)
u = h @ W_out # (V,)
exp_u = np.exp(u - np.max(u)) # 防溢出
p = exp_u / exp_u.sum() # (V,)
loss = -np.log(p[true_word])
return h, u, p, loss
# 反向传播
def backward(h, p, context_words, true_word):
y = np.zeros(V)
y[true_word] = 1
grad_u = p - y # (V,)
# 更新输出矩阵
grad_W_out = np.outer(h, grad_u) # (d,V)
# 更新输入矩阵(均分梯度)grad_h = W_out @ grad_u # (d,)
for word in context_words:
W_in[word] -= lr * grad_h / len(context_words)
W_out -= lr * grad_W_out
六大避坑经验
- 初始化陷阱:
- 词向量初始值建议在
[-0.5/d, 0.5/d]范围 -
太大容易导致 softmax 溢出
-
梯度爆炸对策:
- 设置梯度阈值:
grad = np.clip(grad, -5, 5) -
使用学习率衰减:
lr = lr0 / (1 + decay*epoch) -
低频词处理:
- 对高频词降采样:
P(w_i) = 1 - sqrt(t/freq(w_i)) -
对低频词设置最小出现次数(如 <5 次则忽略)
-
维度验证技巧:
- 每次矩阵乘法后打印 shape
-
例如
(3,) @ (3,5)应该得到(5,) -
计算效率优化:
- 实际使用负采样替代 softmax
-
用稀疏矩阵存储 one-hot 向量
-
调试必看信号:
- 正常情况:loss 在前几轮快速下降,后缓慢收敛
- 异常情况:loss 震荡→调小学习率;loss 不变→检查梯度
延伸思考
- 与 Skip-gram 对比:
- CBOW 梯度来自多个上下文词的平均
-
Skip-gram 需要对每个上下文词单独计算梯度
-
PyTorch 自动微分版:
# 只需将 numpy 数组改为 torch.Tensor W_in = torch.randn(V, d, requires_grad=True) loss.backward() # 自动计算所有梯度
通过这次手算实践,我深刻体会到:理解梯度流动比会调库更重要。建议读者尝试用不同窗口大小训练模型,观察词向量在二维 PCA 图上的分布变化——你会发现模型真的学会了语义关系!
正文完
