File size: 6,497 Bytes
f343795
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
"""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")