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

1次阅读
没有评论

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

image.webp

CBOW 模型基础与痛点分析

CBOW(Continuous Bag-of-Words)是 Word2Vec 的经典实现之一,通过上下文词预测中心词来学习词向量。其核心优势在于能高效捕获词语的分布式表示,但初学者常因以下问题卡壳:

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

  • 维度混乱:输入层、隐藏层、输出层的矩阵形状变化不清晰
  • 梯度困惑:softmax 的导数计算与链式法则结合时容易迷失方向
  • 实践断层:理论公式与代码实现难以对应

手算梯度全流程拆解

1. 模型结构说明

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

# 参数初始化(关键变量注释)W_in = np.random.randn(5, 3)  # 输入层到隐藏层的权重矩阵(V,d)
W_out = np.random.randn(3, 5)  # 隐藏层到输出层的权重矩阵(d,V)

2. 前向传播步骤

  1. 输入层→隐藏层:
  2. 上下文词索引转为 one-hot 向量(如[0,1,0,0,0])
  3. 计算隐藏层向量:$h = \frac{1}{C}\sum_{c=1}^C W_{in}^T x_c$(C= 窗口大小)
  4. 维度变化:(d,V) × (V,1) → (d,1)

  5. 隐藏层→输出层:

  6. 计算未归一化得分:$u = W_{out}^T h$
  7. softmax 概率:$p_i = \frac{e^{u_i}}{\sum_j e^{u_j}}$

3. 损失函数与梯度推导

交叉熵损失:$L = -\sum_{i=1}^V y_i \log p_i$

关键梯度计算(建议手推验证):

  1. $\frac{\partial L}{\partial u} = p – y$(y 是中心词的 one-hot 标签)
  2. $\frac{\partial L}{\partial W_{out}} = h (p – y)^T$
  3. $\frac{\partial L}{\partial h} = W_{out}(p – y)$
  4. $\frac{\partial L}{\partial W_{in}} = \frac{1}{C}\sum_c x_c (\frac{\partial L}{\partial h})^T$

Python 实现关键代码

def forward(context_words, W_in, W_out):
    """context_words: 上下文词索引列表"""
    h = np.mean([W_in[word] for word in context_words], axis=0)
    u = W_out.T @ h
    p = np.exp(u) / np.sum(np.exp(u))
    return h, u, p

def backward(y_true, p, h, W_out, context_words):
    """y_true: 中心词的 one-hot 编码"""
    grad_u = p - y_true
    grad_W_out = np.outer(h, grad_u)
    grad_h = W_out @ grad_u
    grad_W_in = np.zeros_like(W_in)
    for word in context_words:
        grad_W_in[word] += grad_h / len(context_words)
    return grad_W_in, grad_W_out

避坑实践指南

  • 初始化技巧 :词向量初始值建议在[-0.5/d, 0.5/d] 均匀分布
  • 梯度爆炸:当词表较大时,对梯度进行裁剪(grad = np.clip(grad, -5, 5))
  • 低频词处理:采用 subsampling 技术,概率丢弃高频词

延伸思考

  1. 与 Skip-gram 对比:
  2. CBOW 梯度来自多个上下文词的平均,训练更稳定
  3. Skip-gram 对低频词表现更好,但计算量更大

  4. PyTorch 实现建议:

  5. 利用 nn.Embedding 层代替手动矩阵操作
  6. 自动微分简化梯度计算(示例代码片段):
# PyTorch 自动微分版
embedding = nn.Embedding(vocab_size, dim)
loss_func = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(embedding.parameters(), lr=0.01)

# 训练循环中...
optimizer.zero_grad()
h = torch.mean(embedding(context_words), dim=0)
loss = loss_func(h @ W_out, target_word)
loss.backward()
optimizer.step()

总结

通过这次手算实践,我们拆解了 CBOW 模型的黑箱操作。建议读者:
1. 在小词表(如 V =5)上手工验算前 3 次迭代
2. 尝试添加负采样加速训练
3. 可视化训练前后的词向量(TSNE 降维)观察变化

这种从零实现的经历,能帮助建立对词嵌入技术更本质的理解。当后续使用 gensim 等工具时,你会更清楚参数调整对结果的影响。

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