File size: 11,485 Bytes
49b8205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
"""auto_learner.py — Auto-aprendizado: ajuste dinâmico de LR e controle de gradiente.

Implementa as seções 9-10 de dados.txt:

    9. AJUSTE DINÂMICO DAS TAXAS DE APRENDIZADO:
        fator_G = exp(-κ * ||grad_G||) * (1 + v_media) / 2
        novo_eta_G = eta_G * fator_G

        fator_V = exp(-κ * ||grad_V||) * (1 + L_V_media)^{-1}
        novo_eta_V = eta_V * fator_V

    10. CONTROLE DE EXPLOSÃO DE GRADIENTE E ESPECTRAL NORM:
        - clip_grad_norm_(params, GRAD_CLIP)
        - Spectral normalization (power iteration) para Conv1D e Linear
"""
from __future__ import annotations

import logging
import math
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple

import torch
import torch.nn as nn

logger = logging.getLogger(__name__)


@dataclass
class AutoLearnConfig:
    """Configuração do auto-aprendizado."""
    kappa_curv: float = 0.01     # κ — fator de ajuste de η com norma do gradiente
    grad_clip: float = 1.0       # τ — clipping de gradiente (norma L2)
    spectral_radius: float = 1.0 # limite para norma espectral
    lr_min: float = 1e-6         # LR mínimo
    lr_max: float = 1e-2         # LR máximo
    initial_lr_G: float = 1e-3   # LR inicial do gerador
    initial_lr_V: float = 1e-3   # LR inicial do verificador
    use_spectral_norm: bool = True
    apply_after_step: bool = True
    # NOVO: peso L2 usado pelo otimizador AdamW (corresponde a L2_REG do dados.txt)
    l2_reg: float = 1e-5
    # NOVO: potências de iteração para spectral norm (1 era muito pouco)
    spectral_n_iters: int = 1
    # NOVO: device padrão para buffers internos
    device: str = "cpu"


class AutoLearner:
    """Controla ajuste dinâmico de LR, clipping e spectral norm.

    Não é um nn.Module — é um controlador que atua sobre modelos externos.
    """

    def __init__(self, config: AutoLearnConfig):
        self.cfg = config
        self.current_lr_G = config.initial_lr_G
        self.current_lr_V = config.initial_lr_V
        self.history: List[Dict] = []

    def compute_total_grad_norm(self, model: nn.Module) -> float:
        """Calcula a norma L2 total dos gradientes do modelo."""
        total = 0.0
        for p in model.parameters():
            if p.grad is not None:
                total += p.grad.data.norm(2).item() ** 2
        return math.sqrt(total)

    def update_learning_rates(
        self,
        grad_G_norm: float,
        grad_V_norm: float,
        v_mean: float,
        L_V_mean: float,
    ) -> Tuple[float, float]:
        """Atualiza dinamicamente as taxas de aprendizado.

        Args:
            grad_G_norm: norma L2 do gradiente do gerador
            grad_V_norm: norma L2 do gradiente do verificador
            v_mean: média das probabilidades do verificador
            L_V_mean: perda média do verificador

        Returns:
            (novo_eta_G, novo_eta_V)
        """
        # Fator do gerador: exp(-κ * ||grad||) * (1 + v_media) / 2
        fator_G = math.exp(-self.cfg.kappa_curv * grad_G_norm) * ((1.0 + v_mean) / 2.0)
        novo_eta_G = self.current_lr_G * fator_G

        # Fator do verificador: exp(-κ * ||grad||) * (1 + L_V)^{-1}
        # Evita divisão por zero
        fator_V = math.exp(-self.cfg.kappa_curv * grad_V_norm) / max(1.0 + L_V_mean, 1e-6)
        novo_eta_V = self.current_lr_V * fator_V

        # Clamp para evitar valores absurdos
        novo_eta_G = max(self.cfg.lr_min, min(self.cfg.lr_max, novo_eta_G))
        novo_eta_V = max(self.cfg.lr_min, min(self.cfg.lr_max, novo_eta_V))

        self.current_lr_G = novo_eta_G
        self.current_lr_V = novo_eta_V

        self.history.append({
            "lr_G": novo_eta_G,
            "lr_V": novo_eta_V,
            "grad_G_norm": grad_G_norm,
            "grad_V_norm": grad_V_norm,
            "v_mean": v_mean,
            "L_V_mean": L_V_mean,
        })

        return novo_eta_G, novo_eta_V

    def apply_clipping(self, *models: nn.Module) -> None:
        """Aplica gradient clipping global nos modelos."""
        for model in models:
            if model is not None:
                torch.nn.utils.clip_grad_norm_(model.parameters(), self.cfg.grad_clip)

    def apply_spectral_norm(self, model: nn.Module) -> int:
        """Aplica normalização espectral via power iteration em Conv1d/Linear.

        Após cada step, força ||W||_2 <= spectral_radius.

        CORREÇÃO: O bug original armazenava `module._spec_u` como atributo
        comum (não buffer), que NÃO migra com `.to(device)`. Agora usamos
        `register_buffer` na primeira chamada para garantir movimentação
        corta com o device do modelo.
        """
        if not self.cfg.use_spectral_norm:
            return 0
        n_normalized = 0
        for module in model.modules():
            if isinstance(module, (nn.Linear, nn.Conv1d, nn.Conv2d)):
                if module.weight is not None and module.weight.dim() >= 2:
                    try:
                        with torch.no_grad():
                            # Power iteration (n_iters iterações)
                            W = module.weight
                            # Reshape para 2D se necessário
                            W_flat = W.reshape(W.size(0), -1)
                            # u, v aleatórios (ou reutiliza se já existe)
                            buffer_name = "_spec_u"
                            if not hasattr(module, buffer_name):
                                u = torch.randn(W_flat.size(0), 1, device=W.device, dtype=W.dtype)
                                u = u / u.norm().clamp(min=1e-8)
                                # Registrar como buffer (move com .to(device))
                                module.register_buffer(buffer_name, u, persistent=False)
                            else:
                                u = getattr(module, buffer_name)
                                # Garantir que está no device correto
                                if u.device != W.device:
                                    u = u.to(W.device)
                                    setattr(module, buffer_name, u)
                            # n_iters iterações de power iteration
                            for _ in range(self.cfg.spectral_n_iters):
                                v = torch.matmul(W_flat.T, u)
                                v = v / v.norm().clamp(min=1e-8)
                                u = torch.matmul(W_flat, v)
                                u = u / u.norm().clamp(min=1e-8)
                            # Atualizar buffer (in-place para manter no mesmo device)
                            u_new = u.detach()
                            getattr(module, buffer_name).copy_(u_new)
                            sigma = (u.T @ W_flat @ v).item()
                            if sigma > self.cfg.spectral_radius and sigma > 0:
                                scale = self.cfg.spectral_radius / sigma
                                module.weight.data.mul_(scale)
                                n_normalized += 1
                    except Exception as e:
                        logger.debug("Spectral norm falhou em %s: %s", type(module).__name__, e)
        return n_normalized

    def set_optimizer_lr(self, optimizer: torch.optim.Optimizer, lr: float) -> None:
        """Atualiza a taxa de aprendizado de um otimizador."""
        for pg in optimizer.param_groups:
            pg["lr"] = lr


def orthogonal_init_model(model: nn.Module) -> int:
    """Aplica inicialização ortogonal em todas as camadas Linear/Conv/GRUCell."""
    n = 0
    for module in model.modules():
        if isinstance(module, (nn.Linear, nn.Conv1d, nn.Conv2d)):
            try:
                nn.init.orthogonal_(module.weight)
                if module.bias is not None:
                    nn.init.zeros_(module.bias)
                n += 1
            except Exception:
                pass
        elif isinstance(module, (nn.GRUCell, nn.GRU, nn.LSTM)):
            for name, p in module.named_parameters():
                if "weight" in name and p.dim() >= 2:
                    try:
                        nn.init.orthogonal_(p)
                        n += 1
                    except Exception:
                        pass
                elif "bias" in name:
                    nn.init.zeros_(p)
    return n


def estimate_curvature(
    model: nn.Module,
    loss_fn,
    *args,
    eps: float = 1e-3,
    n_samples: int = 5,
) -> torch.Tensor:
    """Estima curvatura da perda via diferenças finitas.

    Para uma amostra de parâmetros, calcula:
        curv ≈ (L(θ + ε) - 2L(θ) + L(θ - ε)) / ε²

    Retorna a média das estimativas.

    CORREÇÃO: O bug original mutava `flat[i]` in-place sem try/finally,
    corrompendo o modelo se uma exceção ocorresse entre `+eps` e o restore.
    Agora usamos try/finally para garantir restauração sempre.
    """
    curvatures = []
    params = [p for p in model.parameters() if p.requires_grad and p.dim() >= 2]
    if not params:
        # Device-safe zero
        try:
            device = next(model.parameters()).device
        except StopIteration:
            device = torch.device("cpu")
        return torch.zeros((), device=device)

    # Subamostra
    n_samples = min(n_samples, len(params))
    indices = torch.randperm(len(params))[:n_samples]

    for idx in indices:
        p = params[idx]
        # Pega um elemento aleatório
        flat = p.data.view(-1)
        if flat.numel() == 0:
            continue
        i = torch.randint(0, flat.numel(), (1,)).item()
        orig = flat[i].item()

        # TRY/FINALLY para garantir restauração sempre
        try:
            with torch.no_grad():
                # L(θ)
                try:
                    loss_0 = loss_fn(*args)
                    if isinstance(loss_0, dict):
                        loss_0 = loss_0["total"]
                    loss_0 = float(loss_0)
                except Exception as e:
                    logger.debug("curvature L(θ) falhou: %s", e)
                    continue

                # L(θ + ε)
                flat[i] = orig + eps
                try:
                    loss_p = loss_fn(*args)
                    if isinstance(loss_p, dict):
                        loss_p = loss_p["total"]
                    loss_p = float(loss_p)
                except Exception as e:
                    logger.debug("curvature L(θ+ε) falhou: %s", e)
                    continue

                # L(θ - ε)
                flat[i] = orig - eps
                try:
                    loss_m = loss_fn(*args)
                    if isinstance(loss_m, dict):
                        loss_m = loss_m["total"]
                    loss_m = float(loss_m)
                except Exception as e:
                    logger.debug("curvature L(θ-ε) falhou: %s", e)
                    continue

                curv = (loss_p - 2 * loss_0 + loss_m) / (eps ** 2)
                curvatures.append(curv)
        finally:
            # SEMPRE restaurar o valor original
            flat[i] = orig

    if not curvatures:
        try:
            device = next(model.parameters()).device
        except StopIteration:
            device = torch.device("cpu")
        return torch.zeros((), device=device)

    return torch.tensor(sum(curvatures) / len(curvatures))


__all__ = [
    "AutoLearnConfig",
    "AutoLearner",
    "orthogonal_init_model",
    "estimate_curvature",
]