Готовим данные для дообучения чат-модели

Урок 3 из 299 курса «OpenAI Cookbook»: официальный курс OpenAI Academy (ОпенАИ) на русском языке. Этот урок бесплатный.

Подготовка и анализ данных для fine-tuning'а чат-модели

Этот notebook служит инструментом для предварительной обработки и анализа набора данных чата, используемого для fine-tuning'а чат-модели. Он проверяет наличие ошибок формата, предоставляет базовую статистику и оценивает количество token'ов для расчета стоимости fine-tuning'а. Представленный здесь метод соответствует текущему методу fine-tuning'а для gpt-3.5-turbo. См. устаревший метод fine-tuning'а для таких моделей, как 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("Num examples:", len(dataset))
print("First example:")
for message in dataset[0]["messages"]:
    print(message)
Num examples: 5
First example:
{'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 fine-tuning'а. Ошибки классифицируются по их характеру для упрощения отладки.

  1. Проверка типа данных: Проверяет, является ли каждая запись в наборе данных словарем (dict). Тип ошибки: data_type.
  2. Наличие списка сообщений: Проверяет, присутствует ли список messages в каждой записи. Тип ошибки: missing_messages_list.
  3. Проверка ключей сообщения: Проверяет, что каждое сообщение в списке messages содержит ключи role и content. Тип ошибки: message_missing_key.
  4. Нераспознанные ключи в сообщениях: Регистрирует, если сообщение содержит ключи, отличные от role, content, weight, function_call и name. Тип ошибки: message_unrecognized_key.
  5. Проверка роли: Гарантирует, что role является одной из: "system", "user" или "assistant". Тип ошибки: unrecognized_role.
  6. Проверка содержимого: Проверяет, что content содержит текстовые данные и является строкой. Тип ошибки: missing_content.
  7. Наличие сообщения от assistant'а: Проверяет, что каждый диалог содержит хотя бы одно сообщение от assistant'а. Тип ошибки: 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("Ошибок не найдено")
No errors found

Вспомогательные утилиты для подсчета token'ов

Давайте определим несколько полезных утилит, которые будут использоваться в оставшейся части notebook'а.

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 / 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)}")

Предупреждения о данных и подсчет token'ов

С помощью небольшого анализа мы можем выявить потенциальные проблемы в наборе данных, такие как отсутствующие сообщения, и предоставить статистические данные о количестве сообщений и token'ов.

  1. Отсутствующие сообщения system/user: Подсчитывает количество диалогов, в которых отсутствует сообщение "system" или "user". Такие сообщения критически важны для определения поведения assistant'а и начала диалога.
  2. Количество сообщений на пример: Обобщает распределение количества сообщений в каждом диалоге, предоставляя представление о сложности диалога.
  3. Общее количество token'ов на пример: Вычисляет и обобщает распределение общего количества token'ов в каждом диалоге. Важно для понимания стоимости fine-tuning'а.
  4. Token'ы в сообщениях assistant'а: Вычисляет количество token'ов в сообщениях assistant'а для каждого диалога и обобщает это распределение. Полезно для понимания многословности assistant'а.
  5. Предупреждения о лимите token'ов: Проверяет, превышают ли какие-либо примеры максимальный лимит token'ов (16 385 token'ов), так как такие примеры будут усечены во время fine-tuning'а, что потенциально может привести к потере данных.
# Предупреждения и подсчет 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, "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 > 16385 for l in convo_lens)
print(f"\n{n_too_long} примеров могут превышать лимит в 16 385 token'ов; они будут усечены во время fine-tuning'а")
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

0 examples may be over the 16,385 token limit, they will be truncated during fine-tuning

Оценка стоимости

В этом заключительном разделе мы оцениваем общее количество token'ов, которые будут использованы для fine-tuning'а, что позволяет нам приблизительно рассчитать стоимость. Стоит отметить, что продолжительность задач fine-tuning'а также будет увеличиваться с ростом количества token'ов.

# Оценка стоимости и количества эпох по умолчанию
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} эпох на этом наборе данных")
print(f"По умолчанию вам будет выставлен счет за ~{n_epochs * n_billing_tokens_in_dataset} token'ов")
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

См. https://openai.com/pricing для оценки общей стоимости.

Полезные гиды