| """classroom_memory.py — EWC y Replay Buffer para protección de memoria."""
|
|
|
| from __future__ import annotations
|
|
|
| import random
|
| import time
|
| from collections import deque
|
| from dataclasses import dataclass, field
|
|
|
| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
|
|
|
|
| def compute_ewc_baseline(
|
| model: nn.Module,
|
| tokenizer: object,
|
| ewc_lambda: float,
|
| ewc_samples: int,
|
| seq_len: int,
|
| device: torch.device,
|
| ) -> "EWC":
|
| """Calcula Fisher Information sobre datos que el modelo ya maneja bien.
|
|
|
| Returns:
|
| Instancia de EWC con Fisher calculada.
|
| """
|
| baseline_prompts = [
|
| "def suma(a, b):",
|
| "for i in range(10):",
|
| "class Punto:",
|
| "if x > 0:",
|
| "import os\n",
|
| "def fibonacci(n):",
|
| "return sorted(",
|
| "try:\n ",
|
| "with open('",
|
| "result = [x for x in",
|
| ]
|
|
|
| baseline_tokens: list[torch.Tensor] = []
|
| model.eval()
|
| for prompt in baseline_prompts:
|
| ids = tokenizer.Encode(prompt)
|
| if len(ids) < 4:
|
| continue
|
| for _ in range(20):
|
| if len(ids) > 2:
|
| start = random.randint(0, max(0, len(ids) - 3))
|
| chunk = ids[start : start + min(seq_len, len(ids) - start)]
|
| baseline_tokens.append(torch.tensor(chunk, dtype=torch.long))
|
|
|
| ewc = EWC(model, ewc_lambda)
|
| if baseline_tokens:
|
| ewc.compute_fisher(model, baseline_tokens, device, ewc_samples)
|
|
|
| return ewc
|
|
|
|
|
| class EWC:
|
| """
|
| Elastic Weight Consolidation (Kirkpatrick et al., 2017).
|
|
|
| Simula LTP biológica: identifica pesos importantes (alta Fisher info)
|
| y penaliza moverlos durante entrenamiento nuevo.
|
|
|
| L_total = L_task + (λ/2) * Σ F_i * (θ_i - θ*_i)²
|
| """
|
|
|
| def __init__(self, model: nn.Module, lam: float = 500.0):
|
| self.lam = lam
|
| self.params_star: dict[str, torch.Tensor] = {}
|
| self.fisher: dict[str, torch.Tensor] = {}
|
|
|
| def compute_fisher(
|
| self,
|
| model: nn.Module,
|
| data_loader: list[torch.Tensor],
|
| device: torch.device,
|
| n_samples: int = 200,
|
| ) -> None:
|
| """Calcula la Diagonal Fisher Information Matrix sobre datos existentes."""
|
| model.eval()
|
|
|
| self.params_star = {
|
| n: p.data.clone() for n, p in model.named_parameters() if p.requires_grad
|
| }
|
|
|
| self.fisher = {
|
| n: torch.zeros_like(p.data)
|
| for n, p in model.named_parameters()
|
| if p.requires_grad
|
| }
|
|
|
| n = min(n_samples, len(data_loader))
|
| samples = random.sample(data_loader, n) if len(data_loader) > n else data_loader
|
|
|
| for tokens in samples:
|
| model.zero_grad()
|
| tokens = tokens.to(device)
|
| if tokens.dim() == 1:
|
| tokens = tokens.unsqueeze(0)
|
|
|
| input_ids = tokens[:, :-1]
|
| targets = tokens[:, 1:]
|
| logits, _, _ = model(input_ids)
|
|
|
| loss = F.cross_entropy(
|
| logits.reshape(-1, logits.size(-1)),
|
| targets.reshape(-1),
|
| ignore_index=-100,
|
| )
|
| loss.backward()
|
|
|
| for name, param in model.named_parameters():
|
| if param.requires_grad and param.grad is not None:
|
| self.fisher[name] += param.grad.data.pow(2) / n
|
|
|
| model.zero_grad()
|
|
|
| def penalty(self, model: nn.Module) -> torch.Tensor:
|
| """Calcula la penalización EWC: (λ/2) * Σ F_i * (θ_i - θ*_i)²"""
|
| loss = torch.tensor(0.0, device=next(model.parameters()).device)
|
| for name, param in model.named_parameters():
|
| if name in self.fisher:
|
| loss += (
|
| self.fisher[name] * (param - self.params_star[name]).pow(2)
|
| ).sum()
|
| return (self.lam / 2.0) * loss
|
|
|
|
|
| class ReplayBuffer:
|
| """
|
| Buffer circular de ejemplos exitosos.
|
|
|
| Mezcla ejemplos nuevos con viejos para evitar olvido catastrófico.
|
| Como el replay neuronal durante el sueño: reactiva memorias viejas
|
| mientras integra las nuevas.
|
| """
|
|
|
| def __init__(self, maxsize: int = 100):
|
| self.buffer: deque[dict] = deque(maxlen=maxsize)
|
|
|
| def add(
|
| self,
|
| problem: str,
|
| solution: str,
|
| input_ids: torch.Tensor,
|
| labels: torch.Tensor,
|
| level: int,
|
| ) -> None:
|
| self.buffer.append(
|
| {
|
| "problem": problem,
|
| "solution": solution,
|
| "input_ids": input_ids.cpu(),
|
| "labels": labels.cpu(),
|
| "level": level,
|
| "timestamp": time.time(),
|
| }
|
| )
|
|
|
| def sample(self, n: int) -> list[dict]:
|
| if len(self.buffer) == 0:
|
| return []
|
| n = min(n, len(self.buffer))
|
| return random.sample(list(self.buffer), n)
|
|
|
| def __len__(self) -> int:
|
| return len(self.buffer)
|
|
|
|
|
| @dataclass
|
| class LessonResult:
|
| """Resultado de una lección."""
|
|
|
| lesson_id: int
|
| level: int
|
| problem: str
|
| student_answer: str
|
| teacher_solution: str
|
| correct: bool
|
| feedback: str
|
| loss: float
|
| ewc_penalty: float
|
| brain_score: float
|
| timestamp: float = field(default_factory=time.time)
|
|
|