共计 3052 个字符,预计需要花费 8 分钟才能阅读完成。
背景介绍
词嵌入是自然语言处理(NLP)中的一项基础技术,它将词语映射到低维连续向量空间中,使得语义相似的词在向量空间中距离相近。这种表示方法克服了传统 one-hot 编码的高维稀疏问题,为后续的文本分类、情感分析等任务提供了更好的特征表示。

在众多词嵌入模型中,CBOW(Continuous Bag-of-Words)因其简单高效而广受欢迎。它通过上下文词预测中心词的方式学习词向量,适合处理大规模语料库。
CBOW 模型原理解析
CBOW 模型的核心思想可以用以下数学公式表示:
给定一个中心词 $w_t$ 和其上下文窗口大小为 $m$,模型目标是最大化:
$$P(w_t|w_{t-m},…,w_{t-1},w_{t+1},…,w_{t+m})$$
模型结构主要包含三个部分:
- 输入层:将上下文词的 one-hot 表示拼接起来
- 隐藏层:通过词嵌入矩阵 $W_{V×N}$ 将高维稀疏向量转换为低维稠密向量(V 是词表大小,N 是嵌入维度)
- 输出层:通过另一个矩阵 $W’_{N×V}$ 计算每个词作为中心词的概率
graph LR
A[上下文词 one-hot] --> B[词嵌入矩阵 W]
B --> C[上下文向量平均]
C --> D[输出矩阵 W']
D --> E[softmax 概率]
手写实现步骤
数据预处理与上下文窗口构建
首先我们需要准备训练数据。以句子 ”the quick brown fox jumps” 为例:
- 构建词汇表并分配索引
- 定义窗口大小(通常 2 -5)
- 生成 (上下文, 中心词) 训练对
对于窗口大小 2,生成的训练样本为:
– ([the, quick], brown)
– ([quick, brown], fox)
– ([brown, fox], jumps)
初始化词嵌入矩阵
词嵌入矩阵 $W$ 的维度为 $V×N$,其中 V 是词表大小,N 是嵌入维度(通常 50-300)。初始化方法:
embedding_dim = 100
vocab_size = len(vocab)
W = np.random.randn(vocab_size, embedding_dim) * 0.01 # 小随机数初始化
W_prime = np.random.randn(embedding_dim, vocab_size) * 0.01
前向传播计算
- 将上下文词的 one-hot 向量与 $W$ 相乘得到嵌入向量
- 对上下文向量取平均
- 与 $W’$ 相乘得到输出分数
- 通过 softmax 计算概率分布
数学表达式:
$$h = \frac{1}{C} \sum_{c=1}^C W^T x_c$$
$$u = W’^T h$$
$$y = \text{softmax}(u)$$
损失函数定义
使用交叉熵损失函数:
$$L = -\sum_{j=1}^V y_j \log(\hat{y}_j)$$
其中 $y$ 是真实分布(one-hot),$\hat{y}$ 是预测分布。
反向传播与矩阵更新
通过链式法则计算梯度并更新参数:
- 计算输出层误差:$\delta_{out} = \hat{y} – y$
- 更新 $W’$:$\Delta W’ = h \cdot \delta_{out}^T$
- 计算隐藏层误差:$\delta_{hidden} = W’ \cdot \delta_{out}$
- 更新 $W$:对每个上下文词 $x_c$,$\Delta W += x_c \cdot \delta_{hidden}^T$
完整 Python 代码实现
import numpy as np
from collections import defaultdict
class CBOW:
def __init__(self, vocab_size, embedding_dim, window_size=2):
self.vocab_size = vocab_size
self.embedding_dim = embedding_dim
self.window_size = window_size
# 初始化参数
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):
"""前向传播"""
h = np.mean(self.W[context_indices, :], axis=0)
u = np.dot(self.W_prime.T, h)
y_pred = self._softmax(u)
return y_pred, h
def backward(self, context_indices, target_index, y_pred, h, learning_rate=0.01):
"""反向传播"""
# 计算输出层误差
delta_out = y_pred.copy()
delta_out[target_index] -= 1
# 更新 W'
self.W_prime -= learning_rate * np.outer(h, delta_out)
# 计算隐藏层误差并平均分配到上下文词
delta_hidden = np.dot(self.W_prime, delta_out) / len(context_indices)
# 更新 W
for idx in context_indices:
self.W[idx, :] -= learning_rate * delta_hidden
def _softmax(self, x):
"""数值稳定的 softmax 实现"""
e_x = np.exp(x - np.max(x))
return e_x / e_x.sum()
# 示例用法
corpus = ["the", "quick", "brown", "fox", "jumps"]
vocab = {word:i for i, word in enumerate(set(corpus))}
model = CBOW(len(vocab), embedding_dim=10)
# 训练一个样本
context = [vocab["the"], vocab["quick"]]
target = vocab["brown"]
y_pred, h = model.forward(context)
model.backward(context, target, y_pred, h)
性能优化建议
- 向量化计算:避免循环,使用矩阵运算
- 负采样:替代计算量大的 softmax
- 学习率调整:使用 Adam 等自适应优化器
- 批量训练:积累多个样本的梯度后统一更新
- 高频词降采样:平衡常见词和罕见词的影响
常见问题与解决方案
梯度消失问题:
– 使用 ReLU 等非饱和激活函数
– 合适的参数初始化(如 Xavier)
稀疏矩阵处理:
– 使用稀疏矩阵存储 one-hot 向量
– 只更新实际用到的词向量
训练不稳定:
– 梯度裁剪
– 学习率 warmup
延伸思考
要将此方法扩展到更大语料库,可以考虑:
- 使用 Huffman 树或负采样加速 softmax
- 分布式训练框架(如 TensorFlow/PyTorch)
- 加入 subword 信息处理 OOV 问题
- 结合字符级特征增强表示
进一步学习资源
- Mikolov et al. 2013 论文《Efficient Estimation of Word Representations in Vector Space》
- Stanford CS224N 课程讲义
- Gensim 库的 Word2Vec 实现
- TensorFlow 官方 Word2Vec 教程
通过本文的手写实现,相信你对 CBOW 模型的内部机制有了更深入的理解。词嵌入作为 NLP 的基础组件,掌握其原理对后续学习 BERT 等先进模型大有裨益。建议读者尝试在更大数据集上实践,并比较不同超参数对结果的影响。
