1. 先搞清楚word2vec到底在解决什么问题
1.1 从one-hot编码说起:为什么词需要用向量表示
做自然语言处理的人,第一天就会遇到一个绕不开的问题:计算机不认识中文、不认识英文,只认识数字。你想让机器处理"苹果"和"香蕉"这种词,就必须先把它们变成数字。早期最朴素的办法就是one-hot编码,也就是"独热编码"。假设你有一本词典,里面有10万个词,那么每个词就对应一个10万维的向量,当前词的位置上是1,其它位置全是0。"苹果"是第1个词,它就是[1,0,0,...,0],"香蕉"是第2个词,它就是[0,1,0,...,0]。
这种方式简单是简单,但问题非常致命。第一个问题是维度爆炸,10万个词的词典,每篇文章用10万维向量表示,存储和计算都是灾难。第二个问题更关键:它完全无法表达词与词之间的语义关系。在one-hot的世界里,"苹果"和"香蕉"之间的距离,跟"苹果"和"汽车"之间的距离一模一样,都是(\sqrt{2})。机器完全不知道"苹果"和"香蕉"都是水果,也不知道"猫"和"狗"都是宠物。这显然不符合人类对语言的理解方式。
所以后来出现了分布式表示(Distributed Representation),核心思想是用一个低维的稠密向量来表示每个词,比如300维或者500维,每一维不是一个非0即1的标志位,而是带有一定语义含义的数值。这样一来,"苹果"和"香蕉"的向量在空间里就会离得比较近,"苹果"和"汽车"就会离得远。word2vec做的就是这件事:它通过海量语料训练,把每个词映射成一个低维实数向量,而且这个向量能很好地捕捉词的语义和句法特征。
1.2 词向量的核心意义:让机器"理解"词之间的关系
你可能会问,向量离得近有什么用?用处太大了。我们可以直接对向量做加减法。比如经典的例子:(vec("国王") - vec("男人") + vec("女人")),结果会非常接近(vec("女王"))。这个结果不是人为设计的,而是word2vec从语料里自己学出来的。因为它能学到"国王"和"女王"之间的差异,和"男人"和"女人"之间的差异,在向量空间里是同一种平移关系。
更进一步,我们可以用余弦相似度计算两个词的语义相似度。在实际项目里,我经常用这个能力做三件事:一是找同义词,比如做搜索系统时用户搜"笔记本电脑",我可以召回"手提电脑""笔记本"这些词;二是做词聚类,把一堆词按语义分成几组;三就是作为更复杂模型(比如BERT、LSTM、TextCNN)的输入底座,预训练好的词向量能显著提升下游任务的起点。
这就是word2vec的核心价值:它用无监督的方式从大规模语料里学习词的低维稠密向量表示,让语义相近的词在向量空间里距离更近,让词与词之间的关系变得可计算。在2013年它刚出来的时候,效果是颠覆性的,训练速度比当时的神经网络语言模型快了几个数量级,而且在词相似度任务上效果大幅提升。哪怕放到今天,如果你没有大规模GPU资源去训练BERT,word2vec依然是性价比最高的词向量方案。
2. 两种主流模型:CBOW和Skip-gram怎么选
2.1 CBOW:拿上下文猜中间词
word2vec提供的第一个模型叫CBOW,全称是Continuous Bag-of-Words,连续词袋模型。它的思路非常直观:给你一个词周围的上下文词,让你猜中间这个词是什么。比如有一句话"我喜欢喝_茶",CBOW就是拿着["我","喜欢","喝","茶"]这四个词,去预测中间那个被遮住的词"绿"。
为什么叫"词袋"?因为在这个模型里,上下文词的顺序是被忽略的,它们被当成一个集合来处理。具体做法是:把上下文里的每个词都映射成向量,然后把这些向量取平均(或者求和),得到一个综合的上下文向量,再拿这个向量去预测中间词。整个过程就像你听一个人说话,听到前后几个词,心里大概能猜出他中间要说什么。CBOW对高频词的预测效果比较好,而且训练速度更快,因为它一次处理多个上下文词的信息,梯度更新次数相对少。
这里要注意一个细节:CBOW的输入是上下文词的one-hot或者索引,输出是中间词的概率分布。训练时,我们会拿语料里的每一个词,把它当作目标词,取它前后一定窗口内的词作为上下文,构造训练样本。窗口大小是一个超参数,通常取5,也就是前后各看5个词,一共最多10个上下文词。窗口太小,模型看不到足够的信息;窗口太大,又会引入太多无关词汇,反而干扰预测。
2.2 Skip-gram:拿中间词猜上下文
Skip-gram模型正好和CBOW反过来,它拿中间词去预测周围的上下文词。还是刚才那句话,"我喜欢喝绿茶",Skip-gram会拿"绿"这个中间词,去预测它周围的["我","喜欢","喝","茶"]。这个思路很像你记住一个词,然后在脑海里回忆这个词通常会跟什么样的词一起出现。
Skip-gram对低频词和生僻词更友好,因为它在训练过程中会把更多注意力放在每一个单独的词上,周围每个上下文词都是一个独立的预测任务。代价是训练速度比CBOW慢,因为它等价于在一个样本里做了多个预测。实际使用时,如果你的语料量不大、或者你特别在意罕见词的表示质量,Skip-gram往往是更好的选择。Mikolov等人在原论文里的实验也表明,Skip-gram在语义相似度任务上整体表现更好一些,尤其是在大规模语料上,效果差距会更明显。
2.3 实战选型建议:到底用哪个
我自己的习惯是分场景选。如果做的是搜索、推荐这类需要精确处理大量实体词和低频词的项目,优先用Skip-gram,词向量的质量普遍更细腻。如果做的是文本分类、情感分析这类任务,语料又非常大,CBOW更合适,它训练快、对高频词拟合好,下游任务效果也够用。
再补充一个判断维度:训练时间。同样的语料、同样的维度,CBOW通常比Skip-gram快2到3倍。如果项目迭代节奏很快,需要频繁重跑模型,CBOW能帮你节约不少时间。如果项目本身就离线训练、跑一次能用很久,Skip-gram多花的那点时间完全值得。
| 对比维度 | CBOW | Skip-gram |
|---|---|---|
| 训练任务 | 用上下文预测中间词 | 用中间词预测上下文 |
| 训练速度 | 快 | 慢(约慢2-3倍) |
| 低频词质量 | 一般 | 更好 |
| 高频词质量 | 好 | 好 |
| 语料较小场景 | 谨慎使用 | 更推荐 |
| 大型语料场景 | 推荐 | 推荐 |
这个表格看着简单,但我建议你在项目里还是做一次小规模的A/B测试再做决定。因为不同语料的词频分布差异很大,理论上的结论在具体场景里未必总是成立。
3. 训练过程背后的关键细节
3.1 网络结构其实很简单:一个隐层的神经网络
很多人一听神经网络就头大,觉得word2vec的训练复杂无比。实际上word2vec的原始网络结构非常简单:一个输入层、一个隐藏层、一个输出层,中间没有任何激活函数和复杂的层结构。它的隐藏层本质上就是一个查找表,也就是一个形状为(V \times N)的矩阵,(V)是词典大小,(N)是你设定的词向量维度。输入某个词的one-hot向量,乘上这个矩阵,得到的其实就是这个词对应的那一行向量。
可以这么理解:这个隐藏层的权重矩阵,就是所有词的向量表。训练的目的,就是不断调整这个矩阵里的数值,让最终每个词对应的行向量能够携带足够丰富的语义信息。在网络训练完之后,我们扔掉输出层,只保留这个权重矩阵,用它作为查表工具,把词变成向量。
要注意的是,输入层用的还是one-hot,但这里只是作为索引来用。实际工程实现里,根本不会真的做one-hot向量和矩阵的乘法,而是直接按词在词典里的索引去取矩阵的对应行,这样计算效率极高。
3.2 输出层的softmax问题:词汇量一大就卡死
如果按照最朴素的思路,word2vec的输出层应该是一个标准的softmax。也就是说,模型在预测目标词的时候,需要计算所有词的得分,再做一个归一化的概率分布。假设词典有100万个词,那每个训练样本都要做100万次的乘法计算,这在实际工程里是行不通的。
我打个比方:你在一家超市做问卷调查,需要从10万个顾客里找到最符合你条件的那一个人,你挨个问一遍,问一次花时间不多,但100万个样本每个都要问一遍,时间就爆炸了。这就是为什么word2vec不能直接使用标准softmax的原因。
为了解决这个问题,原论文提供了两个非常经典的优化方案:负采样(Negative Sampling)和层次Softmax(Hierarchical Softmax)。这两个方案你可以理解成两种"偷懒"但效果很好的办法,它们让word2vec在百万级词典上也能在可接受的时间内训练完成。
3.3 负采样:把多分类变成二分类
负采样的思路很巧妙。原始的正向传播需要计算所有词的概率,工作量巨大。负采样把它简化为:给定上下文和中心词,我只需要判断这个词是不是真的应该出现在这里。也就是说,把"在10万个词里挑一个正确的"这个多分类问题,变成了"判断这组词搭配是否正确"的二分类问题。
具体做法是训练时对于每个正样本,也就是真实出现在语料里的词对,我们再随机抽出几个负样本,也就是从词典里随机挑几个跟当前位置无关的词,然后让模型学会给正样本打高分、给负样本打低分。负样本的数量通常取5到20个,远小于词典大小,计算量大幅下降。
负采样时不是均匀地随机抽,而是用一种基于词频的概率分布来抽,这个分布叫unigram distribution的3/4次方。为什么要做3/4次方?因为这样可以稍微压制那些频率极高的词,给低频词更多被抽中的机会。我在实际项目中很少去调整这个参数,gensim里默认的实现效果已经很好。
负采样还有一个隐藏的好处:它的训练过程天然会学习到词和词之间的区分性,所以词向量在语义空间里的分布会更均匀,相似度计算的效果也更稳定。这也是我在大多数项目里优先选择负采样的原因。
3.4 层次Softmax:用霍夫曼树加速归一化
层次Softmax是另一个优化方案,它的思路是彻底不计算所有词的得分,而是把输出层的多分类问题转化成一连串的二分类问题。具体做法是:先根据词频构造一棵霍夫曼树,词频越高的词离根节点越近。树的每个叶子节点对应一个词,每个内部节点对应一次二分类的决策。预测一个词的时候,只需从根节点沿着路径走,经过大约(log_2(V))个内部节点,也就是大约(\log_2(1000000)\approx20)次决策,而不是计算100万次。
这个方案在词频分布极不均衡的语料里表现很好,因为高频词的路径短,训练高频词时计算很快。如果你用gensim训练word2vec,默认情况下其实用的是负采样,而不是层次Softmax,这是实践中的一个默认取舍。层次Softmax的缺点是当词向量维度较高时,它可能不如负采样灵活,而且实现起来更容易出细节错误。
我对层次Softmax的建议是:理解原理即可,除非你有明确理由,否则用负采样就够了。之前有朋友问过我,为什么自己实现的层次Softmax效果总比gensim差,我说你检查一下霍夫曼树的构造和路径编码是否完全一致,这个细节不对,效果就千差万别。
4. 实操过程中的参数与调优经验
4.1 核心超参数:窗口、维度、最小词频
训练word2vec时,参数设置直接决定词向量的质量,这里我逐个讲讲最核心的几个参数,都是我自己踩过坑总结出来的经验。
第一个是窗口大小(window)。窗口决定了每个词能看到多大的上下文范围。窗口太小,模型学到的主要是语法搭配,比如"我"后面经常跟"喜欢";窗口太大,模型学到的主要是主题相似性,比如"篮球"和"体育"经常出现在同一段话里。一般做语义相似度任务,窗口选5到10;做句法、词性相关任务,窗口选2到5。我在做搜索推荐项目时,窗口取5通常是最稳的,如果你发现词向量学出来的词和预期差距很大,先把窗口调一调,很多时候问题就出在这。
第二个是向量维度(size)。这个参数决定了向量的表达能力。维度太小,比如50,词与词之间的差异表达不出来,很多语义信息会被压缩丢失;维度太大,比如1000,训练时间变长,而且小的语料容易过拟合。常规做法是在100到300之间选。我可以给你一个参考:如果你的语料有1亿词以上,300维完全合理;如果语料只有几千万词,200维左右更稳。一个非常实用的判断方法:如果你发现某些不相关词之间的相似度异常高,很可能是语料不足而维度太高导致的过拟合。
第三个是最小词频(min_count)。这个词频参数会过滤掉出现次数太少的词。为什么要过滤?因为低频词本身信息量不足,训练出来的向量往往不可靠,而且会拖慢训练速度。一般设为5或10。但你得根据实际语料规模来灵活调整:语料特别大时,可以设高一点,比如50;语料本身很小,设5可能就够。需要注意的是,这个参数会直接影响词典大小,你过滤掉的词越少,词典越大,内存占用越高。
4.2 训练数据预处理:这一步比调参更重要
我在项目里反复跟人讲,word2vec的最终效果,语料质量和预处理的影响要占七成,参数只占三成。很多人都把精力花在调参上,结果效果还是不理想,回头一看,原始语料乱糟糟的,分词也不对,那再怎么调参都白搭。
中文语料必须先分词。word2vec本身不认识"中文句子"这个概念,它只认空格分隔的词序列。如果你把整句话直接扔进去,模型学到的东西完全没有意义。我个人推荐使用jieba分词,简单方便,对于绝大多数场景够用。但如果你做的是专业领域,比如医疗、法律,建议在通用分词基础上加上自定义词典,把领域内的专有名词保护起来,否则"心肌梗死"会被切成"心肌"和"梗死",语义就歪了。
另外,很多人在意要不要去停用词,我的建议是看场景。做情感分析时,停用词确实可以去掉,因为它们和情感表达关系不大;但做语言模型或者词向量训练时,我不建议过度清理,因为"的""了""是"这些高频功能词虽然单独看没什么语义,但它们的上下文搭配信息对捕捉语法结构是有帮助的。更好的做法是设置min_count,让这些词自然留下来,但对结果影响有限。
最后还有一个容易被忽略的点:样本采样参数(sample)。word2vec在训练时会根据词频对每个词做随机下采样,也就是说,高频词会以一定概率被丢弃,以此来平衡低频词和高频词的比例。这个参数的默认值是0.001,通常不用改。但如果你的语料里有极端高频的噪声词,可以适当调低这个值,比如到0.0001,可以明显提升低频词的向量质量。
4.3 训练过程:迭代次数与训练效果的关系
训练word2vec时,迭代次数(epochs)也值得琢磨。很多人默认设5次,我发现这个值并不总能保证最佳效果。在语料规模较小的情况下,多迭代几次能让模型充分学习,但也不是越多越好,迭代次数过多,词向量会逐渐过拟合到语料中的偶然共现上。
怎么判断迭代次数合不合适?我常用的办法是训练过程中实时检查一组验证词对,比如"北京"和"上海"的相似度、"中国"和"美国"的相似度。如果随着迭代次数增加,这些词对的相似度不断提高且趋于稳定,说明模型还在学习;如果相似度出现反弹或者开始变得不合理,说明已经过拟合了。实际操作中,我通常会让模型训练到验证集上的相似度指标不再明显上升为止,然后在此基础上去掉一部分迭代次数作为安全余量。
另外还有一点:训练时设置多个线程可以显著加快速度,但线程数并不是越大越好。线程过多时,每个线程各自取样训练,整体效果可能会略微下降,因为不同线程之间的更新存在一定的竞争。如果语料特别大,一般在16到32个线程之间比较合适。
5. 训练完成后的应用与评估技巧
5.1 词向量的评估方法:不只靠肉眼观察
训练完模型后,如何评估词向量的好坏?很多初学者喜欢用most_similar函数查一下"苹果"的近义词,看看结果是否合理。这个方法可以做初步判断,但不能作为唯一的评估手段。因为示例抽查带有很大的随机性,你可能恰好选中了表现好的词。
更可靠的评估方式是用标准数据集跑相似度任务。比如说SimLex-999,就是一份人工标注了999对词相似度分数的数据集。你可以把word2vec算出来的余弦相似度和人工打分算相关性(Spearman相关系数),这个指标能比较客观地反映词向量的质量。在做中文项目时,也有对应的中文词相似度评测集,比如Wordsim-240和Wordsim-297。
还有一个很实用的评估方法是类比推理任务。经典的问题就是"国王-男人+女人=女王"。你可以自己构建一组类比词对,比如"北京-中国+日本=东京"、"父亲-儿子+女儿=母亲"等等,然后用词向量做向量加减法,看看模型能不能给出合理答案。这个方法操作起来非常快,而且能直观地反映词向量是否学到了语义关系。
5.2 词向量在下游任务里的使用方式
训练好的word2vec词向量,最常见的用法是作为各种深度学习模型的输入层。比如做文本分类时,可以把一句话里每个词对应的向量拼起来,作为卷积神经网络或者循环神经网络的输入。这种方式在你没有资源训练大规模预训练模型时,是非常好的baseline方案。
还有一种用法是做词向量的融合。有时候单个词向量维度过高或者信息不够充分,我们可以训练多组不同参数设置下的词向量,然后把它们拼接或者取平均,获得更丰富的表示。我在一个文本匹配项目里试过,把窗口5和窗口10训练出来的两组词向量拼接起来,最终效果比单组词向量提升了大约2个百分点。这个方法成本很低,值得一试。
有一点必须注意:词向量是静态的。一个词在任何语境下都对应同一个向量,所以"苹果"(水果)和"苹果"(手机品牌)会被表示成同一个向量。因此,在做下游任务时,词向量只能作为特征输入,不能完全替代上下文建模。如果你的任务对一词多义特别敏感,还是得考虑ELMo、BERT这类动态模型。但即便如此,word2vec作为理解和起步的工具,依然有不可替代的价值。
5.3 常见问题与排查技巧实录
模型训练报错:内存不足。这种情况多半是词典太大或者语料一次性加载太多造成的。先用min_count过滤低频词,控制词典大小;然后检查是否能用迭代读取语料的方式,避免把全部语料同时放进内存。gensim本身支持流式读取,词向量的内存占用约等于(词典大小 \times 维度 \times 4)字节,你可以估算一下,比如词典10万、维度300,大概需要1.2GB内存。
词向量相似度结果非常奇怪。我遇到过"苹果"最近似的词是"华为"、"三星"这种,多半是语料里频繁出现"苹果手机""苹果公司"这类搭配。这不是模型的bug,而是语料本身的问题。这提醒你,词向量学习的是语料里的共现统计,而不是真实世界百科常识。想改善,就要在预处理时结合语料背景做修正,比如增加更多通用语料来稀释领域偏置。
训练很慢,如何提速?检查一下是不是词典过大、样本数过多,或者线程数设得太少。还有一个容易被忽略的点:负采样的负样本数量不要设得过大,我见过有人把negative设成50,训练速度慢到怀疑人生。通常5到15就足够,没有特殊情况不建议超过20。
模型训练完成,但个别生僻词没有词向量。这是OOV(out-of-vocabulary)问题。低频词被min_count过滤掉了,于是训练后查不到。解决思路是降低min_count、扩大语料规模,或者使用FastText这类能基于子词信息生成OOV词向量的模型。FastText本质上是word2vec的扩展版,在训练时额外的字符级n-gram信息,相关的做法是把它当作word2vec的备选方案,效果在很多任务里都很不错。如果已经开始用新的模型做训练,那也可以考虑用它来对比一下新模型与word2vec类方法的差异,能帮助你更好地理解不同方法之间的差异。
训练结果随机性大。相同语料、相同参数跑两次,每次结果都不完全一样。这是因为训练时存在随机初始化、负采样随机抽取等因素。这不算bug,但如果你需要稳定复现结果,可以固定随机种子。科学实验对比时,建议固定种子后再去对比不同参数的效果,否则你很难判断效果差异到底来自参数调整还是随机波动。
写在最后的经验和建议
我在实际项目里发现,很多人一上来就追求复杂的模型,BERT、GPT轮着试,反而忽视了word2vec这类基础工具的价值。其实在小样本、低算力的业务场景里,word2vec加一个简单分类器的方案会是一个相当实用的起点,接地气且能快速落地。它有近十年的历史,今天依然在推荐系统召回、语义匹配、词义消歧等环节发挥着作用,而且很多深度学习模型的Embedding层,本质上学的也是同一种东西。
最后再分享一个我自己的小习惯:训练好的词向量,不管后续用不用,我都会存一份保留下来。因为词向量本质上是对语料的深度压缩,它把海量文本蕴含的词汇关系浓缩成了一份可查询的词表。这份词表不仅能用来初始化下游模型,还能在数据审计、相似词挖掘、词典构建这些工作中反复复用。花一次训练的成本,换来的是一个可以长期使用的语义基础设施,这个投资非常划算。