共计 3400 个字符,预计需要花费 9 分钟才能阅读完成。
词嵌入是 NLP 任务中的基础技术,它将词语映射到低维连续向量空间,使得语义相似的词在向量空间中也相近。CBOW(Continuous Bag of Words)模型是词嵌入的经典方法之一,通过上下文词预测目标词来学习词向量。本文将从数学原理到 Python 实现,逐步拆解 CBOW 模型的手算过程。

1. CBOW 模型的数学计算步骤
CBOW 模型的核心思想是利用上下文词预测目标词。假设我们的词汇表大小为 $V$,词向量维度为 $N$,上下文窗口大小为 $C$。以下是 CBOW 模型的主要计算步骤:
1.1 输入层到隐藏层
-
上下文词表示:对于每个上下文词,我们使用 one-hot 编码表示,记为 $\mathbf{x}_c$,其中 $c$ 是上下文词的索引。
-
词嵌入矩阵:我们有一个词嵌入矩阵 $\mathbf{W}_{V \times N}$,其中每一行对应一个词的词向量。通过矩阵乘法,我们可以得到上下文词的词向量:
$$
\mathbf{v}_c = \mathbf{W}^T \mathbf{x}_c
$$ -
隐藏层向量:将上下文词的词向量求平均,得到隐藏层向量 $\mathbf{h}$:
$$
\mathbf{h} = \frac{1}{C} \sum_{c=1}^C \mathbf{v}_c
$$
1.2 隐藏层到输出层
-
输出矩阵:我们还有一个输出矩阵 $\mathbf{W}’_{N \times V}$,用于将隐藏层向量映射到输出层。
-
得分计算:通过矩阵乘法计算每个词的得分:
$$
\mathbf{u} = \mathbf{W}’^T \mathbf{h}
$$ -
Softmax 归一化:对得分进行 Softmax 归一化,得到每个词的概率分布:
$$
p(w_j | w_{c1}, …, w_{cC}) = \frac{\exp(u_j)}{\sum_{k=1}^V \exp(u_k)}
$$ -
损失函数:使用交叉熵损失函数计算预测误差:
$$
E = -\log p(w_O | w_{c1}, …, w_{cC})
$$
2. Python 实现
以下是 CBOW 模型的 Python 实现代码,使用 NumPy 库完成矩阵运算:
import numpy as np
# 参数初始化
V = 10000 # 词汇表大小
N = 300 # 词向量维度
C = 4 # 上下文窗口大小
# 初始化词嵌入矩阵和输出矩阵
W = np.random.randn(V, N) * 0.01
W_prime = np.random.randn(N, V) * 0.01
# 前向传播
def forward(context_indices, target_index):
# 上下文词的 one-hot 编码
x = np.zeros((C, V))
for i, idx in enumerate(context_indices):
x[i, idx] = 1
# 输入层到隐藏层
v = np.dot(x, W) # (C, N)
h = np.mean(v, axis=0) # (N,)
# 隐藏层到输出层
u = np.dot(W_prime.T, h) # (V,)
# Softmax
exp_u = np.exp(u - np.max(u)) # 数值稳定性
p = exp_u / np.sum(exp_u)
# 损失
loss = -np.log(p[target_index])
return h, u, p, loss
# 反向传播
def backward(context_indices, target_index, h, u, p):
# 计算输出层的梯度
grad_u = p.copy()
grad_u[target_index] -= 1
# 更新输出矩阵
grad_W_prime = np.outer(h, grad_u) # (N, V)
# 隐藏层的梯度
grad_h = np.dot(W_prime, grad_u) # (N,)
# 更新词嵌入矩阵
grad_v = grad_h / C # (N,)
grad_W = np.zeros((V, N))
for idx in context_indices:
grad_W[idx] += grad_v
return grad_W, grad_W_prime
# 梯度检查
def gradient_check(context_indices, target_index):
epsilon = 1e-7
h, u, p, loss = forward(context_indices, target_index)
grad_W, grad_W_prime = backward(context_indices, target_index, h, u, p)
# 检查词嵌入矩阵的梯度
for i in range(V):
for j in range(N):
W[i, j] += epsilon
_, _, _, loss_plus = forward(context_indices, target_index)
W[i, j] -= 2 * epsilon
_, _, _, loss_minus = forward(context_indices, target_index)
W[i, j] += epsilon
numeric_grad = (loss_plus - loss_minus) / (2 * epsilon)
assert np.abs(grad_W[i, j] - numeric_grad) < 1e-5, "Gradient check failed for W"
# 检查输出矩阵的梯度
for i in range(N):
for j in range(V):
W_prime[i, j] += epsilon
_, _, _, loss_plus = forward(context_indices, target_index)
W_prime[i, j] -= 2 * epsilon
_, _, _, loss_minus = forward(context_indices, target_index)
W_prime[i, j] += epsilon
numeric_grad = (loss_plus - loss_minus) / (2 * epsilon)
assert np.abs(grad_W_prime[i, j] - numeric_grad) < 1e-5, "Gradient check failed for W'"
print("Gradient check passed!")
3. 对比 Skip-gram 模型
CBOW 和 Skip-gram 是 Word2Vec 的两种主要模型,它们的区别在于:
-
目标函数:CBOW 通过上下文词预测目标词,而 Skip-gram 通过目标词预测上下文词。
-
计算复杂度:CBOW 对上下文词求平均,适合处理高频词;Skip-gram 对每个上下文词单独处理,适合处理低频词。
-
性能:在小数据集上,CBOW 通常表现更好;在大数据集上,Skip-gram 更能捕捉词之间的复杂关系。
4. 工程实践优化
4.1 上下文窗口大小的选择
- 小窗口(2-5):捕捉语法关系(如动词和宾语的搭配)。
- 大窗口(5-10):捕捉语义关系(如“中国”和“北京”)。
4.2 词频统计与 Subsampling
- 高频词降采样:对高频词(如“the”、“and”)按概率丢弃,避免它们主导训练过程。
- 采样概率公式:
$$
P(w_i) = 1 – \sqrt{\frac{t}{f(w_i)}}
$$
其中 $t$ 是阈值(如 $10^{-5}$),$f(w_i)$ 是词的频率。
4.3 负采样加速训练
- 负采样:随机采样 $K$ 个负样本(非目标词),代替计算全词汇表的 Softmax。
- 损失函数:
$$
E = -\log \sigma(\mathbf{u}O^T \mathbf{h}) – \sum)
$$
其中 $\sigma$ 是 Sigmoid 函数。}^K \log \sigma(-\mathbf{u}_k^T \mathbf{h
5. 避坑指南
5.1 稀疏矩阵的内存优化
- 使用稀疏矩阵 :对于大型词汇表,使用
scipy.sparse存储 one-hot 向量。 - 哈希技巧:用哈希函数映射词到固定大小的向量。
5.2 学习率衰减策略
- 线性衰减:
$$
\eta_t = \eta_0 \cdot (1 – t / T)
$$
其中 $T$ 是总迭代次数。 - 指数衰减:
$$
\eta_t = \eta_0 \cdot \gamma^t
$$
其中 $\gamma$ 是衰减率(如 0.95)。
5.3 词向量可视化技巧
- PCA 降维:用 PCA 将词向量降到 2D 或 3D 后绘图。
- t-SNE:对高维数据更有效,但计算成本较高。
6. 开放问题
- 生僻词处理:CBOW 对低频词的表现较差,如何改进?
- 与 BERT 对比:BERT 等预训练模型是否完全替代了 Word2Vec?
CBOW 模型虽然简单,但仍然是理解词嵌入的基础。通过手算和代码实现,我们可以更深入地掌握其原理和优化技巧。
