简介:这是一份基于深度学习的Wiki中文语料Word2Vec词向量模型构建资源包,适合自然语言处理入门者及高校课程设计。资源围绕中文语料的获取、预处理、分词、模型训练与匹配测试的完整流程,给出可直接运行的Python源码和配套设计报告,可帮助读者掌握Word2Vec模型从数据准备到效果验证的基本方法。压缩包共8个文件,以4个Python脚本为核心,辅以设计报告、说明文档、命令文本及License信息,整体约967KB,结构紧凑适合快速上手。已有662人学习/下载。内容涵盖环境准备、语料获取、预处理、模型构建和测试五个实践环节,既有源码也有报告,便于对照学习与二次修改,是完成深度学习或自然语言处理课程设计的实用参考。
1. 基于深度学习的Wiki中文语料word2vec向量模型:它在解决什么问题
如果你做过中文文本召回或者情感分类,大概率会遇到这个场景:用户输入的是模型没见过的说法,你想靠词向量兜底。word2vec 训练出的中文词向量,把“电脑”和“计算机”这类近义词在向量空间里放得很近,下游模型就算没碰到原词,也能靠相似度泛化。标题里的 Wiki 中文语料 + word2vec 是一套非常经典的中文词向量生产方案:Wiki 语料句子规范、同义改写多,比直接抓网页干净;word2vec 又是浅层网络,CPU 上几小时就能跑完一份几 GB 的 dump,不需要深度学习 GPU 环境。这套方案适合做检索、聚类、文本特征工程的从业者,也适合刚接触词向量的新手拿它当第一个完整落地的中文 NLP 项目。它不是黑匣子,语料清洗、参数调整、结果验证都能自己控制。
2. zhwiki 中文语料的清洗与分词:从 dump 到可训练语料
2.1 语料选择与重定向过滤
为什么不用通用爬虫语料?爬虫拿回来的网页噪声太多,正文夹广告、乱码、重复句子;word2vec 的训练信号来自上下文共现,语料里混一句垃圾,词向量就被拉偏一点。Wiki 中文语料每篇文章围绕一个主题展开,句子大多是陈述句,上下文语义集中,训练出来的向量和人的直觉更贴合。要做的是把 wiki 原始 dump 转成纯文本。
常见做法是先用 WikiExtractor 把zhwiki-latest-pages-articles.xml.bz2抽取成 JSON 格式,一个文档一行。抽出来的结果是一个一个文档对象,包含 title 和 text。这里要注意:抽取后的目录是分层的,直接用 glob 递归读文件最省事,不要手工拼路径。
import glob import json import os def valid_docs_from_dir(input_dir): for path in glob.glob(os.path.join(input_dir, '**', '*'), recursive=True): if not os.path.isfile(path): continue with open(path, 'r', encoding='utf-8') as f: for line in f: if not line.strip(): continue doc = json.loads(line) text = doc.get('text', '') # 重定向页面只有一行跳转指令,对训练没有价值 if text.startswith('#REDIRECT'): continue yield doc['title'], text这里按行读 JSON 是为了不把整个 dump 塞进内存,CPU 机器上尤其重要。#REDIRECT判断放在最前面,能省掉后面大量无意义清洗。这个生成器函数是整个预处理的内存底线,后续所有清洗都复用它。glob 的递归模式覆盖 WikiExtractor 按日期或文章名分出来的多层目录,比手写文件名稳定得多。
2.2 清洗流水线:去引用、去模板、统一繁简
Wiki 文本里真正影响训练的是三类东西:引用脚注<ref>...</ref>、信息框模板{{...}}、以及内链[[目标|显示文字]]。引用的网址和日期会把数字灌进分词结果,模板会把大量无关属性名带进正文,内链如果不处理,分词后会出现竖线和方括号碎片。
我一般维护一个五步正则清洗函数,顺序不能乱:
import re def clean_wiki_text(raw): # 1. 注释与脚注整体删除,避免残留网址和日期碎片 text = re.sub(r'<!--.*?-->', '', raw, flags=re.S) text = re.sub(r'<ref[^>]*>.*?</ref>', '', text, flags=re.S) # 2. 模板包含的信息常带竖线属性,先用非贪婪把整块去掉 text = re.sub(r'\{\{[^{}]*\}\}', '', text) # 3. 内链只保留竖线后的显示文字 text = re.sub(r'\[\[[^\[\]]*\|([^\[\]]*)\]\]', r'\1', text) text = re.sub(r'\[\[([^\[\]]*)\]\]', r'\1', text) # 4. 外链、HTTP 与残留 HTML 标签 text = re.sub(r'(https?://|www\.)\S+', '', text) text = re.sub(r'<[^>]+>', '', text) # 5. 去掉表格控制符与连续空白 text = text.replace('{|', ' ').replace('|}', ' ') text = re.sub(r'\s+', ' ', text).strip() return text第 2 步的正则对嵌套模板无能为力,但 wiki 信息框的嵌套深度通常只有一层,实际跑下来足够。第 3 步把[[目标|显示文本]]保留成显示文本,是要保住一句话里的自然语义,比如[[北京大学|北大]]始建于1898年,最后应该保留“北大始建于1898年”。这个清洗流程对后来做 LLM 知识库抽取的人同样有参考价值,很多 RAG 项目卡住的不是向量模型,而是源头文本太脏。
清洗后要做繁简转换。zhwiki 里繁体和简体内容可能同时存在,“程序”和“程式”如果同时进词表,词向量训练时会被拆成两个低频向量。我用 OpenCC 统一转简体:
opencc -i zhwiki_clean.txt -o zhwiki_simplified.txt -c t2s.jsont2s.json表示繁体转简体。转换后最好再抽样看几行,确认人名地名没有错转,尤其台湾、香港译名,OpenCC 对一部分专名会转成大陆说法,这在词向量任务里通常可以接受。如果机器上没有 opencc 命令,也可以用 Python 的 opencc-python 包,接口是一样的,正文语料大时命令行的速度更快。
2.3 分句与分词:给 word2vec 的句子定一个“边界”
Wiki 里一篇文章经常一段几百字。word2vec 的窗口是在句子范围内滑动的,如果一段话太长,语境会被无关主题污染。所以分词之前先按中文标点把文本切成短句,每一句作为训练样本里的一条 sentence。
import jieba SENT_SPLIT = re.compile(r'[。!?!?;;]') def segment_and_tokenize(text): sentences = [] for chunk in SENT_SPLIT.split(text): if len(chunk) < 4: continue tokens = jieba.cut(chunk, cut_all=False) words = [w for w in tokens if w.strip()] if len(words) >= 3: sentences.append(' '.join(words)) return sentencescut_all=False是精确模式,会按词典做最长匹配,比如“中华人民共和国”会被切成完整词而不是拆成“中华/人民/共和国”。分句长度小于 4 的碎片直接丢掉,能过滤掉年份、编号这类没有上下文的噪声。词表之外的专名可以准备一个 userdict.txt,用jieba.load_userdict('userdict.txt')加载,里面有 3D 打印这类长词时,分词稳定很多。
分词完成以后不要做停用词过滤。word2vec 需要“的、了、在”这类虚词作为句法胶水,强行删掉会把“我的电脑”变成“我 电脑”,窗口里的共现关系反而被破坏。虚词问题留给模型参数里的 sample 去处理,预处理阶段删停用词是最常见的翻车操作。把所有句子按一行一个、空格分隔词的方式写入zhwiki_simplified_seg.txt,这个文件就是训练阶段的输入。
3. word2vec 里值得较真的参数:sg、negative 与 window 怎么定
3.1 skip-gram 与 CBOW 的差异,以及中文语料为什么默认选 skip-gram
word2vec 有两种训练方式,CBOW 用上下文预测中心词,skip-gram 用中心词预测上下文。这两者不是随便选一个就能跑,决定了低频词的表现和训练时间。CBOW 对频繁词更稳,训练更快,因为同一个中心词的概率被多次平均;skip-gram 对低频词更友好,因为它把每个词都当一次预测目标,等价于给低频词更多曝光机会。wiki 中文语料里不少实体词只出现几次,如果选 CBOW,这些词会被高频上下文淹没,最后学出来的向量跟随机初始化差不多。
| 下游场景 | 首选 | 理由 |
|---|---|---|
| 通用中文词向量 | sg=1 | 低频词与罕见词表示更稳 |
| 大规模语料、追求速度 | sg=0 | 训练更快,常用词语义足够 |
| 下游是文本分类 | 都行 | 分类任务对词向量精度不敏感 |
| 下游是相似度计算 | sg=1 | 相似度质量更稳定 |
在 word2vec 原始论文的实验里,skip-gram 在小语料上也更能打。wiki 中文语料属于中等体量,所以我通常直接 sg=1。需要注意 skip-gram 的训练时间大约比 CBOW 多 20% 到 50%,但 CPU 上这个差距可以接受。如果你在做一个需要频繁迭代参数的 demo,可以先 CBOW 跑通流程,最后再换 skip-gram 出正式结果。
3.2 负采样与下采样:word2vec 训练过程里容易被忽略的两个闸门
负采样数量的意思是,训练每个正例时,从全局词频分布里随机抽几个词当作负例。gensim 里对应negative参数,默认 5。这个数字太小时,常见词没有被充分推开,相似度结果里会混入虚词;太大时,常见词被推得过远,语义之间的梯度消失。中文 wiki 这类页面语料,我一般用 10。负采样还跟向量维度有关系,维度越高,需要的负例越多,但不要超过 15,不然每个 batch 的更新成本明显上升。
下采样阈值sample控制高频词被随机丢弃的概率。word2vec 原始做法是:词频 t 越高,保留概率越低,公式为保留概率=sqrt(sample/t)+sample/t,其中 sample 是阈值。默认 1e-3 对英文语料够用,但中文里“的、了、在”出现频率极高,1e-3 会让它们保留太多,窗口里全是虚词共现。我把 sample 设成 1e-4,让高频词更狠地被丢弃,低频词相对保留更多。
这两个参数不是深度学习里那种动辄上百万的 parameter,它们影响的是每个训练样本的更新成本以及最终向量空间的干净程度。如果你发现相似度结果里“的”“了”“在”这类词频繁出现,第一反应不应该是加停用词表,而是把 sample 往下调,同时确认 negative 不是太小。
3.3 window 与 vector_size:两个决定“距离感”的数字
窗口大小window决定中心词左右各看多少个词。默认 5 在英文语料上很稳,但中文句子信息密度更高,修饰结构也更紧,我通常从 5 起步。窗口太小,比如 2,词向量只学到紧邻搭配,语义太“实”,找不出“北京”和“城市”这种跨词关系;窗口太大,比如 10,同句里无关的词会被当成正例,wiki 长句多,这个问题会被放大。如果真的要做词义相似度,可以把 window 提高到 8,但要牺牲一部分语法关系。
向量维度vector_size我建议 200,而不是越高越好。中文 wiki 清洗后词表通常在几十万到一百万之间,200 维已经能表达语义差异。OpenAI 的词向量基准实验里,300 维以后准确率增长曲线很平缓。把维度开到 512 或 1024,除了占内存,还会让低频词在小样本下过拟合。如果你的下游任务是短文本相似度,可以 300 维;任务本身是文本分类的话,200 维足够。word2vec 对硬件的唯一要求是内存,向量维度、词表大小和 workers 三者共同决定峰值内存,而不是有没有 GPU。
4. 用 gensim 跑通 wiki 中文语料的 word2vec 训练脚本
4.1 一个可直接改用的训练脚本
训练阶段我直接用 gensim 的 Word2Vec,它把负采样、下采样和 Hierarchical Softmax 都封装好了,比手写 PyTorch 实现稳定得多。下面这个脚本是完整可跑的,文件路径改成自己预处理生成的zhwiki_simplified_seg.txt即可。
import logging import multiprocessing from gensim.models import Word2Vec from gensim.models.word2vec import LineSentence logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') train_file = 'zhwiki_simplified_seg.txt' model_file = 'zhwiki_word2vec.model' vector_file = 'zhwiki_word2vec.txt' # 返回生成器,逐行读取,不会一次性把全量语料读进内存 sentences = LineSentence(train_file) model = Word2Vec( sentences, vector_size=200, window=5, min_count=10, sg=1, negative=10, sample=1e-4, workers=multiprocessing.cpu_count() // 2, epochs=5, ) model.save(model_file) model.wv.save_word2vec_format(vector_file, binary=False) print('vocab size:', len(model.wv))每个参数的作用,我按踩过的坑重新排一下优先级。min_count=10是词表闸门,出现少于 10 次的词直接丢弃;wiki 里低频词大多是人名、编号、错字,留它们在模型里只会让词表膨胀,并让每个低频词学到不可靠的向量。如果发现词表有一百多万,不要急着调 min_count,先去检查清洗环节是不是把模板里的属性名漏掉了。workers建议取物理核心数的一半,超线程机器上multiprocessing.cpu_count()返回逻辑核,直接赋给 workers 会导致上下文切换开销大于并行收益。可以在命令行先执行lscpu或grep 'core id' /proc/cpuinfo | sort -u | wc -l拿到物理核数,再写死到脚本里。
LineSentence在这里是流式读取,任何时刻只保留当前行,所以内存占用只和词表有关,和语料总行数无关。如果已经准备好在内存里的 list,也能直接传,但我不建议,因为 wiki 清洗后文件可能几个 GB,list 会占掉几个 GB 内存,CPU 机器很容易因为内存不足被系统杀掉。训练中观察什么?第 1 轮 epoch 结束后打开日志,应该看到vocab size在合理范围,几十万到上百万都正常。如果词表只有几万,要么是分词没切好,要么是 min_count 设太高;如果词表上百万,多半是清洗没把模板和引用清干净,低频碎片太多。这个阶段宁可重跑预处理,也不要直接调参数硬压词表,因为垃圾词和正常词混在一起会让后续相似度结果看起来“能跑但不对”。
4.2 增量训练与两种导出方式的取舍
业务上线后还会继续进来新语料,比如每天的用户反馈文本。常见做法是做一个增量训练接口,把新语料分词后继续喂给旧模型。注意不要重新初始化,而是基于旧模型继续训练:
new_sentences = LineSentence('new_user_feedback.txt') model.build_vocab(new_sentences, update=True) model.train(new_sentences, total_examples=model.corpus_count, epochs=3) model.save(model_file)这段代码里update=True是往旧模型的词表里追加新词,不传的话会重新清空词表,等于把刚刚学到的词向量全部丢掉。total_examples用model.corpus_count,gensim 会按这个值估算学习率衰减,这一点很容易被忽略。很多人直接用epochs=5但忘了给 total_examples,导致学习率没有正确衰减,新词和旧词的向量尺度对不上,增量训练完相似度结果明显变差。
导出成文本格式save_word2vec_format(binary=False)是给其他框架准备的。PyTorch 或 Paddle 加载预训练向量时,通常要求一行一个词,格式是“词 空格 数值”。第 4 行的二进制版只适合 gensim 自己加载,跨框架用容易踩坑,我一般都会同时存两份。model.save保存的是完整模型对象,包含训练状态,用来后续继续训练;文本格式的.txt用来做推理部署。两份文件都有存在意义,不要只保留一份。有时候别人给你一个word2vec.bin,你用 gensim 加载会报版本不兼容,那多半是 C 语言版 word2vec 的格式,不是 gensim 的 model 文件,转换时要用KeyedVectors.load_word2vec_format单独读。
5. word2vec 训练最容易翻车的五个地方:现象、根因、后悔药
5.1 词表里全是功能词,相似度结果没法用
现象:用most_similar查“中国”,返回的结果全是“了”“的”“在”“与”这类虚词,看起来像词向量训反了。原因:高频虚词在窗口里几乎和所有实词共现,负采样不够,虚词成了语义中心。另一个成因是分句太碎,比如“中国的北京”切成“中国/的/北京”,窗口就把“的”和实体绑死。解决:先确认sample是否生效,把sample=1e-4再往下调,改成5e-5,让高频虚词被丢弃得更狠;同时检查分句规则,的、了、在不应该成为句子的一部分。注意不要用停用词表直接删虚词,否则窗口里的句法关系断裂,会带来新的相似度偏差。
5.2 简繁并存让同义词被劈成两半
现象:模型里“程序”和“程式”同时存在,且相似度不高;“计算机”和“电脑”能接近,但“程式”游离在外。原因:zhwiki 里有繁体条目,清洗时没有统一,繁简两个写法在向量空间各自占据一块区域,低频的写法因为样本少,语义不稳定。解决:清洗流水线里必须加 OpenCC 繁转简,并且转换要在分词之前。后悔药也有:如果模型已经训完,可以用model.wv.add_vector手工把“程式”的词向量初始化为“程序”的向量,但这样只能补救几个词,不如重跑一次预处理。真正上线前,拿五十个高频词在词表里查一遍简繁体写法,比看整体指标快。
5.3 训练中段内存 OOM,进程被系统直接杀掉
现象:CPU 机器跑 word2vec,日志停在某个 epoch,内存不断上涨,最终进程被 kill。原因:最常见的是语料没有走 LineSentence,而是用list(texts)读进内存;或者workers开满,每个子进程都复制一份词表和 hashmap。解决:把 list 改成生成器,代码见第 4 章;workers 降到物理核心数的一半。另一个容易被忽略的是vector_size开得太大,词表又大,几千维度的矩阵会占掉很多内存。对 wiki 这个体量,200 维足够,512 维就是给自己制造 OOM。
5.4 推理阶段 OOV 词太多,词向量形同虚设
现象:训练时词表几十万,上线时一个业务句子 20 个词有 8 个不在词表里。原因:分词不一致,尤其是英文大小写、数字、中英文混杂。训练语料里“AI”和“ai”被分成两个词,推理时“AI”查不到;数字版式“2024/01/01”和“2024年1月1日”也不一样。解决:在分词阶段统一大小写和数字格式,把连续数字归一成<NUM>占位符;同时给 jieba 准备 userdict,把业务专名提前纳入。如果模型已经冻结,就接受 OOV,用wv.most_similar做退避策略,把不在词表里的词映射到相似词上。这个坑几乎不会在训练时报错,只会在下游任务里以“效果差”的形式暴露,所以验证词表覆盖率要提前做。
5.5 epochs 越多越好?词向量在后期反而变形
现象:把 epochs 从 5 加到 20,相似度结果没有变好,反而“北京”和“首都”的关联被冲淡。原因:word2vec 是浅层模型,训练目标是上下文预测,多轮迭代会让权重越来越贴近语料的局部统计,甚至记住噪声共现。维基百科句子规范,信息密度高,5 轮已经饱和。解决:用验证集观察,固定训练集,取一小段语料不参与训练,每轮结束后在验证集上算most_similar的命中率;如果第 7 轮开始命中率下滑,就回滚到第 5 轮的 checkpoint。这就是词向量训练的后悔药,前提是训练时把每轮的 checkpoint 都存下来。
6. 验证词向量的三个手段和一个落地习惯
6.1 三个验证手段
模型训练完先别急着接下游,用三个手段做快速体检。
# 1. 相似度抽样 wv = model.wv for word in ['中国', '北京', '深度学习']: print(word, [w for w, s in wv.most_similar(word, topn=10)]) # 2. 词类类比:北京 - 中国 + 日本 = 东京 result = wv.most_similar(positive=['北京', '日本'], negative=['中国']) print(result) # 3. 业务词表覆盖率 with open('business_words.txt', 'r', encoding='utf-8') as f: words = [line.strip() for line in f] out_of_vocab = [w for w in words if w not in wv] print('OOV率:', len(out_of_vocab) / len(words))相似度抽样看的是语义是否“像人话”,词类类比看的是向量空间里的方向关系是否成立,OOV 率看的是模型对业务词汇的覆盖。三个都过了,再去接下游任务。只跑most_similar很容易被几个热门词骗过去,真正上线后才发现专业术语全在词表外。
6.2 词向量进入深度学习模型的落地习惯
把 word2vec 导出的文本格式向量装进 PyTorch 的 Embedding 层,是它和深度学习框架衔接最常见的方式。加载时要跳过第一行的“词表大小 维度”头,然后构建 embedding_matrix。
import torch pretrained = {} with open('zhwiki_word2vec.txt', 'r', encoding='utf-8') as f: next(f) # 跳过文件头的词表数量与维度 for line in f: parts = line.rstrip().split() if len(parts) > 200: continue pretrained[parts[0]] = torch.tensor( list(map(float, parts[1:])), dtype=torch.float32) embedding_matrix = torch.nn.init.normal_(torch.empty(len(vocab), 200)) for idx, token in enumerate(vocab): if token in pretrained: embedding_matrix[idx] = pretrained[token] embedding = torch.nn.Embedding.from_pretrained( embedding_matrix, freeze=False)这里freeze=False表示下游训练时会继续微调词向量。我现在的习惯是:先微调,让模型适应任务;如果训练数据很少,再改成freeze=True,防止小数据把预训练向量带偏。词向量质量差的时候,深度学习模型怎么调参都救不回来;词向量质量过关,下游只需要少量标注数据就能稳住底线。每次训练完我会先跑一遍上面三个验证,再决定要不要进下一步。希望这些参数和踩坑记录能帮到你,少走一段我走过的弯路。
本文还有配套的精品资源,点击获取