PaddleNLP 语义匹配模型库 semantic_search 模块详解:ERNIE 双塔与交叉编码器实战
【免费下载链接】PaddleNLPEasy-to-use and powerful LLM and SLM library with awesome model zoo.项目地址: https://gitcode.com/gh_mirrors/pa/PaddleNLP
semantic_search 是 PaddleNLP transformers 目录下的语义匹配(语义检索)模型模块,围绕 ERNIE 预训练模型封装了 ErnieEncoder、ErnieDualEncoder 与 ErnieCrossEncoder 三类组件,分别对应向量编码、双塔召回与交叉排序三种经典语义匹配范式。读完本文,你将掌握这三类编码器的构造参数、推理与训练调用方式、RocketQA 预训练模型的选择方法,并能将其接入端到端语义检索系统。
模块定位与文档入口
在 PaddleNLP 中,paddlenlp.transformers.semantic_search是专门承载语义匹配模型实现的子模块,其 API 文档入口为 paddlenlp.transformers.semantic_search,对应实现文件为 semantic_search/modeling.py。
模块对外导出的类通过__all__明确声明(见init.py):
__all__ = ["ErnieDualEncoder", "ErnieCrossEncoder", "ErnieEncoder"]- ErnieEncoder:单塔语义编码器,基于 ERNIE 输出句向量,是另外两个类的基础组件;
- ErnieDualEncoder:双塔(Dual-Encoder)模型,同时封装 Query 编码器与 Title/Passage 编码器,用于向量召回(Retrieval);
- ErnieCrossEncoder:交叉(Cross-Encoder)模型,将 Query 与 Passage 拼接后输入单个 ERNIE,用于精排(Ranking)。
三者共用同一套 ERNIE 骨干网络,体现了"召回用双塔、精排用交叉"的工业级语义检索标准流程。
ErnieEncoder:语义编码基座
ErnieEncoder 继承自 ErniePretrainedModel,以 ErnieConfig 为配置入口,内部封装一个完整的 ErnieModel(见 modeling.py):
class ErnieEncoder(ErniePretrainedModel): def __init__(self, config: ErnieConfig, output_emb_size: int | None = None): super(ErnieEncoder, self).__init__(config) self.ernie = ErnieModel(config) dropout = config.classifier_dropout if config.classifier_dropout is not None else 0.1 self.dropout = nn.Dropout(dropout) self.classifier = nn.Linear(config.hidden_size, config.num_labels) # Compatible to ERNIE-Search for adding extra linear layer if output_emb_size is not None and output_emb_size > 0: weight_attr = paddle.ParamAttr(initializer=paddle.nn.initializer.TruncatedNormal(std=0.02)) self.emb_reduce_linear = paddle.nn.Linear(config.hidden_size, output_emb_size, weight_attr=weight_attr)其结构要点如下:
- 骨干网络:
self.ernie = ErnieModel(config)提供完整的 ERNIE Transformer 编码能力; - Dropout 与分类头:默认 Dropout 为
config.classifier_dropout,未配置时回退到0.1;classifier是一个hidden_size -> num_labels的线性层,供交叉编码分类使用; - 输出降维层(emb_reduce_linear):当传入
output_emb_size > 0时,额外增加一个使用TruncatedNormal(std=0.02)初始化的线性层,将 hidden_size 维向量压缩到指定维度。这是为了兼容 ERNIE-Search 等场景——向量维度越小,ANN 索引的内存占用与检索开销越低; - 初始化钩子:
_init_weights将 LayerNorm 的 epsilon 统一设置为1e-12,保证数值稳定性。
forward返回(sequence_output, pool_output),其中sequence_output是全序列的 token 级向量,pool_output是句级池化输出。get_pooled_embedding正是从sequence_output[:, 0](即 [CLS] token 向量)提取句向量,这也是 BERT 系模型最常见的句向量取法。
ErnieDualEncoder:双塔向量召回模型
构造参数详解
ErnieDualEncoder 将两个 ErnieEncoder 封装进同一个 nn.Layer(见 modeling.py),使得 Query 向量与 Passage 向量可以通过一个模型同时计算、联合训练。其完整构造签名如下:
def __init__( self, query_model_name_or_path=None, title_model_name_or_path=None, share_parameters=False, output_emb_size=None, dropout=None, reinitialize=False, use_cross_batch=False, ):| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
query_model_name_or_path | str | None | Query 端 ERNIE 的预训练模型名或本地路径 |
title_model_name_or_path | str | None | Title/Passage 端 ERNIE 的预训练模型名或本地路径 |
share_parameters | bool | False | 为 True 时两端共享同一套参数(self.title_ernie = self.query_ernie),大幅减少参数量 |
output_emb_size | int | None | 输出句向量维度,>0 时启用 emb_reduce_linear 降维 |
dropout | float | None | Dropout 比率(目前透传给 ErnieEncoder 由其 config 决定) |
reinitialize | bool | False | 为 True 时执行init_epsilon_weights,将 LayerNorm epsilon 重置为1e-5,用于兼容 RocketQA v2 权重初始化 |
use_cross_batch | bool | False | 训练时是否跨 batch 聚合负样本(见下文) |
构造逻辑:
- 若
query_model_name_or_path非空,加载 Query 编码器; - 若
share_parameters为 True,Title 编码器直接复用 Query 编码器; - 否则若提供了
title_model_name_or_path,独立加载 Title 编码器; - 断言至少一个编码器存在(
At least one of query_ernie and title_ernie should not be None),防止空模型。
官方文档示例:向量提取
模型 Docstring 中给出了最小可用示例,完整继承如下(见 modeling.py):
import paddle from paddlenlp.transformers import ErnieDualEncoder, ErnieTokenizer model = ErnieDualEncoder("rocketqa-zh-dureader-query-encoder", "rocketqa-zh-dureader-para-encoder") tokenizer = ErnieTokenizer.from_pretrained("rocketqa-zh-dureader-query-encoder") inputs = tokenizer("Welcome to use PaddlePaddle and PaddleNLP!") inputs = {k: paddle.to_tensor([v]) for (k, v) in inputs.items()} # Get query embedding query_embedding = model.get_pooled_embedding(**inputs) # Get title embedding title_embedding = model.get_pooled_embedding(**inputs, is_query=False)这里分别加载了 RocketQA 的 Query 编码器与 Passage 编码器,同一个 tokenizer 编码出的文本,通过is_query开关即可从对应塔中拿到向量。get_pooled_embedding内部(见 modeling.py)会断言is_query与初始化方式一致,避免误用未加载的塔。
批量向量化:get_semantic_embedding
针对文档库批量建索引的场景,模型提供了生成器方法get_semantic_embedding(data_loader)(见 modeling.py):
def get_semantic_embedding(self, data_loader): self.eval() with paddle.no_grad(): for batch_data in data_loader: input_ids, token_type_ids = batch_data input_ids = paddle.to_tensor(input_ids) token_type_ids = paddle.to_tensor(token_type_ids) text_embeddings = self.get_pooled_embedding(input_ids, token_type_ids=token_type_ids) yield text_embeddings它以yield方式逐 batch 产出句向量,自动切到eval()并关闭梯度,适合配合 DataLoader 将海量段落写入 ANN 索引库。
相似度计算:cosine_sim
cosine_sim同时计算 Query 与 Title 的句向量,并输出两者的内积(此处输入向量均已归一化,内积等价于余弦相似度,见 modeling.py):
query_cls_embedding = self.get_pooled_embedding( query_input_ids, query_token_type_ids, query_position_ids, query_attention_mask ) title_cls_embedding = self.get_pooled_embedding( title_input_ids, title_token_type_ids, title_position_ids, title_attention_mask, is_query=False ) cosine_sim = paddle.sum(query_cls_embedding * title_cls_embedding, axis=-1)该方法可直接用于离线评测或在线打分,返回 batch 维度的相似度张量。
训练前向:三元组与交叉批负样本
forward接收一组(query, pos_title, neg_title)三元组(见 modeling.py),分别提取 Query、正例、负例向量后执行:
- 合并候选:
paddle.concat([pos_title_cls_embedding, neg_title_cls_embedding], axis=0)将正负 Passage 拼接; - 交叉批负样本:若
use_cross_batch=True,先通过paddle.distributed.all_gather聚合所有卡上的候选向量,再paddle.concat,使每张卡的 Query 都能看到全局 batch 的负样本,从而显著扩大负样本池、提升训练信号; - 打分与标签:
logits = paddle.matmul(query_cls_embedding, all_title_cls_embedding, transpose_y=True)计算 Query 与全部候选的内积得分矩阵;标签为paddle.arange(...)构造的对角索引,即"Query 只与同 batch 的正例匹配"; - 指标与损失:通过
paddle.metric.accuracy计算命中率,F.cross_entropy计算多分类交叉熵损失,返回{"loss": loss, "accuracy": accuracy}; - 预测模式:当
is_prediction=True时,只对(query, pos_title)对做paddle.dot并返回{"probs": ..., "q_rep": ..., "p_rep": ...},便于上线阶段提取向量或直接打分。
需要说明的是,labels计算中引用了self.rank,从代码结构看这是为分布式场景设计的字段,实际使用时需结合paddle.distributed的 rank 信息配合use_cross_batch使用。
ErnieCrossEncoder:交叉精排模型
双塔模型以向量相似度召回候选,但 Query 与 Passage 之间缺乏细粒度交互;交叉编码器将两者拼接后一起送入 ERNIE,通过自注意力充分建模交互语义,精度更高但计算开销更大,因此常用于精排阶段(见 modeling.py)。
构造方式只需指定一个预训练模型:
def __init__(self, pretrain_model_name_or_path, num_classes=2, reinitialize=False, dropout=None):num_classes默认 2(二分类:相关/不相关),透传给内部 ErnieEncoder 的 classifier;reinitialize=True时同样将 LayerNorm epsilon 重置为1e-5,兼容 RocketQA v2。
官方 Docstring 示例(完整继承):
import paddle from paddlenlp.transformers import ErnieCrossEncoder, ErnieTokenizer model = ErnieCrossEncoder("rocketqa-zh-dureader-cross-encoder") tokenizer = ErnieTokenizer.from_pretrained("rocketqa-zh-dureader-cross-encoder") inputs = tokenizer("你们好", text_pair="你好") inputs = {k: paddle.to_tensor([v]) for (k, v) in inputs.items()} # Get embedding of text pair. embedding = model.matching(**inputs)注意这里tokenizer通过text_pair参数把 Query 与 Passage 拼接成单条输入,这正是交叉编码的典型用法。
交叉编码器提供了三个匹配方法,对应不同的特征来源与模型代际:
| 方法 | 特征来源 | 适用场景 | 说明 |
|---|---|---|---|
matching | pool_output(句级池化) | RocketQA v1 点式预测 | 返回 softmax 后取probs[:, 1],即"相关"类的概率 |
matching_v2 | sequence_output[:, 0](CLS token) | RocketQA v2 列式预测 | 用 CLS token 特征替代句级池化 |
matching_v3 | pool_output(句级池化) | ERNIE-Search 列式预测 | 直接返回未过 softmax 的 logits |
三个方法都先取特征 → Dropout →classifier线性层。matching默认返回probs[:, 1](二分类相关概率),传入return_prob_distributation=True时返回完整概率分布。
forward默认调用matching并返回完整概率分布;若提供labels,则同时计算accuracy与F.cross_entropy损失并返回{"loss": ..., "accuracy": ...},从而支持有监督精排训练。
RocketQA 系列预训练模型与配置
semantic_search 模块的所有示例均基于 RocketQA 系列预训练模型,其完整配置注册在 ernie/configuration.py 的PRETRAINED_INIT_CONFIGURATION中。以中英文档检索常用的rocketqa-zh-dureader-query-encoder为例(见 configuration.py):
{ "attention_probs_dropout_prob": 0.1, "hidden_act": "relu", "hidden_dropout_prob": 0.1, "hidden_size": 768, "initializer_range": 0.02, "max_position_embeddings": 513, "num_attention_heads": 12, "num_hidden_layers": 12, "type_vocab_size": 2, "vocab_size": 18000, "pad_token_id": 0 }可见该系列采用 12 层、12 头、768 维的 ERNIE 结构,中文词表大小 18000,激活函数为 relu。仓库中注册的 RocketQA 模型按用途可分为:
- 双塔召回模型:
rocketqa-zh-dureader-query-encoder/rocketqa-zh-dureader-para-encoder(中文 DuReader 场景)、rocketqa-v1-marco-query-encoder/rocketqa-v1-marco-para-encoder(英文 MS MARCO 场景),以及rocketqa-zh-base/medium-*-encoder等不同规格; - 交叉精排模型:
rocketqa-zh-dureader-cross-encoder、rocketqa-v1-marco-cross-encoder,以及rocketqa-base/medium/mini/micro/nano-cross-encoder多档位版本(见 configuration.py)。
模型名作为from_pretrained的入参即可自动下载对应权重与配置,无需手工组织文件。选型时可按"小模型(nano/mini)用于原型验证、大模型(base)用于追求精度"的原则取舍。
端到端实战:双塔召回 + 交叉精排的语义检索系统
semantic_search 模块的模型可直接服务于 PaddleNLP 生态中的完整检索系统。仓库在 slm/pipelines/examples/semantic-search/README.md 中给出了"召回(DensePassageRetriever)+ 精排(ErnieRanker)+ ANN 索引 + WebUI"的端到端方案,其核心思路正是语义检索:不依赖字面关键词,而是捕捉 Query 的真实意图,在高维向量空间中度量相似度。
一键运行示例(源自 semantic_search_example.py):
# GPU 环境 export CUDA_VISIBLE_DEVICES=0 python examples/semantic-search/semantic_search_example.py --device gpu --search_engine faiss # 仅 CPU 环境(耗时较长) unset CUDA_VISIBLE_DEVICES python examples/semantic-search/semantic_search_example.py --device cpu --search_engine faiss示例脚本暴露的常用参数与默认值如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
--device | gpu | 运行设备,可选 cpu/gpu |
--index_name | dureader_index | ANN 索引名 |
--search_engine | faiss | 检索引擎,可选 faiss/milvus |
--max_seq_len_query | 64 | Query 最大长度 |
--max_seq_len_passage | 256 | Passage 最大长度 |
--retriever_batch_size | 16 | 建索引时的向量化 batch 大小 |
--query_embedding_model | rocketqa-zh-nano-query-encoder | Query 编码模型 |
--passage_embedding_model | rocketqa-zh-nano-query-encoder | Passage 编码模型(nano 版默认共享) |
--embedding_dim | 312 | ANN 索引向量维度 |
--pooling_mode | cls_token | 句向量池化方式(max/mean/mean_sqrt_len/cls) |
--model_type | ernie | 模型类型(ernie_search/ernie/bert/neural_search) |
其中embedding_dim与output_emb_size的对应关系值得注意:当model_type为ernie_search或neural_search时,示例会将output_emb_size设为embedding_dim,即启用本模块emb_reduce_linear降维层,把向量压缩到索引所需维度以降低存储与检索开销(见 semantic_search_example.py 中DensePassageRetriever的构造逻辑)。
完整的 Web 检索系统由三部分组成:基于 ElasticSearch 的 ANN 服务(默认端口 9200)、基于 RestAPI 的模型服务、基于 Streamlit 的 WebUI,详细搭建步骤可参考 README.md 第 3.4 节,多路召回(关键词 + 语义)方案见 Multi_Recall.md,自定义模型训练与接入流程见 Neural_Search.md。
小结
paddlenlp.transformers.semantic_search以 ERNIE 为骨干,提供了语义匹配领域最核心的三种建模范式:
- ErnieEncoder提供统一的句向量提取与分类能力,
output_emb_size降维机制让向量可以灵活适配 ANN 索引维度; - ErnieDualEncoder将 Query/Passage 双塔封装为一模型,支持参数共享、三元组训练、跨 batch 负样本与 cosine 打分,是向量召回的现成组件;
- ErnieCrossEncoder通过 Query-Passage 拼接交互实现精排,
matching/matching_v2/matching_v3覆盖 RocketQA v1、v2 与 ERNIE-Search 三代特征方案。
配合仓库预置的 RocketQA 系列权重与 semantic-search 示例,开发者可以快速搭建"双塔召回 + 交叉精排"的完整语义检索链路,将上述模型直接用于文本相似度计算、FAQ 匹配、文档检索等生产场景。
【免费下载链接】PaddleNLPEasy-to-use and powerful LLM and SLM library with awesome model zoo.项目地址: https://gitcode.com/gh_mirrors/pa/PaddleNLP
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考