Skip to main content

uqa_ml/
deep_fusion.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Multi-layer fusion operator (Section 7, Paper 4).
8//!
9//! Implements deep Bayesian fusion as a multi-layer network:
10//!
11//! ```text
12//!     l^(k) = g( l^(k-1) + sum_j logit(P_j^(k)) )
13//!     P_final = sigmoid(l^(K))
14//! ```
15//!
16//! The internal channel map keys per-document feature vectors. Each
17//! `Layer` variant updates that map, including signal, dense, convolutional,
18//! recurrent, normalization, attention, and graph-aware propagation / pooling
19//! layers.
20
21use std::collections::BTreeMap;
22use std::sync::Arc;
23
24use uqa_core::{IndexStats, Payload, PostingEntry, PostingList, Value};
25use uqa_scoring::prob::{confidence_scaled_log_odds_pool_weighted, logit, sigmoid, PROB_EPSILON};
26
27use uqa_operators::{
28    base::{Direction, OperatorResult},
29    ExecutionContext, Operator,
30};
31use uqa_storage::{StorageBackendError, StorageBackendResult};
32
33use crate::backend::{try_filled_vec, try_vec_with_capacity, MLError, MLResult};
34
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub enum Gating {
37    #[default]
38    None,
39    Softplus,
40    Sigmoid,
41    ReLU,
42    Swish,
43    Gelu,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum GlobalPoolMethod {
48    Avg,
49    Max,
50    AvgMax,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum AggregationKind {
55    Mean,
56    Sum,
57    Max,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum PoolMethod {
62    Avg,
63    Max,
64}
65
66#[allow(clippy::upper_case_acronyms)]
67#[derive(Clone)]
68pub enum Layer {
69    /// Runtime-provided feature vector. This layer is a no-op during
70    /// execution; it marks the expected input dimension for trained
71    /// models that receive feature batches from an ML backend.
72    Input { dimensions: usize },
73    /// Run a list of `Operator` signals, fuse them via log-odds
74    /// conjunction at the configured `alpha`, then add the resulting
75    /// logit to channel 0 as a residual connection.
76    Signal(Vec<Arc<dyn Operator>>),
77    /// Initialize the channel map from a raw embedding vector. Element
78    /// `i` becomes node `i+1` with a single-channel value.
79    Embed(Vec<f64>),
80    /// Fully connected: `out = W @ input + bias`, then gating.
81    Dense {
82        /// `output_channels x input_channels`, row-major.
83        weights: Vec<f64>,
84        bias: Vec<f64>,
85        output_channels: usize,
86        input_channels: usize,
87    },
88    /// Concatenate every node's channel vector into a single vector.
89    Flatten,
90    /// Reduce all spatial nodes to one vector.
91    GlobalPool(GlobalPoolMethod),
92    /// Numerically stable softmax per node.
93    Softmax,
94    /// Per-channel batch normalization across all nodes.
95    BatchNorm { epsilon: f64 },
96    /// Inference-mode dropout: scale every value by `1 - p`.
97    Dropout { p: f64 },
98    /// One-dimensional CNN over sorted sequence positions.
99    ///
100    /// Weights are row-major as `output_channels x kernel_size x input_channels`.
101    CNN1D {
102        weights: Vec<f64>,
103        bias: Vec<f64>,
104        output_channels: usize,
105        input_channels: usize,
106        kernel_size: usize,
107        stride: usize,
108        padding: usize,
109    },
110    /// Two-dimensional CNN over flattened `H x W x C` spatial positions.
111    ///
112    /// Weights are row-major as
113    /// `output_channels x kernel_height x kernel_width x input_channels`.
114    CNN2D {
115        weights: Vec<f64>,
116        bias: Vec<f64>,
117        output_channels: usize,
118        input_channels: usize,
119        input_height: usize,
120        input_width: usize,
121        kernel_height: usize,
122        kernel_width: usize,
123        stride_height: usize,
124        stride_width: usize,
125        padding_height: usize,
126        padding_width: usize,
127    },
128    /// Propagate channel-0 scores through graph edges.
129    ///
130    /// `aggregation` averages / sums / maxes the in-bounds neighbor
131    /// probabilities; the resulting logit is added as a residual on
132    /// channel 0. Requires `ExecutionContext::graph`.
133    Propagate {
134        /// Edge label to follow. An empty string selects every edge label.
135        edge_label: String,
136        aggregation: AggregationKind,
137        direction: Direction,
138    },
139    /// Weighted multi-hop graph convolution on channel 0.
140    ///
141    /// `hop_weights[0]` is the self weight, `hop_weights[i]` weights
142    /// the average over the hop-`i` neighbor ring. Weights are
143    /// L1-normalized; the result is converted back to logit and added
144    /// as a residual.
145    Conv {
146        /// Edge label to follow. An empty string selects every edge label.
147        edge_label: String,
148        hop_weights: Vec<f64>,
149        direction: Direction,
150    },
151    /// Spatial downsampling via greedy BFS partitioning.
152    ///
153    /// Groups `pool_size` neighboring nodes via BFS, aggregates their
154    /// channel vectors element-wise (`PoolMethod::{Avg, Max}`), and
155    /// keeps the smallest doc id as the representative.
156    Pool {
157        /// Edge label to follow. An empty string selects every edge label.
158        edge_label: String,
159        pool_size: usize,
160        method: PoolMethod,
161        direction: Direction,
162    },
163    /// Self-attention across the per-node channel vectors with
164    /// `Q = K = V = X`, scaled-dot-product, no learned projections.
165    Attention,
166    /// Vanilla RNN over sorted sequence positions.
167    ///
168    /// Weights are row-major as `hidden_channels x input_channels` and
169    /// `hidden_channels x hidden_channels`.
170    RNN {
171        weights_input: Vec<f64>,
172        weights_hidden: Vec<f64>,
173        bias: Vec<f64>,
174        hidden_channels: usize,
175        input_channels: usize,
176        return_sequences: bool,
177    },
178    /// LSTM over sorted sequence positions.
179    ///
180    /// Gate order is input, forget, candidate, output. Both weight
181    /// matrices are row-major with `4 * hidden_channels` rows.
182    LSTM {
183        weights_input: Vec<f64>,
184        weights_hidden: Vec<f64>,
185        bias: Vec<f64>,
186        hidden_channels: usize,
187        input_channels: usize,
188        return_sequences: bool,
189    },
190}
191
192pub struct DeepFusionOperator {
193    layers: Vec<Layer>,
194    alpha: f64,
195    gating: Gating,
196}
197
198impl DeepFusionOperator {
199    pub fn new(layers: Vec<Layer>, alpha: f64, gating: Gating) -> MLResult<Self> {
200        validate_layers(&layers, alpha)?;
201        Ok(Self {
202            layers,
203            alpha,
204            gating,
205        })
206    }
207
208    pub fn layers(&self) -> &[Layer] {
209        &self.layers
210    }
211
212    pub fn alpha(&self) -> f64 {
213        self.alpha
214    }
215
216    pub fn gating(&self) -> Gating {
217        self.gating
218    }
219}
220
221mod attention;
222mod cnn;
223mod execution;
224mod graph_layers;
225mod layer_dispatch;
226mod recurrent;
227mod runtime;
228mod state;
229mod tensor_layers;
230mod validation;
231
232use attention::{apply_attention, build_result};
233use cnn::{apply_cnn_1d, apply_cnn_2d};
234use graph_layers::{apply_conv, apply_pool, apply_propagate};
235use layer_dispatch::{apply_cnn_1d_layer, apply_cnn_2d_layer, apply_lstm_layer, apply_rnn_layer};
236use recurrent::{apply_lstm, apply_rnn};
237use runtime::{
238    apply_gating, runtime_filled_vec, runtime_model_error, runtime_vec_with_capacity, safe_logit,
239    usize_to_f64_exact,
240};
241use state::{Convolution1D, Convolution2D, ForwardState, LongShortTermMemory, Recurrent};
242use tensor_layers::{
243    apply_batch_norm, apply_dense, apply_dropout, apply_embed, apply_flatten, apply_global_pool,
244    apply_signal, apply_softmax,
245};
246use validation::{validate_layers, validate_state};
247
248#[cfg(test)]
249mod tests;