File size: 8,684 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 213 | //! `jepa::testes` — 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::*;
#[cfg(test)]
mod testes {
use super::*;
fn tensor_2d(dados: &[Vec<f64>], dev: &Device) -> Tensor {
let flat: Vec<f32> = dados.iter().flat_map(|v| v.iter().map(|&x| x as f32)).collect();
Tensor::from_vec(flat, (dados.len(), dados[0].len()), dev).unwrap()
}
#[test]
fn teste_mascaras_validas() {
// fração respeitada ±, particionamento contexto/alvo não-vazio
let mut rng = RngDeterministico::novo(1);
let m2 = mascara_blocos_2d(8, 8, 4, 0.3, &mut rng);
assert_eq!(m2.len(), 64);
let frac = m2.iter().filter(|&&b| b).count() as f64 / 64.0;
assert!(frac >= 0.15 && frac <= 0.55, "frac 2d = {frac}");
assert!(m2.iter().any(|&b| !b), "contexto vazio");
let m3 = mascara_tubos_3d(4, 6, 6, 3, 0.3, &mut rng);
assert_eq!(m3.len(), 144);
let f3 = m3.iter().filter(|&&b| b).count() as f64 / 144.0;
assert!(f3 >= 0.15 && f3 <= 0.6, "frac 3d = {f3}");
let ms = mascara_segmentos(4, 32, 0.25, &mut rng);
assert_eq!(ms.len(), 128);
assert!(ms.iter().any(|&b| b) && ms.iter().any(|&b| !b));
let m1 = mascara_intervalos_1d(64, 0.3, &mut rng);
let f1 = m1.iter().filter(|&&b| b).count() as f64 / 64.0;
assert!(f1 >= 0.25 && f1 <= 0.45, "frac 1d = {f1}");
}
#[test]
fn teste_fps_cobre() {
let mut rng = RngDeterministico::novo(2);
let pontos: Vec<Vec<f64>> = (0..40)
.map(|i| {
let a = i as f64 * 0.1;
vec![a.cos() * 3.0, a.sin() * 3.0, (i % 7) as f64 * 0.3]
})
.collect();
let centros = pontos_mais_distantes(&pontos, 6, &mut rng);
assert_eq!(centros.len(), 6);
// centros distintos
let mut s = centros.clone();
s.sort();
s.dedup();
assert_eq!(s.len(), 6);
}
#[test]
fn teste_j1_colapso_impossivel() {
// No colapso total (z constante), a perda VC = γ² > 0 — verificando a
// fórmula implementada com um lote constante.
let dev = Device::Cpu;
let nucleo = NucleoJepa::novo(4, 8, 0, 4, 0.99, 1.0, 1.0, &dev).unwrap();
let z = Tensor::from_vec(vec![0.5f32; 8 * 8], (8, 8), &dev).unwrap();
let lvc = nucleo.perda_vc(&z).unwrap();
let v = lvc.to_scalar::<f32>().unwrap() as f64;
// std=0 ⇒ dobradiça = γ = 1 ⇒ V/d = 1; cov = 0
assert!(v > 0.9, "perda VC no colapso = {v} (deveria ser ~1)");
}
#[test]
fn teste_j2_ema_ponto_fixo() {
// encoder congelado ⇒ após muitas passadas EMA, alvo = encoder
let dev = Device::Cpu;
let mut nucleo = NucleoJepa::novo(4, 8, 0, 4, 0.5, 0.1, 1.0, &dev).unwrap();
for _ in 0..40 {
nucleo.ema_passo().unwrap();
}
let a = nucleo.alvo.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
let b = nucleo.encoder.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
let dif: f32 = a.iter().zip(&b).map(|(x, y)| (x - y).abs()).fold(0.0, f32::max);
assert!(dif < 1e-5, "EMA não convergiu: dif máx = {dif}");
}
#[test]
fn teste_jepa_aprende_latente_previsivel() {
// J-3: com alvo previsível (função determinística do contexto), a
// perda pred must cair substancialmente e o std das incorporações
// NÃO colapsa (J-1).
let dev = Device::Cpu;
let mut rng = RngDeterministico::novo(3);
// patches = 64 vetores 8-D em 4 grupos (estrutura previsível)
let mut dados: Vec<Vec<f64>> = Vec::new();
for g in 0..4 {
let c = g as f64 * 2.0;
for _ in 0..16 {
dados.push((0..8).map(|k| c + rng.normal(0.0, 0.3) + (k as f64) * 0.05).collect());
}
}
let patches = tensor_2d(&dados, &dev);
let cfg = CfgJepa { d_emb: 16, epocas: 40, lr: 5e-3, ..Default::default() };
let rel = treinar_variante(
&patches,
&|n, r| mascara_intervalos_1d(n, 0.3, r),
&cfg,
VarianteJepa::TJepa,
None,
&dev,
)
.unwrap();
assert!(
rel.perda_final < rel.perda_inicial * 0.8,
"perda deveria cair: {} → {}",
rel.perda_inicial,
rel.perda_final
);
assert!(
rel.std_emb_final > 0.05,
"sem colapso: std_final = {}",
rel.std_emb_final
);
}
#[test]
fn teste_adaptadores_formas() {
let dev = Device::Cpu;
let img = Tensor::randn(0f32, 1.0, (3, 16, 16), &dev).unwrap();
let p = em_patches_2d(&img, 4, 4).unwrap();
assert_eq!(p.dims().to_vec(), vec![16, 48]);
let vid = Tensor::randn(0f32, 1.0, (4, 8, 8), &dev).unwrap();
let t3 = em_tubos_3d(&vid, 2, 4, 4).unwrap();
assert_eq!(t3.dims().to_vec(), vec![8, 32]);
let spec = Tensor::randn(0f32, 1.0, (16, 32), &dev).unwrap();
let ps = em_patches_espectrograma(&spec, 4, 8).unwrap();
assert_eq!(ps.dims().to_vec(), vec![16, 32]);
let ts = Tensor::randn(0f32, 1.0, (4, 32), &dev).unwrap();
let pt_ = em_patches_serie(&ts, 8).unwrap();
assert_eq!(pt_.dims().to_vec(), vec![16, 8]);
let vol = Tensor::randn(0f32, 1.0, (8, 8, 8), &dev).unwrap();
let vx = em_voxels(&vol, 4, 4, 4).unwrap();
assert_eq!(vx.dims().to_vec(), vec![8, 64]);
let pc = Tensor::randn(0f32, 1.0, (50, 3), &dev).unwrap();
let mut rng = RngDeterministico::novo(5);
let gr = em_grupos_pontos(&pc, 6, 4, &mut rng).unwrap();
assert_eq!(gr.dims().to_vec(), vec![6, 12]);
}
#[test]
fn teste_sonic_moe_capacidade_e_balanceamento() {
// SM-1: carga por especialista ≤ capacidade; SM-2: aux ≥ ~1 quando
// roteador é ~uniforme (início).
let dev = Device::Cpu;
let moe = SonicMoe::novo(8, 4, 2, 6, &dev).unwrap();
let x = Tensor::randn(0f32, 1.0, (20, 8), &dev).unwrap();
let s = moe.fwd(&x).unwrap();
for (e, c) in s.carga.iter().enumerate() {
assert!(*c <= 6, "especialista {e} com carga {c} > capacidade 6");
}
assert_eq!(s.saida.dims().to_vec(), vec![20, 8]);
let aux = s.perda_aux.to_scalar::<f32>().unwrap() as f64;
// com roteador quase uniforme, aux ≈ E·Σ (1/E)·(1/E) = 1
assert!(aux > 0.5 && aux < 3.0, "aux = {aux}");
// cada token processado por ≤ n_top especialistas
let total_carga: usize = s.carga.iter().sum();
assert!(total_carga <= 20 * 2, "carga total {total_carga} > n·topk");
}
#[test]
fn teste_replay_ssm_estabilidade_e_replay() {
// R-1: estados finitos e limitados; R-2: com replay α>0, após treinar
// a tarefa nova, a dinâmica antiga (magnitude do estado) permanece
// estável (não diverge).
let dev = Device::Cpu;
let mut ssm = ReplaySsm::novo(4, 6, 0.5, 8, &dev).unwrap();
let vars = ssm.vars();
let mut otim = crate::treino::AdamWManual::novo(vars, 1e-2, 0.0);
// tarefa "antiga": sinal constante 1.0
let x_antigo = Tensor::from_vec(vec![1.0f32; 16 * 4], (16, 4), &dev).unwrap();
for _ in 0..30 {
let h = ssm.scan(&x_antigo).unwrap();
let alvo = h.detach();
ssm.passo_treino(&x_antigo, &alvo, &mut otim).unwrap();
}
// estado após treino antigo: finito
let h1 = ssm.scan(&x_antigo).unwrap();
let m1 = h1.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
assert!(m1.is_finite() && m1 < 50.0, "estado explodiu: {m1}");
// tarefa "nova": sinal −1.0 — treino com replay
let x_novo = Tensor::from_vec(vec![-1.0f32; 16 * 4], (16, 4), &dev).unwrap();
for _ in 0..30 {
let h = ssm.scan(&x_novo).unwrap();
let alvo = h.detach();
ssm.passo_treino(&x_novo, &alvo, &mut otim).unwrap();
}
// a dinâmica antiga continua estável (sem explosão)
let h2 = ssm.scan(&x_antigo).unwrap();
let m2 = h2.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
assert!(m2.is_finite() && m2 < 50.0, "estado antigo explodiu: {m2}");
// portas dentro de (0,1)
let a = ssm.a.as_tensor().to_vec1::<f32>().unwrap();
for v in a {
assert!(v.is_finite());
}
}
}
|