Skip to main content

qualia_core_db/solvers/learning/active/
mod.rs

1//! **Active learning** — spend the human's attestation budget wisely.
2//!
3//! The mission frame: machine carries Data→Knowledge, but *wisdom* (the final
4//! judgement, the label, the ratification) stays with the human. Human attention is
5//! the scarce, expensive resource. Active learning is the theory of **ranking which
6//! few items are most worth a human's judgement** — so a model improves fastest per
7//! label asked, and the person is never asked to grind through the obvious.
8//!
9//! This is the supply-side of frugality: instead of demanding mass labelling (which
10//! burdens exactly the people this project protects), the engine surfaces the handful
11//! of genuinely-informative cases and routes them for attestation — the same
12//! `RequiresHumanReview` discipline as the rest of the stack.
13//!
14//! Three classic query strategies, each over the *predictions already produced* by the
15//! existing estimators ([`crate::solvers::learning`]) — no new model, pure ranking:
16//!
17//! * [`uncertainty`] — query where one model is least sure (least-confidence, margin,
18//!   entropy).
19//! * [`committee`] — query where an ensemble *disagrees* (vote/consensus entropy, KL).
20//! * [`density`] — weight uncertainty by how *representative* a point is, so the model
21//!   is not lured into labelling unrepresentative outliers.
22//!
23//! Every entry fails closed ([`ActiveError`]); ranking reuses the engine's information
24//! theory ([`crate::solvers::statistics::information`]). Kernel-class `Reduction`.
25
26pub mod committee;
27pub mod density;
28pub mod uncertainty;
29
30pub use committee::{average_kl_disagreement, consensus_entropy, vote_entropy};
31pub use density::{cosine_similarity, information_density};
32pub use uncertainty::{rank_informative, score, Strategy};
33
34/// Fail-closed errors for active-learning ranking.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum ActiveError {
37    /// Inconsistent shapes (e.g. ragged probability rows, mismatched class counts).
38    InvalidDimension,
39    /// Not enough data to rank (empty pool / empty committee).
40    InsufficientData,
41}
42
43impl core::fmt::Display for ActiveError {
44    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
45        match self {
46            ActiveError::InvalidDimension => write!(f, "inconsistent active-learning input shapes"),
47            ActiveError::InsufficientData => write!(f, "insufficient data to rank a query"),
48        }
49    }
50}
51impl std::error::Error for ActiveError {}
52
53/// Argsort `scores` descending (highest first), returning indices. Stable on ties.
54pub(crate) fn argsort_desc(scores: &[f64]) -> Vec<usize> {
55    let mut idx: Vec<usize> = (0..scores.len()).collect();
56    idx.sort_by(|&a, &b| {
57        scores[b]
58            .partial_cmp(&scores[a])
59            .unwrap_or(core::cmp::Ordering::Equal)
60    });
61    idx
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn argsort_orders_high_to_low() {
70        assert_eq!(argsort_desc(&[0.1, 0.9, 0.5]), vec![1, 2, 0]);
71    }
72}