Ajuste fino de LLAMAv2 con QLora en Google Colab de forma gratuita
Fine-tuning LLAMAv2 with QLora on Google Colab for free
Fue un sueño ajustar un modelo de 7B en una sola GPU de forma gratuita en Google Colab hasta hace poco. El 23 de mayo de 2023, Tim Dettmers y su equipo presentaron un artículo revolucionario[1] sobre el ajuste fino de Modelos de Lenguaje Cuantizados de Gran Tamaño.
Un modelo cuantizado es un modelo que tiene sus pesos en un tipo de datos que es inferior al tipo de datos en el que fue entrenado. Por ejemplo, si entrenas un modelo en un punto flotante de 32 bits, y luego conviertes esos pesos a un tipo de datos inferior como punto flotante de 16/8/4 bits de manera que haya un impacto mínimo o nulo en el rendimiento del modelo.
No vamos a hablar mucho sobre la teoría de la cuantización aquí. Puedes consultar la excelente publicación de blog de Hugging-Face[2][3] y un excelente video de YouTube[4] del propio Tim Dettmers para entender la teoría subyacente.
- Noticias de VoAGI, 20 de septiembre Python en Excel Esto cambiará l...
- Top 5 Certificados Universitarios en 2024
- Conversaciones seguras Protegiendo la privacidad y los datos al usa...
En resumen, se puede decir que QLora significa:
Ajuste fino de modelos de lenguaje cuantizados de gran tamaño utilizando matrices de adaptación de rango bajo (LoRA)[5]
Vamos directamente al código:
Preparación de datos
Es importante entender que los modelos de lenguaje grandes están diseñados para seguir instrucciones, esto se introdujo por primera vez en el artículo ACL 2021[6]. La idea es simple, le damos a un modelo de lenguaje una instrucción, y sigue la instrucción y realiza esa tarea. Por lo tanto, el conjunto de datos en el que queremos ajustar fino nuestro modelo debe estar en el formato instruct, si no lo está, podemos convertirlo.
Uno de los formatos comunes es el formato instruct. Utilizaremos la Plantilla de Prompts de Alpaca[7] que es
A continuación se muestra una instrucción que describe una tarea, junto con una entrada que proporciona más contexto. Escribe una respuesta que complete adecuadamente la solicitud.
### Instrucción:
{instruction}
### Entrada:
{input}
### Respuesta:
{response}
Utilizaremos el conjunto de datos SNLI, que es un conjunto de datos que contiene 2 oraciones y la relación entre ellas, ya sea contradicción, implicación mutua o neutral. Lo utilizaremos para generar contradicción para una oración utilizando LLAMAv2. Podemos cargar este conjunto de datos simplemente utilizando pandas.
import pandas as pd
df = pd.read_csv('snli_1.0_train_matched.csv')
df['gold_label'].value_counts().plot(kind='barh')
Aquí podemos ver algunos ejemplos aleatorios de contradicción.
df[df['gold_label'] == 'contradiction'].sample(10)[['sentence1', 'sentence2']]
Ahora podemos crear una pequeña función que tome solo las oraciones contradictorias y convierta el conjunto de datos al formato instruct.
def convert_to_format(row):
sentence1 = row['sentence1']
sentence2 = row['sentence2']ccccc
prompt = """A continuación se muestra una instrucción que describe una tarea junto con una entrada que proporciona más contexto. Escribe una respuesta que complete adecuadamente la solicitud."""
instruction = """Dada la siguiente oración, tu tarea es generar la negación para ella en formato json"""
input = str(sentence1)
response = f"""```json
{{'orignal_sentence': '{sentence1}', 'generated_negation': '{sentence2}'}}
```
"""
if len(input.strip()) == 0: # prompt + 2 new lines + ###instruction + new line + input + new line + ###response
text = prompt + "\n\n### Instrucción:\n" + instruction + "\n### Respuesta:\n" + response
else:
text = prompt + "\n\n### Instrucción:\n" + instruction + "\n### Entrada:\n" + input + "\n" + "\n### Respuesta:\n" + response
# necesitamos 4 columnas para auto entrenamiento, instrucción, entrada, salida, texto
return pd.Series([instruction, input, response, text])
new_df = df[df['gold_label'] == 'contradiction'][['sentence1', 'sentence2']].apply(convert_to_format, axis=1)
new_df.columns = ['instrucción', 'entrada', 'salida', 'texto']
new_df.to_csv('snli_instruct.csv', index=False)
Aquí hay un ejemplo del punto de datos de muestra:
"A continuación se muestra una instrucción que describe una tarea junto con una entrada que proporciona más contexto. Escriba una respuesta que complete adecuadamente la solicitud.
### Instrucción:
Dada la siguiente oración, su tarea es generar la negación de la misma en formato json.
### Entrada:
Una pareja jugando con un niño pequeño en la playa.
### Respuesta:
```json
{'orignal_sentence': 'Una pareja jugando con un niño pequeño en la playa.', 'generated_negation': 'Una pareja observa a una niña pequeña jugar sola en la playa.'}
```
Ahora tenemos nuestro conjunto de datos en el formato correcto, comencemos con el ajuste fino. Antes de comenzar, instalemos los paquetes necesarios. Utilizaremos accelerate, peft (Parameter efficient Fine Tuning), combinado con Bits and bytes de Hugging Face y transformers.
!pip install -q accelerate==0.21.0 peft==0.4.0 bitsandbytes==0.40.2 transformers==4.31.0 trl==0.4.7
import os
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
HfArgumentParser,
TrainingArguments,
pipeline,
logging,
)
from peft import LoraConfig, PeftModel
from trl import SFTTrainer
Puede cargar el conjunto de datos formateado en Google Drive y cargarlo en Colab.
from google.colab import drive
import pandas as pd
drive.mount('/content/drive')
df = pd.read_csv('/content/drive/MyDrive/snli_instruct.csv')
Puede convertirlo fácilmente al formato de conjunto de datos de Hugging Face utilizando el método from_pandas
. Esto será útil para entrenar el modelo.
from datasets import Dataset
dataset = Dataset.from_pandas(df)
Utilizaremos el modelo LLamav2 ya cuantizado que proporciona abhishek/llama-2–7b-hf-small-shards. Definamos algunos hiperparámetros y variables aquí:
# El modelo que desea entrenar desde el centro de Hugging Face
model_name = "abhishek/llama-2-7b-hf-small-shards"
# Nombre del modelo ajustado finamente
new_model = "llama-2-contradictor"
################################################################################
# Parámetros de QLoRA
################################################################################
# Dimensión de atención de LoRA
lora_r = 64
# Parámetro alfa para escalar LoRA
lora_alpha = 16
# Probabilidad de dropout para capas LoRA
lora_dropout = 0.1
################################################################################
# Parámetros de bitsandbytes
################################################################################
# Activar carga de modelo base con precisión de 4 bits
use_4bit = True
# Tipo de cálculo para modelos base de 4 bits
bnb_4bit_compute_dtype = "float16"
# Tipo de cuantización (fp4 o nf4)
bnb_4bit_quant_type = "nf4"
# Activar cuantización anidada para modelos base de 4 bits (doble cuantización)
use_nested_quant = False
################################################################################
# Parámetros de TrainingArguments
################################################################################
# Directorio de salida donde se almacenarán las predicciones y los puntos de control del modelo
output_dir = "./results"
# Número de épocas de entrenamiento
num_train_epochs = 1
# Habilitar entrenamiento fp16/bf16 (establecer bf16 en True con un A100)
fp16 = False
bf16 = False
# Tamaño de lote por GPU para entrenamiento
per_device_train_batch_size = 4
# Tamaño de lote por GPU para evaluación
per_device_eval_batch_size = 4
# Número de pasos de actualización para acumular los gradientes
gradient_accumulation_steps = 1
# Habilitar checkpoint de gradientes
gradient_checkpointing = True
# Máxima normalización de gradiente (recorte de gradiente)
max_grad_norm = 0.3
# Tasa de aprendizaje inicial (optimizador AdamW)
learning_rate = 1e-5
# Decaimiento de peso para aplicar a todas las capas excepto los pesos de sesgo/LayerNorm
weight_decay = 0.001
# Optimizador a utilizar
optim = "paged_adamw_32bit"
# Programa de tasa de aprendizaje
lr_scheduler_type = "cosine"
# Número de pasos de entrenamiento (anula num_train_epochs)
max_steps = -1
# Proporción de pasos para un calentamiento lineal (de 0 a tasa de aprendizaje)
warmup_ratio = 0.03
# Agrupar secuencias en lotes con misma longitud
# Ahorra memoria y acelera considerablemente el entrenamiento
group_by_length = True
# Guardar punto de control cada X pasos de actualización
save_steps = 0
# Registrar cada X pasos de actualización
logging_steps = 100
################################################################################
# Parámetros de SFT
################################################################################
# Longitud máxima de secuencia a utilizar
max_seq_length = None
# Empaquetar múltiples ejemplos cortos en la misma secuencia de entrada para aumentar la eficiencia
packing = False
# Cargar el modelo completo en la GPU 0
device_map = {"": 0}
La mayoría de estos son hiperparámetros bastante directos que tienen estos valores predeterminados. Siempre puedes consultar la documentación para obtener más detalles.
Ahora podemos usar simplemente la clase BitsAndBytesConfig para crear la configuración para el ajuste fino de 4 bits.
compute_dtype = getattr(torch, bnb_4bit_compute_dtype)
bnb_config = BitsAndBytesConfig(
load_in_4bit=use_4bit,
bnb_4bit_quant_type=bnb_4bit_quant_type,
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=use_nested_quant,
)
Ahora podemos cargar el modelo base con la configuración de 4 bits de BitsAndBytesConfig y el tokenizador para el ajuste fino.
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map=device_map
)
model.config.use_cache = False
model.config.pretraining_tp = 1
Ahora podemos crear la configuración de LoRA y establecer los parámetros de entrenamiento.
# Cargar la configuración de LoRA
peft_config = LoraConfig(
lora_alpha=lora_alpha,
lora_dropout=lora_dropout,
r=lora_r,
bias="none",
task_type="CAUSAL_LM",
)
# Establecer los parámetros de entrenamiento
training_arguments = TrainingArguments(
output_dir=output_dir,
num_train_epochs=num_train_epochs,
per_device_train_batch_size=per_device_train_batch_size,
gradient_accumulation_steps=gradient_accumulation_steps,
optim=optim,
save_steps=save_steps,
logging_steps=logging_steps,
learning_rate=learning_rate,
weight_decay=weight_decay,
fp16=fp16,
bf16=bf16,
max_grad_norm=max_grad_norm,
max_steps=max_steps,
warmup_ratio=warmup_ratio,
group_by_length=group_by_length,
lr_scheduler_type=lr_scheduler_type,
report_to="tensorboard"
)
Ahora simplemente podemos usar SFTTrainer, que es proporcionado por trl de HuggingFace, para iniciar el entrenamiento.
# Establecer los parámetros de ajuste fino supervisado
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
peft_config=peft_config,
dataset_text_field="text", # esta es la columna de texto en el conjunto de datos
max_seq_length=max_seq_length,
tokenizer=tokenizer,
args=training_arguments,
packing=packing,
)
# Entrenar el modelo
trainer.train()
# Guardar el modelo entrenado
trainer.model.save_pretrained(new_model)
Esto iniciará el entrenamiento durante el número de épocas que hayas establecido anteriormente. Una vez que el modelo esté entrenado, asegúrate de guardarlo en el drive para que puedas cargarlo nuevamente (ya que debes reiniciar la sesión en Colab). Puedes guardar el modelo en el drive utilizando los comandos zip y mv.
!zip -r llama-contradictor.zip results llama-contradictor
!mv llama-contradictor.zip /content/drive/MyDrive
Ahora, cuando reinicies la sesión de Colab, puedes moverlo nuevamente a tu sesión.
!unzip /content/drive/MyDrive/llama-contradictor.zip -d .
Necesitas cargar el modelo base nuevamente y combinarlo con las matrices de LoRA ajustadas. Esto se puede hacer utilizando la función merge_and_unload()
.
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
base_model = AutoModelForCausalLM.from_pretrained(
"abhishek/llama-2-7b-hf-small-shards",
low_cpu_mem_usage=True,
return_dict=True,
torch_dtype=torch.float16,
device_map={"": 0},
)
model = PeftModel.from_pretrained(base_model, '/content/llama-contradictor')
model = model.merge_and_unload()
pipe = pipeline(task="text-generation", model=model, tokenizer=tokenizer, max_length=200)
Inferencia
Puedes probar tu modelo simplemente pasando las entradas en la misma plantilla de instrucciones que hemos definido anteriormente.
prompt_template = """### Instrucción:
Dada la siguiente oración, tu trabajo es generar la negación para ella en formato json
### Entrada:
{}
### Respuesta:
"""
oración = "El pronóstico del tiempo predice un día soleado con una temperatura alta alrededor de 30 grados Celsius, perfecto para un día en la playa con amigos y familia."
oración_entrada = prompt_template.format(oración.strip())
resultado = pipe(oración_entrada)
print(resultado)
Salida
### Instrucción:
Dada la siguiente oración, tu trabajo es generar la negación para ella en formato json
### Entrada:
El pronóstico del tiempo predice un día soleado con una temperatura alta alrededor de 30 grados Celsius, perfecto para un día en la playa con amigos y familia.
### Respuesta:
```json
{
"oración": "El pronóstico del tiempo predice un día soleado con una temperatura alta alrededor de 30 grados Celsius, perfecto para un día en la playa con amigos y familia.",
"negación": "El pronóstico del tiempo predice un día lluvioso con una temperatura baja alrededor de 10 grados Celsius, no ideal para un día en la playa con amigos y familia."
}
```
Filtrar Salida Útil
Habrá muchas ocasiones en las que el modelo seguirá prediciendo incluso después de que se genere la respuesta debido al límite de tokens. En este caso, necesitas agregar una función de postprocesamiento que filtre la parte JSON que es lo que necesitamos. Esto se puede hacer usando una expresión regular simple.
import re
import json
def format_results(s):
patrón = r'```json\n(.*?)\n```'
# Encuentra todas las ocurrencias de objetos JSON en la cadena
coincidencias_json = re.findall(patrón, s, re.DOTALL)
if not coincidencias_json:
# intenta encontrar el segundo patrón
patrón = r'\{.*?"oración":.*?"negación":.*?\}'
coincidencias_json = re.findall(patrón, s)
# Devuelve el primer objeto JSON encontrado, o None si no se encuentra ninguna coincidencia
return json.loads(coincidencias_json[0]) if coincidencias_json else None
Esto te dará la salida requerida en lugar de que el modelo repita tokens de salida aleatorios.
Resumen
En este blog, aprendiste los conceptos básicos de QLora, cómo ajustar finamente un modelo LLama v2 en Colab usando QLora, Ajuste de Instrucciones y una plantilla de muestra del conjunto de datos Alpaca que se puede usar para ajustar aún más un modelo.
Referencias
[1]: QLoRA: Ajuste eficiente de LLMs cuantizados, 23 de mayo de 2023, Tim Dettmers et al.
[2]: https://huggingface.co/blog/hf-bitsandbytes-integration
[3]: https://huggingface.co/blog/4bit-transformers-bitsandbytes
[4]: https://www.youtube.com/watch?v=y9PHWGOa8HA
[5]: https://arxiv.org/abs/2106.09685
[6]: https://aclanthology.org/2022.acl-long.244/
[7]: https://crfm.stanford.edu/2023/03/13/alpaca.html
[8]: Cuaderno de Colab de @maximelabonne https://colab.research.google.com/drive/1PEQyJO1-f6j0S_XJ8DV50NkpzasXkrzd?usp=sharing
Ahmad Anis es un apasionado Ingeniero e Investigador de Aprendizaje Automático que actualmente trabaja en redbuffer.ai. Más allá de su trabajo diario, Ahmad participa activamente en la comunidad de Aprendizaje Automático. Es líder regional de Cohere for AI, una organización sin fines de lucro dedicada a la ciencia abierta, y es un creador de la comunidad de AWS. Ahmad es un colaborador activo en Stackoverflow, donde tiene más de 2300 puntos. Ha contribuido a muchos proyectos de código abierto famosos, incluyendo Shap-E de OpenAI.