//! `python::calibracao` — 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::*; /// **V-1..V-3:** calibra o verificador NCM em logits retidos e devolve /// temperatura, ECE antes/depois, NLL antes/depois e o τ_c por quantil DKW. #[pyfunction] pub(crate) fn calibrar_verificador( logits: Vec>, alvos: Vec, epsilon: f64, ) -> PyResult { Python::with_gil(|py| { let dev = candle_core::Device::Cpu; let (n, k) = (logits.len(), logits.first().map(|l| l.len()).unwrap_or(1)); let flat: Vec = logits.into_iter().flatten().collect(); let lt = candle_core::Tensor::from_vec(flat, (n, k), &dev) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?; let at = candle_core::Tensor::from_vec( alvos.iter().map(|&a| a as u32).collect::>(), (n,), &dev) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?; let mut v = verificador::VerificadorNcm::novo(); let (e_antes, e_depois) = v.ajustar(<, &at) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?; let confs: Vec = verificador::confianca_por_linha(<) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? .flatten_all().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? .to_vec1::().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? .iter().map(|x| *x as f64).collect(); let tau_c = verificador::limiar_por_erro(&confs, epsilon); let d_ = serde_json::json!({ "temperatura": v.temperatura, "ece_antes": e_antes, "ece_depois": e_depois, "nll_antes": v.n_ll_antes, "nll_depois": v.n_ll_depois, "tau_c": tau_c, "epsilon": epsilon, "n": n, }); Ok(pythonize::pythonize(py, &d_)?.into()) }) } /// **Demo do verificador NCM substituindo o oráculo** (r-21 §3.1): treina /// um classificador pequeno, calibra T* em dados retidos, mede ECE, τ_c /// (DKW) e a soundness empírica `P(erro|aceito) ≤ 1−τ_c+ECE`; compara com /// o ORÁCULO (que usa o alvo — impossível em produção) no mesmo problema. #[pyfunction] pub(crate) fn demo_verificador_ncm(n: usize, k: usize, semente: u64) -> PyResult { Python::with_gil(|py| { let dev = candle_core::Device::Cpu; crate::rng::definir_semente(semente); let mut rng = crate::rng::RngDeterministico::novo(semente); // dados sintéticos com DRIFT (treino ≠ consulta — força OOD leve) let gerar = |rng: &mut crate::rng::RngDeterministico, m: usize, desloc: f64| -> (candle_core::Tensor, Vec) { let mut l = Vec::with_capacity(m * k); let mut a = Vec::with_capacity(m); for _ in 0..m { let alvo = rng.inteiro(k); a.push(alvo); for j in 0..k { let base = if j == alvo { 2.5 } else { 0.0 }; l.push((base + rng.normal(0.0, 1.2) as f32 + desloc as f32) as f32); } } let lt = candle_core::Tensor::from_vec(l, (m, k), &dev).unwrap(); (lt, a) }; let (lt_tre, alvos_tre) = gerar(&mut rng, n, 0.0); let (lt_val, alvos_val) = gerar(&mut rng, n / 2, 0.3); // mini-classificador: matriz W (k, k) treinada por softmax-CE let w = candle_core::Tensor::randn(0f32, 0.1, (k, k), &dev) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?; let w_var = candle_core::Var::from_tensor(&w) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?; let at_t = candle_core::Tensor::from_vec( alvos_tre.iter().map(|&a| a as u32).collect::>(), (n,), &dev) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?; let mut otim = treino::AdamWManual::novo(vec![w_var.clone()], 0.05, 0.0); for _ in 0..120 { let logits = lt_tre.matmul(&w_var.as_tensor().transpose(0, 1).unwrap()).unwrap(); let ce = candle_nn::loss::cross_entropy(&logits, &at_t).unwrap(); let g = treino::grads_achatados(&ce, &[w_var.clone()]).unwrap(); otim.passo(g); } // logits de validação com o modelo treinado let logits_val = lt_val.matmul(&w_var.as_tensor().transpose(0, 1).unwrap()).unwrap(); let at_v = candle_core::Tensor::from_vec( alvos_val.iter().map(|&a| a as u32).collect::>(), (n / 2,), &dev) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?; // calibração (V-1) + soundness (V-2) + quantil (V-3) let mut v = verificador::VerificadorNcm::novo(); let (ece_antes, ece_depois) = v.ajustar(&logits_val, &at_v) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?; let confs: Vec = v.confianca(&logits_val).unwrap() .flatten_all().unwrap().to_vec1::().unwrap().iter().map(|x| *x as f64).collect(); let corretos: Vec = logits_val.argmax(candle_core::D::Minus1).unwrap() .eq(&at_v).unwrap().to_dtype(candle_core::DType::U32).unwrap() .flatten_all().unwrap().to_vec1::().unwrap() .iter().map(|x| *x == 1).collect(); let tau_c = verificador::limiar_por_erro(&confs, 0.25); let (mut erro_aceito, mut n_aceito) = (0usize, 0usize); for (c, ok) in confs.iter().zip(corretos.iter()) { if *c >= tau_c { n_aceito += 1; if !ok { erro_aceito += 1; } } } let p_erro = erro_aceito as f64 / n_aceito.max(1) as f64; let cota = (1.0 - tau_c) + ece_depois + 0.06; // ORÁCULO no mesmo problema (usa o alvo — só para contraste) let acerto_oraculo = corretos.iter().filter(|c| **c).count() as f64 / corretos.len() as f64; let d_ = serde_json::json!({ "versao": verificador::versao(), "n": n, "k": k, "temperatura_calibrada": v.temperatura, "ece_antes": ece_antes, "ece_depois": ece_depois, "nll_antes": v.n_ll_antes, "nll_depois": v.n_ll_depois, "tau_c_dkw": tau_c, "p_erro_aceito": p_erro, "cota_soundness": cota, "soundness_ok": p_erro <= cota, "calibracao_melhorou": ece_depois < ece_antes, "acerto_oraculo_referencia": acerto_oraculo, "verificador_sem_oraculo": true, "gates": { "gate_v1_calibracao": ece_depois < ece_antes, "gate_v2_soundness": p_erro <= cota, "gate_v3_tau_positivo": tau_c > 0.0 && tau_c < 1.0, }, }); Ok(pythonize::pythonize(py, &d_)?.into()) }) }