不依赖任何第三方库实现词嵌入(Word Embedding),本质上就是亲手编写词向量的完整训练流程——从文本清洗、词典构建,到生成上下文样本,再到定义前向传播与反向更新。其核心在于透彻理解 Skip‑gram 或 CBOW 的前向计算与梯度回传机制。下面我们用纯 Python(仅使用 math、random、collections)搭建一个可运行的轻量级词嵌入模型,以 Skip‑gram + 负采样(Negative Sampling)为例。虽然无法处理大规模语料,但每个步骤都对应着 NLP 表征学习的核心动机,值得亲手走一遍。

1. 文本预处理与词汇表构建
第一步是将原始句子转换为整数索引序列,同时过滤低频词和停用词(可选,但通常能提升嵌入质量)。具体拆解如下:
- 分词:按空格或简单标点切分,统一转为小写,丢弃空字符串;
- 词频统计:使用
Counter统计所有词的出现次数; - 词汇表构建:仅保留出现次数 ≥
min_count的词,按频次从高到低排序;务必为UNK(未登录词)预留索引 0; - 映射表:建立两个字典——
word2idx(词→索引)和idx2word(索引→词),后续所有查表操作均依赖它们。
2. 生成 Skip‑gram 训练样本
对于每个中心词,提取窗口内(例如左右各2个)的词作为正样本;负样本则依据词频分布按概率采样——这里有一个常用技巧:将一元分布的 3/4 次方作为平滑权重,使低频词也有机会被选中。
- 遍历句子中的每个词(跳过窗口边界处的词),以它为中心,收集左右
window_size内的词作为正样本; - 针对每个正样本,随机采样
n_neg个负样本:预先计算每个词的采样概率(freq^(3/4)后归一化),可采用轮盘赌或别名采样;简易版本直接使用random.choices并设置权重即可; - 每条样本存储为三元组
(center_idx, context_idx, label),正样本label=1,负样本label=0。
3. 初始化与前向传播(Sigmoid + 点积)
需要维护两组向量:输入矩阵 W(尺寸 vocabulary_size × embedding_dim)和输出矩阵 W'(相同大小)。Skip‑gram 的做法是:从 W 中查询中心词得到向量 v_c,从 W' 中查询上下文词得到向量 v_w,计算两者点积 v_c ⋅ v_w,再通过 Sigmoid 得到概率值。
- 初始化:
W和W'均赋值为小随机数,例如uniform(-0.5/dim, 0.5/dim),以避免对称性导致向量趋同; - Sigmoid 手写实现:
def sigmoid(x): return 1 / (1 + math.exp(-max(-50, min(50, x))))——通过截断防止数值溢出; - 前向计算:
score = sum(v_c[i] * v_w[i] for i in range(dim)),然后prob = sigmoid(score)。
4. 手动反向传播与参数更新
损失函数选用二元交叉熵:L = −[y·log(p) + (1−y)·log(1−p)]。正样本时 y=1,梯度为 (p−1);负样本时 y=0,梯度为 p。依据该误差信号更新两组向量:
- 令
g = prob - label(即误差信号); - 更新中心词向量:
W[center_idx][i] -= lr * g * W_prime[context_idx][i]; - 更新上下文词向量:
W_prime[context_idx][i] -= lr * g * W[center_idx][i]; - 学习率
lr建议初始值设为 0.025,并随训练步数线性衰减。
整个过程不依赖 NumPy 或 PyTorch,全部使用原生 Python 列表与循环手动实现。虽然效率较低——仅适合小语料或教学演示——但优点在于清晰展现了词向量如何通过局部共现被“拉近”,以及负采样如何绕过 softmax 的计算瓶颈。归根结底,核心不在于代码的简洁性,而在于每一步都映射了“分布假设”的数学本质。
