use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use std::collections::VecDeque;
use std::fmt::Debug;
use super::bptt::{
flatten_parameters, meta_gradient_frozen, rollout_loss_frozen, set_parameters, BpttConfig,
FrozenRollout, MetaTrainingTask, NetworkGradients,
};
use super::features::build_lstm_features;
use super::{LSTMNetwork, OutputTransform};
use crate::error::{OptimError, Result};
const FEATURE_HISTORY: usize = 5;
#[derive(Debug, Clone)]
struct AdamState<T: Float + Debug + Send + Sync + 'static> {
first_moment: Vec<T>,
second_moment: Vec<T>,
step: usize,
}
impl<T: Float + Debug + Send + Sync + 'static> AdamState<T> {
fn new(width: usize) -> Self {
Self {
first_moment: vec![T::zero(); width],
second_moment: vec![T::zero(); width],
step: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct MetaTrainer<T: Float + Debug + Send + Sync + 'static> {
config: BpttConfig,
adam: Option<AdamState<T>>,
last_gradient: Vec<T>,
loss_history: VecDeque<T>,
}
impl<T> MetaTrainer<T>
where
T: Float + Debug + Send + Sync + 'static + Default + Clone,
{
pub fn new(config: BpttConfig) -> Self {
Self {
config,
adam: None,
last_gradient: Vec::new(),
loss_history: VecDeque::new(),
}
}
pub fn config(&self) -> &BpttConfig {
&self.config
}
pub fn last_gradient_vector(&self) -> Vec<T> {
self.last_gradient.clone()
}
pub fn loss_history(&self) -> &VecDeque<T> {
&self.loss_history
}
pub fn prepare_network(&self, network: &mut LSTMNetwork<T>, dimension: usize) -> Result<()> {
if dimension == 0 {
return Err(OptimError::InvalidConfig(
"task dimension must be greater than 0".to_string(),
));
}
if network.output_projection.output_size() != dimension {
let hidden = network.output_projection.weights.ncols();
network.output_projection.reset(hidden, dimension);
}
Ok(())
}
pub fn capture_rollout<K>(
&self,
network: &mut LSTMNetwork<T>,
task: &K,
input_features: usize,
learning_rate: T,
) -> Result<FrozenRollout<T>>
where
K: MetaTrainingTask<T> + ?Sized,
{
if self.config.unroll_steps == 0 {
return Err(OptimError::InvalidConfig(
"unroll_steps must be greater than 0".to_string(),
));
}
self.prepare_network(network, task.dimension())?;
network.reset_state();
let transform = network.output_projection.output_transform;
let mut params = task.initial_parameters();
let mut history: VecDeque<Array1<T>> = VecDeque::new();
let mut losses: VecDeque<T> = VecDeque::new();
let mut inputs = Vec::with_capacity(self.config.unroll_steps);
let mut learning_rates = Vec::with_capacity(self.config.unroll_steps);
let mut adaptive_scales = Vec::with_capacity(self.config.unroll_steps);
for _ in 0..self.config.unroll_steps {
let gradient = task.gradient(¶ms);
let loss = task.loss(¶ms);
let recent: Vec<&Array1<T>> = if history.len() >= FEATURE_HISTORY {
history.iter().rev().take(FEATURE_HISTORY).collect()
} else {
Vec::new()
};
let loss_features = if losses.len() >= 2 {
let current = losses[losses.len() - 1];
let previous = losses[losses.len() - 2];
let eps: T = scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(T::zero);
let ratio = if previous.abs() > eps {
current / previous
} else {
T::one()
};
Some(vec![current - previous, ratio])
} else {
None
};
let features =
build_lstm_features(&gradient, &recent, loss_features.as_deref(), input_features)?;
let grad_norm = gradient
.iter()
.map(|&g| g * g)
.fold(T::zero(), |a, b| a + b)
.sqrt();
let adaptive_scale = T::one() / (T::one() + grad_norm);
let output = network.forward(&features)?;
let update = match transform {
OutputTransform::Identity => output.clone(),
OutputTransform::Tanh | OutputTransform::LearnedNonlinear => {
output.mapv(|v| v.tanh())
}
OutputTransform::ScaledTanh { scale } => {
let s: T = scirs2_core::numeric::NumCast::from(scale).unwrap_or_else(T::one);
output.mapv(|v| v.tanh() * s)
}
OutputTransform::AdaptiveScale => output.mapv(|v| v * adaptive_scale),
}
.mapv(|v| v * learning_rate);
for j in 0..params.len().min(update.len()) {
params[j] = params[j] - update[j];
}
history.push_back(gradient);
while history.len() > FEATURE_HISTORY {
history.pop_front();
}
losses.push_back(loss);
while losses.len() > FEATURE_HISTORY {
losses.pop_front();
}
inputs.push(features);
learning_rates.push(learning_rate);
adaptive_scales.push(adaptive_scale);
}
Ok(FrozenRollout {
inputs,
learning_rates,
adaptive_scales,
initial_parameters: task.initial_parameters(),
})
}
pub fn evaluate<K>(&self, network: &mut LSTMNetwork<T>, tasks: &[K], weights: &[T]) -> Result<T>
where
K: MetaTrainingTask<T>,
{
Self::validate_batch(tasks, weights)?;
let input_features = Self::input_width(network)?;
let learning_rate = self.rollout_learning_rate();
let mut total = T::zero();
let mut total_weight = T::zero();
for (task, &weight) in tasks.iter().zip(weights.iter()) {
let rollout = self.capture_rollout(network, task, input_features, learning_rate)?;
let loss = rollout_loss_frozen(network, task, &rollout)?;
total = total + weight * loss;
total_weight = total_weight + weight;
}
if total_weight <= T::zero() {
return Err(OptimError::InvalidConfig(
"task weights must sum to a positive value".to_string(),
));
}
Ok(total / total_weight)
}
pub fn meta_step<K>(
&mut self,
network: &mut LSTMNetwork<T>,
tasks: &[K],
weights: &[T],
) -> Result<T>
where
K: MetaTrainingTask<T>,
{
Self::validate_batch(tasks, weights)?;
let dimension = tasks[0].dimension();
if let Some(bad) = tasks.iter().position(|t| t.dimension() != dimension) {
return Err(OptimError::InvalidConfig(format!(
"task {bad} has dimension {} but task 0 has {dimension}; a single \
meta-step needs a shared optimizee dimension because the \
controller's output projection is sized to it",
tasks[bad].dimension()
)));
}
self.prepare_network(network, dimension)?;
let input_features = Self::input_width(network)?;
let learning_rate = self.rollout_learning_rate();
let mut accumulated: Option<NetworkGradients<T>> = None;
let mut total_loss = T::zero();
let mut total_weight = T::zero();
for (task, &weight) in tasks.iter().zip(weights.iter()) {
let rollout = self.capture_rollout(network, task, input_features, learning_rate)?;
let (loss, grads) = meta_gradient_frozen(network, task, &rollout)?;
total_loss = total_loss + weight * loss;
total_weight = total_weight + weight;
match accumulated.as_mut() {
Some(acc) => acc.add_assign_weighted(&grads, weight),
None => {
let mut scaled = grads;
scaled.scale_by(weight);
accumulated = Some(scaled);
}
}
}
if total_weight <= T::zero() {
return Err(OptimError::InvalidConfig(
"task weights must sum to a positive value".to_string(),
));
}
let mut gradients = match accumulated {
Some(g) => g,
None => {
return Err(OptimError::InsufficientData(
"no task produced a meta-gradient".to_string(),
))
}
};
gradients.scale_by(T::one() / total_weight);
if self.config.gradient_clip > 0.0 {
let clip: T = scirs2_core::numeric::NumCast::from(self.config.gradient_clip)
.unwrap_or_else(T::one);
let norm = gradients.l2_norm();
if norm > clip && norm > T::zero() {
gradients.scale_by(clip / norm);
}
}
let flat_gradient = gradients.to_vec();
self.apply_adam(network, &flat_gradient)?;
self.last_gradient = flat_gradient;
let mean_loss = total_loss / total_weight;
self.loss_history.push_back(mean_loss);
while self.loss_history.len() > 1024 {
self.loss_history.pop_front();
}
Ok(mean_loss)
}
fn apply_adam(&mut self, network: &mut LSTMNetwork<T>, gradient: &[T]) -> Result<()> {
let mut params = flatten_parameters(network);
if params.len() != gradient.len() {
return Err(OptimError::ComputationError(format!(
"meta-gradient has {} entries but the controller has {}",
gradient.len(),
params.len()
)));
}
let state = self
.adam
.get_or_insert_with(|| AdamState::new(params.len()));
if state.first_moment.len() != params.len() {
*state = AdamState::new(params.len());
}
state.step += 1;
let lr: T = scirs2_core::numeric::NumCast::from(self.config.meta_learning_rate)
.unwrap_or_else(T::zero);
let beta1: T = scirs2_core::numeric::NumCast::from(0.9).unwrap_or_else(T::zero);
let beta2: T = scirs2_core::numeric::NumCast::from(0.999).unwrap_or_else(T::zero);
let eps: T = scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(T::zero);
let bias1: T = scirs2_core::numeric::NumCast::from(1.0 - 0.9_f64.powi(state.step as i32))
.unwrap_or_else(T::one);
let bias2: T = scirs2_core::numeric::NumCast::from(1.0 - 0.999_f64.powi(state.step as i32))
.unwrap_or_else(T::one);
for i in 0..params.len() {
let g = gradient[i];
state.first_moment[i] = beta1 * state.first_moment[i] + (T::one() - beta1) * g;
state.second_moment[i] = beta2 * state.second_moment[i] + (T::one() - beta2) * g * g;
let m_hat = state.first_moment[i] / bias1;
let v_hat = state.second_moment[i] / bias2;
params[i] = params[i] - lr * m_hat / (v_hat.sqrt() + eps);
}
set_parameters(network, ¶ms)
}
fn rollout_learning_rate(&self) -> T {
T::one()
}
fn input_width(network: &LSTMNetwork<T>) -> Result<usize> {
match network.layers.first() {
Some(layer) => Ok(layer.weight_ih.ncols()),
None => Err(OptimError::InvalidConfig(
"the controller has no LSTM layers".to_string(),
)),
}
}
fn validate_batch<K>(tasks: &[K], weights: &[T]) -> Result<()> {
if tasks.is_empty() {
return Err(OptimError::InsufficientData(
"meta-training needs at least one task".to_string(),
));
}
if weights.len() < tasks.len() {
return Err(OptimError::InvalidConfig(format!(
"got {} tasks but only {} weights",
tasks.len(),
weights.len()
)));
}
Ok(())
}
}