Пошаговое руководство по тонкой настройке GPT-3

Прежде всего, это моя первая статья здесь, и мое намерение состоит в том, чтобы, в первую очередь, закрепить свои знания и, надеюсь, помочь другим, сталкивающимся с той же задачей тонкой настройки GPT-3.

GPT-3 — это мощная языковая модель, обученная на массивном корпусе текстовых данных и способная генерировать человекоподобный текст с высокой степенью точности. Однако предварительно обученная модель не всегда подходит для конкретной задачи или предметной области. Вот где тонкая настройка входит в картину.

Несмотря на то, что мы наблюдаем такой большой прогресс в области LLM, с ChatGPT, GPT-4 и многими другими моделями, GPT-3 по-прежнему актуален, особенно когда модели GPT-3.5 недоступны для тонкой настройки.

OpenAI предоставляет API и очень подробную документацию (плюс эту), которая позволяет разработчикам точно настроить GPT-3 для своего конкретного варианта использования без больших вычислительных ресурсов. В этой статье мы рассмотрим процесс тонкой настройки GPT-3 с помощью OpenAI API. Мы рассмотрим ключевые этапы тонкой настройки и предоставим фрагменты кода, которые помогут вам начать работу.

Но обо всем по порядку, ключи API

  1. Создайте учетную запись OpenAI: перейдите на веб-сайт OpenAI и нажмите Зарегистрироваться в правом верхнем углу. Следуйте инструкциям, чтобы создать учетную запись.
  2. Нажмите на значок своего профиля в правом верхнем углу страницы и выберите «Просмотреть ключи API».
  3. Нажмите «Создать новый секретный ключ», чтобы сгенерировать новый ключ 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.