1use crate::layers::lstm_cell::LSTMCellGradients;
2use crate::loss::{LossFunction, MSELoss};
3use crate::models::lstm_network::{pad_batch, LSTMNetwork};
4use crate::optimizers::{Optimizer, ScheduledOptimizer, SGD};
5use crate::persistence::SerializableLSTMNetwork;
6use crate::schedulers::LearningRateScheduler;
7use ndarray::Array2;
8use std::time::Instant;
9
10pub fn sequence_gradients<L: LossFunction>(
13 network: &mut LSTMNetwork,
14 loss_function: &L,
15 inputs: &[Array2<f64>],
16 targets: &[Array2<f64>],
17) -> (f64, Vec<LSTMCellGradients>) {
18 let (outputs, caches) = network.forward_sequence_with_cache(inputs);
19 let mut total_loss = 0.0;
20 let d_outputs: Vec<Array2<f64>> = outputs
21 .iter()
22 .zip(targets)
23 .map(|((output, _), target)| {
24 total_loss += loss_function.compute_loss(output, target);
25 loss_function.compute_gradient(output, target)
26 })
27 .collect();
28 let (gradients, _) = network.backward_sequence(&d_outputs, &caches);
29 (total_loss, gradients)
30}
31
32pub fn clip_global_norm(gradients: &mut [LSTMCellGradients], max_norm: f64) {
34 let norm = gradients
35 .iter()
36 .map(LSTMCellGradients::squared_norm)
37 .sum::<f64>()
38 .sqrt();
39 if norm > max_norm {
40 for g in gradients.iter_mut() {
41 g.scale(max_norm / norm);
42 }
43 }
44}
45
46pub struct TrainingConfig {
48 pub epochs: usize,
49 pub print_every: usize,
50 pub clip_gradient: Option<f64>,
51 pub log_lr_changes: bool,
52 pub early_stopping: Option<EarlyStoppingConfig>,
53}
54
55#[derive(Debug, Clone)]
57pub struct EarlyStoppingConfig {
58 pub patience: usize,
60 pub min_delta: f64,
62 pub restore_best_weights: bool,
64 pub monitor: EarlyStoppingMetric,
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub enum EarlyStoppingMetric {
71 ValidationLoss,
72 TrainLoss,
73}
74
75impl Default for EarlyStoppingConfig {
76 fn default() -> Self {
77 EarlyStoppingConfig {
78 patience: 10,
79 min_delta: 1e-4,
80 restore_best_weights: true,
81 monitor: EarlyStoppingMetric::ValidationLoss,
82 }
83 }
84}
85
86impl Default for TrainingConfig {
87 fn default() -> Self {
88 TrainingConfig {
89 epochs: 100,
90 print_every: 10,
91 clip_gradient: Some(5.0),
92 log_lr_changes: true,
93 early_stopping: None,
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct TrainingMetrics {
101 pub epoch: usize,
102 pub train_loss: f64,
103 pub validation_loss: Option<f64>,
104 pub time_elapsed: f64,
105 pub learning_rate: f64,
106}
107
108#[derive(Debug, Clone)]
110pub struct EarlyStopper {
111 config: EarlyStoppingConfig,
112 best_score: f64,
113 wait_count: usize,
114 stopped_epoch: Option<usize>,
115 best_weights: Option<SerializableLSTMNetwork>, }
117
118impl EarlyStopper {
119 pub fn new(config: EarlyStoppingConfig) -> Self {
120 EarlyStopper {
121 config,
122 best_score: f64::INFINITY,
123 wait_count: 0,
124 stopped_epoch: None,
125 best_weights: None,
126 }
127 }
128
129 pub fn should_stop(
132 &mut self,
133 current_metrics: &TrainingMetrics,
134 network: &LSTMNetwork,
135 ) -> (bool, bool) {
136 let current_score = match self.config.monitor {
137 EarlyStoppingMetric::ValidationLoss => {
138 match current_metrics.validation_loss {
139 Some(val_loss) => val_loss,
140 None => {
141 current_metrics.train_loss
143 }
144 }
145 }
146 EarlyStoppingMetric::TrainLoss => current_metrics.train_loss,
147 };
148
149 let is_improvement = current_score < self.best_score - self.config.min_delta;
150
151 if is_improvement {
152 self.best_score = current_score;
153 self.wait_count = 0;
154
155 if self.config.restore_best_weights {
157 self.best_weights = Some(network.into());
158 }
159
160 (false, true)
161 } else {
162 self.wait_count += 1;
163
164 if self.wait_count >= self.config.patience {
165 self.stopped_epoch = Some(current_metrics.epoch);
166 (true, false)
167 } else {
168 (false, false)
169 }
170 }
171 }
172
173 pub fn stopped_epoch(&self) -> Option<usize> {
175 self.stopped_epoch
176 }
177
178 pub fn best_score(&self) -> f64 {
180 self.best_score
181 }
182
183 pub fn restore_best_weights(&self, network: &mut LSTMNetwork) -> Result<(), String> {
185 if let Some(ref weights) = self.best_weights {
186 *network = weights.clone().into();
187 Ok(())
188 } else {
189 Err("No best weights available to restore".to_string())
190 }
191 }
192}
193
194pub struct LSTMTrainer<L: LossFunction, O: Optimizer> {
196 pub network: LSTMNetwork,
197 pub loss_function: L,
198 pub optimizer: O,
199 pub config: TrainingConfig,
200 pub metrics_history: Vec<TrainingMetrics>,
201 early_stopper: Option<EarlyStopper>,
202}
203
204impl<L: LossFunction, O: Optimizer> LSTMTrainer<L, O> {
205 pub fn new(network: LSTMNetwork, loss_function: L, optimizer: O) -> Self {
206 LSTMTrainer {
207 network,
208 loss_function,
209 optimizer,
210 config: TrainingConfig::default(),
211 metrics_history: Vec::new(),
212 early_stopper: None,
213 }
214 }
215
216 pub fn with_config(mut self, config: TrainingConfig) -> Self {
217 self.early_stopper = config
219 .early_stopping
220 .as_ref()
221 .map(|es_config| EarlyStopper::new(es_config.clone()));
222 self.config = config;
223 self
224 }
225
226 pub fn train_sequence(&mut self, inputs: &[Array2<f64>], targets: &[Array2<f64>]) -> f64 {
228 if inputs.len() != targets.len() {
229 panic!("Inputs and targets must have the same length");
230 }
231
232 self.network.train();
233
234 let (total_loss, mut total_gradients) =
235 sequence_gradients(&mut self.network, &self.loss_function, inputs, targets);
236
237 if let Some(clip_value) = self.config.clip_gradient {
238 clip_global_norm(&mut total_gradients, clip_value);
239 }
240
241 self.network
242 .update_parameters(&total_gradients, &mut self.optimizer);
243
244 total_loss / inputs.len() as f64
245 }
246
247 pub fn train(
249 &mut self,
250 train_data: &[(Vec<Array2<f64>>, Vec<Array2<f64>>)],
251 validation_data: Option<&[(Vec<Array2<f64>>, Vec<Array2<f64>>)]>,
252 ) {
253 println!("Starting training for {} epochs...", self.config.epochs);
254
255 for epoch in 0..self.config.epochs {
256 let start_time = Instant::now();
257 let mut epoch_loss = 0.0;
258
259 self.network.train();
261 for (inputs, targets) in train_data {
262 let loss = self.train_sequence(inputs, targets);
263 epoch_loss += loss;
264 }
265 epoch_loss /= train_data.len() as f64;
266
267 let validation_loss = if let Some(val_data) = validation_data {
268 self.network.eval();
269 Some(self.evaluate(val_data))
270 } else {
271 None
272 };
273
274 let time_elapsed = start_time.elapsed().as_secs_f64();
275
276 let current_lr = self.optimizer.get_learning_rate();
277 let metrics = TrainingMetrics {
278 epoch,
279 train_loss: epoch_loss,
280 validation_loss,
281 time_elapsed,
282 learning_rate: current_lr,
283 };
284
285 self.metrics_history.push(metrics.clone());
286
287 let mut should_stop = false;
289 let mut is_best = false;
290 if let Some(ref mut early_stopper) = self.early_stopper {
291 let (stop, best) = early_stopper.should_stop(&metrics, &self.network);
292 should_stop = stop;
293 is_best = best;
294 }
295
296 if epoch % self.config.print_every == 0 {
297 let best_indicator = if is_best { " *" } else { "" };
298 if let Some(val_loss) = validation_loss {
299 println!(
300 "Epoch {}: Train Loss: {:.6}, Val Loss: {:.6}, LR: {:.2e}, Time: {:.2}s{}",
301 epoch, epoch_loss, val_loss, current_lr, time_elapsed, best_indicator
302 );
303 } else {
304 println!(
305 "Epoch {}: Train Loss: {:.6}, LR: {:.2e}, Time: {:.2}s{}",
306 epoch, epoch_loss, current_lr, time_elapsed, best_indicator
307 );
308 }
309 }
310
311 if should_stop {
312 let stopped_epoch = self
313 .early_stopper
314 .as_ref()
315 .unwrap()
316 .stopped_epoch()
317 .unwrap();
318 let best_score = self.early_stopper.as_ref().unwrap().best_score();
319 println!(
320 "Early stopping triggered at epoch {} (best score: {:.6})",
321 stopped_epoch, best_score
322 );
323
324 if let Some(ref early_stopper) = self.early_stopper {
326 if let Err(e) = early_stopper.restore_best_weights(&mut self.network) {
327 println!("Warning: Could not restore best weights: {}", e);
328 } else {
329 println!(
330 "Restored best weights from epoch with score {:.6}",
331 best_score
332 );
333 }
334 }
335 break;
336 }
337 }
338
339 println!("Training completed!");
340 }
341
342 pub fn evaluate(&mut self, data: &[(Vec<Array2<f64>>, Vec<Array2<f64>>)]) -> f64 {
344 self.network.eval();
345
346 let mut total_loss = 0.0;
347 let mut total_samples = 0;
348
349 for (inputs, targets) in data {
350 if inputs.len() != targets.len() {
351 continue;
352 }
353
354 let (outputs, _) = self.network.forward_sequence_with_cache(inputs);
355
356 for ((output, _), target) in outputs.iter().zip(targets.iter()) {
357 let loss = self.loss_function.compute_loss(output, target);
358 total_loss += loss;
359 total_samples += 1;
360 }
361 }
362
363 if total_samples > 0 {
364 total_loss / total_samples as f64
365 } else {
366 0.0
367 }
368 }
369
370 pub fn predict(&mut self, inputs: &[Array2<f64>]) -> Vec<Array2<f64>> {
372 self.network.eval();
373
374 let (outputs, _) = self.network.forward_sequence_with_cache(inputs);
375 outputs.into_iter().map(|(output, _)| output).collect()
376 }
377
378 pub fn get_latest_metrics(&self) -> Option<&TrainingMetrics> {
379 self.metrics_history.last()
380 }
381
382 pub fn get_metrics_history(&self) -> &[TrainingMetrics] {
383 &self.metrics_history
384 }
385
386 pub fn set_training_mode(&mut self, training: bool) {
388 if training {
389 self.network.train();
390 } else {
391 self.network.eval();
392 }
393 }
394}
395
396pub struct ScheduledLSTMTrainer<L: LossFunction, O: Optimizer, S: LearningRateScheduler> {
398 pub network: LSTMNetwork,
399 pub loss_function: L,
400 pub optimizer: ScheduledOptimizer<O, S>,
401 pub config: TrainingConfig,
402 pub metrics_history: Vec<TrainingMetrics>,
403 early_stopper: Option<EarlyStopper>,
404}
405
406impl<L: LossFunction, O: Optimizer, S: LearningRateScheduler> ScheduledLSTMTrainer<L, O, S> {
407 pub fn new(
408 network: LSTMNetwork,
409 loss_function: L,
410 optimizer: ScheduledOptimizer<O, S>,
411 ) -> Self {
412 ScheduledLSTMTrainer {
413 network,
414 loss_function,
415 optimizer,
416 config: TrainingConfig::default(),
417 metrics_history: Vec::new(),
418 early_stopper: None,
419 }
420 }
421
422 pub fn with_config(mut self, config: TrainingConfig) -> Self {
423 self.early_stopper = config
425 .early_stopping
426 .as_ref()
427 .map(|es_config| EarlyStopper::new(es_config.clone()));
428 self.config = config;
429 self
430 }
431
432 pub fn train_sequence(&mut self, inputs: &[Array2<f64>], targets: &[Array2<f64>]) -> f64 {
434 if inputs.len() != targets.len() {
435 panic!("Inputs and targets must have the same length");
436 }
437
438 self.network.train();
439
440 let (total_loss, mut total_gradients) =
441 sequence_gradients(&mut self.network, &self.loss_function, inputs, targets);
442
443 if let Some(clip_value) = self.config.clip_gradient {
444 clip_global_norm(&mut total_gradients, clip_value);
445 }
446
447 self.network
448 .update_parameters(&total_gradients, &mut self.optimizer);
449
450 total_loss / inputs.len() as f64
451 }
452
453 pub fn train(
455 &mut self,
456 train_data: &[(Vec<Array2<f64>>, Vec<Array2<f64>>)],
457 validation_data: Option<&[(Vec<Array2<f64>>, Vec<Array2<f64>>)]>,
458 ) {
459 println!(
460 "Starting training for {} epochs with {} scheduler...",
461 self.config.epochs,
462 self.optimizer.scheduler_name()
463 );
464
465 for epoch in 0..self.config.epochs {
466 let start_time = Instant::now();
467 let mut epoch_loss = 0.0;
468
469 self.network.train();
471 for (inputs, targets) in train_data {
472 let loss = self.train_sequence(inputs, targets);
473 epoch_loss += loss;
474 }
475 epoch_loss /= train_data.len() as f64;
476
477 let validation_loss = if let Some(val_data) = validation_data {
478 self.network.eval();
479 Some(self.evaluate(val_data))
480 } else {
481 None
482 };
483
484 let prev_lr = self.optimizer.get_learning_rate();
486 if let Some(val_loss) = validation_loss {
487 self.optimizer.step_with_val_loss(val_loss);
488 } else {
489 self.optimizer.step();
490 }
491 let new_lr = self.optimizer.get_learning_rate();
492
493 if self.config.log_lr_changes && (new_lr - prev_lr).abs() > 1e-10 {
495 println!(
496 "Learning rate changed from {:.2e} to {:.2e}",
497 prev_lr, new_lr
498 );
499 }
500
501 let time_elapsed = start_time.elapsed().as_secs_f64();
502
503 let metrics = TrainingMetrics {
504 epoch,
505 train_loss: epoch_loss,
506 validation_loss,
507 time_elapsed,
508 learning_rate: new_lr,
509 };
510
511 self.metrics_history.push(metrics.clone());
512
513 let mut should_stop = false;
515 let mut is_best = false;
516 if let Some(ref mut early_stopper) = self.early_stopper {
517 let (stop, best) = early_stopper.should_stop(&metrics, &self.network);
518 should_stop = stop;
519 is_best = best;
520 }
521
522 if epoch % self.config.print_every == 0 {
523 let best_indicator = if is_best { " *" } else { "" };
524 if let Some(val_loss) = validation_loss {
525 println!(
526 "Epoch {}: Train Loss: {:.6}, Val Loss: {:.6}, LR: {:.2e}, Time: {:.2}s{}",
527 epoch, epoch_loss, val_loss, new_lr, time_elapsed, best_indicator
528 );
529 } else {
530 println!(
531 "Epoch {}: Train Loss: {:.6}, LR: {:.2e}, Time: {:.2}s{}",
532 epoch, epoch_loss, new_lr, time_elapsed, best_indicator
533 );
534 }
535 }
536
537 if should_stop {
538 let stopped_epoch = self
539 .early_stopper
540 .as_ref()
541 .unwrap()
542 .stopped_epoch()
543 .unwrap();
544 let best_score = self.early_stopper.as_ref().unwrap().best_score();
545 println!(
546 "Early stopping triggered at epoch {} (best score: {:.6})",
547 stopped_epoch, best_score
548 );
549
550 if let Some(ref early_stopper) = self.early_stopper {
552 if let Err(e) = early_stopper.restore_best_weights(&mut self.network) {
553 println!("Warning: Could not restore best weights: {}", e);
554 } else {
555 println!(
556 "Restored best weights from epoch with score {:.6}",
557 best_score
558 );
559 }
560 }
561 break;
562 }
563 }
564
565 println!("Training completed!");
566 }
567
568 pub fn evaluate(&mut self, data: &[(Vec<Array2<f64>>, Vec<Array2<f64>>)]) -> f64 {
570 self.network.eval();
571
572 let mut total_loss = 0.0;
573 let mut total_samples = 0;
574
575 for (inputs, targets) in data {
576 if inputs.len() != targets.len() {
577 continue;
578 }
579
580 let (outputs, _) = self.network.forward_sequence_with_cache(inputs);
581
582 for ((output, _), target) in outputs.iter().zip(targets.iter()) {
583 let loss = self.loss_function.compute_loss(output, target);
584 total_loss += loss;
585 total_samples += 1;
586 }
587 }
588
589 if total_samples > 0 {
590 total_loss / total_samples as f64
591 } else {
592 0.0
593 }
594 }
595
596 pub fn predict(&mut self, inputs: &[Array2<f64>]) -> Vec<Array2<f64>> {
598 self.network.eval();
599
600 let (outputs, _) = self.network.forward_sequence_with_cache(inputs);
601 outputs.into_iter().map(|(output, _)| output).collect()
602 }
603
604 pub fn get_latest_metrics(&self) -> Option<&TrainingMetrics> {
605 self.metrics_history.last()
606 }
607
608 pub fn get_metrics_history(&self) -> &[TrainingMetrics] {
609 &self.metrics_history
610 }
611
612 pub fn set_training_mode(&mut self, training: bool) {
614 if training {
615 self.network.train();
616 } else {
617 self.network.eval();
618 }
619 }
620
621 pub fn get_current_lr(&self) -> f64 {
623 self.optimizer.get_current_lr()
624 }
625
626 pub fn get_current_epoch(&self) -> usize {
628 self.optimizer.get_current_epoch()
629 }
630
631 pub fn reset_optimizer(&mut self) {
633 self.optimizer.reset();
634 }
635}
636
637pub struct LSTMBatchTrainer<L: LossFunction, O: Optimizer> {
640 pub network: LSTMNetwork,
641 pub loss_function: L,
642 pub optimizer: O,
643 pub config: TrainingConfig,
644 pub metrics_history: Vec<TrainingMetrics>,
645 early_stopper: Option<EarlyStopper>,
646}
647
648impl<L: LossFunction, O: Optimizer> LSTMBatchTrainer<L, O> {
649 pub fn new(network: LSTMNetwork, loss_function: L, optimizer: O) -> Self {
650 LSTMBatchTrainer {
651 network,
652 loss_function,
653 optimizer,
654 config: TrainingConfig::default(),
655 metrics_history: Vec::new(),
656 early_stopper: None,
657 }
658 }
659
660 pub fn with_config(mut self, config: TrainingConfig) -> Self {
661 self.early_stopper = config
663 .early_stopping
664 .as_ref()
665 .map(|es_config| EarlyStopper::new(es_config.clone()));
666 self.config = config;
667 self
668 }
669
670 pub fn train_batch(
679 &mut self,
680 batch_inputs: &[Vec<Array2<f64>>],
681 batch_targets: &[Vec<Array2<f64>>],
682 ) -> f64 {
683 assert_eq!(
684 batch_inputs.len(),
685 batch_targets.len(),
686 "Batch inputs and targets must have same length"
687 );
688
689 if batch_inputs.is_empty() {
690 return 0.0;
691 }
692
693 self.network.train();
694
695 let batch_size = batch_inputs.len();
696 let hidden_size = self.network.hidden_size;
697 let padded_inputs = pad_batch(batch_inputs, self.network.input_size);
698 let (outputs, caches) = self.network.forward_sequence_with_cache(&padded_inputs);
699
700 let mut total_loss = 0.0;
701 let mut valid_steps = 0;
702 let mut d_outputs = Vec::with_capacity(outputs.len());
703 for (t, (prediction, _)) in outputs.iter().enumerate() {
704 let active: Vec<usize> = (0..batch_size)
705 .filter(|&b| t < batch_inputs[b].len() && t < batch_targets[b].len())
706 .collect();
707 let mut d_output = Array2::zeros((hidden_size, batch_size));
708 if !active.is_empty() {
709 let mut preds = Array2::zeros((hidden_size, active.len()));
710 let mut targs = Array2::zeros((hidden_size, active.len()));
711 for (i, &b) in active.iter().enumerate() {
712 preds.column_mut(i).assign(&prediction.column(b));
713 targs.column_mut(i).assign(&batch_targets[b][t].column(0));
714 }
715 total_loss += self.loss_function.compute_batch_loss(&preds, &targs);
716 valid_steps += 1;
717 let grad = self.loss_function.compute_batch_gradient(&preds, &targs);
718 for (i, &b) in active.iter().enumerate() {
719 d_output.column_mut(b).assign(&grad.column(i));
720 }
721 }
722 d_outputs.push(d_output);
723 }
724
725 let (mut total_gradients, _) = self.network.backward_sequence(&d_outputs, &caches);
726
727 if let Some(clip_value) = self.config.clip_gradient {
728 clip_global_norm(&mut total_gradients, clip_value);
729 }
730
731 self.network
733 .update_parameters(&total_gradients, &mut self.optimizer);
734
735 if valid_steps > 0 {
736 total_loss / valid_steps as f64
737 } else {
738 0.0
739 }
740 }
741
742 pub fn train(
749 &mut self,
750 train_data: &[(Vec<Array2<f64>>, Vec<Array2<f64>>)],
751 validation_data: Option<&[(Vec<Array2<f64>>, Vec<Array2<f64>>)]>,
752 batch_size: usize,
753 ) {
754 println!(
755 "Starting batch training for {} epochs with batch size {}...",
756 self.config.epochs, batch_size
757 );
758
759 for epoch in 0..self.config.epochs {
760 let start_time = Instant::now();
761 let mut epoch_loss = 0.0;
762 let mut num_batches = 0;
763
764 for batch_start in (0..train_data.len()).step_by(batch_size) {
766 let batch_end = (batch_start + batch_size).min(train_data.len());
767 let batch = &train_data[batch_start..batch_end];
768
769 let batch_inputs: Vec<_> = batch.iter().map(|(inputs, _)| inputs.clone()).collect();
770 let batch_targets: Vec<_> =
771 batch.iter().map(|(_, targets)| targets.clone()).collect();
772
773 let batch_loss = self.train_batch(&batch_inputs, &batch_targets);
774 epoch_loss += batch_loss;
775 num_batches += 1;
776 }
777
778 epoch_loss /= num_batches as f64;
779
780 let validation_loss = if let Some(val_data) = validation_data {
782 self.network.eval();
783 Some(self.evaluate_batch(val_data, batch_size))
784 } else {
785 None
786 };
787
788 let time_elapsed = start_time.elapsed().as_secs_f64();
789 let current_lr = self.optimizer.get_learning_rate();
790
791 let metrics = TrainingMetrics {
792 epoch,
793 train_loss: epoch_loss,
794 validation_loss,
795 time_elapsed,
796 learning_rate: current_lr,
797 };
798
799 self.metrics_history.push(metrics.clone());
800
801 let mut should_stop = false;
803 let mut is_best = false;
804 if let Some(ref mut early_stopper) = self.early_stopper {
805 let (stop, best) = early_stopper.should_stop(&metrics, &self.network);
806 should_stop = stop;
807 is_best = best;
808 }
809
810 if epoch % self.config.print_every == 0 {
811 let best_indicator = if is_best { " *" } else { "" };
812 if let Some(val_loss) = validation_loss {
813 println!("Epoch {}: Train Loss: {:.6}, Val Loss: {:.6}, LR: {:.2e}, Time: {:.2}s, Batches: {}{}",
814 epoch, epoch_loss, val_loss, current_lr, time_elapsed, num_batches, best_indicator);
815 } else {
816 println!(
817 "Epoch {}: Train Loss: {:.6}, LR: {:.2e}, Time: {:.2}s, Batches: {}{}",
818 epoch, epoch_loss, current_lr, time_elapsed, num_batches, best_indicator
819 );
820 }
821 }
822
823 if should_stop {
824 let stopped_epoch = self
825 .early_stopper
826 .as_ref()
827 .unwrap()
828 .stopped_epoch()
829 .unwrap();
830 let best_score = self.early_stopper.as_ref().unwrap().best_score();
831 println!(
832 "Early stopping triggered at epoch {} (best score: {:.6})",
833 stopped_epoch, best_score
834 );
835
836 if let Some(ref early_stopper) = self.early_stopper {
838 if let Err(e) = early_stopper.restore_best_weights(&mut self.network) {
839 println!("Warning: Could not restore best weights: {}", e);
840 } else {
841 println!(
842 "Restored best weights from epoch with score {:.6}",
843 best_score
844 );
845 }
846 }
847 break;
848 }
849 }
850
851 println!("Batch training completed!");
852 }
853
854 pub fn evaluate_batch(
856 &mut self,
857 data: &[(Vec<Array2<f64>>, Vec<Array2<f64>>)],
858 batch_size: usize,
859 ) -> f64 {
860 self.network.eval();
861
862 let mut total_loss = 0.0;
863 let mut num_batches = 0;
864
865 for batch_start in (0..data.len()).step_by(batch_size) {
866 let batch_end = (batch_start + batch_size).min(data.len());
867 let batch = &data[batch_start..batch_end];
868
869 let batch_inputs: Vec<_> = batch.iter().map(|(inputs, _)| inputs.clone()).collect();
870 let batch_targets: Vec<_> = batch.iter().map(|(_, targets)| targets.clone()).collect();
871
872 let batch_outputs = self.network.forward_batch_sequences(&batch_inputs);
874
875 let mut batch_loss = 0.0;
876 let mut valid_samples = 0;
877
878 for (outputs, targets) in batch_outputs.iter().zip(batch_targets.iter()) {
879 for ((output, _), target) in outputs.iter().zip(targets.iter()) {
880 let loss = self.loss_function.compute_loss(output, target);
881 batch_loss += loss;
882 valid_samples += 1;
883 }
884 }
885
886 if valid_samples > 0 {
887 total_loss += batch_loss / valid_samples as f64;
888 num_batches += 1;
889 }
890 }
891
892 if num_batches > 0 {
893 total_loss / num_batches as f64
894 } else {
895 0.0
896 }
897 }
898
899 pub fn predict_batch(&mut self, inputs: &[Vec<Array2<f64>>]) -> Vec<Vec<Array2<f64>>> {
901 self.network.eval();
902
903 let batch_outputs = self.network.forward_batch_sequences(inputs);
904 batch_outputs
905 .into_iter()
906 .map(|sequence_outputs| {
907 sequence_outputs
908 .into_iter()
909 .map(|(output, _)| output)
910 .collect()
911 })
912 .collect()
913 }
914
915 pub fn get_latest_metrics(&self) -> Option<&TrainingMetrics> {
916 self.metrics_history.last()
917 }
918
919 pub fn get_metrics_history(&self) -> &[TrainingMetrics] {
920 &self.metrics_history
921 }
922
923 pub fn set_training_mode(&mut self, training: bool) {
924 if training {
925 self.network.train();
926 } else {
927 self.network.eval();
928 }
929 }
930}
931
932pub fn create_basic_trainer(network: LSTMNetwork, learning_rate: f64) -> LSTMTrainer<MSELoss, SGD> {
934 let loss_function = MSELoss;
935 let optimizer = SGD::new(learning_rate);
936 LSTMTrainer::new(network, loss_function, optimizer)
937}
938
939pub fn create_step_lr_trainer(
941 network: LSTMNetwork,
942 learning_rate: f64,
943 step_size: usize,
944 gamma: f64,
945) -> ScheduledLSTMTrainer<MSELoss, SGD, crate::schedulers::StepLR> {
946 let loss_function = MSELoss;
947 let optimizer =
948 ScheduledOptimizer::step_lr(SGD::new(learning_rate), learning_rate, step_size, gamma);
949 ScheduledLSTMTrainer::new(network, loss_function, optimizer)
950}
951
952pub fn create_one_cycle_trainer(
954 network: LSTMNetwork,
955 max_lr: f64,
956 total_steps: usize,
957) -> ScheduledLSTMTrainer<MSELoss, crate::optimizers::Adam, crate::schedulers::OneCycleLR> {
958 let loss_function = MSELoss;
959 let optimizer =
960 ScheduledOptimizer::one_cycle(crate::optimizers::Adam::new(max_lr), max_lr, total_steps);
961 ScheduledLSTMTrainer::new(network, loss_function, optimizer)
962}
963
964pub fn create_cosine_annealing_trainer(
966 network: LSTMNetwork,
967 learning_rate: f64,
968 t_max: usize,
969 eta_min: f64,
970) -> ScheduledLSTMTrainer<MSELoss, crate::optimizers::Adam, crate::schedulers::CosineAnnealingLR> {
971 let loss_function = MSELoss;
972 let optimizer = crate::optimizers::Adam::new(learning_rate);
973 let scheduler = crate::schedulers::CosineAnnealingLR::new(t_max, eta_min);
974 let scheduled_optimizer =
975 crate::optimizers::ScheduledOptimizer::new(optimizer, scheduler, learning_rate);
976
977 ScheduledLSTMTrainer::new(network, loss_function, scheduled_optimizer)
978}
979
980pub fn create_basic_batch_trainer(
982 network: LSTMNetwork,
983 learning_rate: f64,
984) -> LSTMBatchTrainer<MSELoss, SGD> {
985 let loss_function = MSELoss;
986 let optimizer = SGD::new(learning_rate);
987 LSTMBatchTrainer::new(network, loss_function, optimizer)
988}
989
990pub fn create_adam_batch_trainer(
992 network: LSTMNetwork,
993 learning_rate: f64,
994) -> LSTMBatchTrainer<MSELoss, crate::optimizers::Adam> {
995 let loss_function = MSELoss;
996 let optimizer = crate::optimizers::Adam::new(learning_rate);
997 LSTMBatchTrainer::new(network, loss_function, optimizer)
998}
999
1000#[cfg(test)]
1001mod tests {
1002 use super::*;
1003 use ndarray::arr2;
1004
1005 #[test]
1006 fn test_trainer_creation() {
1007 let network = LSTMNetwork::new(2, 3, 1);
1008 let trainer = create_basic_trainer(network, 0.01);
1009
1010 assert_eq!(trainer.network.input_size, 2);
1011 assert_eq!(trainer.network.hidden_size, 3);
1012 assert_eq!(trainer.network.num_layers, 1);
1013 }
1014
1015 #[test]
1016 fn test_sequence_training() {
1017 let network = LSTMNetwork::new(2, 3, 1);
1018 let mut trainer = create_basic_trainer(network, 0.01);
1019
1020 let inputs = vec![arr2(&[[1.0], [0.0]]), arr2(&[[0.0], [1.0]])];
1021 let targets = vec![arr2(&[[1.0], [0.0], [0.0]]), arr2(&[[0.0], [1.0], [0.0]])];
1022
1023 let loss = trainer.train_sequence(&inputs, &targets);
1024 assert!(loss >= 0.0);
1025 }
1026}