Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,694 @@
//! Mean Teacher Implementation
//!
//! Semi-supervised learning method using exponential moving average teacher models.
//! Based on "Mean teachers are better role models" (Tarvainen & Valpola, 2017).
//!
//! Key features:
//! - Student model (trainable) and teacher model (EMA of student)
//! - Consistency regularization between student and teacher predictions
//! - Support for labeled and unlabeled data
//! - Noise injection for robustness (Gaussian noise, dropout)
//! - Consistency weight ramp-up scheduling
//! - EMA update mechanism for teacher parameters
use crate::prelude::*;
use std::sync::Arc;
use parking_lot::RwLock;
/// Mean Teacher configuration parameters
#[derive(Debug, Clone)]
pub struct MeanTeacherConfig {
/// Exponential moving average decay rate for teacher updates
pub ema_decay: f32,
/// Maximum weight for consistency loss
pub consistency_weight: f32,
/// Number of epochs for consistency weight ramp-up
pub consistency_rampup: usize,
/// Noise level for input augmentation
pub noise_level: f32,
/// Augmentation strategy (Gaussian, Dropout, or Both)
pub augmentation_strategy: AugmentationStrategy,
}
impl Default for MeanTeacherConfig {
fn default() -> Self {
Self {
ema_decay: 0.999,
consistency_weight: 100.0,
consistency_rampup: 5,
noise_level: 0.15,
augmentation_strategy: AugmentationStrategy::Gaussian,
}
}
}
impl MeanTeacherConfig {
/// Create new Mean Teacher configuration
pub fn new() -> Self {
Self::default()
}
/// Set EMA decay rate
pub fn with_ema_decay(mut self, ema_decay: f32) -> Self {
self.ema_decay = ema_decay;
self
}
/// Set consistency weight
pub fn with_consistency_weight(mut self, consistency_weight: f32) -> Self {
self.consistency_weight = consistency_weight;
self
}
/// Set consistency ramp-up epochs
pub fn with_consistency_rampup(mut self, consistency_rampup: usize) -> Self {
self.consistency_rampup = consistency_rampup;
self
}
/// Set noise level
pub fn with_noise_level(mut self, noise_level: f32) -> Self {
self.noise_level = noise_level;
self
}
/// Set augmentation strategy
pub fn with_augmentation_strategy(mut self, augmentation_strategy: AugmentationStrategy) -> Self {
self.augmentation_strategy = augmentation_strategy;
self
}
}
/// Augmentation strategy for noise injection
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AugmentationStrategy {
/// Gaussian noise only
Gaussian,
/// Dropout noise only
Dropout,
/// Both Gaussian noise and dropout
Both,
}
/// Student model wrapper (trainable)
#[derive(Debug)]
pub struct StudentModel {
/// Linear layers
layers: Vec<LinearLayer>,
/// Input dimension
input_dim: usize,
/// Output dimension
output_dim: usize,
/// Training mode flag
training: bool,
/// Device
device: Device,
}
/// Teacher model wrapper (non-trainable, EMA of student)
#[derive(Debug)]
pub struct TeacherModel {
/// Linear layers
layers: Vec<LinearLayer>,
/// Input dimension
input_dim: usize,
/// Output dimension
output_dim: usize,
/// Device
device: Device,
}
/// Linear layer for neural networks
#[derive(Debug)]
struct LinearLayer {
weight: Arc<RwLock<Tensor>>,
bias: Arc<RwLock<Tensor>>,
}
impl LinearLayer {
fn new(input_dim: usize, output_dim: usize, device: &Device) -> Result<Self> {
// Xavier initialization
let scale = (2.0 / (input_dim + output_dim) as f32).sqrt();
let weight = Tensor::randn(vec![input_dim, output_dim], DType::F32, device)?
.mul_scalar(scale)?;
let bias = Tensor::zeros(vec![output_dim], device)?;
Ok(Self {
weight: Arc::new(RwLock::new(weight)),
bias: Arc::new(RwLock::new(bias)),
})
}
fn forward(&self, input: &Tensor) -> Result<Tensor> {
let weight = self.weight.read();
let bias = self.bias.read();
let output = input.matmul(&*weight)?;
output.add(&*bias)
}
fn get_weight(&self) -> Tensor {
self.weight.read().clone()
}
fn get_bias(&self) -> Tensor {
self.bias.read().clone()
}
fn set_weight(&self, new_weight: Tensor) -> Result<()> {
*self.weight.write() = new_weight;
Ok(())
}
fn set_bias(&self, new_bias: Tensor) -> Result<()> {
*self.bias.write() = new_bias;
Ok(())
}
}
impl StudentModel {
/// Create new student model
pub fn new(input_dim: usize, hidden_dim: usize, output_dim: usize, device: &Device) -> Result<Self> {
let layer1 = LinearLayer::new(input_dim, hidden_dim, device)?;
let layer2 = LinearLayer::new(hidden_dim, output_dim, device)?;
Ok(Self {
layers: vec![layer1, layer2],
input_dim,
output_dim,
training: false,
device: device.clone(),
})
}
/// Get input dimension
pub fn input_dim(&self) -> usize {
self.input_dim
}
/// Get output dimension
pub fn output_dim(&self) -> usize {
self.output_dim
}
/// Check if in training mode
pub fn is_training(&self) -> bool {
self.training
}
/// Set training mode
pub fn train(&mut self) {
self.training = true;
}
/// Set evaluation mode
pub fn eval(&mut self) {
self.training = false;
}
/// Forward pass through student model
pub fn forward(&self, input: &Tensor) -> Result<Tensor> {
let mut x = input.clone();
// First layer with ReLU activation
x = self.layers[0].forward(&x)?;
x = x.relu()?;
// Output layer
x = self.layers[1].forward(&x)?;
Ok(x)
}
/// Get all parameters
pub fn parameters(&self) -> Vec<Tensor> {
self.layers.iter().flat_map(|layer| {
vec![layer.get_weight(), layer.get_bias()]
}).collect()
}
/// Set parameters
pub fn set_parameters(&self, params: Vec<Tensor>) -> Result<()> {
assert_eq!(params.len(), self.layers.len() * 2);
for (i, layer) in self.layers.iter().enumerate() {
layer.set_weight(params[i * 2].clone())?;
layer.set_bias(params[i * 2 + 1].clone())?;
}
Ok(())
}
}
impl TeacherModel {
/// Create new teacher model
pub fn new(input_dim: usize, hidden_dim: usize, output_dim: usize, device: &Device) -> Result<Self> {
let layer1 = LinearLayer::new(input_dim, hidden_dim, device)?;
let layer2 = LinearLayer::new(hidden_dim, output_dim, device)?;
Ok(Self {
layers: vec![layer1, layer2],
input_dim,
output_dim,
device: device.clone(),
})
}
/// Get input dimension
pub fn input_dim(&self) -> usize {
self.input_dim
}
/// Get output dimension
pub fn output_dim(&self) -> usize {
self.output_dim
}
/// Check if in training mode (always false for teacher)
pub fn is_training(&self) -> bool {
false
}
/// Forward pass through teacher model (always in eval mode)
pub fn forward(&self, input: &Tensor) -> Result<Tensor> {
let mut x = input.clone();
// First layer with ReLU activation
x = self.layers[0].forward(&x)?;
x = x.relu()?;
// Output layer
x = self.layers[1].forward(&x)?;
Ok(x)
}
/// Get all parameters
pub fn parameters(&self) -> Vec<Tensor> {
self.layers.iter().flat_map(|layer| {
vec![layer.get_weight(), layer.get_bias()]
}).collect()
}
/// Set parameters
pub fn set_parameters(&self, params: Vec<Tensor>) -> Result<()> {
assert_eq!(params.len(), self.layers.len() * 2);
for (i, layer) in self.layers.iter().enumerate() {
layer.set_weight(params[i * 2].clone())?;
layer.set_bias(params[i * 2 + 1].clone())?;
}
Ok(())
}
}
/// Exponential Moving Average updater for teacher parameters
#[derive(Debug)]
pub struct EMAUpdater {
/// Decay rate for EMA
decay: f32,
/// Current step count
step: usize,
/// Device
device: Device,
}
impl EMAUpdater {
/// Create new EMA updater
pub fn new(decay: f32, device: &Device) -> Result<Self> {
Ok(Self {
decay,
step: 0,
device: device.clone(),
})
}
/// Get decay rate
pub fn decay(&self) -> f32 {
self.decay
}
/// Get current step
pub fn step(&self) -> usize {
self.step
}
/// Update teacher parameter with student parameter
pub fn update(&mut self, teacher_param: &Tensor, student_param: &Tensor) -> Result<Tensor> {
// EMA update: teacher = decay * teacher + (1 - decay) * student
let updated = teacher_param.mul_scalar(self.decay)?
.add(&student_param.mul_scalar(1.0 - self.decay)?)?;
self.step += 1;
Ok(updated)
}
/// Update all teacher parameters
pub fn update_all(&mut self, teacher_params: &[Tensor], student_params: &[Tensor]) -> Result<Vec<Tensor>> {
assert_eq!(teacher_params.len(), student_params.len());
let mut updated_params = Vec::new();
for (teacher_param, student_param) in teacher_params.iter().zip(student_params.iter()) {
updated_params.push(self.update(teacher_param, student_param)?);
}
Ok(updated_params)
}
}
/// Noise augmenter for input perturbation
#[derive(Debug)]
pub struct NoiseAugmenter {
strategy: AugmentationStrategy,
noise_level: f32,
device: Device,
}
impl NoiseAugmenter {
/// Create new noise augmenter
pub fn new(strategy: AugmentationStrategy, noise_level: f32, device: &Device) -> Result<Self> {
Ok(Self {
strategy,
noise_level,
device: device.clone(),
})
}
/// Apply noise to input tensor
pub fn apply_noise(&self, input: &Tensor, seed: Option<u64>) -> Result<Tensor> {
match self.strategy {
AugmentationStrategy::Gaussian => self.apply_gaussian_noise(input, seed),
AugmentationStrategy::Dropout => self.apply_dropout_noise(input, seed),
AugmentationStrategy::Both => {
let gaussian = self.apply_gaussian_noise(input, seed)?;
self.apply_dropout_noise(&gaussian, seed.map(|s| s.wrapping_add(1)))
}
}
}
fn apply_gaussian_noise(&self, input: &Tensor, _seed: Option<u64>) -> Result<Tensor> {
let noise = Tensor::randn(input.shape().clone(), &self.device)?
.mul_scalar(self.noise_level)?;
input.add(&noise)
}
fn apply_dropout_noise(&self, input: &Tensor, _seed: Option<u64>) -> Result<Tensor> {
// Simulate dropout by randomly scaling elements
let keep_prob = 1.0 - self.noise_level;
let mask = Tensor::rand(input.shape().clone(), DType::F32, &self.device)?;
let dropout_mask = mask.gt_scalar(self.noise_level)?
.to_dtype(DType::F32)?
.div_scalar(keep_prob)?;
input.mul(&dropout_mask)
}
}
/// Compute consistency loss (MSE) between student and teacher predictions
pub fn compute_consistency_loss(student_pred: &Tensor, teacher_pred: &Tensor) -> Result<Tensor> {
let diff = student_pred.sub(teacher_pred)?;
let squared_diff = diff.mul(&diff)?;
squared_diff.mean(None, false)
}
/// Consistency weight ramp-up scheduler
#[derive(Debug)]
pub struct ConsistencyRampUp {
rampup_epochs: usize,
max_weight: f32,
}
impl ConsistencyRampUp {
/// Create new consistency ramp-up scheduler
pub fn new(rampup_epochs: usize, max_weight: f32) -> Self {
Self {
rampup_epochs,
max_weight,
}
}
/// Get rampup epochs
pub fn rampup_epochs(&self) -> usize {
self.rampup_epochs
}
/// Get maximum weight
pub fn max_weight(&self) -> f32 {
self.max_weight
}
/// Get current consistency weight based on epoch
pub fn get_weight(&self, epoch: usize) -> f32 {
if epoch >= self.rampup_epochs {
self.max_weight
} else if self.rampup_epochs == 0 {
self.max_weight
} else {
// Linear ramp-up function (simple and predictable)
let progress = epoch as f32 / self.rampup_epochs as f32;
progress * self.max_weight
}
}
}
/// Training result from Mean Teacher step
#[derive(Debug, Clone)]
pub struct MeanTeacherTrainingResult {
/// Supervised loss (classification loss on labeled data)
pub supervised_loss: f32,
/// Consistency loss (between student and teacher predictions)
pub consistency_loss: f32,
/// Total loss (supervised + weighted consistency)
pub total_loss: f32,
}
/// Mean Teacher trainer
#[derive(Debug)]
pub struct MeanTeacherTrainer {
config: MeanTeacherConfig,
student: StudentModel,
teacher: TeacherModel,
ema_updater: EMAUpdater,
noise_augmenter: NoiseAugmenter,
rampup_scheduler: ConsistencyRampUp,
current_epoch: usize,
device: Device,
}
impl MeanTeacherTrainer {
/// Create new Mean Teacher trainer
pub fn new(
config: MeanTeacherConfig,
input_dim: usize,
hidden_dim: usize,
output_dim: usize,
device: &Device,
) -> Result<Self> {
let student = StudentModel::new(input_dim, hidden_dim, output_dim, device)?;
let mut teacher = TeacherModel::new(input_dim, hidden_dim, output_dim, device)?;
// Initialize teacher parameters with student parameters
let student_params = student.parameters();
teacher.set_parameters(student_params)?;
let ema_updater = EMAUpdater::new(config.ema_decay, device)?;
let noise_augmenter = NoiseAugmenter::new(
config.augmentation_strategy.clone(),
config.noise_level,
device,
)?;
let rampup_scheduler = ConsistencyRampUp::new(
config.consistency_rampup,
config.consistency_weight,
);
Ok(Self {
config: config.clone(),
student,
teacher,
ema_updater,
noise_augmenter,
rampup_scheduler,
current_epoch: 0,
device: device.clone(),
})
}
/// Get configuration
pub fn config(&self) -> &MeanTeacherConfig {
&self.config
}
/// Get current epoch
pub fn current_epoch(&self) -> usize {
self.current_epoch
}
/// Set current epoch
pub fn set_epoch(&mut self, epoch: usize) {
self.current_epoch = epoch;
}
/// Check if in training mode
pub fn is_training(&self) -> bool {
self.student.is_training()
}
/// Set training mode
pub fn train(&mut self) {
self.student.train();
}
/// Set evaluation mode
pub fn eval(&mut self) {
self.student.eval();
}
/// Get student parameters
pub fn get_student_parameters(&self) -> Result<Vec<Tensor>> {
Ok(self.student.parameters())
}
/// Get teacher parameters
pub fn get_teacher_parameters(&self) -> Result<Vec<Tensor>> {
Ok(self.teacher.parameters())
}
/// Supervised training step (labeled data only)
pub fn supervised_step(
&mut self,
images: &Tensor,
labels: &Tensor,
epoch: usize,
) -> Result<MeanTeacherTrainingResult> {
self.set_epoch(epoch);
// Forward pass through student
let student_pred = self.student.forward(images)?;
// Compute supervised loss (simplified cross-entropy)
let supervised_loss = self.compute_supervised_loss(&student_pred, labels)?;
// Update teacher parameters
self.update_teacher_parameters()?;
Ok(MeanTeacherTrainingResult {
supervised_loss: supervised_loss.to_vec::<f32>()?[0],
consistency_loss: 0.0,
total_loss: supervised_loss.to_vec::<f32>()?[0],
})
}
/// Unsupervised training step (unlabeled data only)
pub fn unsupervised_step(
&mut self,
unlabeled_images: &Tensor,
epoch: usize,
) -> Result<MeanTeacherTrainingResult> {
self.set_epoch(epoch);
// Apply different noise to inputs for student and teacher
let student_input = self.noise_augmenter.apply_noise(unlabeled_images, Some(42))?;
let teacher_input = self.noise_augmenter.apply_noise(unlabeled_images, Some(24))?;
// Forward passes
let student_pred = self.student.forward(&student_input)?;
let teacher_pred = self.teacher.forward(&teacher_input)?;
// Compute consistency loss
let consistency_loss = compute_consistency_loss(&student_pred, &teacher_pred)?;
// Apply consistency weight ramp-up
let consistency_weight = self.rampup_scheduler.get_weight(epoch);
let weighted_consistency_loss = consistency_loss.mul_scalar(consistency_weight)?;
// Update teacher parameters
self.update_teacher_parameters()?;
Ok(MeanTeacherTrainingResult {
supervised_loss: 0.0,
consistency_loss: weighted_consistency_loss.to_vec::<f32>()?[0],
total_loss: weighted_consistency_loss.to_vec::<f32>()?[0],
})
}
/// Mixed training step (labeled + unlabeled data)
pub fn mixed_step(
&mut self,
labeled_images: &Tensor,
labels: &Tensor,
unlabeled_images: &Tensor,
epoch: usize,
) -> Result<MeanTeacherTrainingResult> {
self.set_epoch(epoch);
// Supervised loss on labeled data
let student_pred_labeled = self.student.forward(labeled_images)?;
let supervised_loss = self.compute_supervised_loss(&student_pred_labeled, labels)?;
// Consistency loss on unlabeled data
let student_input = self.noise_augmenter.apply_noise(unlabeled_images, Some(42))?;
let teacher_input = self.noise_augmenter.apply_noise(unlabeled_images, Some(24))?;
let student_pred_unlabeled = self.student.forward(&student_input)?;
let teacher_pred_unlabeled = self.teacher.forward(&teacher_input)?;
let consistency_loss = compute_consistency_loss(&student_pred_unlabeled, &teacher_pred_unlabeled)?;
let consistency_weight = self.rampup_scheduler.get_weight(epoch);
let weighted_consistency_loss = consistency_loss.mul_scalar(consistency_weight)?;
// Total loss
let total_loss = supervised_loss.add(&weighted_consistency_loss)?;
// Update teacher parameters
self.update_teacher_parameters()?;
Ok(MeanTeacherTrainingResult {
supervised_loss: supervised_loss.to_vec::<f32>()?[0],
consistency_loss: weighted_consistency_loss.to_vec::<f32>()?[0],
total_loss: total_loss.to_vec::<f32>()?[0],
})
}
/// Extract features using student model (evaluation mode)
pub fn extract_features(&self, input: &Tensor) -> Result<Tensor> {
self.student.forward(input)
}
fn compute_supervised_loss(&self, predictions: &Tensor, labels: &Tensor) -> Result<Tensor> {
// Simplified cross-entropy loss
let log_probs = predictions.log_softmax(-1)?;
let labels_one_hot = self.to_one_hot(labels, predictions.shape()[1])?;
let loss = log_probs.mul(&labels_one_hot)?.sum(None, false)?.neg()?;
loss.div_scalar(predictions.shape()[0] as f32)
}
fn to_one_hot(&self, labels: &Tensor, num_classes: usize) -> Result<Tensor> {
let batch_size = labels.shape()[0];
let mut one_hot = Tensor::zeros(vec![batch_size, num_classes], DType::F32, &self.device)?;
// Simplified one-hot encoding (would need proper indexing in real implementation)
for i in 0..batch_size {
let label_val = labels.get(i)?.to_vec::<i64>()?[0] as usize;
if label_val < num_classes {
one_hot = one_hot.index_put(
&[Some(i)],
&Tensor::ones(vec![1], &self.device)?
)?;
}
}
Ok(one_hot)
}
fn update_teacher_parameters(&mut self) -> Result<()> {
let student_params = self.student.parameters();
let teacher_params = self.teacher.parameters();
let updated_params = self.ema_updater.update_all(&teacher_params, &student_params)?;
self.teacher.set_parameters(updated_params)?;
Ok(())
}
}