☰
从余弦相似度到Word2Vec:文本向量语义表示与相似度计算实战
2026/10/14 7:44:14 网站建设 项目流程

1. 从字符串到向量:为什么文本必须变成数字

1.1 机器看不懂文字,但看得懂坐标

做自然语言处理这些年,我越来越觉得,这个领域最核心的矛盾就一句话:人用符号交流,机器用数字计算。你给计算机扔一句“今天天气不错”,它看到的只是一串Unicode码点,它不知道“天气”和“气候”有关系,也不知道“不错”和“糟糕”是反义词。所以整个NLP工程的第一道门槛,就是怎么把文本变成一串有意义的数字——这就是文本表示要解决的问题。

我见过不少刚入行的朋友,一上来就急着上大模型、调API,结果连最基础的文本相似度都算不准。问题往往出在底层表示上:你喂给模型的特征本身就是一锅粥,后面再花哨的架构也救不回来。所以这篇东西我想把向量语义这条线从头捋一遍,重点讲三个东西:余弦相似度、TF-IDF、Word2Vec。这三个不是并列关系,而是一条清晰的演进链——从“怎么比较两个向量”,到“怎么把词变成有区分度的向量”,再到“怎么让向量带上语义”。

先说清楚适合谁看。如果你正在做搜索、推荐、文本去重、问答匹配、评论聚类这类活儿,或者你刚学完Python想找个NLP方向练手,那这篇内容基本能覆盖你80%的日常需求。不需要你有深度学习背景,但得会一点Python和基本的线性代数概念——知道什么是向量、什么是点积就够了。

1.2 向量语义到底在解决什么问题

一句话概括:向量语义就是让“意思相近的文本,在向量空间里的距离也相近”。这句话听起来简单,但它是搜索、推荐、问答系统的地基。

举个我实际遇到的场景。之前帮一个团队做FAQ自动匹配,用户问“怎么修改绑定的手机号”,知识库里有一条“如何更换预留电话”。字面上看,这两句话只有“修改/更换”“手机号/电话”勉强对得上,如果按关键词匹配,很容易漏掉。但如果把两句话都映射成向量,算一下余弦相似度,发现相似度有0.82,那就能正确匹配上。这就是向量语义的价值——它捕捉的是语义层面的接近,而不是字面的重合。

再往深一层说,向量语义要解决三个递进的问题:

  • 表示问题:一段文本怎么变成固定长度的向量?这是TF-IDF和Word2Vec各自回答的问题。
  • 度量问题:两个向量怎么算“像不像”?这是余弦相似度回答的问题。
  • 泛化问题:没见过的新词、新句子怎么办?这是Word2Vec这类分布式表示要解决的。

这三个问题环环相扣。你光有好的表示,不会度量,等于白搭;你光会度量,表示做得稀烂,算出来的相似度也没意义。所以下面我会按“度量→表示→语义”的顺序展开,这也是我建议的学习路径。

2. 余弦相似度:判断两个向量像不像的那把尺子

2.1 为什么是余弦而不是欧氏距离

先说结论:在文本场景下,余弦相似度几乎总是比欧氏距离更合适。原因在于文本向量的一个特点——长度(模长)往往反映的是文档长短,而不是内容差异。

我举个具体的例子你就明白了。假设有两个文档:

  • 文档A:“苹果 手机 电池 续航”
  • 文档B:“苹果 手机 电池 续航 苹果 手机 电池 续航 苹果 手机 电池 续航”

如果按词频向量来算,文档B的向量模长是文档A的3倍,但它们的方向完全一致。用欧氏距离算,这两个文档“距离很远”;用余弦相似度算,相似度是1.0,完全一样。显然,从语义角度,这两个文档讲的是同一件事,只是B重复啰嗦了三遍。余弦相似度只看方向、不看长度,正好符合这个直觉。

余弦相似度的公式是:

cos(A, B) = (A · B) / (||A|| * ||B||)

其中A·B是点积,||A||是向量的L2范数。取值范围是[-1, 1],1表示方向完全相同,0表示正交(无关),-1表示方向完全相反。在TF-IDF这种非负向量里,取值范围实际是[0, 1]。

注意:余弦相似度对向量的绝对大小不敏感,但对方向极其敏感。这意味着如果你做了归一化(把向量除以模长),余弦相似度就退化成点积,计算会快很多。工程上我一般都会先归一化再算,省一次除法。

2.2 手算一遍,把公式变成肌肉记忆

光看公式没用,我带你手算一遍。假设有两个三文档的TF-IDF向量(后面会讲怎么来的,这里先当已知):

  • A = [0.5, 0.8, 0.0]
  • B = [0.4, 0.9, 0.1]

先算点积:0.5×0.4 + 0.8×0.9 + 0.0×0.1 = 0.2 + 0.72 + 0 = 0.92

再算模长:||A|| = √(0.25 + 0.64 + 0) = √0.89 ≈ 0.943;||B|| = √(0.16 + 0.81 + 0.01) = √0.98 ≈ 0.990

最后:0.92 / (0.943 × 0.990) ≈ 0.92 / 0.934 ≈ 0.985

相似度0.985,非常高,说明这两个文档语义高度接近。你把这个手算过程走一遍,比看十遍公式都管用。

2.3 工程实现里的三个坑

实际写代码的时候,余弦相似度有几个容易踩的坑,我一个个说。

第一个坑:稀疏向量直接算会爆内存。文本向量动辄几万维,而且绝大多数是0。如果你用numpy的稠密数组存,一个10万维的向量就是800KB,一万个文档就是8GB,直接崩。正确做法是用scipy的稀疏矩阵(scipy.sparse),只存非零元素。我实测过,同样一万个文档,稀疏存储能省95%以上的内存。

第二个坑:零向量。如果某个文档所有词都不在词表里,它的向量全是0,模长是0,除零会报错或者返回NaN。工程上必须加保护,遇到零向量直接返回相似度0。

第三个坑:批量计算的效率。如果你要算一个查询和一万个文档的相似度,千万别写for循环一个个算。用矩阵乘法一次性算完:把查询向量和文档矩阵做点积,再除以模长。numpy的@运算符或者sklearn的cosine_similarity都能做,速度差几十倍。

import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 假设 query_vec 是 (1, D),doc_matrix 是 (N, D) # 一次性算完,比循环快几十倍 sims = cosine_similarity(query_vec, doc_matrix)[0] top_k = np.argsort(sims)[::-1][:10]

这段代码是我做检索时的标准模板,cosine_similarity内部做了归一化,直接出结果,省心。

3. TF-IDF:给词加上“重要性”的权重

3.1 从词袋模型说起

要理解TF-IDF,得先理解它要解决的前一个问题——词袋模型(Bag of Words)。词袋模型的思路极其朴素:把一段文本里出现的词,按词表顺序数一遍,每个词出现几次就填几。比如词表是[苹果, 手机, 电池, 续航],文档“苹果 手机 电池 续航 苹果”就变成[2, 1, 1, 1]。

词袋模型能用,但有个致命缺陷:它把所有词看得一样重要。可实际上,“的”“了”“是”这种词满篇都是,但它们对区分文档内容毫无帮助;而“电池”“续航”这种词虽然出现次数少,却是文档的核心。TF-IDF就是来解决这个问题的。

3.2 TF和IDF各自在干什么

TF-IDF拆开看是两部分:

  • TF(词频):一个词在当前文档里出现得越多,它对这个文档越重要。最朴素的算法就是出现次数,但工程上一般用对数平滑:TF = 1 + log(词频),避免长文档里高频词权重爆炸。
  • IDF(逆文档频率):一个词在多少个文档里出现过。出现的文档越多,说明它越普通,权重应该越低。公式是IDF = log(总文档数 / 包含该词的文档数)。

两者相乘就是TF-IDF权重。直觉上就是:一个词在当前文档里出现得多,同时在其他文档里出现得少,它才最有区分度。

我举个实际例子。假设有1000篇文档,“的”出现在999篇里,IDF = log(1000/999) ≈ 0.001,权重几乎为0;“电池”只出现在5篇里,IDF = log(1000/5) ≈ 5.3,权重很高。这样算出来的向量,自然就把区分度高的词凸显出来了。

3.3 参数选择:平滑、归一化、子线性

TF-IDF看着简单,但sklearn的TfidfVectorizer有一堆参数,选错了效果差很多。我把几个关键参数拎出来说。

参数作用我的建议
sublinear_tf用1+log(tf)代替原始词频长文档场景建议开True
norm向量归一化方式默认'l2',配合余弦相似度用
smooth_idfIDF加1平滑,避免除零保持默认True
min_df忽略出现次数太少的词小数据集设2,大数据集设5
max_df忽略出现太频繁的词设0.8~0.9,过滤停用词
ngram_range词组合范围中文建议(1,2),英文(1,1)够用

sublinear_tf这个参数特别值得说。假设一个词在文档里出现100次,原始TF是100,对数平滑后是1+log(100)≈5.6。差距巨大。为什么?因为一个词出现10次和出现100次,对文档主题的贡献并不是10倍关系,而是递减的。对数平滑正好模拟这个递减效应。我做过对比实验,在长文档分类任务上,开sublinear_tf能提升2-3个百分点的准确率。

3.4 中文场景的特殊处理

TF-IDF在英文里天然好用,因为英文有空格分词。中文不行,得先分词。这里有个坑:分词粒度直接决定TF-IDF的效果。

我试过用“苹果手机电池续航”这句话,用不同分词器:

  • 粗粒度:苹果手机 / 电池 / 续航→ 3个词
  • 细粒度:苹果 / 手机 / 电池 / 续航→ 4个词

粗粒度保留了“苹果手机”这个完整概念,但词表会膨胀;细粒度词表小,但“苹果”和“手机”分开后,可能丢失组合语义。我的经验是:做短文本相似度用细粒度,做长文档分类用粗粒度。另外,中文停用词表一定要加,像“的、了、是、在、和”这些,不加的话IDF算出来全是噪声。

实操心得:中文TF-IDF我一般会加一步“自定义词典”,把领域专有名词(比如产品名、术语)塞进分词器的词典里,防止被切碎。这一步对效果提升很明显,尤其是垂直领域。

4. Word2Vec:让向量真正带上语义

4.1 TF-IDF的天花板在哪

TF-IDF有个绕不过去的硬伤:它假设词与词之间是独立的。在TF-IDF的向量空间里,“手机”和“电话”是两个完全正交的维度,相似度是0。可实际上这两个词意思很接近。这就导致一个问题:如果用户搜“电话”,而文档里写的是“手机”,TF-IDF算出来的相似度很低,会漏掉。

这个问题的根源是:TF-IDF是稀疏的、高维的、基于字面共现的表示。它没法表达“词与词之间的语义关系”。要突破这个天花板,就得换一种表示方式——把词映射到一个低维稠密的空间里,让语义相近的词在空间里也靠近。这就是词嵌入(Word Embedding),而Word2Vec是其中最经典的代表。

4.2 Word2Vec的两个训练套路

Word2Vec的核心思想一句话:一个词的含义,由它周围的词决定。这个假设叫分布式假设,是所有这些词嵌入方法的理论基础。

它有两种训练方式:

  • CBOW(连续词袋):用上下文预测中心词。比如句子“我 爱 吃 苹果”,用“我、爱、吃”预测“苹果”。适合小数据集,训练快。
  • Skip-gram:用中心词预测上下文。用“苹果”预测“我、爱、吃”。适合大数据集,对低频词效果更好。

我一般默认用Skip-gram,因为它在低频词上的表现明显更好,而低频词恰恰是TF-IDF处理不好的部分。代价是训练慢一些,但值得。

训练出来的结果是什么?每个词变成一个比如100维或300维的稠密向量。神奇的地方在于,这些向量满足一些语义运算关系,最著名的例子是:

向量(国王) - 向量(男人) + 向量(女人) ≈ 向量(女王)

这说明Word2Vec学到的向量不只是“共现统计”,而是捕捉到了某种语义结构。我第一次跑出这个结果的时候确实挺震撼的。

4.3 关键参数怎么调

Word2Vec的参数比TF-IDF多,而且对结果影响很大。我把最关键的几个列出来。

参数含义常用值说明
vector_size向量维度100~300太小欠拟合,太大过拟合且慢
window上下文窗口5~10大窗口捕捉主题,小窗口捕捉语法
min_count最低词频5低于此频率的词直接丢弃
sg训练方式11是Skip-gram,0是CBOW
negative负采样数5~15越大越准但越慢
epochs训练轮数5~20看语料大小,小语料多跑几轮

window这个参数特别有意思。窗口设小(比如2),学到的向量偏向语法相似,比如“跑”和“跳”接近;窗口设大(比如10),学到的偏向主题相似,比如“跑”和“运动”接近。所以做语法任务用小窗口,做主题任务用大窗口,这个经验很实用。

min_count也别小看。设成1的话,所有只出现一次的词都会进词表,这些词没有足够上下文,学出来的向量基本是噪声,还会拖慢训练。我一般设5,语料大的话设10。

4.4 从词向量到句向量:一个绕不开的坎

Word2Vec给的是词向量,但实际任务里我们要的是句向量或文档向量。怎么从词向量得到句向量?最朴素的做法是平均:把句子里所有词的向量加起来除以词数。

这个做法简单,但有个问题:它把每个词看得一样重。像“的”“了”这种词也参与平均,会稀释语义。改进做法是TF-IDF加权平均:用每个词的TF-IDF权重去加权它的词向量。这样重要的词贡献大,停用词贡献小。

import numpy as np def sentence_vector(words, w2v_model, tfidf_weights): vecs = [] weights = [] for w in words: if w in w2v_model.wv: vecs.append(w2v_model.wv[w]) weights.append(tfidf_weights.get(w, 1.0)) if not vecs: return np.zeros(w2v_model.vector_size) vecs = np.array(vecs) weights = np.array(weights).reshape(-1, 1) return (vecs * weights).sum(axis=0) / weights.sum()

这段代码是我常用的句向量生成模板,加权平均比简单平均在检索任务上能提升5个百分点左右的召回率。当然,现在有更好的方案(比如Sentence-BERT),但Word2Vec加权平均胜在轻量、快、不需要GPU,很多场景够用了。

5. 三者怎么配合:一条完整的实战链路

5.1 选型决策:什么场景用什么

这三个技术不是替代关系,而是各有适用场景。我整理了一张决策表,你对着选就行。

场景推荐方案理由
关键词搜索、精确匹配TF-IDF + 余弦快、可解释、无需训练
短文本去重TF-IDF + 余弦字面重合度高,够用
语义搜索、问答匹配Word2Vec + 余弦能捕捉同义词、近义表达
冷启动、无标注数据TF-IDF不需要训练,直接算
大规模语料、有训练时间Word2Vec语义泛化能力强
对可解释性要求高TF-IDF能看出哪些词贡献大

我的实际经验是:先用TF-IDF搭baseline,跑通了再上Word2Vec对比。很多时候TF-IDF的效果已经够用,上Word2Vec反而引入训练成本和调参负担。别为了用新技术而用新技术。

5.2 一个完整的检索流程

我把整个流程串一遍,从原始文本到最终排序。

第一步:预处理。分词、去停用词、转小写。中文用jieba或类似工具,英文用nltk。这一步别偷懒,脏数据进去,垃圾结果出来。

第二步:建TF-IDF矩阵。用TfidfVectorizer拟合整个语料,得到文档-词矩阵。注意fit只在训练集上做,测试集用transform,否则会数据泄露。

第三步:训练Word2Vec。用同样的分词结果训练,得到词向量。语料小的话可以加载预训练词向量,效果更稳。

第四步:生成句向量。查询和文档都用TF-IDF加权平均生成句向量。

第五步:算相似度排序。用余弦相似度算查询和所有文档的相似度,取Top-K。

第六步:融合两路结果。把TF-IDF的相似度和Word2Vec的相似度加权融合,比如0.4 * tfidf_sim + 0.6 * w2v_sim。权重靠验证集调。

这套流程我在多个项目里用过,稳定可靠。融合两路结果通常比单用一路好,因为TF-IDF擅长字面匹配,Word2Vec擅长语义匹配,互补。

5.3 效果评估:别只看准确率

评估检索效果,别只看准确率。我一般看三个指标:

  • Recall@K:前K个结果里有没有正确答案。K一般取10或20。
  • MRR(平均倒数排名):正确答案排得越靠前越好。
  • NDCG:考虑排序位置的加权指标,最贴近真实体验。

这三个指标配合看,才能全面反映系统好坏。只看准确率容易漏掉“答案排在第二页”这种问题。

6. 踩过的坑和排查清单

6.1 常见问题速查

现象可能原因排查方向
相似度全是0向量未归一化或词表不匹配检查fit/transform是否一致
相似度全是1向量维度塌缩检查是否所有文档向量相同
结果和直觉不符停用词没去干净打印Top词看权重
Word2Vec训练慢语料太大或维度太高降维度、加min_count
内存爆掉用了稠密矩阵换scipy稀疏矩阵
新词查不到词表外词(OOV)用subword或预训练向量

6.2 三个我踩过的真实坑

坑一:TF-IDF的fit和transform混用。我早期做在线服务,每次来一个查询就重新fit一次,结果每次词表都不一样,相似度算出来乱七八糟。正确做法是训练时fit一次,把vectorizer存下来,线上只transform。这个坑很隐蔽,因为离线测试时不容易发现。

坑二:Word2Vec的随机性。Word2Vec训练有随机初始化,同样的语料跑两次结果不一样。做对比实验时一定要固定随机种子,否则你以为是参数调优带来的提升,其实是随机波动。我一般设seed=42,跑三次取平均。

坑三:中文分词的领域适配。通用分词器在垂直领域经常切错。比如“深度学习框架”可能被切成“深度/学习/框架”,但你要的是“深度学习/框架”。解决办法是加自定义词典,把领域术语提前塞进去。这一步花十分钟,效果提升可能超过调参一整天。

6.3 性能优化的几个技巧

最后分享几个提速技巧。第一,预计算文档向量,别每次查询都重算,存到内存或向量数据库里。第二,用矩阵运算代替循环,前面说过,差几十倍。第三,降维,TF-IDF可以用TruncatedSVD降到几百维,速度提升明显,精度损失很小。第四,缓存高频查询,很多系统里20%的查询占了80%的流量,缓存命中率很高。

提示:如果文档量超过百万级,单机内存放不下所有向量,就该考虑向量数据库了。但那是另一个话题,这里不展开。

这套东西我从头到尾跑过很多遍,从最初手写余弦相似度,到后来用sklearn、gensim,再到自己封装工具类。核心逻辑没变过:好的表示 + 对的度量 + 合理的融合。把这三块吃透,NLP里一大半的检索和匹配问题都能自己搞定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询