llama-index
179 строк · 6.2 Кб
1# Validates training data and estimates token usage
2# Copied from https://platform.openai.com/docs/guides/fine-tuning/preparing-your-dataset
3# Usage:
4# python validate_json.py <path_to_jsonl_file>
5
6
7# We start by importing the required packages
8
9import json
10import os
11import sys
12from collections import defaultdict
13from typing import Dict, List
14
15import numpy as np
16import tiktoken
17
18
19def validate_json(data_path: str) -> None:
20# Load dataset
21with open(data_path) as f:
22dataset = [json.loads(line) for line in f]
23
24# We can inspect the data quickly by checking the number
25# of examples and the first item
26
27# Initial dataset stats
28print("Num examples:", len(dataset))
29print("First example:")
30for message in dataset[0]["messages"]:
31print(message)
32
33# Now that we have a sense of the data, we need to go through all the different
34# examples and check to make sure the formatting is correct and matches the Chat
35# completions message structure
36
37# Format error checks
38format_errors: Dict[str, int] = defaultdict(int)
39
40for ex in dataset:
41if not isinstance(ex, dict):
42format_errors["data_type"] += 1
43continue
44
45messages = ex.get("messages", None)
46if not messages:
47format_errors["missing_messages_list"] += 1
48continue
49
50for message in messages:
51if "role" not in message or "content" not in message:
52format_errors["message_missing_key"] += 1
53
54if any(k not in ("role", "content", "name") for k in message):
55format_errors["message_unrecognized_key"] += 1
56
57if message.get("role", None) not in ("system", "user", "assistant"):
58format_errors["unrecognized_role"] += 1
59
60content = message.get("content", None)
61if not content or not isinstance(content, str):
62format_errors["missing_content"] += 1
63
64if not any(message.get("role", None) == "assistant" for message in messages):
65format_errors["example_missing_assistant_message"] += 1
66
67if format_errors:
68print("Found errors:")
69for k, v in format_errors.items():
70print(f"{k}: {v}")
71else:
72print("No errors found")
73
74# Beyond the structure of the message, we also need to ensure that the length does
75# not exceed the 4096 token limit.
76
77# Token counting functions
78encoding = tiktoken.get_encoding("cl100k_base")
79
80# not exact!
81# simplified from https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
82def num_tokens_from_messages(
83messages: List[dict], tokens_per_message: int = 3, tokens_per_name: int = 1
84) -> int:
85num_tokens = 0
86for message in messages:
87num_tokens += tokens_per_message
88for key, value in message.items():
89num_tokens += len(encoding.encode(value))
90if key == "name":
91num_tokens += tokens_per_name
92num_tokens += 3
93return num_tokens
94
95def num_assistant_tokens_from_messages(messages: List[dict]) -> int:
96num_tokens = 0
97for message in messages:
98if message["role"] == "assistant":
99num_tokens += len(encoding.encode(message["content"]))
100return num_tokens
101
102def print_distribution(values: list, name: str) -> None:
103print(f"\n#### Distribution of {name}:")
104print(f"min / max: {min(values)}, {max(values)}")
105print(f"mean / median: {np.mean(values)}, {np.median(values)}")
106print(f"p5 / p95: {np.quantile(values, 0.1)}, {np.quantile(values, 0.9)}")
107
108# Last, we can look at the results of the different formatting operations before
109# proceeding with creating a fine-tuning job:
110
111# Warnings and tokens counts
112n_missing_system = 0
113n_missing_user = 0
114n_messages = []
115convo_lens = []
116assistant_message_lens = []
117
118for ex in dataset:
119messages = ex["messages"]
120if not any(message["role"] == "system" for message in messages):
121n_missing_system += 1
122if not any(message["role"] == "user" for message in messages):
123n_missing_user += 1
124n_messages.append(len(messages))
125convo_lens.append(num_tokens_from_messages(messages))
126assistant_message_lens.append(num_assistant_tokens_from_messages(messages))
127
128print("Num examples missing system message:", n_missing_system)
129print("Num examples missing user message:", n_missing_user)
130print_distribution(n_messages, "num_messages_per_example")
131print_distribution(convo_lens, "num_total_tokens_per_example")
132print_distribution(assistant_message_lens, "num_assistant_tokens_per_example")
133n_too_long = sum(length > 4096 for length in convo_lens)
134print(
135f"\n{n_too_long} examples may be over the 4096 token limit, "
136"they will be truncated during fine-tuning"
137)
138
139# Pricing and default n_epochs estimate
140MAX_TOKENS_PER_EXAMPLE = 4096
141
142MIN_TARGET_EXAMPLES = 100
143MAX_TARGET_EXAMPLES = 25000
144TARGET_EPOCHS = 3
145MIN_EPOCHS = 1
146MAX_EPOCHS = 25
147
148n_epochs = TARGET_EPOCHS
149n_train_examples = len(dataset)
150if n_train_examples * TARGET_EPOCHS < MIN_TARGET_EXAMPLES:
151n_epochs = min(MAX_EPOCHS, MIN_TARGET_EXAMPLES // n_train_examples)
152elif n_train_examples * TARGET_EPOCHS > MAX_TARGET_EXAMPLES:
153n_epochs = max(MIN_EPOCHS, MAX_TARGET_EXAMPLES // n_train_examples)
154
155n_billing_tokens_in_dataset = sum(
156min(MAX_TOKENS_PER_EXAMPLE, length) for length in convo_lens
157)
158print(
159f"Dataset has ~{n_billing_tokens_in_dataset} tokens that will "
160"be charged for during training"
161)
162print(f"By default, you'll train for {n_epochs} epochs on this dataset")
163print(
164"By default, you'll be charged for "
165f"~{n_epochs * n_billing_tokens_in_dataset} tokens"
166)
167
168print("As of August 22, 2023, fine-tuning gpt-3.5-turbo is $0.008 / 1K Tokens.")
169print(
170"This means your total cost for training will be "
171f"${n_billing_tokens_in_dataset * 0.008 / 1000} per epoch."
172)
173
174
175if __name__ == "__main__":
176data_path = sys.argv[1]
177if not os.path.exists(data_path):
178raise ValueError(f"Path {data_path} does not exist")
179validate_json(data_path)
180