我们在LlamaIndex中添加了功能,允许你在任何模型(sentence_transformers、OpenAI等)生成的嵌入之上进行线性适配器的微调。
这使你可以将嵌入表示转化为一个新的潜在空间,该空间经过了针对你特定的数据和查询进行了优化的重新调整。这可能会导致检索性能的小幅提升,从而进一步提高RAG系统的性能。
一个好处是:使用这个适配器,你不需要重新嵌入文档!只需转换查询即可。
语境
微调嵌入模型的概念非常有用。实际上,我们受到了启发,添加了一个完整的示例存储库和博文,以及LlamaIndex中的本地抽象,展示了如何对任何非结构化文本语料库进行sentence_transformers模型的微调(使用我们的SentenceTransformersFinetuneEngine)。
然而,这种方法存在一些局限性:
1. SentenceTransformersFinetuneEngine仅限于对sentence_transformers模型进行微调。
2. 在微调嵌入模型之后,你需要重新嵌入文档语料库。
在一次Finetuning + RAG网络研讨会上,Jo(Vespa)提到了完全相同的问题:对嵌入模型进行微调需要重新索引文档。然而,他与Vespa的合作研究探讨了使用基础模型“冻结”文档嵌入的概念,而是在查询嵌入上训练一个转换。
这给我们提供了灵感,尝试一种类似的嵌入微调方法,既更加通用,又允许我们冻结现有的文档嵌入。
方法
我们全新的EmbeddingAdapterFinetuneEngine在任何模型生成的查询嵌入之上微调一个线性适配器。线性适配器只是一个线性变换,专门转换查询嵌入,同时保持文档嵌入不变。
线性适配器可以在任何现有的嵌入模型上使用: SBERT嵌入、OpenAI嵌入、Cohere嵌入等。因此,你可以将其直接连接到你已经使用的任何嵌入模型之上!
由于文档嵌入没有改变,这意味着在生成文档嵌入后,你始终可以对这个线性适配器进行微调。你可以选择任意在不同数据分布上重新训练这个适配器,而无需重新嵌入所有文档。
技术细节
如上所述,线性适配器只是在查询嵌入之上执行线性变换,同时保持文档嵌入固定(使用一个权重矩阵W和一个偏置项b):
就是这样!如果将文档嵌入表示为一个(nxd)矩阵D,其中n是文档数量,d是嵌入维度,那么嵌入相似度就可以通过以下方式进行衡量:
线性适配器使用了与sentence_transformers中的MultipleNegativesRankingLoss函数类似的损失项来进行训练。给定一批正样本(问题,上下文)例子,该函数在底层使用交叉熵损失来惩罚真实的(问题,上下文)对之间的距离远以及交换对之间的距离太近。
Notebook演示
在这个Notebook演示中,我们按照我们之前的关于嵌入微调的博文采取了类似的步骤:
1. 为训练和评估生成一个合成的问题-上下文数据集。
2. 在现有模型(例如SBERT)之上微调我们的线性适配器。
3. 获取嵌入模型,并进行评估。
我们使用我们的辅助函数generate_qa_embedding_pairs来生成我们的训练和评估数据集。该函数接受任何文本节点(块)的集合,并生成一个包含(问题,上下文)对的结构化数据集。
from llama_index.finetuning import (
generate_qa_embedding_pairs,
EmbeddingQAFinetuneDataset,
)
# generate
train_dataset = generate_qa_embedding_pairs(train_nodes)
val_dataset = generate_qa_embedding_pairs(val_nodes)
# save
train_dataset.save_json("train_dataset.json")
val_dataset.save_json("val_dataset.json")
# load
train_dataset = EmbeddingQAFinetuneDataset.from_json("train_dataset.json")
val_dataset = EmbeddingQAFinetuneDataset.from_json("val_dataset.json")
然后,我们在现有的嵌入模型上微调我们的线性适配器。我们导入我们的新的EmbeddingAdapterFinetuneEngine抽象,它接受一个现有的嵌入模型和一组训练参数。
在这个例子中,我们使用了bge-small-en sentence-transformers模型,但我们也可以使用LlamaIndex/LangChain中的任何嵌入模型。
from llama_index.finetuning import EmbeddingAdapterFinetuneEngine
from llama_index.embeddings import resolve_embed_model
import torch
base_embed_model = resolve_embed_model("local:BAAI/bge-small-en")
# alternative: use OpenAI
# from llama_index.embeddings import OpenAIEmbedding
# openai = OpenAIEmbedding()
finetune_engine = EmbeddingAdapterFinetuneEngine(
train_dataset,
base_embed_model,
model_output_path="<model_output_path>",
epochs=4,
verbose=True,
# can optionally pass along any parameters that go into `train_model`
# optimizer_class=torch.optim.SGD,
# optimizer_params={"lr": 0.01}
)
然后,我们可以调用fine-tune来启动微调任务。训练线性模型非常简单,不需要大量的计算资源 - 它可以在Macbook上轻松运行。
finetune_engine.finetune()
获取嵌入模型并对其进行评估
一旦微调任务完成,我们可以获取我们的嵌入模型。
我们可以直接从finetune_engine中获取,或者以更手动的方式导入我们的新的LinearAdapterEmbeddingModel并构建它。
选项1:
embed_model = finetune_engine.get_finetuned_model()
选项2:
from llama_index.embeddings import LinearAdapterEmbeddingModel
embed_model = LinearAdapterEmbeddingModel(base_embed_model, "<model_output_path>")
下一步是对其进行评估。我们将精调模型与基础模型以及text-embedding-ada-002进行比较。
我们使用两个排名指标进行评估:
1. 命中率指标: 对于每个(查询,上下文)对,我们检索具有查询的前k个文档。如果结果包含了正确的上下文,就算是命中。
2. 平均倒数排名(Mean Reciprocal Rank):一种稍微更精细的排名指标,它查看了在检索到的前k个文档集合中的正确上下文的“倒数排名”。倒数排名定义为1/排名。当然,如果结果中不包含上下文,则倒数排名为0。
结果
就命中率而言,基础模型在验证数据集上的命中率为78.7%,而微调模型为79.8%。与此同时,text-embedding-ada-002的命中率为87.0%。
就MRR而言,基础模型为64.3%,微调模型为66%,text-embedding-ada-002为68.4%。
微调模型在性能上有一些提升,尽管可以承认这个提升很小 - 它比直接在最新数据集上微调sentence_transformers获得的性能提升要小。
结论
我们在LlamaIndex中创建了一个全新的模块,可以让你在任何嵌入模型之上进行线性适配器的微调。
它可以帮助你在检索指标上获得一些微小的改进;重要的是,它允许你保持文档嵌入固定,只对查询进行转换。