共计 3389 个字符,预计需要花费 9 分钟才能阅读完成。
背景介绍
词嵌入(Word Embedding)是自然语言处理(NLP)中的基础技术,它将词汇映射到低维连续向量空间,使得语义相似的词在向量空间中距离相近。CBOW(Continuous Bag of Words)模型是词嵌入的经典算法之一,由 Mikolov 等人在 2013 年提出。与 Skip-gram 模型相比,CBOW 通过上下文预测中心词,更适合处理高频词汇和小型数据集。

CBOW 在 NLP 中有广泛的应用场景,例如:
- 文本分类
- 机器翻译
- 信息检索
- 情感分析
CBOW vs. Skip-gram
CBOW 和 Skip-gram 是两种主要的词嵌入模型,各有优缺点:
- CBOW:
- 优点:训练速度快,对高频词表现更好
-
缺点:对低频词捕捉能力较弱
-
Skip-gram:
- 优点:对低频词表现更好,适合大型数据集
- 缺点:训练速度较慢
CBOW 的数学原理
CBOW 模型的核心思想是通过上下文词汇预测中心词。假设我们有一个句子:”the quick brown fox jumps”,窗口大小为 2,则中心词为 ”brown” 时,上下文词汇为 [“the”, “quick”, “fox”, “jumps”]。
模型结构
- 输入层 :将上下文词汇的 one-hot 向量输入模型。
- 投影层 :将 one-hot 向量与词嵌入矩阵 $W_{V \times N}$ 相乘,得到词向量。
- 隐藏层 :对上下文词向量求平均。
- 输出层 :通过 softmax 计算每个词作为中心词的概率。
数学表达式如下:
-
上下文词向量的平均值:
$$ h = \frac{1}{C} \sum_{c=1}^C W^T x_c $$ -
输出层计算:
$$ u_j = W’^T_j h $$
$$ p(w_j|context) = y_j = \frac{exp(u_j)}{\sum_{j’=1}^V exp(u_{j’})} $$ -
损失函数(交叉熵):
$$ L = -\sum_{j=1}^V y_j \log(\hat{y}_j) $$
手算实例
假设我们有一个极简的词汇表:[“I”, “love”, “NLP”],词嵌入维度为 2。随机初始化词嵌入矩阵 $W$ 和 $W’$:
W = np.array([[0.1, 0.2], # I
[0.3, 0.4], # love
[0.5, 0.6]]) # NLP
W_prime = np.array([[0.7, 0.8], # I
[0.9, 1.0], # love
[1.1, 1.2]]) # NLP
假设上下文为 [“I”, “NLP”],中心词为 ”love”:
-
计算上下文词向量平均值 $h$:
$$ h = \frac{1}{2}([0.1, 0.2] + [0.5, 0.6]) = [0.3, 0.4] $$ -
计算输出分数 $u_j$:
$$ u = [0.3, 0.4] \times W’^T = [0.30.7 + 0.40.8, 0.30.9 + 0.41.0, 0.31.1 + 0.41.2] = [0.53, 0.67, 0.81] $$ -
计算 softmax 概率:
$$ y = \frac{exp([0.53, 0.67, 0.81])}{sum(exp([0.53, 0.67, 0.81]))} \approx [0.30, 0.33, 0.37] $$ -
计算损失:
真实分布为 [0, 1, 0](中心词是 ”love”),所以:
$$ L = -\log(0.33) \approx 1.11 $$
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, center_index):
# 计算隐藏层
h = np.mean(self.W[context_indices], axis=0)
# 计算输出层
u = np.dot(h, self.W_prime)
# softmax
exp_u = np.exp(u - np.max(u)) # 数值稳定性
y = exp_u / np.sum(exp_u)
# 计算损失
loss = -np.log(y[center_index])
return loss, h, y
def backward(self, context_indices, center_index, h, y, learning_rate=0.01):
# 计算梯度
grad_y = y.copy()
grad_y[center_index] -= 1
# 更新参数
grad_W_prime = np.outer(h, grad_y)
grad_h = np.dot(self.W_prime, grad_y)
# 平均梯度到每个上下文词
grad_W = np.zeros_like(self.W)
for idx in context_indices:
grad_W[idx] += grad_h / len(context_indices)
# 参数更新
self.W -= learning_rate * grad_W
self.W_prime -= learning_rate * grad_W_prime
性能优化建议
负采样技术
原始 CBOW 的 softmax 计算复杂度为 $O(V)$,当词汇表很大时效率极低。负采样(Negative Sampling)通过只更新少数负样本的参数来加速训练:
def negative_sampling_loss(self, context_indices, center_index, k=5):
h = np.mean(self.W[context_indices], axis=0)
# 正样本得分
pos_score = np.dot(h, self.W_prime[:, center_index])
pos_loss = -np.log(sigmoid(pos_score))
# 负样本
neg_loss = 0
neg_indices = np.random.choice([i for i in range(self.vocab_size) if i != center_index],
size=k,
replace=False)
for neg_idx in neg_indices:
neg_score = np.dot(h, self.W_prime[:, neg_idx])
neg_loss += -np.log(sigmoid(-neg_score))
total_loss = pos_loss + neg_loss
return total_loss
矩阵运算优化
使用批处理(batch processing)和向量化运算可以大幅提升训练速度:
def batch_forward(self, batch_contexts, batch_centers):
# batch_contexts: (batch_size, context_size)
# batch_centers: (batch_size,)
# 向量化计算
h = np.mean(self.W[batch_contexts], axis=1) # (batch_size, embedding_dim)
u = np.dot(h, self.W_prime) # (batch_size, vocab_size)
# softmax
exp_u = np.exp(u - np.max(u, axis=1, keepdims=True))
y = exp_u / np.sum(exp_u, axis=1, keepdims=True)
# 交叉熵损失
losses = -np.log(y[np.arange(len(batch_centers)), batch_centers])
return np.mean(losses)
避坑指南
- 参数设置误区 :
- 窗口大小:一般 2 -5,太大会引入不相关词汇
- 学习率:建议从 0.025 开始,随训练逐渐减小
-
词向量维度:50-300,太大会导致过拟合
-
小数据集过拟合 :
- 使用早停(early stopping)
- 增加 L2 正则化
- 采用预训练词向量初始化
思考题
如何将 CBOW 应用于特定领域(如医疗、法律)文本?
- 领域词汇扩充:收集领域术语,扩展词汇表
- 迁移学习:在通用词向量基础上进行微调
- 领域语料预处理:保留领域特有的短语和缩写
通过本文的讲解和代码示例,相信读者已经掌握了 CBOW 模型的核心原理和实现方法。在实际应用中,建议先从简单实现开始,逐步加入优化技巧,并根据具体任务调整模型参数。
