首页/新闻资讯/正文详情

PyTorch从零实现CBOW词向量:中英混排语料与可复现训练

发布时间:2026/11/5 4:20:39 来源:云帆数科 栏目:资讯中心
PyTorch从零实现CBOW词向量:中英混排语料与可复现训练
简介面向自然语言处理初学者与课程实践者的一份词向量训练作业资源围绕 PyTorch 实现的 CBOW 模型展开可用于完成中英文语料上的词向量训练、停用词处理与结果输出适合作为课堂作业参考或 word2vec 入门练习。压缩包共 12 个文件约 13.58MB主要由 txt 语料与词向量文本、xml 项目配置、Python 训练脚本及少量工程文件组成按数据、脚本、输出等模块组织便于直接定位训练流程与实验产物。已有 2695 人学习说明该作业案例具有一定参考价值。读者可从中获得可运行的中英文 CBOW 训练代码、较完整的数据组织方式、停用词处理思路以及训练后词向量文件的输出示例并据此理解词向量模型的输入构造、参数设置与中英文语料切换方法适合对照自己的实验过程查漏补缺。1. 一份中英混排语料摆在面前最省事的做法反而是自己用 PyTorch 写一遍 CBOW前阵子接了个活儿手里有一批中英混排的技术文档和问答记录下游是个小分类任务想配一份领域词向量。上手现成工具包后才发现词表不可控中文分词口径、标点清洗、unk归并全是黑匣子下游效果一波动就查不出原因。索性用 PyTorch 从零搭了个 CBOW 词向量训练流程语料中英文各一份词表、滑窗、负采样、学习率这些全部摊开在配置里。这套东西适合两类人想搞明白 word2vec 到底怎么算出来的学习者以及需要在私有语料上稳定复现训练结果的开发者。它不追求跑分追求的是每一步都能改、每一步都能看。2. 项目骨架目录职责、依赖版本和双语料的预处理分叉先把整体摆开。这套实现刻意压小了模型部分把复杂度全部压到数据和配置上因为训词向量翻车的地方九成不在网络结构而在语料和词表。2.1 目录结构与每个文件的职责目录按「配置 - 数据 - 核心逻辑 - 脚本入口」四层切开训练脚本不写业务逻辑只做参数解析和串联cbow_word2vec/ ├── configs/ │ └── default.yaml # 所有超参集中在这里代码里不写魔数 ├── data/ │ ├── raw/ │ │ ├── zh_corpus.txt # 一行一句的中文语料 │ │ └── en_corpus.txt # 一行一句的英文语料 │ └── processed/ │ ├── vocab.json # stoi 映射 │ └── itos.json # 严格按 id 顺序存的词表必须和模型一起落盘 ├── src/ │ ├── config.py # yaml - dataclass带默认值和类型校验 │ ├── tokenizer.py # 中英文两条分词线 │ ├── vocab.py # 词表构建、特殊符号、持久化 │ ├── dataset.py # 滑窗采样、高频词下采样、collate │ ├── negative_sampling.py # 0.75 次方噪声分布表 │ ├── model.py # CBOW 网络 │ └── trainer.py # 训练循环、日志、checkpoint ├── scripts/ │ ├── build_vocab.py # 只建词表不训练 │ ├── train.py # 训练入口 │ └── evaluate.py # 近邻检索、类比推理 └── requirements.txtbuild_vocab.py单独拎出来是个关键设计。词表构建要扫全量语料几十万句要跑几分钟如果和训练耦在一起每次调batch_size都得重扫一遍血泪经验。词表落盘之后训练阶段只读 json改超参随便改。itos.json单独存一份不是为了好看。nn.Embedding的第 0 行是pad、第 1 行是unk这个顺序就是权重矩阵的行号。哪天你重新构建了词表但用了旧权重向量和词就整体错位了模型不报错相似词全乱。这是最典型的后悔药场景。配置层用 dataclass 包一层好处是 IDE 能补全、拼错字段名会直接 raise# src/config.py from dataclasses import dataclass, field import yaml dataclass class CorpusConfig: zh_path: str data/raw/zh_corpus.txt en_path: str data/raw/en_corpus.txt min_count: int 5 # 低于这个词频直接进 unk中文语料别设太小 max_vocab: int 200000 # 词表上限防止输出层参数爆炸 share_vocab: bool True # 中英文是否共用一张词表 lowercase: bool True dataclass class ModelConfig: embed_dim: int 200 window_size: int 5 negative: int 5 # 每个正样本配几个负样本 subsample: float 1e-5 # 高频词下采样阈值 t dataclass class TrainConfig: batch_size: int 1024 epochs: int 5 lr: float 1e-3 clip_grad: float 5.0 num_workers: int 4 seed: int 42 device: str cuda dataclass class Config: corpus: CorpusConfig field(default_factoryCorpusConfig) model: ModelConfig field(default_factoryModelConfig) train: TrainConfig field(default_factoryTrainConfig) def load_config(path: str) - Config: with open(path, encodingutf-8) as f: raw yaml.safe_load(f) or {} # 只覆盖 yaml 里出现的字段其余走默认值 return Config( corpusCorpusConfig(**raw.get(corpus, {})), modelModelConfig(**raw.get(model, {})), trainTrainConfig(**raw.get(train, {})), )**raw.get(corpus, {})这种写法有个副作用yaml 里多写一个不存在的字段会直接TypeError。这恰恰是想要的配置文件拼错字段名比跑完之后发现参数没生效强得多。share_vocab这个开关要说清楚。如果中英文来自同一份对照文档同一段里既有中文术语又有英文术语共享词表有意义共现能把「embedding」和「嵌入」拉到相近位置。如果只是把两份独立语料拼一起共享词表只会让两个语言空间勉强挤在一块最后中文词的近邻全是英文高频虚词得不偿失。我的默认做法是语料有对齐关系才共享否则分开训两份、各存各的权重。2.2 中英文两条分词线别指望一个函数通吃中文和英文的 token 边界规则完全不同混在一个函数里做最后一定是一堆 if。分成两个函数各自返回list[str]后面统一进词表。# src/tokenizer.py import re import unicodedata import jieba # 英文单词含缩写撇号、小数、整数 EN_PATTERN re.compile(r[a-z](?:[a-z])?|\d\.\d|\d) def normalize(text: str) - str: NFKC 归一化全角字母数字转半角消除同一字符的多种码位写法 text unicodedata.normalize(NFKC, text) # 零宽字符和软连字符会凭空造出重复词直接清掉 for ch in (\u200b, \u200c, \u00ad): text text.replace(ch, ) return re.sub(r\s, , text).strip() def tokenize_zh(line: str) - list[str]: line normalize(line) tokens [] for tok in jieba.lcut(line): tok tok.strip() # 丢掉纯标点、纯空白保留中文、字母和数字 if not tok: continue if all(not ch.isalnum() for ch in tok): continue tokens.append(tok.lower()) # 中文不受影响英文统一小写 return tokens def tokenize_en(line: str) - list[str]: return EN_PATTERN.findall(normalize(line).lower())normalize里做 NFKC 是为了解决全角数字、全角括号导致「」和「2024」被当成两个词的问题。有个坑要提前说NFKC 会顺手把①变成1、㈱变成(株)中文正文里这种字符少问题不大但如果语料里有大量特殊符号建议改成只做全角转半角的手工映射。句子边界在预处理阶段就切好一行一句jieba.lcut不跨行。CBOW 的窗口是句内窗口跨句滑窗会把「上一段结尾」和「下一段开头」硬凑成上下文学出来的向量质量会明显下降。预处理跑成一次性脚本python -m scripts.build_vocab \ --config configs/default.yaml \ --out data/processed2.3 依赖清单和第一次跑起来的完整命令依赖不多装完就能跑pip install torch2.0 jieba pyyaml numpy tqdmjieba首次加载词典要 1 秒左右num_workers 0时每个 worker 都会加载一次进程一多内存会明显涨。语料在百万句以内建议num_workers: 2别一上来就 8。词汇表构建 训练 评测三步走# 1. 建词表扫全量语料输出 vocab.json / itos.json python -m scripts.build_vocab --config configs/default.yaml # 2. 训练 python -m scripts.train --config configs/default.yaml --out checkpoints/ # 3. 看近邻 python -m scripts.evaluate --ckpt checkpoints/cbow_best.pt --word 神经网络 --topk 10训练日志里我固定打三样东西loss、lr、每秒处理的样本对数pairs/s。只有 loss 一个数字的时候出问题根本分不清是数据管线慢还是模型慢。pairs/s这个指标很朴素但比GPU-Util靠谱后者在小白表上经常显示 0。3. 数据管线拆解从一个句子到一条 (上下文, 中心词, 负样本) 训练对数据管线是这套实现里代码最多的部分也是最值得抄作业的部分。CBOW 的模型只有十几行训不好基本都能怪到这里来。3.1 词表构建与四个特殊标记词表构建看着简单顺序问题能让人调半天。核心原则排序必须完全确定。Counter.most_common()在词频相同时顺序依赖插入顺序同一份语料两次构建可能得到不同的 id 分配权重就白训了。# src/vocab.py import json from collections import Counter PAD, UNK, BOS, EOS pad, unk, bos, eos def build_vocab(sentences, min_count5, max_vocab200000): counter Counter(tok for sent in sentences for tok in sent) # 先按词频降序词频相同按字典序升序保证结果稳定可复现 items sorted(counter.items(), keylambda kv: (-kv[1], kv[0])) itos [PAD, UNK, BOS, EOS] for tok, cnt in items: if cnt min_count or len(itos) max_vocab: break itos.append(tok) stoi {tok: i for i, tok in enumerate(itos)} return stoi, itos def save_vocab(stoi, itos, out_dir): with open(f{out_dir}/vocab.json, w, encodingutf-8) as f: json.dump(stoi, f, ensure_asciiFalse) with open(f{out_dir}/itos.json, w, encodingutf-8) as f: json.dump(itos, f, ensure_asciiFalse)min_count5这个值在中英文上体感完全不同。英文语料几十万句5 够用中文语料如果规模小比如十万句以内5 会把大量低频专业术语全推进unk这时候调到 23 更合适。判断依据很简单建完词表看一眼unk在验证语料里的出现比例超过 5% 就说明阈值设高了。bos和eos在当前实现里不参与训练只是个占位方便后面接 RNN 类下游任务时词表不用重建。如果纯做词向量这两个 id 可以保留但完全可以不管。max_vocab主要防输出层参数爆炸。词表 20 万、embed_dim200光输入 Embedding 就是 4000 万参数输出矩阵再来 4000 万。如果用完整 softmax两个矩阵都要算梯度显存直接翻倍。3.2 滑动窗口采样与高频词下采样CBOW 的标准做法是对句子里的每个位置i取[i-w, iw]去掉i本身作为上下文预测i位置的词。原版 word2vec 还做了两件事——动态窗口和二次采样这两件事对最终向量质量影响极大。# src/dataset.py import math import random from torch.utils.data import Dataset class CBOWDataset(Dataset): def __init__(self, sentences, stoi, window5, subsample1e-5, seed42): self.window window rng random.Random(seed) unk_id stoi[unk] # 用 id 序列跑采样比反复查 stoi 快一个数量级 id_sents [[stoi.get(t, unk_id) for t in s] for s in sentences] total sum(len(s) for s in id_sents) or 1 freq Counter(t for s in id_sents for t in s) # word2vec 的保留概率P(keep) (sqrt(f/t) 1) * t / f keep {} for wid, c in freq.items(): f c / total p (math.sqrt(f / subsample) 1) * subsample / f keep[wid] min(p, 1.0) self.pairs [] for sent in id_sents: kept [w for w in sent if rng.random() keep[w]] if len(kept) 2: continue for i, center in enumerate(kept): # 动态窗口实际半径在 1..window 之间随机远距离词出现次数更少 radius rng.randint(1, window) ctx kept[max(0, i - radius):i] kept[i 1:i radius 1] if ctx: self.pairs.append((ctx, center)) def __len__(self): return len(self.pairs) def __getitem__(self, idx): return self.pairs[idx]动态窗口这一手很容易被忽略。固定窗口会让 5 个距离上的词被采样次数一样多而语言里近邻词的信息量明显更高。radius randint(1, window)效果上是距离 1 被采到的概率是 1距离 5 只有 1/5正好符合直觉。二次采样处理的是「的、了、the、of」这类超高频词。它们的向量在训练好的模型里通常也不差因为出现太多但它们贡献了海量训练对却不提供区分信息白白占掉训练时间。《Attention is all you need》里那句「高频词提供的信号很少」说的就是这事儿。subsample1e-5是默认值语料越小可以调大一点比如 1e-3免得句子被削得太短。一个细节下采样之后句子的长度变了位置i的含义和原句对不上了。这对 CBOW 没影响因为上下文关系仍然成立但如果你的下游要做位置相关的建模就得重新考虑。3.3 负采样表与批次组装完整 softmax 在 20 万词表上是跑不动的所以要上负采样每个正样本配 K 个随机词作为负例把多分类降成二分类。负样本的抽样分布是freq^0.75这个 0.75 次方是原版论文的经验值作用是压一压超高频词被抽中的概率抬一抬低频词。# src/negative_sampling.py import numpy as np class NegativeSampler: def __init__(self, counts, power0.75, table_size1_000_000, seed42): p np.asarray(counts, dtypenp.float64) ** power p / p.sum() rng np.random.default_rng(seed) # 预先抽好一张大表训练时直接查表省掉每次 multinomial 的开销 self.table rng.choice(len(counts), sizetable_size, pp) self.table_size table_size def sample(self, batch_size, k, rngNone): rng rng or np.random idx rng.randint(0, self.table_size, size(batch_size, k)) return self.table[idx]table_size1_000_000是内存和精度的折中占 8MB 左右int64。词表很大百万级时可以用int32省一半内存。这张表在建DataLoader之前构建一次训练循环里只做randint开销可以忽略。组批用自定义collate_fn因为上下文长度不固定要 pad# src/dataset.py 续 import numpy as np import torch def make_collate(pad_id: int): def collate(batch): ctxs, centers zip(*batch) max_len max(len(c) for c in ctxs) ctx_arr np.full((len(ctxs), max_len), pad_id, dtypenp.int64) for i, c in enumerate(ctxs): ctx_arr[i, :len(c)] c return { ctx: torch.from_numpy(ctx_arr), center: torch.tensor(centers, dtypetorch.long), } return collatepad_id必须是 0而且模型里nn.Embedding要显式传padding_idx0。光靠 mask 手动清零也能跑但padding_idx会让 pad 行的梯度恒为 0权重永远是初始化的全零向量后面做相似度计算的时候这一行不会跑出来干扰结果。num_workers 0时注意random.Random(seed)在__init__里已经跑完了__getitem__只做取数所以样本生成是确定性的。这样每次训练拿到的训练对完全一致调参时变量才可控。4. CBOW 模型和训练循环一个 Embedding、一个平均、一个带权输出模型部分短但每个选择都有理由。写清楚这些理由比把代码贴出来更有用。4.1 网络结构输入求和/平均 输出词表映射CBOW 的假设很粗暴一个窗口里所有上下文词的向量加起来能代表中心词。所以结构就是 Embedding → 池化 → 输出层。# src/model.py import torch import torch.nn as nn import torch.nn.functional as F class CBOW(nn.Module): def __init__(self, vocab_size: int, embed_dim: int 200, pad_id: int 0): super().__init__() # 输入矩阵和输出矩阵分离这是 word2vec 原版的做法 self.in_embed nn.Embedding(vocab_size, embed_dim, padding_idxpad_id) self.out_embed nn.Embedding(vocab_size, embed_dim, padding_idxpad_id) # 初始化为小均匀分布范围 0.5/dim避免一开始点积过大 nn.init.uniform_(self.in_embed.weight, -0.5 / embed_dim, 0.5 / embed_dim) nn.init.zeros_(self.out_embed.weight) def encode(self, ctx): mask (ctx ! 0).unsqueeze(-1).float() # (B, L, 1) emb self.in_embed(ctx) * mask # pad 行归零 denom mask.sum(dim1).clamp(min1.0) # (B, 1)防除零 return emb.sum(dim1) / denom # (B, D) 平均池化 def forward(self, ctx, center, negs): ctx_vec self.encode(ctx) # (B, D) center_vec self.out_embed(center) # (B, D) neg_vec self.out_embed(negs) # (B, K, D) pos_score (ctx_vec * center_vec).sum(dim-1) # (B,) neg_score torch.bmm(neg_vec, ctx_vec.unsqueeze(-1)).squeeze(-1) # (B, K) loss -F.logsigmoid(pos_score).mean() - F.logsigmoid(-neg_score).mean() return loss平均还是求和是个需要说清楚的点。原版 word2vec 用的是求和因为它的学习率是按 batch 大小和窗口大小一起调的求和相当于把梯度缩放了1/L。这里用平均好处是切换窗口大小比如从 5 改到 10时梯度量级不漂调参更省心。两种写法在数学上只差一个常数因子最终学到的向量方向基本一致但学习率要跟着改。翻车案例里有一半是改了窗口大小忘了改 lrloss 曲线直接起飞。输入、输出两个矩阵分开是另一个容易搞混的地方。word2vec 训完之后只取输入矩阵in_embed.weight作为词向量输出矩阵丢掉。有人问为什么不共享——共享之后高频词的输入向量会被输出层的梯度主导低频词反过来向量分布会畸形。这事没有绝对答案但对小语料分离更稳。4.2 两种损失实现完整 softmax 和负采样怎么选模型里只写了负采样版。如果要换成完整 softmax词表小于 5 万时可行改动很小class CBOWSoftmax(CBOW): def __init__(self, vocab_size, embed_dim200, pad_id0): super().__init__(vocab_size, embed_dim, pad_id) # 输出矩阵换成 vocab_size x vocab_size 的显式投影 self.proj nn.Linear(embed_dim, vocab_size, biasFalse) def forward(self, ctx, center): ctx_vec self.encode(ctx) logits self.proj(ctx_vec) # (B, V) return F.cross_entropy(logits, center)选型看词表规模和语料量方案词表上限每步计算量适用场景完整 softmax约 5 万O(B×V×D)小语料快速验证、教学演示负采样 K5百万级O(B×K×D)真实语料训练层次 softmax百万级O(B×logV×D)词表极大且不方便调 K手工算一下B1024、V200000、D200完整 softmax 一步的矩阵乘大约是 4×10^10 次浮点运算光是输出层就吃掉整个 GPU换成 K5计算量降到约 10^9差 40 倍。这就是为什么真正的语料训练几乎都用负采样。负采样的 K 值不是越大越好。K5 是原版默认语料小百万词以内可以到 1015语料大就 25。K 变大不会让向量质量线性变好但训练时间线性上升。4.3 训练循环、学习率衰减与显存控制训练循环里我固定加了三样东西seed 固定、梯度裁剪、学习率线性衰减。# src/trainer.py节选核心循环 import random import numpy as np import torch def train(model, loader, sampler, cfg, device): random.seed(cfg.seed) np.random.seed(cfg.seed) torch.manual_seed(cfg.seed) model.to(device) optimizer torch.optim.Adam(model.parameters(), lrcfg.lr) total_steps len(loader) * cfg.epochs global_step 0 for epoch in range(cfg.epochs): model.train() for batch in loader: ctx batch[ctx].to(device, non_blockingTrue) center batch[center].to(device, non_blockingTrue) # 负样本 id 在 CPU 上抽抽完再搬比在 GPU 上做 choice 省事 negs torch.from_numpy( sampler.sample(len(center), kcfg.negative) ).to(device, non_blockingTrue) loss model(ctx, center, negs) optimizer.zero_grad(set_to_noneTrue) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.clip_grad) optimizer.step() # 学习率线性衰减到初始值的 1e-4原版 word2vec 的做法 global_step 1 lr cfg.lr * max(1e-4, 1.0 - global_step / total_steps) for g in optimizer.param_groups: g[lr] lr if global_step % 200 0: print(fepoch{epoch} step{global_step} loss{loss.item():.4f} lr{lr:.6f}) torch.save({ model: model.state_dict(), itos: cfg.itos, embed_dim: cfg.embed_dim, }, f{cfg.ckpt_dir}/cbow_epoch{epoch}.pt)zero_grad(set_to_noneTrue)比默认的zero_grad()省一次全参数写零大模型上能省几个百分点的时间小模型差别不大但习惯养起来没坏处。clip_grad_norm_在负采样里很有必要。正样本得分是logsigmoid负样本是logsigmoid(-x)一旦某批样本的得分跑到 ±10 以外梯度就会尖峰学习率 1e-3 下可能直接把向量打飞。裁剪到 5.0 是个保险值。衰减这块Adam 线性衰减和 SGD 线性衰减效果都不错。但有个细节Adam 自带二阶动量衰减到1e-4倍之后基本就不动了所以epochs不宜太少。小语料跑 5 个 epoch、每 epoch 都被DataLoader完整扫一遍就够大语料 3 个 epoch 也常见。checkpoint 里必须存itos。加载的时候先读itos再load_state_dict两边行号严格对齐。少了这一步重训一次词表或者改一次min_count加载出来的向量就是一群随机排序的数。5. 常见问题排查loss 不降、近邻全是虚词、复现不了这套流程跑起来不难难的是结果不对的时候怎么定位。下面五条是我自己在不同语料上真实遇到过的。5.1 现象loss 停在 4.5 左右不动或者降得很慢原因负采样版的 loss 绝对值没有可比性-log σ(x) - log σ(-x)在随机初始化时大约是 1.38 左右两个 0.69不是 4.x。如果看到 loss 一直在 45 徘徊基本可以判定正负样本的得分没有被拉开而最常见的原因是学习率太大导致 embedding 直接被推到饱和区logsigmoid在饱和区梯度接近 0。解决先看in_embed.weight的范数训练几步之后如果单个向量范数超过 10说明学习率不对。把lr从 1e-3 降到 2e-4 再跑 200 步对比同时打开梯度裁剪。另一个可能原因是负采样表没有传对counts用了错误的全零数组这时负样本全是 id 0正负样本几乎不重叠loss 也降不下去。检查办法是打印一批negs看 id 是否分散。5.2 现象中文相似词全是「的、了、是、在」英文全是 the/of原因高频词没有做二次采样或者subsample设得太大导致句子被削得只剩高频词。还有一种更隐蔽的情况中文语料里混了繁体、全角标点分词之后产生了大量只有一个字符的碎片 token这些碎片在高频词表里挤占了位置。解决先把subsample从 1e-5 调到 1e-3 试一轮观察「的」这类词在训练对里的占比是否下降。同时检查词表前 20 个词如果出现单独的「」「。」说明分词后的标点过滤有漏回到tokenize_zh那里把所有not ch.isalnum()的 token 清掉。另外中文语料如果句长普遍很短比如问答对窗口 5 会覆盖整句上下文几乎一样这时候把window_size降到 23 更合理。5.3 现象GPU 利用率很低一个 epoch 跑几个小时原因九成是数据管线卡在 CPU 上。CBOWDataset.__init__里构建全部pairs是纯 Python 循环几十万句、每句几十个词会产生千万级的训练对构建阶段就要几分钟到十几分钟而且这部分内存是常驻的每个 pair 存一个 listPython 对象的开销远大于实际数据。解决第一把pairs改成两个 numpy 数组ctx_flatctx_offset的 CSR 式存法内存能降一个数量级。第二num_workers设成 24persistent_workersTrue避免每个 epoch 重启 worker 进程。第三batch_size从 1024 往上加CBOW 的模型很小在 16G 显存上跑到 4096 完全没问题批次大了 GPU 才不会等数据。第四负采样sample里用np.random.randint而不是 Python 的random后者在千万次调用下慢得明显。5.4 现象训练完加载 checkpointmost_similar出来的词完全对不上原因词表顺序和权重行号错位。要么是保存 checkpoint 的时候没存itos要么是加载的时候用了重新构建的词表min_count或者语料顺序变了id 分配就全变了。还有一种情况save_vocab写了vocab.json但训练脚本读的是缓存里的旧stoi。解决把itos作为 checkpoint 的一部分强绑定保存加载时永远从 checkpoint 读itos不从磁盘上另外读词表文件。evaluate.py里加一句断言len(itos) model.in_embed.weight.shape[0]不相等直接报错别让它静默跑出一堆乱码结果。5.5 现象同样的配置跑两次结果不一样原因seed 没有全链路固定。torch.manual_seed只固定了 torch 的随机数random和numpy各自有独立状态DataLoader的 worker 进程还会继承父进程状态再各自演化。jieba的分词本身是确定的但只要词表构建的排序不稳定后面全乱。解决训练入口统一设random.seed、np.random.seed、torch.manual_seed三个DataLoader传generatortorch.Generator().manual_seed(seed)。build_vocab的排序用keylambda kv: (-kv[1], kv[0])保证确定性。最后在训练开始前打印一次len(stoi)和词表前 10 个词两次跑出来不一致就能立刻发现。提示num_workers 0时worker 里的随机状态默认由 PyTorch 按 base_seed 派生只要主进程 seed 固定结果是可复现的。但如果你的Dataset.__getitem__里调了random.random()那就得用worker_init_fn单独设种子。6. 二次开发把这份词向量接进下游任务以及怎么验证它真的有用训完词向量最容易犯的错是拿几个相似词看看觉得「还行」就直接上生产。真正该做的验证是把这批向量冻住灌进一个简单的下游模型和随机初始化、和公开预训练向量各跑一遍比较验证集指标。具体做法我一般是这样把in_embed.weight导成numpy按itos建一个{word: vec}的字典存成.npy加一份itos.json下游分类模型里用nn.Embedding.from_pretrained(tensor, freezeTrue)加载接一个 TextCNN 或者平均池化加全连接跑一遍小验证集。三个对照组随机初始化、这批领域词向量、通用词向量。领域语料的量不大时自训向量能比通用向量好 13 个点如果完全没差别说明语料量不够或者min_count太高把关键术语都推进unk了。导出这一步有个小技巧导出前把向量做一次 L2 归一化再存。下游做余弦相似度的时候不用再算范数而且避免某些任务里向量范数过大主导了初始化。import numpy as np import torch def export_vectors(ckpt_path, out_path): ckpt torch.load(ckpt_path, map_locationcpu) itos ckpt[itos] emb ckpt[model][in_embed.weight].numpy().astype(np.float32) # 逐行 L2 归一化pad 行保持全零 norm np.linalg.norm(emb, axis1, keepdimsTrue) emb emb / np.clip(norm, 1e-9, None) emb[0] 0.0 np.save(out_path, emb) with open(out_path.replace(.npy, _itos.json), w, encodingutf-8) as f: json.dump(itos, f, ensure_asciiFalse)emb[0] 0.0这行不能省。pad行的范数是 0归一化之后虽然还是 0但下游如果用argmax找最近邻全零向量和任何向量的余弦相似度都是 0会混进结果里。显式清零再加一个if not vec.any(): continue更稳妥。另一个常见的改造方向是增量更新新来一批语料不想全量重训。可行的做法是加载旧权重把学习率调到原来的 1/10用新语料只跑 1 个 epoch同时保持词表不变新词全部映射到unk。这个做法在多轮迭代里很实用但要注意新语料如果和旧语料的领域差异太大旧向量会被拉偏最好保留旧 checkpoint 做 A/B 对比。从那以后我每次训词向量都把「先落盘itos、再打印词表前 10 个词、最后跑一次近邻自检」当成固定动作三步过不了就不往下游走。希望帮到你。本文还有配套的精品资源点击获取

相关推荐

策略路由与路由策略:核心区别与应用指南
策略路由与路由策略:核心区别与应用指南

1. 网络策略中的两大核心概念在复杂网络架构中,策略路由(Policy-Based Routing)和路由策略(Routing Policy)这两个术语经常被混为一谈。作为网络工程师,我在实际项目中至少遇到过十几次因为概念混淆导致的配… · 2026/11/5 4:20:39

STM32驱动AD9850:低成本DDS信号源与扫频实践
STM32驱动AD9850:低成本DDS信号源与扫频实践

简介:围绕正点原子STM32精英板与AD9850 DDS芯片的波形发生器工程资料,面向具备一定嵌入式基础的电子工程师、高校学生及信号处理、通信实验教学人员,用于解决STM32驱动AD9850输出正弦波、方波、三角波等常见波形的软硬件实现问题。压缩包共20… · 2026/11/5 4:20:39

金融系统架构转型:实时化与云原生的实践路径
金融系统架构转型:实时化与云原生的实践路径

1. 金融联机与批次系统的现状与挑战金融行业的核心业务系统长期以来都围绕着"联机交易"和"批次处理"两大支柱运转。联机系统负责处理实时交易请求,如支付、转账等需要即时响应的业务;批次系统则处理日终结算、报表生成等批量计算任务… · 2026/11/5 4:20:39

DeepSeek Harness 的上下文管理、记忆和知识库剖析
DeepSeek Harness 的上下文管理、记忆和知识库剖析

DeepSeek Harness 里,上下文、记忆和知识库并没有被设计成三个彼此独立的重型系统。它们围绕 Session Log 协同工作。 用三句话概括: 上下文管理负责决定当前这一轮模型能看到什么。系统提示词、动态环境、工具定义和会话消息,会在模型调用… · 2026/11/5 5:19:03

Linux --线程同步与互斥
Linux --线程同步与互斥

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 5:19:03

X-AnyLabeling 图像标注工具快速上手:从安装到 YOLO 格式导出只需 4 步
X-AnyLabeling 图像标注工具快速上手:从安装到 YOLO 格式导出只需 4 步

X-AnyLabeling 图像标注工具快速上手:从安装到 YOLO 格式导出只需 4 步 【免费下载链接】X-AnyLabeling X-AnyLabeling: A lightweight, efficient, and unified cross-platform desktop application for annotating text, image, video, point cloud and multimoda… · 2026/11/5 5:19:03

机器学习:线性回归4(欠拟合,正好拟合,过拟合)
机器学习:线性回归4(欠拟合,正好拟合,过拟合)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 5:19:03

JAX-Fluids 神经网络耦合实战:jaxfluids_nn 训练流场代理模型全流程
JAX-Fluids 神经网络耦合实战:jaxfluids_nn 训练流场代理模型全流程

【免费下载链接】JAXFLUIDS Differentiable Fluid Dynamics Package 项目地址: https://gitcode.com/gh_mirrors/ja/JAXFLUIDS 点击查看 免费下载 JAX-Fluids 是一款完全可微分的计算流体力学(CFD)求解器,基于 JAX 编写&#xff… · 2026/11/5 5:19:03

Linux驱动初始化
Linux驱动初始化

一、嵌入式设备启动流程1.ROM Boot(第一阶段,芯片内置ROM,出厂固化,运行厂家固化在IROM里的代码)初始化基础硬件(需要的时钟和设备):PLL、最小RAM判断启动方式:emmc&… · 2026/11/5 5:18:02

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38

多智能体协作系统架构设计与实践优化
多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39

大模型系统性入门实战指南:从原理到部署微调的全链路路线
大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39

鸿蒙文件系统沙箱机制解析与开发实践
鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码