ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

词向量评价实战:相似度、类比与下游任务

词向量评价实战:相似度、类比与下游任务

1. 词向量评价到底在评什么

训练完一批词向量,文件夹里躺着一个几百兆的.vec,这时候最想知道的就一件事:它到底好不好用。词向量评价说白了就是回答这个问题的一套方法,它不产生新模型,只负责给已有的向量打分、排名、挑毛病。词向量本身是词的稠密实数表示,语义相近的词在空间里距离近,这个大家都知道;但"近"到什么程度上算好,"近"的方向对不对,就必须靠评价来量化。

我在实际项目里见过太多这样的情况:有人把语料翻了三倍,维度从 100 调到 300,训练loss看着降了不少,于是宣布新模型更好,结果一接到下游任务上,效果反而掉了两个点。loss 低只说明模型更会拟合训练语料的共现统计,跟"语义质量"并不是一回事。词向量评价存在的意义,就是把这层窗户纸捅破,用外部的、和训练目标无关的标尺去衡量向量,避免自我感觉良好。

这套东西适合谁?刚入门自然语言处理的同学,需要一套能跑通的脚本和判断标准;已经在做文本分类、检索、聚类的工程师,需要在上线前给自己一个交代;还有做垂直领域语料的团队,通用的向量往往不好用,得靠评价来验证领域微调到底有没有收益。不管你是哪一类,核心诉求都一样:用尽量小的成本,得到一个可信的、能横向比较的分数。

评价大体分两派。一派叫内在评价(intrinsic),直接拿向量本身做相似度判断、类比推理、聚类,看结果和人工标注是否吻合;另一派叫外在评价(extrinsic),把向量丢进真实任务,看任务指标涨没涨。前者快、便宜、可解释,后者准、贵、噪声大。真正靠谱的做法是两个都用,先用内在评价快速筛掉明显不合格的版本,再用外在评价做最终裁决。

2. 内在评价:不碰下游任务也能摸清底细

2.1 相似度任务与 Spearman 相关系数

相似度评价的逻辑特别朴素:找一批词对,人工给每对打个语义相关度分数,然后让模型算余弦相似度,最后比较两个序列的排序一致性。这里不能用 Pearson 相关系数,因为人工打分的绝对尺度(比如 0 到 10)和余弦相似度的绝对尺度(-1 到 1)根本没有可比性,我们关心的是"谁比谁更相似"这个相对顺序,所以用 Spearman 秩相关,取值范围 -1 到 1,越接近 1 越好。

公开的人工标注集有不少,常见的是几百个词对规模的那种,覆盖具体名词、抽象名词、动词、形容词。行业里的一般经验是:在通用语料上训练的词向量,Spearman 能到 0.6 到 0.75 之间算正常水平,超过 0.75 已经不错,低于 0.5 基本要怀疑训练配置或者语料有问题。但这个数字强依赖于评价集本身,不同标注集的难度差异很大,跨数据集比较没有意义,只能在同一数据集内部比较不同模型。

实操上有个坑必须提前说:词对覆盖率。评价集里有些词在你的词表里压根不存在,尤其是低频词、带连字符的词、专有名词。直接跳过这些词对会让分数虚高,因为模型"答不上来"的题被剔除了,剩下的都是它会的。正确做法是记录被跳过的比例,如果超过 10%,这个分数就不该拿去和别人比。我一般的处理是把覆盖率作为报告的一部分一起打出来,而不是悄悄扔掉。

2.2 类比任务:king - man + woman 的老把戏与新陷阱

类比任务大概是词向量最出圈的玩法:给a对b的关系,找一个c,使得c对某个d也满足同样的关系。经典例子是"国王之于男人,相当于王后之于女人",向量运算写成vec(king) - vec(man) + vec(woman),然后在词表里找余弦相似度最高的词,看是不是queen。

这个任务直观、好讲、适合做演示,但它作为评价手段的可靠性一直被高估。原因有几个。第一,类比集的构造成分很重,很多题目是围绕少数几个高频关系批量生成的,模型只要把这几个方向的偏移量学准,分数就能刷上去,泛化能力未必真好。第二,答案往往不唯一,king - man + woman排第一的可能是queen,也可能是princess、monarch,只取 top-1 会低估模型。第三,排除策略很关键,算出来的向量最相似的三个词往往就是a、b、c本身,必须把它们从候选里剔除,否则准确率会莫名其妙地很高。

我自己的做法是同时报 top-1 和 top-3 准确率,并且明确写出排除策略。另外,类比任务对词形很敏感:如果语料里woman和women都高频出现,模型的偏移方向会被复数形式带偏。做英文语料的时候,我一般会先做词形归一,或者干脆把不同词形的向量取平均,效果通常更稳。

还有一种更稳的变体叫 3CosMul,把加法换成乘法再归一化,公式大致是cos(b, b*) * cos(c, c*) / (cos(a, a*) + ε),对小语料训练出来的向量更友好。我实测过,在千万级 token 的语料上,3CosMul 比 3CosAdd 稳定一些,但在十亿级语料上差距不明显。

2.3 聚类与异常词检测:换个角度看看空间结构

相似度和类比都是从"点对"角度看问题,聚类和异常词检测则是从"局部结构"角度看。做法是挑一组语义上属于同一类的词,比如若干种水果,把它们聚成若干簇,看同类的词是不是被分到同一个簇里。指标可以用纯度、NMI 或者简单的簇内平均距离。这个评价不像 Spearman 那样有标准分数,更适合做定性观察和版本对比。

异常词检测更好玩:给一组词,其中有一个语义上格格不入,让模型把它揪出来。比如给"苹果、香蕉、橘子、汽车",模型应该指向"汽车"。这种题能有效暴露模型是不是把高频共现当成了语义,因为"汽车"和水果在语料里几乎不共现,能挑出来说明向量确实编码了类别信息。实际用的时候我会自己构造几十组这样的题目,覆盖不同的抽象层级,作为相似度评价的补充,成本很低但很有诊断价值。

3. 外在评价:把向量丢进真实任务见真章

3.1 下游任务怎么选、向量怎么用

外在评价的核心问题是:选什么任务。我的原则是选一个和你最终用途结构相似的任务。如果你的向量是要拿去做检索,那就用句对匹配或者排序任务来评;如果是要做分类特征,就用文本分类;如果是要做实体识别,那就用序列标注。拿一个八竿子打不着的任务来评,测出来的差距很可能来自任务本身的特性,而不是向量的质量。

向量在任务里的使用方式也直接决定结论。常见三种:第一种是冻结,把词向量当固定特征,只训练上层的分类器,这是最纯粹的评价方式,因为它完全排除了向量被任务数据"救回来"的可能;第二种是微调,向量跟着一起训练,这时候评价的其实是"向量作为初始化"的价值,和"向量本身质量"是两回事;第三种是直接算距离,比如用平均词向量做句子表示,然后算最近邻,这种最轻量,适合快速验证。

我的建议是:第一次评价一定用冻结方式,得到一个干净的数字;然后再用微调方式跑一遍,看两者差距。如果微调之后差距被抹平了,说明你的向量只是提供了个还行的起点,本身没有特别的信息量;如果冻结方式就明显更好,那才是真的有用。

3.2 控制变量:别让评价被别的东西污染

外在评价最怕的就是变量没控住。我踩过的坑包括:换了向量同时也换了分词器,结果差距不知道是谁贡献的;训练集划分不一样,测试集有重叠,分数虚高;随机种子不同,跑了三次结果波动 3 个点,却拿单次结果下结论。

正确的做法是把所有和向量无关的因素全部固定下来:同一份训练/验证/测试划分、同一个分类器结构、同一组超参数、同一批随机种子(至少 3 个,取均值并报告标准差)。如果条件允许,做配对显著性检验,样本量小的时候差异很容易是噪声。我在一个中等规模的分类任务上做过实验,同一个向量换 5 个种子,准确率波动范围能到 1.8 个点,所以低于 1 个点的差距我一律当作没有差异。

还有一个容易忽略的点是词表对齐。外在评价用的任务数据里,有些词在向量词表里不存在,必须处理成 UNK 向量。UNK 向量怎么初始化影响很大:全零、随机、或者是所有词向量的均值,三种方式在小数据集上能差出好几个点。我一般用均值,并且在报告里注明,避免别人复现时对不上。

4. 实操:搭一套可复现的评价流程

4.1 数据准备与词表对齐

评价流程的第一步不是写代码,是把数据理顺。需要准备三样东西:向量文件、相似度评价集(每行词1 词2 分数,制表符分隔)、类比评价集(每行四个词,空格分隔)。格式定死,后面的脚本就能通用。

读写向量我用gensim的KeyedVectors,它把词到索引的映射和矩阵分开了,查词和批量算相似度都很快。加载大文件时用mmap='r'可以避免一次性吃满内存,几百兆的文本向量文件大概几秒就能挂上。如果你的向量是自己训练的,直接model.wv也能用,但建议先save_word2vec_format落盘一份,保证评价阶段用的是固定快照,而不是训练中途的状态。

import numpy as np from gensim.models import KeyedVectors from scipy.stats import spearmanr kv = KeyedVectors.load_word2vec_format( "vectors.vec", binary=False, mmap="r" ) print("词表大小:", len(kv.index_to_key), "维度:", kv.vector_size)

跑任何评价之前,先看一眼词表大小和维度,再看几个高频词的最近邻,确认向量没有加载错、没有维度错位。我曾经因为文件第一行的词数 维度头写错了,导致整个矩阵错位,评价分数低得离谱,排查了半天才发现是格式问题。

4.2 相似度与类比评测的实现

相似度评测的核心就是逐对算余弦、收集分数、算秩相关。这里不用kv.similarity,因为它内部会做额外的归一化处理,自己写更可控,也便于替换成欧氏距离等其他度量做对比。

def cosine(u, v): nu, nv = np.linalg.norm(u), np.linalg.norm(v) if nu == 0 or nv == 0: return 0.0 return float(np.dot(u, v) / (nu * nv)) def eval_similarity(kv, path): gold, pred, miss = [], [], 0 with open(path, encoding="utf-8") as f: for line in f: parts = line.rstrip("\n").split("\t") if len(parts) != 3: continue w1, w2, s = parts if w1 not in kv or w2 not in kv: miss += 1 continue gold.append(float(s)) pred.append(cosine(kv[w1], kv[w2])) if len(gold) < 10: return None rho, _ = spearmanr(gold, pred) cover = len(gold) / (len(gold) + miss) return {"rho": round(rho, 4), "n": len(gold), "coverage": round(cover, 4)}

覆盖率这个字段一定要打出来。我见过有人在只有 300 个词对、覆盖率 62% 的情况下报了个 0.81 的 Spearman,拿去和覆盖率 95%、分数 0.72 的模型比,结论完全反了。

类比评测稍微复杂一点,难点在候选排除和 top-k 统计。下面这版用kv.cosine_similarities一次性算完整个词表,比逐个查快得多。

def eval_analogy(kv, path, topk=3): total = hit1 = hitk = skip = 0 for line in open(path, encoding="utf-8"): parts = line.strip().split(" ") if len(parts) != 4: continue a, b, c, d = parts if not all(w in kv for w in parts): skip += 1 continue total += 1 query = kv[a] - kv[b] + kv[c] sims = kv.cosine_similarities(query, kv.vectors) order = np.argsort(-sims) picked = [] for idx in order: w = kv.index_to_key[int(idx)] if w in (a, b, c): continue picked.append(w) if len(picked) >= topk: break if picked and picked[0] == d: hit1 += 1 if d in picked: hitk += 1 if total == 0: return None return { "top1": round(hit1 / total, 4), "top3": round(hitk / total, 4), "n": total, "skipped": skip, }

跑完这两个函数,你就有了最基础的一组数字。别急着下结论,先把它们和覆盖率、跳过量放在一起看。如果类比集的跳过量超过 20%,说明你的词表太小或者词形处理没过关,这时候的分数参考价值很低。

4.3 结果记录与版本对比

评价最容易犯的错是"只跑一次,记在脑子里",一周后完全不记得当时用的什么配置。我现在的习惯是建一张表,每次实验追加一行,字段固定:向量文件、训练语料来源和规模、维度、窗口、最小词频、相似度 rho、覆盖率、类比 top1、类比 top3、备注。

版本语料规模维度窗口最小词频相似度 rho覆盖率类比 top1类比 top3
v13000 万 token100550.6210.910.2840.512
v23000 万 token200550.6480.930.3010.529
v31.2 亿 token200530.7020.950.3520.581
v41.2 亿 token200830.7110.950.3360.574
v5(词形归一)1.2 亿 token200530.7240.970.3810.607

这张表能看出很多东西。v1 到 v2 只调维度,涨了 2 个多点,说明维度不够是当时的瓶颈;v2 到 v3 扩语料,涨了 5 个多点,说明语料规模比维度更重要;v4 把窗口从 5 拉到 8,相似度略涨但类比略降,典型的话题集中度换来的代价;v5 做词形归一,两个指标都涨,属于低成本高回报的操作。这些结论只有把数据摆在一起才看得出来。

5. 常见问题与排查手册

5.1 分数虚高的四个典型来源

第一个是各向异性。训练出来的词向量往往不是均匀分布在球面上的,而是挤在一个狭窄的锥形区域里,导致任意两个词的余弦相似度都是 0.3 到 0.7 这种量级,区分度被压缩。这种情况下相似度评价的绝对分数会偏高但不一定有意义。一个常用的缓解办法叫 all-but-the-top:算所有词向量的均值并减掉,再做 PCA 去掉前一到两个主成分,然后再算相似度。我实测在中等规模语料上,这个处理能让相似度评分的区分度明显提升,但会不会改善下游任务要看具体情况,不能一概而论。

第二个是hubness。有些词莫名其妙地成为大量词的最近邻,不管查什么都能撞上它,这类词通常高频、语义泛化(比如"时间""东西"这类)。检测方法是统计每个词出现在 top-10 最近邻里的次数,画个分布,如果少数词占了极大比例,说明 hubness 严重。处理办法包括归一化、用 CSLS 做双向距离惩罚等。

第三个是频率偏置。低频词的向量训练不充分,方向基本是随机的,但它们在评价集里往往被当作难题出现。如果评价集里低频词占比高,分数就会整体偏低。这时候应该按词频分桶,分别报告分数,而不是只看总体。

第四个是评价集泄漏。如果你用的评价集词汇大量出现在训练语料里(这几乎必然),模型对这些词学得特别准,分数就会虚高。严格的做法的做法是把评价集的词从训练语料里去掉再训一遍,但这不现实。退一步的做法是至少知道这件事,别把 0.8 的 Spearman 当成"模型懂语义"的证据。

5.2 排查速查表

现象可能原因排查方法处理方式
相似度 rho 低于 0.4语料太小或噪声大看词表大小、抽查最近邻扩语料,加最小词频过滤
分数极高但下游任务不涨各向异性或评价集偏简单算相似度分布、去主成分后重测去主成分、换更难的评价集
覆盖率低于 0.8分词或词形不一致统计缺失词清单统一分词器,做词形归一
类比 top1 高但 top3 不高模型把答案排在 2、3 位打印错误样例本身不算问题,报 top3 即可
换种子波动超过 2 点评价集太小多种子重复扩评价集或用配对检验
最近邻全是同一个词hubness统计 top-10 出现频次CSLS 或归一化处理
中文效果明显差于英文分词粒度不匹配对比字级和词级向量尝试字级向量或混合粒度

5.3 几个只有踩过才知道的细节

大小写。英文语料里Apple和apple是不是合并,对相似度评价影响不小。合并之后词频变高、向量更稳,但专有名词的区分度丢了。我的做法是保留原始大小写训练,评价时先精确匹配,匹配不上再试小写形式,这样既保住了信息,又不至于让覆盖率掉太多。

标点和特殊符号。评价集里出现带撇号的词(比如缩写形式)时,如果你的分词器把撇号切掉了,就永远匹配不上。这不是模型的问题,是预处理链路不一致。统一用同一个分词器处理语料和评价集,是最省事的解法。

中文的分词粒度。中文词向量评价里最头疼的是切分歧义。"研究生命起源"切成"研究/生命/起源"还是"研究生/命/起源",直接决定词表里有没有"研究生"这个词。我一般的做法是同时准备词级和字级两套向量,词级用于需要精确语义的任务,字级用于覆盖率要求高的场景。字级向量的相似度评价没法直接用人标的词对,得自己做映射,麻烦但避开了 OOV。

维度选择的经验值。不是越高越好。100 到 300 维是大多数场景的甜点区,再往上收益递减,而且小语料上高维更容易过拟合。我做过一组对比,3000 万 token 的语料,维度从 100 加到 300,相似度涨了 3 个点;从 300 加到 500,只涨了不到 1 个点,训练时间却翻倍。这个边际收益曲线值得每个人自己画一遍。

6. 训练配置怎样影响最终评价分数

6.1 窗口、负采样与最小词频

窗口大小决定了模型看到多远的上下文。窗口小(2 到 5),学到的偏向于词性、句法层面的相似,同义词、反义词容易挨得很近;窗口大(8 到 15),学到的是话题层面的相似,同领域的词会聚在一起。这直接体现在评价分数上:用小窗口训练的向量,在类比任务上通常更好,因为句法关系更清晰;用大窗口训练的,在主题聚类类的评价上更好。所以看到"窗口 5 比窗口 10 好"这种结论时,一定要问一句是在什么任务上比的。

负采样数量影响的是训练的充分程度。默认的 5 到 15 在大多数语料上都够用,样本量特别大的时候可以适当提高,但边际收益也很有限。真正影响大的是最小词频。设成 1,词表里塞满只出现一两次的词,这些词的向量基本是噪声,还会拉低相似度评价的覆盖率相关指标;设成 5 到 10,词表会小一圈,但留下的都是相对可靠的。我一般设 5,然后在报告里注明,方便对比。

还有一个经常被忽略的是下采样。高频词("的""是""and""the")在语料里占比极高,不下采样的话它们会主导训练。常见做法是按词频的平方根倒数进行下采样,阈值设在万分之一左右。这个操作对评价分数的影响是正向的,尤其是相似度任务,因为它让中频词获得了更多的训练信号。

6.2 语料领域与评价集的匹配问题

这是我最想强调的一点:通用评价集上的高分,不代表你的垂直领域能用。拿新闻语料训出来的向量去评医疗文本的相似度,可能一堆词都不在词表里;反过来,用医疗语料训的向量去跑通用评价集,覆盖率可能只有六七成,分数自然难看。

我一般的做法是准备两套评价:一套通用评价集,用来和公开结果做粗略对照;一套自建的领域评价集,几百个词对就够,由业务方或者熟悉领域的人标注,专门用来验证领域向量的质量。自建评价集的成本不高,但价值极大,因为它反映的是你真正关心的那部分语义。标注的时候有几个要点:词对要覆盖高频和低频、具体和抽象、同义和相关但不完全同义,不要全挑容易的;每个词对至少两个人标,一致性太低的重标或者剔除。

领域评价集还有一个隐性好处:它能暴露语料本身的偏差。比如你的语料全是产品评论,那"质量"这个词的向量会严重偏向"好/差"的评价维度,而不是物理意义上的质量。这种偏移在通用评价集上完全看不出来,但在领域评价集上一目了然。

7. 我自己的一套判断顺序

跑了这么多轮下来,我现在拿到一批新向量,基本是按固定顺序过一遍的。先看词表大小和覆盖率,覆盖率不到 0.85 的直接打回去查预处理;然后抽十个高频词看最近邻,肉眼判断有没有明显离谱的,比如"医生"最近的邻居是"护士""病人"就正常,是"但是""因为"就有问题;接着跑相似度评价,同时报分桶结果,看低频词和高频词的差距有多大;再跑类比 top1 和 top3;最后才上外在评价。

这个顺序的好处是前面几步都很快,几分钟就能跑完,能挡掉大部分明显不合格的版本,不至于在一个坏向量上浪费半天做下游实验。外在评价我一般只跑一到两个任务,多了没意义还会引入噪声。

关于分数怎么看,我的经验是:不要追绝对值的极限,要看增量是否稳定。同一个评价集上,从 0.65 提到 0.70 可能是真提升,但从 0.78 提到 0.79 很可能就是噪声。评价集本身的规模决定了它的分辨率上限,几百个词对的评价集,分辨不出 1 个点的差异。想要更细的区分,唯一的办法是扩评价集,而不是反复调训练参数去凑那个小数点。

最后一个习惯:每次评价完,把错误样例存下来。相似度任务里那些"人工打高分但模型算低分"的词对,类比任务里排第二第三的答案,这些比分数本身有信息量得多。看多了你会发现,模型的错误往往高度模式化,集中在某几类语义关系上,这种发现对下一步怎么改语料、怎么调参数,比一个总分有用得多。

返回列表