案例目标
本案例展示了如何在任何黑盒嵌入模型(如sentence_transformers、OpenAI等)之上微调适配器。通过将嵌入表示转换为针对特定数据和查询优化的新潜在空间,可以提高检索性能,进而提升RAG系统的整体表现。
该案例实现了三种类型的适配器微调:
- 线性适配器(Linear Adapter)
- 两层神经网络适配器(2-Layer NN Adapter)
- 自定义神经网络适配器(Custom NN Adapter)
技术栈与核心依赖
- LlamaIndex: 提供EmbeddingAdapterFinetuneEngine等核心微调功能
- llama-index-embeddings-openai: 提供OpenAI嵌入模型支持
- llama-index-embeddings-adapter: 提供适配器嵌入模型支持
- llama-index-finetuning: 提供微调引擎和工具
- PyTorch: 用于构建自定义神经网络适配器
- BAAI/bge-small-en: 作为基础嵌入模型进行微调
- OpenAI Embedding (text-embedding-ada-002): 作为对比基准
环境配置
安装必要的依赖包:
%pip install llama-index-embeddings-openai %pip install llama-index-embeddings-adapter %pip install llama-index-finetuning案例实现
数据准备与加载
使用Lyft 2021年10K报告作为训练数据,Uber 2021年10K报告作为验证数据:
TRAIN_FILES = ["./data/10k/lyft_2021.pdf"] VAL_FILES = ["./data/10k/uber_2021.pdf"]定义加载和解析文档的函数:
def load_corpus(files, verbose=False): if verbose: print(f"Loading files {files}") reader = SimpleDirectoryReader(input_files=files) docs = reader.load_data() if verbose: print(f"Loaded {len(docs)} docs") parser = SentenceSplitter() nodes = parser.get_nodes_from_documents(docs, show_progress=verbose) if verbose: print(f"Parsed {len(nodes)} nodes") return nodes生成合成查询
使用LLM(gpt-3.5-turbo)为每个文本块生成问题,构建(问题,上下文)对作为微调数据:
from llama_index.finetuning import generate_qa_embedding_pairs from llama_index.core.evaluation import EmbeddingQAFinetuneDataset train_dataset = generate_qa_embedding_pairs(train_nodes) val_dataset = generate_qa_embedding_pairs(val_nodes)线性适配器微调
使用EmbeddingAdapterFinetuneEngine对BAAI/bge-small-en模型进行线性适配器微调:
from llama_index.finetuning import EmbeddingAdapterFinetuneEngine from llama_index.core.embeddings import resolve_embed_model import torch base_embed_model = resolve_embed_model("local:BAAI/bge-small-en") finetune_engine = EmbeddingAdapterFinetuneEngine( train_dataset, base_embed_model, model_output_path="model_output_test", epochs=4, verbose=True, ) finetune_engine.finetune() embed_model = finetune_engine.get_finetuned_model()两层神经网络适配器微调
定义并使用两层神经网络适配器进行微调:
from llama_index.core.embeddings.adapter_utils import TwoLayerNN base_embed_model = resolve_embed_model("local:BAAI/bge-small-en") adapter_model = TwoLayerNN( 384, # input dimension 1024, # hidden dimension 384, # output dimension bias=True, ) finetune_engine = EmbeddingAdapterFinetuneEngine( train_dataset, base_embed_model, model_output_path="model5_output_test", model_checkpoint_path="model5_ck", adapter_model=adapter_model, epochs=25, verbose=True, ) finetune_engine.finetune() embed_model_2layer = finetune_engine.get_finetuned_model(adapter_cls=TwoLayerNN)自定义神经网络适配器微调
通过继承BaseAdapter类创建自定义神经网络适配器:
from llama_index.core.embeddings.adapter_utils import BaseAdapter import torch.nn.functional as F from torch import nn, Tensor from typing import Dict class CustomNN(BaseAdapter): """Custom NN transformation. Is a copy of our TwoLayerNN, showing it here for notebook purposes. Args: in_features (int): Input features hidden_features (int): Hidden features out_features (int): Output features bias (bool): Whether to use bias add_residual (bool): Whether to add residual connection """ def __init__( self, in_features: int, hidden_features: int, out_features: int, bias: bool = True, add_residual: bool = True, ): super().__init__() self.in_features = in_features self.hidden_features = hidden_features self.out_features = out_features self.bias = bias self._add_residual = add_residual self.linear1 = nn.Linear(in_features, hidden_features, bias=bias) self.linear2 = nn.Linear(hidden_features, out_features, bias=bias) self.residual_weight = nn.Parameter(torch.ones(1)) def forward(self, embed: Tensor) -> Tensor: output1 = self.linear1(embed) output1 = F.relu(output1) output2 = self.linear2(output1) if self._add_residual: output2 = self.residual_weight * output2 + embed return output2 def get_config_dict(self) -> Dict: """Get config dict.""" return { "in_features": self.in_features, "hidden_features": self.hidden_features, "out_features": self.out_features, "bias": self.bias, "add_residual": self._add_residual, }模型评估
使用命中率(Hit-rate)和平均倒数排名(MRR)指标评估模型性能:
from llama_index.embeddings.openai import OpenAIEmbedding from llama_index.core import VectorStoreIndex from llama_index.core.schema import TextNode from tqdm.notebook import tqdm import pandas as pd from eval_utils import evaluate, display_results # 评估OpenAI ada模型 ada = OpenAIEmbedding() ada_val_results = evaluate(val_dataset, ada) # 评估基础BGE模型 bge = "local:BAAI/bge-small-en" bge_val_results = evaluate(val_dataset, bge) # 评估微调后的模型 ft_val_results = evaluate(val_dataset, embed_model)案例效果
通过对比实验,评估了不同模型的检索性能:
| 模型 | 命中率(Hit-rate) | 平均倒数排名(MRR) |
|---|---|---|
| OpenAI ada | 0.870886 | 0.72884 |
| BGE基础模型 | 0.787342 | 0.643038 |
| 线性适配器微调模型 | 0.798734 | 0.662152 |
| 两层神经网络适配器微调模型 | 0.798734 | 0.662848 |
| 自定义神经网络适配器微调模型 | 0.789873 | 0.645127 |
关键发现:
- OpenAI的ada模型表现最佳,但需要API调用且成本较高
- 微调后的适配器模型相比基础BGE模型有显著提升
- 线性适配器和两层神经网络适配器性能相近
- 适配器微调提供了一种在保持基础模型不变的情况下提升性能的方法
案例实现思路
本案例的核心实现思路是:
- 数据准备:使用金融文档(Lyft和Uber的10K报告)构建训练和验证语料库
- 合成查询生成:利用LLM为每个文本块生成相关问题,构建(问题,上下文)对
- 适配器微调:在基础嵌入模型之上添加可训练的适配器层,只训练适配器参数而不修改基础模型
- 多类型适配器:实现线性、两层神经网络和自定义神经网络三种适配器结构
- 性能评估:使用命中率和MRR指标评估微调效果,并与OpenAI和基础模型对比
适配器微调的优势在于:
- 不需要修改基础嵌入模型,适用于任何黑盒模型
- 训练参数量少,计算资源需求低
- 可以针对特定领域数据进行定制化优化
- 支持多种适配器结构,可根据需求选择
扩展建议
- 更多适配器结构:尝试更复杂的神经网络结构,如注意力机制、Transformer等
- 领域特定优化:针对特定领域(如医疗、法律等)设计专门的适配器结构
- 多模态适配器:扩展到多模态嵌入模型,支持文本、图像等多种数据类型
- 动态适配器:根据查询内容动态选择或组合不同的适配器
- 适配器压缩:研究如何压缩适配器模型,减少存储和计算开销
- 适配器共享:探索在多个任务或领域间共享适配器参数的方法
- 增量微调:支持在新数据上增量更新适配器,而不需要重新训练
总结
本案例详细介绍了如何在黑盒嵌入模型上微调适配器,通过将嵌入表示转换为针对特定数据和查询优化的新潜在空间,提高了检索性能。案例实现了线性、两层神经网络和自定义神经网络三种适配器结构,并通过实验验证了微调后模型相比基础模型的性能提升。
适配器微调方法具有不修改基础模型、训练参数少、计算资源需求低等优势,为优化现有嵌入模型提供了一种灵活高效的途径。这种方法特别适用于需要在保持基础模型不变的情况下,针对特定领域或任务进行定制化优化的场景。