节前,我们星球组织了一场算法岗技术&面试讨论会,邀请了一些互联网大厂朋友、参加社招和校招面试的同学.
针对算法岗技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备、面试常考点分享等热门话题进行了深入的讨论。
汇总合集:《大模型面试宝典》(2024版) 发布!
本文将会介绍如何使用 Sentence Transformers 对开源的Embedding模型bge-base-zh-v1.5
进行微调,并验证 Embedding 模型微调后的效果。
在RAG框架或者语义相似度计算任务时,Embedding模型是我们常常会打交道的模型。
Sentence Transformers
是一个 Python 库,用于使用和训练各种应用的Embedding模型,例如检索增强生成 (RAG)、语义搜索、语义文本相似度、释义挖掘 (paraphrase mining) 等等。其 3.0 版本的更新是该工程自创建以来最大的一次,引入了一种新的训练方法。
本文将会以智源研究院(BAAI)开源的Embedding模型bge-base-zh-v1.5
作为基准模型,展示如何使用Sentence Transformers
进行评估,并对其进行微调,验证微调后的模型效果会有所提升。
评估指标Baseline
使用LlamaIndex框架对RAG流程中的各种Retrieve算法,包括Embedding模型召回,进行了评估,评估指标采用Hit Rate
和MRR
。本文将继续使用这篇文章中给出的数据集进行评估。
示例评估代码如下:
# -*- coding: utf-8 -*-
# @file: bge_base_zh_eval.py
import os
import json
import time
import torch
from pprint import pprint
from sentence_transformers import SentenceTransformer
from sentence_transformers.evaluation import InformationRetrievalEvaluator
from sentence_transformers.util import cos_simproject_dir = os.path.dirname(os.path.abspath(__file__)).split('/src')[0]# data process
# load dataset, get corpus, queries, relevant_docs
with open(os.path.join(project_dir, "data/doc_qa.json"), "r", encoding="utf-8") as f:content = json.loads(f.read())corpus = content['corpus']
queries = content['queries']
relevant_docs = content['relevant_docs']# # Load a model
# 替换成自己的模型完整路径或使用huggingface modl id
model_name = "bge-base-zh-v1.5"
model_path = os.path.join(project_dir, f"models/{model_name}")
model = SentenceTransformer(model_path, device="cuda" if torch.cuda.is_available() else "cpu")
print("Model loaded")s_time = time.time()# # Evaluate the model
evaluator = InformationRetrievalEvaluator(queries=queries,corpus=corpus,relevant_docs=relevant_docs,name=f"{os.path.basename(model_path)}",score_functions={"cosine": cos_sim}
)# Evaluate the model
result = evaluator(model)
pprint(result)
print(f"Time cost: {time.time() - s_time:.2f}s")
我们在评估器中传入queries, corpus, relevant_docs字典,加载完模型后即可进行评估。
评估结果在下文中给出,作为baseline(基准)指标。
技术交流群
前沿技术资讯、算法交流、求职内推、算法竞赛、面试交流(校招、社招、实习)等、与 10000+来自港科大、北大、清华、中科院、CMU、腾讯、百度等名校名企开发者互动交流~
我们建了大模型算法岗技术与面试交流群, 想要交流、需要源码&资料、提升技术的同学,可以直接加微信号:mlc2060。加的时候备注一下:研究方向 +学校/公司+CSDN,即可。然后就可以拉你进群了。
方式①、微信搜索公众号:机器学习社区,后台回复:加群
方式②、添加微信号:mlc2060,备注:CSDN + 技术交流
微调数据合成
在LlamaIndex
框架中,可方便地使用generate_qa_embedding_pairs方法,利用Prompt工程对文本生成相关问题并进行关联。
Embedding模型的微调数据合成脚本如下:
# -*- coding: utf-8 -*-
# @file: make_ft_corpus.py
import os
from llama_index.legacy.finetuning import (generate_qa_embedding_pairs
)
from llama_index.llms.openai import OpenAI
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
from dotenv import load_dotenvload_dotenv()project_dir = os.path.dirname(os.path.abspath(__file__)).split('/src')[0]TRAIN_FILES = [os.path.join(project_dir, "data/ft_train.txt")]
VAL_FILES = [os.path.join(project_dir, "data/ft_test.txt")]TRAIN_CORPUS_FPATH = os.path.join(project_dir, "data/ft_train_corpus.json")
VAL_CORPUS_FPATH = os.path.join(project_dir, "data/ft_val_corpus.json")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(chunk_size=250, chunk_overlap=0)nodes = parser.get_nodes_from_documents(docs, show_progress=verbose)if verbose:print(f"Parsed {len(nodes)} nodes")return nodestrain_nodes = load_corpus(TRAIN_FILES, verbose=True)
val_nodes = load_corpus(VAL_FILES, verbose=True)llm = OpenAI(model="gpt-3.5-turbo", api_key=os.getenv("OPENAI_API_KEY"))qa_generate_prompt_tmpl = """\
Context information is below.---------------------
{context_str}
---------------------Given the context information and not prior knowledge.
generate only questions based on the below query.You are a Professor. Your task is to setup \
{num_questions_per_chunk} questions for an upcoming \
quiz/examination in Chinese. The questions should be diverse in nature \
across the document in Chinese. The questions should not contain options, not start with Q1/ Q2. \
Restrict the questions to the context information provided.
"""train_dataset = generate_qa_embedding_pairs(nodes=train_nodes, llm=llm, num_questions_per_chunk=1, qa_generate_prompt_tmpl=qa_generate_prompt_tmpl)
val_dataset = generate_qa_embedding_pairs(nodes=val_nodes, llm=llm, num_questions_per_chunk=1, qa_generate_prompt_tmpl=qa_generate_prompt_tmpl)train_dataset.save_json(TRAIN_CORPUS_FPATH)
val_dataset.save_json(VAL_CORPUS_FPATH)
输出结果如下:
Output:Loading files ['/Users/admin/PycharmProjects/embedding_model_exp/data/ft_train.txt']
Loaded 1 docs
Parsing nodes: 100%|██████████| 1/1 [00:00<00:00, 23.54it/s]
Parsing nodes: 0%| | 0/1 [00:00<?, ?it/s]Parsed 137 nodes
Loading files ['/Users/admin/PycharmProjects/embedding_model_exp/data/ft_test.txt']
Loaded 1 docs
Parsing nodes: 100%|██████████| 1/1 [00:00<00:00, 45.84it/s]0%| | 0/137 [00:00<?, ?it/s]Parsed 111 nodes
100%|██████████| 137/137 [03:34<00:00, 1.57s/it]
100%|██████████| 111/111 [01:55<00:00, 1.04s/it]
这样,我们就能得到微调数据集了,保存为ft_train_corpus.json和ft_val_corpus.json。
Embedding模型微调
接下来,我们将会对bge-base-zh-v1.5
模型进行微调,微调的目的是让模型更适配我们自己的数据集,从而取得更好的召回效果。
使用 `sentence-transformers v3`
这里,我们使用的sentence-transformers模块的版本为V3.0.0。
利用该模块,我们不难实现Embedding模型微调,微调代码如下:
# -*- coding: utf-8 -*-
# @file: ft_sentence_transformers_trainer.py
import os
import json
import time
import torch
from datasets import Dataset
from sentence_transformers import SentenceTransformer
from sentence_transformers.evaluation import InformationRetrievalEvaluator
from sentence_transformers.util import cos_sim
from sentence_transformers.losses import MultipleNegativesRankingLoss
from sentence_transformers import SentenceTransformerTrainingArguments
from sentence_transformers.training_args import BatchSamplers
from sentence_transformers import SentenceTransformerTrainerstart_time = time.time()
project_dir = os.path.dirname(os.path.abspath(__file__)).split('/src')[0]# load eval dataset
with open(os.path.join(project_dir, "data/ft_val_dataset.json"), "r", encoding="utf-8") as f:eval_content = json.loads(f.read())corpus, queries, relevant_docs = eval_content['corpus'], eval_content['queries'], eval_content['relevant_docs']
# load train dataset
with open(os.path.join(project_dir, "data/ft_train_dataset.json"), "r", encoding="utf-8") as f:train_content = json.loads(f.read())train_anchor, train_positive = [], []
for query_id, context_id in train_content['relevant_docs'].items():train_anchor.append(train_content['queries'][query_id])train_positive.append(train_content['corpus'][context_id[0]])train_dataset = Dataset.from_dict({"positive": train_positive, "anchor": train_anchor})print(train_dataset)
print(train_dataset[0:5])# Load a model
model_name = 'bge-base-zh-v1.5'
# 替换成自己的模型完整路径或使用huggingface modl id
model_path = os.path.join(project_dir, f"models/{model_name}")
model = SentenceTransformer(model_path, device="cuda:0" if torch.cuda.is_available() else "cpu")
print("Model loaded")# # Evaluate the model
evaluator = InformationRetrievalEvaluator(queries=queries,corpus=corpus,relevant_docs=relevant_docs,name=f"{model_name}",score_functions={"cosine": cos_sim}
)
train_loss = MultipleNegativesRankingLoss(model)# define training arguments
args = SentenceTransformerTrainingArguments(output_dir=f"ft_{model_name}", # output directory and hugging face model IDnum_train_epochs=5, # number of epochsper_device_train_batch_size=2, # train batch sizegradient_accumulation_steps=2, # for a global batch size of 512per_device_eval_batch_size=4, # evaluation batch sizewarmup_ratio=0.1, # warmup ratiolearning_rate=2e-5, # learning rate, 2e-5 is a good valuelr_scheduler_type="cosine", # use constant learning rate scheduleroptim="adamw_torch_fused", # use fused adamw optimizertf32=True, # use tf32 precisionbf16=True, # use bf16 precisionbatch_sampler=BatchSamplers.NO_DUPLICATES,eval_strategy="epoch", # evaluate after each epochsave_strategy="epoch", # save after each epochlogging_steps=10, # log every 10 stepssave_total_limit=3, # save only the last 3 modelsload_best_model_at_end=True, # load the best model when training endsmetric_for_best_model=f"eval_{model_name}_cosine_ndcg@10", # Optimizing for the best ndcg@10 score
)# train the model
trainer = SentenceTransformerTrainer(model=model, # the model to trainargs=args, # training argumentstrain_dataset=train_dataset.select_columns(["positive", "anchor"]), # training datasetloss=train_loss,evaluator=evaluator
)trainer.train()
trainer.save_model()
print(f"cost time: {time.time() - start_time:.2f}s")
笔者在1张NVIDIA A800-SXM4-80GB型号的GPU上进行训练,耗时约63.10秒。同时,我们会将微调后的Embedding模型保存在GPU上。
总结
本文重点介绍了如何使用 Sentence Transformers 对开源的Embedding模型bge-base-zh-v1.5
进行微调,并验证Embedding模型微调后的效果。
Sentence Transformers 是一个宝库,它介绍了关于Embedding模型方方面面的内容,是了解、深入Embedding模型必不可少的工具。后续笔者将会介绍Embedding模型量化、俄罗斯套娃嵌入模型(Matryoshka Representation Learning, MRL)等相关方面的内容。
参考文献
-
Training and Finetuning Embedding Models with Sentence Transformers v3: https://huggingface.co/blog/train-sentence-transformers
-
Fine-tune Embedding models for Retrieval Augmented Generation (RAG): https://www.philschmid.de/fine-tune-embedding-model-for-rag
-
俄罗斯套娃 (Matryoshka) 嵌入模型概述: https://huggingface.co/blog/zh/matryoshka
-
Finetune Embeddings: https://docs.llamaindex.ai/en/stable/examples/finetuning/embeddings/finetune_embedding/