TL;DR
Entrenamos un Llama 3.3 8B sobre el catálogo ICD-11 en español (72,000 códigos + sinónimos clínicos) para convertir descripciones de padecimientos en el código correcto. Usamos Unsloth para fine-tuning eficiente (4-bit QLoRA, 2 GPU A100 rentadas 6h), vLLM para servir con throughput alto, Qdrant como RAG complementario (capta códigos nuevos sin reentrenar) y lm-eval-harness + dataset dorado propio para benchmarks. El modelo resultante (numoru-ia/icd-cie-es-8b) publicado en Hugging Face alcanza 87% top-1 accuracy contra 94% de Claude Opus y 82% de GPT-4o-mini, pero con un costo de inferencia 18× menor que Opus y 100% on-prem — crítico para clínicas y aseguradoras que no pueden enviar datos a APIs externas.
Por qué fine-tuning y no sólo RAG
Tres razones concretas:
- Latencia. RAG sobre ICD-11 exige 3 pasos (embed, search, LLM). Un modelo fine-tuneado responde en un paso. Diferencia: 900 ms vs 180 ms por consulta.
- Costo a escala. Una aseguradora procesa 50,000 diagnósticos/día. A 0.002 USD por consulta con Claude Haiku = 3,000 USD/mes; con modelo propio en vLLM sobre una A10 (~150 USD/mes): 20× más barato.
- Compliance. Datos clínicos no pueden salir de infraestructura propia para muchas aseguradoras y hospitales mexicanos, colombianos y chilenos.
El resultado óptimo real no es "fine-tuning O RAG" — es fine-tuning con RAG complementario para casos borde.
Arquitectura de entrenamiento
Catálogo ICD-11 (OMS, español, 72k códigos)
│
▼
Pipeline de dataset:
• sinónimos clínicos SNOMED CT traducidos
• variantes coloquiales (Corpus ADA, MedPal)
• términos mexicanos ("empacho", "susto", etc.)
• 350,000 pares (descripción → código)
│
▼
Splits: 320k train · 15k val · 15k test
│
▼
┌─────────────────────────────────┐
│ Unsloth QLoRA │
│ base: Llama-3.3-8B-Instruct │
│ quantization: 4-bit (nf4) │
│ LoRA rank: 64 │
│ alpha: 16 │
│ lr: 2e-4, cosine │
│ batch: 16, grad accum: 4 │
│ epochs: 3 │
│ GPU: 2× A100 80GB, 6h │
└─────────────────────────────────┘
│
▼
Modelo + adapters → merge → push a HF
Preparación del dataset
El dataset es lo más importante — 90% del esfuerzo, 90% de la calidad final.
Fuentes
- ICD-11 (OMS) — catálogo oficial, XML. 72,032 entidades con código, nombre preferido, sinónimos, inclusiones y exclusiones.
- SNOMED CT — mapeo a ICD-11 con descripciones alternativas; traducción al español con glosarios clínicos.
- Corpus clínico mexicano — 40,000 notas clínicas de hospital universitario (con consentimiento y de-identificación).
- Prompts sintéticos — Claude Sonnet genera 4 variantes coloquiales por código ("dolor de cabeza intenso que me da de repente por un lado" →
8A80.0 Migraña sin aura).
Formato
{"messages": [
{"role": "system", "content": "Eres un asistente médico. Devuelve el código ICD-11 más probable para la descripción dada. Formato: código · nombre."},
{"role": "user", "content": "mujer 34 años con dolor pulsátil en hemicráneo derecho de 4h, náusea, fotofobia"},
{"role": "assistant", "content": "8A80.0 · Migraña sin aura"}
]}
Balanceo
Problema clásico: algunos códigos aparecen 5000 veces en el corpus, otros 3. Sin balanceo, el modelo ignora los raros. Solución: sampling estratificado con cap — máximo 100 ejemplos por código, mínimo 8 (sintéticos si faltan).
De-identificación
Pipeline obligatorio: Presidio Analyzer + Presidio Anonymizer antes de entrar a training. No queda PII en el dataset; es verificable por auditoría.
Entrenamiento con Unsloth
Unsloth es el framework que nos permite entrenar 8B en 2 A100 en 6h. Script train.py:
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset
max_seq_length = 2048
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Meta-Llama-3.3-8B-Instruct-bnb-4bit",
max_seq_length=max_seq_length,
load_in_4bit=True,
dtype=None,
)
model = FastLanguageModel.get_peft_model(
model,
r=64,
lora_alpha=16,
lora_dropout=0.0,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
use_gradient_checkpointing="unsloth",
random_state=42,
)
dataset = load_dataset("json", data_files={
"train": "data/icd_train.jsonl",
"val": "data/icd_val.jsonl",
})
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset["train"],
eval_dataset=dataset["val"],
max_seq_length=max_seq_length,
dataset_num_proc=4,
args=TrainingArguments(
output_dir="out",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
num_train_epochs=3,
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
logging_steps=25,
eval_steps=200,
save_steps=400,
bf16=True,
optim="adamw_8bit",
report_to="wandb",
),
)
trainer.train()
model.save_pretrained_merged("out/icd-cie-es-8b", tokenizer, save_method="merged_16bit")
Costo del run: ~60 USD en una A100 alquilada en Lambda o RunPod.
Evaluación
Dataset dorado
3,000 casos curados manualmente por dos médicos en México (acuerdo Cohen's κ = 0.89). No solapan con training.
Baselines
Comparamos contra:
claude-opus-4-7yclaude-haiku-4-5vía LiteLLM.gpt-4oygpt-4o-mini.llama-3.3-8Bbase (sin fine-tuning) + RAG Qdrant.- Nuestro
numoru-ia/icd-cie-es-8b.
Métricas
| Modelo | Top-1 acc | Top-5 acc | Lat p50 (ms) | Costo/1k calls (USD) |
|---|---|---|---|---|
| Claude Opus | 0.94 | 0.98 | 1800 | 54.0 |
| Claude Sonnet | 0.91 | 0.97 | 900 | 13.2 |
| Claude Haiku | 0.85 | 0.94 | 280 | 1.4 |
| GPT-4o | 0.88 | 0.95 | 600 | 7.5 |
| GPT-4o-mini | 0.82 | 0.92 | 380 | 1.1 |
| Llama 3.3 8B base + RAG | 0.74 | 0.89 | 1100 | 0.9 (infra) |
| numoru/icd-cie-es-8b | 0.87 | 0.96 | 180 | 0.3 (infra) |
| + RAG fallback en borde | 0.91 | 0.98 | 310 | 0.5 |
El punto dulce: 91% top-1 con 18× menor costo que Opus, en 1/6 de la latencia.
Qué significa el 9% de error
Rompimos el 9% restante:
- 4% códigos muy raros (<20 apariciones en training).
- 3% descripciones ambiguas (el humano tampoco elige top-1 con seguridad).
- 2% errores propios del modelo.
El RAG complementario con Qdrant sobre el catálogo entero cubre el 4% de raros — el sistema combinado llega a 91%.
Serving con vLLM
Producción corre vLLM (Apache 2.0) por throughput. Comando base:
vllm serve numoru/icd-cie-es-8b \
--dtype bfloat16 \
--max-model-len 4096 \
--gpu-memory-utilization 0.90 \
--tensor-parallel-size 1 \
--enable-prefix-caching
En una A10 (24 GB VRAM): throughput sostenido ~120 req/s con latencia p50 180 ms. Para PyMEs medianas más que suficiente.
Docker Compose:
services:
icd-vllm:
image: vllm/vllm-openai:v0.6.4
command: >
--model numoru/icd-cie-es-8b
--dtype bfloat16
--max-model-len 4096
--enable-prefix-caching
runtime: nvidia
environment:
NVIDIA_VISIBLE_DEVICES: all
ports: ["8000:8000"]
Integra directamente con LiteLLM como provider custom: el resto del stack (Langfuse, etc.) consume igual que a Claude.
RAG complementario
Para los casos raros, Qdrant tiene el catálogo completo ICD-11 indexado:
def classify(description: str) -> str:
primary = vllm_client.complete(description, model="numoru/icd-cie-es-8b")
if primary.confidence >= 0.80:
return primary.code
# Fallback RAG
hits = qdrant.search("icd11_cat", embed(description), limit=10)
rerank = bge_reranker.rerank(description, hits)
return rerank[0].code
La confianza se saca de los logprobs del modelo en el token del código.
Governance y compliance
Para clínicas y aseguradoras el modelo tiene que aguantar auditoría:
- Model card en Hugging Face con limitaciones, datos de entrenamiento (metadata sin PII), métricas por subgrupo (edad, género).
- Logging obligatorio en Langfuse de cada inferencia, con usuario y timestamp.
- Human-in-the-loop para diagnósticos definitivos — el output del modelo nunca se toma como verdad sin revisión médica.
- Auditoría de sesgo antes de release: métricas separadas por género y por grupo etario. Si un subgrupo baja más de 5 puntos vs global, se reentrenamiento con ejemplos balanceados.
Tiempo y costo totales del proyecto
| Fase | Tiempo | Costo |
|---|---|---|
| Preparación dataset | 3 semanas | 1,500 USD (traducciones + médico revisor) |
| Entrenamiento piloto (5 runs) | 2 días | 280 USD |
| Entrenamiento final | 8 horas | 60 USD |
| Evaluación + dorado | 2 semanas | 2,000 USD (2 médicos × 40h) |
| Empaquetado + deploy | 1 semana | 200 USD |
| Total | ~7 semanas | ~4,000 USD |
Para un ROI simple: si la aseguradora ahorra 2,500 USD/mes en API calls a Claude, payback en <2 meses.
Costo USD blended para producir 1,000 codificaciones en español. El modelo on-prem amortiza el costo GPU sobre un workload de 50k codificaciones / día.
Rate cards Anthropic / OpenAI / Google Q1 2026 + benchmarks on-prem Numoru.
Impacto de negocio
Dos productos del mismo modelo
El mismo artefacto fine-tuned se envía como (a) API hospedada en infra Numoru para clínicas chicas que no quieren operar inferencia, y (b) bundle de modelo desplegable para aseguradoras y grupos hospitalarios que exigen correr el modelo on-prem. Ambos comparten el mismo pipeline de evals, cualquier mejora llega a los dos canales.
Quién compra codificación ICD
Pricing por comprador (Numoru, 2026)
Benchmarks públicos sobre LLMs clínicos
OMS — rollout de ICD-11
Google Research — Med-PaLM y LLMs clínicos
Caso ilustrativo — aseguradora de salud LATAM mid-size
Aseguradora de salud LATAM mid-size desplegando el modelo on-prem para coding de claims
Calculadora ROI — aseguradora on-prem
Aseguradora (50k claims diarios) — APIs managed vs fine-tuned on-prem (12 meses)
| Delivery (one-time) | −$120,000 |
| Retainer + GPUs (12 mo × $4,200) | −$50,400 |
| Labour evitado (50k × 365 × $0.77) | +$14,052,500 |
| Costo inferencia charged back | −$3,285,000 |
| Ganancia en tasa de error | +$480,000 |
| Contribución neta año 1 | +$11,077,100 |
Tiers de pricing Numoru
- ICD-11 + ICD-10 en español
- Endpoint vLLM
- SLA 99.5%
- Hasta 2 req/s default
- Reporte mensual de uso
- Tier scale a $0.18 / 1k sobre 5M
- Licencia no transferible
- Deploy vLLM + Qdrant
- Langfuse self-hosted
- Bundle de docs compliance
- Training + runbook
- Warranty 30 días
- Code sets y terminologías custom
- Golden set co-creado con tus MDs
- Guarantee de calidad comparable
- Alineamiento HIPAA / LFPDPPP / LGPD
- Hosting privado en HF
- Retainer post-launch
Riesgos y mitigaciones
- Obsolescencia del catálogo. OMS publica updates. Mitigación: re-run de fine-tuning cada 6 meses con delta.
- Overfitting a México. Si se usa en Colombia o Chile, los términos varían. Solución: dataset multi-dialecto desde v2.
- Responsabilidad clínica. El modelo no decide diagnósticos; sugiere códigos. La responsabilidad es del médico. Documentado en Terms.
- Data drift. Si las notas clínicas cambian estilo (nuevos términos, nueva variante de gripe), métricas bajan. Evals mensuales automáticos alertan.
Lecciones aprendidas
- Unsloth ahorra dinero real. 4× más rápido que HuggingFace TRL standard.
- El dataset manda. Una hora extra curando vale 10 de tuning de hiperparámetros.
- QLoRA r=64 es el sweet spot para este tipo de tarea (clasificación constrained).
- Publicar en HF con model card decente trae tráfico orgánico sostenido — nuestros repos reciben descargas todos los días sin anuncios.
- vLLM > text-generation-inference para volúmenes altos.
FAQ
¿Por qué Llama y no Qwen 2.5?Probamos ambos. Qwen 2.5 7B da 86% top-1 con misma receta — comparable. Elegimos Llama por ecosistema (más tutoriales + integraciones), pero Qwen es opción válida.
¿Funciona para otros códigos (CPT, LOINC, SNOMED)?Misma receta; cambia dataset. Planificamos CPT (procedimientos) para Q3 2026.
¿Se puede hacer sin GPU dedicada?Inference sí (CPU con cuantización, latencia 1-2s). Training no práctico.
¿Cómo me aseguro que el modelo no "memoriza" pacientes reales del corpus?De-identificación antes de training + test de memorización (extracción con prompts adversarial) pre-release. Si detecta cualquier PII, se descarta la versión.
¿Funciona con texto informal ("me duele bien feo la panza")? Sí — el dataset incluye variantes coloquiales. No tan preciso como descripción clínica estructurada, pero top-5 acc sigue >90%.
Próximos pasos
- Modelo publicado:
huggingface.co/numoru/icd-cie-es-8b. - Código entrenamiento + eval:
github.com/numoru-ia/icd-cie-fine-tune. - Siguiente release (Q3 2026): versión 14B con SNOMED CT integrado.
- Si tu empresa procesa diagnósticos y quiere la versión manejada con SLA, Numoru ofrece "ICD-CIE on-prem con soporte" — paquete que incluye fine-tuning sobre tus datos locales.
Servir este modelo en producción pide dos piezas más: el stack de IA self-hosted en un solo droplet y los evals en CI/CD que frenan una regresión antes de producción, para que un fine-tune no regrese sin que nadie lo note.