Пошаговое руководство по тонкой настройке GPT-3
Прежде всего, это моя первая статья здесь, и мое намерение состоит в том, чтобы, в первую очередь, закрепить свои знания и, надеюсь, помочь другим, сталкивающимся с той же задачей тонкой настройки GPT-3.
GPT-3 — это мощная языковая модель, обученная на массивном корпусе текстовых данных и способная генерировать человекоподобный текст с высокой степенью точности. Однако предварительно обученная модель не всегда подходит для конкретной задачи или предметной области. Вот где тонкая настройка входит в картину.
Несмотря на то, что мы наблюдаем такой большой прогресс в области LLM, с ChatGPT, GPT-4 и многими другими моделями, GPT-3 по-прежнему актуален, особенно когда модели GPT-3.5 недоступны для тонкой настройки.
OpenAI предоставляет API и очень подробную документацию (плюс эту), которая позволяет разработчикам точно настроить GPT-3 для своего конкретного варианта использования без больших вычислительных ресурсов. В этой статье мы рассмотрим процесс тонкой настройки GPT-3 с помощью OpenAI API. Мы рассмотрим ключевые этапы тонкой настройки и предоставим фрагменты кода, которые помогут вам начать работу.
Но обо всем по порядку, ключи API
- Создайте учетную запись OpenAI: перейдите на веб-сайт OpenAI и нажмите Зарегистрироваться в правом верхнем углу. Следуйте инструкциям, чтобы создать учетную запись.
- Нажмите на значок своего профиля в правом верхнем углу страницы и выберите «Просмотреть ключи API».
- Нажмите «Создать новый секретный ключ», чтобы сгенерировать новый ключ API. Обязательно сохраните этот ключ, так как вы не сможете просмотреть его снова.
Имея ключи в руках, мы можем установить все необходимые зависимости (я добавляю дополнительные зависимости, которые я собираюсь использовать, чтобы представить некоторые знания).
pip install pandas pip install openai pip install seaborn pip install tiktoken pip install scikit-learn
Следующим шагом является подготовка обучающих данных. Это должен быть документ JSONL с prompt
и completion
. Я ежедневно работаю с pandas
, поэтому для создания данных в ожидаемом формате с помощью pandas
можно использовать или адаптировать следующий фрагмент для загрузки пользовательского набора данных:
import pandas as pd df = pd.DataFrame(zip(prompts, completions), columns=["prompt", "completion"]) df.to_json("training_data.jsonl", orient="records", lines=True)
Каждый prompt
должен заканчиваться суффиксом для \n\n###\n\n
или ->
. Кроме того, каждый completion
также имеет суффикс, например, \n
, ###
или любой другой токен, который не появляется ни в одном завершении.
OpenAI предлагает инструмент командной строки, который проверяет, дает предложения и переформатирует данные. После экспорта вашего ключа
export OPENAI_API_KEY="<OPENAI_API_KEY>"
его можно вызвать в Python с помощью:
!openai tools fine_tunes.prepare_data -f "training_data.jsonl" Analyzing... - Your file contains 37561 prompt-completion pairs - Based on your data it seems like you're trying to fine-tune a model for classification - For classification, we recommend you try one of the faster and cheaper models, such as `ada` - For classification, you can estimate the expected model performance by keeping a held out dataset, which is not used for training - There are 14 duplicated prompt-completion sets. These are rows: [11130, 11196, 11581, 11657, 11671, 11761, 11764, 11803, 11807, 11858, 11871, 11872, 11883, 11904] - Your data does not contain a common separator at the end of your prompts. Having a separator string appended to the end of the prompt makes it clearer to the fine-tuned model where the completion should begin. See https://platform.openai.com/docs/guides/fine-tuning/preparing-your-dataset for more detail and examples. If you intend to do open-ended generation, then you should leave the prompts empty - The completion should start with a whitespace character (` `). This tends to produce better results due to the tokenization we use. See https://platform.openai.com/docs/guides/fine-tuning/preparing-your-dataset for more details Based on the analysis we will perform the following actions: - [Recommended] Remove 14 duplicate rows [Y/n]: Y - [Recommended] Add a suffix separator `\n\n###\n\n` to all prompts [Y/n]: Y Your data will be written to a new JSONL file. Proceed [Y/n]: Y Wrote modified files to `training_data_prepared_train.jsonl` and `training_data_prepared_valid.jsonl` Feel free to take a look! Now use that file when fine-tuning: > openai api fine_tunes.create -t "training_data_prepared_train.jsonl" -v "training_data_prepared_valid.jsonl" --compute_classification_metrics --classification_n_classes 8 After you’ve fine-tuned a model, remember that your prompt has to end with the indicator string `\n\n###\n\n` for the model to start generating completions, rather than continuing with the prompt. Once your model starts training, it'll approximately take 15.06 hours to train a `curie` model, and less for `ada` and `babbage`. Queue will approximately take half an hour per job ahead of you.
Обратите внимание, что выходные данные также указывают, что я работаю над задачей классификации, доказывая, что команда CLI запускает поезд с правильным количеством классов. Документы OpenAI предоставляют рекомендации по тонкой настройке GPT-3 для задач классификации.
Упомянутая выше команда является единственной командой CLI, для которой я не смог найти соответствующую для вызова через Python API. Поскольку я хочу произвести обучение, я не хочу использовать интерфейс командной строки.
Кроме того, у меня была интересная ошибка во время экспериментов по проверке количества данных, необходимых для получения хорошего результата производительности:
Количество классов в файле-xxx не соответствует количеству классов, указанному в гиперпараметрах.
Это произошло потому, что я получаю другое количество классов в наборе данных для обучения и проверки (без стратификации). Похоже, это известная проблема. Но для меня эта проблема, а также невозможность использовать prepare_data
в качестве вызова функции Python заставили меня переключиться на scikit-learn
для разделения данных. Имейте в виду, однако, что при использовании этой стратегии вы должны обязательно вручную удалить все дубликаты и добавить разделитель суффиксов. Вот как я это сделал:
import pandas pd from sklearn.model_selection import train_test_split df = pd.read_json("training_data.jsonl", lines=True) df = df.drop_duplicates(subset=["prompt"]) df["prompt"] = df["prompt"].apply(lambda x: x + " ->") df["completion"] = df["completion"].apply(lambda x: x + "\n") train_df, valid_df = train_test_split(df, test_size=0.2, random_state=42) train_df.to_json("training_data_prepared_train.jsonl", orient="records", lines=True) valid_df.to_json("training_data_prepared_valid.jsonl", orient="records", lines=True)
Когда подготовленные данные обучения и проверки готовы, нам нужно загрузить их, чтобы они были доступны во время тонкой настройки.
import os import openai openai.api_key = os.getenv("OPENAI_API_KEY") def upload_file(file_name: str) -> str: upload_response = openai.File.create( file=open(file_name, "rb"), purpose="fine-tune" ) return upload_response.id train_file_id = upload_file("training_data_prepared_train.jsonl") valid_file_id = upload_file("training_data_prepared_valid.jsonl")
Затем мы можем создать задание тонкой настройки:
n_epochs = 10 n_classes = 8 model = "ada" fine_tuning_job = openai.FineTune.create( training_file=train_file_id, validation_file=valid_file_id, compute_classification_metrics=True, classification_n_classes=n_classes, n_epochs=n_epochs, model=model )
Обратите внимание, что для использования compute_classification_metrics
необходимо предоставить файл проверки, а также установить classification_n_classes
для многоклассовой классификации. Также важно отметить, что:
[...] эти оценки предполагают, что вы используете текстовые метки для классов, которые токенизируются до одного токена, [...]. Если эти условия не выполняются, полученные вами числа, скорее всего, будут неверными.
Это означает, что если compute_classification_metrics
равно True
, каждый класс должен начинаться с другого токена. Для проверки токенизации текста можно использовать OpenAI tokenizer. Программно tiktoken
можно использовать:
import tiktoken def tokenize(text: str) -> list[int]: return tiktoken.encoding_for_model(text) for k in df["completion"].unique().to_list(): print(k, enc.encode(k))
fine_tuning_job
содержит информацию о задании, такую как используемые гиперпараметры, файлы обучения и проверки, а также точно настроенное имя модели. Это запрос типа выстрелил-забыл, поэтому мы не получим уведомление после завершения тонкой настройки. Вместо этого нам нужно получить задание и проверить, не является ли точно настроенная модель null
.
import time from functools import wraps def retry_until_not_none(sleep_time: float=0) -> str: """ Decorator that retries the execution of a function until response is not None. """ def decorate(func): @wraps(func) def wrapper(*args, **kwargs): response = None while response is None: response = func(*args, **kwargs) time.sleep(sleep_time) return response return wrapper return decorate @retry_until_not_none(sleep_time=1800) def retrieve_model_name(job_id: str) -> str: return openai.FineTune.retrieve(id=job_id).fine_tuned_model fine_tuned_model = retrieve_model(fine_tuning_job.id)
Чтобы визуализировать точность проверки, необходимо получить файл результатов, содержащий метрики проверки:
from io import BytesIO import seaborn as sns fine_tuning_job = openai.FineTune.retrieve(id=fine_tuning_job.id) results = openai.File.download(fine_tuning_job.result_files[0].id) df = pd.read_csv(BytesIO(results)) accuracy = df[df["classification/accuracy"].notnull()]["classification/accuracy"].to_list() df = pd.DataFrame({"accuracy": accuracy, "epochs": range(1, n_epochs+1)}) sns.lineplot(data=df, x="epochs", y="accuracy").set(title='Validation Accuracy /Epochs');
Как только работа по тонкой настройке завершена и модель доступна, мы готовы протестировать ее в новом приглашении:
def create_completion( prompts: list[str], fine_tuned_model: str, suffix_separator: str, max_tokens: int) -> list[str]: prompts = [prompt + suffix_separator for prompt in prompts] answer = openai.Completion.create( model=fine_tuned_model, prompt=prompts, max_tokens=max_tokens, temperature=0 ) completions = [answer["choices"][i]["text"].strip() for i in range(len(answer["choices"]))] return completions create_completion(["my prompt"], fine_tuned_model, " ->", 1)
Выводы
- Используйте
scikit-learn
train_test_split
, чтобы избежать проблем с тонкой настройкой, связанных с разделением данных. - Убедитесь, что каждый класс начинается с другого токена.
- Для задач классификации в момент вывода установите
max_tokens=1
.