use crate::Result;
use super::{WeightMatrix, VectorCodebooks, QuantizationIndices};
#[derive(Debug)]
pub struct TeacherGuidedRefiner {
max_iterations: usize,
kl_weight: f32,
cosine_weight: f32,
learning_rate: f32,
convergence_threshold: f32,
}
impl TeacherGuidedRefiner {
pub fn new(max_iterations: usize, kl_weight: f32, cosine_weight: f32, learning_rate: f32) -> Self {
Self {
max_iterations,
kl_weight,
cosine_weight,
learning_rate,
convergence_threshold: 1e-6,
}
}
pub fn refine(
&mut self,
codebooks: &mut VectorCodebooks,
indices: &QuantizationIndices,
original_weights: &WeightMatrix,
teacher_outputs: Option<&[f32]>,
) -> Result<f32> {
if teacher_outputs.is_none() {
return self.refine_reconstruction_only(codebooks, indices, original_weights);
}
let teacher_outputs = teacher_outputs.unwrap();
let mut prev_loss = f32::INFINITY;
let mut best_accuracy: f32 = 0.0;
for iteration in 0..self.max_iterations {
let student_weights = self.reconstruct_from_codebooks(codebooks, indices, original_weights)?;
let student_outputs = self.compute_model_outputs(&student_weights)?;
let kl_loss = self.compute_kl_divergence(teacher_outputs, &student_outputs);
let cosine_loss = self.compute_cosine_similarity_loss(teacher_outputs, &student_outputs);
let total_loss = self.kl_weight * kl_loss + self.cosine_weight * cosine_loss;
self.update_codebooks(codebooks, indices, original_weights, teacher_outputs, &student_outputs)?;
if (prev_loss - total_loss).abs() < self.convergence_threshold {
break;
}
prev_loss = total_loss;
let accuracy = 1.0 - (total_loss / (kl_loss + cosine_loss + 1e-8));
best_accuracy = best_accuracy.max(accuracy);
if iteration % 10 == 0 {
println!("Iteration {}: Loss = {:.6}, Accuracy = {:.4}", iteration, total_loss, accuracy);
}
}
Ok(best_accuracy)
}
fn refine_reconstruction_only(
&mut self,
codebooks: &mut VectorCodebooks,
indices: &QuantizationIndices,
original_weights: &WeightMatrix,
) -> Result<f32> {
let mut prev_loss = f32::INFINITY;
let mut best_accuracy: f32 = 0.0;
for iteration in 0..self.max_iterations {
let reconstructed = self.reconstruct_from_codebooks(codebooks, indices, original_weights)?;
let mse_loss = self.compute_mse_loss(&original_weights.data, &reconstructed);
self.update_codebooks_mse(codebooks, indices, original_weights)?;
if (prev_loss - mse_loss).abs() < self.convergence_threshold {
break;
}
prev_loss = mse_loss;
let max_val = original_weights.data.iter().fold(0.0f32, |acc, &x| acc.max(x.abs()));
let normalized_mse = mse_loss / (max_val * max_val + 1e-8);
let accuracy = (1.0 - normalized_mse).max(0.0);
best_accuracy = best_accuracy.max(accuracy);
if iteration % 10 == 0 {
println!("Iteration {}: MSE = {:.6}, Accuracy = {:.4}", iteration, mse_loss, accuracy);
}
}
Ok(best_accuracy)
}
fn reconstruct_from_codebooks(
&self,
codebooks: &VectorCodebooks,
indices: &QuantizationIndices,
original_weights: &WeightMatrix,
) -> Result<Vec<f32>> {
let rows = original_weights.rows();
let cols = original_weights.cols();
let num_subspaces = codebooks.level1_codebooks.len();
let mut reconstructed = vec![0.0; rows * cols];
for row_idx in 0..rows {
for subspace_idx in 0..num_subspaces {
let start_col = subspace_idx * codebooks.subspace_size;
let l1_idx = indices.level1_indices[row_idx][subspace_idx] as usize;
let l2_idx = indices.level2_indices[row_idx][subspace_idx] as usize;
let l1_centroid = &codebooks.level1_codebooks[subspace_idx][l1_idx].centroid;
let l2_centroid = &codebooks.level2_codebooks[subspace_idx][l2_idx].centroid;
for (offset, (&l1_val, &l2_val)) in l1_centroid.iter().zip(l2_centroid.iter()).enumerate() {
let col_idx = start_col + offset;
let flat_idx = row_idx * cols + col_idx;
reconstructed[flat_idx] = l1_val + l2_val;
}
}
}
Ok(reconstructed)
}
fn compute_model_outputs(&self, weights: &[f32]) -> Result<Vec<f32>> {
let mut outputs = Vec::with_capacity(weights.len());
for &weight in weights {
let activated = if weight > 0.0 {
weight * (1.0 - (-weight).exp()) } else {
weight * 0.01 };
outputs.push(activated);
}
Ok(outputs)
}
fn compute_kl_divergence(&self, teacher_outputs: &[f32], student_outputs: &[f32]) -> f32 {
assert_eq!(teacher_outputs.len(), student_outputs.len());
let teacher_probs = self.softmax(teacher_outputs);
let student_probs = self.softmax(student_outputs);
let mut kl_div = 0.0;
for (&p, &q) in teacher_probs.iter().zip(student_probs.iter()) {
if p > 1e-8 && q > 1e-8 {
kl_div += p * (p / q).ln();
}
}
kl_div
}
fn compute_cosine_similarity_loss(&self, teacher_hidden: &[f32], student_hidden: &[f32]) -> f32 {
assert_eq!(teacher_hidden.len(), student_hidden.len());
let dot_product: f32 = teacher_hidden.iter().zip(student_hidden.iter()).map(|(&a, &b)| a * b).sum();
let teacher_norm: f32 = teacher_hidden.iter().map(|&x| x * x).sum::<f32>().sqrt();
let student_norm: f32 = student_hidden.iter().map(|&x| x * x).sum::<f32>().sqrt();
let cosine_sim = if teacher_norm > 1e-8 && student_norm > 1e-8 {
dot_product / (teacher_norm * student_norm)
} else {
0.0
};
1.0 - cosine_sim
}
fn compute_mse_loss(&self, original: &[f32], reconstructed: &[f32]) -> f32 {
assert_eq!(original.len(), reconstructed.len());
let mse: f32 = original.iter()
.zip(reconstructed.iter())
.map(|(&orig, &recon)| (orig - recon).powi(2))
.sum::<f32>() / original.len() as f32;
mse
}
fn softmax(&self, logits: &[f32]) -> Vec<f32> {
let max_logit = logits.iter().fold(f32::NEG_INFINITY, |acc, &x| acc.max(x));
let exp_logits: Vec<f32> = logits.iter().map(|&x| (x - max_logit).exp()).collect();
let sum_exp: f32 = exp_logits.iter().sum();
exp_logits.iter().map(|&x| x / sum_exp).collect()
}
fn update_codebooks(
&mut self,
codebooks: &mut VectorCodebooks,
indices: &QuantizationIndices,
original_weights: &WeightMatrix,
teacher_outputs: &[f32],
student_outputs: &[f32],
) -> Result<()> {
self.update_codebooks_mse(codebooks, indices, original_weights)
}
fn update_codebooks_mse(
&mut self,
codebooks: &mut VectorCodebooks,
indices: &QuantizationIndices,
original_weights: &WeightMatrix,
) -> Result<()> {
let rows = original_weights.rows();
let num_subspaces = codebooks.level1_codebooks.len();
for subspace_idx in 0..num_subspaces {
let start_col = subspace_idx * codebooks.subspace_size;
let end_col = start_col + codebooks.subspace_size;
for centroid_idx in 0..codebooks.level1_codebooks[subspace_idx].len() {
let mut assigned_vectors = Vec::new();
for row_idx in 0..rows {
if indices.level1_indices[row_idx][subspace_idx] == centroid_idx as u8 {
let row = original_weights.get_row(row_idx);
assigned_vectors.push(&row[start_col..end_col]);
}
}
if !assigned_vectors.is_empty() {
let centroid = &mut codebooks.level1_codebooks[subspace_idx][centroid_idx].centroid;
for dim in 0..centroid.len() {
let mean = assigned_vectors.iter().map(|v| v[dim]).sum::<f32>() / assigned_vectors.len() as f32;
centroid[dim] = centroid[dim] * (1.0 - self.learning_rate) + mean * self.learning_rate;
}
}
}
for centroid_idx in 0..codebooks.level2_codebooks[subspace_idx].len() {
let mut assigned_residuals = Vec::new();
for row_idx in 0..rows {
if indices.level2_indices[row_idx][subspace_idx] == centroid_idx as u8 {
let row = original_weights.get_row(row_idx);
let original_subspace = &row[start_col..end_col];
let l1_idx = indices.level1_indices[row_idx][subspace_idx] as usize;
let l1_centroid = &codebooks.level1_codebooks[subspace_idx][l1_idx].centroid;
let residual: Vec<f32> = original_subspace.iter()
.zip(l1_centroid.iter())
.map(|(&orig, &l1)| orig - l1)
.collect();
assigned_residuals.push(residual);
}
}
if !assigned_residuals.is_empty() {
let centroid = &mut codebooks.level2_codebooks[subspace_idx][centroid_idx].centroid;
for dim in 0..centroid.len() {
let mean = assigned_residuals.iter().map(|v| v[dim]).sum::<f32>() / assigned_residuals.len() as f32;
centroid[dim] = centroid[dim] * (1.0 - self.learning_rate) + mean * self.learning_rate;
}
}
}
}
Ok(())
}
}
pub struct DistillationLoss {
pub kl_divergence: f32,
pub cosine_similarity_loss: f32,
pub total_loss: f32,
pub accuracy_estimate: f32,
}
impl DistillationLoss {
pub fn new(kl_div: f32, cosine_loss: f32, kl_weight: f32, cosine_weight: f32) -> Self {
let total = kl_weight * kl_div + cosine_weight * cosine_loss;
let accuracy = (1.0 - total / (kl_div + cosine_loss + 1e-8)).max(0.0);
Self {
kl_divergence: kl_div,
cosine_similarity_loss: cosine_loss,
total_loss: total,
accuracy_estimate: accuracy,
}
}
pub fn print_summary(&self) {
println!("Distillation Loss Summary:");
println!(" KL Divergence: {:.6}", self.kl_divergence);
println!(" Cosine Loss: {:.6}", self.cosine_similarity_loss);
println!(" Total Loss: {:.6}", self.total_loss);
println!(" Accuracy Estimate: {:.4}", self.accuracy_estimate);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::novaq::{CodebookBuilder, CodebookEntry};
#[test]
fn test_refiner_creation() {
let refiner = TeacherGuidedRefiner::new(100, 1.0, 0.5, 0.001);
assert_eq!(refiner.max_iterations, 100);
assert!((refiner.kl_weight - 1.0).abs() < 1e-6);
assert!((refiner.cosine_weight - 0.5).abs() < 1e-6);
}
#[test]
fn test_kl_divergence_calculation() {
let refiner = TeacherGuidedRefiner::new(10, 1.0, 0.5, 0.001);
let teacher_outputs = vec![1.0, 2.0, 3.0];
let student_outputs = vec![1.1, 1.9, 3.1];
let kl_div = refiner.compute_kl_divergence(&teacher_outputs, &student_outputs);
assert!(kl_div >= 0.0); assert!(kl_div < 1.0); }
#[test]
fn test_cosine_similarity_loss() {
let refiner = TeacherGuidedRefiner::new(10, 1.0, 0.5, 0.001);
let teacher_hidden = vec![1.0, 2.0, 3.0];
let student_hidden = vec![1.0, 2.0, 3.0];
let cosine_loss = refiner.compute_cosine_similarity_loss(&teacher_hidden, &student_hidden);
assert!(cosine_loss.abs() < 1e-6); }
#[test]
fn test_mse_loss() {
let refiner = TeacherGuidedRefiner::new(10, 1.0, 0.5, 0.001);
let original = vec![1.0, 2.0, 3.0, 4.0];
let reconstructed = vec![1.1, 1.9, 3.1, 3.9];
let mse = refiner.compute_mse_loss(&original, &reconstructed);
let expected_mse = (0.1*0.1 + 0.1*0.1 + 0.1*0.1 + 0.1*0.1) / 4.0;
assert!((mse - expected_mse).abs() < 1e-6);
}
#[test]
fn test_softmax() {
let refiner = TeacherGuidedRefiner::new(10, 1.0, 0.5, 0.001);
let logits = vec![1.0, 2.0, 3.0];
let probs = refiner.softmax(&logits);
let sum: f32 = probs.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
assert!(probs.iter().all(|&p| p > 0.0));
let max_idx = logits.iter().enumerate().max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()).unwrap().0;
let max_prob_idx = probs.iter().enumerate().max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()).unwrap().0;
assert_eq!(max_idx, max_prob_idx);
}
#[test]
fn test_reconstruction() {
let refiner = TeacherGuidedRefiner::new(10, 1.0, 0.5, 0.001);
let data = vec![1.0, 2.0, 3.0, 4.0];
let weights = WeightMatrix::new(data.clone(), vec![2, 2], "test".to_string());
let level1_codebooks = vec![vec![
CodebookEntry { centroid: vec![1.0, 2.0], usage_count: 1 },
CodebookEntry { centroid: vec![3.0, 4.0], usage_count: 1 },
]];
let level2_codebooks = vec![vec![
CodebookEntry { centroid: vec![0.0, 0.0], usage_count: 2 },
]];
let codebooks = VectorCodebooks {
level1_codebooks,
level2_codebooks,
subspace_size: 2,
};
let indices = QuantizationIndices {
level1_indices: vec![vec![0], vec![1]],
level2_indices: vec![vec![0], vec![0]],
};
let reconstructed = refiner.reconstruct_from_codebooks(&codebooks, &indices, &weights).unwrap();
assert_eq!(reconstructed.len(), 4);
}
}