Ir al contenido principal

Gradient checkpointing: ajuste eficiente con Unsloth y NVIDIA

Aprende cómo el gradient checkpointing optimizado para NVIDIA de Unsloth y QLoRA reducen la VRAM al ajustar Qwen3.5 4B para OCR de documentos médicos.
Actualizado 14 ago 2026  · 12 min leer

Explorar con IA

ChatGPTClaudePerplexity

La colaboración de Unsloth con NVIDIA se centra en acelerar el ajuste fino reduciendo sobrecargas ocultas del entrenamiento. En lugar de depender solo de GPUs más grandes o modelos más pequeños, las mejoras atacan cuellos de botella dentro del propio proceso de entrenamiento, como la construcción repetida de metadatos, los retrasos al recargar activaciones y el enrutamiento ineficiente de tokens. Para ti, esto se traduce en entrenamientos más rápidos y un flujo de trabajo de ajuste fino más fluido en GPUs NVIDIA compatibles.

En esta guía, veremos estas nuevas mejoras de rendimiento de Unsloth y aplicaremos su flujo de trabajo de ajuste fino optimizado a una tarea práctica de visión y lenguaje. Ajustaremos Qwen3.5 Vision 4B para OCR médico, donde el modelo aprende a extraer texto estructurado de imágenes de documentos médicos usando un pequeño subconjunto de un dataset de OCR con aspecto médico.

Usaremos:

  • Qwen3.5 4B (Vision) como modelo base
  • QLoRA en 4 bits para reducir el uso de VRAM
  • Adaptadores LoRA para un ajuste fino eficiente
  • Gradient checkpointing de Unsloth para ahorrar memoria durante el entrenamiento
  • Un subconjunto de 300 muestras de un dataset de OCR médico
  • Preprocesado de imágenes a tamaño fijo para un entrenamiento de visión más estable
  • Evaluación antes y después para comparar las salidas del modelo base y del ajustado

Uso del flujo de trabajo de ajuste fino optimizado para NVIDIA de Unsloth

Antes de empezar a ajustar, conviene entender qué aporta la colaboración de Unsloth con NVIDIA y cómo conecta con esta guía.

Unsloth indica que su colaboración con NVIDIA hace que el entrenamiento de LLMs sea alrededor de un 25% más rápido, sin pérdida de precisión, además de sus aceleraciones existentes de 2 a 5 veces en ajuste fino. Estas mejoras provienen de reducir sobrecargas ocultas alrededor del proceso principal de entrenamiento, en lugar de cambiar el objetivo de aprendizaje del modelo. En otras palabras, el objetivo es hacer el ajuste fino más rápido y eficiente manteniendo la precisión.

Source: How to Make LLM Training Faster with Unsloth and NVIDIA

Fuente: How to Make LLM Training Faster with Unsloth and NVIDIA 

¿Quieres iniciarte en la IA Generativa?

Aprende a trabajar con LLMs en Python directamente en tu navegador

Empieza Ahora

Rendimiento de entrenamiento mejorado

La colaboración recoge varias mejoras de rendimiento, entre ellas:

  • 14,3% más rápido por lote en un benchmark Qwen3-14B QLoRA SFT gracias a la caché de metadatos de secuencias empaquetadas
  • Un 8,4% de aceleración en modelos de 8B, 6,7% en 14B y 4,6% en 32B con gradient checkpointing asíncrono con doble búfer
  • Alrededor de un 10–15% de aceleración en el entrenamiento MoE de GPT-OSS, con un 23% más rápido en forward y 13% más rápido en backward en la ruta de enrutamiento objetivo

Las mayores ganancias se dan en entrenamiento de texto empaquetado y modelos Mixture-of-Experts. No los usamos aquí porque nuestro flujo se centra en el ajuste OCR con Qwen3.5 Vision. 

En esta guía usamos una GPU NVIDIA RTX 3090, así que el flujo se apoya en la aceleración de GPU NVIDIA y el camino de ajuste optimizado de Unsloth. No estamos comparando Unsloth con otro entrenador; por tanto, no debe interpretarse como una prueba independiente de las aceleraciones reportadas. Aquí aplicamos el flujo optimizado de Unsloth a una tarea real de visión y lenguaje.

Gradient checkpointing

Para este flujo, la optimización más relevante es el gradient checkpointing de Unsloth. Ayuda a reducir el uso de memoria durante el entrenamiento evitando almacenar todas las activaciones en la GPU. Es especialmente útil en ajuste de modelos de visión y lenguaje, donde el modelo procesa imágenes y texto.

1. Configurar Unsloth para un ajuste más rápido

Para seguir esta guía, necesitas acceso a una GPU NVIDIA. Puedes alquilar una en plataformas como RunPod, Vast.ai u otros proveedores de GPU en la nube. Inicialmente probé RunPod porque suele ser rápido y fiable, pero en ese momento había pocas opciones de RTX 3090. Por eso usé una máquina con GPU RTX 3090 en Vast.ai para este flujo. 

Para comparar plataformas, echa un vistazo a nuestra guía de los mejores proveedores de GPU en la nube.

Vast.ai RTX 3090 instance

Fuente:  Vast.ai | Console 

Tras lanzar la instancia, abrí Jupyter Notebook y creé un cuaderno nuevo. En Vast.ai seleccioné el kernel de entorno principal disponible para instalar los paquetes de Python necesarios en el entorno del cuaderno sin afectar a dependencias del sistema. 

Instalar los paquetes necesarios

Primero, instala los paquetes para Unsloth, PyTorch, entrenamiento de modelos de visión, carga de datasets e integración con Hugging Face: 

!pip install --upgrade \
    "torch>=2.8.0" "triton>=3.4.0" \
    numpy pillow torchvision bitsandbytes \
    unsloth "unsloth_zoo>=2026.4.6" \
    "datasets>=4.0.0" huggingface_hub hf_transfer pandas \
    transformers==5.2.0 torchcodec timm

Estos paquetes siguen la configuración oficial del notebook de Unsloth e incluyen las librerías necesarias para cargar Qwen3.5 Vision, preparar datos imagen-texto y ajustar el modelo con Unsloth. 

Configurar el dispositivo CUDA

Después configuramos el dispositivo CUDA y verificamos que la GPU NVIDIA correcta está disponible. Como esta guía usa una RTX 3090, el código comprueba si CUDA está habilitado, confirma la GPU seleccionada, imprime las versiones de CUDA y PyTorch, y verifica que la máquina tiene VRAM suficiente para este experimento. 

import os
import platform

CUDA_DEVICE_INDEX = 0
TARGET_GPU_NAME = "3090"

# Must be set before CUDA / Unsloth are initialized. Restart the kernel if you change these.
os.environ["CUDA_VISIBLE_DEVICES"] = str(CUDA_DEVICE_INDEX)

# RunPod + Qwen3.5 Vision OCR can hit Torch Dynamo fullgraph recompile limits.
# This disables Unsloth's torch.compile path while keeping Unsloth model loading,
# LoRA, gradient checkpointing, collator, and 8-bit optimizer benefits.
os.environ["UNSLOTH_COMPILE_DISABLE"] = "1"
os.environ["TORCH_COMPILE_DISABLE"] = "1"

import torch

DEVICE = torch.device("cuda:0")

print("Python:", platform.python_version())
print("PyTorch:", torch.__version__)
print("CUDA available:", torch.cuda.is_available())

if not torch.cuda.is_available():
    raise RuntimeError("CUDA is not available. Select a GPU instance before continuing.")

torch.cuda.set_device(0)
props = torch.cuda.get_device_properties(0)
gpu_name = torch.cuda.get_device_name(0)
total_gpu_memory_gb = props.total_memory / 1024**3

print("Selected device:", DEVICE)
print("GPU:", gpu_name)
print("CUDA version:", torch.version.cuda)
print("BF16 supported:", torch.cuda.is_bf16_supported())
print("Total GPU memory:", round(total_gpu_memory_gb, 2), "GB")

if TARGET_GPU_NAME not in gpu_name:
    raise RuntimeError(f"Expected an RTX {TARGET_GPU_NAME}, but CUDA device 0 is: {gpu_name}")

if total_gpu_memory_gb < 20:
    raise RuntimeError(f"Expected a 24 GB class 3090, but only found {total_gpu_memory_gb:.2f} GB VRAM.")

En mi caso, el entorno devolvió la siguiente configuración de GPU:

Python: 3.12.13
PyTorch: 2.12.0+cu130
CUDA available: True
Selected device: cuda:0
GPU: NVIDIA GeForce RTX 3090
CUDA version: 13.0
BF16 supported: True
Total GPU memory: 23.56 GB

Esto confirma que el cuaderno se está ejecutando en una NVIDIA GeForce RTX 3090 con VRAM suficiente para el experimento de ajuste fino.

Definir ajustes de entrenamiento y prompts

Tras verificar la GPU, definimos el modelo, el dataset, los ajustes de entrenamiento, los directorios de salida, el tamaño de imagen y los prompts de OCR.

MODEL_NAME = "unsloth/Qwen3.5-4B"
DATASET_NAME = "naazimsnh02/medocr-vision-dataset"

SAMPLE_COUNT = 300
EVAL_INDEX = 0
MAX_LENGTH = 4096
MAX_STEPS = 30

PER_DEVICE_BATCH_SIZE = 4
GRADIENT_ACCUMULATION_STEPS = 2
LEARNING_RATE = 2e-4
SEED = 3407

OUTPUT_DIR = "outputs/qwen35_vision_medical_ocr"
ADAPTER_DIR = "qwen35-vision-medical-ocr-lora"

# Medical document images vary heavily in size. Fixed-size canvases avoid
# repeated Torch Dynamo recompiles during vision training.
# 768x1024 is a practical portrait-page compromise for a 24 GB 3090 smoke test.
FIXED_IMAGE_SIZE = (768, 1024)

# Official Unsloth Qwen3.5 Vision notebook uses False here for 16-bit LoRA.
# Set True only if you hit VRAM limits.
LOAD_IN_4BIT = True

SYSTEM_PROMPT = "You are a medical OCR transcription engine. Return only the exact text visible in the medical document image."
INSTRUCTION = "Extract all readable text from this medical document exactly. Preserve structure when possible. Return only the OCR text, with no explanation, no diagnosis, no medical advice, and no reasoning."

Aquí usamos Qwen3.5 Vision 4B de Unsloth y el dataset de visión para OCR médico. Para esta guía, seleccionamos 300 muestras y entrenamos durante 30 pasos, lo que mantiene la ejecución ligera y muestra cómo el modelo se adapta al formato de OCR objetivo.

El tamaño fijo de imagen de 768×1024 ayuda a mantener entradas coherentes durante el entrenamiento. Los documentos médicos pueden variar mucho en resolución y proporción, así que redimensionarlos a un lienzo fijo hace el flujo más estable y reduce problemas de formas durante el ajuste de visión y lenguaje.

2. Cargar el modelo

Con el entorno listo, cargamos el modelo Qwen3.5 Vision 4B usando el FastVisionModel de Unsloth

import unsloth
from unsloth import FastVisionModel
torch.cuda.set_device(0)

model, tokenizer = FastVisionModel.from_pretrained(
    MODEL_NAME,
    load_in_4bit=LOAD_IN_4BIT,
    use_gradient_checkpointing="unsloth",
)

print("Loaded:", MODEL_NAME)
print("4-bit:", LOAD_IN_4BIT)
print("Model device:", next(model.parameters()).device)

Tras cargar el modelo, la salida confirma que se ha cargado el modelo correcto, que el modo en 4 bits está activo y que el modelo está en la GPU: 

Loaded: unsloth/Qwen3.5-4B
4-bit: True
Model device: cuda:0

Aquí, FastVisionModel.from_pretrained() carga el modelo de visión y lenguaje y aplica las optimizaciones de Unsloth para un ajuste fino más rápido y con menor uso de memoria. También activamos load_in_4bit, que reduce la VRAM cargando el modelo en precisión de 4 bits. Es útil con GPUs de 24 GB como la RTX 3090.

También activamos el gradient checkpointing de Unsloth con use_gradient_checkpointing="unsloth"

Esto ayuda a reducir la memoria durante el entrenamiento, algo clave en modelos de visión y lenguaje porque procesan entradas de imagen y texto. 

3. Añadir adaptadores LoRA

A continuación, añadimos adaptadores LoRA al modelo. LoRA permite ajustar un conjunto más pequeño de parámetros entrenables en lugar de actualizar el modelo completo. Esto hace el entrenamiento más rápido, eficiente en memoria y fácil de ejecutar en una sola GPU. 

 
model = FastVisionModel.get_peft_model(
    model,
    finetune_vision_layers=True,
    finetune_language_layers=True,
    finetune_attention_modules=True,
    finetune_mlp_modules=True,
    r=16,
    lora_alpha=16,
    lora_dropout=0,
    bias="none",
    random_state=SEED,
    use_rslora=False,
    loftq_config=None,
)

En esta guía, los adaptadores se añaden tanto a la parte de visión como a la de lenguaje. Esto ayuda al modelo a aprender a leer imágenes de documentos médicos y producir el texto estructurado esperado. Tras este paso, el modelo está listo para entrenarse con el dataset de OCR médico. 

4. Cargar el dataset de OCR médico

Ahora cargamos el dataset de OCR médico de Hugging Face y preparamos un subconjunto pequeño para el ajuste.

from datasets import load_dataset
from PIL import Image

raw_dataset = load_dataset(DATASET_NAME, split="train")

MEDICAL_KEYWORDS = [
    "doctor", "dr.", "clinic", "hospital", "patient", "medication",
    "medications", "prescription", "signature", "department", "report",
    "diagnosis", "lab", "laboratory", "blood", "hemoglobin", "mg", "dose",
    "<s_ocr>",
]

El dataset contiene imágenes de documentos y su texto OCR correspondiente. Como aquí solo queremos ejemplos con estilo médico, filtramos con un enfoque sencillo basado en palabras clave. El código busca términos habituales en documentos médicos: doctor, clínica, paciente, medicación, receta, diagnóstico y términos de dosis, entre otros. 

def looks_medical(sample):
    text = str(sample.get("text", "")).lower()
    return any(keyword in text for keyword in MEDICAL_KEYWORDS)

medical_indices = []
for idx, sample in enumerate(raw_dataset):
    if looks_medical(sample):
        medical_indices.append(idx)
        if len(medical_indices) >= SAMPLE_COUNT:
            break

if not medical_indices:
    raise RuntimeError("No medical-looking OCR samples found. Broaden MEDICAL_KEYWORDS or inspect the dataset text field.")

print(f"Selected {len(medical_indices)} medical-looking samples.")

Esto nos da una forma ligera de seleccionar ejemplos relevantes para la tarea de OCR médico. Para esta ejecución, escogemos 300 muestras con apariencia médica.

Después, normalizamos cada imagen a un lienzo fijo de 768×1024. Como las imágenes varían en tamaño y proporción, este paso ayuda a hacer los datos de entrenamiento más consistentes. La imagen se redimensiona manteniendo su proporción y se coloca sobre un fondo blanco. 

def normalize_ocr_image(image, size=FIXED_IMAGE_SIZE):
    image = image.convert("RGB")
    target_w, target_h = size
    scale = min(target_w / image.width, target_h / image.height)
    new_w = max(1, int(image.width * scale))
    new_h = max(1, int(image.height * scale))
    resized = image.resize((new_w, new_h), Image.Resampling.LANCZOS)

    canvas = Image.new("RGB", size, "white")
    left = (target_w - new_w) // 2
    top = (target_h - new_h) // 2
    canvas.paste(resized, (left, top))
    return canvas

En lugar de usar datasets.map, construimos una lista simple en Python. Así evitamos bloqueos ocasionales en algunos notebooks en la nube al reescribir imágenes de PIL. 

dataset = []
for idx in medical_indices:
    sample = raw_dataset[idx]
    dataset.append(
        {
            "image": normalize_ocr_image(sample["image"]),
            "text": sample["text"],
        }
    )

print("Examples:", len(dataset))
print("Columns:", list(dataset[0].keys()))
print("Fixed image size:", dataset[EVAL_INDEX]["image"].size)
print("Sample text:", dataset[EVAL_INDEX]["text"])

Tras el preprocesado, cada ejemplo contiene dos campos: la imagen normalizada y el texto OCR objetivo. 

Examples: 300
Columns: ['image', 'text']
Fixed image size: (768, 1024)
Sample text: <s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

También podemos previsualizar uno de los ejemplos redimensionados:

dataset[EVAL_INDEX]["image"].resize((384, 512))

La vista previa muestra un documento de estilo médico con datos de clínica, nombre del doctor, información del paciente, medicación y firma. Confirma que el dataset es adecuado para el ajuste de OCR. 

Doctors prescription.

5. Convertir muestras a conversaciones de visión

Con el dataset cargado y las imágenes normalizadas, necesitamos convertir cada ejemplo al formato de conversación que espera Qwen3.5 Vision.

Cada muestra de entrenamiento debe incluir tres partes:

  • Un mensaje del sistema que define el rol del modelo como motor de transcripción OCR médica
  • Un mensaje de usuario que contiene la imagen y la instrucción de OCR
  • Un mensaje del asistente con la salida OCR esperada
def build_ocr_messages(image=None, target_text=None, instruction=INSTRUCTION):
    user_content = [
        {"type": "image"},
        {"type": "text", "text": instruction},
    ]

    if image is not None:
        user_content[0]["image"] = image

    messages = [
        {"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
        {"role": "user", "content": user_content},
    ]

    if target_text is not None:
        messages.append(
            {
                "role": "assistant",
                "content": [{"type": "text", "text": target_text}],
            }
        )

    return messages

La función anterior crea la estructura de mensajes para entrenamiento e inferencia. En entrenamiento, incluimos el texto OCR objetivo como respuesta del asistente. En inferencia, solo aportamos imagen e instrucción y pedimos al modelo que genere el texto OCR.

A continuación, convertimos cada muestra del dataset a este formato de conversación:

def convert_to_conversation(sample):
    return {
        "messages": build_ocr_messages(
            image=sample["image"],
            target_text=sample["text"],
        )
    }


converted_dataset = [convert_to_conversation(sample) for sample in dataset]
converted_dataset[0]

Tras la conversión, cada muestra contiene una lista de mensajes. El primer ejemplo incluye el prompt del sistema, la imagen del documento médico, la instrucción de OCR y la transcripción estructurada esperada. Este formato permite al modelo aprender a mapear una imagen y una instrucción al texto correcto. 

{'messages': [{'role': 'system',
   'content': [{'type': 'text',
     'text': 'You are a medical OCR transcription engine. Return only the exact text visible in the medical document image.'}]},
  {'role': 'user',
   'content': [{'type': 'image',
     'image': <PIL.Image.Image image mode=RGB size=768x1024>},
    {'type': 'text',
     'text': 'Extract all readable text from this medical document exactly. Preserve structure when possible. Return only the OCR text, with no explanation, no diagnosis, no medical advice, and no reasoning.'}]},
  {'role': 'assistant',
   'content': [{'type': 'text',
     'text': '<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>'}]}]}

6. Evaluar el modelo base antes del ajuste

Antes de entrenar, conviene probar el modelo base en un ejemplo de OCR. Así tendremos un punto de referencia para comparar las salidas antes y después del ajuste.

Primero definimos una función auxiliar para aplicar la plantilla de chat del modelo. Algunas versiones del tokenizador soportan enable_thinking=False y otras no, por lo que incluimos una alternativa para mantener la compatibilidad.

def render_ocr_chat_template(tokenizer, messages):
    try:
        return tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
            enable_thinking=False,
        )
    except TypeError:
        return tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
        )

Luego definimos la función de generación. Construye el prompt de OCR, pasa la imagen y la instrucción de texto al tokenizador, genera la salida del modelo y decodifica solo los tokens nuevos. 

def generate_ocr_text(model, tokenizer, image, instruction=INSTRUCTION, max_new_tokens=512):
    messages = build_ocr_messages(instruction=instruction)
    input_text = render_ocr_chat_template(tokenizer, messages)
    inputs = tokenizer(
        images=image,
        text=input_text,
        add_special_tokens=False,
        return_tensors="pt",
    ).to(DEVICE)

    with torch.inference_mode():
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            use_cache=True,
            do_sample=False,
            temperature=None,
            top_p=None,
        )

    prompt_length = inputs["input_ids"].shape[-1]
    generated_tokens = outputs[:, prompt_length:]
    return tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]

Cambiamos el modelo a modo inferencia y generamos el texto OCR para la primera imagen de evaluación:

FastVisionModel.for_inference(model)
eval_image = dataset[EVAL_INDEX]["image"]
base_output = generate_ocr_text(model, tokenizer, eval_image)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nBase model output:")
print(base_output)

La salida del modelo base es legible, pero no sigue exactamente la estructura objetivo:

Target:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Base model output:
Meadowview Health
45 Oak Ave.
Prescribed by: Dr. A. Smith
Date: 2024-12-16
Patient: John Doe, Age: 35
Hydrochlorothiazide 25 mg - Before meals
Signature: Dr. A. Smith

Es un buen punto de partida. El modelo base ya puede leer gran parte del documento, pero devuelve el texto en un estilo OCR natural en lugar de seguir el formato estructurado del dataset. El ajuste fino debería alinear la salida con el formato objetivo y hacerla más consistente.

7. Entrenar el modelo

Con el dataset en el formato de conversación de visión, podemos entrenar el modelo usando SFTTrainer de TRL con el data collator de visión de Unsloth. 

from unsloth.trainer import UnslothVisionDataCollator
from trl import SFTTrainer, SFTConfig

FastVisionModel.for_training(model)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    data_collator=UnslothVisionDataCollator(model, tokenizer),
    train_dataset=converted_dataset,
    args=SFTConfig(
        per_device_train_batch_size=PER_DEVICE_BATCH_SIZE,
        gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
        warmup_steps=5,
        max_steps=MAX_STEPS,
        learning_rate=LEARNING_RATE,
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.001,
        lr_scheduler_type="linear",
        seed=SEED,
        output_dir=OUTPUT_DIR,
        report_to="none",
        remove_unused_columns=False,
        dataset_text_field="",
        dataset_kwargs={"skip_prepare_dataset": True},
        max_length=MAX_LENGTH,
    ),
)
trainer_stats = trainer.train()

Primero, pasamos el modelo a modo entrenamiento con FastVisionModel.for_training(model). Luego creamos el entrenador usando el dataset de OCR convertido.

La clave aquí es el UnslothVisionDataCollator. Como es una tarea multimodal, el entrenador debe manejar correctamente las imágenes de documentos médicos y el texto OCR objetivo. El collator prepara estos ejemplos para pasarlos al modelo durante el ajuste supervisado.

En esta guía entrenamos 30 pasos con un batch por dispositivo de 4 y acumulación de gradiente de 2, dando un batch efectivo de 8. Mantiene la ejecución ligera a la vez que muestra cómo el modelo empieza a adaptarse al formato de OCR estructurado.

Fine-tuning the Qwen 3.5 4b Vision language model

Durante el entrenamiento, Unsloth imprime información útil sobre la configuración: número de ejemplos, pasos y lotes, parámetros entrenables y funciones de ahorro de memoria. En esta ejecución, Unsloth indica que el doble búfer está habilitado para la pasada hacia atrás, lo que reduce esperas durante el gradient checkpointing.

8. Evaluar el modelo ajustado

Tras el entrenamiento, volvemos a poner el modelo en modo inferencia y generamos el texto OCR para la misma imagen de evaluación utilizada antes.

FastVisionModel.for_inference(model)
fine_tuned_output = generate_ocr_text(model, tokenizer, eval_image)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nBase model output:")
print(base_output)
print("\nFine-tuned output:")
print(fine_tuned_output)

Después del ajuste fino, la salida del modelo se acerca mucho más a la estructura objetivo del dataset:

Target:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Base model output:
Meadowview Health
45 Oak Ave.
Prescribed by: Dr. A. Smith
Date: 2024-12-16
Patient: John Doe, Age: 35
Hydrochlorothiazide 25 mg - Before meals
Signature: Dr. A. Smith


Fine-tuned output:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Esto muestra que el modelo ajustado ha aprendido el formato de respuesta esperado. El modelo base ya extraía la mayoría del texto visible, pero el ajuste ayudó a alinear la salida con el formato estructurado de los datos de entrenamiento.

Podemos probar el modelo con otro ejemplo del dataset:

EVAL_INDEX_2 = 35
eval_image_2 = dataset[EVAL_INDEX_2]["image"]

fine_tuned_output = generate_ocr_text(model, tokenizer, eval_image_2)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nFine-tuned output:")
print(fine_tuned_output)

En este segundo ejemplo, el modelo respeta la estructura, pero comete un pequeño error de OCR al generar Amoxicillin en lugar de Amlodipine:

Target:
<s_ocr> doctor_name: Dr. C. Rossi clinic_name: Riverside Clinic clinic_address: 45 Oak Ave. patient_name: Wei Li patient_age: 70 date: 2024-12-16 medications: - Acetaminophen 20 mg - Take twice daily - Amlodipine 20 mg - After meals signature: Dr. C. Rossi </s>

Fine-tuned output:
<s_ocr> doctor_name: Dr. C. Rossi clinic_name: Riverside Clinic clinic_address: 45 Oak Ave. patient_name: Wei Li patient_age: 70 date: 2024-12-16 medications: - Acetaminophen 20 mg - Take twice daily - Amoxicillin 20 mg - After meals signature: Dr. C. Rossi </s>

Es un recordatorio útil: el modelo mejora en la alineación de formato, pero la precisión de OCR sigue dependiendo de la calidad y claridad de los datos, el tamaño del entrenamiento y el número de pasos. Para un sistema de producción, entrenarías con un dataset mayor y más diverso y evaluarías la precisión en un rango amplio de ejemplos.

9. Guardar el adaptador ajustado

Cuando termina el entrenamiento, guardamos localmente el adaptador LoRA y el tokenizador.

model.save_pretrained(ADAPTER_DIR)
tokenizer.save_pretrained(ADAPTER_DIR)

print("Saved adapter to:", ADAPTER_DIR)

La salida confirma que el adaptador se ha guardado:

Saved adapter to: qwen35-vision-medical-ocr-lora

Esto guarda solo los pesos del adaptador ajustado, no una copia completa del modelo base. Más adelante podrás recargar el modelo base Qwen3.5-4B y aplicar este adaptador para reutilizar el comportamiento OCR ajustado. Así el modelo guardado es ligero y fácil de almacenar, compartir o desplegar.

Reflexiones finales

El proceso de entrenamiento fue ligero y práctico en una sola NVIDIA RTX 3090. Aunque el ajuste de visión y lenguaje suele ser intensivo en memoria, el uso de VRAM fue menor de lo esperado. El máximo rondó los 14 GB y la media estuvo más cerca de 9 GB, algo notable para ajustar un modelo Qwen3.5 Vision.

El modelo también se adaptó rápido. Tras pocos pasos, la salida se acercó mucho al formato de OCR objetivo. El modelo base ya leía el documento, pero después del ajuste seguía el formato del dataset con más consistencia. 

Dicho esto, la experiencia de instalación no fue perfecta. Instalar Unsloth requirió bastante prueba y error. Puede ser difícil de configurar, sobre todo cambiando entre entornos locales, entornos virtuales, versiones de CUDA y proveedores de GPU en la nube. 

En algunos casos, problemas de compatibilidad con CUDA pueden romper el entorno y depurarlos lleva más tiempo del previsto. Incluso empezar con una imagen de Docker de Unsloth en la nube puede ser costoso en tiempo si el entorno no funciona limpio desde el principio.

Otra lección importante es que la plantilla del modelo importa. Si el dataset no se convierte al formato correcto de chat o conversación de visión, el modelo puede no aprender bien. En Qwen3.5 Vision, usar la estructura de mensajes imagen-texto adecuada es esencial. Sin la plantilla correcta, el entrenamiento puede ejecutarse, pero el modelo quizá no se adapte realmente a la tarea.

En conjunto, Unsloth es una gran opción si tienes acceso limitado a GPU y quieres ajustar modelos de forma eficiente en máquinas locales o GPUs alquiladas. Reduce el uso de memoria, hace que hardware más modesto rinda mejor y puede acelerar la experimentación. Sin embargo, si ajustas y entrenas modelos con frecuencia, la complejidad de la instalación puede resultar frustrante. El entrenamiento estándar con Transformers suele ser más estable, fácil de instalar y sencillo de reproducir entre entornos.

Si lo que te frena es la instalación, te recomiendo leer nuestra guía de Unsloth Studio, donde verás cómo ajustar Qwen3.5-9B sin configuración manual del entorno en la interfaz web local de Unsloth.


Abid Ali Awan's photo
Author
Abid Ali Awan
LinkedIn
Twitter

Soy un científico de datos certificado que disfruta creando aplicaciones de aprendizaje automático y escribiendo blogs sobre ciencia de datos. Actualmente me centro en la creación de contenidos, la edición y el trabajo con grandes modelos lingüísticos.

Temas

Los mejores cursos de IA

Curso

Introducción al aprendizaje profundo con PyTorch

4 h
88.5K
Aprende a crear tu primera red neuronal, ajustar hiperparámetros y abordar problemas de clasificación y regresión en PyTorch.
Ver detallesRight Arrow
Iniciar Curso
Ver másRight Arrow
Relacionado
An avian AI exits its cage

blog

12 alternativas de código abierto a GPT-4

Alternativas de código abierto a GPT-4 que pueden ofrecer un rendimiento similar y requieren menos recursos informáticos para funcionar. Estos proyectos vienen con instrucciones, fuentes de código, pesos del modelo, conjuntos de datos e IU de chatbot.
Abid Ali Awan's photo

Abid Ali Awan

9 min

Tutorial

Cómo ajustar GPT 3.5: Liberar todo el potencial de la IA

Explore GPT-3.5 Turbo y descubra el potencial transformador del ajuste fino. Aprenda a personalizar este modelo de lenguaje avanzado para aplicaciones especializadas, mejore su rendimiento y comprenda los costes asociados, la seguridad y las consideraciones de privacidad.
Moez Ali's photo

Moez Ali

Tutorial

Tutorial de DeepChecks: Automatizar las pruebas de aprendizaje automático

Aprende a realizar la validación de datos y modelos para garantizar un sólido rendimiento del aprendizaje automático utilizando nuestra guía paso a paso para automatizar las pruebas con DeepChecks.
Abid Ali Awan's photo

Abid Ali Awan

Tutorial

Ajuste fino de GPT-3 mediante la API OpenAI y Python

Libere todo el potencial de GPT-3 mediante el ajuste fino. Aprenda a utilizar la API de OpenAI y Python para mejorar este modelo de red neuronal avanzado para su caso de uso específico.
Zoumana Keita 's photo

Zoumana Keita

Tutorial

Visión GPT-4: Guía completa para principiantes

Este tutorial le presentará todo lo que necesita saber sobre GPT-4 Vision, desde cómo acceder a él hasta ejemplos prácticos del mundo real y sus limitaciones.
Arunn Thevapalan's photo

Arunn Thevapalan

Tutorial

Ajuste fino de SAM 2 en un conjunto de datos personalizado: Tutorial

Aprende a afinar SAM 2 de Meta AI utilizando el conjunto de datos de segmentación de TC torácica para mejorar el rendimiento de segmentación de imágenes del modelo en el análisis de imágenes médicas.
Aashi Dutt's photo

Aashi Dutt

Ver MásVer Más