Inicio / Artículos / Notas prácticas: De la afinación fino a la precisión: reducción significativa

Notas prácticas: De la afinación fino a la precisión: reducción significativa

Guía práctica paso a paso: Desde el ajuste fino hasta la precisión: reducción significativa de los contratos, verificaciones y espacios para código adicional para los equipos que implementan este patrón.

2822 palabras

Esta guía reconstruye el proceso desde las materias primas hasta un sistema funcional para: De la afinación fino a la precisión: reduciendo significativamente las alucinaciones en tu pipeline RAG. El enfoque está en pasos operativos, verificaciones explícitas y código que puedes incorporar a un repositorio sin tener que adivinar la intención.

1. Introducción

En la etapa de introducción, define las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Considera esta etapa como un contrato entre las entradas y los resultados validados. Nombra los artefactos, define las verificaciones de éxito y rechaza las completaciones parciales silenciosas. Cita los pasajes que realmente sustentan la respuesta; sin citas, los operadores no pueden distinguir entre alucinaciones y lagunas en el indexado.

You: What was the revenue from contracts with customers in 2024?
Assistant: The revenue was €179,058,000.  ← Wrong! Correct answer is €159,088,000.

2. ¿Por qué la afinación fino?

En la fase de ajuste fino “2 Why”, defina las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido, sin tener que adivinar el estado oculto. Registre los tiempos de ejecución y el costo en tokens o consultas junto con los resultados funcionales. La visibilidad temprana de los costos evita facturas inesperadas cuando el proceso pasa de entornos de demostración a entornos compartidos. Cite los pasajes que realmente sirvieron de base para la respuesta; sin citas, los operadores no pueden distinguir entre alucinaciones y brechas en el indexado.

La solución: Ajuste fino con MLX

Para el ajuste fino de The Solution con etapas, defina las entradas, el responsable de cada paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido, sin tener que adivinar el estado oculto. Guarde la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de datos confidenciales y las banderas de funcionalidad deben encontrarse en un único lugar que los operadores puedan auditar sin necesidad de leer todo el sistema. Cite los pasajes que realmente sustentan la respuesta. Sin citaciones, los operadores no pueden distinguir entre alucinaciones y lagunas en el indexado.

3. Diagrama de arquitectura

En la fase del diagrama de arquitectura 3, defina las entradas, el responsable de cada paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido, sin tener que adivinar el estado oculto. Documente tanto la ruta óptima como la ruta de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores. Cite los pasajes que realmente sustentan la respuesta. Sin citas, los operadores no pueden distinguir entre alucinaciones y lagunas en el indexado.

┌─────────────────────────────────────────────────────────────────────────────┐
│                       PART 3: FINE-TUNING WORKFLOW (M1)                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   1. Dataset                2. MLX Fine-Tuning                              │
│  ┌──────────────────┐      ┌────────────────────────────────────────────┐   │
│  │ Chart Images     │─────▶│ Base Model (Qwen2-VL-2B)                   │   │
│  │ Q&A Pairs        │      │  + LoRA Adapters (mlx_vlm.lora)            │   │
│  │ (train.jsonl)    │      │  + Unified Memory Training on Apple Silicon│   │
│  └──────────────────┘      └────────────────┬───────────────────────────┘   │
│                                             │                               │
│                                             ▼                               │
│                            ┌──────────────────────────────────────────┐     │
│                            │ Fine-Tuned LoRA Adapters                 │     │
│                            │ (./fine_tuned_adapters/)                 │     │
│                            └────────────────┬─────────────────────────┘     │
│                                             │                               │
│                                             ▼                               │
│                            ┌──────────────────────────────────────────┐     │
│                            │ 3. Merge & Export to GGUF                │     │
│                            │ (mlx_vlm.fuse + convert_hf_to_gguf.py)   │     │
│                            └────────────────┬─────────────────────────┘     │
│                                             │                               │
│                                             ▼                               │
│                            ┌──────────────────────────────────────────┐     │
│                            │ 4. Deploy with Ollama                    │     │
│                            │ ollama create my-chart-model             │     │
│                            │ (text.gguf + mmproj.gguf)                │     │
│                            └────────────────┬─────────────────────────┘     │
│                                             │                               │
│                                             ▼                               │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ 5. Update rag_engine.py                                              │   │
│  │                                                                      │   │
│  │   VISION_MODEL = "my-chart-model"  # ← Change ONE line               │   │
│  │   TEXT_MODEL   = "llama3.2:3b"     # Unchanged                       │   │
│  │                                                                      │   │
│  │   ✔ Existing app.py (from Part 2) automatically uses the new model!  │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

4. Preparación del conjunto de datos

En la etapa 4 de Preparación del conjunto de datos, defina las entradas, el responsable de la tarea y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la tarea a partir de un punto de control conocido sin tener que adivinar el estado oculto. Prefiera unidades pequeñas y verificables en lugar de scripts extensos. Cuando una tarea falla, el error debe indicar una única responsabilidad y no un proceso complicado. Cite los pasajes que realmente sustentan la respuesta. Sin citaciones, los operadores no pueden distinguir entre alucinaciones y fallos en el indexado. En la etapa 4 de Preparación del conjunto de datos, defina las entradas, el responsable de la tarea y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar la tarea a partir de un punto de control conocido sin tener que adivinar el estado oculto. Registre los tiempos de ejecución y el costo en tokens o consultas junto con los resultados funcionales. La visibilidad temprana del costo evita facturas inesperadas cuando el proceso pasa de la versión de demostración al entorno compartido.

Model sees:    (Chart image)
Model reads:   (Question)
Model learns:  (Correct answer)

Fuentes del conjunto de datos

Al trabajar en la etapa de fuentes del conjunto de datos, anote primero el contrato: entradas requeridas, señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Guarde la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de secretos y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin tener que leer todo el sistema. Mida la tasa de recuperación en un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

Preparación de datos paso a paso

Al trabajar en la etapa de preparación de datos paso a paso, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Documente junto con ello el camino óptimo y el camino de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no de mejoras posteriores. Mida el rendimiento en un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

Step 1: Create Raw Dataset       →  chart_dataset/raw_train.jsonl
Step 2: Convert to MLX Format    →  chart_dataset/train.jsonl
Step 3: Verify Dataset           →  Check that train.jsonl exists

Paso 1: Crear el conjunto de datos en bruto

Al trabajar en el Paso 1: Crear la etapa, anote primero el contrato: los datos de entrada requeridos, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación garantiza que los cambios posteriores en el código sean transparentes. Prefiera unidades pequeñas y verificables a scripts extensos. Cuando un paso falla, el fallo debe referirse a una sola responsabilidad y no a un proceso complicado. Mida el rendimiento en un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente. Al trabajar en el Paso 1: Crear la etapa, anote primero el contrato: los datos de entrada requeridos, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación garantiza que los cambios posteriores en el código sean transparentes. Registre los tiempos de ejecución y el costo en tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando se pasa de entornos de demostración a entornos compartidos.

# download_chartqa.py
from datasets import load_dataset
import json
import os

os.makedirs("chart_dataset", exist_ok=True)

# Download ChartQA dataset
dataset = load_dataset("ahmed-masry/ChartQA", split="train")

# Convert to flat format with standard keys
converted = []
for item in dataset:
    converted.append({
        "image": item["imgname"],      # Path to chart image
        "question": item["query"],
        "answer": item["label"]
    })

# Save as raw dataset
with open("chart_dataset/raw_train.jsonl", "w") as f:
    for entry in converted:
        f.write(json.dumps(entry) + "\n")

print(f"✅ Converted {len(converted)} ChartQA samples to chart_dataset/raw_train.jsonl")
README.md: 100%|█████████████████████████████████████████████| 2.12k/2.12k [00:00<00:00, 3.72MB/s]
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
data/train-00000-of-00003.parquet: downloading bytes: ████████████████████████|  213MB, 17.3MB/s
......
Generating test split: 100%|████████████████████████| 2500/2500 [00:00<00:00, 20517.87 examples/s]
✅ Converted 28299 ChartQA samples to raw_train.jsonl

Opción B: Generar datos sintéticos

La etapa de generación de datos sintéticos de la Opción B funciona mejor cuando se trata como una superficie medible. Capture una transcripción de éxito, un caso de fallo y la nota de reversión antes de ampliar el alcance. Mantenga la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de secretos y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin tener que leer todo el grafo. Separe la política de particionamiento de la política de recuperación. Cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

# generate_synthetic_data.py
import json
import matplotlib.pyplot as plt
import numpy as np
import os

os.makedirs("chart_dataset/images", exist_ok=True)

dataset = []
for i in range(100):
    categories = ['Q1', 'Q2', 'Q3', 'Q4']
    values = np.random.randint(100, 500, 4)

    plt.figure()
    plt.bar(categories, values)
    plt.title(f"Quarterly Revenue {i}")
    plt.savefig(f"chart_dataset/images/chart_{i:03d}.png")
    plt.close()

    dataset.append({
        "image": f"images/chart_{i:03d}.png",
        "question": "Which quarter had the highest revenue?",
        "answer": f"Q{np.argmax(values) + 1} with ${max(values)} million"
    })

with open("chart_dataset/raw_train.jsonl", "w") as f:
    for entry in dataset:
        f.write(json.dumps(entry) + "\n")

print("✅ Generated 100 synthetic samples in chart_dataset/raw_train.jsonl")

Paso 2: Convertir a formato MLX

La fase 2, “Convertir a etapa”, funciona mejor cuando se trata como una superficie medible. Capture un registro exitoso, un caso de fallo y la nota de reversión antes de ampliar el alcance. Documente tanto el camino óptimo como el camino de recuperación juntos. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son ajustes realizados posteriormente. Separe la política de fragmentación de la política de recuperación; cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

# convert_to_mlx_format.py
import json
import os

def convert_raw_to_mlx_format(data_dir="chart_dataset"):
    """
    Convert raw dataset to MLX format.

    Input:  chart_dataset/raw_train.jsonl
    Output: chart_dataset/train.jsonl (MLX-compatible)
    """
    raw_file = os.path.join(data_dir, "raw_train.jsonl")
    out_file = os.path.join(data_dir, "train.jsonl")

    if not os.path.exists(raw_file):
        print(f"❌ {raw_file} not found. Run download_chartqa.py or generate_synthetic_data.py first.")
        return None

    dataset = []
    with open(raw_file, "r") as f:
        for line in f:
            item = json.loads(line)

            # Build full image path
            img_str = item["image"]
            image_path = img_str if img_str.startswith(data_dir) else os.path.join(data_dir, img_str)

            dataset.append({
                "images": [image_path],  # MUST be a list
                "messages": [
                    {"role": "user", "content": item["question"]},
                    {"role": "assistant", "content": item["answer"]}
                ]
            })

    with open(out_file, "w") as f:
        for entry in dataset:
            f.write(json.dumps(entry) + "\n")

    print(f"✅ Converted {len(dataset)} samples to {out_file}")
    return dataset

if __name__ == "__main__":
    convert_raw_to_mlx_format()
python convert_to_mlx_format.py

Paso 3: Verificar el conjunto de datos

La Etapa 3: Verificar el funcionamiento es más efectiva cuando se trata como una superficie medible. Capture un registro exitoso, un caso de fallo y la nota de reversión antes de ampliar el alcance. Prefiera unidades pequeñas y verificables en lugar de scripts extensos. Cuando una etapa falla, el fallo debe apuntar a una única responsabilidad y no a un proceso complicado. Separe la política de fragmentación de la política de recuperación; cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

ls -la chart_dataset/
chart_dataset/
├── raw_train.jsonl     # Raw Q&A pairs (flat keys)
├── train.jsonl         # MLX-formatted (overwritten by prepare_mlx_dataset)
└── images/             # Chart images

📌 Important: Before running the fine-tuning command, ensure train.jsonl exists in your dataset directory.
If you see a FileNotFoundError, you haven't run the conversion step yet.

5. Ajuste fino con MLX

La etapa de Afinación 5 con MLX funciona mejor cuando se trata como una superficie medible. Capture un transcripto exitoso, un caso de fallo y la nota de reversión antes de ampliar el alcance. Considere esta etapa como un contrato entre las entradas y las salidas validadas. Asigne nombres a los artefactos, defina verificaciones de éxito y rechace las completaciones parciales silenciosas. Separe la política de fragmentación de la política de recuperación; cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

Requisitos previos

La etapa de Requisitos previos funciona mejor cuando se trata como una superficie medible. Capture un registro de éxito ejemplar, un caso de fallo y la nota de reversión antes de ampliar el alcance. Registre los tiempos y el costo de tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando el proceso pasa de la fase de demostración a entornos compartidos. Separe la política de fragmentación de la política de recuperación; cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

# For fine-tuning vision models
pip install mlx-vlm

# For merging adapters (needed after training)
pip install mlx-lm

Ejecutar el ajuste fino

La etapa de ajuste fino del proceso funciona mejor cuando se trata como una superficie medible. Capture un transcripto exitoso, un caso de fallo y la nota de reversión antes de ampliar el alcance. Mantenga la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de datos confidenciales y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin tener que leer todo el sistema. Separe la política de particionamiento de la política de recuperación. Cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

python -m mlx_vlm.lora \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --dataset ./chart_dataset/train.jsonl \
    --iters 1000 \
    --batch-size 1 \
    --lora-rank 8 \
    --gradient-accumulation-steps 4 \
    --max-seq-length 512

Comprensión de la salida del entrenamiento

La etapa de salida del entrenamiento de comprensión funciona mejor cuando se trata como una superficie medible. Capture una transcripción exitosa, un caso de fallo y la nota de reversión antes de ampliar el alcance. Documente tanto el camino óptimo como el camino de recuperación juntos. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores. Separe la política de fragmentación de la política de recuperación; cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

INFO:__main__:Loading model from Qwen/Qwen2-VL-2B-Instruct
Fetching 11 files: 100%|████████████████████████████████████████| 11/11 [00:00<00:00, 1465.47it/s]
Download complete: :                                                          |  0.00B
Reconstruction complete: |                                           |  0.00B /  0.00B
INFO:__main__:Loading dataset from ./chart_dataset/train.jsonl
INFO:__main__:Setting up LoRA
#trainable params: 9.232384 M || all params: 2208.9856 M || trainable%: 0.418%
INFO:__main__:Setting up optimizer
INFO:__main__:Training model (sft)
Starting training..., iterations: 1000
No validation dataset provided — training will run without validation.
......

Iter 120: Train loss 7.77592850, Learning Rate 2.000e-05,
It/sec 0.242, Tokens/sec 103.862, Trained Tokens 51480, Peak mem 8.069 GB
.....

Saved final adapter weights to adapters.safetensors.
INFO:__main__:Training completed! Model saved to adapters.safetensors

6. Solución de problemas

La etapa de solución de problemas número 6 funciona mejor cuando se trata como un área medible. Capture una transcripción de referencia, un caso de fallo y la nota de reversión antes de ampliar el alcance. Prefiera unidades pequeñas y verificables en lugar de scripts extensos. Cuando un paso falla, el fallo debe apuntar a una sola responsabilidad y no a un proceso complicado. Separe la política de fragmentación de la política de recuperación; cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

Error: Conjunto de datos no encontrado

La etapa del error “Conjunto de datos no encontrado” funciona mejor cuando se trata como un área medible. Capture una transcripción de referencia, un caso de fallo y la nota de reversión antes de ampliar el alcance.

FileNotFoundError: Couldn't find any data file at .../chart_dataset/train.jsonl
python convert_to_mlx_format.py

Error: Falta de memoria (OOM)

RuntimeError: [METAL] Command buffer execution failed: Insufficient Memory
python -m mlx_vlm.lora \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --dataset ./chart_dataset/train.jsonl \
    --iters 1000 \
    --batch-size 1 \              # ← Reduced from 4 to 1
    --lora-rank 4 \               # ← Reduced from 8 to 4
    --gradient-accumulation-steps 8 \  # ← Added
    --max-seq-length 256               # ← Added

Error: Ruta del adaptador no encontrada

FileNotFoundError: The adapter path does not exist: fine_tuned_adapters
ls -la adapters.safetensors adapter_config.json
python -m mlx_lm fuse \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --adapter-path . \
    --save-path ./fine_tuned_model_merged

Error: Caché incompleta

IncompleteSnapshotError: The cached snapshot for 'Qwen/Qwen2-VL-2B-Instruct' is incomplete
rm -rf ~/.cache/huggingface/hub/models--Qwen--Qwen2-VL-2B-Instruct

7. ¿Cuánto tiempo tomará el entrenamiento?

Total Time (seconds) = Total Iterations ÷ It/sec
Total Time (minutes) = Total Time (seconds) ÷ 60
1000 ÷ 0.242 = 4,132 seconds
4,132 ÷ 60 = ~69 minutes

8. Fusión de adaptadores LoRA

python -m mlx_lm fuse \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --adapter-path . \
    --save-path ./fine_tuned_model_merged

9. Exportar a GGUF para Ollama

Paso 1: Exportar MLX al formato de Hugging Face

#!/usr/bin/env python3
import mlx_lm
from mlx_lm import load, save

model, tokenizer, config = load("./fine_tuned_model_merged")
save(model, tokenizer, config, "./hf_export")
print("✅ Exported to ./hf_export")
python export_to_hf.py

Paso 2: Convertir a GGUF

# Clone llama.cpp (if not already done)
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp

# Convert text model
python convert_hf_to_gguf.py ../hf_export \
    --outfile chart_model-text.gguf \
    --outtype f16

Paso 3: Convertir el proyector de visión

python convert_hf_to_gguf.py ../hf_export \
    --outfile chart_model-mmproj.gguf \
    --outtype f16 \
    --mmproj
ls ~/.cache/huggingface/hub/models--Qwen--Qwen2-VL-2B-Instruct/snapshots/

# sample output . You'll see a hash directory (e.g., 895c3a49...)
# 895c3a49bc3fa70a340399125c650a463535e71c
cd llama.cpp
# replace <hash> with the output above (e.g., 895c3a49...)

python convert_hf_to_gguf.py ~/.cache/huggingface/hub/models--Qwen--Qwen2-VL-2B-Instruct/snapshots/<hash>/ \
    --outfile base-mmproj.gguf \
    --outtype f16 \
    --mmproj

10. Despliegue con Ollama

FROM ./chart_model-text.gguf
FROM ./chart_model-mmproj.gguf
PARAMETER temperature 0.2

Crear el modelo

ollama create my-chart-model -f Modelfile

Verificar el modelo

ollama list

# output :

# NAME                     ID              SIZE      MODIFIED
# my-chart-model:latest    b3fd6d8fd742    4.4 GB    7 hours ago
# qwen2.5vl:3b             fb90415cde1e    3.2 GB    9 days ago
# llama3.2:3b              a80c4f17acd5    2.0 GB    10 days ago
# llama3:latest            365c0bd3c000    4.7 GB    3 months ago

# You should see my-chart-model in the list.

Probar el modelo

ollama run my-chart-model "What is 2+2?"

11. Integrar en su aplicación RAG existente

# In rag_engine.py (from Parts 1 & 2)

# Before:
VISION_MODEL = "qwen2.5vl:3b"

# After:
VISION_MODEL = "my-chart-model"  # Your fine-tuned model
TEXT_MODEL = "llama3.2:3b"       # Unchanged
python app.py

12. Evaluación: modelo base vs. modelado fino

Comparación

13. Conclusión

Obtener el código completo

git clone https://github.com/froilan-sia/m1_multimodal_rag.git
cd m1_multimodal_rag

Conectarse conmigo

Lista de verificación operativa

Lecturas relacionadas