Aplicar Fine-tunning a Llama 3.2 para crear un custom dataset sobre un tema en concreto con Google Colab

Tiempo de lectura: 6 minutos

Hoy vamos a crear una versión custom de Llama 3.2 mediante la técnica de Fine-tunning usando Google Colab cómo máquina de entrenamiento.

Imagen Llama - Pexels

Primero tenemos que ir a Google Colab y crear un nuevo cuaderno. https://colab.research.google.com/

Una vez creado vamos a ir a Entorno de ejecución o Runtime y pulsamos en cambiar tipo de entorno de ejecución:

Cambiar tiipo de entorno ejecución en Google Colab

Elegimos T4 GPU

T4 GPU Google Colab

Y ahora vamos a configurar el entorno.

Para ello escribimos en el cuaderno:

!pip install unsloth

En el siguiente párrafo de código:

from unsloth import FastLanguageModel
from unsloth.chat_templates import get_chat_template
import torch

max_seq_length = 2048 
dtype = None 
load_in_4bit = True 

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Llama-3.2-1B-bnb-4bit",
    max_seq_length = max_seq_length,
    dtype = dtype,
    load_in_4bit = load_in_4bit,
)

# Cargar el template de chat
tokenizer = get_chat_template(
    tokenizer,  # El tokenizer cargado
    chat_template="llama-3.1"  # En este caso usamos el template de llama 3.1
)

Aquí tenemos la lista de modelos: https://huggingface.co/unsloth

En mi caso uso: unsloth/Llama-3.2-1B-bnb-4bit también se puede usar unsloth/Llama-3.2-3B-Instruct

Ahora ejecutamos:

Una vez ejecutado, está todo correcto y el modelo descargado.

Podemos seguir con la configuración de fine-tune:

model = FastLanguageModel.get_peft_model(
    model,
    r = 16,
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj",],
    lora_alpha = 16,
    lora_dropout = 0,
    bias = "none",
    use_gradient_checkpointing = "unsloth",
    random_state = 3407,
    use_rslora = False,
    loftq_config = None,
)

Ahora para el conjunto de entrenamiento vamos a utilizar el formato Llama 3.1.

Ahora creamos los datos de entrenamiento con este formato:

{
  "conversations":[
     {"role": "user", "content": "¿Pregunta?"},
     {"role": "assistant", "content": "Respuesta..."}
  ]
}

Quedando asi:

# Ejemplo de datos de entrenamiento
training_data = [
    {
        "conversations": [
            {"role": "user", "content": "¿Cómo hago un pedido?"},
            {"role": "assistant", "content": "Para hacer un pedido, selecciona los productos que desees, agrégales al carrito y sigue los pasos para completar el pago."}
        ]
    },
    {
        "conversations": [
            {"role": "user", "content": "¿Cuánto tarda en llegar mi pedido?"},
            {"role": "assistant", "content": "El tiempo de entrega es de 3 a 5 días hábiles."}
        ]
    },
    {
        "conversations": [
            {"role": "user", "content": "¿Qué métodos de pago aceptan?"},
            {"role": "assistant", "content": "Aceptamos tarjetas de crédito, PayPal y transferencia bancaria."}
        ]
    },
    {
        "conversations": [
            {"role": "user", "content": "¿Puedo cancelar mi pedido?"},
            {"role": "assistant", "content": "Sí, puedes cancelar tu pedido dentro de las 24 horas después de realizarlo."}
        ]
    },
    {
        "conversations": [
            {"role": "user", "content": "¿Cómo puedo rastrear mi pedido?"},
            {"role": "assistant", "content": "Recibirás un correo con el número de seguimiento una vez que se haya enviado tu pedido."}
        ]
    }
]

# Convertir el ejemplo a un dataset
import pandas as pd

# Convertir la lista de conversaciones a un DataFrame
df = pd.DataFrame(training_data)

dataset = df.to_dict(orient='records')

from datasets import Dataset

formatted_dataset = Dataset.from_dict({
    "conversations": [convo["conversations"] for convo in dataset]  # Solo extraemos la lista de conversaciones
})

Con esta plantilla podemos ir completando nuestros datos de entrenamiento.

La transformamos a formato Hugging Face:

from unsloth.chat_templates import standardize_sharegpt
# Estandarizar el dataset
standardized_dataset = standardize_sharegpt(formatted_dataset)

Ahora vamos a crear un bucle que aplicará el template esperado a los datos de entrenamiento, de esta forma nos permitirá entrenar el modelo:

from unsloth.chat_templates import get_chat_template
from transformers import AutoTokenizer

def formatting_prompts_func(examples):
    texts = []
    
    for convo in examples["conversations"]:
        convo_text = ""
        
        try:
            for turn in convo:
                role = turn["role"]
                content = turn["content"]
                
                # Crear el texto usando el formato deseado
                text = f"<|{role}|>{content}<|endoftext|>"
                convo_text += text + " "
                
            texts.append(convo_text.strip())
        
        except Exception as e:
            print(f"Error procesando la conversación: {convo}, error: {e}")
    
    return {"text": texts}

# Formatear el dataset
formatted_dataset = standardized_dataset.map(formatting_prompts_func, batched=True)

print(formatted_dataset)

print(formatted_dataset[:5]["conversations"])  # Muestra las primeras 5 entradas del dataset

print(formatted_dataset[:5]["text"]) #Muestra el formato text

Ahora aplicamos el entrenador:

from trl import SFTTrainer
from transformers import TrainingArguments, DataCollatorForSeq2Seq
from unsloth import is_bfloat16_supported

# Configuración del entrenador
trainer = SFTTrainer(
    model=model,  # Asegúrate de que tienes tu modelo cargado
    tokenizer=tokenizer,
    train_dataset=formatted_dataset,  # Dataset formateado
    dataset_text_field="text",
    max_seq_length=512,  # Ajusta esto según sea necesario
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer),
    dataset_num_proc=2,
    packing=False,
    args=TrainingArguments(
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,
        warmup_steps=5,
        num_train_epochs=1,  # Establece esto para el número de épocas que desees
        learning_rate=2e-4,
        fp16=not is_bfloat16_supported(),
        bf16=is_bfloat16_supported(),
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.01,
        lr_scheduler_type="linear",
        seed=3407,
        output_dir="outputs",
    ),
)

Ajustamos el formato:

from unsloth.chat_templates import train_on_responses_only
trainer = train_on_responses_only(
    trainer,
    instruction_part = "<|start_header_id|>user<|end_header_id|>\n\n",
    response_part = "<|start_header_id|>assistant<|end_header_id|>\n\n",
)

tokenizer.decode(trainer.train_dataset[2]["input_ids"])

Y finalmente entrenamos

trainer_stats = trainer.train()
Entrenamiento Llama 3.2

Ollama format:

!curl -fsSL https://ollama.com/install.sh | sh

Guardar el modelo a formato Ollama:

Con este fichero podemos transformar a distintos formatos:

model.save_pretrained_gguf("model", tokenizer,)

Ahora arrancamos el servidor Ollama:

import subprocess
subprocess.Popen(["ollama", "serve"])
import time
time.sleep(3) # Wait for a few seconds for Ollama to load!

Generar el archivo Modelfile:

print(tokenizer._ollama_modelfile)

Ahora nos imprimirá el Modelfile, debemos modificar la ruta y copiar el contenido para guardarlo dentro de:

model/Modelfile

En mi caso queda así:

FROM ./model/unsloth.Q8_0.gguf
TEMPLATE """{{ if .Messages }}
{{- if or .System .Tools }}<|start_header_id|>system<|end_header_id|>
{{- if .System }}

{{ .System }}
{{- end }}
{{- if .Tools }}

You are a helpful assistant with tool calling capabilities. When you receive a tool call response, use the output to format an answer to the orginal use question.
{{- end }}
{{- end }}<|eot_id|>
{{- range $i, $_ := .Messages }}
{{- $last := eq (len (slice $.Messages $i)) 1 }}
{{- if eq .Role "user" }}<|start_header_id|>user<|end_header_id|>
{{- if and $.Tools $last }}

Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt.

Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}. Do not use variables.

{{ $.Tools }}
{{- end }}

{{ .Content }}<|eot_id|>{{ if $last }}<|start_header_id|>assistant<|end_header_id|>

{{ end }}
{{- else if eq .Role "assistant" }}<|start_header_id|>assistant<|end_header_id|>
{{- if .ToolCalls }}

{{- range .ToolCalls }}{"name": "{{ .Function.Name }}", "parameters": {{ .Function.Arguments }}}{{ end }}
{{- else }}

{{ .Content }}{{ if not $last }}<|eot_id|>{{ end }}
{{- end }}
{{- else if eq .Role "tool" }}<|start_header_id|>ipython<|end_header_id|>

{{ .Content }}<|eot_id|>{{ if $last }}<|start_header_id|>assistant<|end_header_id|>

{{ end }}
{{- end }}
{{- end }}
{{- else }}
{{- if .System }}<|start_header_id|>system<|end_header_id|>

{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>

{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>

{{ end }}{{ .Response }}{{ if .Response }}<|eot_id|>{{ end }}"""
PARAMETER stop "<|start_header_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|eom_id|>"
PARAMETER temperature 1.5
PARAMETER min_p 0.1

Ahora podemos importarlo a ollama:

!ollama create unsloth_model -f ./nuevo_modelo/Modelfile

Una vez importado, podemos realizarle consultas:

!curl http://localhost:11434/api/generate -d '{ \
    "model": "unsloth_model", \
    "stream": false, \
    "prompt": "¿Cuánto tarda en llegar mi pedido?" \
    }'

NOTA: Para que funcione mejor, debemos entrenarlo con bastantes datos.

Publicar modelo en huggingface:

# https://huggingface.co/settings/tokens for a token!
model.push_to_hub_gguf("hf/model", tokenizer, token = "")

Guardar en diferentes formatos:

16bit GGUF:

model.save_pretrained_gguf("model", tokenizer, quantization_method = "f16")
model.push_to_hub_gguf("hf/model", tokenizer, quantization_method = "f16", token = "")

q4_k_m GGUF:

model.save_pretrained_gguf("model", tokenizer, quantization_method = "q4_k_m")
model.push_to_hub_gguf("hf/model", tokenizer, quantization_method = "q4_k_m", token = "")

Guardar el Modelo Usando Múltiples Opciones de Cuantización:

    model.push_to_hub_gguf(
        "hf/model", # Change hf to your username!
        tokenizer,
        quantization_method = ["q4_k_m", "q8_0", "q5_k_m",],
        token = "",
    )

*En token debes especificar el token de Huggin Face

Una vez entrenado podemos guardarlo de la siguiente forma:

# Guardar el modelo entrenado y el tokenizer
output_dir = "./output_dir"  # Directorio donde se guardará el modelo

trainer.save_model(output_dir)  # Guarda el modelo
tokenizer.save_pretrained(output_dir)  # Guarda el tokenizer

# También puedes guardar la configuración del modelo directamente
model.config.save_pretrained(output_dir)  # Guarda la configuración del modelo

Descargar modelo creado:

Comprimimos el modelo:

!zip -r nuevo_modelo.zip nuevo_modelo/

Lo descargamos:

from google.colab import files

files.download('nuevo_modelo.zip')

Ahora podemos utilizarlo por ejemplo en Ollama con Docker compose: https://devcodelight.com/ollama-con-llama-3-2-en-docker/

Primero descomprimimos el zip descargado en la carpeta ./models

Despues levantamos el docker compose si no lo tenemos levantado:

docker compose up -d

Y ahora cargamos el modelo:

ollama create unsloth_model -f ./nuevo_modelo/Modelfile

Y genera el modelo llamado unsloth_model (podemos llamarlo como queramos).

Finalmente podemos ejecutarlo:

!curl http://localhost:11434/api/generate -d '{ \
    "model": "unsloth_model", \
    "stream": false, \
    "prompt": "¿Cuánto tarda en llegar mi pedido?" \
    }'

Deja un comentario