| """classroom_training.py — Tokenización y paso de entrenamiento del Classroom."""
|
|
|
| from __future__ import annotations
|
|
|
| import unicodedata
|
| from dataclasses import dataclass
|
| from typing import Protocol
|
|
|
| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
|
|
|
|
| def _norm_for_tok(text: str) -> str:
|
| """Elimina diacríticos y reemplaza puntuación invertida para compatibilidad
|
| con el tokenizador pampar_48k (que no tiene á/é/ó/ñ/¿/¡ en vocabulario)."""
|
| nfkd = unicodedata.normalize("NFKD", text)
|
| without_accents = "".join(c for c in nfkd if unicodedata.category(c) != "Mn")
|
| return without_accents.replace("¿", "?").replace("¡", "!")
|
|
|
|
|
| class HasLRConfig(Protocol):
|
| """Protocolo mínimo para la config de LR."""
|
|
|
| lr_base: float
|
| lr_llaves_mult: float
|
| lr_attn_mult: float
|
| lr_embed_mult: float
|
| lr_ffn_mult: float
|
|
|
|
|
| def setup_optimizer(
|
| model: nn.Module,
|
| config: HasLRConfig,
|
| ) -> tuple[torch.optim.Optimizer, float, list[dict]]:
|
| """Configura AdamW con Learning Rate diferencial.
|
|
|
| Returns:
|
| (optimizer, baseline_lr, param_group_info) donde param_group_info
|
| es una lista de dicts con 'label', 'lr', 'n_params' para logging.
|
| """
|
| param_groups: list[dict] = []
|
| assigned: set[str] = set()
|
|
|
|
|
| llaves_params = []
|
| for name, param in model.named_parameters():
|
| if any(k in name for k in ["talamo", "llaves", "attn_proj"]):
|
| if param.requires_grad:
|
| llaves_params.append(param)
|
| assigned.add(name)
|
| if llaves_params:
|
| param_groups.append(
|
| {
|
| "params": llaves_params,
|
| "lr": config.lr_base * config.lr_llaves_mult,
|
| "label": "llaves_talamo",
|
| }
|
| )
|
|
|
|
|
| attn_params = []
|
| for name, param in model.named_parameters():
|
| if name not in assigned and any(
|
| k in name for k in ["attn", "q_proj", "k_proj", "v_proj", "o_proj"]
|
| ):
|
| if param.requires_grad:
|
| attn_params.append(param)
|
| assigned.add(name)
|
| if attn_params:
|
| param_groups.append(
|
| {
|
| "params": attn_params,
|
| "lr": config.lr_base * config.lr_attn_mult,
|
| "label": "attention",
|
| }
|
| )
|
|
|
|
|
| embed_params = []
|
| for name, param in model.named_parameters():
|
| if name not in assigned and any(k in name for k in ["tok_emb", "emb"]):
|
| if param.requires_grad:
|
| embed_params.append(param)
|
| assigned.add(name)
|
| if embed_params:
|
| param_groups.append(
|
| {
|
| "params": embed_params,
|
| "lr": config.lr_base * config.lr_embed_mult,
|
| "label": "embeddings",
|
| }
|
| )
|
|
|
|
|
| ffn_params = []
|
| for name, param in model.named_parameters():
|
| if name not in assigned and param.requires_grad:
|
| ffn_params.append(param)
|
| assigned.add(name)
|
| if ffn_params:
|
| param_groups.append(
|
| {
|
| "params": ffn_params,
|
| "lr": config.lr_base * config.lr_ffn_mult,
|
| "label": "ffn_generation",
|
| }
|
| )
|
|
|
| optimizer = torch.optim.AdamW(param_groups, betas=(0.9, 0.95), weight_decay=0.01)
|
|
|
| info = []
|
| for g in param_groups:
|
| n = sum(p.numel() for p in g["params"])
|
| info.append({"label": g["label"], "lr": g["lr"], "n_params": n})
|
|
|
| return optimizer, config.lr_base, info
|
|
|
|
|
| def tokenize_pair(
|
| tokenizer: object,
|
| problem: str,
|
| solution: str,
|
| seq_len: int,
|
| ) -> tuple[torch.Tensor, torch.Tensor]:
|
| """Tokeniza un par problema→solución con máscara de loss.
|
|
|
| Returns:
|
| (input_ids, labels) donde labels tiene -100 en el prompt
|
| para que el loss solo se compute sobre la solución.
|
| """
|
| prompt = f"### Problem:\n{problem}\n### Solution:\n```python\n"
|
| prompt_ids = tokenizer.Encode(_norm_for_tok(prompt))
|
| solution_ids = tokenizer.Encode(_norm_for_tok(solution + "\n```"))
|
|
|
| all_ids = prompt_ids + solution_ids
|
| if len(all_ids) > seq_len:
|
| all_ids = all_ids[:seq_len]
|
| n_prompt = min(len(prompt_ids), len(all_ids))
|
| else:
|
| n_prompt = len(prompt_ids)
|
|
|
| input_ids = torch.tensor(all_ids, dtype=torch.long)
|
| labels = input_ids.clone()
|
| labels[:n_prompt] = -100
|
|
|
| return input_ids, labels
|
|
|
|
|
| def tokenize_teaching(
|
| tokenizer: object,
|
| text: str,
|
| seq_len: int,
|
| ) -> tuple[torch.Tensor, torch.Tensor]:
|
| """Tokeniza contenido de enseñanza donde TODOS los tokens son entrenables.
|
|
|
| Se usa para que el alumno absorba explicaciones y ejemplos del mentor.
|
| """
|
| ids = tokenizer.Encode(_norm_for_tok(text))
|
| if len(ids) > seq_len:
|
| ids = ids[:seq_len]
|
| input_ids = torch.tensor(ids, dtype=torch.long)
|
| labels = input_ids.clone()
|
| return input_ids, labels
|
|
|
|
|
| def train_step(
|
| model: nn.Module,
|
| optimizer: torch.optim.Optimizer,
|
| ewc: object,
|
| examples: list[tuple[torch.Tensor, torch.Tensor]],
|
| device: torch.device,
|
| ) -> tuple[float, float, dict]:
|
| """Un paso de entrenamiento con loss masking.
|
|
|
| Args:
|
| model: Modelo PamparV3.
|
| optimizer: Optimizer con LR diferencial.
|
| ewc: Objeto EWC para penalización.
|
| examples: lista de (input_ids, labels) donde labels=-100 en prompt.
|
| device: dispositivo de cómputo.
|
|
|
| Returns: (loss_ce, ewc_penalty, last_info)
|
| """
|
| model.train()
|
| optimizer.zero_grad()
|
|
|
| total_loss = torch.tensor(0.0, device=device)
|
| total_ce = 0.0
|
| n = 0
|
| last_info: dict = {}
|
|
|
| for input_ids, labels in examples:
|
| input_ids = input_ids.to(device)
|
| labels = labels.to(device)
|
| if input_ids.dim() == 1:
|
| input_ids = input_ids.unsqueeze(0)
|
| labels = labels.unsqueeze(0)
|
| if input_ids.shape[1] < 3:
|
| continue
|
|
|
| inp = input_ids[:, :-1]
|
| tgt = labels[:, 1:]
|
| logits, _, info = model(inp)
|
| last_info = info
|
|
|
| loss_ce = F.cross_entropy(
|
| logits.reshape(-1, logits.size(-1)),
|
| tgt.reshape(-1),
|
| ignore_index=-100,
|
| )
|
| total_loss = total_loss + loss_ce
|
| total_ce += loss_ce.item()
|
| n += 1
|
|
|
| if n == 0:
|
| return 0.0, 0.0, {}
|
|
|
| total_loss = total_loss / n
|
|
|
| ewc_pen = ewc.penalty(model)
|
| total_loss = total_loss + ewc_pen
|
|
|
| total_loss.backward()
|
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| optimizer.step()
|
|
|
| return total_ce / n, ewc_pen.item(), last_info
|
|
|