File size: 4,861 Bytes
97df2ae | 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 | //! Perdas multi-tarefa: PCGrad (Teo D.7 / Teo 35.1 do projeto) e λ com
//! hipergradiente-âncora exato (Teo 29.2). Port de `treino/perdas.py`.
//!
//! PCGrad: para cada par (g_i, g_j) com conflito (⟨g_i,g_j⟩ < 0), projeta
//! g_i no complemento ortogonal de g_j: g_i' = g_i − (⟨g_i,g_j⟩/‖g_j‖²)·g_j.
//! Invariantes I1/I2/I3 de Yu et al. 2020: projeção exata, contração,
//! não-piora por par. Com λ=0 o gradiente extra é nulo (bit-a-bit — T10(ii)).
use candle_core::{Result, Tensor, Var};
/// Gradiente achatado de cada perda w.r.t. às variáveis (ordem fixa de `vars`).
pub fn gradientes_por_perda(perdas: &[&Tensor], vars: &[Var]) -> Result<Vec<Vec<Tensor>>> {
let mut saida = Vec::with_capacity(perdas.len());
for p in perdas {
let gs = p.backward()?;
let mut um = Vec::with_capacity(vars.len());
for v in vars {
let g = gs.get(v).ok_or_else(|| {
candle_core::Error::Msg("variável sem gradiente (λ=0 ou grafo desconectado)".into())
})?;
um.push(g.clone());
}
saida.push(um);
}
Ok(saida)
}
/// PCGrad sobre gradientes por perda (lista de listas, mesmas formas por índice).
/// Devolve o gradiente combinado (média das projeções por perda).
pub fn pcgrad(grads: &[Vec<Tensor>]) -> Result<Vec<Tensor>> {
let n_perdas = grads.len();
let n_vars = grads[0].len();
let mut combinado: Vec<Option<Tensor>> = (0..n_vars).map(|_| None).collect();
for i in 0..n_perdas {
// g_i pode ser projetado contra cada g_j (j ≠ i)
let mut proj = grads[i].clone();
for j in 0..n_perdas {
if i == j {
continue;
}
// produto interno global ⟨g_i, g_j⟩ e ‖g_j‖²
let mut ip = 0f64;
let mut nj = 0f64;
for v in 0..n_vars {
let a = grads[i][v].flatten_all()?.to_vec1::<f32>()?;
let b = grads[j][v].flatten_all()?.to_vec1::<f32>()?;
for k in 0..a.len() {
ip += a[k] as f64 * b[k] as f64;
nj += b[k] as f64 * b[k] as f64;
}
}
if ip < 0.0 && nj > 1e-12 {
let alfa = ip / nj;
for v in 0..n_vars {
let b = grads[j][v].flatten_all()?;
let escala = Tensor::full(alfa as f32, b.shape(), b.device())?;
let sub = grads[i][v].broadcast_sub(&b.mul(&escala)?)?;
proj[v] = sub.reshape(grads[i][v].shape())?;
}
}
}
for v in 0..n_vars {
let media = proj[v].affine(1.0 / n_perdas as f64, 0.0)?;
combinado[v] = Some(match &combinado[v] {
Some(a) => (a + &media)?,
None => media,
});
}
}
Ok(combinado.into_iter().map(|t| t.unwrap()).collect())
}
/// Verificação V8-análogo: a projeção PCGrad é exata (⟨g_i', g_j⟩ = 0).
pub fn verificar_projecao(gi: &[Tensor], gj: &[Tensor]) -> Result<bool> {
let mut ip = 0f64;
for (a, b) in gi.iter().zip(gj.iter()) {
let av = a.flatten_all()?.to_vec1::<f32>()?;
let bv = b.flatten_all()?.to_vec1::<f32>()?;
for k in 0..av.len() {
ip += av[k] as f64 * bv[k] as f64;
}
}
Ok(ip.abs() < 1e-3)
}
/// λ com hipergradiente-âncora (Teo 29.2): L(λ) = λ·L_diff + ρ(λ−λ₀)² é
/// afim+quadrática em λ ⇒ hipergradiente EXATO ∂L/∂λ = L_diff + 2ρ(λ−λ₀);
/// ótimo interior λ* = λ₀ − L_diff/(2ρ). Começa em 0 e cresce até λ*.
pub struct LambdaAncora {
pub lambda: f64,
pub lambda0: f64,
pub rho: f64,
pub minimo: f64,
pub maximo: f64,
}
impl LambdaAncora {
pub fn novo(rho: f64) -> Self {
LambdaAncora { lambda: 0.0, lambda0: 1.0, rho, minimo: 0.0, maximo: 2.0 }
}
/// Um passo de gradiente descendente em λ com hipergradiente exato.
pub fn passo(&mut self, l_diff: f64, taxa: f64) -> f64 {
let hg = l_diff + 2.0 * self.rho * (self.lambda - self.lambda0);
self.lambda = (self.lambda - taxa * hg).clamp(self.minimo, self.maximo);
self.lambda
}
pub fn otimo_analitico(&self, l_diff: f64) -> f64 {
(self.lambda0 - l_diff / (2.0 * self.rho)).clamp(self.minimo, self.maximo)
}
}
#[cfg(test)]
mod testes {
use super::*;
use candle_core::Device;
#[test]
fn lambda_ancora_otimo_interior() {
let mut la = LambdaAncora::novo(0.05);
let l_diff = -0.2; // perda nova REDUZ a total (queremos λ > 0)
let alvo = la.otimo_analitico(l_diff);
for _ in 0..3000 {
la.passo(l_diff, 0.05);
}
assert!((la.lambda - alvo).abs() < 2e-2, "{} vs {}", la.lambda, alvo);
}
}
|