Ajuste un modelo Mistral-7b con optimización directa de preferencias

Aumente el rendimiento de sus modelos supervisados ​​y ajustados

Imagen del autor

Los modelos de lenguaje grande (LLM) previamente entrenados solo pueden realizar predicciones del siguiente token, lo que les impide responder preguntas. Es por eso que estos modelos básicos se ajustan a pares de instrucciones y respuestas para que actúen como asistentes útiles. Sin embargo, este proceso aún puede tener fallas: los LLM ajustados pueden ser sesgados, tóxicos, dañinos, etc. Aquí es donde entra en juego el aprendizaje por refuerzo a partir de la retroalimentación humana (RLHF).

RLHF proporciona diferentes respuestas al LLM, que se clasifican según el comportamiento deseado (ayuda, toxicidad, etc.). El modelo aprende a generar la mejor respuesta entre estos candidatos, imitando así el comportamiento que queremos inculcar. A menudo visto como una forma de censurar modelos, este proceso se ha vuelto popular recientemente para mejorar el rendimiento, como se muestra en chat-neural-7b-v3–1.

En este artículo crearemos NeuralHermes-2.5mediante un ajuste fino AbiertoHermes-2.5 utilizando una técnica similar a RLHF: Optimización de preferencias directas (DPO). Para ello, introduciremos un conjunto de datos de preferencias, describiremos cómo funciona el algoritmo DPO y lo aplicaremos a nuestro modelo. Veremos que mejora significativamente el rendimiento del modelo base en la tabla de clasificación Open LLM.

Como es habitual, el código está disponible en GitHub y colaboración de google.

🥇 Conjuntos de datos de preferencias

Los conjuntos de datos de preferencias no están estandarizados, pero normalmente consisten en una colección de respuestas clasificadas por humanos. Esta clasificación es esencial, ya que el proceso RLHF afina los LLM para generar la respuesta preferida. He aquí un ejemplo de Antrópico/hh-rlhfun conjunto de datos de preferencias populares:

Imagen del autor

La estructura del conjunto de datos es sencilla: para cada fila, hay una respuesta elegida (preferida) y una respuesta rechazada. El objetivo de RLHF es guiar el modelo para generar la respuesta preferida.

Los conjuntos de datos de preferencias son notoriamente costosos y difíciles de crear, ya que requieren recopilar comentarios manuales de los humanos. Esta retroalimentación también es subjetiva y puede fácilmente estar sesgada hacia respuestas seguras (pero incorrectas) o contradecirse (diferentes anotadores tienen diferentes valores). Con el tiempo, se han propuesto varias soluciones para abordar estos problemas, como reemplazar la retroalimentación humana con retroalimentación de IA (RLAIF).

Estos conjuntos de datos también tienden a ser mucho más pequeños que los conjuntos de datos de ajuste fino. Para ilustrar esto, el excelente chat-neural-7b-v3–1 (mejor 7B LLM en el Tabla de clasificación abierta de LLM cuando se lanzó) utiliza 518k muestras para ajustes finos (Orca abierta/SlimOrca) pero sólo 12,9k muestras para RLHF (Intel/orca_dpo_pairs). En este caso, los autores generaron respuestas con GPT-4/3.5 para crear las respuestas preferidas, y con Llama 2 13b charla para crear las respuestas rechazadas. Es una forma inteligente de evitar la retroalimentación humana y confiar únicamente en modelos con diferentes niveles de rendimiento.

🎓 Optimización de preferencias directas

Si bien el concepto de RLHF se ha utilizado en robótica durante mucho tiempo, se popularizó para los LLM en el artículo de OpenAI. Cómo ajustar los modelos de lenguaje a partir de las preferencias humanas. En este artículo, los autores presentan un marco en el que se entrena un modelo de recompensa para aproximarse a la retroalimentación humana. Este modelo de recompensa se utiliza luego para optimizar la política del modelo ajustado utilizando el Optimización de políticas próximas (PPO) algoritmo.

Imagen del autor

El concepto central de PPO gira en torno a realizar actualizaciones incrementales y más pequeñas de la política, ya que las actualizaciones más grandes pueden generar inestabilidad o soluciones subóptimas. Por experiencia, lamentablemente esta técnica sigue siendo inestable (las pérdidas divergen), difícil de reproducir (numerosos hiperparámetros, sensible a semillas aleatorias) y computacionalmente costosa.

Aquí es donde entra en juego la Optimización de Preferencias Directas (DPO). DPO simplifica el control al tratar la tarea como un problema de clasificación. En concreto, utiliza dos modelos: el modelo entrenado (o modelo de política) y una copia del mismo llamada modelo de referencia. Durante el entrenamiento, el objetivo es asegurarse de que el modelo entrenado genere mayores probabilidades de respuestas preferidas que el modelo de referencia. Por el contrario, también queremos que genere menores probabilidades de respuestas rechazadas. Significa que penalizamos al LLM por las malas respuestas y lo recompensamos por las buenas.

Imagen del autor

Al utilizar el LLM en sí como modelo de recompensa y emplear objetivos binarios de entropía cruzada, DPO alinea eficientemente los resultados del modelo con las preferencias humanas sin la necesidad de un muestreo extenso, ajuste del modelo de recompensa o ajustes de hiperparámetros complejos. Da como resultado un proceso más estable, más eficiente y computacionalmente menos exigente.

💾 Formatear los datos

En este ejemplo, ajustaremos el excelente OpenHermes-2.5-Mistral-7B, que es un modelo Mistral-7b que solo fue ajustado y supervisado. Para este fin, usaremos el Intel/orca_dpo_pairs conjunto de datos para alinear nuestro modelo y mejorar su rendimiento. A este nuevo modelo lo llamamos NeuralHermes-2.5-Mistral-7B.

El primer paso consiste en instalar las bibliotecas necesarias de la siguiente manera.

pip install -q datasets trl peft bitsandbytes sentencepiece wandb

Una vez hecho esto, podemos importar las bibliotecas. También estoy usando la pestaña de secretos en Google Colab para almacenar mi token de Hugging Face.

import os
import gc
import torch

import transformers
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, BitsAndBytesConfig
from datasets import load_dataset
from peft import LoraConfig, PeftModel, get_peft_model, prepare_model_for_kbit_training
from trl import DPOTrainer
import bitsandbytes as bnb
from google.colab import userdata
import wandb

# Defined in the secrets tab in Google Colab
hf_token = userdata.get('huggingface')
wb_token = userdata.get('wandb')
wandb.login(key=wb_token)

model_name = "teknium/OpenHermes-2.5-Mistral-7B"
new_model = "NeuralHermes-2.5-Mistral-7B"

OpenHermes-2.5-Mistral-7B utiliza una plantilla de chat específica, llamada ChatML. A continuación se muestra un ejemplo de una conversación formateada con esta plantilla:

<|im_start|>system
You are a helpful chatbot assistant.<|im_end|>
<|im_start|>user
Hi<|im_end|>
<|im_start|>assistant
Hi, how can I help you?<|im_end|>

Como puede ver, ChatML define diferentes roles (sistema, usuario, asistente) y agrega tokens especiales (<|im_start|> y <|im_end|>) para separarlos. Además, DPOEntrenador También requiere un formato específico con tres columnas: solicitud, elegido y rechazado.

Nuestro conjunto de datos contiene cuatro columnas: sistema, pregunta, chatgpt y llama2–13b-chat. Simplemente concatenaremos las columnas del sistema y de preguntas con la columna de solicitud. También asignaremos la columna chatgpt a “elegido” y llama2–13b-chat a “rechazado”. Para formatear el conjunto de datos de manera confiable, usaremos la función apply_chat_template() del tokenizador, que ya usa ChatML.

def chatml_format(example):
# Format system
if len(example['system']) > 0:
message = {"role": "system", "content": example['system']}
system = tokenizer.apply_chat_template([message], tokenize=False)
else:
system = ""

# Format instruction
message = {"role": "user", "content": example['question']}
prompt = tokenizer.apply_chat_template([message], tokenize=False, add_generation_prompt=True)

# Format chosen answer
chosen = example['chosen'] + "<|im_end|>\n"

# Format rejected answer
rejected = example['rejected'] + "<|im_end|>\n"

return {
"prompt": system + prompt,
"chosen": chosen,
"rejected": rejected,
}

# Load dataset
dataset = load_dataset("Intel/orca_dpo_pairs")['train']

# Save columns
original_columns = dataset.column_names

# Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "left"

# Format dataset
dataset = dataset.map(
chatml_format,
remove_columns=original_columns
)

Imprimamos una muestra del conjunto de datos formateado para confirmar que todo funciona como se esperaba:

{'prompt': '<|im_start|>system\nYou are an AI assistant. You will be given a task. You must generate a detailed and long answer.<|im_end|>\n<|im_start|>user\nGenerate an approximately fifteen-word sentence that describes all this data: Midsummer House eatType restaurant; Midsummer House food Chinese; Midsummer House priceRange moderate; Midsummer House customer rating 3 out of 5; Midsummer House near All Bar One<|im_end|>\n<|im_start|>assistant\n',
'chosen': 'Midsummer House is a moderately priced Chinese restaurant with a 3/5 customer rating, located near All Bar One.<|im_end|>\n',
'rejected': ' Sure! Here\'s a sentence that describes all the data you provided:\n\n"Midsummer House is a moderately priced Chinese restaurant with a customer rating of 3 out of 5, located near All Bar One, offering a variety of delicious dishes."<|im_end|>\n'}

Podemos ver que el mensaje combina instrucciones del sistema y del usuario. Gracias al argumento add_generación_prompt=True, también agrega el comienzo de la respuesta del asistente. Si desea omitir este paso, puede utilizar directamente el conjunto de datos preprocesado como mlabonne/chatml_dpo_pairs.

⚙️ Entrenando el modelo con DPO

A continuación, definimos las configuraciones de LoRA para entrenar el modelo. Como se describe en Publicación del blog de Intel, configuramos el valor de clasificación para que sea igual a lora_alpha, lo cual es inusual (2 * r como regla general). También apuntamos a todos los módulos lineales con adaptadores.

# LoRA configuration
peft_config = LoraConfig(
r=16,
lora_alpha=16,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=['k_proj', 'gate_proj', 'v_proj', 'up_proj', 'q_proj', 'o_proj', 'down_proj']
)

Ahora estamos listos para cargar el modelo que queremos ajustar con DPO. En este caso se requieren dos modelos: el modelo a ajustar y el modelo de referencia. Esto se debe principalmente a la legibilidad, ya que el objeto DPOTrainer crea automáticamente un modelo de referencia si no se proporciona ninguno.

# Model to fine-tune
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
load_in_4bit=True
)
model.config.use_cache = False

# Reference model
ref_model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
load_in_4bit=True
)

El último paso consiste en proporcionar todos los hiperparámetros a TrainingArguments y DPOTrainer:

  • Entre ellos, el parámetro beta es exclusivo de DPO ya que controla la divergencia con respecto a la política inicial (0,1 es un valor típico para él).
  • En comparación con los valores descritos en Publicación del blog de Intel, bajamos la tasa de aprendizaje (de 5e-4 a 5e-5) y el número de pasos (de 1.000 a 200). Optimicé manualmente estos valores después de algunas carreras para estabilizar el entrenamiento y lograr los mejores resultados.

Ahora podemos comenzar a entrenar el modelo. Tenga en cuenta que requiere una GPU A100 y tarda entre 1 hora en completar la capacitación.

# Training arguments
training_args = TrainingArguments(
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
gradient_checkpointing=True,
learning_rate=5e-5,
lr_scheduler_type="cosine",
max_steps=200,
save_strategy="no",
logging_steps=1,
output_dir=new_model,
optim="paged_adamw_32bit",
warmup_steps=100,
bf16=True,
report_to="wandb",
)

# Create DPO trainer
dpo_trainer = DPOTrainer(
model,
ref_model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
peft_config=peft_config,
beta=0.1,
max_prompt_length=1024,
max_length=1536,
)

# Fine-tune model with DPO
dpo_trainer.train()

Nuestro modelo ahora está afinado. Puedes consultar el proyecto en Weights & Biases en esta dirección. Aquí hay algunas métricas interesantes para analizar:

Imagen del autor

Curiosamente, la pérdida de entrenamiento cae rápidamente a cero (antes de los 50 pasos), a pesar de los 100 pasos de calentamiento. Mientras tanto, las otras métricas siguen evolucionando.

Los gráficos de tren/recompensas/elegidos y de tren/recompensas/rechazados corresponden a la diferencia media entre las probabilidades logarítmicas generadas por los modelos entrenados y de referencia. Tiene sentido que, con el tiempo, diverjan a medida que nuestro modelo entrenado aprende las respuestas preferidas. El gráfico de tren/recompensas/márgenes también muestra la diferencia entre estos dos gráficos. Finalmente, el gráfico tren/recompensa/precisiones muestra la frecuencia con la que se elige la respuesta preferida. El modelo entrenado alcanza rápidamente una puntuación de precisión perfecta, lo cual es una buena señal, pero también podría significar que la diferencia entre las respuestas preferidas y rechazadas es demasiado obvia.

Ahora que está entrenado, podemos fusionar el adaptador con el modelo original. A continuación, guardamos el modelo fusionado y el tokenizador antes de enviarlo al Hugging Face Hub.

# Save artifacts
dpo_trainer.model.save_pretrained("final_checkpoint")
tokenizer.save_pretrained("final_checkpoint")

# Flush memory
del dpo_trainer, model, ref_model
gc.collect()
torch.cuda.empty_cache()

# Reload model in FP16 (instead of NF4)
base_model = AutoModelForCausalLM.from_pretrained(
model_name,
return_dict=True,
torch_dtype=torch.float16,
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# Merge base model with the adapter
model = PeftModel.from_pretrained(base_model, "final_checkpoint")
model = model.merge_and_unload()

# Save model and tokenizer
model.save_pretrained(new_model)
tokenizer.save_pretrained(new_model)

# Push them to the HF Hub
model.push_to_hub(new_model, use_temp_dir=False, token=hf_token)
tokenizer.push_to_hub(new_model, use_temp_dir=False, token=hf_token)

Veamos cómo se comporta nuestro modelo en una prueba real. Formatearemos el mensaje para formular una pregunta básica: “¿Qué es un modelo de lenguaje grande?”

# Format prompt
message = [
{"role": "system", "content": "You are a helpful assistant chatbot."},
{"role": "user", "content": "What is a Large Language Model?"}
]
tokenizer = AutoTokenizer.from_pretrained(new_model)
prompt = tokenizer.apply_chat_template(message, add_generation_prompt=True, tokenize=False)

# Create pipeline
pipeline = transformers.pipeline(
"text-generation",
model=new_model,
tokenizer=tokenizer
)

# Generate text
sequences = pipeline(
prompt,
do_sample=True,
temperature=0.7,
top_p=0.9,
num_return_sequences=1,
max_length=200,
)
print(sequences[0]['generated_text'])

Aquí está la respuesta del modelo:

A large language model is a type of artificial intelligence (AI) system that has been trained on vast amounts of text data. These models are designed to understand and generate human language, allowing them to perform various natural language processing tasks, such as text generation, language translation, and question answering. Large language models typically use deep learning techniques, like recurrent neural networks (RNNs) or transformers, to learn patterns and relationships in the data, enabling them to generate coherent and contextually relevant responses. The size of these models, in terms of the number of parameters and the volume of data they are trained on, plays a significant role in their ability to comprehend and produce complex language structures.

Todo parece funcionar, ahora podemos evaluar el modelo fusionado. Como se trata de un modelo de propósito general, podemos aprovechar la arnés-de-evaluación-lm para evaluarlo. Como el proceso requiere bastantes recursos, también podemos enviarlo directamente para su evaluación en el Tabla de clasificación abierta de LLM. Tomó algunos días, pero aquí están los resultados en comparación con otros modelos de OpenHermes:

Imagen del autor

En comparación con el modelo original, el modelo NeuralHermes-2–5-Mistral-7B mejoró la puntuación media en 6,7 puntos (particularmente en GSM8K). Esta es una mejora inesperadamente grande, que muestra el poder de la optimización directa de preferencias.

Conclusión

En este artículo, ajustamos un modelo ajustado ya supervisado utilizando DPO y creamos el nuestro propio. NeuralHermes-2.5 modelo. Al aprovechar un conjunto de datos de preferencias de alta calidad, creamos un canal de ajuste eficiente con muestras que produjo una mejora significativa en la tabla de clasificación Open LLM. Si quieres probarlo, puedes encontrar variantes cuantificadas de este modelo o usar este Abrazando el espacio de la cara.

Tenga en cuenta que nuestro proceso de ajuste aún se puede mejorar de diferentes maneras. Por ejemplo, el conjunto de datos de preferencias aún está bastante sin editar y podría mejorarse con más filtrado y utilizando diferentes modelos. Además, aún se pueden modificar numerosos hiperparámetros para lograr mejores resultados. En particular, la tasa de aprendizaje aún se puede reducir para entrenar el modelo en más pasos e inyectar más datos de preferencias.

Referencias

Obtenga más información sobre el aprendizaje automático y respalde mi trabajo con un solo clic; conviértase en miembro de Medium aquí:

Únase a Medium con mi enlace de referencia – Maxime Labonne


Ajuste un modelo Mistral-7b con optimización directa de preferencias fue publicado originalmente en Hacia la ciencia de datos en Medium, donde las personas continúan la conversación resaltando y respondiendo a esta historia.