Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
261
vendor/ruvector/crates/ruvector-nervous-system/src/compete/inhibition.rs
vendored
Normal file
261
vendor/ruvector/crates/ruvector-nervous-system/src/compete/inhibition.rs
vendored
Normal file
@@ -0,0 +1,261 @@
|
||||
//! Lateral Inhibition Model
|
||||
//!
|
||||
//! Implements inhibitory connections between neurons for winner-take-all dynamics.
|
||||
|
||||
/// Lateral inhibition mechanism
|
||||
///
|
||||
/// Models inhibitory connections between neurons, where active neurons
|
||||
/// suppress nearby neurons through inhibitory synapses.
|
||||
///
|
||||
/// # Model
|
||||
///
|
||||
/// - Mexican hat connectivity (surround inhibition)
|
||||
/// - Distance-based inhibition strength
|
||||
/// - Exponential decay with time
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ruvector_nervous_system::compete::LateralInhibition;
|
||||
///
|
||||
/// let mut inhibition = LateralInhibition::new(10, 0.5, 0.9);
|
||||
/// let mut activations = vec![0.1, 0.2, 0.9, 0.3, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
/// inhibition.apply(&mut activations, 2); // Winner at index 2
|
||||
/// // Activations at indices near 2 will be suppressed
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LateralInhibition {
|
||||
/// Inhibitory connection weights (sparse representation)
|
||||
size: usize,
|
||||
|
||||
/// Base inhibition strength
|
||||
strength: f32,
|
||||
|
||||
/// Temporal decay factor
|
||||
decay: f32,
|
||||
|
||||
/// Inhibition radius (neurons within this distance are inhibited)
|
||||
radius: usize,
|
||||
}
|
||||
|
||||
impl LateralInhibition {
|
||||
/// Create a new lateral inhibition model
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - Number of neurons
|
||||
/// * `strength` - Base inhibition strength (0.0-1.0)
|
||||
/// * `decay` - Temporal decay factor (0.0-1.0)
|
||||
pub fn new(size: usize, strength: f32, decay: f32) -> Self {
|
||||
Self {
|
||||
size,
|
||||
strength: strength.clamp(0.0, 1.0),
|
||||
decay: decay.clamp(0.0, 1.0),
|
||||
radius: (size as f32).sqrt() as usize, // Default radius based on size
|
||||
}
|
||||
}
|
||||
|
||||
/// Set inhibition radius
|
||||
pub fn with_radius(mut self, radius: usize) -> Self {
|
||||
self.radius = radius;
|
||||
self
|
||||
}
|
||||
|
||||
/// Apply lateral inhibition from winner neuron
|
||||
///
|
||||
/// Suppresses activations of neurons near the winner based on distance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `activations` - Current activation levels (modified in-place)
|
||||
/// * `winner` - Index of winning neuron
|
||||
pub fn apply(&self, activations: &mut [f32], winner: usize) {
|
||||
assert_eq!(activations.len(), self.size, "Activation size mismatch");
|
||||
assert!(winner < self.size, "Winner index out of bounds");
|
||||
|
||||
let winner_activation = activations[winner];
|
||||
|
||||
for (i, activation) in activations.iter_mut().enumerate() {
|
||||
if i == winner {
|
||||
continue; // Don't inhibit winner
|
||||
}
|
||||
|
||||
// Calculate distance (can use topology-aware distance in future)
|
||||
let distance = if i > winner { i - winner } else { winner - i };
|
||||
|
||||
if distance <= self.radius {
|
||||
// Inhibition strength decreases with distance (Mexican hat)
|
||||
let distance_factor = 1.0 - (distance as f32 / self.radius as f32);
|
||||
let inhibition = self.strength * distance_factor * winner_activation;
|
||||
|
||||
// Apply inhibition (multiplicative suppression)
|
||||
*activation *= 1.0 - inhibition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply global inhibition (all neurons inhibit all others)
|
||||
///
|
||||
/// Used for sparse coding where multiple weak activations compete.
|
||||
pub fn apply_global(&self, activations: &mut [f32]) {
|
||||
let total_activation: f32 = activations.iter().sum();
|
||||
let mean_activation = total_activation / activations.len() as f32;
|
||||
|
||||
for activation in activations.iter_mut() {
|
||||
// Global inhibition proportional to mean activity
|
||||
let inhibition = self.strength * mean_activation;
|
||||
*activation = (*activation - inhibition).max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute inhibitory weight between two neurons
|
||||
///
|
||||
/// Returns inhibition strength based on distance and connectivity pattern.
|
||||
pub fn weight(&self, from: usize, to: usize) -> f32 {
|
||||
if from == to {
|
||||
return 0.0; // No self-inhibition
|
||||
}
|
||||
|
||||
let distance = if to > from { to - from } else { from - to };
|
||||
|
||||
if distance > self.radius {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Mexican hat profile
|
||||
let distance_factor = 1.0 - (distance as f32 / self.radius as f32);
|
||||
self.strength * distance_factor
|
||||
}
|
||||
|
||||
/// Get full inhibition matrix (for visualization/analysis)
|
||||
///
|
||||
/// Returns a size × size matrix of inhibitory weights.
|
||||
/// Note: This is expensive and should only be used for debugging.
|
||||
pub fn weight_matrix(&self) -> Vec<Vec<f32>> {
|
||||
let mut matrix = vec![vec![0.0; self.size]; self.size];
|
||||
|
||||
for i in 0..self.size {
|
||||
for j in 0..self.size {
|
||||
matrix[i][j] = self.weight(i, j);
|
||||
}
|
||||
}
|
||||
|
||||
matrix
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_inhibition_basic() {
|
||||
let inhibition = LateralInhibition::new(10, 0.8, 0.9);
|
||||
let mut activations = vec![0.0; 10];
|
||||
activations[5] = 1.0; // Strong activation at index 5
|
||||
activations[4] = 0.5; // Weak activation nearby
|
||||
activations[6] = 0.5; // Weak activation nearby
|
||||
|
||||
inhibition.apply(&mut activations, 5);
|
||||
|
||||
// Winner should remain unchanged
|
||||
assert_eq!(activations[5], 1.0);
|
||||
|
||||
// Nearby neurons should be suppressed
|
||||
assert!(activations[4] < 0.5, "Nearby neuron should be inhibited");
|
||||
assert!(activations[6] < 0.5, "Nearby neuron should be inhibited");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inhibition_radius() {
|
||||
let inhibition = LateralInhibition::new(20, 0.8, 0.9).with_radius(2);
|
||||
let mut activations = vec![0.5; 20];
|
||||
activations[10] = 1.0; // Winner
|
||||
|
||||
inhibition.apply(&mut activations, 10);
|
||||
|
||||
// Within radius should be inhibited
|
||||
assert!(activations[9] < 0.5);
|
||||
assert!(activations[11] < 0.5);
|
||||
|
||||
// Outside radius should be less affected
|
||||
assert!(activations[7] >= activations[9]);
|
||||
assert!(activations[13] >= activations[11]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inhibition_no_self_inhibition() {
|
||||
let inhibition = LateralInhibition::new(10, 1.0, 0.9);
|
||||
assert_eq!(inhibition.weight(5, 5), 0.0, "No self-inhibition");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inhibition_symmetric() {
|
||||
let inhibition = LateralInhibition::new(10, 0.8, 0.9);
|
||||
|
||||
// Inhibition should be symmetric
|
||||
assert_eq!(
|
||||
inhibition.weight(3, 7),
|
||||
inhibition.weight(7, 3),
|
||||
"Inhibition should be symmetric"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_inhibition() {
|
||||
let inhibition = LateralInhibition::new(10, 0.5, 0.9);
|
||||
let mut activations = vec![0.8; 10];
|
||||
|
||||
inhibition.apply_global(&mut activations);
|
||||
|
||||
// All activations should be suppressed equally
|
||||
assert!(activations.iter().all(|&x| x < 0.8));
|
||||
assert!(activations.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inhibition_strength_bounds() {
|
||||
let inhibition1 = LateralInhibition::new(10, -0.5, 0.9);
|
||||
let inhibition2 = LateralInhibition::new(10, 1.5, 0.9);
|
||||
|
||||
// Strength should be clamped to [0, 1]
|
||||
assert_eq!(inhibition1.strength, 0.0);
|
||||
assert_eq!(inhibition2.strength, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weight_matrix_structure() {
|
||||
let inhibition = LateralInhibition::new(5, 0.8, 0.9).with_radius(1);
|
||||
let matrix = inhibition.weight_matrix();
|
||||
|
||||
// Matrix should be square
|
||||
assert_eq!(matrix.len(), 5);
|
||||
assert!(matrix.iter().all(|row| row.len() == 5));
|
||||
|
||||
// Diagonal should be zero (no self-inhibition)
|
||||
for i in 0..5 {
|
||||
assert_eq!(matrix[i][i], 0.0);
|
||||
}
|
||||
|
||||
// Should be symmetric
|
||||
for i in 0..5 {
|
||||
for j in 0..5 {
|
||||
assert_eq!(matrix[i][j], matrix[j][i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mexican_hat_profile() {
|
||||
let inhibition = LateralInhibition::new(10, 0.8, 0.9).with_radius(3);
|
||||
|
||||
// Inhibition should decrease with distance
|
||||
let w1 = inhibition.weight(5, 6); // Distance 1
|
||||
let w2 = inhibition.weight(5, 7); // Distance 2
|
||||
let w3 = inhibition.weight(5, 8); // Distance 3
|
||||
|
||||
assert!(w1 > w2, "Inhibition decreases with distance");
|
||||
assert!(w2 > w3, "Inhibition decreases with distance");
|
||||
assert_eq!(inhibition.weight(5, 9), 0.0, "Beyond radius");
|
||||
}
|
||||
}
|
||||
382
vendor/ruvector/crates/ruvector-nervous-system/src/compete/kwta.rs
vendored
Normal file
382
vendor/ruvector/crates/ruvector-nervous-system/src/compete/kwta.rs
vendored
Normal file
@@ -0,0 +1,382 @@
|
||||
//! K-Winner-Take-All Layer
|
||||
//!
|
||||
//! Selects top-k neurons for sparse distributed coding and attention mechanisms.
|
||||
|
||||
/// K-Winner-Take-All competition layer
|
||||
///
|
||||
/// Selects k neurons with highest activations for sparse distributed representations.
|
||||
/// Used in HNSW for multi-path routing and attention mechanisms.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// - O(n + k log k) using partial sorting
|
||||
/// - <10μs for 1000 neurons, k=50
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ruvector_nervous_system::compete::KWTALayer;
|
||||
///
|
||||
/// let kwta = KWTALayer::new(100, 5);
|
||||
/// let inputs: Vec<f32> = (0..100).map(|i| i as f32 / 100.0).collect();
|
||||
/// let winners = kwta.select(&inputs);
|
||||
/// assert_eq!(winners.len(), 5);
|
||||
/// // Winners are indices [95, 96, 97, 98, 99] (top 5 values)
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KWTALayer {
|
||||
/// Number of competing neurons
|
||||
size: usize,
|
||||
|
||||
/// Number of winners to select
|
||||
k: usize,
|
||||
|
||||
/// Optional activation threshold
|
||||
threshold: Option<f32>,
|
||||
}
|
||||
|
||||
impl KWTALayer {
|
||||
/// Create a new K-WTA layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - Total number of neurons
|
||||
/// * `k` - Number of winners to select
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if k > size or k == 0
|
||||
pub fn new(size: usize, k: usize) -> Self {
|
||||
assert!(k > 0, "k must be positive");
|
||||
assert!(k <= size, "k cannot exceed layer size");
|
||||
|
||||
Self {
|
||||
size,
|
||||
k,
|
||||
threshold: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set activation threshold
|
||||
///
|
||||
/// Only neurons exceeding this threshold can be selected as winners.
|
||||
pub fn with_threshold(mut self, threshold: f32) -> Self {
|
||||
self.threshold = Some(threshold);
|
||||
self
|
||||
}
|
||||
|
||||
/// Select top-k neurons
|
||||
///
|
||||
/// Returns indices of k neurons with highest activations, sorted in descending order.
|
||||
/// If threshold filtering results in fewer than k candidates, returns all candidates.
|
||||
/// Returns empty vec if no candidates meet the threshold.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// - O(n + k log k) using partial sort
|
||||
/// - Faster than full sort for small k
|
||||
pub fn select(&self, inputs: &[f32]) -> Vec<usize> {
|
||||
assert_eq!(inputs.len(), self.size, "Input size mismatch");
|
||||
|
||||
// Create (index, value) pairs
|
||||
let mut indexed: Vec<(usize, f32)> =
|
||||
inputs.iter().enumerate().map(|(i, &v)| (i, v)).collect();
|
||||
|
||||
// Filter by threshold if set
|
||||
if let Some(threshold) = self.threshold {
|
||||
indexed.retain(|(_, v)| *v >= threshold);
|
||||
}
|
||||
|
||||
// Handle empty case after filtering
|
||||
if indexed.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Partial sort to get top-k
|
||||
let k_actual = self.k.min(indexed.len());
|
||||
indexed.select_nth_unstable_by(k_actual - 1, |a, b| {
|
||||
b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
// Take top k and sort descending by value
|
||||
let mut winners: Vec<(usize, f32)> = indexed[..k_actual].to_vec();
|
||||
winners.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
// Return only indices
|
||||
winners.into_iter().map(|(i, _)| i).collect()
|
||||
}
|
||||
|
||||
/// Select top-k neurons with their activation values
|
||||
///
|
||||
/// Returns (index, value) pairs sorted by descending activation.
|
||||
/// Returns empty vec if no candidates meet the threshold.
|
||||
pub fn select_with_values(&self, inputs: &[f32]) -> Vec<(usize, f32)> {
|
||||
assert_eq!(inputs.len(), self.size, "Input size mismatch");
|
||||
|
||||
let mut indexed: Vec<(usize, f32)> =
|
||||
inputs.iter().enumerate().map(|(i, &v)| (i, v)).collect();
|
||||
|
||||
// Filter by threshold if set
|
||||
if let Some(threshold) = self.threshold {
|
||||
indexed.retain(|(_, v)| *v >= threshold);
|
||||
}
|
||||
|
||||
// Handle empty case after filtering
|
||||
if indexed.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Partial sort to get top-k
|
||||
let k_actual = self.k.min(indexed.len());
|
||||
indexed.select_nth_unstable_by(k_actual - 1, |a, b| {
|
||||
b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
// Take top k and sort descending
|
||||
let mut winners: Vec<(usize, f32)> = indexed[..k_actual].to_vec();
|
||||
winners.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
winners
|
||||
}
|
||||
|
||||
/// Create sparse activation vector
|
||||
///
|
||||
/// Returns a vector of size `size` with only top-k activations preserved.
|
||||
/// All other values are set to 0.
|
||||
pub fn sparse_activations(&self, inputs: &[f32]) -> Vec<f32> {
|
||||
let winners = self.select_with_values(inputs);
|
||||
let mut sparse = vec![0.0; self.size];
|
||||
|
||||
for (idx, value) in winners {
|
||||
sparse[idx] = value;
|
||||
}
|
||||
|
||||
sparse
|
||||
}
|
||||
|
||||
/// Create normalized sparse activation vector
|
||||
///
|
||||
/// Like `sparse_activations` but normalizes winner activations to sum to 1.0.
|
||||
pub fn sparse_normalized(&self, inputs: &[f32]) -> Vec<f32> {
|
||||
let winners = self.select_with_values(inputs);
|
||||
let mut sparse = vec![0.0; self.size];
|
||||
|
||||
// Calculate sum of winner activations
|
||||
let sum: f32 = winners.iter().map(|(_, v)| v).sum();
|
||||
|
||||
if sum > 0.0 {
|
||||
for (idx, value) in winners {
|
||||
sparse[idx] = value / sum;
|
||||
}
|
||||
}
|
||||
|
||||
sparse
|
||||
}
|
||||
|
||||
/// Get number of winners
|
||||
pub fn k(&self) -> usize {
|
||||
self.k
|
||||
}
|
||||
|
||||
/// Get layer size
|
||||
pub fn size(&self) -> usize {
|
||||
self.size
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_kwta_basic() {
|
||||
let kwta = KWTALayer::new(10, 3);
|
||||
let inputs: Vec<f32> = (0..10).map(|i| i as f32).collect();
|
||||
|
||||
let winners = kwta.select(&inputs);
|
||||
|
||||
assert_eq!(winners.len(), 3);
|
||||
assert_eq!(winners, vec![9, 8, 7], "Top 3 indices in descending order");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kwta_with_values() {
|
||||
let kwta = KWTALayer::new(10, 3);
|
||||
let inputs: Vec<f32> = (0..10).map(|i| i as f32).collect();
|
||||
|
||||
let winners = kwta.select_with_values(&inputs);
|
||||
|
||||
assert_eq!(winners.len(), 3);
|
||||
assert_eq!(winners[0], (9, 9.0));
|
||||
assert_eq!(winners[1], (8, 8.0));
|
||||
assert_eq!(winners[2], (7, 7.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kwta_threshold() {
|
||||
let kwta = KWTALayer::new(10, 5).with_threshold(7.0);
|
||||
let inputs: Vec<f32> = (0..10).map(|i| i as f32).collect();
|
||||
|
||||
let winners = kwta.select(&inputs);
|
||||
|
||||
// Only 3 values (7.0, 8.0, 9.0) exceed threshold
|
||||
assert_eq!(winners.len(), 3);
|
||||
assert_eq!(winners, vec![9, 8, 7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kwta_sparse_activations() {
|
||||
let kwta = KWTALayer::new(10, 3);
|
||||
let inputs: Vec<f32> = (0..10).map(|i| i as f32).collect();
|
||||
|
||||
let sparse = kwta.sparse_activations(&inputs);
|
||||
|
||||
assert_eq!(sparse.len(), 10);
|
||||
assert_eq!(sparse[9], 9.0);
|
||||
assert_eq!(sparse[8], 8.0);
|
||||
assert_eq!(sparse[7], 7.0);
|
||||
assert!(
|
||||
sparse[..7].iter().all(|&x| x == 0.0),
|
||||
"Non-winners should be zero"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kwta_sparse_normalized() {
|
||||
let kwta = KWTALayer::new(10, 3);
|
||||
let inputs: Vec<f32> = (0..10).map(|i| i as f32).collect();
|
||||
|
||||
let sparse = kwta.sparse_normalized(&inputs);
|
||||
|
||||
// Sum should be 1.0
|
||||
let sum: f32 = sparse.iter().sum();
|
||||
assert!(
|
||||
(sum - 1.0).abs() < 1e-6,
|
||||
"Normalized activations should sum to 1.0"
|
||||
);
|
||||
|
||||
// Winners should have proportional activations
|
||||
let expected_sum = 9.0 + 8.0 + 7.0; // Sum of top 3
|
||||
assert!((sparse[9] - 9.0 / expected_sum).abs() < 1e-6);
|
||||
assert!((sparse[8] - 8.0 / expected_sum).abs() < 1e-6);
|
||||
assert!((sparse[7] - 7.0 / expected_sum).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kwta_sorted_order() {
|
||||
let kwta = KWTALayer::new(10, 5);
|
||||
let inputs = vec![0.5, 0.9, 0.2, 0.8, 0.1, 0.7, 0.3, 0.6, 0.4, 0.0];
|
||||
|
||||
let winners = kwta.select_with_values(&inputs);
|
||||
|
||||
// Winners should be in descending order by value
|
||||
for i in 0..winners.len() - 1 {
|
||||
assert!(
|
||||
winners[i].1 >= winners[i + 1].1,
|
||||
"Winners should be sorted by descending value"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kwta_determinism() {
|
||||
let kwta = KWTALayer::new(100, 10);
|
||||
let inputs: Vec<f32> = (0..100).map(|i| (i * 7) as f32 % 100.0).collect();
|
||||
|
||||
let winners1 = kwta.select(&inputs);
|
||||
let winners2 = kwta.select(&inputs);
|
||||
|
||||
assert_eq!(winners1, winners2, "K-WTA should be deterministic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kwta_all_zeros() {
|
||||
let kwta = KWTALayer::new(10, 3);
|
||||
let inputs = vec![0.0; 10];
|
||||
|
||||
let winners = kwta.select(&inputs);
|
||||
|
||||
// Should still return k winners even if all equal
|
||||
assert_eq!(winners.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kwta_ties() {
|
||||
let kwta = KWTALayer::new(10, 3);
|
||||
let inputs = vec![1.0, 1.0, 1.0, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.0];
|
||||
|
||||
let winners = kwta.select_with_values(&inputs);
|
||||
|
||||
// Should select 3 winners from tied values
|
||||
assert_eq!(winners.len(), 3);
|
||||
assert!(
|
||||
winners.iter().all(|(_, v)| *v == 1.0),
|
||||
"Should select from highest tier"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "k must be positive")]
|
||||
fn test_kwta_zero_k() {
|
||||
KWTALayer::new(10, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "k cannot exceed layer size")]
|
||||
fn test_kwta_k_exceeds_size() {
|
||||
KWTALayer::new(10, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kwta_performance() {
|
||||
use std::time::Instant;
|
||||
|
||||
let kwta = KWTALayer::new(1000, 50);
|
||||
let inputs: Vec<f32> = (0..1000).map(|i| (i * 7) as f32 % 1000.0).collect();
|
||||
|
||||
let start = Instant::now();
|
||||
for _ in 0..1000 {
|
||||
let _ = kwta.select(&inputs);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let avg_micros = elapsed.as_micros() as f64 / 1000.0;
|
||||
println!("Average K-WTA selection time: {:.2}μs", avg_micros);
|
||||
|
||||
// Should complete in reasonable time (very relaxed for CI environments)
|
||||
assert!(
|
||||
avg_micros < 10000.0,
|
||||
"K-WTA should be reasonably fast (got {:.2}μs)",
|
||||
avg_micros
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kwta_small_k_advantage() {
|
||||
use std::time::Instant;
|
||||
|
||||
let inputs: Vec<f32> = (0..10000).map(|i| (i * 7) as f32 % 10000.0).collect();
|
||||
|
||||
// Small k
|
||||
let kwta_small = KWTALayer::new(10000, 10);
|
||||
let start = Instant::now();
|
||||
for _ in 0..100 {
|
||||
let _ = kwta_small.select(&inputs);
|
||||
}
|
||||
let time_small = start.elapsed();
|
||||
|
||||
// Large k
|
||||
let kwta_large = KWTALayer::new(10000, 1000);
|
||||
let start = Instant::now();
|
||||
for _ in 0..100 {
|
||||
let _ = kwta_large.select(&inputs);
|
||||
}
|
||||
let time_large = start.elapsed();
|
||||
|
||||
println!("Small k (10): {:?}", time_small);
|
||||
println!("Large k (1000): {:?}", time_large);
|
||||
|
||||
// Small k should be faster (partial sort advantage)
|
||||
// Note: This may not always hold due to variance, but generally true
|
||||
}
|
||||
}
|
||||
42
vendor/ruvector/crates/ruvector-nervous-system/src/compete/mod.rs
vendored
Normal file
42
vendor/ruvector/crates/ruvector-nervous-system/src/compete/mod.rs
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
//! Winner-Take-All Competition Module
|
||||
//!
|
||||
//! Implements neural competition mechanisms for sparse activation and fast routing.
|
||||
//! Based on cortical competition principles with lateral inhibition.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
//! - `WTALayer`: Single winner competition with lateral inhibition
|
||||
//! - `KWTALayer`: K-winners variant for sparse distributed coding
|
||||
//! - `LateralInhibition`: Inhibitory connection model
|
||||
//!
|
||||
//! # Performance
|
||||
//!
|
||||
//! - Single winner: <1μs for 1000 neurons
|
||||
//! - K-winners: <10μs for 1000 neurons, k=50
|
||||
//!
|
||||
//! # Use Cases
|
||||
//!
|
||||
//! 1. Fast routing in HNSW graph traversal
|
||||
//! 2. Sparse activation patterns for efficiency
|
||||
//! 3. Attention head selection in transformers
|
||||
|
||||
mod inhibition;
|
||||
mod kwta;
|
||||
mod wta;
|
||||
|
||||
pub use inhibition::LateralInhibition;
|
||||
pub use kwta::KWTALayer;
|
||||
pub use wta::WTALayer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_module_exports() {
|
||||
// Verify all exports are accessible
|
||||
let _wta = WTALayer::new(10, 0.5, 0.8);
|
||||
let _kwta = KWTALayer::new(10, 3);
|
||||
let _inhibition = LateralInhibition::new(10, 0.1, 0.9);
|
||||
}
|
||||
}
|
||||
287
vendor/ruvector/crates/ruvector-nervous-system/src/compete/wta.rs
vendored
Normal file
287
vendor/ruvector/crates/ruvector-nervous-system/src/compete/wta.rs
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
//! Winner-Take-All Layer Implementation
|
||||
//!
|
||||
//! Fast single-winner competition with lateral inhibition and refractory periods.
|
||||
//! Optimized for HNSW navigation decisions.
|
||||
|
||||
use crate::compete::inhibition::LateralInhibition;
|
||||
|
||||
/// Winner-Take-All competition layer
|
||||
///
|
||||
/// Implements neural competition where the highest-activation neuron dominates
|
||||
/// and suppresses others through lateral inhibition.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// - O(1) parallel time complexity with implicit argmax
|
||||
/// - Sub-microsecond winner selection for 1000 neurons
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ruvector_nervous_system::compete::WTALayer;
|
||||
///
|
||||
/// let mut wta = WTALayer::new(5, 0.5, 0.8); // 5 neurons to match input size
|
||||
/// let inputs = vec![0.1, 0.3, 0.9, 0.2, 0.4];
|
||||
/// let winner = wta.compete(&inputs);
|
||||
/// assert_eq!(winner, Some(2)); // Index 2 has highest activation (0.9)
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WTALayer {
|
||||
/// Membrane potentials for each neuron
|
||||
membranes: Vec<f32>,
|
||||
|
||||
/// Activation threshold for firing
|
||||
threshold: f32,
|
||||
|
||||
/// Strength of lateral inhibition (0.0-1.0)
|
||||
inhibition_strength: f32,
|
||||
|
||||
/// Refractory period in timesteps
|
||||
refractory_period: u32,
|
||||
|
||||
/// Current refractory counters
|
||||
refractory_counters: Vec<u32>,
|
||||
|
||||
/// Lateral inhibition model
|
||||
inhibition: LateralInhibition,
|
||||
}
|
||||
|
||||
impl WTALayer {
|
||||
/// Create a new WTA layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - Number of competing neurons (must be > 0)
|
||||
/// * `threshold` - Activation threshold for firing
|
||||
/// * `inhibition` - Lateral inhibition strength (0.0-1.0)
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `size` is 0.
|
||||
pub fn new(size: usize, threshold: f32, inhibition: f32) -> Self {
|
||||
assert!(size > 0, "size must be > 0");
|
||||
Self {
|
||||
membranes: vec![0.0; size],
|
||||
threshold,
|
||||
inhibition_strength: inhibition.clamp(0.0, 1.0),
|
||||
refractory_period: 10,
|
||||
refractory_counters: vec![0; size],
|
||||
inhibition: LateralInhibition::new(size, inhibition, 0.9),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run winner-take-all competition
|
||||
///
|
||||
/// Returns the index of the winning neuron, or None if no neuron exceeds threshold.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// - O(n) single-pass for update and max finding
|
||||
/// - <1μs for 1000 neurons
|
||||
pub fn compete(&mut self, inputs: &[f32]) -> Option<usize> {
|
||||
assert_eq!(inputs.len(), self.membranes.len(), "Input size mismatch");
|
||||
|
||||
// Single-pass: update membrane potentials and find max simultaneously
|
||||
let mut best_idx = None;
|
||||
let mut best_val = f32::NEG_INFINITY;
|
||||
|
||||
for (i, &input) in inputs.iter().enumerate() {
|
||||
if self.refractory_counters[i] == 0 {
|
||||
self.membranes[i] = input;
|
||||
if input > best_val {
|
||||
best_val = input;
|
||||
best_idx = Some(i);
|
||||
}
|
||||
} else {
|
||||
self.refractory_counters[i] = self.refractory_counters[i].saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
let winner_idx = best_idx?;
|
||||
|
||||
// Check if winner exceeds threshold
|
||||
if best_val < self.threshold {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Apply lateral inhibition
|
||||
self.inhibition.apply(&mut self.membranes, winner_idx);
|
||||
|
||||
// Set refractory period for winner
|
||||
self.refractory_counters[winner_idx] = self.refractory_period;
|
||||
|
||||
Some(winner_idx)
|
||||
}
|
||||
|
||||
/// Soft competition with normalized activations
|
||||
///
|
||||
/// Returns activation levels for all neurons after competition.
|
||||
/// Uses softmax-like transformation with lateral inhibition.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// - O(n) for normalization
|
||||
/// - ~2-3μs for 1000 neurons
|
||||
pub fn compete_soft(&mut self, inputs: &[f32]) -> Vec<f32> {
|
||||
assert_eq!(inputs.len(), self.membranes.len(), "Input size mismatch");
|
||||
|
||||
// Update membrane potentials
|
||||
for (i, &input) in inputs.iter().enumerate() {
|
||||
self.membranes[i] = input;
|
||||
}
|
||||
|
||||
// Find max for numerical stability
|
||||
let max_val = self
|
||||
.membranes
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// Softmax with temperature (controlled by inhibition strength)
|
||||
let temperature = 1.0 / (1.0 + self.inhibition_strength);
|
||||
let mut activations: Vec<f32> = self
|
||||
.membranes
|
||||
.iter()
|
||||
.map(|&x| ((x - max_val) / temperature).exp())
|
||||
.collect();
|
||||
|
||||
// Normalize
|
||||
let sum: f32 = activations.iter().sum();
|
||||
if sum > 0.0 {
|
||||
for a in &mut activations {
|
||||
*a /= sum;
|
||||
}
|
||||
}
|
||||
|
||||
activations
|
||||
}
|
||||
|
||||
/// Reset layer state
|
||||
pub fn reset(&mut self) {
|
||||
self.membranes.fill(0.0);
|
||||
self.refractory_counters.fill(0);
|
||||
}
|
||||
|
||||
/// Get current membrane potentials
|
||||
pub fn membranes(&self) -> &[f32] {
|
||||
&self.membranes
|
||||
}
|
||||
|
||||
/// Set refractory period
|
||||
pub fn set_refractory_period(&mut self, period: u32) {
|
||||
self.refractory_period = period;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_wta_basic() {
|
||||
let mut wta = WTALayer::new(5, 0.5, 0.8);
|
||||
let inputs = vec![0.1, 0.3, 0.9, 0.2, 0.4];
|
||||
|
||||
let winner = wta.compete(&inputs);
|
||||
assert_eq!(winner, Some(2), "Highest activation should win");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wta_threshold() {
|
||||
let mut wta = WTALayer::new(5, 0.95, 0.8);
|
||||
let inputs = vec![0.1, 0.3, 0.9, 0.2, 0.4];
|
||||
|
||||
let winner = wta.compete(&inputs);
|
||||
assert_eq!(winner, None, "No neuron exceeds threshold");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wta_soft_competition() {
|
||||
let mut wta = WTALayer::new(5, 0.5, 0.8);
|
||||
let inputs = vec![0.1, 0.3, 0.9, 0.2, 0.4];
|
||||
|
||||
let activations = wta.compete_soft(&inputs);
|
||||
|
||||
// Sum should be ~1.0
|
||||
let sum: f32 = activations.iter().sum();
|
||||
assert!((sum - 1.0).abs() < 0.001, "Activations should sum to 1.0");
|
||||
|
||||
// Highest input should have highest activation
|
||||
let max_idx = activations
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
|
||||
.map(|(i, _)| i)
|
||||
.unwrap();
|
||||
assert_eq!(max_idx, 2, "Highest input should have highest activation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wta_refractory_period() {
|
||||
let mut wta = WTALayer::new(3, 0.5, 0.8);
|
||||
wta.set_refractory_period(2);
|
||||
|
||||
// First competition
|
||||
let inputs = vec![0.6, 0.7, 0.8];
|
||||
let winner1 = wta.compete(&inputs);
|
||||
assert_eq!(winner1, Some(2));
|
||||
|
||||
// Second competition - winner should be in refractory
|
||||
let inputs = vec![0.6, 0.7, 0.8];
|
||||
let winner2 = wta.compete(&inputs);
|
||||
assert_ne!(winner2, Some(2), "Winner should be in refractory period");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wta_determinism() {
|
||||
let mut wta1 = WTALayer::new(10, 0.5, 0.8);
|
||||
let mut wta2 = WTALayer::new(10, 0.5, 0.8);
|
||||
|
||||
let inputs = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
|
||||
|
||||
let winner1 = wta1.compete(&inputs);
|
||||
let winner2 = wta2.compete(&inputs);
|
||||
|
||||
assert_eq!(winner1, winner2, "WTA should be deterministic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wta_reset() {
|
||||
let mut wta = WTALayer::new(5, 0.5, 0.8);
|
||||
let inputs = vec![0.1, 0.3, 0.9, 0.2, 0.4];
|
||||
|
||||
wta.compete(&inputs);
|
||||
wta.reset();
|
||||
|
||||
assert!(
|
||||
wta.membranes().iter().all(|&x| x == 0.0),
|
||||
"Membranes should be reset"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wta_performance() {
|
||||
use std::time::Instant;
|
||||
|
||||
let mut wta = WTALayer::new(1000, 0.5, 0.8);
|
||||
let inputs: Vec<f32> = (0..1000).map(|i| (i as f32) / 1000.0).collect();
|
||||
|
||||
let start = Instant::now();
|
||||
for _ in 0..1000 {
|
||||
wta.reset();
|
||||
let _ = wta.compete(&inputs);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let avg_micros = elapsed.as_micros() as f64 / 1000.0;
|
||||
println!("Average WTA competition time: {:.2}μs", avg_micros);
|
||||
|
||||
// Should be fast (relaxed for CI environments)
|
||||
assert!(
|
||||
avg_micros < 100.0,
|
||||
"WTA should be fast (got {:.2}μs)",
|
||||
avg_micros
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user