简介:词向量是自然语言处理的基础设施,它将离散的词语映射为稠密的语义向量,使机器学习模型能够理解文本之间的相似性与关联性。在中文场景下,通用预训练词向量资源稀缺,且常受分词版本与领域偏置影响。基于分布式假设,word2vec通过上下文共现学习词义,而doc2vec进一步为句子或文档生成定长向量,两者在文本匹配、语义检索、推荐召回和聚类等任务中具有广泛应用价值。本文以维基百科为语料,系统介绍从数据清洗、繁简转换、jieba分词到构建word2vec与doc2vec模型的完整流程,并给出关键参数配置、效果验证及工程落地中的常见坑点,为中文NLP项目提供一套可直接借鉴的预训练模型构建方案。 中文词向量的落地方案,我一直觉得是很多NLP项目里最容易被轻视的一环。这两年大模型火归火,但实际做检索、聚类、相似度计算、推荐召回这类任务时,一套训练好的word2vec或doc2vec模型,依然是性价比极高的基础设施。尤其是中文场景,网上能直接下载的优质预训练词向量并不算多,很多还带着陈旧的分词版本或者领域偏置。所以我自己用中文维基百科的语料完整走了一遍训练流程,把word2vec和doc2vec模型都训练好,打包成zip分享了出来。这篇文章就是完整记录这套模型的来龙去脉,包括数据怎么处理、参数怎么调、模型怎么用、有哪些坑,算是一份可以直接抄作业的参考。
这个zip里不是只有一个孤零零的模型文件,而是包含了两套东西:一套是word2vec中文词向量模型,另一套是doc2vec句向量/文档向量模型,外加加载脚本和简单的相似度检索demo。适合谁用呢?如果你是做中文文本匹配、短文本分类、关键词扩展、语义搜索或者说要做baseline对比的,这套模型可以直接帮你省掉从零训练的时间。需要注意的是,这并非一个面向SOTA效果的精调模型,而是一个覆盖面广、通用性较强的预训练基础模型,用来做初始化特征或者离线相似度计算非常合适。
1. 为什么选维基百科语料,而不是百度百科或新闻语料
很多人一上来就问,训练词向量到底用什么语料最好。我的答案一直是:看你要干什么。如果你做的是医疗领域,那新闻语料训练的模型拿去处理病历文本,效果会惨不忍睹。但如果目标是做一个通用的、覆盖大部分中文词汇和表达的基础模型,那维基百科几乎是首选,没有之一。
中文维基百科的语料质量非常高,覆盖了科学、历史、地理、文化、人物、技术等几乎所有常见领域。相比新闻语料,维基百科的句子通常结构完整、信息密度高、用词规范,这就给word2vec的学习提供了一个非常好的环境。要知道word2vec的核心假设是"一个词的含义由它周围的词决定",即分布式假设。语料里如果全是口水话或者重复句式,模型学到的东西就会很偏。维基百科的句子长短适中,上下文关系紧密,训练出来的词向量语义方向性非常好。
百度百科的训练语料我也试过,内容量和覆盖面其实更大,但是有两个问题:一是抓取门槛高,需要自己写爬虫,反爬机制很麻烦。二是百度百科的词条内容里夹杂了大量表格、模板、维护性文字,清洗成本比维基百科高不少。如果你去对比两个语料训练出来的模型,在大多数语义类比任务上,维基百科训练的结果并没有明显劣势,很多维度上甚至更干净。
我当时用的是zhwiki-latest-pages-articles.xml.bz2这个dump文件,也就是维基百科的全量词条快照。这个文件一般几个GB级别,解压之后更大,里面是结构化的XML格式,要用专门的解析工具来处理。
这里有个关键操作:维基百科的dump文件是一个很大的XML,直接用正则去抠文本是可行的,但效率很低而且容易漏。推荐使用WikiExtractor这个开源工具来处理,它能直接输出清洗后的纯文本,每个词条一个文件或者合并到一个大文件里。整个处理链路是这样的:
- 下载最新的
zhwiki-latest-pages-articles.xml.bz2 - 用
WikiExtractor.py抽取正文文本,过滤掉模板、图片链接、参考资料等干扰信息 - 用
OpenCC做繁简转换,把粤语、文言文等非现代标准汉语的内容做过滤 - 对文本进行分句、分词、去停用词
在WikiExtractor处理完后,会有大量繁体和简体混合的内容,这直接影响后续分词。不用OpenCC统一转成简体的话,同一个词在繁简两种形态下会被切成两个token,等于把语料活生生地割裂了,模型学出来的向量也会有问题。这一步绝对不能省。我在第一次训练时就偷懒跳过繁简转换,结果跑出来的模型用"手机"查相似词,排在前面的居然是"手機"这个繁体写法,这种结果在我后续代码里几乎没法直接使用,只能在交付前重新训练。
2. 分词与停用词处理:不重视细节,模型质量直接降一个档次
中文和英文一个很大的区别就是中文分词跑不掉。word2vec处理的是词的序列,你给它什么粒度,它就学什么粒度。分词工具我用的是jieba,这是目前中文NLP社区里用得最多的依赖包,稳定且可控。如果是做特定领域文本,可以考虑用pkuseg或者LTP,但从通用角度来说,jieba的词典覆盖度和可控性最均衡。
分词的直接决定了你模型的词表。比如"自然语言处理"这个词组,jieba可能切成了"自然语言"和"处理"两个token。这种粒度问题各有优劣:切成多个token,模型的词表更小,统计意义上每个词的语料量更高,训练出的向量更稳定;把整个词组作为一个token保留,则模型在表示专有表达时更准确,但是需要足够多的共现次数来支撑。我们用维基百科这种大规模语料,默认采用jieba的精确模式,保留分词后的词组形式,不做进一步压缩。
分词之后,我做了停用词过滤。这里就值得说道说道了。很多人直接拿网上下载的停用词表一顿乱删,把"的、了、是、在"这种高频功能词直接去掉。这个大方向没错,但有一个地方要小心:维基百科语料本身是百科性质的,很多词条名称是专有名词,比如地名、人名、学术名词,这些是不应该当作停用词删掉的。网上的通用停用词表基本不会包含这些词,问题不大。真正需要注意的是标点符号和纯数字。
我在清洗脚本里还加入了一个规则:过滤掉所有只包含数字和字母的无意义token。像"2019年"这种词到底保不保留?我建议保留,因为年份、数字在文本相似度计算里是有实际作用的,尤其在百科文本里,年份和时间词经常是关键的定位信息。所以我的过滤条件只是去掉"纯数字"或者"数字+单个量词"这种噪音token,而不是一刀切把所有含数字的词都删掉。
分词结束后,我统计了一遍词频分布。维基百科语料训练出的模型,词表大小通常在几十万量级,高频词的覆盖非常充足。如果你用同样的流程跑出来的词表明显过小,那大概率是清洗环节出了问题,比如文本抽取不完整或者分句逻辑写错了。
3. word2vec与doc2vec的训练细节:参数不是拍脑袋定的
训练工具我选的是gensim,这个库虽然性能和工程化程度比不过一些工业级实现,但胜在API友好、文档齐全,适合做研究和中小型项目落地。如果你要训练超大规模的词向量,可以考虑用fastText官方工具或者spaCy的管道,但gensim对于几GB的中文语料,跑起来完全够用。
在gensim里,word2vec的核心参数就那几个:vector_size、window、min_count、workers、epochs、negative、sample。
vector_size:向量的维度。我最终选的是300维。这个值不是一个绝对标准,200维也能用,但300维是社区验证过的一个平衡点。维度太低语义信息不够,维度太高不仅训练慢,还会引入噪声。中文维基百科这个规模的语料,300维完全养得起。window:上下文窗口大小。我选的5,也就是前后各看5个词。窗口越小,词向量越关注句法关系;窗口越大,越关注主题语义。如果你做的是短文本分类,窗口可以适当调大到10;如果你做的是词汇类比、句法分析,窗口5是个很稳的选择。min_count:最低词频阈值。我设的是5,也就是一个词在整个语料中出现次数低于5次就直接丢弃。这样做的原因是低频词的向量估计往往不靠谱,训练样本太少,学出来的向量基本是噪声。丢弃低频词还能显著减小词表大小、加快训练速度。negative:负采样数量。这个参数在gensim里默认是5,我保持了这个默认值。负采样是word2vec加速训练的核心技巧,它不需要对词表中所有词做softmax,而是随机抽取几个负样本做二分类。负采样数量不是越大越好,5到10之间是个合理区间,太多了会引入过多的"这俩词没关系"的信号,反而干扰训练。sample:高频词下采样阈值。我设的是1e-4。下采样的逻辑是:像"的"、"是"这种词在语料中出现频率极高,但它们提供的上下文信号信息量极低。下采样会以一定概率随机丢弃这些高频词,让模型的训练更加聚焦在信息量丰富的词上。这个参数对训练速度和模型质量都有正面收益,1e-4到1e-5之间是常见配置。epochs:训练轮数。我选了5轮。word2vec并不需要像深度学习模型那样训练几十个epoch,它是单层网络结构,训练太久反而会过拟合到语料中的噪声。5轮在这个语料规模上,loss已经收敛得足够好了。
训练word2vec选Skip-gram还是CBOW也是个老生常谈的问题。我最终用的是Skip-gram。理由很简单:Skip-gram对低频词的向量学习更友好,虽然训练速度比CBOW慢,但在中文维基百科这种大规模语料下,模型质量优先。如果你着急出结果且对低频词不敏感,CBOW也能用,但Skip-gram在整体语义质量上确实更胜一筹。
doc2vec的训练则完全是另一套逻辑。doc2vec的核心思想是给每个文档学习一个向量表示,这个向量可以用于文档级相似度计算。gensim里的Doc2Vec实现了两种训练方法:PV-DM(Distributed Memory)和PV-DBOW(Distributed Bag of Words)。
PV-DM的核心是模仿word2vec的CBOW思路,在预测中心词时,除了考虑上下文的词向量,还额外拼接了文档向量。这样训练出来的文档向量包含了丰富的词序信息,效果通常更好,但缺点是训练时间长、参数多。PV-DBOW则忽略词序,只根据文档向量去预测文档中随机抽样的词,训练速度更快,语义信息更聚焦于主题。我的选择是用PV-DM去训练,但加了dbow_words=1这个参数,让它同时训练word向量,这样doc2vec模型也能当word2vec用。
doc2vec训练时有一个必须注意的点:epochs要设得比word2vec更大。gensim老版本里doc2vec默认epochs只有40,后来改来改去甚至有默认值不一致的情况。doc2vec的文档向量是通过多次遍历文档逐步收敛的,epochs太少文档向量根本没学到位。我最终设的是20轮。还有一个参数是dm_concat,设为1的时候会把上下文向量直接拼接而不是求平均,继承了词序信息但同时会大幅增加计算量。我保持默认0,用平均值模式,因为在这个应用场景下,平均模式已经足够稳定。
4. 模型效果评测:直接上手验证语义质量
模型训练完,不能只看loss就宣布成功。我习惯做三个维度的验证:词汇类比、相似词检索、文档相似度。
词汇类比是word2vec最经典的能力验证方式。比如"中国-北京+日本=?"这个经典的类比任务。如果用most_similar方法去计算中国 - 北京 + 日本的余弦相似度,返回的前几个词如果包含"东京",说明模型学习到了国家-首都这一语义关系。我跑了一下自己训练的模型,这个类比的结果非常干脆,东京排第一,后面跟着的也是日本的大城市,语义方向性很清晰。
再比如"男人-女人+国王=?"这个语法类比,训练好的模型会返回"女王"。这类类比的本质是:两个词向量做减法得到一个语义方向,把这个方向加到另一个词向量上,在向量空间中找一个最接近的目标。如果模型没有学好,返回的词会乱七八糟。我在实际测试中,这个模型的语法类比成功率还是相当高的,说明语料质量和参数配置是靠谱的。
相似词检索可以直接用.most_similar("深度学习")来看返回结果。训练好的模型给出的结果往往让会你意外地合理。"机器学习"、"人工智能"、"神经网络"这些词会靠前排列,因为它们所在的语言环境——共现上下文——高度一致。
doc2vec模型的验证稍微复杂一点。我的做法是:从测试语料中取若干段文本,计算它们之间的余弦相似度。比如拿一段关于"自然语言处理"的百科内容和一段关于"中国历史"的内容做对比,理论上这两段文本的相似度应该低于同主题文本的相似度。doc2vec模型对这种主题级别的区分度表现得非常明显。另外,doc2vec可以直接用model.dv.most_similar(doc_id)检索相似的文档,这在做去重、推荐、聚类预筛选时非常有用。
这里有个我自己踩过的坑:doc2vec训练时,gensim要求每个文档必须带一个唯一的TaggedDocument标签,标签可以是数字也可以是字符串。如果你用的是字符串标签,保存模型后重新加载时,docvecs的索引会基于你训练时给的标签生成,但如果在加载之后再往里面新增文档向量,索引对齐问题就会冒出来。所以我在封装的时候,明确要求使用者:这个模型训练阶段的标签是数字编号,新文档如果要加入模型做增量训练,也要保持数字标签的格式,不能混用字符串。这个问题在中文社区里讨论不多,但实际使用中会经常碰见。
5. 使用这套模型时最容易踩的坑
模型交付出去,用户反馈的常见问题其实都集中在那几个点。
第一个是模型加载后的词表缺失问题。用gensim加载word2vec模型后,如果你查询一个分词器分出来的词在模型里不存在,会直接报KeyError。这非常容易让初学者懵住。原因往往是:这个词在训练语料中出现频率太低,被min_count过滤掉了,或者分词工具在预测时切出来的粒度与训练时不一致。我的建议是封装一个get_vector函数,查询前先判断词是否在wv.index_to_key里,不在就返回一个全零向量或者随机向量。全零向量有一个问题就是它是零范数,后续做归一化时除以零会报错,所以实际工程中,对OOV词我用的是一个小范围随机向量加掩码标记,至少保证后续计算不会崩。
第二个是词向量的"静态"本质。word2vec训练出来的词向量是静态的——一个词只有一个向量,它没有多义词区分能力。比如"苹果"这个词,既可以是水果,也可以是手机品牌,但在word2vec里它只有一个向量。这在很多场景下会带来误差。如果你做的是短文本分类或者关键词匹配,这种误差可以接受;但如果你做的是情感分析这种对语义细微差别敏感的任务,建议在一层模型之上再叠加微调流程。
第三个是中文分词粒度对最终效果的影响。这个词向量是基于jieba分词训练的,如果你在实际使用时用的是别的分词器,那表示同一个意思的词可能被切成不同的token,比如"自然语言处理",jieba常见切法是"自然语言/处理",另一个分词器可能切成"自然/语言/处理"。你查询"自然语言处理"这个词,直接用另一个分词器的结果去模型里找,很可能找不到,因为模型里存的是"自然语言"和"处理"两个分开的token。解决办法是:要么在你的管道里也用jieba分词,要么在模型前面加一层分词对齐逻辑。
第四个坑是关于KeyedVectors的使用方式。gensim老版本里大家习惯直接model["词"]来取向量,新版本里这个方式虽然还能用,但官方更推荐使用model.wv["词"]。如果你是用我的zip包里的脚本,这些细节我已经帮你处理好了。但如果你自己从gensim加载模型写代码,注意模型的版本兼容性。旧版gensim保存的词向量格式和新版之间偶尔会有兼容问题,如果加载时报错,多半是gensim版本不一致导致的,升级或降级gensim版本可以解决。
6. 从word2vec到fastText:什么时候该换算法
这个话题虽然不在模型本身范围内,但既然在中文词向量这个背景下,我一定会提一嘴。fastText和word2vec的核心差别在于:fastText在训练时会把每个词拆成字符级别的n-gram,比如"中国"会被拆成"中"、"国"、"中国"以及带边界符号的字符组合,然后词向量是这些n-gram向量的加总。这个设计带来的直接好处是:对于训练语料里没出现过的词,fastText也能根据它的字符子串拼出一个词向量,解决了OOV问题。
那么什么时候选word2vec,什么时候选fastText呢?我的判断维度是任务类型。如果你做的是语义相似度、关键词检索、文本聚类这类需要"词的含义空间"的任务,word2vec更合适,因为它生成的向量更平滑、语义浓缩更充分。如果你做的是命名实体识别、序列标注、拼写纠错这类需要关注"词形态信息"的任务,fastText的效果会更好。
还有一个区别是,fastText训练出来的模型文件往往比word2vec大很多,因为额外存储了n-gram向量。我当时打包的时候考虑过要不要再加一个fastText模型,最终因为体积问题砍掉了。如果你对OOV问题特别敏感,可以自行用相同的语料流程跑一个fastText模型出来,gensim里对fastText的支持也是开箱即用的。
7. 实际工程中doc2vec模型还能这样扩展
很多人拿到doc2vec模型后不知道怎么用,觉得它不如word2vec直观。实际上doc2vec的应用场景非常广:
一个典型的场景是短文本聚类。假设你有一堆用户反馈文本,每一段都是几句话,你想把它们自动归类为"功能问题""体验问题""价格问题"等。传统做法是TF-IDF加KMeans,但TF-IDF的致命弱点是特征维度太高、语义信息割裂。用doc2vec把每段反馈映射成一个300维的向量,然后对向量做聚类,效果会好很多。因为doc2vec学到的向量天然编码了语义相似度,两个表达方式不同但表达同一含义的句子,它们的向量会很接近。
另一个实用场景是推荐系统的召回。电商场景里,每件商品的标题和描述可以拼成一段文本,用doc2vec训练一个商品向量。用户浏览了一个商品,就把这个商品的doc2vec向量拿到商品池里做最近邻搜索,召回一批向量相似的商品。这个方案在冷启动阶段远比行为协同过滤管用,因为它不依赖用户历史行为,只看商品本身的文本语义。我见过不少中小型团队就是靠这个思路,在没有丰富用户行为数据的情况下把推荐召回做起来的。
doc2vec输出的是定长向量,这也是它比传统词袋模型更适合做机器学习特征的重要原因。不管是接一个逻辑回归、随机森林还是接一个简单的神经网络,doc2vec向量都能直接作为输入特征。而TF-IDF那种高维稀疏特征,在传统机器学习模型里虽然也能跑,但内存开销和过拟合风险都更大。
8. 关于训练流程自动化与复现的一些建议
整个流程虽然看起来繁琐,但实际上每一步都是可以脚本化的。我第一次跑全流程时踩了不少坑,后来总结了一套固定的流水线:下载数据、解析XML、繁简转换、过滤噪音、分句、分词、训练word2vec、训练doc2vec、压缩打包。这个过程在普通笔记本上也能跑通,只是耗时不同。word2vec在CPU上训练大概需要几个小时,doc2vec的时间会更多一些。如果你有GPU环境,gensim其实对GPU支持有限,这一块反而快不起来,但好在两个模型都不算大,CPU完全能接受。
自动化还有一个隐藏的好处:语料可以持续更新。维基百科每个月都有新的dump发布,跑一次流程就能得到最新版词向量。我自己现在每次发布新版本模型时,都会把自己的清洗脚本一起放出来,方便大家一起改进。
最后再分享一个小技巧。如果你在most_similar的结果里发现某个词的相似结果明显有问题,比如查询"篮球"却返回了几个完全不相关的词,不要急着去调参数,先去看这个词在语料里的实际上下文。用model.wv.contexts或者直接检索原文,看看是不是文本清洗环节引入了脏数据,比如把一个词条的内容解析成了无意义的字符序列。语料里的脏数据是词向量质量最大的杀手,参数调优反而救不回来。
本文还有配套的精品资源,点击获取