从零手算CBOW词嵌入:梯度计算与实现细节全解析

1次阅读
没有评论

共计 1874 个字符,预计需要花费 5 分钟才能阅读完成。

image.webp

CBOW(Continuous Bag-of-Words)模型是词嵌入技术的基石之一,它通过上下文词预测中心词的方式学习词向量。对于 NLP 初学者来说,理解 CBOW 的梯度计算过程往往是掌握词嵌入原理的第一道门槛。

从零手算 CBOW 词嵌入:梯度计算与实现细节全解析

为什么梯度计算容易卡壳?

初学者常在这些地方陷入困惑:

  • 矩阵维度变化不直观(如从 [vocab_size, dim][dim, 1]
  • softmax 求导时对 i=ji≠j的情况处理混乱
  • 反向传播时梯度分配的逻辑(谁该对谁的误差负责)

手把手拆解计算流程

1. 前向传播三步走

假设我们的词表大小 V=5,词向量维度 d=3,上下文窗口为 2:

  1. 输入层→隐藏层
  2. 上下文词索引 [2,4](假设是 one-hot 编码)
  3. 词向量矩阵 W_in.shape=(5,3)
  4. 计算:h = (W_in[2] + W_in[4]) / 2 → 得到 (3,) 向量

  5. 隐藏层→输出层

  6. 输出矩阵 W_out.shape=(3,5)
  7. 计算 u = h @ W_out → 得到 (5,) 分数向量

  8. 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

六大避坑经验

  1. 初始化陷阱
  2. 词向量初始值建议在 [-0.5/d, 0.5/d] 范围
  3. 太大容易导致 softmax 溢出

  4. 梯度爆炸对策

  5. 设置梯度阈值:grad = np.clip(grad, -5, 5)
  6. 使用学习率衰减:lr = lr0 / (1 + decay*epoch)

  7. 低频词处理

  8. 对高频词降采样:P(w_i) = 1 - sqrt(t/freq(w_i))
  9. 对低频词设置最小出现次数(如 <5 次则忽略)

  10. 维度验证技巧

  11. 每次矩阵乘法后打印 shape
  12. 例如 (3,) @ (3,5) 应该得到 (5,)

  13. 计算效率优化

  14. 实际使用负采样替代 softmax
  15. 用稀疏矩阵存储 one-hot 向量

  16. 调试必看信号

  17. 正常情况:loss 在前几轮快速下降,后缓慢收敛
  18. 异常情况:loss 震荡→调小学习率;loss 不变→检查梯度

延伸思考

  1. 与 Skip-gram 对比
  2. CBOW 梯度来自多个上下文词的平均
  3. Skip-gram 需要对每个上下文词单独计算梯度

  4. PyTorch 自动微分版

    # 只需将 numpy 数组改为 torch.Tensor
    W_in = torch.randn(V, d, requires_grad=True)
    loss.backward()  # 自动计算所有梯度

通过这次手算实践,我深刻体会到:理解梯度流动比会调库更重要。建议读者尝试用不同窗口大小训练模型,观察词向量在二维 PCA 图上的分布变化——你会发现模型真的学会了语义关系!

正文完
 0
评论(没有评论)