Blog/8 de junio de 2026

Fine-tuning supervisado de LFM2.5-VL-450M para detección de incendios aéreos

Flujo completo de fine-tuning supervisado (SFT) de un Vision-Language Model (LFM2.5-VL-450M) sobre imágenes aéreas de incendios del dataset FLAME 3 Sycan Marsh: dataset, labels, entrenamiento, evaluación y análisis de errores.

Fine-tuning supervisado de LFM2.5-VL-450M para detección de incendios aéreos

Fine-tuning supervisado de LFM2.5-VL-450M para detección de incendios aéreos

Este informe presenta un flujo completo de fine-tuning supervisado (SFT) de un Vision-Language Model aplicado a imágenes aéreas de incendios dentro del dataset FLAME 3 Sycan Marsh. El objetivo es que ingenieros de ML y CV comprendan el pipeline, puedan reproducirlo y aplicar mejoras basadas en un análisis de errores detallado.


1. Contexto / Por qué importa

  • Problema: adaptar un modelo visión‑lenguaje a una tarea de monitoreo de incendios en FLAME 3, concretamente para generar salidas estructuradas (JSON) a partir de pares de imágenes RGB y térmicas.
  • Relevancia técnica: la SFT puede mejorar la precisión en campos estructurados críticos como fire_present, thermal_hotspot_intensity, fire_size, smoke_visible y image_quality_limited. El uso de SFT frente a enfoques ligeros u otros paradigmas (p. ej., LoRA) implica trade-offs de recursos de entrenamiento, tamaño del dataset y complejidad de exportación de salidas.
  • Alcance técnico: el flujo cubre preparación de dataset, generación de labels, construcción de train.jsonl, entrenamiento con I/O estructurado, evaluación en test set y análisis de errores, con foco en generalización y robustez frente a condiciones visuales variadas (RGB corregido, térmico IR, calidad de imagen).

Notas importantes:

  • El artículo asume que el lector entiende conceptos de entrenamiento de modelos multimodales, puntuación de pérdidas y evaluación de precisión en tareas de salida estructurada.
  • No se introduce código innecesario; cada fragmento sirve para ilustrar un paso reproducible.

2. Conceptos clave / Cómo funciona

  • Modelo mental: el flujo de post-entrenamiento con SFT para un VLM recibe como entrada RGB + Thermal + prompt y devuelve un JSON estructurado con cinco campos de análisis de incendio. La pérdida se calcula sobre tokens del assistant, no sobre system ni user.
  • Idea central: cada ejemplo de entrenamiento es un bloque de mensajes (system → user → assistant) que genera una salida JSON. Se utiliza un entrenamiento completo sin LoRA para la versión base LFM2.5-VL-450M.
  • Flujo general (alto nivel): datos → generación de labels → splits estratificado 80/20 → train.jsonl (formato para SFT) → entrenamiento SFT → evaluación en test set → análisis de resultados.

A continuación se presenta el diagrama del flujo general del pipeline de SFT para FLAME 3.

Notas sobre el diagrama:

  • Los nodos describen etapas con roles y artefactos claros (train.jsonl, label generation, etc.).
  • Se observa un pipeline lineal, con una salida de evaluación que contrasta base vs fine-tuned.

3. Secciones técnicas centrales

3.1 Preparación de datos y generación de labels

Objetivo: definir la estructura de entrada y el pipeline de generación de labels a partir de datos brutos para producir train.jsonl con 5 campos por muestra.

  • Estructura de entradas de cada muestra:
    • RGB corrected (640×512 px)
    • Thermal (640×512 px, formato JPEG bruta)
    • Celsius TIFF (ground truth térmico)
    • Salida esperada: un JSON con los campos:
      • fire_present
      • thermal_hotspot_intensity
      • fire_size
      • smoke_visible
      • image_quality_limited
  • Proceso de split estratificado 80/20:
    • Train: 591 escenas (con 498 Fire / 93 No Fire)
    • Test: 147 escenas (con 124 Fire / 23 No Fire)
  • Formato train.jsonl para entrenamiento:
    • Cada línea corresponde a una muestra con el formato de multi‑turnos (system, user, assistant) tal como en el ejemplo de la sección 0.
  • Fragmentos de código prácticos

Lectura y creación de train.jsonl (fragmento Python)

import json
from pathlib import Path

ROOT = Path(".")
FT_DIR = ROOT / "flame3_finetune"
DATA_DIR = FT_DIR / "data"

# Supuesto: generate_labels.py ya produjo labels a partir de Celsius TIFF
train_path = DATA_DIR / "train.jsonl"
train_rows = [json.loads(l) for l in train_path.read_text().splitlines() if l.strip()]

# Mostrar una muestra
sample = train_rows[0]
print("Muestra de train.jsonl (estructura):")
print(json.dumps(sample, indent=2)[:800])

Lectura de labels (ejemplo de extracción de ground truth)

# Extraer JSON de salida esperada (ground truth) para una muestra
gt_json = json.loads(sample["messages"][2]["content"][0]["text"])
print("Campos ground truth:", list(gt_json.keys()))

Formato de entrenamiento esperado en cada muestra (resumen)

  • ROLE: system
    • Texto: “Eres un asistente de visión para monitoreo de incendios…”
  • ROLE: user
    • Contenido: [RGB], [Thermal], y breve prompt textual
  • ROLE: assistant
    • Contenido: JSON con los 5 campos

Notas operativas:

  • La pérdida se calcula únicamente sobre los tokens del assistant (salida JSON).
  • Este enfoque fuerza al modelo a aprender a mapear entrada (RGB + Thermal + prompt) a la estructura de salida deseada.

Diagrama Mermaid adicional: flujo de generación de labels y formato de dataset

Fragmento de lectura / muestra de JSONL (mini muestra)

{"system": "Eres un asistente de visión para monitoreo de incendios. Dado RGB + Thermal devuelve un JSON con 5 campos.", "user": [{"type": "image", "image": "<rgb_path>"} , {"type": "image", "image": "<thermal_path>"} , {"type": "text", "text": "Return the wildfire JSON for this scene."}], "assistant": {"fire_present": true, "thermal_hotspot_intensity": "high", "fire_size": "medium", "smoke_visible": true, "image_quality_limited": false}}

Notas sobre estructura de muestras:

  • Cada muestra es una triple de mensajes (system, user, assistant).
  • La loss se evalúa sobre tokens del assistant (JSON generado).

Ejemplo real de una muestra de entrenamiento: par RGB + térmica con su etiqueta JSON


3.2 Configuración de fine-tuning y entrenamiento

Objetivo: detallar la configuración base del fine-tuning para la versión LFM2.5-VL-450M y las herramientas utilizadas.

  • Modelo base:
    • LiquidAI/LFM2.5-VL-450M
  • Herramientas y orquestación:
    • Ray Train
    • Hugging Face Trainer (HF Trainer)
  • Recursos y configuración típica:
    • Hardware: GPU tipo H100 80GB (para entrenamiento completo en el escenario descrito)
    • Épocas: 3; steps por epoch: 35; total de steps: 105
    • Batch efectivo: 16
    • Learning rate: 2e-5 con schedule cosine
    • Componentes afinados: tres partes (language_model, multimodal_projector, vision_tower)
  • Puntos de interés:
    • El pipeline mantiene un fine-tuning completo sin LoRA para la versión reportada.
    • Se reportan métricas de pérdida y precisión por campo, así como una métrica global (overall).
  • Bloques de código de configuración (ejemplos reproducibles)

Bloque de configuración de paths y verificación de existencia

from pathlib import Path

ROOT        = Path(".")
FT_DIR      = ROOT / "flame3_finetune"
MODEL_DIR   = FT_DIR / "model_finetuned"
BASE_MODEL  = "LiquidAI/LFM2.5-VL-450M"
IMAGES_DIR  = FT_DIR / "data" / "images"
EVALS_DIR   = FT_DIR / "evals"
PLOTS_DIR   = EVALS_DIR / "plots"

# Verificar que todo existe
for p in [FT_DIR, MODEL_DIR, IMAGES_DIR, EVALS_DIR]:
    status = "✅" if p.exists() else "❌"
    print(f"{status} {p}")

Fragmento de configuración de entrenamiento (pseudoejemplos reproducibles)

from transformers import AutoProcessor, AutoModelForImageTextToText

processor = AutoProcessor.from_pretrained(str(MODEL_DIR), trust_remote_code=True)
model = AutoModelForImageTextToText.from_pretrained(
    str(MODEL_DIR), dtype=torch.float32, device_map="cpu", trust_remote_code=True
)
model.eval()

Notas técnicas:

  • La elección de 3 componentes a optimizar (language model, multimodal projector, vision tower) refleja la arquitectura típica de VLM multimodal.
  • El scheduling se describe como cosine con warmup; las curvas de pérdida y las normas de gradiente se analizan para evitar explosiones y asegurar convergencia.
  • El reporte de pérdida: desde ~0.24 al inicio hasta ~0.028 al final, con ganancia de ~8.4x.
  • Latencia de entrenamiento y consumo de recursos dependen del tamaño del dataset y del hardware disponible.

Diagrama Mermaid adicional: flujo de entrenamiento y componentes

Bloques de código para lectura de curvas de entrenamiento (ejemplo)

import json
import matplotlib.pyplot as plt

state = json.loads((EVALS_DIR / "trainer_state.json").read_text())
train_log = [e for e in state["log_history"] if "loss" in e]
eval_log  = [e for e in state["log_history"] if "eval_loss" in e]

print(f"Training steps: {len(train_log)}")
print(f"Eval checkpoints: {len(eval_log)}")
print(f"Total epochs: {state['num_train_epochs']}")
print(f"Max steps: {state['max_steps']}")

Bloques de código para generar curvas de pérdida y LR

fig, ax = plt.subplots(figsize=(10, 5))

steps_train = [e["step"] for e in train_log]
loss_train  = [e["loss"] for e in train_log]
steps_eval  = [e["step"] for e in eval_log]
loss_eval   = [e["eval_loss"] for e in eval_log]

ax.plot(steps_train, loss_train, marker="o", ms=4, lw=1.5,
        color="#1f77b4", label="train loss")
ax.plot(steps_eval, loss_eval, marker="s", ms=10, lw=2.5,
        color="#d62728", label="eval loss (fin de epoch)")

ax.set_yscale("log")
ax.set_xlabel("Step", fontsize=11)
ax.set_ylabel("Loss (log scale)", fontsize=11)
ax.set_title("Curva de pérdida — FLAME 3 fine-tuning LFM2.5-VL-450M (3 epochs)", fontsize=12)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Curva de pérdida (train + eval) durante el fine-tuning, 3 epochs

Resultados reportados:

  • Loss inicial: 0.24 → Loss final: 0.0284 (8.4x mejora)
  • Overall accuracy: 0.21 → 0.86 (4.1x mejora)

Notas de optimización (por componente):

  • LR schedule coseno con warmup, configurado por componente: lr/language_model, lr/multi_modal_projector, lr/vision_tower.
  • Gradiente estable durante las 3 epochs; las grad_norm se mantienen en un rango razonable, sin explosión.
  • Latencia de entrenamiento y uso de recursos compatibles con la escala reportada (H100 80GB).

3.3 Evaluación y métricas (test set)

Objetivo: medir generalización del modelo finetuneado frente al baseline en el test set de FLAME 3 Sycan Marsh (147 escenas).

  • Métricas principales:
    • Accuracy por campo y global (overall)
    • Latencia de inferencia
  • Observaciones:
    • En el reporte de ejemplo del dataset, la base muestra overall de 0.21 y el fine-tuned 0.86, con Δ de 0.65 en overall (4.1x mejor).
    • El rendimiento por campo varía; fire_present y smoke_visible tienden a ser predictados con mayor precisión que fire_size, especialmente fire_size que presenta 0.53 de accuracy en el análisis detallado.
  • Fragmentos de código para parsing de trainer_state.json y generación de gráficos

Comparativa base vs fine-tuned (tabla)

import json, pandas as pd

base_meta = json.loads((EVALS_DIR / "base" / "meta.json").read_text())
ft_meta   = json.loads((EVALS_DIR / "finetuned" / "meta.json").read_text())

fields = ["valid_json", "fields_present", "fire_present",
          "thermal_hotspot_intensity", "fire_size",
          "smoke_visible", "image_quality_limited"]

rows = []
for f in fields:
    b = base_meta["per_field"][f]
    ft = ft_meta["per_field"][f]
    rows.append({"campo": f, "base": f"{b:.2f}", "fine-tuned": f"{ft:.2f}",
                 "Δ": f"+{ft-b:.2f}" if ft >= b else f"{ft-b:.2f}"})

rows.append({"campo": "⭐ overall",
             "base": f"{base_meta['overall']:.2f}",
             "fine-tuned": f"{ft_meta['overall']:.2f}",
             "Δ": f"+{ft_meta['overall']-base_meta['overall']:.2f}"})
rows.append({"campo": "⏱ avg latency (s)",
             "base": f"{base_meta['avg_latency_s']:.2f}",
             "fine-tuned": f"{ft_meta['avg_latency_s']:.2f}",
             "Δ": f"{ft_meta['avg_latency_s']-base_meta['avg_latency_s']:+.2f}"})

df = pd.DataFrame(rows).set_index("campo")
print(f"Test set: {base_meta['n']} samples")
print(df)

Gráfico de barras comparativo (base vs finetuned)

import matplotlib.pyplot as plt
import numpy as np
import json

# Asumiendo existencia de base_meta y ft_meta como en el bloque anterior
fields = ["valid_json", "fields_present", "fire_present",
          "thermal_hotspot_intensity", "fire_size",
          "smoke_visible", "image_quality_limited"]

base_vals = [base_meta["per_field"][f] for f in fields]
ft_vals   = [ft_meta["per_field"][f]   for f in fields]
x = np.arange(len(fields))
w = 0.38

fig, ax = plt.subplots(figsize=(12, 4.8))
bars_b  = ax.bar(x - w/2, base_vals, w, label=f"base", color="#9aa6b2")
bars_ft = ax.bar(x + w/2, ft_vals,   w, label=f"fine-tuned", color="#1f77b4")
ax.set_xticks(x)
ax.set_xticklabels(fields, rotation=22, ha="right", fontsize=9)
ax.set_ylim(0, 1.12)
ax.set_ylabel("Accuracy")
ax.set_title("Accuracy por campo — test set (n=147)")
ax.legend()
ax.grid(True, axis="y", alpha=0.3)
for bar, v in list(zip(bars_b, base_vals)) + list(zip(bars_ft, ft_vals)):
    ax.text(bar.get_x() + bar.get_width()/2, v + 0.01, f"{v:.2f}", ha="center", fontsize=7.5)

plt.tight_layout()
plt.show()

Accuracy por campo y overall — base vs fine-tuned (test set, n=147)

Evaluación en vivo (inferencia) sobre imágenes de test set

  • Nota: se describe que la inferencia en este escenario se realiza en CPU, con tiempos de ~5–10 segundos por imagen.
  • Fragmentos de código de inferencia end-to-end, que muestran la carga del modelo, la preparación de mensajes y la decodificación de la salida JSON.
# Ejemplo de inferencia (pseudocódigo) con PyTorch + HF
import sys, torch
sys.path.insert(0, str(FT_DIR / "scripts"))
from prompts import SYSTEM_PROMPT, USER_TEXT, FIELDS
from transformers import AutoProcessor, AutoModelForImageTextToText

processor = AutoProcessor.from_pretrained(str(MODEL_DIR), trust_remote_code=True)
model = AutoModelForImageTextToText.from_pretrained(
    str(MODEL_DIR), dtype=torch.float32, device_map="cpu", trust_remote_code=True
)
model.eval()

Inferencia de una escena Fire

def infer(scene_id: str) -> dict:
    rgb_path = IMAGES_DIR / f"{scene_id}_rgb.jpg"
    thr_path = IMAGES_DIR / f"{scene_id}_thermal.jpg"

    rgb = Image.open(rgb_path).convert("RGB")
    thr = Image.open(thr_path).convert("RGB")

    # Construcción de mensajes y procesamiento
    messages = [
        {"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT.strip()}]},
        {"role": "user",   "content": [
            {"type": "image", "image": rgb},
            {"type": "image", "image": thr},
            {"type": "text",  "text": USER_TEXT},
        ]},
    ]
    inputs = processor.apply_chat_template(
        messages, add_generation_prompt=True,
        return_tensors="pt", return_dict=True, tokenize=True
    )
    with torch.no_grad():
        out = model.generate(**inputs, max_new_tokens=128, do_sample=False)
    text = processor.batch_decode(out[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True)[0]

    try:
        pred = json.loads(text[text.find("{"):text.rfind("}")+1])
    except Exception:
        pred = {"raw": text}
    return pred

Ejemplos:

  • Escena Fire (fire_00497)
pred = infer("fire_00497")

Salida esperada (ejemplo en el documento fuente):

  • Latencia: ~5.27s

  • Predicción: { "fire_present": true, "thermal_hotspot_intensity": "high", "fire_size": "medium", "smoke_visible": true, "image_quality_limited": false }

  • Escena No Fire (no_fire_00050)

pred = infer("no_fire_00050")

Salida esperada:

  • Latencia similar (~5.2s)
  • Predicción: { "fire_present": false, "thermal_hotspot_intensity": "none", "fire_size": "none", "smoke_visible": false, "image_quality_limited": false }

Batch eval (n escenas aleatorias del test set)

import random, time, json
test_rows = [json.loads(l) for l in (FT_DIR / "data" / "test.jsonl").read_text().splitlines() if l.strip()]
N = 10
sample = random.sample(test_rows, N)

results, latencies = [], []
for row in sample:
    user_content = row["messages"][1]["content"]
    scene = user_content[0]["image"].rsplit("_rgb.jpg", 1)[0]
    truth = json.loads(row["messages"][2]["content"][0]["text"])

    rgb = Image.open(IMAGES_DIR / f"{scene}_rgb.jpg").convert("RGB")
    thr = Image.open(IMAGES_DIR / f"{scene}_thermal.jpg").convert("RGB")
    messages = [
        {"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT.strip()}]},
        {"role": "user",   "content": [
            {"type": "image", "image": rgb}, {"type": "image", "image": thr},
            {"type": "text",  "text": USER_TEXT},
        ]},
    ]
    inputs = processor.apply_chat_template(
        messages, add_generation_prompt=True,
        return_tensors="pt", return_dict=True, tokenize=True
    )
    t0 = time.time()
    with torch.no_grad():
        out = model.generate(**inputs, max_new_tokens=128, do_sample=False)
    dt = time.time() - t0
    latencies.append(dt)
    text = processor.batch_decode(out[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True)[0]
    try:
        pred = json.loads(text[text.find("{"):text.rfind("}")+1])
    except Exception:
        pred = {}
    per = {f: int(pred.get(f) == truth.get(f)) for f in FIELDS}
    results.append({"scene": scene, "truth": truth, "pred": pred, "per": per, "latency": dt})

# Resumen
overall = sum(sum(r["per"].values()) for r in results) / (len(results) * len(FIELDS))
print(f"\nOverall en {N} muestras:  {overall:.3f}")
print(f"Latencia promedio:     {sum(latencies)/len(latencies):.2f}s")
print(f"Latencia mínima:       {min(latencies):.2f}s")
print(f"Latencia máxima:       {max(latencies):.2f}s")

Ejemplos de salida (del documento fuente):

  • Overall en 10 muestras: 0.840
  • Latencia promedio: 5.27s
  • Latencia mínima: 5.22s
  • Latencia máxima: 5.32s

3.4 Análisis de errores y diagnóstico

Objetivo: identificar debilidades y líneas de mejora a partir de un análisis estructurado de errores por campo, con foco en fire_size.

  • Errores por campo (test set completo, 147 muestras):
    • fire_present: precisión 1.00 (0 errores)
    • thermal_hotspot_intensity: 0.82 (27 errores)
    • fire_size: 0.53 (69 errores)
    • smoke_visible: 0.99 (1 error)
    • image_quality_limited: 0.96 (6 errores)
  • Observación clave:
    • fire_size es el campo débil. Los errores se agrupan en confusiones entre none/small/medium/large, con mayor incidencia en "medium" vs "small" y viceversa.
  • Análisis de confusión para fire_size (ground truth vs predicción)
    • Gráficos de distribución de errores: se observa desbalance en la distribución de ground truth y en la predicción.
  • Visualización de escenas con errores:
    • Se selecciona una escena con discrepancia entre ground truth y predicción para examinar la interacción entre RGB y Thermal.
  • Metodología de diagnóstico:
    • Revisión de casos por escena, comparación entre RGB y Thermal, y verificación de consistencia en el JSON de salida.
  • Fragmentos de código para extracción de errores y visualización

Gráfico de distribución de errores de fire_size

from collections import Counter
size_errors = errors["fire_size"]
pred_wrong  = Counter(r["pred"].get("fire_size", "None") for r in size_errors)
truth_wrong = Counter(r["truth"].get("fire_size", "None") for r in size_errors)

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
labels = ["none", "small", "medium", "large"]

axes[0].bar(labels, [truth_wrong.get(l, 0) for l in labels], color="#d62728")
axes[0].set_title("fire_size — ground truth en los errores")
axes[0].set_ylabel("casos")

axes[1].bar(labels, [pred_wrong.get(l, 0) for l in labels], color="#ff7f0e")
axes[1].set_title("fire_size — predicción errónea")
axes[1].set_ylabel("casos")

plt.suptitle("Análisis de confusión — fire_size (campo más débil: 0.53 accuracy)", fontsize=12)
plt.tight_layout()
plt.show()

Distribución de errores de fire_size: ground truth vs predicción

Ejemplo detallado de un error por escena

sample_error = size_errors[0]
scene = sample_error["scene"]

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(Image.open(IMAGES_DIR / f"{scene}_rgb.jpg"))
axes[0].set_title("RGB"); axes[0].axis("off")
axes[1].imshow(Image.open(IMAGES_DIR / f"{scene}_thermal.jpg"))
axes[1].set_title("Thermal"); axes[1].axis("off")

truth = sample_error["truth"]
pred  = sample_error["pred"]
plt.suptitle(
    f"Escena: {scene}  |  truth fire_size={truth['fire_size']}  →  pred fire_size={pred.get('fire_size','?')}",
    fontsize=11, color="#d62728"
)
plt.tight_layout(); plt.show()

print(f"Truth: {json.dumps(truth, indent=2)}")
print(f"Pred:  {json.dumps(pred, indent=2)}")

Escena con discrepancia en fire_size (small vs medium): RGB y térmica

Ejemplo de salida (discrepancia de fire_size)

  • Truth: { "fire_present": true, "thermal_hotspot_intensity": "high", "fire_size": "small", "smoke_visible": true, "image_quality_limited": false }

  • Pred: { "fire_present": true, "thermal_hotspot_intensity": "high", "fire_size": "medium", "smoke_visible": true, "image_quality_limited": false }

  • Interpretación: la escena presenta small vs medium; el modelo aprovecha señales térmicas y visuales, pero la frontera entre tamaños no está suficientemente separada con la distribución actual.

Análisis de errores por campo (resumen numérico)

# Errores por campo en el test set (147 samples)
print("Errores por campo en el test set (147 samples):")
print(f"{'campo':<30} {'errores':>8} {'accuracy':>10}")
print("─" * 52)
for f in FIELDS:
    n_err = len(errors[f])
    acc = 1 - n_err / len(ft_results)
    print(f"{f:<30} {n_err:>8}   {acc:>9.2f}")

La salida ilustra la variación de precisión entre campos; fire_size suele ser el cuello de botella.


4. Mini caso práctico / Ejemplo end-to-end

Este apartado integra el flujo completo desde la carga de imágenes hasta la salida de predicción en JSON y su verificación.

  • Escenario 1: fire_00497 (escena con incendio)
  • Escenario 2: no_fire_00050 (escena sin incendio)

Pasos:

  1. Preparación de entradas
  • Cargar RGB_corrected y Thermal
  • Generar las entradas para el prompt
  1. Inferencia end-to-end
  • Pasar por processor + modelo
  • Decodificar salida JSON
  1. Verificación de salida
  • Verificar que el JSON contiene los 5 campos y tipos esperados
  1. Visualización
  • Mostrar imágenes de entrada y salida JSON

Código completo (end-to-end)

import json
import time
from PIL import Image
import matplotlib.pyplot as plt

# Supuestos: SYSTEM_PROMPT, USER_TEXT, FIELDS definidos en prompts.py
from prompts import SYSTEM_PROMPT, USER_TEXT, FIELDS
from transformers import AutoProcessor, AutoModelForImageTextToText

# Cargar modelo finetuned
processor = AutoProcessor.from_pretrained(str(MODEL_DIR), trust_remote_code=True)
model = AutoModelForImageTextToText.from_pretrained(
    str(MODEL_DIR), dtype=torch.float32, device_map="cpu", trust_remote_code=True
)
model.eval()

def infer_and_show(scene_id: str):
    rgb = Image.open(IMAGES_DIR / f"{scene_id}_rgb.jpg").convert("RGB")
    thr = Image.open(IMAGES_DIR / f"{scene_id}_thermal.jpg").convert("RGB")

    # Mostrar entradas
    fig, ax = plt.subplots(1, 2, figsize=(8, 4))
    ax[0].imshow(rgb); ax[0].set_title("RGB (Corrected FOV)"); ax[0].axis("off")
    ax[1].imshow(thr); ax[1].set_title("Thermal (Raw JPG)"); ax[1].axis("off")
    plt.suptitle(f"Escena: {scene_id}", fontsize=12)
    plt.tight_layout(); plt.show()

    messages = [
        {"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT.strip()}]},
        {"role": "user",   "content": [
            {"type": "image", "image": rgb},
            {"type": "image", "image": thr},
            {"type": "text",  "text": USER_TEXT},
        ]},
    ]
    inputs = processor.apply_chat_template(
        messages, add_generation_prompt=True,
        return_tensors="pt", return_dict=True, tokenize=True
    )

    with torch.no_grad():
        out = model.generate(**inputs, max_new_tokens=128, do_sample=False)
    text = processor.batch_decode(out[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True)[0]

    try:
        pred = json.loads(text[text.find("{"):text.rfind("}")+1])
    except Exception:
        pred = {"raw": text}

    print(f"Predicción: {json.dumps(pred, indent=2)}")
    return pred

# Ejecutar para fire_00497
pred_fire = infer_and_show("fire_00497")

# Ejecutar para no_fire_00050
pred_no_fire = infer_and_show("no_fire_00050")

Inferencia sobre una escena con incendio: entradas RGB/térmica y salida JSON

Inferencia sobre una escena sin incendio

Diagramas Mermaid para la inferencia end-to-end

Cierre de este bloque: la ejecución end-to-end debe producir una salida JSON con la estructura esperada y permitir verificación rápida de los campos.


5. Errores comunes (y cómo evitarlos)

  • Error: sesgo en split (desbalance entre Fire y No Fire)
    • Solución: estratificar 80/20 preservando proporciones de Fire/No Fire en train y test.
  • Error: overfitting a patrones de entrenamiento
    • Solución: monitorizar gradient norms, aplicar regularización moderada y validar en escenas no vistas.
  • Error: parsing de JSON de salida
    • Solución: robustecer la extracción con try/except y validar claves esperadas antes de asignar métricas.
  • Error: divergencia de LR entre componentes
    • Solución: usar schedule cosine con warmup, y mantener valores razonables por componente.
  • Error: discrepancias entre RGB y Thermal que causan errores en fire_size
    • Solución: analizar correctamente las escenas con confusión, usar visualización de pares RGB/ Thermal para calibración de prompts.

6. Checklist accionable

  • Estructurar dataset FLAME 3: rutas, splits y labels.
  • Generar train.jsonl y asegurar formato de 5 campos.
  • Configurar entrenamiento: LR, epochs, steps, batch.
  • Verificar trainer_state.json y extraer curvas de pérdida.
  • Realizar evaluación en test set y comparar con base.
  • Analizar errores por campo y plan de mejora.
  • Validar inferencia en escenas de ejemplo y medir latencia.
  • Documentar hallazgos y recomendaciones prácticas.

7. Cómo medir / Qué sigue

  • Métricas de éxito:
    • Mejora de overall accuracy (comparar base vs fine-tuned)
    • Reducción de errores en fire_size (campo débil)
    • Latencia de inferencia aceptable para escenarios operativos
    • Estabilidad de gradientes durante el entrenamiento
  • Siguientes pasos concretos:
    • Iterar con distinto LR schedule (p. ej., cambios suaves en el periodo de warmup)
    • Probar pequeños ajustes de prompting y estructura de salida
    • Evaluar la incorporación de LoRA si es aplicable
    • Ampliar tamaño del dataset de entrenamiento
    • Pruebas en GPU para reducir latencia de inferencia

8. Preguntas frecuentes (FAQ)

  • FAQ 1: ¿Por qué 5 campos JSON en la salida es suficiente?
    • Razonamiento: los cinco campos cubren las dimensiones esenciales de monitoreo de incendio (presentación, intensidad, tamaño, humo y calidad de la imagen). Mantener una salida estructurada facilita la evaluación automática y la integración con pipelines de monitorización.
  • FAQ 2: ¿Qué hacer si la predicción falla de manera intermitente?
    • Recomendaciones: revisar la distribución de fire_size y la correlación entre RGB y Thermal; verificar consistencia de ground-truth con escenas similares; considerar ajustes en prompting y en el schedule de LR.
  • FAQ 3: ¿Cómo interpretar la curva de loss en log scale?
    • Señales útiles: descenso sostenido y estabilización indican convergencia; picos o saltos abruptos pueden sugerir inestabilidad de entrenamiento o necesidad de ajustar el LR.
  • FAQ 4: ¿Qué considerar al pasar de CPU a GPU para inferencia?
    • Consideraciones: la latencia por imagen se reducirá significativamente; el tamaño del batch y la memoria de GPU deben ajustarse; configurar device_map correctamente para aprovechar las particiones de memoria.
  • FAQ 5: ¿Cómo reproducir exactamente las mismas métricas en otro entorno?
    • Recomendaciones: fijar seeds, usar la misma versión de transformers y HF Trainer, conservar el mismo esquema de datos (train.jsonl) y las mismas especificaciones de hardware (si posible). Exportar y registrar metadatos de entrenamiento y evaluación para trazabilidad.

Este REPORTE TÉCNICO sintetiza un flujo reproducible de fine-tuning SFT de un Vision-Language Model sobre FLAME 3 para incendios aéreos, con foco en la generación de salidas estructuradas y la evaluación de generalización. El objetivo es que el lector pueda seguir, reproducir y extender el pipeline, manteniendo una visión clara de trade-offs, límites y posibles mejoras. La información aquí se apoya exclusivamente en el Documento Fuente proporcionado y en prácticas técnica‑estándar de VLMs y pipelines de SFT.

Autor

Zilocore