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

1次阅读
没有评论

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

image.webp

背景痛点:从 one-hot 到分布式表示

传统 NLP 处理文本时常用 one-hot 编码表示词汇,这种方法存在两个显著缺陷:

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

  • 维度灾难:词表越大向量维度越高,百万级词表需要百万维向量
  • 语义缺失:所有词向量相互正交,无法体现 ” 猫 ” 与 ” 狗 ” 比 ” 桌子 ” 更相似的关系

词嵌入技术通过低维稠密向量(通常 50-300 维)解决这些问题。其中 CBOW(Continuous Bag-of-Words)模型通过上下文预测中心词,是生成词嵌入的经典方法。

技术对比:CBOW vs Skip-gram

两种模型选择取决于实际场景:

  • CBOW:适合小型数据集,对高频词表现更好,训练速度较快
  • Skip-gram:在大型数据集上表现更优,能更好处理低频词

本文聚焦 CBOW 实现,假设我们有句子 ”the quick brown fox jumps”,设置窗口大小为 2,以下演示预测 ”brown” 的过程。

前向传播手算过程

1. 初始化参数

设词向量维度 $d=3$,词表 V =[“the”, “quick”, “brown”, “fox”, “jumps”],则:

  • 输入矩阵 $W_{in} \in \mathbb{R}^{5×3}$(5 个词×3 维)
  • 输出矩阵 $W_{out} \in \mathbb{R}^{3×5}$(3 维×5 个词)

随机初始化值示例:

W_in = np.array([[0.1, 0.2, 0.3],  # the
    [0.4, 0.5, 0.6],  # quick
    [0.7, 0.8, 0.9],  # brown (目标词,暂不参与计算)
    [0.2, 0.4, 0.6],  # fox
    [0.3, 0.5, 0.7]   # jumps
])

2. 计算隐藏层

上下文词为 [“the”, “quick”, “fox”, “jumps”],取平均:

$$
\begin{aligned}
h &= \frac{1}{4}(v_{the} + v_{quick} + v_{fox} + v_{jumps}) \
&= \frac{1}{4}\left(
\begin{bmatrix}0.1\0.2\0.3\end{bmatrix} +
\begin{bmatrix}0.4\0.5\0.6\end{bmatrix} +
\begin{bmatrix}0.2\0.4\0.6\end{bmatrix} +
\begin{bmatrix}0.3\0.5\0.7\end{bmatrix}
\right) = \begin{bmatrix}0.25\0.4\0.55\end{bmatrix}
\end{aligned}
$$

3. 输出层计算

通过 $W_{out}$ 计算未归一化得分:

$$
u = W_{out}^T h = \begin{bmatrix}0.1&0.4&0.2&0.3\0.2&0.5&0.4&0.5\0.3&0.6&0.6&0.7\end{bmatrix}^T \begin{bmatrix}0.25\0.4\0.55\end{bmatrix}
$$

假设 $W_{out}$ 初始化为:

W_out = np.random.randn(3,5)*0.01

最终通过 softmax 得到概率分布:
$$
p_i = \frac{e^{u_i}}{\sum_{j=1}^V e^{u_j}}
$$

反向传播与权重更新

定义损失函数为交叉熵:
$$
L = -\log p_{brown}
$$

通过链式法则计算梯度(推导过程略),得到更新规则:

$$
W_{out}^{(new)} = W_{out}^{(old)} – \eta \cdot \frac{\partial L}{\partial W_{out}}
$$

$$
W_{in}^{(new)} = W_{in}^{(old)} – \eta \cdot \frac{\partial L}{\partial W_{in}}
$$

Python 实现核心代码

import numpy as np
from collections import defaultdict

# 数据预处理
def build_vocab(sentences):
    word_counts = defaultdict(int)
    for sent in sentences:
        for word in sent.split():
            word_counts[word] += 1
    vocab = {w:i for i,w in enumerate(word_counts.keys())}
    return vocab

# CBOW 模型类
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_words, target_word_idx):
        # 平均上下文词向量 [embedding_dim,]
        h = np.mean([self.W_in[i] for i in context_words], axis=0)

        # 输出层得分 [vocab_size,]
        u = np.dot(self.W_out.T, h)

        # softmax 概率
        exp_u = np.exp(u - np.max(u))  # 数值稳定性处理
        probs = exp_u / np.sum(exp_u)

        # 计算损失
        loss = -np.log(probs[target_word_idx])

        return h, probs, loss

    def backward(self, context_words, target_word_idx, h, probs, learning_rate):
        # 输出层梯度 [vocab_size,]
        grad_out = probs.copy()
        grad_out[target_word_idx] -= 1

        # 更新 W_out [embedding_dim, vocab_size]
        self.W_out -= learning_rate * np.outer(h, grad_out)

        # 输入层梯度 [embedding_dim,]
        grad_in = np.dot(self.W_out, grad_out) / len(context_words)

        # 更新 W_in 中上下文词的向量 [len(context_words), embedding_dim]
        for i in context_words:
            self.W_in[i] -= learning_rate * grad_in

避坑指南

  1. 学习率设置
  2. 典型初始值 0.025,随训练逐渐衰减
  3. 过大导致震荡,过小收敛缓慢

  4. 负采样技术

  5. 当词表超过 1 万词时建议启用
  6. 修改输出层为 sigmoid,随机采样 5 -20 个负例

  7. 维度选择

  8. 50-100 维:小型数据集或语义简单的任务
  9. 200-300 维:大型语料或复杂语义关系
  10. 超过 300 维可能出现过拟合

延伸应用:文本分类

训练好的词向量可通过以下方式用于分类:

  1. 求文档向量:平均所有词的嵌入
  2. 作为 LSTM/CNN 的输入特征
  3. 冻结词向量或微调取决于数据量

示例代码片段:

# 加载预训练词向量
def load_embeddings(emb_file):
    embeddings = {}
    with open(emb_file) as f:
        for line in f:
            values = line.split()
            word = values[0]
            vector = np.array(values[1:], dtype='float32')
            embeddings[word] = vector
    return embeddings

# 文本转为向量序列
def text_to_sequence(text, embeddings, max_len=100):
    seq = []
    for word in text.split()[:max_len]:
        if word in embeddings:
            seq.append(embeddings[word])
    return np.array(seq)

通过理解 CBOW 的手算过程,我们能更深入地把握词嵌入的本质,为后续复杂模型打下坚实基础。建议读者尝试在小规模数据(如电影评论)上从头训练词向量,观察不同参数下的效果差异。

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