数据准备与分析,用于聊天模型微调
本笔记本作为工具,用于预处理和分析用于微调聊天模型的聊天数据集。 它会检查格式错误,提供基本统计信息,并估算微调成本的 token 数量。 这里展示的方法对应于 gpt-3.5-turbo 的 当前微调方法。 对于 babbage-002 和 davinci-002 等模型,请参阅 旧版微调。
import json
import tiktoken # 用于 token 计数
import numpy as np
from collections import defaultdict
数据加载
我们首先从 示例 JSONL 文件 加载聊天数据集。
data_path = "data/toy_chat_fine_tuning.jsonl"
# 加载数据集
with open(data_path, 'r', encoding='utf-8') as f:
dataset = [json.loads(line) for line in f]
# 初始数据集统计
print("示例数量:", len(dataset))
print("第一个示例:")
for message in dataset[0]["messages"]:
print(message)
示例数量: 5
第一个示例:
{'role': 'system', 'content': 'You are a happy assistant that puts a positive spin on everything.'}
{'role': 'user', 'content': 'I fell off my bike today.'}
{'role': 'assistant', 'content': "It's great that you're getting exercise outdoors!"}
格式验证
我们可以执行各种错误检查,以验证数据集中每个对话是否符合微调 API 预期的格式。错误根据其性质进行分类,以便于调试。
- 数据类型检查: 检查数据集中的每个条目是否为字典 (
dict
)。错误类型:data_type
。 - 消息列表存在性: 检查每个条目中是否存在
messages
列表。错误类型:missing_messages_list
。 - 消息键检查: 验证
messages
列表中的每条消息是否包含role
和content
键。错误类型:message_missing_key
。 - 消息中未识别的键: 如果消息具有
role
、content
、weight
、function_call
和name
以外的键,则记录日志。错误类型:message_unrecognized_key
。 - 角色验证: 确保
role
是 "system"、"user" 或 "assistant" 之一。错误类型:unrecognized_role
。 - 内容验证: 验证
content
是否为文本数据且为字符串。错误类型:missing_content
。 - 助手消息存在性: 检查每个对话是否至少有一条来自助手的消息。错误类型:
example_missing_assistant_message
。
下面的代码执行这些检查,并打印找到的每种错误类型的计数。这对于调试和确保数据集已准备好进行后续步骤非常有用。
# 格式错误检查
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", "weight") 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("发现错误:")
for k, v in format_errors.items():
print(f"{k}: {v}")
else:
print("未发现错误")
未发现错误
Token 计数工具
让我们定义一些有用的工具供笔记本的其余部分使用。
encoding = tiktoken.get_encoding("cl100k_base")
# 不精确!
# 简化自 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#### {name} 的分布:")
print(f"最小值 / 最大值: {min(values)}, {max(values)}")
print(f"平均值 / 中位数: {np.mean(values)}, {np.median(values)}")
print(f"5% / 95%: {np.quantile(values, 0.1)}, {np.quantile(values, 0.9)}")
数据警告和 Token 计数
通过一些轻量级分析,我们可以识别数据集中潜在的问题,例如缺少消息,并提供有关消息和 token 计数的统计见解。
- 缺少系统/用户消息: 计算缺少“system”或“user”消息的对话数量。这些消息对于定义助手的行为和发起对话至关重要。
- 每个示例的消息数量: 汇总每个对话中消息数量的分布,以了解对话的复杂性。
- 每个示例的总 Token 数: 计算并汇总每个对话中总 Token 数量的分布。这对于了解微调成本很重要。
- 助手消息中的 Token 数: 计算每个对话中助手消息中的 Token 数量,并汇总此分布。这对于了解助手的冗长程度很有用。
- Token 限制警告: 检查是否有任何示例超过最大 Token 限制(16,385 个 token),因为此类示例将在微调期间被截断,可能导致数据丢失。
# 警告和 token 计数
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("缺少系统消息的示例数量:", n_missing_system)
print("缺少用户消息的示例数量:", n_missing_user)
print_distribution(n_messages, "每个示例的消息数量")
print_distribution(convo_lens, "每个示例的总 Token 数")
print_distribution(assistant_message_lens, "助手消息中的 Token 数")
n_too_long = sum(l > 16385 for l in convo_lens)
print(f"\n{n_too_long} 个示例可能超过了 16,385 个 token 的限制,它们将在微调期间被截断")
缺少系统消息的示例数量: 1
缺少用户消息的示例数量: 1
#### 每个示例的消息数量 的分布:
最小值 / 最大值: 2, 9
平均值 / 中位数: 3.8, 3.0
5% / 95%: 2.0, 6.6000000000000005
#### 每个示例的总 Token 数 的分布:
最小值 / 最大值: 26, 8032
平均值 / 中位数: 1648.4, 45.0
5% / 95%: 26.8, 4863.6
#### 助手消息中的 Token 数 的分布:
最小值 / 最大值: 4, 8000
平均值 / 中位数: 1610.2, 10.0
5% / 95%: 6.0, 4811.200000000001
0 个示例可能超过了 16,385 个 token 的限制,它们将在微调期间被截断
成本估算
在最后一部分,我们估算用于微调的总 token 数量,这可以帮助我们估算成本。值得注意的是,微调作业的持续时间也会随着 token 数量的增加而增加。
# 定价和默认 n_epochs 估算
MAX_TOKENS_PER_EXAMPLE = 16385
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"数据集包含约 {n_billing_tokens_in_dataset} 个将在训练期间计费的 token")
print(f"默认情况下,您将在此数据集上训练 {n_epochs} 个 epoch")
print(f"默认情况下,您将为约 {n_epochs * n_billing_tokens_in_dataset} 个 token 付费")
数据集包含约 4306 个将在训练期间计费的 token
默认情况下,您将在此数据集上训练 20 个 epoch
默认情况下,您将为约 86120 个 token 付费
请参阅 https://openai.com/pricing 来估算总成本。