PAMPAr-Coder / scripts /classroom_training.py
lucas-mella's picture
feat: upload PAMPAr-Coder code and documentation
a2d6a0d verified
Raw
History Blame Contribute Delete
7.03 kB
"""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()
# Grupo 1: LLAVES / Tálamo — casi congelado
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",
}
)
# Grupo 2: Atención — aprende lento
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",
}
)
# Grupo 3: Embeddings — aprende lento
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",
}
)
# Grupo 4: FFN / StreamFFN / todo lo demás
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