TFT (10 downloads eliminated): - lstm_encoder: gpu_select_dim1 per-timestep, gpu_cat_dim0 assembly - variable_selection: gpu_narrow_2d column select, gpu_broadcast_mul_col weighted sum, gpu_mean_all per-column stats - quantile_outputs: gpu_select_dim1 last timestep, gpu_stack_2d output, GPU quantile loss (sub→abs→scale→add→mean_all) - mod.rs: GPU 3D broadcast for static context Mamba2 (10 downloads eliminated): - loss.rs: GPU MSE (sub→sqr→mean_all), GPU directional MSE with sign detection (mul→abs→div→scalar_sub→scale→mean_all) - scan_algorithms: GPU sequential scan via gpu_select_dim1 per-step - selective_state: GPU importance scoring (abs→mean_all) - mod.rs: GPU state-space recurrence (select_dim1→matmul→add), GPU accuracy computation (sub→abs→relu→clamp→mean_all) 169/169 ml-supervised tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
213 lines
7.7 KiB
Rust
213 lines
7.7 KiB
Rust
//! Variable Selection Network for TFT
|
|
//!
|
|
//! Implements learnable feature selection using gated linear units and
|
|
//! soft feature selection weights for improved interpretability.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use cudarc::driver::CudaStream;
|
|
use ml_core::MLError;
|
|
|
|
use super::GatedResidualNetwork;
|
|
use crate::gpu_tensor::{
|
|
gpu_add, gpu_cat_dim1, gpu_narrow_2d, gpu_softmax, GpuLinear, GpuTensor,
|
|
};
|
|
|
|
/// Variable Selection Network for feature importance learning
|
|
#[derive(Debug)]
|
|
pub struct VariableSelectionNetwork {
|
|
pub input_size: usize,
|
|
pub hidden_size: usize,
|
|
// Gated Linear Units for variable selection
|
|
single_var_grns: Vec<GatedResidualNetwork>,
|
|
// Soft attention weights
|
|
attention_weights: GpuLinear,
|
|
// Feature importance tracking
|
|
importance_scores: HashMap<usize, f64>,
|
|
stream: Arc<CudaStream>,
|
|
}
|
|
|
|
impl VariableSelectionNetwork {
|
|
pub fn new(input_size: usize, hidden_size: usize, stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
|
// Create GRN for flattened inputs (keeps param symmetry with original)
|
|
let _flattened_grn = GatedResidualNetwork::new(input_size, hidden_size, stream)?;
|
|
|
|
// Create individual GRNs for each variable
|
|
let mut single_var_grns = Vec::new();
|
|
for _i in 0..input_size {
|
|
let grn = GatedResidualNetwork::new(1, hidden_size, stream)?;
|
|
single_var_grns.push(grn);
|
|
}
|
|
|
|
// Attention layer for variable selection
|
|
let attention_weights = GpuLinear::new(hidden_size * input_size, input_size, stream)?;
|
|
|
|
Ok(Self {
|
|
input_size,
|
|
hidden_size,
|
|
single_var_grns,
|
|
attention_weights,
|
|
importance_scores: HashMap::new(),
|
|
stream: Arc::clone(stream),
|
|
})
|
|
}
|
|
|
|
pub fn forward(
|
|
&mut self,
|
|
inputs: &GpuTensor,
|
|
context: Option<&GpuTensor>,
|
|
) -> Result<GpuTensor, MLError> {
|
|
let batch_size = inputs.dim(0)?;
|
|
let is_2d = inputs.shape.len() == 2;
|
|
let seq_len = if is_2d { 1 } else { inputs.dim(1)? };
|
|
let input_feat = if is_2d { inputs.dim(1)? } else { inputs.dim(2)? };
|
|
|
|
// Reshape to 2D for processing: [batch * seq_len, input_feat]
|
|
let flat_input = if is_2d {
|
|
inputs.reshape(&[batch_size, input_feat])?
|
|
} else {
|
|
inputs.reshape(&[batch_size * seq_len, input_feat])?
|
|
};
|
|
let flat_rows = flat_input.dim(0)?;
|
|
|
|
// Process individual variables on GPU
|
|
let mut var_outputs: Vec<GpuTensor> = Vec::with_capacity(self.input_size);
|
|
|
|
for (i, grn) in self.single_var_grns.iter().enumerate() {
|
|
// Extract variable i from flat_input: column i -> [flat_rows, 1]
|
|
let var_tensor = gpu_narrow_2d(&flat_input, 1, i, 1)?;
|
|
let var_output = grn.forward(&var_tensor, context)?; // [flat_rows, hidden_size]
|
|
var_outputs.push(var_output);
|
|
}
|
|
|
|
// Concatenate all variable outputs along dim1: [flat_rows, hidden_size * input_size]
|
|
let mut cat_tensor = var_outputs.first().ok_or_else(|| {
|
|
MLError::InvalidInput("No variable outputs".to_owned())
|
|
})?.clone();
|
|
for var_out in var_outputs.iter().skip(1) {
|
|
cat_tensor = gpu_cat_dim1(&cat_tensor, var_out)?;
|
|
}
|
|
|
|
// Compute attention weights
|
|
let raw_weights = self.attention_weights.forward(&cat_tensor)?; // [flat_rows, input_size]
|
|
let attention = gpu_softmax(&raw_weights)?;
|
|
|
|
// Update importance scores (single scalar download per variable -- acceptable for monitoring)
|
|
self.update_importance_scores(&attention)?;
|
|
|
|
// Apply variable selection: weighted sum of per-variable GRN outputs
|
|
// attention: [flat_rows, input_size], var_outputs[i]: [flat_rows, hidden_size]
|
|
// result = sum_i(attention[:, i:i+1] * var_outputs[i])
|
|
let mut result = GpuTensor::zeros(&[flat_rows, self.hidden_size], &self.stream)?;
|
|
for (i, var_out) in var_outputs.iter().enumerate() {
|
|
// Extract attention weight for variable i: [flat_rows, 1]
|
|
let attn_col = gpu_narrow_2d(&attention, 1, i, 1)?;
|
|
// Broadcast multiply: [flat_rows, 1] * [flat_rows, hidden_size] -> [flat_rows, hidden_size]
|
|
// We need to broadcast attn_col across hidden_size columns.
|
|
// Use gpu_broadcast_mul_col: a[rows, cols] * b[rows, 1] -> [rows, cols]
|
|
let weighted = crate::gpu_tensor::gpu_broadcast_mul_col(var_out, &attn_col)?;
|
|
result = gpu_add(&result, &weighted)?;
|
|
}
|
|
|
|
// Reshape to [batch, seq_len, hidden_size]
|
|
result.reshape(&[batch_size, seq_len, self.hidden_size])
|
|
}
|
|
|
|
fn update_importance_scores(&mut self, attention_weights: &GpuTensor) -> Result<(), MLError> {
|
|
// Use gpu_mean_all on per-column slices to avoid bulk download
|
|
let cols = attention_weights.dim(1)?;
|
|
self.importance_scores.clear();
|
|
for c in 0..cols {
|
|
let col = gpu_narrow_2d(attention_weights, 1, c, 1)?;
|
|
let mean_val = crate::gpu_tensor::gpu_mean_all(&col)?;
|
|
self.importance_scores.insert(c, mean_val as f64);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_importance_scores(&self) -> Result<Vec<f64>, MLError> {
|
|
let mut scores = vec![0.0; self.input_size];
|
|
for (i, &score) in &self.importance_scores {
|
|
if *i < self.input_size {
|
|
scores[*i] = score;
|
|
}
|
|
}
|
|
|
|
let sum: f64 = scores.iter().sum();
|
|
if sum == 0.0 {
|
|
let uniform_score = 1.0 / self.input_size as f64;
|
|
scores.fill(uniform_score);
|
|
}
|
|
|
|
Ok(scores)
|
|
}
|
|
|
|
pub fn get_top_features(&self, k: usize) -> Vec<(usize, f64)> {
|
|
let mut features: Vec<(usize, f64)> = self
|
|
.importance_scores
|
|
.iter()
|
|
.map(|(&idx, &score)| (idx, score))
|
|
.collect();
|
|
|
|
features.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
|
features.truncate(k);
|
|
features
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use cudarc::driver::CudaContext;
|
|
|
|
fn test_stream() -> Arc<CudaStream> {
|
|
let ctx = CudaContext::new(0).expect("CUDA context required");
|
|
ctx.new_stream().expect("Failed to create CUDA stream")
|
|
}
|
|
|
|
#[test]
|
|
fn test_variable_selection_network_creation() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
let vsn = VariableSelectionNetwork::new(10, 64, &stream)?;
|
|
assert_eq!(vsn.input_size, 10);
|
|
assert_eq!(vsn.hidden_size, 64);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_variable_selection_forward_2d() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
let mut vsn = VariableSelectionNetwork::new(5, 32, &stream)?;
|
|
let input = GpuTensor::from_vec(
|
|
vec![1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
|
&[2, 5],
|
|
&stream,
|
|
)?;
|
|
let output = vsn.forward(&input, None)?;
|
|
assert_eq!(output.shape, vec![2, 1, 32]);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_variable_selection_forward_3d() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
let mut vsn = VariableSelectionNetwork::new(3, 16, &stream)?;
|
|
let input = GpuTensor::from_vec(vec![1.0_f32; 24], &[2, 4, 3], &stream)?;
|
|
let output = vsn.forward(&input, None)?;
|
|
assert_eq!(output.shape, vec![2, 4, 16]);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_importance_scores() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
let vsn = VariableSelectionNetwork::new(5, 32, &stream)?;
|
|
let scores = vsn.get_importance_scores()?;
|
|
assert_eq!(scores.len(), 5);
|
|
let sum: f64 = scores.iter().sum();
|
|
assert!((sum - 1.0).abs() < 1e-6);
|
|
Ok(())
|
|
}
|
|
}
|