Gensim加载Word2vec预训练模型:相似度测试与避坑指南
NLP入门的时候很多人会在词向量这里绕不少弯路。一开始看概念总觉得是一堆数学公式堆出来的东西后来用Gensim库跑了一次Word2vec模型的加载和测试才明白原来“语义”也可以用距离来描述。这篇文章不打算把数学推导铺开只做两件事先把Word2vec的基本逻辑说清楚然后加载一个预训练模型跑几个最常见的测试再讲怎么看结果、怎么避坑。适合谁看如果你会一点Python正准备做文本相似度、关键词扩展或者语义搜索这篇可以从零带你走一遍。如果你的项目里已经有标注语料但想先用无监督词向量搭一个快速基线这篇文章也合适。这是Gensim库使用系列里Word2vec模型的第一篇这一篇先把加载和测试跑通后面再聊训练和调参。我尽量少用术语非用不可的地方就用生活里的例子把它掰开揉碎来讲。1. 先理解Word2vec在学什么从“词”到“向量”的一次转变1.1 为什么非要用向量表示词在自然语言处理里计算机一开始根本不认识“苹果”“香蕉”这些词。最早的表示方式叫one-hot也就是给词表里的每个词一个唯一编号编号位取1其余全部取0。这种做法的最大问题是维度爆炸一个十万词表就得用十万维向量来表示一个词而且任意两个词之间的距离都一样完全没有语义信息。换句话说one-hot只解决了“词能不能进模型”的问题没解决“词和词之间是什么关系”的问题。词向量分布式表示的思路完全不同。它把每个词映射到一个低维稠密空间比如100维或300维用向量在空间里的位置来表达语义。位置接近的词语义往往也接近。可以把它理解成给每个词在地图上标了一个坐标而不是发一个互不相通的房间号。这个转变是整个Word2vec模型的价值所在也是后面所有相似度测试能成立的前提。1.2 CBOW与Skip-gram两种最容易听说的训练思路Word2vec实际上一口气提供了两种训练目标。CBOWContinuous Bag of Words连续词袋的思路是“完形填空”给你一个词周围的上下文让你猜中间这个词是谁。Skip-gram刚好反过来是“拿着一个词编故事”给你中心词让你预测它周围可能出现的上下文词。两种目标最终都会训练出词的向量只是训练效率和偏好不同。CBOW训练速度快对高频词的效果相对稳定Skip-gram训练慢一些但在低频词和短语上的表现通常更好。初学者不需要纠结选哪个先知道有这回事即可。很多公开预训练模型会把两个版本都发布出来加载以后真正用到的都是那套词向量结果和训练目标之间的关系已经不那么直接了。1.3 词向量最重要的性质相似之外还有方向词向量最迷人的地方不只是“苹果”和“香蕉”相似度高还在于向量之间可以做加减法。Word2vec训练过程中会反复学习“词和上下文共现”的模式所以向量方向里隐含了某种关系。经典的例子是“国王 - 男人 女人 ≈ 女王”。这个式子说明在向量空间里“男性”和“女性”的关系是一条可以被加减捕捉的方向轴。这类类比测试是很多人第一次真正觉得“词向量好用”的时刻。不过实际使用时要降低预期公开模型在经典词对上效果不错但在中文领域、专业术语上经常翻车这和训练语料、预处理方式都有关系。2. 为什么用Gensim库来做这件事接口、版本和文件格式一次理清2.1 Gensim在文本处理生态里的位置Gensim是一个专注于文本无监督建模的Python库它最常被提起的两个功能是Word2vec和主题模型。和深度学习框架相比Gensim的API要轻得多没有复杂的计算图概念。特别是加载预训练词向量的时候核心对象是KeyedVectors它把词表、向量、相似度检索都封装好了几行代码就能完成“查相似词”的操作非常适合做快速验证和特征工程。如果非要用某个深度学习框架来加载词向量也不是不行但需要自己维护词表、自己实现余弦相似度还要注意张量坐标和词表索引的对应关系。这样的工作偶尔做一次没什么一旦要反复做不同模型的对比测试工作量就很浪费。Gensim帮你把最麻烦的索引和距离计算部分做好了所以我建议在“跑通流程”阶段优先选择它。2.2 安装和版本选择别让版本问题成为第一个坑新项目直接安装最新版即可命令很简单pip install gensim python -c import gensim; print(gensim.__version__)我建议在安装前先确认Python版本。Gensim 4.x对Python 3.8以上支持得更好同时它会引入底层数值计算库的版本要求。如果你的环境里已经有一个旧版本的低层数值计算库pip install gensim可能会顺手把它升级导致一些其他包报错。遇到这种情况建议单独建一个虚拟环境来用不要让Gensim干扰主环境里的依赖。另外要注意Gensim 4.x改动了不少API网上很多教程还是3.x时代的写法。比如4.x里很多旧属性被移除了加载模型的方式也更强调KeyedVectors。如果你是从别人项目里复制的代码先看当前环境里的gensim.__version__再决定代码是按3.x还是4.x写。这个版本问题是最容易让人一上来就卡住的点。2.3 模型文件格式.model、.kv、.vec、.bin到底有什么区别预训练词向量在网上流传时通常有几种不同包装必须区分清楚格式内容适合场景.model完整Word2Vec对象含训练配置、词表、向量需要继续增量训练时.kvKeyedVectors专用格式只有词向量和词表日常加载复用推荐.vec文本格式的词向量文件开源模型常见可阅读.bin二进制格式的词向量文件体积更大加载快如果手里是.model文件一般用Word2Vec.load()来加载如果是.kv文件用KeyedVectors.load()如果是.vec或.bin这种word2vec格式要用KeyedVectors.load_word2vec_format()并指定是不是二进制。这里面最容易被忽略的是load_word2vec_format()只能读纯词向量不能读.model完整模型也不能用它来继续训练。理解这一点后面加载时能少踩很多坑。3. 加载预训练模型这件事值得认认真真准备一下3.1 先看语料领域再选模型文件很多人拿到一个预训练模型就急着加载结果测试出来相似度乱七八糟第一反应是“模型不行”。其实很多时候是模型训练语料和任务领域不匹配。公开的预训练词向量大多来自新闻、百科或者网页通用语料对日常词汇覆盖很好但如果你要做医学文本、法律文书或者代码注释通用模型里可能连很多核心术语都没有。所以拿到模型后我建议先做一个“词表体检”把自己任务里的高频词挑五十个逐一确认是否在词表里。如果五十个里有十个以上不在这个模型可以直接放弃省得后面越测越怀疑人生。词表体检的代码很简单后面会专门说怎么查。3.2 拿到文件后的三步检查假定你已经下载了一个名为pretrained.vec的文本格式词向量文件第一步是用文本工具看前几行head -3 pretrained.vecword2vec文本格式的第一行通常是“词表大小 向量维度”比如“100000 300”表示有十万个词每个词用300维向量表示。从第二行开始每行是一个词和它对应的向量数值。如果第一行不是这个格式文件可能已经被二次处理过加载时就要当心。第二步是确认文件里有没有内容损坏。文本文件里偶尔会出现一行只有词、没有向量或者某个向量的数值列数不对加载时Gensim会直接报错。这一步可以先跳过等加载报错时再回来检查。第三步是明确文件的大小和编码。如果是中文预训练模型通常保存为UTF-8如果是英文模型可能是纯ASCII。加载时遇到编码报错可以用unicode_errorsignore先绕过去但要注意这会丢词。3.3 真正把模型加载进来两种方式都要会加载纯词向量时我通常会优先选择load_word2vec_format因为它兼容网上下载的.vec和.bin文件。这个方法有两个参数需要仔细对待一个是binary另一个是unicode_errors我在实际使用中就是在这里踩过坑。如果你手头是.kv格式也可以直接用KeyedVectors.load区别我会在代码后面说明。先看最常见的文本格式加载from gensim.models import KeyedVectors kv KeyedVectors.load_word2vec_format( pretrained.vec, binaryFalse, unicode_errorsignore, ) print(词表大小:, len(kv)) print(向量维度:, kv.vector_size)如果文件是二进制格式使用binaryTrue。如果之前已经把向量保存成了.kv格式加载更快kv KeyedVectors.load(pretrained.kv, mmapr) print(len(kv))加载完之后顺手验证一个词能不能查到vector kv[苹果] print(vector[:10])这段代码如果能在几秒内跑完并输出一段浮点数就说明模型已经加载成功了。注意kv[苹果]返回的是一个数值数组不是普通列表后续做计算更方便。4. 第一轮测试相似度、最相似词和词类比4.1 相似度最直白的语义距离模型加载成功后的第一件事通常是从相似度开始。这一步是验证模型是否靠谱的最快路径。Gensim里直接调用下面的方法返回的是两个词向量的余弦相似度。余弦相似度关注的是向量方向而不是长度两个词哪怕一个高频一个低频只要语义方向一致分数也会比较高。score kv.similarity(苹果, 香蕉) print(score)这个函数返回一个浮点数数值范围通常在-1到1之间但Word2vec词向量里的余弦相似度很少出现负数常见结果集中在0到0.8之间。这里要提醒一句不同预训练模型算出来的相似度分数不能直接跨模型比较。同样一对“苹果-香蕉”在一个模型里可能是0.5在另一个可能是0.7这不代表后者一定更好。要看的是排序关系而不是绝对值。4.2 最相似词结果需要人工过滤相似度是两两对比而most_similar是另一个用得最多的方法。它会从整个词表里找出与目标词余弦距离最近的N个邻居一步就能看出模型对某个词的理解大概是什么样的。下面这段代码会把每个邻居词和它的相似度都打印出来for word, score in kv.most_similar(计算机, topn10): print(f{word}: {score:.4f})输出大概率包含“电脑”“硬件”“软件”“程序员”这类词也可能出现个别看起来很怪的结果。这不是代码出错而是模型只靠共现统计学习语义它并不知道这些词在真实世界里是什么关系。比如“苹果”的最近邻居里可能同时出现水果和手机品牌这是语料本身混入了多义词语义的结果。实际使用时要养成一个习惯不要只看前三个词把top10甚至top20全部拉出来看一遍才能判断这个模型的语义倾向到底符合不符合你所在的任务。4.3 词类比测试验证向量方向的经典操作词类比是Word2vec测试里最经典的一项。Gensim支持用正负向量组合来查询下面这段代码演示了king-manwoman这类经典操作返回结果中通常会出现一个和“queen”语义接近的词result kv.most_similar( positive[king, woman], negative[man], topn5, ) print(result)如果模型训练得足够好返回结果的第一位应该和“queen”或者和它语义接近。这个操作的原理就是前面说的向量加减法king - man woman对应的向量应该在“女王”这个语义附近。不过我也想说词类比测试不能代表模型的全部能力。公开模型里有时会隐含训练语料中的刻板印象比如某些词类比会把职业和性别错误地关联起来。当成一个技术演示没问题但真要拿到业务系统里还要额外做公平性和偏见测试这一点很多人一开始不会注意。5. 我踩过的坑词表外、格式与版本、加载性能5.1 词不在词表里一个KeyError引发的连锁排查加载完模型后第一个高频报错是KeyError。原因很简单你查询的词根本没有出现在预训练词表里。中文场景尤其常见因为模型是按词训练的还是按字训练的词表差异很大。查询之前先确认target_words [苹果, 计算机, 深度学习] for word in target_words: if word in kv.key_to_index: print(f{word} 在词表中) else: print(f{word} 不在词表中)kv.key_to_index是词到索引的映射检查速度很快。如果词不在词表里常见做法有三种直接跳过、用随机向量代替、或者用一个固定的未知词向量。具体选哪种看任务但最忌让程序直接崩溃。5.2 word2vec_format的binary参数一个反直觉的坑load_word2vec_format的binary参数默认是False也就是按文本格式解析。如果实际文件是二进制的而你没设binaryTrue加载会报错或者更隐蔽地把二进制字节当成字符读导致后面的维度解析完全错乱。判断文件格式最简单的办法是用head看前几行。如果第一行是“词数 维数”后面的向量数值能看到成排的小数点就是文本格式如果是密集的乱码就要用binaryTrue。另外网上有些文件后缀叫.bin但其实是文本格式有些后缀叫.vec里面反而是二进制。别只看后缀一定要看内容。5.3 旧版本Gensim模型在新环境里的兼容问题如果拿到的.model文件是用Gensim 3.x保存的在Gensim 4.x环境里加载时很可能报错因为内部对象结构发生了变化。我的处理办法是准备一个临时虚拟环境安装旧版Gensim把旧模型读出来后再保存成.kv格式这样新环境就能直接加载了。这里额外说明一下.model文件并不推荐作为分发格式。它里面包含训练参数和内部状态体积大而且版本敏感。很多公开词向量都用.kv或者word2vec文本格式发布就是因为这些格式更稳定、更省内存。你自己保存模型时也尽量用.kv别嫌麻烦。5.4 大模型加载慢、内存占用高用mmap和子词表解决一些预训练模型动辄几个GB如果直接整体读入内存普通电脑会非常吃力。Gensim的.kv格式支持内存映射加载把磁盘文件直接映射到内存空间而不是一次性复制所有数据kv KeyedVectors.load(pretrained.kv, mmapr)这种方式下很多数据只在真正访问时才从磁盘读入加载速度快很多。对于只需要局部词表的场景我还会提前抽取一个小词表保存成迷你模型small_kv KeyedVectors(kv.vector_size) words [] vectors [] for w in [苹果, 香蕉, 水果, 食物]: if w in kv.key_to_index: words.append(w) vectors.append(kv[w]) small_kv.add_vectors(words, vectors) small_kv.save(small.kv)这一步能从“每次加载几GB”变成“加载几MB”对频繁测试特别管用。6. 测试跑通以后距离真正使用还差什么6.1 用一个小验证集判断模型好不好用最忌讳的是拿一两个词的相似度就下结论。我习惯做一个小验证集准备二十组同义词、二十组反义词然后统计模型对同义词的相似度是否普遍高于反义词。这里的关键是减少偶然性让结论建立在更多样本上。代码可以是positive_pairs [(开心, 高兴), (迅速, 快速)] negative_pairs [(开心, 难过), (白天, 夜晚)] pos_scores [ kv.similarity(a, b) for a, b in positive_pairs if a in kv.key_to_index and b in kv.key_to_index ] neg_scores [ kv.similarity(a, b) for a, b in negative_pairs if a in kv.key_to_index and b in kv.key_to_index ] print(同义词平均相似度:, sum(pos_scores) / len(pos_scores)) print(反义词平均相似度:, sum(neg_scores) / len(neg_scores))如果同义词平均分明显高于反义词说明这个模型至少在你这片领域上还说得过去。如果两个分数差不多甚至倒挂就别指望它能帮你做任务了趁早换一个模型或者直接训练自己的模型。6.2 下一步从“加载别人的”到“训练自己的”这一篇只讲加载和测试下一篇的重点才是用Gensim训练自己的Word2vec模型。简单说一下方向准备分好词的中文语料调用Word2Vec(sentences, vector_size100, window5, min_count5, workers4)完成训练再把训练结果保存成.kv格式。如果你手里的垂直领域语料不够也可以把公开预训练向量加载进来做增量训练Gensim支持这种方式但需要注意词表和超参数这一步我们后面再详细展开。6.3 认清静态词向量的边界Word2vec给每个词只保留一个向量所以“苹果”到底是水果还是手机品牌它没法根据上下文区分。如果你的任务依赖一词多义比如问答、情感分析静态词向量只能作为基线特征不能指望它解决所有语义问题。更好的做法是用上下文相关的模型来补充不过那已经超出这一篇的范围了。我的个人习惯是任何预训练模型加载进来先做一次“任务词表体检”和“同义词反义词小验证”两步加起来不到五分钟却能省下后面调参的大把时间。等你把加载和测试的流程跑顺再去看训练、调参、评估就会发现Gensim这个库是真的能帮人把想法快速落地。