☰
文本表示与相似度计算全解析:从TF-IDF到BERT的选型与避坑指南
2026/10/14 1:21:01 网站建设 项目流程

1. 从"机器看不懂字"说起:文本表示到底在解决什么问题

很多人刚接触自然语言处理时,都会有一个直觉上的困惑:明明计算机连"苹果"和"梨"哪个能吃都分不清,凭什么能判断两句话意思相近?这个问题的答案,就藏在文本表示这四个字里。

文本表示要干的事情,说白了就是把人类能看懂的文字,翻译成机器能计算的数字。机器不会"理解"语义,它只会做加减乘除、算距离、比大小。所以我们必须把一句话、一个词、一整篇文章,变成一串数字或者一个向量,让机器能拿这些数字去做运算。而相似度计算,就是在这套数字体系建立起来之后,去衡量两段文本在数学空间里到底靠得多近。

这两件事是绑在一起的:表示方法决定了你能算出什么样的相似度,相似度计算又反过来检验你的表示到底靠不靠谱。举个最直白的例子,如果你用"词出现了几次"来表示一句话,那"我喜欢你"和"你欢喜我"在机器眼里几乎一模一样,因为词频完全相同,但语义其实差得远。这就是表示方法的局限直接导致了相似度计算的失真。

我在实际做项目的时候,踩过最多的坑不是模型不够先进,而是表示层选错了。很多新手一上来就想上大模型,结果连最基础的词袋模型和TF-IDF都没搞明白,遇到问题根本不知道是数据的问题、表示的问题还是相似度度量的问题。所以这篇内容我打算从最底层讲起,把文本表示的几条主流技术路线、相似度计算的常见方法、以及它们之间的搭配关系讲透,再补充一些只有真正跑过项目才会知道的细节。

这篇适合谁看?如果你是刚入门NLP、被各种"词向量""Embedding""余弦相似度"绕晕的初学者,这篇能帮你把脉络理清;如果你已经用过一些工具但总是调不好效果,这篇里的选型逻辑和避坑经验应该对你有用。全文我会尽量用生活化的类比,把抽象概念落到能上手操作的程度。

2. 文本表示的几条技术路线:从离散符号到稠密向量

文本表示的发展其实有一条很清晰的主线:从离散走向连续,从稀疏走向稠密,从只看词本身走向看上下文。理解这条主线,比死记某个具体算法重要得多。

2.1 离散表示:独热编码与词袋模型为什么"能用但不好用"

最早期的表示方法非常朴素。独热编码(One-Hot Encoding)的思路是:假设我的词表里有10000个词,那每个词就用一个10000维的向量表示,这个词对应的位置是1,其他全是0。比如"猫"是第3个词,那它的向量就是第3位为1、其余9999位为0。

这个方法的好处是简单、直观、无歧义。但问题也很致命。第一,维度爆炸,词表有多大向量就有多长,实际项目里词表动辄几十万,这个向量根本没法高效存储和计算。第二,也是最要命的,任意两个不同词的向量内积都是0,也就是说"猫"和"狗"在机器眼里和"猫"与"汽车"的距离一样远,完全丢失了语义关联。第三,它无法表达词与词之间的相似性,这对相似度计算来说是灾难。

词袋模型(Bag of Words, BoW)在此基础上往前走了一步。它不再关心单个词,而是把一整句话或一整篇文档表示成"每个词出现了几次"的计数向量。比如"我爱自然语言处理,也爱机器学习",词袋会统计出"爱"出现2次,其他词各1次。它把词序完全丢掉了,所以叫"词袋"——就像把所有的词扔进一个袋子里,谁先谁后不管了。

词袋模型在文本分类这种任务上其实还挺能打,因为分类往往靠关键词就够了。但它的硬伤同样明显:丢失词序意味着"狗咬人"和"人咬狗"表示完全一样;而且它和独热编码一样面临高维稀疏的问题。我见过不少项目在数据量小、任务简单时用词袋就能跑出不错的效果,但一旦涉及语义理解就立刻露馅。

2.2 权重化表示:TF-IDF如何让"重要的词"浮出水面

词袋模型有个明显的不合理之处:它把所有词一视同仁。但实际语言里,"的""了""是"这种词到处都是,却几乎不携带信息;而"量子""并购""过敏"这种词虽然出现少,却往往是理解文本的关键。

TF-IDF(词频-逆文档频率)就是来解决这个问题的。它的核心思想是:一个词在当前文档里出现得越多(TF高),同时在整个语料库里出现得越少(IDF高),那它就越重要。

具体计算上,TF是某个词在当前文档中的出现次数除以文档总词数,做归一化;IDF是语料库总文档数除以包含该词的文档数,再取对数。两者相乘就是这个词的TF-IDF权重。这样"的"这种到处都有的词IDF极低,权重被压下去;而"并购"这种只在少数文档出现的词IDF很高,权重被抬起来。

TF-IDF在信息检索、关键词提取、文本相似度里应用极广,而且它有个巨大优势:不需要训练,纯统计就能算。我在做文档去重、相似文章推荐这类任务时,TF-IDF配合余弦相似度往往能作为非常强的基线,甚至在一些场景下不输深度学习模型。但它依然没有解决语义问题——"电脑"和"计算机"在TF-IDF眼里是两个毫不相关的词,因为它们字面不同。

2.3 稠密表示:Word2Vec与GloVe如何把语义装进向量

真正的转折点来自词嵌入(Word Embedding)。它的核心洞察是:一个词的含义,可以由它周围经常出现的词来定义。这就是分布假说——"观其伴,知其义"。

Word2Vec是其中的代表,它有两种训练方式。CBOW是用上下文词去预测中心词,比如看到"我爱吃___",去猜中间是"苹果";Skip-gram反过来,用中心词去预测周围的词。训练完之后,每个词就得到一个几十到几百维的稠密向量。神奇的地方在于,这些向量天然带有语义结构:语义相近的词向量距离近,而且还能做类比运算,经典的例子是"国王 - 男人 + 女人 ≈ 女王"。

GloVe走的是另一条路,它基于全局的词共现矩阵做矩阵分解,把共现统计信息压缩进低维向量。相比Word2Vec只看局部窗口,GloVe利用了全局统计,在某些任务上表现更稳。

稠密表示最大的价值在于解决了语义相似度问题。现在"电脑"和"计算机"因为经常出现在相似的上下文里,它们的向量会非常接近,相似度计算终于能反映真实语义了。但Word2Vec和GloVe有个共同的硬伤:每个词只有一个固定向量,无法处理一词多义。"苹果"在"吃苹果"和"苹果发布会"里意思完全不同,但它们的向量是一样的。

2.4 上下文表示:ELMo、BERT带来的动态词向量革命

为了解决一词多义,上下文相关的动态表示登场了。核心思路是:一个词的向量不应该固定,而应该根据它所在的句子动态生成。

ELMo用双向LSTM,把前向和后向的语言模型输出拼接起来,为每个词生成一个融合了上下文的向量。同一个"苹果",在不同句子里得到的向量就不一样了。

BERT则用Transformer的编码器结构,通过掩码语言模型(随机遮住一些词让模型猜)和下一句预测两个任务做预训练。它的表示能力更强,而且可以针对下游任务做微调。BERT输出的每个词的向量都深度依赖整句话的上下文,一词多义问题被大幅缓解。

不过这里有个很多人会忽略的点:BERT这类模型直接拿来做句子相似度,效果往往不如预期。因为它的向量空间不是为相似度设计的,直接取[CLS]向量或者平均池化,得到的相似度分布可能很"挤"——所有句子的相似度都集中在0.7到0.9之间,区分度很差。这也是后来出现了Sentence-BERT这类专门为句子相似度优化的模型的原因。

下面这张表把几条路线做个横向对比,方便你选型时快速定位:

表示方法维度特点是否含语义能否处理多义是否需要训练典型适用场景
独热编码高维稀疏否否否教学演示、极简场景
词袋模型高维稀疏否否否文本分类基线
TF-IDF高维稀疏弱否否检索、去重、关键词
Word2Vec/GloVe低维稠密是否是词级任务、特征输入
ELMo/BERT低维稠密是是是(可微调)语义理解、下游微调

3. 相似度计算的度量方法:选错了度量,再好的表示也白搭

表示做完了,接下来就是算两段文本到底有多像。这一步看似简单,其实门道不少,选错度量方式会让整个系统效果大打折扣。

3.1 余弦相似度:为什么它是文本相似度的默认选择

余弦相似度几乎是文本相似度计算的代名词。它算的是两个向量夹角的余弦值,公式是两个向量的点积除以它们模长的乘积。取值范围从-1到1,越接近1越相似。

为什么文本任务偏爱余弦而不是欧氏距离?关键在于余弦只看方向,不看长度。文本向量的模长往往和文本长度相关,一篇长文档的TF-IDF向量模长天然比短句大。如果用欧氏距离,长文档和短文档会被判定为"不相似",仅仅因为它们长度不同,这显然不合理。而余弦相似度把长度因素归一化掉了,只比较方向,也就是只比较"词的分布比例",这更符合我们对文本相似度的直觉。

我在实际项目里有个经验:做短文本相似度时,余弦相似度配合归一化后的向量几乎是标配。但要注意,如果向量里有负值(比如某些词向量),余弦依然能正常工作,但解释起来要小心,因为负相关也是有意义的。

3.2 欧氏距离与曼哈顿距离:什么时候该用"距离"而非"夹角"

欧氏距离就是两点之间的直线距离,直观、好理解。它在图像、聚类等场景用得多,但在文本里要慎用,原因上面说了——受向量模长影响大。

曼哈顿距离(也叫L1距离)是各维度差值的绝对值之和。它对异常值不如欧氏距离敏感,在某些高维稀疏场景下反而更稳。不过说实话,在文本相似度里这两个距离的使用频率远低于余弦相似度,通常只在特定场景(比如已经做过严格归一化的稠密向量)才会考虑。

这里有个容易踩的坑:很多人把"距离小"和"相似度高"搞混。距离是越小越相似,余弦是越大越相似,方向正好相反。写代码时如果混用,会出现"越相似的排越后"的诡异结果。我自己就干过这事,排查了半天才发现是排序方向反了。

3.3 杰卡德相似度与编辑距离:面向集合和字符串的度量

有些场景下,文本不是表示成向量,而是表示成集合或字符串,这时候就要换度量方式。

杰卡德相似度(Jaccard Similarity)算的是两个集合的交集大小除以并集大小。比如把两句话都拆成词的集合,看它们共有的词占所有词的比例。它在去重、短文本匹配里很好用,计算也快。但它同样丢失了词序和词频信息。

编辑距离(Levenshtein Distance)算的是把一个字符串变成另一个字符串所需的最少增删改操作次数。它在拼写纠错、模糊匹配里是主力。比如用户输入"自然语言处里",编辑距离能帮你找到"自然语言处理"。但它的计算复杂度是O(mn),长文本上会很慢,而且它衡量的是字符层面的相似,不是语义层面的。

选哪个度量,取决于你的文本是怎么表示的。向量表示配余弦,集合表示配杰卡德,字符串配编辑距离,这是最基本的搭配原则。

3.4 度量方式与表示方法的搭配矩阵

把表示和度量放在一起看,搭配关系其实很清晰。下面这张表是我总结的常用组合,可以直接对照使用:

表示方法推荐度量不推荐度量原因说明
词袋/TF-IDF余弦相似度欧氏距离模长受文本长度影响,需归一化方向
Word2Vec平均余弦相似度编辑距离稠密向量无语序,编辑距离无意义
BERT句向量余弦相似度杰卡德语义信息在向量里,集合运算丢失信息
词集合杰卡德相似度余弦集合无向量形式,余弦不适用
原始字符串编辑距离余弦字符级匹配,向量化反而绕远路

提示:搭配矩阵不是死规矩,但违背它之前,你得先想清楚为什么。大多数"效果不好"的问题,根源都在表示和度量的错配上。

4. 从零跑通一个文本相似度计算流程

光讲原理容易飘,这一节我把一个完整的文本相似度计算流程拆开,从数据准备到最终输出相似度,每一步都讲清楚在干什么、为什么这么干。

4.1 数据预处理:分词、去停用词与归一化

任何文本任务的第一步都是清洗。中文和英文的处理方式差别很大,这里分开说。

英文相对简单,按空格和标点切分基本就行,但要注意处理缩写(比如"don't"要不要拆成"do not")、大小写统一(通常全部转小写)、词形还原("running"还原成"run")。中文就麻烦多了,因为中文词与词之间没有空格,必须分词。常用的分词工具会把"自然语言处理"切成"自然/语言/处理"还是"自然语言/处理",结果直接影响后续表示。分词粒度没有绝对对错,要看任务——做检索时粗粒度可能更好,做细粒度情感分析时细粒度更合适。

去停用词是去掉"的""了""是""在"这类高频但无信息量的词。这一步能显著降低维度、提升信噪比。但要注意,停用词表不是万能的。比如做情感分析时,"不"这种否定词绝对不能去掉,否则"好"和"不好"就分不清了。我见过有人直接套用网上的通用停用词表,结果把关键否定词也删了,模型效果一塌糊涂。

归一化包括统一大小写、全角转半角、去除多余空白和特殊符号等。这些看似琐碎,但数据不干净,后面所有步骤都是在垃圾上做运算。

4.2 构建向量:以TF-IDF为例的完整计算过程

假设我们有三句话:

  • 文档1:"我爱自然语言处理"
  • 文档2:"我爱机器学习"
  • 文档3:"自然语言处理和机器学习都很有趣"

分词后得到词表:我、爱、自然、语言、处理、机器、学习、和、都、很、有趣。

先算TF。以文档1为例,共5个词,"我"出现1次,TF就是1/5=0.2。再算IDF。以"我"为例,它在文档1和文档2出现,共2个文档包含它,总文档数3,IDF = log(3/2) ≈ 0.405。那么"我"在文档1的TF-IDF就是0.2 × 0.405 ≈ 0.081。

把每个文档的每个词都这么算一遍,就得到一个3×11的矩阵,每行是一个文档的TF-IDF向量。这个矩阵通常非常稀疏,因为每句话只用到词表里的一小部分词。

实际写代码时,用scikit-learn的TfidfVectorizer几行就能搞定,但有几个参数要留意:ngram_range控制是否考虑词组(设为(1,2)会同时统计单词和双词组合),min_df和max_df控制词频过滤(太罕见和太常见的词都可以过滤掉),sublinear_tf开启后会对TF取对数,抑制高频词的过度影响。这些参数调一调,效果可能差不少。

4.3 计算相似度矩阵并解读结果

有了向量矩阵,算相似度就是一行代码的事。用余弦相似度算出3×3的相似度矩阵,对角线是自己和自己比,恒为1。文档1和文档2因为共享"我""爱",相似度会比较高;文档1和文档3共享"自然""语言""处理",相似度也不低;文档2和文档3共享"机器""学习",同样有相似度。

解读这个矩阵时要注意:相似度是相对的,不是绝对的。0.5的相似度算高还是低,取决于你的数据分布。如果所有文档两两相似度都在0.3到0.6之间,那0.5只是中等;如果大部分都在0.1以下,那0.5就是非常相似了。所以实际应用里,通常要结合阈值或者排序来用,而不是死盯一个绝对值。

4.4 用预训练模型做语义相似度的实操差异

如果用BERT这类预训练模型,流程会不一样。你不再需要手动分词构建词表,而是用模型自带的分词器(Tokenizer)把文本转成token id,喂给模型,取出句向量,再算余弦。

但前面提过,直接取BERT的[CLS]向量做相似度效果常常不理想。更靠谱的做法是用Sentence-BERT这类专门优化过的模型,它通过孪生网络结构训练,让语义相近的句子在向量空间里真正靠拢。用这类模型时,你会发现相似度分布明显更"散",区分度好很多。

实操中还有个细节:批量计算时要注意显存和速度的平衡。BERT推理比TF-IDF慢几个数量级,如果只是做粗筛,完全可以用TF-IDF先过滤掉明显不相关的,再用BERT精排。这种"粗排+精排"的两阶段架构在实际系统里非常常见,能兼顾速度和精度。

5. 那些只有真正跑过项目才知道的坑

原理和流程讲完了,但真正让项目翻车的,往往是这些细节。

5.1 相似度分数"挤在一起"怎么办

这是用BERT类模型做相似度时最常遇到的问题:所有句对的相似度都集中在0.8到0.95之间,你根本没法用一个阈值区分"相似"和"不相似"。

根本原因是预训练模型的向量空间不是为相似度设计的,它存在各向异性问题——所有向量都挤在一个狭窄的锥形区域里,导致任意两个向量的余弦都偏高。解决办法有几个:一是换用专门训练的句子相似度模型;二是对向量做白化处理(Whitening),通过线性变换让向量分布更均匀;三是用对比学习微调,让模型学会把相似的和不相似的拉开距离。我实测下来,换用Sentence-BERT类模型是最省事的,白化处理则适合不想换模型、只想后处理的场景。

5.2 长文本相似度:截断、分段还是池化

BERT有最大长度限制(通常512个token),长文档根本塞不进去。常见的处理方式有三种:直接截断(简单但丢信息)、分段后分别编码再池化(保留信息但丢失跨段关联)、用支持长文本的模型(如Longformer类)。

我的经验是:如果关键信息集中在开头或结尾,截断就够用;如果信息均匀分布,分段池化更稳。分段时不要简单平均,可以加权,比如标题和首段权重高一些。另外,长文本相似度往往不是"整体像不像",而是"某几个关键段落像不像",所以有时候先做段落级匹配再聚合,比直接整篇比效果更好。

5.3 领域词、新词与分词错误带来的连锁反应

在垂直领域做相似度,通用分词器经常把专业术语切碎。比如某个行业术语被切成三个毫无意义的片段,那这个词的语义就彻底丢了,相似度自然算不准。

解决办法是自定义词典。主流分词工具都支持加载用户词典,把领域术语加进去,保证它们不被切碎。这一步在医疗、法律、金融等领域几乎是必做的。另外,新词(网络热词、新出现的产品名)也要及时补充,否则模型对它们完全没有表示。

分词错误的影响是连锁的:切错了词,表示就错了,相似度就错了,最后整个系统都不可信。所以我在项目里养成的习惯是,先抽样检查分词结果,确认没问题再往下走,这一步花十分钟能省后面十小时的排查。

5.4 阈值设定:没有万能数字,只有数据驱动的选择

新手最爱问的一个问题是:"相似度多少算相似?"答案是:没有标准答案,必须用你的数据去定。

正确做法是:准备一批标注好的句对(相似/不相似),算出它们的相似度分布,然后找一个能最好地区分两类的阈值。如果两类分布重叠严重,说明你的表示或度量还不够好,光调阈值救不了。可以用ROC曲线或者精确率-召回率曲线来辅助选阈值,根据业务更看重精确率还是召回率来定。

我踩过的坑是:在验证集上定好的阈值,换到线上数据就失效了,因为数据分布变了。所以阈值要定期用新数据重新校准,不能一劳永逸。

6. 选型决策:不同场景下我会怎么选

讲了这么多,最后落到实操上,不同场景到底该怎么选,我按经验给几条明确的建议。

如果任务是文档去重、检索召回,优先用TF-IDF加余弦相似度。它快、稳、不需要训练,作为基线几乎不会让你失望。数据量大时配合倒排索引,效率极高。

如果任务是短文本语义匹配(比如判断两个问题是不是同一个意思),用Sentence-BERT类模型。它对语义的捕捉远超统计方法,而且现在有很多开箱即用的预训练模型,接入成本不高。

如果任务是词级别的相似(比如找同义词、做词聚类),Word2Vec或GloVe依然够用,而且比大模型轻量得多。没必要为了找同义词去跑BERT。

如果任务对一词多义敏感(比如"苹果"在不同语境下含义不同),那必须用上下文相关的动态表示,BERT类模型是首选。

如果数据量极小、标注成本高,别急着上深度学习,先用TF-IDF加规则跑一版,看看效果上限在哪。很多时候简单方法就够,过度工程化反而拖慢进度。

我个人在实际操作中的体会是:文本表示和相似度计算这件事,八成的效果来自数据质量和表示方法的匹配,只有两成来自模型本身的先进程度。与其纠结用哪个最新模型,不如先把分词、停用词、领域词典这些基础工作做扎实。我见过太多项目,换了一堆模型效果都不行,最后发现问题出在分词把关键术语切碎了。把基础打牢,再谈模型升级,这条路走得最稳。

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

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

立即咨询