Skip to main content

trustformers_optim/
tensorflow_compat.rs

1//! TensorFlow Optimizer API Compatibility Layer
2//!
3//! This module provides TensorFlow-compatible optimizer interfaces for seamless
4//! integration with TensorFlow-based training workflows. It wraps our native
5//! optimizers to provide the familiar TensorFlow API while maintaining high performance.
6
7use crate::{Adam, AdamW};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::{Arc, Mutex};
11use trustformers_core::errors::{Result, TrustformersError};
12use trustformers_core::traits::Optimizer;
13use trustformers_core::Tensor;
14
15/// TensorFlow-compatible optimizer configuration
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct TensorFlowOptimizerConfig {
18    pub optimizer_type: String,
19    pub learning_rate: f64,
20    pub beta_1: Option<f64>,
21    pub beta_2: Option<f64>,
22    pub epsilon: Option<f64>,
23    pub weight_decay: Option<f64>,
24    pub clipnorm: Option<f64>,
25    pub clipvalue: Option<f64>,
26    pub global_clipnorm: Option<f64>,
27    pub use_ema: Option<bool>,
28    pub ema_momentum: Option<f64>,
29    pub ema_overwrite_frequency: Option<i32>,
30    pub jit_compile: Option<bool>,
31    pub name: Option<String>,
32    pub parameters: HashMap<String, serde_json::Value>,
33}
34
35impl Default for TensorFlowOptimizerConfig {
36    fn default() -> Self {
37        Self {
38            optimizer_type: "Adam".to_string(),
39            learning_rate: 0.001,
40            beta_1: Some(0.9),
41            beta_2: Some(0.999),
42            epsilon: Some(1e-7),
43            weight_decay: None,
44            clipnorm: None,
45            clipvalue: None,
46            global_clipnorm: None,
47            use_ema: Some(false),
48            ema_momentum: Some(0.99),
49            ema_overwrite_frequency: None,
50            jit_compile: Some(true),
51            name: None,
52            parameters: HashMap::new(),
53        }
54    }
55}
56
57/// TensorFlow-compatible learning rate schedule
58pub trait TensorFlowLearningRateSchedule: Send + Sync {
59    /// Get learning rate at current step
60    fn get_lr(&self, step: i64) -> f64;
61
62    /// Get configuration
63    fn get_config(&self) -> serde_json::Value;
64}
65
66/// TensorFlow-compatible exponential decay schedule
67#[derive(Debug, Clone)]
68pub struct TensorFlowExponentialDecay {
69    initial_learning_rate: f64,
70    decay_steps: i64,
71    decay_rate: f64,
72    staircase: bool,
73}
74
75impl TensorFlowExponentialDecay {
76    pub fn new(
77        initial_learning_rate: f64,
78        decay_steps: i64,
79        decay_rate: f64,
80        staircase: bool,
81    ) -> Self {
82        Self {
83            initial_learning_rate,
84            decay_steps,
85            decay_rate,
86            staircase,
87        }
88    }
89}
90
91impl TensorFlowLearningRateSchedule for TensorFlowExponentialDecay {
92    fn get_lr(&self, step: i64) -> f64 {
93        let decay_factor = if self.staircase {
94            (step / self.decay_steps) as f64
95        } else {
96            step as f64 / self.decay_steps as f64
97        };
98
99        self.initial_learning_rate * self.decay_rate.powf(decay_factor)
100    }
101
102    fn get_config(&self) -> serde_json::Value {
103        serde_json::json!({
104            "initial_learning_rate": self.initial_learning_rate,
105            "decay_steps": self.decay_steps,
106            "decay_rate": self.decay_rate,
107            "staircase": self.staircase,
108        })
109    }
110}
111
112/// TensorFlow-compatible cosine decay schedule
113#[derive(Debug, Clone)]
114pub struct TensorFlowCosineDecay {
115    initial_learning_rate: f64,
116    decay_steps: i64,
117    alpha: f64,
118}
119
120impl TensorFlowCosineDecay {
121    pub fn new(initial_learning_rate: f64, decay_steps: i64, alpha: f64) -> Self {
122        Self {
123            initial_learning_rate,
124            decay_steps,
125            alpha,
126        }
127    }
128}
129
130impl TensorFlowLearningRateSchedule for TensorFlowCosineDecay {
131    fn get_lr(&self, step: i64) -> f64 {
132        let completed_fraction = (step.min(self.decay_steps) as f64) / (self.decay_steps as f64);
133        let cosine_decayed = 0.5 * (1.0 + (std::f64::consts::PI * completed_fraction).cos());
134        let decayed = (1.0 - self.alpha) * cosine_decayed + self.alpha;
135
136        self.initial_learning_rate * decayed
137    }
138
139    fn get_config(&self) -> serde_json::Value {
140        serde_json::json!({
141            "initial_learning_rate": self.initial_learning_rate,
142            "decay_steps": self.decay_steps,
143            "alpha": self.alpha,
144        })
145    }
146}
147
148/// TensorFlow-compatible optimizer interface
149pub trait TensorFlowOptimizer: Send + Sync {
150    /// Apply gradients to variables
151    fn apply_gradients(
152        &mut self,
153        grads_and_vars: &[(Tensor, String)],
154        global_step: Option<i64>,
155    ) -> Result<()>;
156
157    /// Minimize loss function
158    fn minimize(
159        &mut self,
160        loss_fn: Box<dyn Fn() -> Result<Tensor>>,
161        var_list: &[String],
162        global_step: Option<i64>,
163    ) -> Result<Tensor>;
164
165    /// Get optimizer configuration
166    fn get_config(&self) -> TensorFlowOptimizerConfig;
167
168    /// Get optimizer variables (state)
169    fn variables(&self) -> Vec<String>;
170
171    /// Get optimizer weights
172    fn get_weights(&self) -> Vec<Tensor>;
173
174    /// Set optimizer weights
175    fn set_weights(&mut self, weights: Vec<Tensor>) -> Result<()>;
176
177    /// Get learning rate
178    fn get_learning_rate(&self) -> f64;
179
180    /// Set learning rate
181    fn set_learning_rate(&mut self, lr: f64) -> Result<()>;
182
183    /// Get optimizer name
184    fn get_name(&self) -> &str;
185}
186
187/// TensorFlow-compatible Adam optimizer
188pub struct TensorFlowAdam {
189    inner: Adam,
190    config: TensorFlowOptimizerConfig,
191    variables: Arc<Mutex<HashMap<String, Tensor>>>,
192    lr_schedule: Option<Box<dyn TensorFlowLearningRateSchedule>>,
193    global_step: i64,
194}
195
196impl TensorFlowAdam {
197    /// Create new TensorFlow-compatible Adam optimizer
198    pub fn new(
199        learning_rate: f64,
200        beta_1: f64,
201        beta_2: f64,
202        epsilon: f64,
203        weight_decay: Option<f64>,
204        clipnorm: Option<f64>,
205        clipvalue: Option<f64>,
206        global_clipnorm: Option<f64>,
207        use_ema: bool,
208        ema_momentum: f64,
209        jit_compile: bool,
210        name: Option<String>,
211    ) -> Result<Self> {
212        let config = TensorFlowOptimizerConfig {
213            optimizer_type: "Adam".to_string(),
214            learning_rate,
215            beta_1: Some(beta_1),
216            beta_2: Some(beta_2),
217            epsilon: Some(epsilon),
218            weight_decay,
219            clipnorm,
220            clipvalue,
221            global_clipnorm,
222            use_ema: Some(use_ema),
223            ema_momentum: Some(ema_momentum),
224            ema_overwrite_frequency: None,
225            jit_compile: Some(jit_compile),
226            name,
227            parameters: HashMap::new(),
228        };
229
230        // optimizer_config is redundant - using config above
231
232        let inner = Adam::new(
233            learning_rate as f32,
234            (beta_1 as f32, beta_2 as f32),
235            epsilon as f32,
236            weight_decay.unwrap_or(0.0) as f32,
237        );
238
239        Ok(Self {
240            inner,
241            config,
242            variables: Arc::new(Mutex::new(HashMap::new())),
243            lr_schedule: None,
244            global_step: 0,
245        })
246    }
247
248    /// Create with default parameters
249    pub fn with_defaults() -> Result<Self> {
250        Self::new(
251            0.001,
252            0.9,
253            0.999,
254            1e-7,
255            None,
256            None,
257            None,
258            None,
259            false,
260            0.99,
261            true,
262            Some("Adam".to_string()),
263        )
264    }
265
266    /// Create TensorFlow Adam optimizer from configuration
267    pub fn from_config(config: TensorFlowOptimizerConfig) -> Result<Self> {
268        Self::new(
269            config.learning_rate,
270            config.beta_1.unwrap_or(0.9),
271            config.beta_2.unwrap_or(0.999),
272            config.epsilon.unwrap_or(1e-7),
273            config.weight_decay,
274            config.clipnorm,
275            config.clipvalue,
276            config.global_clipnorm,
277            config.use_ema.unwrap_or(false),
278            config.ema_momentum.unwrap_or(0.99),
279            config.jit_compile.unwrap_or(true),
280            config.name,
281        )
282    }
283
284    /// Create with learning rate schedule
285    pub fn with_schedule(
286        schedule: Box<dyn TensorFlowLearningRateSchedule>,
287        beta_1: f64,
288        beta_2: f64,
289        epsilon: f64,
290        weight_decay: Option<f64>,
291        clipnorm: Option<f64>,
292        clipvalue: Option<f64>,
293        global_clipnorm: Option<f64>,
294        use_ema: bool,
295        ema_momentum: f64,
296        jit_compile: bool,
297        name: Option<String>,
298    ) -> Result<Self> {
299        let mut optimizer = Self::new(
300            schedule.get_lr(0),
301            beta_1,
302            beta_2,
303            epsilon,
304            weight_decay,
305            clipnorm,
306            clipvalue,
307            global_clipnorm,
308            use_ema,
309            ema_momentum,
310            jit_compile,
311            name,
312        )?;
313
314        optimizer.lr_schedule = Some(schedule);
315        Ok(optimizer)
316    }
317
318    /// Add variable to optimizer
319    pub fn add_variable(&mut self, name: String, var: Tensor) -> Result<()> {
320        let mut variables = self.variables.lock().map_err(|_| {
321            TrustformersError::lock_error(
322                "tensorflow optimizer variables mutex poisoned".to_string(),
323            )
324        })?;
325        variables.insert(name, var);
326        Ok(())
327    }
328
329    /// Update learning rate based on schedule
330    fn update_learning_rate(&mut self) -> Result<()> {
331        if let Some(ref schedule) = self.lr_schedule {
332            let new_lr = schedule.get_lr(self.global_step);
333            self.config.learning_rate = new_lr;
334
335            // Update inner optimizer learning rate
336            self.inner.set_lr(new_lr as f32);
337        }
338        Ok(())
339    }
340
341    /// Apply gradient clipping in place.
342    ///
343    /// Mirrors Keras semantics: `clipnorm` rescales each gradient whose own L2 norm
344    /// exceeds the threshold, `clipvalue` clamps every element, and `global_clipnorm`
345    /// rescales all gradients by one factor derived from the global L2 norm.
346    fn clip_gradients(&self, gradients: &mut [Tensor]) -> Result<()> {
347        if let Some(clipnorm) = self.config.clipnorm {
348            // Clip by norm (per-gradient)
349            for grad in gradients.iter_mut() {
350                let norm = grad.norm()?;
351                if norm > clipnorm as f32 && norm > 0.0 {
352                    *grad = grad.mul_scalar((clipnorm as f32) / norm)?;
353                }
354            }
355        }
356
357        if let Some(clipvalue) = self.config.clipvalue {
358            // Clip by value (element-wise)
359            for grad in gradients.iter_mut() {
360                *grad = grad.clamp(-clipvalue as f32, clipvalue as f32)?;
361            }
362        }
363
364        if let Some(global_clipnorm) = self.config.global_clipnorm {
365            // Global gradient clipping: a tensor error must not silently contribute 0.
366            let mut sum_sq = 0.0_f64;
367            for grad in gradients.iter() {
368                let norm = grad.norm()? as f64;
369                sum_sq += norm * norm;
370            }
371            let global_norm = sum_sq.sqrt();
372
373            if global_norm > global_clipnorm && global_norm > 0.0 {
374                let scale = global_clipnorm / global_norm;
375                for grad in gradients.iter_mut() {
376                    *grad = grad.mul_scalar(scale as f32)?;
377                }
378            }
379        }
380
381        Ok(())
382    }
383}
384
385impl TensorFlowOptimizer for TensorFlowAdam {
386    fn apply_gradients(
387        &mut self,
388        grads_and_vars: &[(Tensor, String)],
389        global_step: Option<i64>,
390    ) -> Result<()> {
391        if let Some(step) = global_step {
392            self.global_step = step;
393        } else {
394            self.global_step += 1;
395        }
396
397        // Update learning rate if schedule is set
398        self.update_learning_rate()?;
399
400        let mut gradients: Vec<Tensor> = grads_and_vars.iter().map(|(g, _)| g.clone()).collect();
401
402        // Apply gradient clipping
403        self.clip_gradients(&mut gradients)?;
404
405        // Apply gradients using inner optimizer
406        let mut variables = self.variables.lock().map_err(|_| {
407            TrustformersError::lock_error(
408                "tensorflow optimizer variables mutex poisoned".to_string(),
409            )
410        })?;
411        // Use the *clipped* gradients, not the caller's originals.
412        for (clipped_grad, (_, var_name)) in gradients.iter().zip(grads_and_vars.iter()) {
413            if let Some(var) = variables.get_mut(var_name) {
414                self.inner.update_named(var_name, var, clipped_grad)?;
415            }
416        }
417        self.inner.step();
418
419        Ok(())
420    }
421
422    fn minimize(
423        &mut self,
424        loss_fn: Box<dyn Fn() -> Result<Tensor>>,
425        var_list: &[String],
426        global_step: Option<i64>,
427    ) -> Result<Tensor> {
428        let loss = loss_fn()?;
429
430        // Compute gradients (this would normally be done by automatic differentiation)
431        let mut grads_and_vars = Vec::new();
432        {
433            let mut variables = self.variables.lock().map_err(|_| {
434                TrustformersError::lock_error(
435                    "tensorflow optimizer variables mutex poisoned".to_string(),
436                )
437            })?;
438
439            for var_name in var_list {
440                if let Some(var) = variables.get_mut(var_name) {
441                    // Compute numerical gradient using finite differences
442                    let grad = self.compute_numerical_gradient(loss_fn.as_ref(), var, var_name)?;
443                    grads_and_vars.push((grad, var_name.clone()));
444                }
445            }
446        } // variables lock is dropped here
447
448        self.apply_gradients(&grads_and_vars, global_step)?;
449        Ok(loss)
450    }
451
452    fn get_config(&self) -> TensorFlowOptimizerConfig {
453        self.config.clone()
454    }
455
456    fn variables(&self) -> Vec<String> {
457        let variables = self.variables.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
458        variables.keys().cloned().collect()
459    }
460
461    fn get_weights(&self) -> Vec<Tensor> {
462        let variables = self.variables.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
463        variables.values().cloned().collect()
464    }
465
466    fn set_weights(&mut self, weights: Vec<Tensor>) -> Result<()> {
467        let mut variables = self.variables.lock().map_err(|_| {
468            TrustformersError::lock_error(
469                "tensorflow optimizer variables mutex poisoned".to_string(),
470            )
471        })?;
472        let var_names: Vec<String> = variables.keys().cloned().collect();
473
474        if weights.len() != var_names.len() {
475            return Err(TrustformersError::invalid_argument(
476                "Number of weights must match number of variables".to_string(),
477            ));
478        }
479
480        for (weight, var_name) in weights.into_iter().zip(var_names) {
481            variables.insert(var_name, weight);
482        }
483
484        Ok(())
485    }
486
487    fn get_learning_rate(&self) -> f64 {
488        self.config.learning_rate
489    }
490
491    fn set_learning_rate(&mut self, lr: f64) -> Result<()> {
492        self.config.learning_rate = lr;
493
494        // Update inner optimizer
495        self.inner.set_lr(lr as f32);
496
497        Ok(())
498    }
499
500    fn get_name(&self) -> &str {
501        self.config.name.as_deref().unwrap_or("Adam")
502    }
503}
504
505impl TensorFlowAdam {
506    /// Compute numerical gradient using finite differences
507    fn compute_numerical_gradient(
508        &self,
509        loss_fn: &dyn Fn() -> Result<Tensor>,
510        var: &mut Tensor,
511        _var_name: &str,
512    ) -> Result<Tensor> {
513        const EPSILON: f32 = 1e-4;
514
515        let original_loss = loss_fn()?;
516
517        // Compute gradient for each element using finite differences
518        let var_data = var.data()?;
519        let mut grad_data = vec![0.0; var_data.len()];
520
521        for i in 0..var_data.len() {
522            // Forward difference: f(x + h) - f(x) / h
523            let mut var_plus = var_data.clone();
524            var_plus[i] += EPSILON;
525            *var = Tensor::from_vec(var_plus, &var.shape())?;
526
527            let loss_plus = loss_fn()?;
528            let loss_plus_scalar = loss_plus.data()?[0];
529            let original_loss_scalar = original_loss.data()?[0];
530
531            grad_data[i] = (loss_plus_scalar - original_loss_scalar) / EPSILON;
532
533            // Restore original value
534            let var_original = var_data.clone();
535            *var = Tensor::from_vec(var_original, &var.shape())?;
536        }
537
538        let grad = Tensor::from_vec(grad_data, &var.shape())?;
539        Ok(grad)
540    }
541}
542
543/// TensorFlow-compatible AdamW optimizer
544pub struct TensorFlowAdamW {
545    inner: AdamW,
546    config: TensorFlowOptimizerConfig,
547    variables: Arc<Mutex<HashMap<String, Tensor>>>,
548    lr_schedule: Option<Box<dyn TensorFlowLearningRateSchedule>>,
549    global_step: i64,
550}
551
552impl TensorFlowAdamW {
553    /// Create new TensorFlow-compatible AdamW optimizer
554    pub fn new(
555        learning_rate: f64,
556        beta_1: f64,
557        beta_2: f64,
558        epsilon: f64,
559        weight_decay: f64,
560        clipnorm: Option<f64>,
561        clipvalue: Option<f64>,
562        global_clipnorm: Option<f64>,
563        use_ema: bool,
564        ema_momentum: f64,
565        jit_compile: bool,
566        name: Option<String>,
567    ) -> Result<Self> {
568        let config = TensorFlowOptimizerConfig {
569            optimizer_type: "AdamW".to_string(),
570            learning_rate,
571            beta_1: Some(beta_1),
572            beta_2: Some(beta_2),
573            epsilon: Some(epsilon),
574            weight_decay: Some(weight_decay),
575            clipnorm,
576            clipvalue,
577            global_clipnorm,
578            use_ema: Some(use_ema),
579            ema_momentum: Some(ema_momentum),
580            ema_overwrite_frequency: None,
581            jit_compile: Some(jit_compile),
582            name,
583            parameters: HashMap::new(),
584        };
585
586        let _optimizer_config = TensorFlowOptimizerConfig {
587            learning_rate,
588            beta_1: Some(beta_1),
589            beta_2: Some(beta_2),
590            epsilon: Some(epsilon),
591            weight_decay: Some(weight_decay),
592            ..Default::default()
593        };
594
595        let inner = AdamW::new(
596            learning_rate as f32,
597            (beta_1 as f32, beta_2 as f32),
598            epsilon as f32,
599            weight_decay as f32,
600        );
601
602        Ok(Self {
603            inner,
604            config,
605            variables: Arc::new(Mutex::new(HashMap::new())),
606            lr_schedule: None,
607            global_step: 0,
608        })
609    }
610
611    /// Create with default parameters
612    pub fn with_defaults() -> Result<Self> {
613        Self::new(
614            0.001,
615            0.9,
616            0.999,
617            1e-7,
618            0.01,
619            None,
620            None,
621            None,
622            false,
623            0.99,
624            true,
625            Some("AdamW".to_string()),
626        )
627    }
628
629    /// Create with learning rate schedule
630    pub fn with_schedule(
631        schedule: Box<dyn TensorFlowLearningRateSchedule>,
632        beta_1: f64,
633        beta_2: f64,
634        epsilon: f64,
635        weight_decay: f64,
636        clipnorm: Option<f64>,
637        clipvalue: Option<f64>,
638        global_clipnorm: Option<f64>,
639        use_ema: bool,
640        ema_momentum: f64,
641        jit_compile: bool,
642        name: Option<String>,
643    ) -> Result<Self> {
644        let mut optimizer = Self::new(
645            schedule.get_lr(0),
646            beta_1,
647            beta_2,
648            epsilon,
649            weight_decay,
650            clipnorm,
651            clipvalue,
652            global_clipnorm,
653            use_ema,
654            ema_momentum,
655            jit_compile,
656            name,
657        )?;
658
659        optimizer.lr_schedule = Some(schedule);
660        Ok(optimizer)
661    }
662
663    /// Add variable to optimizer
664    pub fn add_variable(&mut self, name: String, var: Tensor) -> Result<()> {
665        let mut variables = self.variables.lock().map_err(|_| {
666            TrustformersError::lock_error(
667                "tensorflow optimizer variables mutex poisoned".to_string(),
668            )
669        })?;
670        variables.insert(name, var);
671        Ok(())
672    }
673
674    /// Update learning rate based on schedule
675    fn update_learning_rate(&mut self) -> Result<()> {
676        if let Some(ref schedule) = self.lr_schedule {
677            let new_lr = schedule.get_lr(self.global_step);
678            self.config.learning_rate = new_lr;
679
680            // Update inner optimizer learning rate
681            self.inner.set_lr(new_lr as f32);
682        }
683        Ok(())
684    }
685
686    /// Apply gradient clipping in place.
687    ///
688    /// Mirrors Keras semantics: `clipnorm` rescales each gradient whose own L2 norm
689    /// exceeds the threshold, `clipvalue` clamps every element, and `global_clipnorm`
690    /// rescales all gradients by one factor derived from the global L2 norm.
691    fn clip_gradients(&self, gradients: &mut [Tensor]) -> Result<()> {
692        if let Some(clipnorm) = self.config.clipnorm {
693            // Clip by norm (per-gradient)
694            for grad in gradients.iter_mut() {
695                let norm = grad.norm()?;
696                if norm > clipnorm as f32 && norm > 0.0 {
697                    *grad = grad.mul_scalar((clipnorm as f32) / norm)?;
698                }
699            }
700        }
701
702        if let Some(clipvalue) = self.config.clipvalue {
703            // Clip by value (element-wise)
704            for grad in gradients.iter_mut() {
705                *grad = grad.clamp(-clipvalue as f32, clipvalue as f32)?;
706            }
707        }
708
709        if let Some(global_clipnorm) = self.config.global_clipnorm {
710            // Global gradient clipping: a tensor error must not silently contribute 0.
711            let mut sum_sq = 0.0_f64;
712            for grad in gradients.iter() {
713                let norm = grad.norm()? as f64;
714                sum_sq += norm * norm;
715            }
716            let global_norm = sum_sq.sqrt();
717
718            if global_norm > global_clipnorm && global_norm > 0.0 {
719                let scale = global_clipnorm / global_norm;
720                for grad in gradients.iter_mut() {
721                    *grad = grad.mul_scalar(scale as f32)?;
722                }
723            }
724        }
725
726        Ok(())
727    }
728}
729
730impl TensorFlowOptimizer for TensorFlowAdamW {
731    fn apply_gradients(
732        &mut self,
733        grads_and_vars: &[(Tensor, String)],
734        global_step: Option<i64>,
735    ) -> Result<()> {
736        if let Some(step) = global_step {
737            self.global_step = step;
738        } else {
739            self.global_step += 1;
740        }
741
742        // Update learning rate if schedule is set
743        self.update_learning_rate()?;
744
745        let mut gradients: Vec<Tensor> = grads_and_vars.iter().map(|(g, _)| g.clone()).collect();
746
747        // Apply gradient clipping
748        self.clip_gradients(&mut gradients)?;
749
750        // Apply gradients using inner optimizer
751        let mut variables = self.variables.lock().map_err(|_| {
752            TrustformersError::lock_error(
753                "tensorflow optimizer variables mutex poisoned".to_string(),
754            )
755        })?;
756        // Use the *clipped* gradients, not the caller's originals.
757        for (clipped_grad, (_, var_name)) in gradients.iter().zip(grads_and_vars.iter()) {
758            if let Some(var) = variables.get_mut(var_name) {
759                self.inner.update_named(var_name, var, clipped_grad)?;
760            }
761        }
762        self.inner.step();
763
764        Ok(())
765    }
766
767    fn minimize(
768        &mut self,
769        loss_fn: Box<dyn Fn() -> Result<Tensor>>,
770        var_list: &[String],
771        global_step: Option<i64>,
772    ) -> Result<Tensor> {
773        let loss = loss_fn()?;
774
775        // Compute gradients (this would normally be done by automatic differentiation)
776        let mut grads_and_vars = Vec::new();
777        {
778            let mut variables = self.variables.lock().map_err(|_| {
779                TrustformersError::lock_error(
780                    "tensorflow optimizer variables mutex poisoned".to_string(),
781                )
782            })?;
783
784            for var_name in var_list {
785                if let Some(var) = variables.get_mut(var_name) {
786                    // Compute numerical gradient using finite differences
787                    let grad = self.compute_numerical_gradient(loss_fn.as_ref(), var, var_name)?;
788                    grads_and_vars.push((grad, var_name.clone()));
789                }
790            }
791        } // variables lock is dropped here
792
793        self.apply_gradients(&grads_and_vars, global_step)?;
794        Ok(loss)
795    }
796
797    fn get_config(&self) -> TensorFlowOptimizerConfig {
798        self.config.clone()
799    }
800
801    fn variables(&self) -> Vec<String> {
802        let variables = self.variables.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
803        variables.keys().cloned().collect()
804    }
805
806    fn get_weights(&self) -> Vec<Tensor> {
807        let variables = self.variables.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
808        variables.values().cloned().collect()
809    }
810
811    fn set_weights(&mut self, weights: Vec<Tensor>) -> Result<()> {
812        let mut variables = self.variables.lock().map_err(|_| {
813            TrustformersError::lock_error(
814                "tensorflow optimizer variables mutex poisoned".to_string(),
815            )
816        })?;
817        let var_names: Vec<String> = variables.keys().cloned().collect();
818
819        if weights.len() != var_names.len() {
820            return Err(TrustformersError::invalid_argument(
821                "Number of weights must match number of variables".to_string(),
822            ));
823        }
824
825        for (weight, var_name) in weights.into_iter().zip(var_names) {
826            variables.insert(var_name, weight);
827        }
828
829        Ok(())
830    }
831
832    fn get_learning_rate(&self) -> f64 {
833        self.config.learning_rate
834    }
835
836    fn set_learning_rate(&mut self, lr: f64) -> Result<()> {
837        self.config.learning_rate = lr;
838
839        // Update inner optimizer
840        self.inner.set_lr(lr as f32);
841
842        Ok(())
843    }
844
845    fn get_name(&self) -> &str {
846        self.config.name.as_deref().unwrap_or("AdamW")
847    }
848}
849
850impl TensorFlowAdamW {
851    /// Compute numerical gradient using finite differences
852    fn compute_numerical_gradient(
853        &self,
854        loss_fn: &dyn Fn() -> Result<Tensor>,
855        var: &mut Tensor,
856        _var_name: &str,
857    ) -> Result<Tensor> {
858        const EPSILON: f32 = 1e-4;
859
860        let original_loss = loss_fn()?;
861
862        // Compute gradient for each element using finite differences
863        let var_data = var.data()?;
864        let mut grad_data = vec![0.0; var_data.len()];
865
866        for i in 0..var_data.len() {
867            // Forward difference: f(x + h) - f(x) / h
868            let mut var_plus = var_data.clone();
869            var_plus[i] += EPSILON;
870            *var = Tensor::from_vec(var_plus, &var.shape())?;
871
872            let loss_plus = loss_fn()?;
873            let loss_plus_scalar = loss_plus.data()?[0];
874            let original_loss_scalar = original_loss.data()?[0];
875
876            grad_data[i] = (loss_plus_scalar - original_loss_scalar) / EPSILON;
877
878            // Restore original value
879            let var_original = var_data.clone();
880            *var = Tensor::from_vec(var_original, &var.shape())?;
881        }
882
883        let grad = Tensor::from_vec(grad_data, &var.shape())?;
884        Ok(grad)
885    }
886}
887
888/// TensorFlow optimizer factory
889pub struct TensorFlowOptimizerFactory;
890
891impl TensorFlowOptimizerFactory {
892    /// Create Adam optimizer
893    pub fn adam(
894        learning_rate: f64,
895        beta_1: f64,
896        beta_2: f64,
897        epsilon: f64,
898        weight_decay: Option<f64>,
899        clipnorm: Option<f64>,
900        clipvalue: Option<f64>,
901        global_clipnorm: Option<f64>,
902        use_ema: bool,
903        ema_momentum: f64,
904        jit_compile: bool,
905        name: Option<String>,
906    ) -> Result<TensorFlowAdam> {
907        TensorFlowAdam::new(
908            learning_rate,
909            beta_1,
910            beta_2,
911            epsilon,
912            weight_decay,
913            clipnorm,
914            clipvalue,
915            global_clipnorm,
916            use_ema,
917            ema_momentum,
918            jit_compile,
919            name,
920        )
921    }
922
923    /// Create AdamW optimizer
924    pub fn adamw(
925        learning_rate: f64,
926        beta_1: f64,
927        beta_2: f64,
928        epsilon: f64,
929        weight_decay: f64,
930        clipnorm: Option<f64>,
931        clipvalue: Option<f64>,
932        global_clipnorm: Option<f64>,
933        use_ema: bool,
934        ema_momentum: f64,
935        jit_compile: bool,
936        name: Option<String>,
937    ) -> Result<TensorFlowAdamW> {
938        TensorFlowAdamW::new(
939            learning_rate,
940            beta_1,
941            beta_2,
942            epsilon,
943            weight_decay,
944            clipnorm,
945            clipvalue,
946            global_clipnorm,
947            use_ema,
948            ema_momentum,
949            jit_compile,
950            name,
951        )
952    }
953
954    /// Create exponential decay schedule
955    pub fn exponential_decay(
956        initial_learning_rate: f64,
957        decay_steps: i64,
958        decay_rate: f64,
959        staircase: bool,
960    ) -> TensorFlowExponentialDecay {
961        TensorFlowExponentialDecay::new(initial_learning_rate, decay_steps, decay_rate, staircase)
962    }
963
964    /// Create cosine decay schedule
965    pub fn cosine_decay(
966        initial_learning_rate: f64,
967        decay_steps: i64,
968        alpha: f64,
969    ) -> TensorFlowCosineDecay {
970        TensorFlowCosineDecay::new(initial_learning_rate, decay_steps, alpha)
971    }
972}
973
974#[cfg(test)]
975mod tests {
976    use super::*;
977    use trustformers_core::Tensor;
978
979    #[test]
980    fn test_tensorflow_adam_creation() {
981        let optimizer = TensorFlowAdam::with_defaults().expect("Operation failed in test");
982        assert_eq!(optimizer.get_learning_rate(), 0.001);
983        assert_eq!(optimizer.get_name(), "Adam");
984    }
985
986    #[test]
987    fn test_tensorflow_adamw_creation() {
988        let optimizer = TensorFlowAdamW::with_defaults().expect("Operation failed in test");
989        assert_eq!(optimizer.get_learning_rate(), 0.001);
990        assert_eq!(optimizer.get_name(), "AdamW");
991    }
992
993    #[test]
994    fn test_tensorflow_exponential_decay() {
995        let schedule = TensorFlowExponentialDecay::new(0.1, 100, 0.96, false);
996        assert_eq!(schedule.get_lr(0), 0.1);
997        assert!(schedule.get_lr(100) < 0.1);
998    }
999
1000    #[test]
1001    fn test_tensorflow_cosine_decay() {
1002        let schedule = TensorFlowCosineDecay::new(0.1, 100, 0.0);
1003        assert_eq!(schedule.get_lr(0), 0.1);
1004        assert!(schedule.get_lr(50) < 0.1);
1005        assert!(schedule.get_lr(100) < 0.1);
1006    }
1007
1008    #[test]
1009    fn test_tensorflow_optimizer_factory() {
1010        let adam = TensorFlowOptimizerFactory::adam(
1011            0.001,
1012            0.9,
1013            0.999,
1014            1e-7,
1015            None,
1016            None,
1017            None,
1018            None,
1019            false,
1020            0.99,
1021            true,
1022            Some("TestAdam".to_string()),
1023        )
1024        .expect("Operation failed in test");
1025        assert_eq!(adam.get_name(), "TestAdam");
1026
1027        let adamw = TensorFlowOptimizerFactory::adamw(
1028            0.001,
1029            0.9,
1030            0.999,
1031            1e-7,
1032            0.01,
1033            None,
1034            None,
1035            None,
1036            false,
1037            0.99,
1038            true,
1039            Some("TestAdamW".to_string()),
1040        )
1041        .expect("Operation failed in test");
1042        assert_eq!(adamw.get_name(), "TestAdamW");
1043    }
1044
1045    #[test]
1046    fn test_learning_rate_schedule_with_optimizer() {
1047        let schedule = Box::new(TensorFlowExponentialDecay::new(0.1, 100, 0.96, false));
1048        let optimizer = TensorFlowAdam::with_schedule(
1049            schedule,
1050            0.9,
1051            0.999,
1052            1e-7,
1053            None,
1054            None,
1055            None,
1056            None,
1057            false,
1058            0.99,
1059            true,
1060            Some("ScheduledAdam".to_string()),
1061        )
1062        .expect("Operation failed in test");
1063
1064        assert_eq!(optimizer.get_learning_rate(), 0.1);
1065    }
1066
1067    #[test]
1068    fn test_variable_management() {
1069        let mut optimizer = TensorFlowAdam::with_defaults().expect("Operation failed in test");
1070
1071        let var1 = Tensor::zeros(&[10, 10]).expect("Failed to create tensor");
1072        let var2 = Tensor::zeros(&[5, 5]).expect("Failed to create tensor");
1073
1074        optimizer
1075            .add_variable("var1".to_string(), var1)
1076            .expect("Operation failed in test");
1077        optimizer
1078            .add_variable("var2".to_string(), var2)
1079            .expect("Operation failed in test");
1080
1081        let variables = optimizer.variables();
1082        assert_eq!(variables.len(), 2);
1083        assert!(variables.contains(&"var1".to_string()));
1084        assert!(variables.contains(&"var2".to_string()));
1085    }
1086
1087    #[test]
1088    fn test_learning_rate_updates() {
1089        let mut optimizer = TensorFlowAdam::with_defaults().expect("Operation failed in test");
1090        assert_eq!(optimizer.get_learning_rate(), 0.001);
1091
1092        optimizer.set_learning_rate(0.01).expect("Operation failed in test");
1093        assert_eq!(optimizer.get_learning_rate(), 0.01);
1094    }
1095
1096    #[test]
1097    fn test_config_serialization() {
1098        let optimizer = TensorFlowAdam::with_defaults().expect("Operation failed in test");
1099        let config = optimizer.get_config();
1100
1101        assert_eq!(config.learning_rate, 0.001);
1102        assert_eq!(config.beta_1, Some(0.9));
1103        assert_eq!(config.beta_2, Some(0.999));
1104        assert_eq!(config.epsilon, Some(1e-7));
1105    }
1106}