WalterWi's picture
Upload 10 files
f343795 verified
Raw History Blame Contribute Delete
6.5 kB
"""Entrena un clasificador de texto con Hugging Face Transformers y lo exporta.
Problema: clasificar comentarios sobre departamentos en "positivo" o "negativo".
Es un BERT diminuto (2 capas, 32 dimensiones) entrenado DESDE CERO, con un tokenizer propio
de vocabulario cerrado. As铆 se ven todas las piezas que publica un modelo de Transformers:
config.json la arquitectura (cu谩ntas capas, dimensiones, etiquetas)
model.safetensors los pesos
tokenizer.json el vocabulario y c贸mo se corta el texto en tokens
tokenizer_config.json la configuraci贸n del tokenizer y sus tokens especiales
En un caso real no se entrena desde cero: se hace fine-tuning de un modelo preentrenado
(por ejemplo, uno en espa帽ol del Hub). Las piezas exportadas son las mismas.
Uso:
python train.py
"""
import json
import random
from itertools import product
from pathlib import Path
import torch
from sklearn.metrics import accuracy_score
from tokenizers import Tokenizer, models, normalizers, pre_tokenizers, processors, trainers
from transformers import BertConfig, BertForSequenceClassification, PreTrainedTokenizerFast
CARPETA = Path(__file__).parent
ETIQUETAS = ["negativo", "positivo"]
random.seed(42)
torch.manual_seed(42)
# ---------------------------------------------------------------------------
# 1. Datos: comentarios armados con plantillas (dataset sint茅tico y chico)
# ---------------------------------------------------------------------------
PLANTILLAS = [
"el departamento es muy {a}",
"me pareci贸 un lugar {a}",
"la verdad es {a} y {b}",
"un depto {a}, bastante {b}",
"es {a} pero sobre todo {b}",
]
POSITIVOS = ["luminoso", "amplio", "tranquilo", "c贸modo", "lindo", "seguro", "moderno", "c谩lido"]
NEGATIVOS = ["oscuro", "chico", "ruidoso", "h煤medo", "feo", "inseguro", "viejo", "fr铆o"]
def armar_ejemplos(adjetivos, etiqueta):
ejemplos = []
for plantilla, a, b in product(PLANTILLAS, adjetivos, adjetivos):
if a != b:
ejemplos.append((plantilla.format(a=a, b=b), etiqueta))
return ejemplos
datos = armar_ejemplos(POSITIVOS, 1) + armar_ejemplos(NEGATIVOS, 0)
datos = list(dict.fromkeys(datos)) # sin repetidos
random.shuffle(datos)
corte = int(len(datos) * 0.8)
train, test = datos[:corte], datos[corte:]
print(f"{len(train)} comentarios de entrenamiento, {len(test)} de test")
# ---------------------------------------------------------------------------
# 2. Tokenizer: aprende el vocabulario de los textos de entrenamiento
# ---------------------------------------------------------------------------
tok = Tokenizer(models.WordLevel(unk_token="[UNK]"))
tok.normalizer = normalizers.Sequence([normalizers.NFD(), normalizers.Lowercase(), normalizers.StripAccents()])
tok.pre_tokenizer = pre_tokenizers.Whitespace()
tok.train_from_iterator(
[texto for texto, _ in train],
trainers.WordLevelTrainer(special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]"]),
)
tok.post_processor = processors.TemplateProcessing(
single="[CLS] $A [SEP]",
special_tokens=[("[CLS]", tok.token_to_id("[CLS]")), ("[SEP]", tok.token_to_id("[SEP]"))],
)
tokenizer = PreTrainedTokenizerFast(
tokenizer_object=tok,
unk_token="[UNK]", pad_token="[PAD]", cls_token="[CLS]", sep_token="[SEP]",
model_max_length=32,
)
print(f"Vocabulario: {tokenizer.vocab_size} tokens")
# ---------------------------------------------------------------------------
# 3. Modelo: un BERT diminuto con una cabeza de clasificaci贸n de 2 etiquetas
# ---------------------------------------------------------------------------
config = BertConfig(
vocab_size=tokenizer.vocab_size,
hidden_size=32,
num_hidden_layers=2,
num_attention_heads=2,
intermediate_size=64,
max_position_embeddings=32,
num_labels=len(ETIQUETAS),
id2label=dict(enumerate(ETIQUETAS)),
label2id={e: i for i, e in enumerate(ETIQUETAS)},
pad_token_id=tokenizer.pad_token_id,
)
modelo = BertForSequenceClassification(config)
def a_tensores(lote):
textos, etiquetas = zip(*lote)
entrada = tokenizer(list(textos), padding=True, return_tensors="pt", return_token_type_ids=False)
return entrada, torch.tensor(etiquetas)
# ---------------------------------------------------------------------------
# 4. Entrenamiento: el mismo ciclo de siempre (predice, p茅rdida, optimizador)
# ---------------------------------------------------------------------------
optimizador = torch.optim.AdamW(modelo.parameters(), lr=1e-3)
modelo.train()
for epoca in range(15):
random.shuffle(train)
perdida_total = 0.0
for i in range(0, len(train), 16):
entrada, etiquetas = a_tensores(train[i:i + 16])
salida = modelo(**entrada, labels=etiquetas) # la p茅rdida (entrop铆a cruzada) viene en la salida
optimizador.zero_grad()
salida.loss.backward()
optimizador.step()
perdida_total += salida.loss.item()
print(f"茅poca {epoca:2d} 路 p茅rdida {perdida_total / (len(train) / 16):.4f}")
# ---------------------------------------------------------------------------
# 5. Evaluaci贸n con comentarios que el modelo no vio
# ---------------------------------------------------------------------------
modelo.eval()
entrada, etiquetas = a_tensores(test)
with torch.no_grad():
predichas = modelo(**entrada).logits.argmax(dim=-1)
print(f"Accuracy en test: {accuracy_score(etiquetas, predichas):.3f}")
# ---------------------------------------------------------------------------
# 6. Exportaci贸n: modelo (config + pesos) y tokenizer, en la misma carpeta
# ---------------------------------------------------------------------------
modelo.save_pretrained(CARPETA) # config.json + model.safetensors
tokenizer.save_pretrained(CARPETA) # tokenizer.json + tokenizer_config.json
# Compatibilidad: Transformers 5 anota una clase de tokenizer que la versi贸n 4 (la de Colab)
# no conoce. PreTrainedTokenizerFast la entienden las dos.
ruta_config_tok = CARPETA / "tokenizer_config.json"
config_tok = json.loads(ruta_config_tok.read_text(encoding="utf-8"))
config_tok["tokenizer_class"] = "PreTrainedTokenizerFast"
config_tok.pop("backend", None)
ruta_config_tok.write_text(json.dumps(config_tok, indent=2, ensure_ascii=False), encoding="utf-8")
print("Modelo y tokenizer guardados en la carpeta")