共计 2483 个字符,预计需要花费 7 分钟才能阅读完成。
理解 CBOW 模型的基本架构
CBOW(Continuous Bag of Words)是 Word2Vec 的一种经典模型,它通过上下文词来预测当前词。对于刚接触 NLP 的同学来说,理解它的核心思想很重要:

- 输入层:上下文词的 one-hot 向量(比如窗口大小为 2 时,就是前后各两个词)
- 隐藏层:所有上下文词向量的平均值(这就是 ”Bag of Words” 的含义)
- 输出层:通过 softmax 计算目标词的概率分布
前向传播的数学表达很简单:
- 将上下文词的 one-hot 向量与词向量矩阵 W 相乘,得到各自的词向量
- 对这些词向量取平均,得到隐藏层向量 h
- 将 h 与另一个词向量矩阵 W ’ 相乘,得到输出向量 u
- 对 u 应用 softmax 函数,得到每个词的概率分布
用公式表示就是:
$$ h = \frac{1}{C} \sum_{c=1}^C W^T x_c $$
$$ u = W’ h $$
$$ y = \text{softmax}(u) $$
反向传播的数学推导
这才是真正的重头戏。我们需要理解误差是如何反向传播并更新词向量的。假设我们的损失函数是交叉熵:
$$ L = -\sum_{j=1}^V y_j \log \hat{y}_j $$
其中 y 是真实标签的 one-hot 向量,ŷ是预测的概率分布。我们需要计算损失对 W 和 W ’ 的梯度。
- 首先计算输出层的误差:
$$ \frac{\partial L}{\partial u} = \hat{y} – y $$
- 然后计算 W ’ 的梯度:
$$ \frac{\partial L}{\partial W’} = h (\hat{y} – y)^T $$
- 计算隐藏层的误差:
$$ \frac{\partial L}{\partial h} = W’ (\hat{y} – y) $$
- 最后计算 W 的梯度(对每个上下文词 x_c):
$$ \frac{\partial L}{\partial W} = \sum_{c=1}^C x_c \left(\frac{1}{C} W’ (\hat{y} – y) \right)^T $$
这个推导过程可能看起来有点复杂,但其实核心思想就是链式法则。每一步都在计算误差如何从输出层反向传播到输入层。
Python 实现代码
理解了数学原理后,我们来看具体实现。下面是一个简化版的 CBOW 实现:
import numpy as np
class CBOW:
def __init__(self, vocab_size, embedding_dim):
# 初始化词向量矩阵
self.W = np.random.randn(vocab_size, embedding_dim) * 0.01
self.W_prime = np.random.randn(embedding_dim, vocab_size) * 0.01
def forward(self, context_indices):
"""
前向传播
:param context_indices: 上下文词的索引列表
:return: 预测的概率分布
"""
# 获取上下文词向量
context_vectors = self.W[context_indices, :]
# 计算隐藏层(取平均)h = np.mean(context_vectors, axis=0)
# 计算输出
u = np.dot(h, self.W_prime)
# softmax
exp_u = np.exp(u - np.max(u)) # 数值稳定性处理
y_pred = exp_u / np.sum(exp_u)
return y_pred, h
def backward(self, context_indices, target_index, y_pred, h, learning_rate=0.01):
"""
反向传播
:param context_indices: 上下文词索引
:param target_index: 目标词索引
:param y_pred: 预测的概率分布
:param h: 隐藏层向量
:param learning_rate: 学习率
"""
# 构造真实标签的 one-hot 向量
y_true = np.zeros_like(y_pred)
y_true[target_index] = 1
# 计算输出层误差
delta = y_pred - y_true
# 更新 W'
dW_prime = np.outer(h, delta)
self.W_prime -= learning_rate * dW_prime
# 计算隐藏层误差
delta_h = np.dot(self.W_prime, delta)
# 更新 W(对每个上下文词)C = len(context_indices)
for idx in context_indices:
self.W[idx, :] -= learning_rate * delta_h / C
实现中的常见问题
在实际实现 CBOW 时,新手容易遇到以下几个问题:
-
梯度消失:当词向量维度很大或学习率很小时,梯度可能会变得非常小,导致训练缓慢
-
解决方案:使用合适的初始化方法(如 Xavier 初始化),调整学习率
-
计算效率低:直接实现 softmax 在词汇表很大时计算开销巨大
-
解决方案:使用负采样 (negative sampling) 或层次 softmax(hierarchical softmax)
-
过拟合:模型可能在训练集上表现很好但在测试集上表现不佳
-
解决方案:使用 dropout 或 L2 正则化
避坑指南
根据我的经验,新手在实现 CBOW 时容易犯的错误包括:
- 忘记对隐藏层向量取平均(这是 CBOW 与 skip-gram 的重要区别)
- 在计算 softmax 时没有做数值稳定性处理(可能导致数值溢出)
- 反向传播时没有正确计算梯度(特别是 W ’ 的更新容易出错)
- 学习率设置不当(太大导致震荡,太小导致收敛慢)
思考与扩展
理解了基础的 CBOW 实现后,你可以尝试以下扩展:
- 实现不同窗口大小的 CBOW(比如窗口大小从 2 改为 4)
- 添加负采样来提高训练效率
- 比较 CBOW 和 skip-gram 在不同数据集上的表现
- 尝试用 CBOW 训练的词向量来做一些简单的语义分析任务
CBOW 虽然结构简单,但它很好地体现了词向量的核心思想。希望通过这篇文章,你能对 CBOW 的反向传播机制有更深入的理解。在实际应用中,你可能需要使用更高效的实现(如 gensim 中的 Word2Vec),但理解底层原理对于调参和解决问题非常有帮助。
