PowerMachine commited on
Commit
b6f8342
·
verified ·
1 Parent(s): 8edbc25

Delete utils/ewc.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. utils/ewc.py +0 -382
utils/ewc.py DELETED
@@ -1,382 +0,0 @@
1
- """
2
- EWC (Elastic Weight Consolidation) — Aprendizado Contínuo sem Catastrophic Forgetting.
3
-
4
- Implementa:
5
- - Cálculo da diagonal da Matriz de Informação de Fisher (empirical Fisher)
6
- - Estado consolidado (theta_star + Fisher) para múltiplas tarefas
7
- - Online EWC (média exponencial de Fisher entre tarefas)
8
- - Penalidade diferenciável L_EWC = sum_i (lambda/2) * F_i * (theta_i - theta_star_i)^2
9
-
10
- Referência matemática: docs/MATH_ANALYSIS.md seção 2.
11
-
12
- Autor: CNN-BiGRU Project
13
- """
14
- from __future__ import annotations
15
-
16
- import logging
17
- from dataclasses import dataclass, field
18
- from typing import Dict, List, Optional, Tuple
19
-
20
- import torch
21
- import torch.nn as nn
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
-
26
- # ============================================================================
27
- # Configuração
28
- # ============================================================================
29
-
30
- @dataclass
31
- class EWCConfig:
32
- """Configuração do EWC (Elastic Weight Consolidation)."""
33
- enabled: bool = True
34
- # Peso da penalidade EWC na perda total
35
- lambda_ewc: float = 100.0
36
- # Número de amostras usadas para estimar a diagonal de Fisher
37
- n_samples_fisher: int = 200
38
- # Decaimento para Online EWC: F_agg = gamma*F_old + (1-gamma)*F_new
39
- # Se None, usa soma direta (EWC padrão — explode em memória após muitas tarefas)
40
- online_gamma: Optional[float] = 0.9
41
- # Clip para estabilizar Fisher (evitar valores extremos)
42
- fisher_clip_max: float = 1e4
43
- # Tipos de parâmetros a penalizar (outros como LayerNorm/bias podem ser excluídos)
44
- include_param_names: Tuple[str, ...] = ("weight",)
45
- exclude_param_names: Tuple[str, ...] = ("bias", "layernorm", "layer_norm", "bn", "batchnorm")
46
- # Device padrão
47
- device: str = "cpu"
48
-
49
-
50
- # ============================================================================
51
- # Estado EWC
52
- # ============================================================================
53
-
54
- class EWCState:
55
- """
56
- Mantém o estado consolidado do EWC: lista de (theta_star, fisher) por tarefa.
57
-
58
- Para Online EWC, mantém uma única Fisher agregada e theta_star atual.
59
- """
60
-
61
- def __init__(self, config: EWCConfig):
62
- self.config = config
63
- # Lista de tarefas: cada entrada é um dict {param_name: (theta_star, fisher)}
64
- self.tasks: List[Dict[str, Tuple[torch.Tensor, torch.Tensor]]] = []
65
- # Online: theta_star e fisher agregados
66
- self.online_theta: Dict[str, torch.Tensor] = {}
67
- self.online_fisher: Dict[str, torch.Tensor] = {}
68
-
69
- # ----------------------------------------------------------------------
70
- # Seleção de parâmetros
71
- # ----------------------------------------------------------------------
72
-
73
- def _select_params(self, model: nn.Module) -> List[Tuple[str, torch.Tensor]]:
74
- """Seleciona parâmetros treináveis elegíveis para EWC."""
75
- selected = []
76
- for name, param in model.named_parameters():
77
- if not param.requires_grad:
78
- continue
79
- name_lower = name.lower()
80
- # Excluir nomes na lista negra
81
- if any(ex in name_lower for ex in self.config.exclude_param_names):
82
- continue
83
- # Incluir apenas nomes na lista branca (se especificada)
84
- if self.config.include_param_names and not any(
85
- inc in name_lower for inc in self.config.include_param_names
86
- ):
87
- continue
88
- selected.append((name, param))
89
- return selected
90
-
91
- # ----------------------------------------------------------------------
92
- # Cálculo da Diagonal de Fisher
93
- # ----------------------------------------------------------------------
94
-
95
- @torch.no_grad()
96
- def compute_fisher(
97
- self,
98
- model: nn.Module,
99
- forward_fn,
100
- n_samples: Optional[int] = None,
101
- ) -> Dict[str, torch.Tensor]:
102
- """
103
- Calcula a diagonal da matriz de Fisher Information (empirical Fisher).
104
-
105
- Args:
106
- model: modelo treinado (parâmetros congelados idealmente)
107
- forward_fn: callable que retorna (logits, target) ou logits.
108
- Deve aceitar nenhum argumento ou um índice.
109
- n_samples: número de amostras (default: config.n_samples_fisher)
110
-
111
- Returns:
112
- Dict {param_name: fisher_diag_tensor} (mesmo shape do parâmetro)
113
-
114
- Fórmula:
115
- F_i = (1/N) sum_n [grad_i log p(y_n* | x_n; theta*)]^2
116
- onde y_n* = argmax_y p(y | x_n; theta*)
117
- """
118
- if n_samples is None:
119
- n_samples = self.config.n_samples_fisher
120
-
121
- model.eval()
122
- selected = self._select_params(model)
123
-
124
- # Inicializar acumulador de grad^2
125
- fisher: Dict[str, torch.Tensor] = {
126
- name: torch.zeros_like(param.detach(), device=param.device)
127
- for name, param in selected
128
- }
129
- count = 0
130
-
131
- # Garantir que requires_grad está ativo para os parâmetros selecionados
132
- # durante todo o cálculo da Fisher (restaurado no finally)
133
- original_requires_grad: Dict[str, bool] = {}
134
- for name, param in selected:
135
- original_requires_grad[name] = param.requires_grad
136
- param.requires_grad_(True)
137
-
138
- try:
139
- for i in range(n_samples):
140
- try:
141
- # Resetar gradientes
142
- for _, param in selected:
143
- if param.grad is not None:
144
- param.grad.zero_()
145
-
146
- # Forward + Backward — TODO em contexto com grad habilitado
147
- # IMPORTANTE: usar torch.set_grad_enabled(True) para forçar gradientes
148
- # mesmo se o caller estiver em contexto no_grad. Deve cobrir TANTO
149
- # o forward QUANTO o backward (senão as operações pós-forward perdem grad_fn).
150
- prev_grad_mode = torch.is_grad_enabled()
151
- torch.set_grad_enabled(True)
152
- try:
153
- result = forward_fn(i)
154
-
155
- if isinstance(result, tuple) and len(result) == 2:
156
- logits, target = result
157
- else:
158
- logits = result
159
- # Usar y* = argmax logits (empirical Fisher)
160
- target = logits.argmax(dim=-1)
161
-
162
- # Garantir que logits é [B, V] ou [B, T, V] → flatten
163
- if logits.dim() == 3:
164
- B, T, V = logits.shape
165
- logits_flat = logits.reshape(-1, V)
166
- target_flat = target.reshape(-1)
167
- else:
168
- logits_flat = logits
169
- target_flat = target.reshape(-1)
170
-
171
- # Log-verossimilhança (CrossEntropy = -log p)
172
- log_prob = torch.nn.functional.log_softmax(logits_flat, dim=-1)
173
- # Pegar log_prob das classes target
174
- picked = log_prob.gather(1, target_flat.unsqueeze(-1)).sum()
175
- # O sinal negativo: queremos grad de log p (não -log p)
176
- loss_for_grad = -picked
177
-
178
- # Backward — AINDA em grad mode (não restauramos ainda)
179
- loss_for_grad.backward(retain_graph=False)
180
- finally:
181
- torch.set_grad_enabled(prev_grad_mode)
182
-
183
- # Acumular grad^2
184
- for name, param in selected:
185
- if param.grad is not None:
186
- g2 = param.grad.detach() ** 2
187
- fisher[name] += g2
188
-
189
- count += 1
190
-
191
- except Exception as e:
192
- logger.warning(f"EWC fisher sample {i} falhou: {e}")
193
- continue
194
- finally:
195
- # Restaurar requires_grad original
196
- for name, param in selected:
197
- if name in original_requires_grad:
198
- param.requires_grad_(original_requires_grad[name])
199
-
200
- if count == 0:
201
- logger.error("EWC: nenhuma amostra válida para Fisher — usando zeros")
202
- return fisher
203
-
204
- # Média
205
- for name in fisher:
206
- fisher[name] = (fisher[name] / count).clamp(0, self.config.fisher_clip_max)
207
-
208
- return fisher
209
- # ----------------------------------------------------------------------
210
- # Consolidar tarefa
211
- # ----------------------------------------------------------------------
212
-
213
- def consolidate(
214
- self,
215
- model: nn.Module,
216
- fisher: Optional[Dict[str, torch.Tensor]] = None,
217
- forward_fn=None,
218
- ) -> None:
219
- """
220
- Consolida o estado atual do modelo como uma nova tarefa.
221
-
222
- Args:
223
- model: modelo treinado
224
- fisher: Fisher diagonal pré-computada (se None, calcula via forward_fn)
225
- forward_fn: necessário se fisher=None
226
- """
227
- if fisher is None:
228
- if forward_fn is None:
229
- raise ValueError("EWC.consolidate requer forward_fn quando fisher=None")
230
- fisher = self.compute_fisher(model, forward_fn)
231
-
232
- # Snapshot dos parâmetros atuais (theta_star)
233
- theta_star: Dict[str, torch.Tensor] = {}
234
- for name, param in model.named_parameters():
235
- if name in fisher:
236
- theta_star[name] = param.detach().clone()
237
-
238
- task_state = {
239
- name: (theta_star[name], fisher[name]) for name in fisher
240
- }
241
- self.tasks.append(task_state)
242
-
243
- # Atualizar estado online
244
- if self.config.online_gamma is not None:
245
- gamma = self.config.online_gamma
246
- if not self.online_fisher:
247
- # Primeira tarefa
248
- for name in fisher:
249
- self.online_fisher[name] = fisher[name].clone()
250
- self.online_theta[name] = theta_star[name].clone()
251
- else:
252
- # Online EWC: F_agg = gamma * F_old + (1-gamma) * F_new
253
- # theta_agg = gamma * theta_old + (1-gamma) * theta_new
254
- for name in fisher:
255
- if name in self.online_fisher:
256
- self.online_fisher[name] = (
257
- gamma * self.online_fisher[name]
258
- + (1 - gamma) * fisher[name]
259
- ).clamp(0, self.config.fisher_clip_max)
260
- self.online_theta[name] = (
261
- gamma * self.online_theta[name]
262
- + (1 - gamma) * theta_star[name]
263
- )
264
- else:
265
- self.online_fisher[name] = fisher[name].clone()
266
- self.online_theta[name] = theta_star[name].clone()
267
- else:
268
- # EWC padrão — somar Fishers de todas as tarefas
269
- if not self.online_fisher:
270
- for name in fisher:
271
- self.online_fisher[name] = fisher[name].clone()
272
- self.online_theta[name] = theta_star[name].clone()
273
- else:
274
- for name in fisher:
275
- self.online_fisher[name] = (
276
- self.online_fisher[name] + fisher[name]
277
- ).clamp(0, self.config.fisher_clip_max)
278
- # Theta online atualiza para o mais recente
279
- self.online_theta[name] = theta_star[name].clone()
280
-
281
- logger.info(
282
- f"EWC consolidado: tarefa #{len(self.tasks)}, "
283
- f"parâmetros rastreados: {len(theta_star)}"
284
- )
285
-
286
- # ----------------------------------------------------------------------
287
- # Penalidade EWC
288
- # ----------------------------------------------------------------------
289
-
290
- def penalty(self, model: nn.Module) -> torch.Tensor:
291
- """
292
- Calcula a penalidade EWC: L_EWC = sum_i (lambda/2) * F_i * (theta_i - theta_star_i)^2
293
-
294
- Usa o estado online (agregado) se disponível, caso contrário soma de
295
- todas as tarefas (EWC padrão).
296
-
297
- Returns:
298
- Escalar (tensor 0-dim) com a penalidade.
299
- """
300
- if not self.config.enabled or not self.online_fisher:
301
- # Retorna zero no device do modelo
302
- try:
303
- device = next(model.parameters()).device
304
- except StopIteration:
305
- device = torch.device(self.config.device)
306
- return torch.zeros((), device=device)
307
-
308
- total = None
309
- for name, param in model.named_parameters():
310
- if name not in self.online_fisher:
311
- continue
312
- F = self.online_fisher[name]
313
- theta_s = self.online_theta[name]
314
- # Penalidade quadrática
315
- diff = param - theta_s.to(param.device)
316
- pen = (F.to(param.device) * diff.pow(2)).sum()
317
- if total is None:
318
- total = pen
319
- else:
320
- total = total + pen
321
-
322
- if total is None:
323
- try:
324
- device = next(model.parameters()).device
325
- except StopIteration:
326
- device = torch.device(self.config.device)
327
- return torch.zeros((), device=device)
328
-
329
- return (self.config.lambda_ewc * 0.5) * total
330
-
331
- # ----------------------------------------------------------------------
332
- # Utilitários
333
- # ----------------------------------------------------------------------
334
-
335
- def num_tasks(self) -> int:
336
- return len(self.tasks)
337
-
338
- def is_active(self) -> bool:
339
- return self.config.enabled and bool(self.online_fisher)
340
-
341
- def state_dict(self) -> Dict:
342
- """Serializa o estado para checkpoint."""
343
- return {
344
- "tasks": [
345
- {name: (ts.cpu(), f.cpu()) for name, (ts, f) in task.items()}
346
- for task in self.tasks
347
- ],
348
- "online_theta": {k: v.cpu() for k, v in self.online_theta.items()},
349
- "online_fisher": {k: v.cpu() for k, v in self.online_fisher.items()},
350
- "config": self.config.__dict__,
351
- }
352
-
353
- def load_state_dict(self, sd: Dict) -> None:
354
- """Carrega o estado a partir de checkpoint."""
355
- self.tasks = [
356
- {name: (ts, f) for name, (ts, f) in task.items()}
357
- for task in sd.get("tasks", [])
358
- ]
359
- self.online_theta = sd.get("online_theta", {})
360
- self.online_fisher = sd.get("online_fisher", {})
361
- if "config" in sd:
362
- for k, v in sd["config"].items():
363
- if hasattr(self.config, k):
364
- setattr(self.config, k, v)
365
-
366
-
367
- # ============================================================================
368
- # API conveniente
369
- # ============================================================================
370
-
371
- def apply_ewc_penalty(model: nn.Module, ewc_state: EWCState) -> torch.Tensor:
372
- """
373
- Atalho: aplica a penalidade EWC ao modelo.
374
-
375
- Uso típico no trainer:
376
- loss = task_loss + ewc_penalty
377
- ewc_penalty = apply_ewc_penalty(model, ewc_state)
378
- """
379
- return ewc_state.penalty(model)
380
-
381
-
382
- __all__ = ["EWCConfig", "EWCState", "apply_ewc_penalty"]