File size: 7,836 Bytes
06e4b6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
//! `jepa::nucleo` — extraído do monólito na v0.10.0
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).

use super::*;


// ---------------------------------------------------------------------------
// Parte 1 — núcleo
// ---------------------------------------------------------------------------

/// MLP 2 camadas com SiLU (d → h → d_saida).
pub struct MlpJepa {
    pub l1: Linear,
    pub l2: Linear,
    pub d_entrada: usize,
    pub d_saida: usize,
}

impl MlpJepa {
    pub fn novo(d_entrada: usize, d_oculta: usize, d_saida: usize, dev: &Device) -> Result<Self> {
        let s1 = (1.0 / (d_entrada as f64).sqrt()) * 0.9;
        let s2 = (1.0 / (d_oculta as f64).sqrt()) * 0.9;
        Ok(Self {
            l1: Linear::sem_vies(d_entrada, d_oculta, s1, dev)?,
            l2: Linear::sem_vies(d_oculta, d_saida, s2, dev)?,
            d_entrada,
            d_saida,
        })
    }

    pub fn fwd(&self, x: &Tensor) -> Result<Tensor> {
        let h = candle_nn::ops::silu(&self.l1.fwd(x)?)?;
        self.l2.fwd(&h)
    }

    pub fn vars(&self) -> Vec<Var> {
        [self.l1.vars(), self.l2.vars()].concat()
    }

    /// EMA: `self ← m·self + (1−m)·outro` (usado no codificador-alvo).
    pub fn ema_de(&mut self, outro: &MlpJepa, m: f64) -> Result<()> {
        for (a, b) in self.vars().iter().zip(outro.vars().iter()) {
            let novo = (a.as_tensor().affine(m, 0.0)? + b.as_tensor().affine(1.0 - m, 0.0)?)?;
            a.set(&novo)?;
        }
        Ok(())
    }
}

/// Núcleo JEPA: codificador + preditor + alvo EMA + regularizador VC.
pub struct NucleoJepa {
    pub encoder: MlpJepa,
    pub preditor: MlpJepa,
    pub alvo: MlpJepa,
    /// tabela de posições (blocos) — entrada extra do preditor.
    pub pos: Var,
    pub d_emb: usize,
    pub d_cond: usize,
    pub m: f64,
    pub lambda_vc: f64,
    pub gamma_vc: f64,
}

impl NucleoJepa {
    pub fn novo(
        d_patch: usize,
        d_emb: usize,
        d_cond: usize,
        n_pos: usize,
        m: f64,
        lambda_vc: f64,
        gamma_vc: f64,
        dev: &Device,
    ) -> Result<Self> {
        let encoder = MlpJepa::novo(d_patch, 2 * d_emb, d_emb, dev)?;
        let preditor = MlpJepa::novo(d_emb + d_cond + d_emb, 2 * d_emb, d_emb, dev)?;
        // alvo nasce IGUAL ao encoder (ponto de partida do EMA)
        let mut alvo = MlpJepa::novo(d_patch, 2 * d_emb, d_emb, dev)?;
        for (va, vb) in alvo.vars().iter().zip(encoder.vars().iter()) {
            va.set(vb.as_tensor())?;
        }
        let pos_t = Tensor::randn(0f32, 0.02, (n_pos.max(1), d_emb), dev)?;
        Ok(Self {
            encoder,
            preditor,
            alvo,
            pos: Var::from_tensor(&pos_t)?,
            d_emb,
            d_cond,
            m,
            lambda_vc,
            gamma_vc,
        })
    }

    pub fn vars(&self) -> Vec<Var> {
        let mut v = self.encoder.vars();
        v.extend(self.preditor.vars());
        v.push(self.pos.clone());
        v // NOTA: alvo NÃO entra — é buffer EMA
    }

    pub fn ema_passo(&mut self) -> Result<()> {
        self.alvo.ema_de(&self.encoder, self.m)
    }

    /// Codifica um conjunto de patches (n, d_patch) → (n, d_emb).
    pub fn codificar(&self, patches: &Tensor) -> Result<Tensor> {
        self.encoder.fwd(patches)
    }

    /// Codifica com o alvo EMA — SEM gradiente (detach por construção: as
    /// vars do alvo não estão no otimizador e o tensor é desconectado).
    pub fn alvo_codificar(&self, patches: &Tensor) -> Result<Tensor> {
        Ok(self.alvo.fwd(patches)?.detach())
    }

    /// Prediz o embedding do bloco `i_pos` a partir do contexto `z_ctx`
    /// (d_emb) e da condição `cond` (d_cond; zeros se None).
    pub fn predizer(&self, z_ctx: &Tensor, i_pos: usize, cond: Option<&Tensor>) -> Result<Tensor> {
        // z_ctx: (b, d_emb) ou (d_emb,)
        let b = z_ctx.dim(0).unwrap_or(1);
        let dev = z_ctx.device();
        let pos = self.pos.as_tensor().get(i_pos.min(self.pos.dim(0)?.saturating_sub(1)))?;
        let pos_b = pos.broadcast_as((b, self.d_emb))?;
        let cond_b = match cond {
            Some(c) => {
                if c.dims().len() == 1 && b > 1 {
                    c.broadcast_as((b, self.d_cond))?
                } else {
                    c.clone()
                }
            }
            None => Tensor::zeros((b, self.d_cond), candle_core::DType::F32, dev)?,
        };
        let entrada = Tensor::cat(&[z_ctx, &pos_b, &cond_b], candle_core::D::Minus1)?;
        self.preditor.fwd(&entrada)
    }

    /// Regularizador variância-covariância do lote de incorporações z (n, d).
    ///
    /// **J-1:** no colapso total `z_i = c ∀i` ⇒ `std_j = 0` ⇒ `V = d·γ²`.
    pub fn perda_vc(&self, z: &Tensor) -> Result<Tensor> {
        let n = z.dim(0)? as f64;
        let d = self.d_emb;
        let media = z.mean_keepdim(0)?; // (1, d)
        let centr = z.broadcast_sub(&media)?;
        let var = (centr.sqr()?.sum(0)? / n)?; // (d,)
        let std = var.sqrt()?;
        // dobradiça: max(0, γ − std)²
        let gamma_t =
            Tensor::new(self.gamma_vc as f32, z.device())?.broadcast_as(std.dims())?;
        let gap = gamma_t.broadcast_sub(&std)?.clamp(0.0, f64::INFINITY)?;
        let v = gap.sqr()?.sum_all()?;
        // covariância fora-diagonal: C = centrᵀ·centr/n; perda = ‖C − diag‖²_F/(2d)
        let cov = (centr.transpose(0, 1)?.matmul(&centr)? / n)?; // (d, d)
        let mask_diag = {
            let mut m = vec![0f32; d * d];
            for i in 0..d {
                m[i * d + i] = 1.0;
            }
            Tensor::from_vec(m, (d, d), z.device())?
        };
        let fora = ((&cov * &mask_diag)?.sqr()?.sum_all()? / (2.0 * d.max(1) as f64))?;
        let v_norm = (v / d.max(1) as f64)?;
        Ok((v_norm + fora)?)
    }

    /// Perda JEPA padrão para UM lote: contexto (número variável de patches),
    /// lista de blocos-alvo (posições), condição opcional.
    ///
    /// `z_ctx` = incorporação agregada do contexto (b, d_emb);
    /// `alvos`: para cada elemento do lote, Vec<(i_pos, patches_alvo)>.
    pub fn perda_lote(
        &self,
        z_ctx: &Tensor,
        alvos: &[Vec<(usize, Tensor)>],
        cond: Option<&Tensor>,
        z_ctx_todos: &Tensor,
    ) -> Result<(Tensor, f64, f64)> {
        let b = z_ctx.dim(0)?;
        let dev = z_ctx.device();
        let mut perdas: Vec<Tensor> = Vec::new();
        let mut n_alvos = 0usize;
        for (i, blocos) in alvos.iter().enumerate() {
            for (i_pos, patches_alvo) in blocos {
                // ẑ_b = média das incorporações-alvo dos patches do bloco
                let zt = self.alvo_codificar(patches_alvo)?; // (m, d_emb)
                let zt_m = zt.mean(0)?; // (d_emb,)
                let zt_b = zt_m.broadcast_as((1, self.d_emb))?;
                let zc = z_ctx.get(i)?.broadcast_as((1, self.d_emb))?;
                let c_i = cond.map(|c| c.get(i)?.broadcast_as((1, self.d_cond))).transpose()?;
                let pred = self.predizer(&zc, *i_pos, c_i.as_ref())?;
                let dif = (&pred - &zt_b)?;
                let l = (dif.sqr()?.sum_all()? / self.d_emb as f64)?;
                perdas.push(l);
                n_alvos += 1;
            }
        }
        let l_pred = if perdas.is_empty() {
            Tensor::zeros((), candle_core::DType::F32, &dev)?
        } else {
            (Tensor::stack(&perdas, 0)?.sum_all()? / n_alvos.max(1) as f64)?
        };
        let l_vc = self.perda_vc(z_ctx_todos)?;
        let total = (l_pred.affine(1.0, 0.0)? + l_vc.affine(self.lambda_vc, 0.0)?)?;
        let lp = l_pred.to_scalar::<f32>()? as f64;
        let lv = l_vc.to_scalar::<f32>()? as f64;
        Ok((total, lp, lv))
    }
}