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
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).

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()

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_normse 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()

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()

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)}")

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:
- Preparación de entradas
- Cargar RGB_corrected y Thermal
- Generar las entradas para el prompt
- Inferencia end-to-end
- Pasar por processor + modelo
- Decodificar salida JSON
- Verificación de salida
- Verificar que el JSON contiene los 5 campos y tipos esperados
- 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")


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
