CBOW模型手算词嵌入步骤详解:从数学原理到Python实现

1次阅读
没有评论

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

image.webp

为什么需要词嵌入

在自然语言处理中,传统 one-hot 编码存在明显的局限性。假设我们有一个包含 1 万个单词的词汇表,每个单词将被表示为一个 1 万维的向量,其中只有对应位置的元素为 1,其余为 0。这种表示方式导致:

CBOW 模型手算词嵌入步骤详解:从数学原理到 Python 实现

  • 向量维度灾难(维度过高)
  • 无法表达单词之间的语义关系(所有向量正交)
  • 难以捕捉 ” 猫 ” 与 ” 狗 ” 比 ” 猫 ” 与 ” 石头 ” 更相似的常识

分布式表示(词嵌入)通过将单词映射到低维连续空间(通常 50-300 维),解决了这些问题。在嵌入空间中,语义相似的单词会聚集在一起,且可以通过向量运算发现有趣的关系(如:国王 – 男 + 女 ≈ 女王)。

CBOW vs Skip-gram

Word2Vec 提供了两种模型架构:

  • CBOW(Continuous Bag-of-Words):通过上下文预测中心词。训练更快,对高频词效果更好,适合小型数据集。
  • Skip-gram:通过中心词预测上下文。能更好处理低频词,适合大型数据集。

本次我们聚焦 CBOW 的实现,因为它的计算过程更直观适合教学演示。假设窗口大小为 2(即使用前后各 2 个词预测中心词),下面拆解具体步骤。

CBOW 前向传播手算示例

1. 数据准备

假设我们有一个微型语料库和词汇表:

corpus = ["the", "cat", "sat", "on", "the", "mat"]
vocab = {"the":0, "cat":1, "sat":2, "on":3, "mat":4}
vocab_size = len(vocab)  # 5
embedding_dim = 3  # 为演示方便设小值 

2. 初始化参数矩阵

需要创建两个权重矩阵:

  • 输入矩阵 $W_{in}$(形状:vocab_size × embedding_dim)
  • 输出矩阵 $W_{out}$(形状:embedding_dim × vocab_size)
import numpy as np
np.random.seed(42)

W_in = np.random.randn(5, 3)  # 输入矩阵
W_out = np.random.randn(3, 5)  # 输出矩阵 

3. 处理训练样本

以 ([“cat”, “sat”, “on”, “the”], “the”) 为例:

  1. 将上下文词转换为 one-hot 向量:
  2. cat: [0,1,0,0,0]
  3. sat: [0,0,1,0,0]
  4. on: [0,0,0,1,0]
  5. the: [1,0,0,0,0]

  6. 计算隐藏层(平均词向量):
    $$ h = \frac{1}{4}(W_{in}^\top x_{cat} + W_{in}^\top x_{sat} + W_{in}^\top x_{on} + W_{in}^\top x_{the}) $$

实际计算过程:

context_indices = [1, 2, 3, 0]  # cat, sat, on, the
h = np.mean([W_in[i] for i in context_indices], axis=0)
# 示例值: array([-0.138, -0.272,  0.464])

  1. 计算输出层得分:
    $$ u = W_{out}^\top h $$

    u = W_out.T @ h  # 形状 (5,)

  2. 应用 softmax 得到概率分布:
    $$ p(w_j|context) = \frac{e^{u_j}}{\sum_{k=1}^V e^{u_k}} $$

    def softmax(x):
        e_x = np.exp(x - np.max(x))  # 数值稳定
        return e_x / e_x.sum()
    
    y_pred = softmax(u)
    # 示例输出: [0.12, 0.23, 0.18, 0.27, 0.20]

反向传播与权重更新

定义交叉熵损失函数:
$$ L = -\sum_{j=1}^V y_j \log p_j $$
其中 $y$ 是真实标签的 one-hot 向量。

通过链式法则计算梯度:

  1. 输出层梯度:
    $$ \frac{\partial L}{\partial W_{out}} = h (p – y)^\top $$

  2. 隐藏层梯度:
    $$ \frac{\partial L}{\partial h} = W_{out}(p – y) $$

  3. 输入层梯度(对每个上下文词):
    $$ \frac{\partial L}{\partial W_{in}^{(k)}} = \frac{1}{C} \frac{\partial L}{\partial h} $$
    其中 $C$ 是上下文词数量(本例为 4)

更新公式($\eta$ 为学习率):
$$ W_{out} := W_{out} – \eta \frac{\partial L}{\partial W_{out}} $$
$$ W_{in} := W_{in} – \eta \frac{\partial L}{\partial W_{in}} $$

Python 完整实现

import numpy as np
from collections import defaultdict

class CBOW:
    def __init__(self, vocab_size, embedding_dim):
        self.W_in = np.random.randn(vocab_size, embedding_dim) * 0.01
        self.W_out = np.random.randn(embedding_dim, vocab_size) * 0.01

    def forward(self, context_indices):
        """上下文词索引列表 -> 预测概率"""
        h = np.mean([self.W_in[i] for i in context_indices], axis=0)
        u = self.W_out.T @ h
        return softmax(u), h

    def backward(self, context_indices, target_idx, learning_rate=0.01):
        """执行一次参数更新"""
        # 前向传播
        y_pred, h = self.forward(context_indices)

        # 构造 one-hot 标签
        y_true = np.zeros_like(y_pred)
        y_true[target_idx] = 1

        # 计算梯度
        dL_du = y_pred - y_true
        dL_dW_out = np.outer(h, dL_du)  # (embed_dim, vocab_size)
        dL_dh = self.W_out @ dL_du      # (embed_dim,)

        # 更新参数
        self.W_out -= learning_rate * dL_dW_out

        # 平均梯度分配给每个上下文词
        dL_dWin_avg = dL_dh / len(context_indices)
        for i in context_indices:
            self.W_in[i] -= learning_rate * dL_dWin_avg

        # 返回当前样本的损失
        return -np.log(y_pred[target_idx] + 1e-8)

# 训练示例
model = CBOW(vocab_size=5, embedding_dim=3)
losses = []

for epoch in range(100):
    # 模拟一个训练样本
    context = [1, 2, 3, 0]  # cat, sat, on, the
    target = 4              # 预测 "mat"
    loss = model.backward(context, target, 0.1)
    losses.append(loss)

实践建议与优化

  1. 学习率选择
  2. 典型值在 0.001 到 0.1 之间
  3. 过大导致震荡,过小收敛慢
  4. 可以使用学习率衰减策略

  5. 负采样加速

  6. 当词汇量很大时(>1 万),softmax 计算成本高
  7. 可改用负采样(Negative Sampling),只更新少数负样本的权重
  8. 在训练中期引入效果更好

  9. 维度选择经验

  10. 小型数据集:50-100 维
  11. 中型数据集:200-300 维
  12. 超 300 维通常收益递减

词向量应用示例

训练好的词向量可以用于文本分类:

  1. 对文档中所有词的向量取平均
  2. 将平均向量输入分类器(如 SVM、神经网络)
# 伪代码示例
def document_vector(doc, word_vectors):
    """将文档转换为向量表示"""
    vectors = [word_vectors[w] for w in doc if w in word_vectors]
    return np.mean(vectors, axis=0) if vectors else None

# 假设已训练好词向量
X_train = [document_vector(doc, model.W_in) for doc in train_docs]
y_train = [...]  # 对应标签

# 训练分类器
from sklearn.svm import SVC
clf = SVC().fit(X_train, y_train)

通过这次手算和实现,我们应该对 CBOW 如何生成词向量有了直观理解。虽然实际生产环境会使用优化过的实现(如 Gensim 库),但理解底层原理对调试模型和解决实际问题至关重要。

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