
简介这份资源面向自然语言处理初学者与课程实践者围绕中英文语料训练CBOW模型获取词向量这一任务展开适合正在学习word2vec原理、需要动手复现词向量训练流程的同学参考。压缩包共12个文件约13.58MB以txt语料与词向量输出文件为主辅以xml工程配置、py训练脚本及gitignore等辅助文件其中脚本可同时处理中文与英文语料data目录存放中英文语料及中文停用词output目录给出训练后的中英文词向量结果。使用时只需在脚本中确定训练中文或英文直接运行即可完成训练。目前已有2693人学习下载读者可借此理解CBOW的输入输出构造、停用词处理与词向量文件生成方式并对照输出结果验证训练效果为后续文本分类、相似度计算等任务打下基础。1. 中英文语料训练 CBOW 模型从零拿到可用的词向量如果你手头有一批中英文混合的文本——比如产品评论、客服对话、双语工单——想快速拿到能算相似度、能做聚类、能喂给下游分类器的词向量CBOW 往往是最先值得试的模型。它不像 BERT 那样吃显存也不像 Word2Vec 的 skip-gram 那样在低频词上磨蹭训练快、代码短、效果对多数业务场景够用。但真正动手时坑集中在三处中文要不要分词、窗口怎么滑、负采样怎么配。这篇笔记按“先跑通再调优”的顺序把 PyTorch 实现 CBOW 的完整路径拆开每一步都给可复制的代码和参数解释目标是让你在自己的中英文语料上当天拿到第一版词向量。2. CBOW 到底在学什么输入输出与选型理由2.1 从一句话看懂 CBOW 的训练目标CBOWContinuous Bag-of-Words的核心假设很朴素一个词的语义由它周围的词决定。给定窗口内的上下文词预测中心词。比如句子“模型 训练 需要 大量 语料”窗口为 2 时用“模型 训练 大量 语料”去预测“需要”。模型学到的不是这个预测任务本身而是训练过程中产生的词嵌入矩阵——那才是我们要的词向量。和 skip-gram 的区别值得说清楚skip-gram 是用中心词预测上下文低频词表现更好但训练慢CBOW 是把上下文词向量求平均后预测中心词训练快、高频词更准。中英文混合语料里中文分词后的词频分布往往比英文更集中CBOW 的平滑效果反而占优。如果你的语料里长尾专有名词特别多再考虑换 skip-gram。2.2 为什么选 PyTorch 而不是 gensimgensim 的 Word2Vec 确实三行代码就能跑但它的封装太厚窗口滑动、负采样、动态学习率这些细节你改不动。用 PyTorch 手写 CBOW你能控制中文分词后如何拼接英文 token负采样表怎么建、采样几个负例嵌入维度、窗口大小、最小词频的联动训练中途如何保存检查点、如何做增量训练代价是代码量从 3 行变成 150 行左右但换来的是可调试、可复现、可迁移到自定义损失函数。对于要落地到生产环境的场景这个交换是值得的。2.3 中英文混合语料的预处理边界中英文混在一起时最容易被忽略的是 token 化策略。常见做法是中文用 jieba 分词英文用空格和标点切分数字统一替换为NUM避免“2024”和“2025”被当成无关词英文统一转小写中文不做大小写处理标点保留为独立 token 还是丢弃取决于下游任务做相似度计算时建议丢弃这里有一个血泪经验如果中文分词后不把英文单词单独切出来jieba 会把“PyTorch实现”切成一个整体导致“PyTorch”和“实现”的向量都学不好。正确做法是先用正则把英文片段抽出来再对中文片段做 jieba 分词最后按原顺序合并。3. 从原始文本到训练样本预处理与词表构建3.1 中英文混合分词脚本import re import jieba from collections import Counter def tokenize_mixed(text): 中英文混合分词 1. 用正则把英文单词、数字、中文片段分开 2. 中文片段走 jieba英文片段直接保留 3. 统一转小写数字替换为 NUM # 先按英文单词/数字/中文块切分 pattern re.compile(r([a-zA-Z]|\d|[^\x00-\x7F])) tokens [] for match in pattern.finditer(text): seg match.group() if re.match(r[a-zA-Z], seg): tokens.append(seg.lower()) elif re.match(r\d, seg): tokens.append(NUM) else: # 中文片段走 jieba tokens.extend(jieba.lcut(seg)) # 过滤空白和单字符标点 tokens [t.strip() for t in tokens if t.strip() and not re.match(r^[^\w]$, t)] return tokens # 示例 text PyTorch实现CBOW模型训练2024年的语料 print(tokenize_mixed(text)) # 输出类似[pytorch, 实现, cbow, 模型, 训练, NUM, 年, 的, 语料]这段代码的关键在正则的[^\x00-\x7F]它匹配所有非 ASCII 字符也就是中文块。英文和数字被单独捕获后分别处理。注意NUM这个特殊 token 要加入词表否则会被当成未知词。参数说明jieba.lcut返回列表比jieba.cut生成器更方便后续统计。如果你有自定义词典在jieba.lcut之前调用jieba.load_userdict加载。3.2 构建词表与最小词频过滤def build_vocab(tokenized_corpus, min_freq5): 统计词频过滤低频词建立词到索引的映射 counter Counter() for tokens in tokenized_corpus: counter.update(tokens) # 保留频率 min_freq 的词 vocab [word for word, freq in counter.items() if freq min_freq] # 特殊 token vocab [PAD, UNK] sorted(vocab) word2idx {word: idx for idx, word in enumerate(vocab)} idx2word {idx: word for word, idx in word2idx.items()} # 计算负采样用的词频分布取 0.75 次方 freqs [counter[word] ** 0.75 for word in vocab[2:]] total sum(freqs) neg_probs [f / total for f in freqs] return word2idx, idx2word, neg_probs # 假设 tokenized_corpus 是分词后的列表的列表 # word2idx, idx2word, neg_probs build_vocab(tokenized_corpus, min_freq5)min_freq5是经验值语料小于 10 万词时调到 2 或 3大于 100 万词时调到 10。负采样概率用 0.75 次方是 Word2Vec 原论文的做法能压低高频词被采样的概率让低频词有机会被更新。3.3 生成 CBOW 训练对def generate_cbow_pairs(tokenized_corpus, word2idx, window_size5): 生成 (上下文索引列表, 中心词索引) 对 上下文取中心词左右各 window_size 个词不足则截断 pairs [] unk_idx word2idx[UNK] for tokens in tokenized_corpus: indices [word2idx.get(t, unk_idx) for t in tokens] for center in range(len(indices)): left max(0, center - window_size) right min(len(indices), center window_size 1) context indices[left:center] indices[center1:right] if len(context) 0: continue pairs.append((context, indices[center])) return pairs # pairs generate_cbow_pairs(tokenized_corpus, word2idx, window_size5)窗口大小 5 意味着左右各 5 个词总共 10 个上下文词。中英文混合语料里中文句子通常比英文短窗口 5 可能覆盖整句英文长句则只覆盖局部。如果语料以短文本为主窗口调到 3 更合适。4. PyTorch 实现 CBOW模型、负采样与训练循环4.1 模型定义与负采样损失import torch import torch.nn as nn import torch.optim as optim import numpy as np class CBOWModel(nn.Module): def __init__(self, vocab_size, embed_dim): super().__init__() # 输入嵌入上下文词 - 向量 self.in_embed nn.Embedding(vocab_size, embed_dim) # 输出嵌入用于计算得分 self.out_embed nn.Embedding(vocab_size, embed_dim) # 初始化 init_range 0.5 / embed_dim self.in_embed.weight.data.uniform_(-init_range, init_range) self.out_embed.weight.data.uniform_(-init_range, init_range) def forward(self, context, target, neg_samples): context: (batch, context_len) 上下文词索引 target: (batch,) 中心词索引 neg_samples: (batch, n_neg) 负采样词索引 # 上下文词向量求平均 ctx_vec self.in_embed(context).mean(dim1) # (batch, embed_dim) # 正样本得分 target_vec self.out_embed(target) # (batch, embed_dim) pos_score torch.sum(ctx_vec * target_vec, dim1) # (batch,) pos_loss -torch.log(torch.sigmoid(pos_score) 1e-10) # 负样本得分 neg_vec self.out_embed(neg_samples) # (batch, n_neg, embed_dim) neg_score torch.bmm(neg_vec, ctx_vec.unsqueeze(2)).squeeze(2) # (batch, n_neg) neg_loss -torch.sum(torch.log(torch.sigmoid(-neg_score) 1e-10), dim1) return (pos_loss neg_loss).mean()负采样损失的本质是把多分类问题转成二分类正样本对打高分负样本对打低分。1e-10是防止 log(0) 的数值稳定项。in_embed和out_embed分开是 Word2Vec 的标准做法训练完后通常取in_embed作为词向量也有把两者相加或拼接的变体。4.2 批处理与负采样表def collate_batch(pairs, neg_probs, n_neg5): 把一个 batch 的 (context, target) 转成张量并采样负例 contexts, targets zip(*pairs) # 上下文 padding 到同一长度 max_len max(len(c) for c in contexts) context_tensor torch.zeros(len(contexts), max_len, dtypetorch.long) for i, c in enumerate(contexts): context_tensor[i, :len(c)] torch.tensor(c, dtypetorch.long) target_tensor torch.tensor(targets, dtypetorch.long) # 负采样按 neg_probs 分布采样 neg_tensor torch.multinomial( torch.tensor(neg_probs, dtypetorch.float), num_sampleslen(targets) * n_neg, replacementTrue ).view(len(targets), n_neg) return context_tensor, target_tensor, neg_tensor # 训练循环 def train_cbow(pairs, vocab_size, embed_dim128, batch_size512, epochs5, lr0.001): model CBOWModel(vocab_size, embed_dim) optimizer optim.Adam(model.parameters(), lrlr) for epoch in range(epochs): np.random.shuffle(pairs) total_loss 0 for i in range(0, len(pairs), batch_size): batch pairs[i:ibatch_size] ctx, tgt, neg collate_batch(batch, neg_probs, n_neg5) optimizer.zero_grad() loss model(ctx, tgt, neg) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, Loss: {total_loss / (len(pairs)//batch_size):.4f}) return modeln_neg5是负采样个数语料越大可以调到 10 到 15。batch_size512在 16GB 显存上跑 128 维嵌入没问题如果显存小就降到 128。Adam 的学习率 0.001 是安全起点训练 loss 不降时先检查负采样表是否归一化。4.3 训练参数怎么调一张表说清参数推荐范围影响调整信号embed_dim64~256维度越高表达力越强但过拟合风险大相似度任务差就升到 200window_size3~10窗口大捕捉主题相似窗口小捕捉功能相似短文本用 3长文档用 8min_freq2~10过滤低频词减少噪声词表超过 10 万就调高n_neg5~15负例越多训练越慢但更准loss 震荡就降到 5batch_size128~1024影响梯度稳定性显存不够就减半lr0.0005~0.005太大不收敛太小收敛慢loss 不降就乘 0.5这张表建议打印出来贴在显示器边上调参时逐项改不要一次动三个以上。5. 避坑与排查中英文 CBOW 训练中的五个翻车现场5.1 中文分词把英文单词切碎现象训练出的词向量里“pytorch”和“torch”相似度极低甚至“pytorch”本身找不到。原因jieba 默认词典不含英文技术词会把“PyTorch”切成“Py”和“Torch”或者直接按字符切。解决在分词前用正则把英文片段整体抽出如 3.1 节代码所示。另外把领域英文词加入 jieba 自定义词典调用jieba.add_word(pytorch)。5.2 负采样表未归一化导致采样崩溃现象训练几个 batch 后 loss 变成 nan。原因torch.multinomial要求输入是概率分布如果neg_probs里有负数或总和不为 1会采样出非法索引。解决构建neg_probs后加一行neg_probs [max(p, 1e-10) for p in neg_probs]再除以总和。另外确保PAD和UNK不参与负采样。5.3 窗口滑动时上下文为空现象某些短句如只有一个词生成训练对时 context 为空列表导致mean(dim1)报错。原因中心词左右都没有词时上下文长度为 0。解决在generate_cbow_pairs里加判断if len(context) 0: continue如 3.3 节代码所示。更稳妥的做法是给空上下文填PAD但会引入噪声直接跳过更干净。5.4 学习率过大导致嵌入向量发散现象loss 先降后升最终稳定在一个很高的值。原因Adam 的默认学习率 0.001 对某些语料偏大尤其是词表超过 20 万时。解决把学习率降到 0.0005或者加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。如果还不行换 SGD 加动量 0.9学习率 0.01。5.5 中英文词频差异导致英文词向量质量差现象中文词的相似度结果合理英文词的最近邻全是无关词。原因中文分词后词频普遍高于英文单词负采样时英文词被采到的概率低更新次数少。解决对英文词单独做上采样或者在负采样概率里给英文词一个最低权重。简单做法是在build_vocab时对英文词频乘以 1.5 再算 0.75 次方。6. 验证词向量质量与增量训练技巧训练完不是终点怎么判断这版词向量能不能用以及新语料来了怎么接着训才是落地时最常被问的。先说验证。最直接的方法是找几组已知相似的词看余弦相似度。比如“模型”和“算法”、“训练”和“学习”、“pytorch”和“tensorflow”。如果相似度低于 0.3说明嵌入维度或窗口大小需要调。另一个方法是做类比任务国王 - 男人 女人 ≈ 女王。中英文混合语料里可以试“北京 - 中国 美国 ≈ 华盛顿”但中文词和英文词混在一起时类比效果会打折建议中英文分开验证。def cosine_sim(word1, word2, model, word2idx, idx2word): if word1 not in word2idx or word2 not in word2idx: return None vec1 model.in_embed.weight[word2idx[word1]].detach() vec2 model.in_embed.weight[word2idx[word2]].detach() return torch.cosine_similarity(vec1.unsqueeze(0), vec2.unsqueeze(0)).item() # 示例 # print(cosine_sim(模型, 算法, model, word2idx, idx2word)) # print(cosine_sim(pytorch, tensorflow, model, word2idx, idx2word))如果相似度普遍偏低优先升embed_dim到 200再把window_size从 5 降到 3。如果相似度虚高所有词都在 0.8 以上说明嵌入空间坍缩了检查负采样个数是否太少或者学习率是否太小。再说增量训练。新语料来了不要从头训。正确做法是用旧词表把新语料转成索引未知词标UNK加载旧模型权重用新语料继续训练 1 到 2 个 epoch学习率降到原来的十分之一如果新语料里有大量新词先扩充词表再冻结旧词的嵌入只训练新词的嵌入# 增量训练示例 model.load_state_dict(torch.load(cbow_old.pt)) optimizer optim.Adam(model.parameters(), lr0.0001) # 学习率降 10 倍 # 然后用新语料的 pairs 跑 1-2 个 epoch最后说一个我自己的习惯每次训练完把词向量导出成文本格式用gensim.models.KeyedVectors.load_word2vec_format加载后做一次最近邻检查。这一步能快速发现“某个词向量全是 0”或“所有词都挤在一起”这类低级错误。导出的代码很简单def save_vectors(model, idx2word, pathcbow_vectors.txt): vectors model.in_embed.weight.detach().cpu().numpy() with open(path, w, encodingutf-8) as f: f.write(f{len(idx2word)} {vectors.shape[1]}\n) for idx, word in idx2word.items(): vec_str .join(f{v:.6f} for v in vectors[idx]) f.write(f{word} {vec_str}\n) # save_vectors(model, idx2word, cbow_vectors.txt)这个格式是 Word2Vec 的标准文本格式第一行是词数和维度后面每行是词加向量。导出后可以用任何支持该格式的工具加载方便对比不同版本的效果。希望帮到你。本文还有配套的精品资源点击获取