从零手算CBOW词嵌入:详解步骤与数学推导

1次阅读
没有评论

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

image.webp

为什么需要手动计算 CBOW 词嵌入?

对于刚接触 NLP 的新手来说,词嵌入模型往往像是一个黑箱。虽然我们可以直接调用现成的工具包(如 Gensim 或 TensorFlow),但如果不了解其内部运作机制,在遇到以下场景时会非常被动:

从零手算 CBOW 词嵌入:详解步骤与数学推导

  • 模型输出异常时无法定位问题根源
  • 需要自定义词向量维度或优化策略时
  • 想要结合业务特性调整损失函数时

手动计算一个微型 CBOW 模型,就像拆解钟表观察齿轮运动,能让我们真正理解词向量如何从原始语料中诞生。

数学推导全过程

1. 输入层:one-hot 编码

假设我们的迷你语料只有 3 个单词:["自然", "语言", "处理"],其 one-hot 编码为:

$$
\begin{aligned}
自然 &= [1,0,0] \
语言 &= [0,1,0] \
处理 &= [0,0,1]
\end{aligned}
$$

2. 隐藏层向量计算

设定词向量维度 $d=2$,初始化权重矩阵 $W_{V\times d}$(V 为词汇表大小):

$$
W = \begin{bmatrix}
0.1 & 0.3 \
0.2 & 0.5 \
0.4 & 0.6
\end{bmatrix}
$$

以窗口大小 2 为例,计算 ” 语言 ” 的上下文向量(前后各 1 个词):

$$
h = \frac{W[ 自然] + W[处理]}{2} = \frac{[0.1,0.3] + [0.4,0.6]}{2} = [0.25, 0.45]
$$

3. 输出层 softmax 计算

定义输出权重矩阵 $W’_{d\times V}$:

$$
W’ = \begin{bmatrix}
0.2 & 0.3 & 0.1 \
0.4 & 0.1 & 0.9
\end{bmatrix}
$$

计算未归一化得分:

$$
u = h \cdot W’ = [0.25\times0.2+0.45\times0.4, …, 0.25\times0.1+0.45\times0.9] = [0.23, 0.08, 0.46]
$$

softmax 概率:

$$
p_i = \frac{e^{u_i}}{\sum e^{u_j}} \Rightarrow [0.30, 0.24, 0.46]
$$

4. 反向传播更新

计算交叉熵损失对 $u$ 的梯度(目标词为 ” 语言 ”):

$$
\frac{\partial L}{\partial u} = p – y = [0.30, -0.76, 0.46]
$$

更新输出层权重:

$$
\Delta W’ = h^T \cdot \frac{\partial L}{\partial u} = \begin{bmatrix}
0.25 \ 0.45
\end{bmatrix}
[0.30, -0.76, 0.46]
$$

Python 实现代码

import numpy as np

# 超参数设置
V, d = 3, 2  # 词汇表大小和词向量维度
lr = 0.01    # 学习率

# 初始化权重
W = np.random.rand(V, d)  # 输入权重
W_prime = np.random.rand(d, V)  # 输出权重

# 语料准备
word_to_idx = {"自然":0, "语言":1, "处理":2}
corpus = ["自然", "语言", "处理"]

# 训练步骤
def train(target_pos):
    # 获取上下文
    context = [corpus[target_pos-1], corpus[(target_pos+1)%3]]

    # 前向传播
    h = np.mean([W[word_to_idx[w]] for w in context], axis=0)
    u = np.dot(h, W_prime)
    p = np.exp(u) / np.sum(np.exp(u))

    # 计算梯度
    y = np.zeros(V)
    y[word_to_idx[corpus[target_pos]]] = 1
    grad_u = p - y

    # 反向传播
    grad_W_prime = np.outer(h, grad_u)
    grad_h = np.dot(W_prime, grad_u)

    # 更新权重
    W_prime -= lr * grad_W_prime
    for w in context:
        W[word_to_idx[w]] -= lr * grad_h / len(context)

    return W.copy(), W_prime.copy()

# 训练演示
for epoch in range(3):
    print(f"Epoch {epoch+1}:")
    for i in range(3):
        new_W, new_W_prime = train(i)
        print(f"Target'{corpus[i]}'| W:\n{new_W}\n  W':\n{new_W_prime}\n")

常见问题与解决方案

词向量维度选择

  • 小语料(<1MB 文本):50-100 维
  • 中等语料:200-300 维
  • 大型语料:300-500 维

低频词处理

  • 采用 subword 信息(如 FastText)
  • 对低频词适当增大学习率
  • 设置最低词频阈值(如 min_count=5)

梯度消失对策

  • 使用负采样替代 softmax 全计算
  • 梯度裁剪(gradient clipping)
  • 改用 Adam 等自适应优化器

延伸思考

与 Skip-gram 的区别

  • CBOW:用上下文预测中心词,适合高频词
  • Skip-gram:用中心词预测上下文,对低频词表现更好
  • 计算量上,Skip-gram 需要更多训练步数

验证方法

  1. 检查损失函数是否单调下降
  2. 可视化词向量(TSNE 降维)观察聚类效果
  3. 人工检查近义词是否相邻

手动实现 CBOW 虽然繁琐,但能建立对词嵌入本质的直觉理解。当你在实际项目中遇到 OOV(未登录词)或语义异常时,这种底层认知会带来关键的调试视角。

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