最新n1n v2.0.1 正式上线!企业级大模型接口聚合平台 (LLM API Gateway),为您接入 500+ AI Models,价格低至 1 折,立即尝试

训练与微调多向量嵌入模型

作者
  • avatar
    姓名
    Nino
    职业
    Senior Tech Editor

信息检索和向量搜索领域正在经历一场重大变革。传统的密集检索依赖于单向量嵌入(即将整个文档压缩为一个数值向量),但这种方法经常遭遇“信息瓶颈”。在压缩过程中,重要的局部上下文、特定关键词和细粒度语义很容易丢失。为了克服这一限制,多向量嵌入模型(以 ColBERT 等架构为代表)应运而生。通过将文档表示为一系列 Token 级别的嵌入,而不是单一的向量,多向量模型保留了更细粒度的语义信息。

随着 Sentence Transformers v3 的发布,训练、微调和评估多向量嵌入模型变得比以往更加容易。构建高性能检索系统的开发人员通常会使用 n1n.ai 平台来调用最先进的大语言模型,但优化底层的检索管道才是实现生产级检索精度的关键。本文将深入探讨多向量模型的原理、它们与传统嵌入的区别,并提供使用 Sentence Transformers 训练自定义多向量模型的完整实现方案。

理解多向量嵌入模型与后期交互机制

传统的搜索架构通常分为两类:双编码器(Bi-encoders)和交叉编码器(Cross-encoders)。双编码器独立地将查询(Query)和文档(Document)编码为单个向量,并使用余弦距离或点积计算相似度。这种方式速度极快,但容易丢失 Token 级别的对齐信息。交叉编码器则将查询和文档拼接后一起输入 Transformer,允许所有 Token 之间进行完全的自注意力计算。虽然精度极高,但计算开销巨大,无法用于数百万文档的实时检索。

多向量模型通过一种被称为**后期交互(Late Interaction)**的机制填补了这一空白。

多向量模型不再将文档压缩为单个向量 d{R}Dd \in \mathbb\{R\}^D,而是输出一个矩阵 D{R}{N×D}D \in \mathbb\{R\}^\{N \times D\},其中 NN 是文档中的 Token 数量,DD 是嵌入维度。在检索时,查询也被嵌入为一个矩阵 Q{R}{M×D}Q \in \mathbb\{R\}^\{M \times D\}(其中 MM 是查询长度)。相似度分数通过 MaxSim 算子计算:

S(Q,D)={i=1}{M}max{j=1}{N}(QiDjT)S(Q, D) = \sum_\{i=1\}^\{M\} \max_\{j=1\}^\{N\} (Q_i \cdot D_j^T)

对于查询中的每个 Token,我们在文档中找到与其最相似的 Token,并将这些最大相似度得分相加。由于这种交互发生在检索管道的后期(仅在最终评分阶段),文档嵌入可以提前计算并存储在向量索引中,从而在保留双编码器速度的同时,获得了交叉编码器的 Token 级匹配精度。

为什么多向量模型对 RAG 至关重要

检索增强生成(RAG)系统的最终效果高度依赖于上下文窗口的质量。如果检索器未能获取包含答案的准确段落,那么即使是通过 n1n.ai 调用的最强大的 LLM,也会生成错误或幻觉响应。

多向量模型在以下场景中表现尤为出色:

  1. 跨领域泛化能力:传统的密集嵌入在面对特定行业术语或新领域时往往表现不佳。多向量模型由于在 Token 级别进行匹配,其泛化能力显著更强。
  2. 关键词保留:密集嵌入有时会忽略罕见但关键的词汇。MaxSim 算子确保了如果查询中包含某个特定术语,它与文档中该术语的精确对齐将极大地影响最终得分。
  3. 长文本处理:将一篇 500 字的文章压缩为单个 768 维的向量不可避免地会丢失信息。而多向量表示的维度会随着文档长度自然扩展。
特性单向量(密集检索)多向量(后期交互)交叉编码器
查询延迟极低 (< 10ms)中低 (< 30ms)高 (> 150ms)
存储开销低 (每个文档 1 个向量)高 (每个文档 N 个向量)无 (无需提前索引)
领域迁移中等极高
准确度良好优秀行业领先
索引速度较慢 (由于高维度存储)不适用 (无法建索引)

使用 Sentence Transformers 训练多向量模型的步骤

Sentence Transformers v3 原生支持多向量训练。在这一部分中,我们将演示如何设置训练管道、配置后期交互模型并运行训练循环。

1. 环境准备

首先,确保安装了最新版本的 sentence-transformerstorch

pip install -U sentence-transformers torch datasets

2. 定义模型架构

构建多向量模型时,我们以标准的 Transformer(如 bert-base-uncased)为基础,但需要修改池化层(Pooling Layer)。我们不需要将 Token 嵌入池化为单个向量,而是保留 Token 级别的嵌入,并将其投影到较低的维度(例如 128 维),以控制存储体积。

以下是使用 Sentence Transformers 定义该架构的方法:

from sentence_transformers import SentenceTransformer, models
import torch.nn as nn

# 1. 加载基础 Transformer 模型
word_embedding_model = models.Transformer("bert-base-uncased", max_seq_length=256)

# 2. 添加投影层以减少每个 Token 的嵌入大小(例如降至 128 维)
# 这对于控制生产环境中的索引大小至关重要。
class TokenProjection(nn.Module):
    def __init__(self, input_dim, output_dim):
        super().__init__()
        self.linear = nn.Linear(input_dim, output_dim, bias=False)
        
    def forward(self, features):
        # 形状: [batch_size, seq_length, input_dim]
        token_embeddings = features["token_embeddings"]
        projected = self.linear(token_embeddings)
        # 归一化嵌入以进行点积搜索
        normalized = projected / projected.norm(dim=-1, keepdim=True)
        features.update({"token_embeddings": normalized})
        return features

# 实例化投影层
projection_layer = models.Dense(
    in_features=word_embedding_model.get_word_embedding_dimension(),
    out_features=128,
    activation_function=None,
    bias=False
)

# 组合成完整的模型结构
# 注意:我们这里不添加 Pooling 层,从而保留了 Token 级别的输出。
model = SentenceTransformer(modules=[word_embedding_model, projection_layer])

3. 准备数据集

对于检索任务的训练,我们通常使用包含 query(查询)、positive(相关文档)和 negative(不相关文档)的三元组数据。

from datasets import Dataset

# 示例数据集结构
training_data = [
    {
        "query": "多向量嵌入是如何工作的?",
        "positive": "多向量嵌入将文档表示为一系列 Token 向量,并在检索时通过后期交互计算相似度。",
        "negative": "单向量嵌入将整个文本压缩为一个向量,这可能会丢失具体的 Token 细节。"
    },
    {
        "query": "后期交互有什么好处?",
        "positive": "后期交互允许预先计算文档向量,同时在查询时保持 Token 级别的对齐评分。",
        "negative": "早期交互模型将查询和文档一起处理,速度较慢,且无法进行预索引。"
    }
]

dataset = Dataset.from_list(training_data)

4. 配置损失函数

为了训练后期交互模型,我们需要一个基于 MaxSim 算子计算相似度的损失函数。我们可以使用 PyTorch 实现一个自定义的损失函数,来处理基于后期交互的 Batch 三元组边缘损失(Triplet Margin Loss):

import torch
import torch.nn as nn

class LateInteractionTripletLoss(nn.Module):
    def __init__(self, model, margin=0.2):
        super().__init__()
        self.model = model
        self.margin = margin

    def maxsim(self, query_embeddings, doc_embeddings, query_mask, doc_mask):
        # query_embeddings: [B, M, D]
        # doc_embeddings: [B, N, D]
        # 计算相似度矩阵: [B, M, N]
        sim_matrix = torch.matmul(query_embeddings, doc_embeddings.transpose(1, 2))
        
        # 掩盖 Padding 填充 Token
        mask = doc_mask.unsqueeze(1) * query_mask.unsqueeze(2)
        sim_matrix = sim_matrix.masked_fill(mask == 0, -1e9)
        
        # MaxSim 计算: 沿文档 Token 取最大值,沿查询 Token 求和
        max_sim_per_query_token, _ = sim_matrix.max(dim=2)
        # 清零被掩盖的查询 Token
        max_sim_per_query_token = max_sim_per_query_token * query_mask
        return max_sim_per_query_token.sum(dim=1)

    def forward(self, sentence_features, labels):
        # 提取查询、正例和反例的特征
        reps = [self.model(x) for x in sentence_features]
        q_rep, p_rep, n_rep = reps[0], reps[1], reps[2]
        
        # 提取 Token 嵌入和注意力掩码
        q_emb, p_emb, n_emb = q_rep["token_embeddings"], p_rep["token_embeddings"], n_rep["token_embeddings"]
        q_mask, p_mask, n_mask = q_rep["attention_mask"], p_rep["attention_mask"], n_rep["attention_mask"]
        
        # 计算得分
        pos_scores = self.maxsim(q_emb, p_emb, q_mask, p_mask)
        neg_scores = self.maxsim(q_emb, n_emb, q_mask, n_mask)
        
        # 三元组损失
        loss = torch.clamp(self.margin - pos_scores + neg_scores, min=0.0)
        return loss.mean()

5. 执行训练循环

接下来,我们使用 Sentence Transformers 的标准 API 初始化训练器,并传入我们的自定义损失函数:

from sentence_transformers import SentenceTransformerTrainer, SentenceTransformerTrainingArguments

# 配置训练参数
args = SentenceTransformerTrainingArguments(
    output_dir="multi-vector-model",
    num_train_epochs=3,
    per_device_train_batch_size=16,
    learning_rate=2e-5,
    warmup_ratio=0.1,
    fp16=True,
    logging_steps=10,
)

# 初始化自定义损失
loss_fn = LateInteractionTripletLoss(model=model)

# 创建训练器
trainer = SentenceTransformerTrainer(
    model=model,
    args=args,
    train_dataset=dataset,
    loss=loss_fn,
)

# 开始训练
trainer.train()

# 保存微调后的模型
model.save_pretrained("fine-tuned-colbert-model")

微调策略与超参数优化

与标准的密集嵌入模型相比,微调多向量模型有其独特的考量:

  1. 学习率管理:对 Transformer 骨干网络使用较小的学习率(例如 1e-53e-5),而对线性投影层可以使用稍大的学习率。这可以防止语言模型预训练语法知识的灾难性遗忘。
  2. 查询与文档 Token 长度:查询通常较短,而文档较长。将查询的 max_seq_length 设置为较小的值(例如 32 或 64),而将文档设置为较大的值(例如 256 或 512)。这可以在训练期间显着减少计算开销。
  3. 难负例挖掘(Hard Negative Mining):多向量模型对细微的语义差异高度敏感。使用简单的随机负例会导致模型效果平庸。建议使用 BM25 或现有的密集检索器来挖掘“难负例”——即表面上看起来相关但实际不包含答案的文档。

通过结合 n1n.ai 提供的统一 API 接口,开发团队可以快速搭建评估流水线,将不同配置下检索到的上下文输入到 LLM 中,从而基准测试最终 RAG 系统回答的准确率。

向量数据库集成与生产环境部署

存储多向量嵌入带来了一定的存储挑战。如果一个文档有 256 个 Token,存储它就需要 256 个大小为 128 维的向量。对于 100 万个文档,这相当于 2.56 亿个向量。

在生产环境中解决这一问题的方法包括:

  • 向量量化(Quantization):使用标量量化(SQ)或二进制量化(BQ)来压缩单个 Token 嵌入的维度。ColBERT 嵌入即使被压缩到每个维度仅 1 或 2 比特,其检索表现依然非常惊人。
  • 原生后期交互支持:诸如 Vespa、Qdrant 和 Milvus 等向量数据库已经提供了对多向量索引和 MaxSim 评分的原生支持,无需在应用层手动计算后期交互。

例如,在 Qdrant 中,您可以通过定义具有多向量配置的命名向量来配置集合以存储多向量,从而允许数据库引擎优化索引布局并加速检索速度。

总结

多向量嵌入模型为 RAG 管道的检索精度带来了质的飞跃,成功弥补了快速双编码器与高精度交叉编码器之间的鸿沟。Sentence Transformers v3 极大地简化了这些模型的训练和微调流程,使开发人员能够针对特定领域定制专属的搜索引擎。使用 n1n.ai 部署和调用上游大语言模型,并结合微调后的多向量检索器,可以构建出高度可靠、能够处理复杂查询的端到端 AI 应用栈。

Get a free API key at n1n.ai