资讯动态

表示型文本匹配模型解析:从DSSM到Sentence-BERT的演进与落地

发布时间:2026/10/15 9:55:28 来源:尧图企业网站定制
一个做搜索的朋友跟我吐槽用户搜“苹果手机”系统里明明有“iPhone 15 Pro”的商品结果就是搜不出来。不是数据问题也不是词表问题而是两句话的字符层面几乎不重合传统的关键词匹配根本兜不住这种语义鸿沟。这种场景就是文本匹配要解决的事而今天想聊的正是其中一个主流的实现分支表示型模型。文本匹配在NLP落地里太常见了搜索召回、FAQ问答、相似文本去重、推荐系统里的item匹配、对话系统的意图兜底……这些场景的核心问题都是同一个——给定两段文本判断它们是不是“同一个意思”。表示型模型的思路非常直接把每段文本各自编码成一个固定维度的向量然后用向量之间的距离余弦距离、内积等来衡量语义相似度。它结构清晰、方便部署、天然适合大规模召回所以是很多工程团队的默认起步方案。这篇文章我会把表示型模型的原理、演进、工程落地和踩坑全部拆开讲一遍从DSSM一路讲到基于预训练模型的双塔再到向量检索和训练细节。无论你是刚接手文本匹配项目的新手还是已经跑过几个模型、想优化线上的老手这篇文章应该都能给你一些增量信息。1. 表示型文本匹配先想清楚它在业务里解决什么问题1.1 为什么匹配不只是“搜关键词”大部分人第一次接触文本匹配都是从搜索引擎开始的。早期搜索系统基本依赖字面匹配query包含哪些词文档包含哪些词通过TF-IDF、BM25这类倒排索引算一个相关分。这套体系在长文档、关键词鲜明的场景里非常好用到今天仍然是搜索引擎召回层的主力。但问题出在语义等价的场景上。拿FAQ客服系统来说用户问“怎么退掉我的月卡”知识库里标准问句是“取消订阅的操作流程”。这句话在字符层面跟用户提问几乎没有重叠可语义上就是同一件事。还有电商搜索里“爸爸装”和“中老年男装”、“跑步鞋”和“运动鞋”这种近义改写字面匹配要么漏召要么召回一堆不相干的东西。表示型模型的作用就是把“字面匹配”升级成“语义匹配”。它不直接比较字符而是让模型先把文本理解成一组语义特征——一个向量然后在向量空间里做距离计算。语义相近的文本向量会落在空间里相近的位置语义无关的向量距离就远。这个思路从诞生那天起就很吸引人因为它把文本匹配问题的复杂度转化成了向量距离计算的复杂度。1.2 表示型与交互型两条路线的分工业内谈起文本匹配通常把它分成表示型和交互型两条路线。这个分野从早期论文至今仍然成立理解它对做技术选型很重要。交互型模型代表性的是Decomposable Attention、BERT匹配类模型会把两段文本的token级信息提前融合在一起模型能看到“A的这个词和B的那个词在位置上怎么对应有没有注意力关系”然后基于交互特征输出匹配分。这类模型精度高是很多线上精排模块的选择。但它有个硬伤两段文本必须一起过模型推理时没办法把文本A的表示预先计算好缓存下来。所以它只适合少量候选的比较比如粗排圈了1000条数据用交互模型精排到前50。表示型模型走的是另一条路两段文本各自过编码器互不干扰产出各自向量后再做相似度计算。这意味着所有的文本都可以提前离线算好向量、建好索引等到线上query来了只要把query编码成向量去向量库里做最近邻检索就行了。这个“解耦”带来的工程优势极其明显决定了它在召回、大规模匹配、以及类似推荐系统双塔召回等场景里的不可替代性。两者不矛盾反而经常配合使用表示型模型扛召回和粗排交互型模型做精排。以前我遇到过团队非要拿BERT匹配模型直接做全量检索结果每个query都要跟千万个候选全量计算一遍延迟直接爆炸。想明白“不同模型服务于不同漏斗阶段”这件事能少走很多弯路。2. 双塔架构拆解表示型模型的核心组件怎么选2.1 编码器从词粒度的DSSM到句子级别的预训练模型表示型模型最底层的组件是编码器它决定了一句话会被编码成什么水平的语义表示。编码器的选择基本决定了整个模型的上限。早期经典的DSSMDeep Structured Semantic Models用的是词哈希Word Hashing加全连接网络。它把文本按照字母n-gram切分映射成低维向量再经过多层全连接得到128维左右的语义向量。这个方案带来的一个好处是解决了词典爆炸和OOV问题英文字母n-gram天然能覆盖形态变化。但它对词序和上下文语义几乎没有建模能力相当于把句子当成一袋子特征来糊墙。今天看来效果粗糙但在2013年左右已经是非常大的突破。后来出现了基于CNN的表示型模型比如ARC-II以及Siamese CNN用卷积在局部窗口内捕捉短语特征也有基于RNN/LSTM的表示型模型比如Siamese LSTM能建模长距离的词序依赖。这些模型比DSSM语义理解能力强不少但训练成本高而且句子长的时候最后一步隐藏状态会丢信息实际效果不稳定。真正带来质变的是预训练语言模型。以BERT为代表的预训练模型用大规模语料先学会了通用的语言表示下游任务只需微调就能把语义编码能力拉上去。表示型模型在这个时期最知名的形态是Sentence-BERTSBERT也就是“以BERT为编码器的双塔”。两句话各自过一个共享权重的BERT得到两个句子向量然后算相似度。这个方案在句子级别的语义相似度任务STS、文本蕴含、FAQ匹配上效果比传统双塔好得多工程上也很成熟。选编码器时有个实用原则数据量小、算力有限、要快出结果从SBERT类方案起步数据量大、延迟预算严格、特征相对简单比如短文本、商品标题尝试蒸馏后的轻量双塔如果既要精度又要速度也可以用交互模型做teacher蒸馏双塔学生模型这块后面实操部分细讲。2.2 池化、相似度与温度决定向量质量的小细节编码器输出的是token级别的向量序列要把它们聚合成一个句子向量这个环节叫池化。常见的选项有CLS池化直接取BERT的[CLS]位置输出作为句子向量。很多预训练模型的训练目标本身就鼓励CLS汇总信息但不是所有微调任务都保证它最优有时候效果并不如pooled output。均值池化mean pooling对所有token向量取平均工程实现简单SBERT官方推荐默认使用很多评测里效果比CLS更稳。最大池化max pooling按维度取最大值适合强调“句子中最重要的某个特征”的任务但容易丢失整体语义分布信息。实际项目里用均值池化作为默认值几乎没有问题。如果发现两类样本区分不开再去尝试CLS池化对比效果。相似度计算也有讲究。最常见的是余弦相似度它只关心方向、不看模长对向量长度变化不敏感是双塔匹配的事实标准。还有些场景用内积即不做归一化的点积好处是方便跟ANN索引里的IP内积检索直接对上但内积会受模长影响如果模型没约束好向量模长的分布可能干扰语义比较。所以我的习惯是训练时就明确用余弦相似度线上检索时把向量L2归一化后再用IP检索两者数学上等价工程检索又方便。温度系数temperature是另一个经常被忽略但影响很大的细节。对比学习里常用类似“除以温度τ再算softmax”的操作。温度参数控制模型对负样本的“惩罚强度”温度越低分布越尖锐模型越要把正样本跟负样本拉得更开温度越高分布越平滑模型相对宽容。但温度设太低容易让训练不稳定设太高又学不到区分度。常见做法是从0.05这个量级开始调经验范围集中在0.02到0.1之间。2.3 损失函数与样本构建对比学习视角下的关键选择有了向量和相似度接下来要定义“怎么算错”。表示型模型最常用的三件套是对比损失、三元组损失以及InfoNCE也就是带温度系数的交叉熵。对比损失的思路是拉近同类、推远异类。简单说同一个句子改写出来的两个正样本向量距离应该近不相关的负样本对向量距离应该远。三元组损失则显式地构造anchorpositivenegative三元组要求anchor跟positive的距离加上一个margin后仍然小于anchor跟negative的距离。margin是个经验超参设小了推不远设大了训练困难。实际工程里我更推荐InfoNCE加batch内负采样的组合。在一个batch里每一句话都有来自另一侧的正样本比如一个query对应一个doc同一个batch里其它样本的doc或query天然可以作为负样本。这样不需要额外建负样本库、不用写复杂的采样逻辑训练效率很高而且batch越大负样本越丰富效果越好。不过单纯的batch内负采样有风险模型很容易学到“跟谁都不像”就对了它会把所有向量推到一个超球面上的不同角落局部聚类但没有语义结构。解决办法是引入难负样本——找那些“表面很像但语义不同”的句子作为负样本比如用户问“怎么退款”负样本可以是“怎么查询退款进度”。难负样本能逼着模型去理解真正区分含义的那部分细微差异。这是表示型模型效果提升最立竿见影的手段之一。3. 从经典DSSM到Sentence-BERT一条主线看懂演进3.1 经典双塔时期工程可用性大于模型复杂度文本匹配领域早期最有名的模型之一是DSSM出自搜索场景最初就是为了解决query和doc之间的语义匹配。DSSM的双塔结构、负采样训练、余弦相似度打分这三个设计到今天依然是表示型模型的基本盘。同一时期还有一个方向叫CDSSM卷积DSSM把词的n-gram窗口用卷积层编码试着弥补DSSM对词序不敏感的问题。还有Siamese LSTM用共享权重的LSTM编码两个句子然后比较最后一步hidden state的相似度。这些模型在评测集上各有胜负但放在今天的工业视角下它们的共性问题是编码能力的上限受限于模型结构本身没有预训练知识的加持通用性差换个领域效果就会掉得很厉害。这个阶段的工程经验也很有意思因为编码器弱传统的特征工程仍是项目重点。例如给句子拼上分词结果、词性、关键词权重再做多层非线性变换能在小数据集上挤出来几个点的提升。但这种方案的脆弱性在于换一批数据就要重新调特征特别消耗人力。所以当预训练时代来临时这些模型迅速被边缘化是技术演进中非常自然的事情。3.2 预训练时代的表示型模型准确性、效率与蒸馏BERT问世后大家一开始想的直接方案是“BERT 分类头”做匹配把两个句子拼在一起过BERT输出相似度分数。精度确实高但如前面说的两段文本必须一起过模型无法预计算检索场景下根本跑不动。Sentence-BERT这篇工作就是在这个背景下出现的用孪生BERT均值池化把每个句子编码成固定向量匹配变成向量检索。这套设计把交互型模型的精度损失控制在一定范围内同时拿到了表示型模型的工程优势普及度极高——今天很多语义向量模型、embedding模型的训练框架依然沿用这个范式。后续有个很关键的工程技巧是“从交互模型蒸馏到双塔模型”。具体做法是训练一个高精度的交互式BERT匹配模型作为teacher然后让双塔模型去模仿它的打分分布而不是只学习离散的0/1标签。这样双塔能学到teacher对“细微语义差异”的感知能力精度有明显提升。我的经验是蒸馏几乎总能带来2到5个点的离线指标提升尤其适合线上资源紧张、只能部署轻量双塔的场景。3.3 向量检索表示型模型背后的隐藏主角表示型模型的另一个关键技术栈是向量检索没有高效的向量索引双塔模型在千万级甚至亿级数据上根本没有可用性。向量检索通常是这么流水线工作的离线把库里所有文档编码成向量存入向量索引线上query编码成向量后通过近似最近邻ANN检索在毫秒级返回TopK。工业界常用的是HNSWHierarchical Navigable Small World图和量化类方法如IVF-PQ。HNSW检索准确率高、召回好但内存占用大IVF-PQ内存占用低但精度略降。如果数据量在百万级、内存不是瓶颈HNSW是省心选择上了亿级再考虑量化压缩。这里有个很容易踩的坑离线评测里向量检索召回是99%线上却掉了不少。原因往往不是模型变差了而是ANN的参数和模型向量不匹配——比如你训练时用的是余弦相似度但索引里计算的是欧氏距离又没有把向量归一化检索结果自然不对。另外索引的efSearch参数控制检索时探索的候选数如果太小召回损耗会很严重。排查这类问题的思路先用暴力精确检索对比ANN检索确认索引自身没问题再去看模型向量本身的质量。4. 实操记录从数据准备到双塔模型落地4.1 数据准备正负样本从哪来表示型模型的训练对样本质量非常敏感。很多团队不是模型调不好是样本根本不合格。第一个常见做法是复用业务日志。在搜索场景用户点击过的querydoc对可以当弱正样本展示但未点击的可以当负样本。这里要注意“点击不等于真相关”用户可能误点、可能点完就跳走。我的做法是给点击行为加一个“停留时长阈值”比如停留超过10秒才算有效正样本能大幅降低噪声。FAQ场景更直接用户问题与标准问句的历史匹配记录加上人工标注就是训练数据。第二个做法是规则构造负样本。可以把标准问题随机跟其它问题配对构造简单负样本也可以把同一标准问句做近义改写、加词、减词、替换关键词构造“难负样本”。难负样本非常关键模型见过足够多表面相似但语义不同的例子后才会去细抠语义边界。我见过一个失败的案例负样本全是随机无关句子结果模型把“便宜”和“便宜点卖吗”学成了同一个意思因为它们的向量距离一直很近——就是因为没构造过难负样本。数据规模上我的参考线是一个靠谱的匹配模型至少需要几十万到百万级别的querydoc对样本其中难负样本占比建议在20%到30%左右。数据量小时优先保质量而不是保数量。4.2 训练实现关键代码与参数细节接下来给一个可参考的双塔训练实现结构用PyTorch风格写重点看损失函数和采样逻辑。import torch import torch.nn as nn import torch.nn.functional as F class DualEncoder(nn.Module): def __init__(self, encoder): super().__init__() self.encoder encoder # BERT或轻量Transformer共享编码器 def encode(self, input_ids, attention_mask): outputs self.encoder(input_idsinput_ids, attention_maskattention_mask) # mean pooling token_embeddings outputs.last_hidden_state input_mask_expanded attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() sum_embeddings torch.sum(token_embeddings * input_mask_expanded, dim1) sum_mask torch.clamp(input_mask_expanded.sum(dim1), min1e-9) return sum_embeddings / sum_mask def forward(self, query_ids, query_mask, doc_ids, doc_mask): q_vec F.normalize(self.encode(query_ids, query_mask), p2, dim1) d_vec F.normalize(self.encode(doc_ids, doc_mask), p2, dim1) return q_vec, d_vec def info_nce_loss(q_vec, d_vec, temperature0.05): # q_vec和d_vec已经L2归一化所以内积即余弦相似度 logits torch.matmul(q_vec, d_vec.T) / temperature labels torch.arange(q_vec.size(0), deviceq_vec.device) loss F.cross_entropy(logits, labels) return loss这里的关键逻辑是In-batch负采样一个batch里每个query的正样本是对应位置的doc而其它位置的所有doc就是这个query的负样本。这个操作的妙处在于几乎零额外成本但能提供大量负例。batch size在这里不只是吞吐指标它直接决定了负样本数量所以双塔训练一般能吃多大batch就开多大。32卡甚至更多卡并行训练不稀奇常见的做法是batch size开到512甚至上千。训练细节方面有几个参数值得记录学习率用1e-5到3e-5之间BERT微调的量级优化器用AdamW先做几轮warmup再衰减。温度一开始设0.05左右如果loss下降太慢可以适当调高到0.1再慢慢降回来。如果显存不够梯度累积是个办法但要注意BN、优化器状态等细节效果不如真正的大batch。4.3 在线推理与索引构建双塔业务闭环模型训练完之后落地的路径一般是离线对所有候选文档向量化并建立索引线上服务负责把query编码成向量然后查询索引最后把TopK结果给到下游。索引构建有几个实际建议。第一向量维度不是越大越好128到768之间比较常见。维度太高索引占内存、检索慢维度太低表达力不足。第二建索引前先做向量归一化这样检索用的距离度量可以和训练保持一致。第三定期做向量索引的全量或增量重建。业务数据更新快时只增量插入不更新旧向量会出现“数据新但是向量旧”的脏数据问题影响召回质量。服务侧的架构也有讲究。query侧编码模型和doc侧编码模型结构一样、权重共享但部署时可以拆开doc向量离线算好query向量在线实时算。在线推理如果用BERT做编码器延迟可能到几十毫秒级别需要用TensorRT或ONNX加速或者换成蒸馏出来的轻量编码器。延迟预算紧的团队很多直接把双塔编码器改成小型Transformer再来一轮蒸馏精度损失可控延迟可以压到几毫秒。4.4 评测方法不要把匹配效果和召回效果混在一起评测表示型模型最容易犯的错误是用一个指标代表所有事情。匹配效果和召回效果其实是两类问题。匹配效果评测常见用语义相似度数据集计算预测相似度和人工标注如0~5分之间的Spearman相关系数或直接做分类看准确率/召回率/F1。这个指标衡量的是“模型对语义相似度的判断能力”。召回效果评测则要看系统整体给定一批query和对应的标注文档用向量检索返回TopK计算RecallK前K个检索结果里有多少比例命中了标注文档。这个指标衡量的是“向量检索链路模型向量质量”的联合效果。建议两个指标都测并且拆开分析。如果Spearman高但RecallK差大概率是向量检索环节或样本分布有问题如果RecallK高但Spearman一般可能是模型过拟合了检索集合的偏置泛化能力弱。此外一定要做人工抽检直接看检索Top10的相关性很多bad case是自动化指标发现不了的。比如模型可能把“怎么借钱”和“借钱会不会影响信用”判成高相似因为它们在向量空间里很近但业务上不是一个意图。5. 常见问题与排查技巧实录5.1 表示型效果不如交互型问题出在哪这是最常见的质疑。表示型模型确实存在信息瓶颈——两段文本在编码阶段完全隔离模型看不到另一侧的任何信息所以上限天然低于交互型。但如果差得太多通常不是架构问题而是训练问题。优先排查三件事。第一样本质量正样本是不是真的相关负样本是不是太简单难负样本比例够不够第二温度系数设太高会让模型学不到区分度。第三编码器能力如果你用的是随机初始化的双塔而不是预训练模型的微调那效果基本没法跟交互模型比——这是资源问题不是架构问题。如果这些都调过还是很差那就是精度与速度的矛盾了。解决办法是在排序阶段加一个交互模型做精排召回阶段保留双塔。这个组合是工程上性价比最高的方案。5.2 向量检索结果差先查这三个环节线上检索效果不好别一上来就骂模型。排查的顺序应该是索引参数、向量质量、数据同步。索引参数方面最常见的问题是efSearch和nprobe设得太小召回损耗大。可以先调大参数看是否能找回如果调大后恢复了说明模型没问题是索引合适度的问题。向量质量方面看query向量是否做了和训练时一致的归一化看是否出现了向量漂移模型更新了但索引没重建。数据同步方面检查索引里是不是混了过期数据、重复数据。另外一个容易被忽视的地方是OOV或特殊字符处理。query里如果有大量emoji或者特殊符号tokenizer可能会统一映射成unk导致向量质量崩坏。这种问题常常只在线上出现因为离线评测集通常比较干净。建议加一条线上query规范的预处理环节比如把表情、特殊符号标准化后再进模型。5.3 训练中的经典踩坑清单最后整理一份踩坑清单都是我实际操作中见过、踩过或帮别人擦过屁股的问题batch内负采样时label写错。Index错位会让正样本配对错模型看似收敛了实际学到的是随机映射。建议训练时定期打印几个batch的相似度矩阵人工核对。池化时没有做mask。B batch里不同句子长度不同padding位置不mask掉池化向量会混入无意义的pad embedding严重损害向量质量。学习率过大导致向量空间退化。BERT微调学习率过大会让encoder的预训练能力快速被冲掉出现过拟合训练集的词汇层面特征。用1e-5到2e-5之间更稳。忘记在损失函数里除以温度或用了错误温度。开箱即用的经验是温度在0.02到0.1之间太大太小都会导致训练信号失真。蒸馏双塔时teacher和student的输出尺度不一致。最好让teacher产出的logits也除以同样的温度再作为软标签不然两边的分布直接对不上。词表不一致。如果query侧和doc侧用了不同的tokenizer或词表常见于复用不同项目代码编码出来的向量空间根本不是同一个匹配效果必然稀碎。检查这个问题的成本极低建议上线前确认。5.3.1 效果上不去的终极排障流程如果上面的细节都check过了模型效果还是不行建议按这个流程排障先拿一小批数据保证正样本绝对正确负样本至少语义明显不同看模型能不能过拟合。单独看几个具体query的最近邻结果比任何指标都直观。如果最近邻结果里出现了明显不相关的样本多半是向量质量问题如果最近邻结果全是表面相似的文本说明模型还没学会深层的语义。把编码器换成更强的预训练模型比如从BERT切到更大型号的模型观察效果变化梯度。如果换了大模型效果也没变化那瓶颈就在数据不在模型能力。检查是否训练数据分布和线上数据分布差异过大。这种情况在业务冷启动时非常常见需要从日志里持续收集线上真实query来扩充训练集。这个流程基本能定位90%以上的“效果上不去”问题。结尾表示型模型这条技术路线从DSSM到Sentence-BERT再到今天各团队自研的语义向量模型核心逻辑没变过把文本映射到向量空间用距离代替字面匹配。它最大的价值是让“语义匹配”从理念变成了可以支撑亿级数据的工程系统虽然在一些需要精细理解的场景里仍不如交互型模型但它永远是召回层、粗排层绕不开的底座。我个人做了这么多文本匹配项目之后最大的体会是双塔模型看着简单真正决定上线效果的往往不是模型结构而是样本、温度和向量检索这些“边缘细节”。那些能让RecallK从80%涨到90%的改动几乎都发生在数据处理和训练策略上。最后再分享一个小技巧——训练完成后把你线上真实query的bad case定期捞出来人工看看向量最近邻里的“怪异邻居”这是最快发现模型和数据问题的方式比任何仪表盘都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑