前言
刚刚openai在官网宣布chatgpt支持微调了!具体支持微调的模型有:
并且GPT-3.5-Turbo-16k和GPT4在今年晚些也会支持微调。
其在官网也介绍了一些微调和准备数据的实战经验,可以学习~
官方文档:
https://platform.openai.com/docs/guides/fine-tuning/common-use-cases
需要微调吗
虽然微调可以更好的cover某一领域,但是毕竟训练需要耗费时间和精力,所以其建议先试试prompt engineering,有可能已经满足需要了,可以参考:
https://platform.openai.com/docs/guides/gpt-best-practices
微调可以处理哪些问题
通过微调可以处理以下场景:
(1)设定风格、语气、格式或其他质量方面的要求。
(2)提高产生所需输出的可靠性。
(3)纠正无法遵循复杂提示的错误。
(4)以特定方式处理许多边缘情况。
(5)执行难以用prompt准确表达的新技能或任务。
微调的另外一个好处就是可以解决时延或者减少花费,比如微调后就可以使用更短的prompt(训练的时候就用更少的prompt,inference自然就可以不用再写一堆prompt engineering的东西)来完成任务,这样就减少了token数;同时由于GPT4比较贵但是其效果又比较好,那么就可以用GPT4的结果做训练数据来微调chatgpt进而达到蒸馏GPT4。
准备微调训练数据
微调gpt-3.5-turbo需要准备的数据格式如下:
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]}
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]}
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]}
babbage-002和davinci-002需要准备的数据格式如下:
{"prompt": "<prompt text>", "completion": "<ideal generated text>"}
{"prompt": "<prompt text>", "completion": "<ideal generated text>"}
{"prompt": "<prompt text>", "completion": "<ideal generated text>"}
- 构建指令
构建指令最关键的就是要确保训练数据是最好的,最满足自己当前需求的;要把这部分高质量的数据尽可能都加到训练集中,尤其是自己手头准备的训练样本并不是很多的时候,这样可以尽最大可能让训练出的模型满足自己的需求。
另外就是前面说的如果你想通过缩短prompt来减少inference成本,那么模型就需要更多的训练样本去学习到被“省略”部分的指令。
为了便于理解,笔者这里举个例子:假设当前在做一个根据小说内容生成一句评论的场景,目前的要求有(1)评论必须有搞笑风格(2)评论必须带有表情符号(3)评论字数必须在10-20个字(4)评论必须要口语化一些。那么我们调用GPT4的时候prompt就可以这么写:
假设你是一个小说评论生成器,生成的评论必须满足以下要求:
(1)评论必须有搞笑风格
(2)评论必须带有表情符号
(3)评论字数必须在10-20个字
(4)评论必须要口语化一些
小说内容如下:
****
通过使用这个指令调用GPT4我们可以很好的得到满足自己需求的response(假设调用chatgpt不能满足需求),这样我们就有了训练样本啦,然后用这里的样本去微调chatgpt就可以蒸馏了GPT4啦。
同时看到当前的场景每个样本开头都有这一段相似的指令:
假设你是一个小说评论生成器,生成的评论必须满足以下要求:
(1)评论必须有搞笑风格
(2)评论必须带有表情符号
(3)评论字数必须在10-20个字
(4)评论必须要口语化一些
如果我们在微调的时候把这段去掉同样可以达到需求的话,那么在inference的时候就可以大大减少token数了(尤其是当指令非常多的时候),极端下我们在微调的时候训练样本的prompt就只是小说内容,response还是GPT4的回复。但是这种情况就需要准备更多的样本,让模型去学习到那些被忽略的隐式“指令”。
- 需要多少条训练样本
最少需要10条训练样本。openai观察到通常使用50-100条样本来微调后,chatgpt就可以发生明显的变化。
官方建议精心准备50条样本来微调,然后观察模型是否显示出改善的迹象。一般来说可能已经能用了,但即使模型还不满足需求,如果看到已经明显改变的迹象了,那剩下的就是沿着当前准备数据的思路提供更多数据即可。如果没有看到改变的迹象,这可能意味着需要重新考虑如何为模型设置任务或重新构建指令数据。
- 划分训练&测试集
官方要求划分训练和测试集,这样在微调过程中其会帮统计一些指标变化,方便辅助看训练的效果
- token 限制
每条训练token总数最大为4096,也就是说需要确保构建的样本最好在4000以内,如果超出会被自动截断,可以使用以下脚本来计算样本的token数:
https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
- 微调成本
- 检查格式
在准备完数据后,最后可以再double check一下数据格式等等准备的是否正确,以免真真启动训练的时候发生错误,为此官方也给了一个check脚本:
# We start by importing the required packages
import json
import os
import tiktoken
import numpy as np
from collections import defaultdict
# Next, we specify the data path and open the JSONL file
data_path = "<YOUR_JSON_FILE_HERE>"
# Load dataset
with open(data_path) as f:
dataset = [json.loads(line) for line in f]
# We can inspect the data quickly by checking the number of examples and the first item
# Initial dataset stats
print("Num examples:", len(dataset))
print("First example:")
for message in dataset[0]["messages"]:
print(message)
# Now that we have a sense of the data, we need to go through all the different examples and check to make sure the formatting is correct and matches the Chat completions message structure
# Format error checks
format_errors = defaultdict(int)
for ex in dataset:
if not isinstance(ex, dict):
format_errors["data_type"] += 1
continue
messages = ex.get("messages", None)
if not messages:
format_errors["missing_messages_list"] += 1
continue
for message in messages:
if "role" not in message or "content" not in message:
format_errors["message_missing_key"] += 1
if any(k not in ("role", "content", "name") for k in message):
format_errors["message_unrecognized_key"] += 1
if message.get("role", None) not in ("system", "user", "assistant"):
format_errors["unrecognized_role"] += 1
content = message.get("content", None)
if not content or not isinstance(content, str):
format_errors["missing_content"] += 1
if not any(message.get("role", None) == "assistant" for message in messages):
format_errors["example_missing_assistant_message"] += 1
if format_errors:
print("Found errors:")
for k, v in format_errors.items():
print(f"{k}: {v}")
else:
print("No errors found")
# Beyond the structure of the message, we also need to ensure that the length does not exceed the 4096 token limit.
# Token counting functions
encoding = tiktoken.get_encoding("cl100k_base")
# not exact!
# simplified from https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
def num_tokens_from_messages(messages, tokens_per_message=3, tokens_per_name=1):
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
num_tokens += len(encoding.encode(value))
if key == "name":
num_tokens += tokens_per_name
num_tokens += 3
return num_tokens
def num_assistant_tokens_from_messages(messages):
num_tokens = 0
for message in messages:
if message["role"] == "assistant":
num_tokens += len(encoding.encode(message["content"]))
return num_tokens
def print_distribution(values, name):
print(f"\n#### Distribution of {name}:")
print(f"min / max: {min(values)}, {max(values)}")
print(f"mean / median: {np.mean(values)}, {np.median(values)}")
print(f"p5 / p95: {np.quantile(values, 0.1)}, {np.quantile(values, 0.9)}")
# Last, we can look at the results of the different formatting operations before proceeding with creating a fine-tuning job:
# Warnings and tokens counts
n_missing_system = 0
n_missing_user = 0
n_messages = []
convo_lens = []
assistant_message_lens = []
for ex in dataset:
messages = ex["messages"]
if not any(message["role"] == "system" for message in messages):
n_missing_system += 1
if not any(message["role"] == "user" for message in messages):
n_missing_user += 1
n_messages.append(len(messages))
convo_lens.append(num_tokens_from_messages(messages))
assistant_message_lens.append(num_assistant_tokens_from_messages(messages))
print("Num examples missing system message:", n_missing_system)
print("Num examples missing user message:", n_missing_user)
print_distribution(n_messages, "num_messages_per_example")
print_distribution(convo_lens, "num_total_tokens_per_example")
print_distribution(assistant_message_lens, "num_assistant_tokens_per_example")
n_too_long = sum(l > 4096 for l in convo_lens)
print(f"\n{n_too_long} examples may be over the 4096 token limit, they will be truncated during fine-tuning")
# Pricing and default n_epochs estimate
MAX_TOKENS_PER_EXAMPLE = 4096
MIN_TARGET_EXAMPLES = 100
MAX_TARGET_EXAMPLES = 25000
TARGET_EPOCHS = 3
MIN_EPOCHS = 1
MAX_EPOCHS = 25
n_epochs = TARGET_EPOCHS
n_train_examples = len(dataset)
if n_train_examples * TARGET_EPOCHS < MIN_TARGET_EXAMPLES:
n_epochs = min(MAX_EPOCHS, MIN_TARGET_EXAMPLES // n_train_examples)
elif n_train_examples * TARGET_EPOCHS > MAX_TARGET_EXAMPLES:
n_epochs = max(MIN_EPOCHS, MAX_TARGET_EXAMPLES // n_train_examples)
n_billing_tokens_in_dataset = sum(min(MAX_TOKENS_PER_EXAMPLE, length) for length in convo_lens)
print(f"Dataset has ~{n_billing_tokens_in_dataset} tokens that will be charged for during training")
print(f"By default, you'll train for {n_epochs} epochs on this dataset")
print(f"By default, you'll be charged for ~{n_epochs * n_billing_tokens_in_dataset} tokens")
print("See pricing page to estimate total costs")
在检查没问题后就可以上传数据用于后续训练了
openai.File.create(
file=open("mydata.jsonl", "rb"),
purpose='fine-tune'
)
创建训练模型
创建的模型:
import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
openai.FineTuningJob.create(training_file="file-abc123", model="gpt-3.5-turbo")
在开启微调后,任务就排队开始训练了,通常需要等几分钟或者几个小时训练,等完成训练后,用户就会收到一份确认邮件了。
当然也可以查看当前任务列表,取消某个任务等等
# List 10 fine-tuning jobs
openai.FineTuningJob.list(limit=10)
# Retrieve the state of a fine-tune
openai.FineTuningJob.retrieve("ft-abc123")
# Cancel a job
openai.FineTuningJob.cancel("ft-abc123")
# List up to 10 events from a fine-tuning job
openai.FineTuningJob.list_events(id="ft-abc123", limit=10)
# Delete a fine-tuned model (must be an owner of the org the model was created in)
import openai
openai.Model.delete("ft-abc123")
使用微调好的模型
一旦训练结束,就可以通过查看任务细节看到模型名字“fine_tuned_model”,然后就可以调用使用了,如果请求错误,可以再等一会,因为其可能在正在加载。
import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
completion = openai.ChatCompletion.create(
model="ft:gpt-3.5-turbo:my-org:custom_suffix:id",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"}
]
)
print(completion.choices[0].message)
- 评估模型
官方会提供一些常规的量化指标比如training loss, training token accuracy, test loss, and test token accuracy。
另外直接看生成的case可能更直观,可以对比chatgpt和微调后的chatgpt的效果,如果想自动化评估也可以使用openai官方提供的evals框架(GPT4自动打分)
https://github.com/openai/evals
- 迭代训练数据质量
如果模型结果不尽人意,那么首先可以看当前训练数据的质量是否过关:
(1) 收集当前的badcase,然后针对性的加对应的数据来纠正模型
(2) 如果模型在语法、风格等方面不满足需要,那需要查看当前的训练样本中是否就已经包含了对应的脏数据,需要剔除或者修正。
(3) 平衡数据:假设训练数据中有60%是"I cannot answer this",但是inference的时候只有5%是这种情况,那么需要平衡好这个比例
(4) 确保模型包含所有信息,比如有个需求是希望模型根据用户的个人特征来赞美他们,而训练数据中包含了与之前对话中未提及的特征相关的赞美,那么模型可能会学会产生虚构的信息即幻觉。
(5) 确保高的一致性:如果当前需求的训练集是多个人协同创建的,那么模型的性能可能会受到多人之间的一致性和一致性水平的限制。例如,在一个文本提取任务中,如果多人只在提取的片段上达成了70%的一致,那么模型可能天花板就是70%。
(6) 训练集确保和官方要求的格式一样。
- 迭代训练数据数量
当确保了当前训练集质量后,就可以考虑增加训练集的数量来更进一步提高性能。增加量级通常有助于模型更好地学习任务,特别是处理一些边缘case。预计每当增加一倍量级时,都会有类似客观的改进程度。可以通过以下方式粗略估计通过增加训练数据大小所带来的预期效果:
(1)在当前数据集上进行微调
(2)在当前数据集的一半上进行微调
(3)观察两者之间的质量差距
一般来说,如果必须进行数据量权衡,较少数量的高质量数据通常比大量低质量数据更有效。
- 迭代训练超参
在第一次试水训练的时候,官方不建议指定epoch,而是由官方根据样本量大小设置一个默认值,然后训练完看效果后再决定怎么调整epoch:
(1)当需求是偏单一场景的时候(比如分类、实体抽取等等),可以适当尝试增加1-2个epoch
(2)当需求是偏多任务场景的时候或者说发现模型缺乏多样性的时候可以尝试减少1-2个epoch
微调示例
这里笔者就不再重述了,感兴趣的可以看原博客,官方给了两个具体的微调示例。
总结
openai这一波实属厉害,它的基座模型天花板本来就很高,而且我们可以看到官方介绍的微调样本数量其实需要的很少就可以得到不错的效果,在这种情况下,其他模型至少目前应该还是很难竞争的。
不过还有一个老问题没解决就是数据的安全性,不论是调用api还是微调,用户数据都是要最终流向openai的,这对安全性要求高的用户来说还是不可行,这个时候训练自己的本地私有化模型还是有必要的。
另外我们也可以借鉴学习一下其提高的迭代数据质量&数据数量等实战经验用以自己的训练开发中。
关注
欢迎关注,下期再见啦~
知乎,csdn,github,微信公众号