OpenAI模型微调快速入门

2024-03-13 12:29:29 浏览数 (1)

官方文档:https://platform.openai.com/docs/guides/fine-tuning

介绍

微调目前可用于以下型号:

  • gpt-3.5-turbo-1106 (推荐)
  • gpt-3.5-turbo-0613
  • babbage-002
  • davinci-002
  • gpt-4-0613 (实验中 — 符合条件的用户将看到一个选项,用于在 fine-tuning UI 中请求访问权限)

还可以微调微调的模型,这在获取其他数据并且不想重复前面的训练步骤时非常有用。

微调前的建议

微调 OpenAI 文本生成模型可以使它们更适合特定应用程序,但这需要仔细投入时间和精力。我们建议首先尝试使用提示工程、提示链接(将复杂任务分解为多个提示)和函数调用来获得良好的结果,主要原因是:

  • 在许多任务中,我们的模型最初可能表现不佳,但可以通过正确的提示来改进结果 - 因此可能不需要微调
  • 迭代提示和其他策略的反馈循环比微调迭代要快得多,微调需要创建数据集并运行训练作业
  • 在仍然需要微调的情况下,初始提示工程工作不会浪费 - 在微调数据中使用良好的提示(或将提示链接/工具使用与微调相结合)时,我们通常会看到最佳结果

官方的Prompt指南提供了一些最有效的战略和战术的背景,这些策略和战术可以在不进行微调的情况下获得更好的性能。

微调的优点

微调可以改善结果的一些好处:

  • 设置风格、基调、格式或其他定性方面
  • 提高产生所需输出的可靠性
  • 更正失败以遵循复杂的提示
  • 以特定方式处理许多边缘情况
  • 执行难以在提示中表达的新技能或任务

准备数据集

数据示例格式

举例:目标是创建一个偶尔会给出讽刺性响应的聊天机器人,以下是为数据集创建的三个训练示例,微调的对象是对话的模型。

代码语言:javascript复制
{"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-002davinci-002 ,可以按照用于旧版微调的提示完成对格式进行操作,如下所示。

代码语言:javascript复制
{"prompt": "<prompt text>", "completion": "<ideal generated text>"}
{"prompt": "<prompt text>", "completion": "<ideal generated text>"}
{"prompt": "<prompt text>", "completion": "<ideal generated text>"}

准备Prompt

通常建议在微调之前采用认为最适合模型的一组指令和提示,并将它们包含在每个训练示例中。这应该可以让你达到最好和最一般的结果,特别是如果你的训练样本相对较少(例如不到一百个)。

如果想缩短每个示例中重复的指令或提示以节省成本,请记住,模型的行为会遵循这些指令,并且可能很难让模型在推理时忽略这些“内置”指令。

可能需要更多的训练示例才能获得良好的结果,因为模型必须完全通过示例来学习,而不需要指导说明。

要微调模型,您需要提供至少 10 个示例。我们通常会看到对 50 到 100 个训练样本进行微调的明显改进,但对 gpt-3.5-turbo 来说,正确的数字因确切的用例而异。

建议从 50 个精心制作的示例开始,看看模型在微调后是否有改进的迹象。在某些情况下,这可能就足够了,但即使模型还不是生产质量,明显的改进也是一个好兆头,表明提供更多数据将继续改进模型。没有改进表明,在扩展到有限的示例集之外之前,您可能需要重新考虑如何为模型设置任务或重构数据。

收集初始数据集后,建议将其拆分为训练和测试部分。当提交包含训练和测试文件的微调作业时,我们将在训练过程中提供两者的统计信息。这些统计数据将是模型改进程度的初始信号。此外,尽早构建测试集将有助于通过在测试集上生成样本来确保能够在训练后评估模型。

每个训练示例限制为 4096 个令牌。训练时,超过此限制的示例将被截断为前 4096 个标记。若要确保整个训练示例符合上下文,请考虑检查消息内容中的令牌总数是否低于 4000。可以使用 OpenAI 中的令牌计数来计算令牌数。

预估成本

每 1K 个令牌的基本成本 * 输入文件中的令牌数 * 训练的 epoch 数。对于在 3 个 epoch 内训练了 100,000 个令牌的训练文件,预期成本为 ~$2.40 USD。

在创建微调作业之前,检查数据格式非常重要。为此,官方创建了一个简单的 Python 脚本,可用于查找潜在错误、查看令牌计数和估算微调作业的成本。

具体参考:https://cookbook.openai.com/examples/chat_finetuning_data_prep

第一步:加载数据
代码语言:javascript复制
data_path = "data/toy_chat_fine_tuning.jsonl"

# Load the dataset
with open(data_path, 'r', encoding='utf-8') as f:
    dataset = [json.loads(line) for line in f]

# Initial dataset stats
print("Num examples:", len(dataset))
print("First example:")
for message in dataset[0]["messages"]:
    print(message)
第二步:校验数据格式
代码语言:javascript复制
# 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", "function_call") for k in message):
            format_errors["message_unrecognized_key"]  = 1
        
        if message.get("role", None) not in ("system", "user", "assistant", "function"):
            format_errors["unrecognized_role"]  = 1
            
        content = message.get("content", None)
        function_call = message.get("function_call", None)
        
        if (not content and not function_call) 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")

输出示例:

代码语言:javascript复制
No errors found
第三步:计算Token数
代码语言:javascript复制
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)}")
第四步:数据集分析
代码语言:javascript复制
# 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")

输出示例:

代码语言:javascript复制
Num examples missing system message: 1
Num examples missing user message: 1

#### Distribution of num_messages_per_example:
min / max: 2, 9
mean / median: 3.8, 3.0
p5 / p95: 2.0, 6.6000000000000005

#### Distribution of num_total_tokens_per_example:
min / max: 26, 8032
mean / median: 1648.4, 45.0
p5 / p95: 26.8, 4863.6

#### Distribution of num_assistant_tokens_per_example:
min / max: 4, 8000
mean / median: 1610.2, 10.0
p5 / p95: 6.0, 4811.200000000001

1 examples may be over the 4096 token limit, they will be truncated during fine-tuning
第五步:估算费用
代码语言:javascript复制
# Pricing and default n_epochs estimate
MAX_TOKENS_PER_EXAMPLE = 4096

TARGET_EPOCHS = 3
MIN_TARGET_EXAMPLES = 100
MAX_TARGET_EXAMPLES = 25000
MIN_DEFAULT_EPOCHS = 1
MAX_DEFAULT_EPOCHS = 25

n_epochs = TARGET_EPOCHS
n_train_examples = len(dataset)
if n_train_examples * TARGET_EPOCHS < MIN_TARGET_EXAMPLES:
    n_epochs = min(MAX_DEFAULT_EPOCHS, MIN_TARGET_EXAMPLES // n_train_examples)
elif n_train_examples * TARGET_EPOCHS > MAX_TARGET_EXAMPLES:
    n_epochs = max(MIN_DEFAULT_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")

输出示例:

代码语言:javascript复制
Dataset has ~4306 tokens that will be charged for during training
By default, you'll train for 20 epochs on this dataset
By default, you'll be charged for ~86120 tokens

验证数据后,需要使用文件 API 上传文件,以便与微调作业一起使用:

代码语言:javascript复制
from openai import OpenAI
client = OpenAI()

client.files.create(
  file=open("mydata.jsonl", "rb"),
  purpose="fine-tune"
)

创建微调模型

支持通过微调 UI 或以编程方式创建微调作业。

使用 OpenAI SDK 启动微调作业

代码语言:javascript复制
from openai import OpenAI
client = OpenAI()

client.fine_tuning.jobs.create(
  training_file="file-abc123", 
  model="gpt-3.5-turbo"
)

model 是要微调的模型的名称( gpt-3.5-turbobabbage-002 davinci-002 或现有的微调模型),并且是 training_file 将训练文件上传到 OpenAI API 时返回的文件 ID。您可以使用 suffix 参数自定义微调模型的名称。

要设置其他微调参数,如 validation_file or hyperparameters ,请参考 API 规范进行微调。

开始微调作业后,可能需要一些时间才能完成。您的作业可能排在我们系统中的其他作业之后,训练模型可能需要几分钟或几小时,具体取决于模型和数据集大小。模型训练完成后,创建微调作业的用户将收到一封电子邮件确认。

除了创建微调作业外,您还可以列出现有作业、检索作业状态或取消作业。

代码语言:javascript复制
from openai import OpenAI
client = OpenAI()

# List 10 fine-tuning jobs
client.fine_tuning.jobs.list(limit=10)

# Retrieve the state of a fine-tune
client.fine_tuning.jobs.retrieve("ftjob-abc123")

# Cancel a job
client.fine_tuning.jobs.cancel("ftjob-abc123")

# List up to 10 events from a fine-tuning job
client.fine_tuning.jobs.list_events(fine_tuning_job_id="ftjob-abc123", limit=10)

# Delete a fine-tuned model (must be an owner of the org the model was created in)
client.models.delete("ft:gpt-3.5-turbo:acemeco:suffix:abc123")

使用微调模型

作业成功后,在检索作业详细信息时,您将看到填充了模型名称的 fine_tuned_model 字段。现在,您可以在聊天完成 (for gpt-3.5-turbo ) 或旧版 Completions API (for babbage-002davinci-002 ) 中将此模型指定为参数,并使用 Playground 向其发出请求。

作业完成后,模型应立即可供推理使用。在某些情况下,模型可能需要几分钟时间才能准备好处理请求。如果对模型的请求超时或找不到模型名称,则可能是因为模型仍在加载中。如果发生这种情况,请过几分钟再试。

代码语言:javascript复制
from openai import OpenAI
client = OpenAI()

response = client.chat.completions.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)

分析微调模型

提供以下在训练过程中计算的训练指标:训练损失、训练令牌准确性、测试损失和测试令牌准确性。这些统计数据旨在提供健全性检查,以确保训练顺利进行(损失应该减少,令牌准确性应该增加)。在运行活动微调作业时,您可以查看包含一些有用指标的事件对象:

代码语言:javascript复制
{
    "object": "fine_tuning.job.event",
    "id": "ftevent-abc-123",
    "created_at": 1693582679,
    "level": "info",
    "message": "Step 100/100: training loss=0.00",
    "data": {
        "step": 100,
        "train_loss": 1.805623287509661e-5,
        "train_mean_token_accuracy": 1.0
    },
    "type": "metrics"
}

微调作业完成后,您还可以通过查询微调作业、从 result_files 中提取文件 ID,然后检索该文件内容来查看有关训练过程如何进行的指标。每个结果 CSV 文件都包含以下列: steptrain_loss train_accuracy valid_lossvalid_mean_token_accuracy

代码语言:javascript复制
step,train_loss,train_accuracy,valid_loss,valid_mean_token_accuracy
1,1.52347,0.0,,
2,0.57719,0.0,,
3,3.63525,0.0,,
4,1.72257,0.0,,
5,1.52379,0.0,,

虽然指标可能会有所帮助,但评估微调模型中的样本可以提供最相关的模型质量感。我们建议在测试集上从基础模型和微调模型生成样本,并并排比较样本。理想情况下,测试集应包括在生产用例中可能发送到模型的输入的完整分布。如果手动评估太耗时,请考虑使用我们的 Evals 库来自动执行后面的评估。

迭代数据质量

如果微调作业的结果不如预期,请考虑以下方法来调整训练数据集:

  • 收集示例以解决剩余问题:如果模型在某些方面仍然不擅长,请添加训练示例,直接向模型展示如何正确地执行这些方面。
  • 仔细检查现有示例中的问题:如果模型存在语法、逻辑或样式问题,请检查数据是否存在任何相同的问题。例如,如果模型现在说“我会为你安排这次会议”(当它不应该这样做时),看看现有的例子是否教会模型说它可以做它不能做的新事情。
  • 考虑数据的平衡性和多样性:如果数据中 60% 的助理回答说“我无法回答这个问题”,但在推理时只有 5% 的回答应该这么说,那么你可能会得到过多的拒绝。
  • 确保您的训练示例包含响应所需的所有信息:如果我们希望模型根据用户的个人特征来赞美用户,并且训练示例包括对前面对话中未发现的特征的助手赞美,则模型可能会学习幻觉信息。
  • 查看训练示例中的一致性:如果多人创建了训练数据,则模型性能可能会受到人与人之间的一致性水平的限制。例如,在文本提取任务中,如果人们只同意 70% 的提取片段,那么模型可能无法做得更好。
  • 确保所有训练示例的格式都与推理所预期相同。

迭代数据量

对示例的质量和分布感到满意后,可以考虑增加训练示例的数量。这往往有助于模型更好地学习任务,尤其是在可能的“边缘情况”周围。我们预计每次将训练样本数量增加一倍时,都会有类似的改进。您可以通过以下方法粗略地估计增加训练数据大小的预期质量增益:

  • 对当前数据集进行微调
  • 对当前数据集的一半进行微调
  • 观察两者之间的质量差距

迭代超参数

我们允许您指定以下超参数:

  • epochs
  • learning rate multiplier
  • batch size

设置超参数,如下所示:

代码语言:javascript复制
from openai import OpenAI
client = OpenAI()

client.fine_tuning.jobs.create(
  training_file="file-abc123", 
  model="gpt-3.5-turbo", 
  hyperparameters={
    "n_epochs":2
  }
)

我们建议在初始训练时不指定任何这些内容,允许我们根据数据集大小为你选择默认值,然后在观察到以下情况时进行调整:

  • 如果模型未像预期的那样遵循训练数据,则将epochs 数增加 1 或 2 个
    • 对于只有一个理想完成(或一小组相似的理想完成)的任务,这种情况更为常见。一些示例包括分类、实体提取或结构化分析。这些任务通常是您可以根据参考答案计算最终准确性指标的任务。
  • 如果模型的多样性低于预期,则将epochs 数减少 1 或 2 个,这种现象常见于对于有各种可能的良好完成的任务的情况。
  • 如果模型似乎没有收敛,则增加learning rate multiplier

微调示例

指定风格和语气

构建一个微调模型,该模型使模型遵循特定的风格和语气指导,而不仅仅是提示。

代码语言:javascript复制
{"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."}]}

示例代码:

代码语言:javascript复制
from openai import OpenAI
client = OpenAI()

file = client.files.create(
  file=open("marv.jsonl", "rb"),
  purpose="fine-tune"
)

client.fine_tuning.jobs.create(
  training_file=file.id,
  model="gpt-3.5-turbo"
)

结构化返回

另一种非常适合微调的用例是让模型提供结构化信息

代码语言:javascript复制
{"messages": [{"role": "system", "content": "Given a sports headline, provide the following fields in a JSON dict, where applicable: "player" (full name)", "team", "sport", and "gender".},{"role": "user", "content": "Sources: Colts grant RB Taylor OK to seek trade"},
{"role": "assistant", "content": "{"player": "Jonathan Taylor", "team": "Colts", "sport": "football", "gender": "male" }"},]}
{"messages": [{"role": "system", "content": "Given a sports headline, provide the following fields in a JSON dict, where applicable: "player" (full name)", "team", "sport", and "gender".},{"role": "user", "content": "OSU 'split down middle' on starting QB battle"},
{"role": "assistant", "content": "{"player": null, "team": "OSU", "sport": "football", "gender": null }"},]}

根据格式化的训练数据,响应应如下所示:

代码语言:javascript复制
{"player": "Sha'Carri Richardson", "team": null", "sport": "track and field", "gender": "female"}

函数调用

聊天完成 API 支持函数调用。在完成 API 中包含一长串函数可能会消耗大量提示令牌,有时模型会产生幻觉或不提供有效的 JSON 输出。

function_callfunctions 已被弃用,取而代之的是 ,但是 tools ,微调 API 此时仍然需要旧格式。

使用函数调用示例微调模型可以:

  • 即使不存在完整的函数定义,也能获得类似格式的响应
  • 获得更准确、更一致的输出

如图所示设置示例格式,每行包含一个“消息”列表和一个可选的“函数”列表:

代码语言:javascript复制
{
    "messages": [
        {"role": "user", "content": "What is the weather in San Francisco?"},
        {"role": "assistant", "function_call": {"name": "get_current_weather", "arguments": "{"location": "San Francisco, USA", "format": "celcius"}"}
    ],
    "functions": [{
        "name": "get_current_weather",
        "description": "Get the current weather",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string", "description": "The city and country, eg. San Francisco, USA"},
                "format": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["location", "format"]
        }
    }]
}

如果您想跟随并自己创建一个微调模型,您将需要至少 10 个示例。

如果您的目标是使用更少的令牌,一些有用的技术是:

  • 省略函数和参数描述:从函数和参数中删除描述字段
  • 省略参数:从参数对象中删除整个属性字段
  • 完全省略函数:从 functions 数组中删除整个函数对象

如果您的目标是最大限度地提高函数调用输出的正确性,我们建议使用相同的函数定义来训练和查询微调模型。

函数调用的微调还可用于自定义模型对函数输出的响应。为此,您可以包含函数响应消息和解释该响应的助手消息:

代码语言:javascript复制
{
    "messages": [
        {"role": "user", "content": "What is the weather in San Francisco?"},
        {"role": "assistant", "function_call": {"name": "get_current_weather", "arguments": "{"location": "San Francisco, USA", "format": "celcius"}"}}
        {"role": "function", "name": "get_current_weather", "content": "21.0"},
        {"role": "assistant", "content": "It is 21 degrees celsius in San Francisco, CA"}
    ],
    "functions": [...] // same as before
}

微调价格

微调后的模型价格如下,其中gpt-3.5-turbo模型价格提升了3倍。

<!--br {mso-data-placement:same-cell;}--> td {white-space:nowrap;border:1px solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}

Model

Training

Input usage

Output usage

Price Rate

gpt-3.5-turbo

$0.0080 / 1K tokens

$0.0030 / 1K tokens

$0.0060 / 1K tokens

3

davinci-002

$0.0060 / 1K tokens

$0.0120 / 1K tokens

$0.0120 / 1K tokens

6

babbage-002

$0.0004 / 1K tokens

$0.0016 / 1K tokens

$0.0016 / 1K tokens

4

常见错误

SDK创建微调任务400错误

报错信息如下:

代码语言:javascript复制
openai.BadRequestError: Error code: 400 - {'error': {'message': 'gpt-3.5-turbo can only be fine-tuned on the new fine-tuning API (`/fine_tuning/jobs`). This API (`/fine-tunes`) is being deprecated. Please refer to our documentation for more information: https://platform.openai.com/docs/api-reference/fine-tuning', 'type': 'invalid_request_error', 'param': None, 'code': None}}

需要使用普通的http请求,指定新的请求url,如下:

代码语言:javascript复制
headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer "   api_key
}

data = {
    "training_file": file_id,
    "model": "gpt-3.5-turbo"
}
response = requests.post("https://api.openai.com/v1/fine_tuning/jobs", json=data, headers=headers)

0 人点赞