HF Transformers v5 NovaMed Analytics IA-Ingenieria-MLOps

Pipeline NLP Clínico con HuggingFace Transformers

Inferencia en producción + fine-tuning supervisado para clasificación de urgencia, extracción de entidades clínicas y resumen de informes de alta médica.

transformers==5.12.0
PyTorch 2.4 · CUDA 12.4
AWS SageMaker A10G
12.000 notas clínicas etiquetadas
Pipelines de inferencia configurados
~42 ms
🏥
Triaje de Urgencia
text-classification
novamed/bert-triage-v2
~118 ms
💊
Entidades Clínicas
token-classification
PlanTL-GOB-ES/bsc-bio-ehr-es
~680 ms
📋
Resumen de Alta
summarization
IIC/mt5-spanish-mlsum
~55 ms
🔤
Detección Idioma
text-classification
papluca/xlm-roberta-base-language-detection
Código de producción — inferencia.py
📄 novamed/inferencia.py Python
# novamed/inferencia.py — Pipeline NLP clínico con Transformers v5 # Instalación: uv pip install "transformers[torch]==5.12.0" accelerate==1.14.0 from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification from transformers import AutoModelForTokenClassification, Pipeline from huggingface_hub import login import os import torch # ✓ Seguridad: token desde variable de entorno, nunca hardcodeado login(token=os.environ["HF_TOKEN"]) DEVICE = 0 if torch.cuda.is_available() else -1 DTYPE = torch.bfloat16 if DEVICE == 0 else torch.float32 # ── 1. Clasificador de urgencia (BERT fine-tuneado) ────────────────────────── triage_pipe = pipeline( "text-classification", model="novamed/bert-triage-v2", tokenizer="novamed/bert-triage-v2", device=DEVICE, torch_dtype=DTYPE, top_k=None, # devuelve scores de todas las clases ) # ── 2. NER clínico (entidades biomédicas en español) ───────────────────────── ner_pipe = pipeline( "token-classification", model="PlanTL-GOB-ES/bsc-bio-ehr-es", aggregation_strategy="simple", device=DEVICE, ) # ── 3. Resumen para enfermería (mT5 fine-tuneado en español) ───────────────── summ_pipe = pipeline( "summarization", model="IIC/mt5-spanish-mlsum", device=DEVICE, torch_dtype=DTYPE, ) def analizar_nota(texto: str) -> dict: """Analiza una nota médica devolviendo triaje, entidades y resumen.""" return { "urgencia" : triage_pipe(texto, truncation=True, max_length=512), "entidades" : ner_pipe(texto), "resumen" : summ_pipe(texto, max_length=80, min_length=20)[0]["summary_text"], }
Demo — clasificación de triaje en tiempo real
output · triage_pipe(notas_turno_09h)
CRÍTICA
Paciente 72a, dolor torácico irradiado a brazo izq., diaforesis intensa. TA 85/50.
score: 0.962
ALTA
Fiebre 39.4°C desde hace 48h, tos productiva, SpO₂ 93%. Antecedente EPOC.
score: 0.841
MEDIA
Migraña recurrente con aura, última crisis hace 6h, náuseas sin vómitos.
score: 0.713
BAJA
Contusión leve tobillo derecho tras caída. Sin deformidad ni crepitación.
score: 0.908
NER clínico — extracción de entidades
Paciente recibe Metformina 850 mg cada 12h y Enalapril 10 mg en dosis única matinal. Refiere dolor epigástrico y náuseas postprandiales. Glucemia basal: 142 mg/dl. Diagnóstico: Diabetes Mellitus tipo 2 + Hipertensión arterial esencial. Se ajusta pauta y se solicita HbA1c.
Medicamento
Síntoma
Diagnóstico
Valor clínico
Resumen automático de informe de alta
Input — informe de alta (fragmento, 480 palabras)
"Paciente varón de 58 años ingresado el 10/06/2026 por cuadro de fiebre alta (39.8°C), tos productiva con expectoración verdosa y dificultad respiratoria progresiva de 3 días de evolución. Antecedentes: EPOC moderado (FEV1 58%), hipertensión arterial, dislipemia. Fumador de 20 paquetes-año hasta 2018. A su llegada SpO₂ 88%, FC 112 lpm, TA 138/82. Rx tórax: condensación basal derecha compatible con neumonía adquirida en la comunidad... [480 palabras]"
Output summ_pipe — resumen para enfermería (68 tokens)
Varón 58a con EPOC ingresado por neumonía basal derecha. Tratado con amoxicilina-clavulánico IV y broncodilatadores. Alta con SpO₂ 95% en reposo. Seguimiento neumología en 4 semanas. Revisar pauta inhalatoria y abstinencia tabáquica.
Fine-tuning con Trainer API — flujo completo
1 Dataset 12k notas parquet
2 Tokenización + split 80/10/10
3 bert-base-multilingual-cased
4 TrainingArguments + bf16
5 trainer.train() · A10G
6 push_to_hub("novamed/...")
📄 novamed/fine_tune_triage.py Python
from datasets import load_dataset, ClassLabel from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, DataCollatorWithPadding, ) import evaluate import numpy as np LABELS = ["baja", "media", "alta", "critica"] MODEL = "bert-base-multilingual-cased" # base para fine-tuning HUB_ID = "novamed/bert-triage-v2" # ── Dataset ────────────────────────────────────────────────────────────────── ds = load_dataset("parquet", data_files="data/notas_etiquetadas.parquet") ds = ds["train"].train_test_split(test_size=0.2, seed=42) tokenizer = AutoTokenizer.from_pretrained(MODEL) def tokenize(batch): return tokenizer(batch["texto"], truncation=True, max_length=512) ds = ds.map(tokenize, batched=True) # ── Modelo ─────────────────────────────────────────────────────────────────── model = AutoModelForSequenceClassification.from_pretrained( MODEL, num_labels=len(LABELS), id2label={i: l for i, l in enumerate(LABELS)}, label2id={l: i for i, l in enumerate(LABELS)}, ) # ── Training Args ───────────────────────────────────────────────────────────── args = TrainingArguments( output_dir="./checkpoints/triage", num_train_epochs=5, per_device_train_batch_size=32, per_device_eval_batch_size=64, learning_rate=2e-5, bf16=True, # bfloat16 en A10G eval_strategy="epoch", save_strategy="best", load_best_model_at_end=True, metric_for_best_model="f1", push_to_hub=True, hub_model_id=HUB_ID, report_to="none", # sin W&B en primera pasada ) metric = evaluate.load("f1") def compute_metrics(eval_pred): logits, labels = eval_pred preds = np.argmax(logits, axis=-1) return metric.compute(predictions=preds, references=labels, average="macro") trainer = Trainer( model=model, args=args, train_dataset=ds["train"], eval_dataset=ds["test"], tokenizer=tokenizer, data_collator=DataCollatorWithPadding(tokenizer), compute_metrics=compute_metrics, ) trainer.train() trainer.push_to_hub() # sube el mejor checkpoint al Hub privado
Métricas de entrenamiento — novamed/bert-triage-v2
94.2%
F1-macro (test)
↑ +31pp vs baseline regex
96.1%
Accuracy
↑ mejor época 5/5
42ms
Latencia p50 inferencia
GPU A10G · batch=1
2.4h
Tiempo fine-tuning
≈ 5€ en SageMaker spot
Log de entrenamiento
Epoch 1/5 step 300/300 loss: 0.8241 eval_loss: 0.5812 f1: 0.7830 [14m 22s] Epoch 2/5 step 300/300 loss: 0.4103 eval_loss: 0.3241 f1: 0.8791 [14m 18s] Epoch 3/5 step 300/300 loss: 0.2714 eval_loss: 0.2188 f1: 0.9102 [14m 21s] Epoch 4/5 step 300/300 loss: 0.1892 eval_loss: 0.1844 f1: 0.9318 [14m 19s] Epoch 5/5 ★ BEST loss: 0.1441 eval_loss: 0.1720 f1: 0.9420 [14m 23s] ✓ Checkpoint guardado en novamed/bert-triage-v2 ✓ Model pushed to Hub (privado, token scope: write)
Matriz de confusión (test 2.400 notas)
↓ real / pred → BAJA MEDIA ALTA CRÍTICA
BAJA 578 18 4 0
MEDIA 12 562 21 5
ALTA 2 24 558 16
CRÍTICA 0 3 11 586
Seguridad HF: el token se carga exclusivamente desde os.environ["HF_TOKEN"] (scope write solo para push_to_hub; scope read para inferencia). El repositorio novamed/bert-triage-v2 está marcado como privado en el Hub. No hardcodear tokens en notebooks ni en .zshrc. Usar HF_HUB_DISABLE_IMPLICIT_TOKEN=1 en entornos multi-tenant.