GPT-3: lecciones del ajuste fino

Guía paso a paso para el ajuste fino de GPT-3

Paula Ceccón Ribeiro
GoPenAI
Foto de Andrew Neel en Pexels.

En primer lugar, este es mi primer artículo aquí, y mi intención es, ante todo, consolidar mi aprendizaje y, con suerte, ayudar a otros que enfrentan la misma tarea de ajustar GPT-3.

GPT-3 es un poderoso modelo de lenguaje que ha sido entrenado en un corpus masivo de datos de texto y tiene la capacidad de generar texto similar al humano con un alto grado de precisión. Sin embargo, es posible que el modelo preentrenado no siempre sea adecuado para una tarea o dominio específico. Aquí es donde el ajuste fino entra en escena.

Si bien estamos viendo mucho progreso en el área de LLM, con ChatGPT, GPT-4 y muchos otros modelos, GPT-3 sigue siendo relevante, especialmente mientras que los modelos GPT-3.5 no están disponibles para ajustes.

OpenAI proporciona una API y una documentación muy completa (más esta) que permite a los desarrolladores ajustar GPT-3 para su caso de uso específico sin grandes recursos computacionales. En este artículo, repasaremos el proceso de ajuste fino de GPT-3 con la API de OpenAI. Cubriremos los pasos clave involucrados en el ajuste fino y proporcionaremos fragmentos de código para ayudarlo a comenzar.

Pero lo primero es lo primero, las claves API

  1. Cree una cuenta de OpenAI: vaya al sitio web de OpenAI y haga clic en «Registrarse» en la esquina superior derecha. Siga las instrucciones para crear una cuenta.
  2. Haga clic en el ícono de su perfil en la esquina superior derecha de la página y seleccione «Ver claves API».
  3. Haga clic en «Crear nueva clave secreta» para generar una nueva clave API. Asegúrese de guardar esa clave, ya que no podrá volver a verla.

Con las llaves en las manos, podemos instalar todas las dependencias necesarias (estoy agregando dependencias adicionales que usaré para presentar algunos aprendizajes).

pip install pandas
pip install openai
pip install seaborn
pip install tiktoken
pip install scikit-learn

El siguiente paso es preparar los datos de entrenamiento. Tiene que ser un documento JSONL con un prompt y un completion. yo trabajo con pandas en mi base diaria, para crear los datos en el formato esperado usando pandasel siguiente fragmento se puede usar o adaptar para cargar un conjunto de datos personalizado:

import pandas as pd

df = pd.DataFrame(zip(prompts, completions), columns=["prompt", "completion"])
df.to_json("training_data.jsonl", orient="records", lines=True)

Cada prompt debe terminar con un sufijo, por \n\n###\n\n o ->. También, cada completion con un sufijo también, por ejemplo, \n, ###o cualquier otro token que no aparezca en ningún completado.

OpenAI ofrece una herramienta CLI que valida, brinda sugerencias y reformatea datos dados. Después de exportar su clave

export OPENAI_API_KEY="<OPENAI_API_KEY>"

se puede llamar en Python con:

!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.

Tenga en cuenta que el resultado también identifica que estoy trabajando con una tarea de clasificación, demostrando que el comando CLI activa el tren con la cantidad correcta de clases. Los documentos de OpenAI proporcionan pautas para ajustar GPT-3 para problemas de clasificación.

El comando mencionado anteriormente es el único comando CLI para el que no pude encontrar uno respectivo para ser llamado a través del API de Python. como quiero producir la capacitación, realmente no quiero usar CLI.

Además, tuve un error interesante durante los experimentos para probar la cantidad de datos necesarios para obtener un buen resultado de rendimiento:

TEl número de clases en el archivo-xxx no coincide con el número de clases especificado en los hiperparámetros.

Esto se debió a que termino con un número diferente de clases en el conjunto de datos de entrenamiento y validación (sin estratificación). Esto parece ser un problema conocido. Pero para mí, este problema, además de la imposibilidad de usar el prepare_data como una llamada a una función de Python me hizo cambiar a scikit-learn para dividir los datos. Tenga en cuenta, sin embargo, que con esta estrategia, debe asegurarse de a mano elimine cualquier duplicado y agregue el separador de sufijo. Así es como lo he hecho:

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)

Con el tren preparado y los datos de validación listos, debemos cargarlos para que estén disponibles durante el ajuste.

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

Entonces, podemos crear un trabajo de ajuste:

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
)

Tenga en cuenta que para usar compute_classification_metricses necesario proporcionar un archivo de validación y establecer classification_n_classes para la clasificación multiclase. También es esencial notar que:

[…] estas evaluaciones asumen que está utilizando etiquetas de texto para clases que se tokenizan en un solo token, […]. Si estas condiciones no se cumplen, es probable que los números que obtenga sean incorrectos.

Esto significa que si compute_classification_metrics es True, cada clase debe comenzar con un token diferente. Para verificar la tokenización de un texto, puede usar el tokenizador OpenAI. Programáticamente, tiktoken puede ser usado:

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))

El fine_tuning_job contiene información sobre el trabajo, como los hiperparámetros utilizados, los archivos de capacitación y validación, y el nombre del modelo ajustado. Esta es una solicitud de disparar y olvidar, por lo que no recibiremos una notificación una vez que se complete el ajuste. En cambio, necesitamos recuperar el trabajo y verificar si el modelo ajustado no está 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)

Para visualizar la precisión de la validación, es necesario recuperar el archivo de resultados que contiene las métricas de validación:

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');

Una vez que se completa el trabajo de ajuste y el modelo está disponible, estamos listos para probarlo en un nuevo aviso:

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)

  • Usar scikit-learn train_test_split para evitar problemas de ajuste relacionados con la forma en que se dividen los datos.
  • Asegúrese de que cada clase comience con un token diferente.
  • Para tareas de clasificación, en el tiempo de inferencia establecido max_tokens=1.

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *