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
342    fn clip_gradients(&self, gradients: &mut [Tensor]) -> Result<()> {
343        if let Some(clipnorm) = self.config.clipnorm {
344            // Clip by norm
345            for grad in gradients.iter_mut() {
346                let norm = grad.norm()?;
347                if norm > clipnorm as f32 {
348                    grad.mul_scalar((clipnorm as f32) / norm)?;
349                }
350            }
351        }
352
353        if let Some(clipvalue) = self.config.clipvalue {
354            // Clip by value
355            for grad in gradients.iter_mut() {
356                grad.clamp(-clipvalue as f32, clipvalue as f32)?;
357            }
358        }
359
360        if let Some(global_clipnorm) = self.config.global_clipnorm {
361            // Global gradient clipping
362            let global_norm: f64 = gradients
363                .iter()
364                .map(|g| g.norm().unwrap_or(0.0).powi(2) as f64)
365                .sum::<f64>()
366                .sqrt();
367
368            if global_norm > global_clipnorm {
369                let scale = global_clipnorm / global_norm;
370                for grad in gradients.iter_mut() {
371                    grad.mul_scalar(scale as f32)?;
372                }
373            }
374        }
375
376        Ok(())
377    }
378}
379
380impl TensorFlowOptimizer for TensorFlowAdam {
381    fn apply_gradients(
382        &mut self,
383        grads_and_vars: &[(Tensor, String)],
384        global_step: Option<i64>,
385    ) -> Result<()> {
386        if let Some(step) = global_step {
387            self.global_step = step;
388        } else {
389            self.global_step += 1;
390        }
391
392        // Update learning rate if schedule is set
393        self.update_learning_rate()?;
394
395        let mut gradients: Vec<Tensor> = grads_and_vars.iter().map(|(g, _)| g.clone()).collect();
396
397        // Apply gradient clipping
398        self.clip_gradients(&mut gradients)?;
399
400        // Apply gradients using inner optimizer
401        let mut variables = self.variables.lock().map_err(|_| {
402            TrustformersError::lock_error(
403                "tensorflow optimizer variables mutex poisoned".to_string(),
404            )
405        })?;
406        for (grad, var_name) in grads_and_vars {
407            if let Some(var) = variables.get_mut(var_name) {
408                self.inner.update(var, grad)?;
409            }
410        }
411        self.inner.step();
412
413        Ok(())
414    }
415
416    fn minimize(
417        &mut self,
418        loss_fn: Box<dyn Fn() -> Result<Tensor>>,
419        var_list: &[String],
420        global_step: Option<i64>,
421    ) -> Result<Tensor> {
422        let loss = loss_fn()?;
423
424        // Compute gradients (this would normally be done by automatic differentiation)
425        let mut grads_and_vars = Vec::new();
426        {
427            let mut variables = self.variables.lock().map_err(|_| {
428                TrustformersError::lock_error(
429                    "tensorflow optimizer variables mutex poisoned".to_string(),
430                )
431            })?;
432
433            for var_name in var_list {
434                if let Some(var) = variables.get_mut(var_name) {
435                    // Compute numerical gradient using finite differences
436                    let grad = self.compute_numerical_gradient(loss_fn.as_ref(), var, var_name)?;
437                    grads_and_vars.push((grad, var_name.clone()));
438                }
439            }
440        } // variables lock is dropped here
441
442        self.apply_gradients(&grads_and_vars, global_step)?;
443        Ok(loss)
444    }
445
446    fn get_config(&self) -> TensorFlowOptimizerConfig {
447        self.config.clone()
448    }
449
450    fn variables(&self) -> Vec<String> {
451        let variables = self.variables.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
452        variables.keys().cloned().collect()
453    }
454
455    fn get_weights(&self) -> Vec<Tensor> {
456        let variables = self.variables.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
457        variables.values().cloned().collect()
458    }
459
460    fn set_weights(&mut self, weights: Vec<Tensor>) -> Result<()> {
461        let mut variables = self.variables.lock().map_err(|_| {
462            TrustformersError::lock_error(
463                "tensorflow optimizer variables mutex poisoned".to_string(),
464            )
465        })?;
466        let var_names: Vec<String> = variables.keys().cloned().collect();
467
468        if weights.len() != var_names.len() {
469            return Err(TrustformersError::invalid_argument(
470                "Number of weights must match number of variables".to_string(),
471            ));
472        }
473
474        for (weight, var_name) in weights.into_iter().zip(var_names) {
475            variables.insert(var_name, weight);
476        }
477
478        Ok(())
479    }
480
481    fn get_learning_rate(&self) -> f64 {
482        self.config.learning_rate
483    }
484
485    fn set_learning_rate(&mut self, lr: f64) -> Result<()> {
486        self.config.learning_rate = lr;
487
488        // Update inner optimizer
489        self.inner.set_lr(lr as f32);
490
491        Ok(())
492    }
493
494    fn get_name(&self) -> &str {
495        self.config.name.as_deref().unwrap_or("Adam")
496    }
497}
498
499impl TensorFlowAdam {
500    /// Compute numerical gradient using finite differences
501    fn compute_numerical_gradient(
502        &self,
503        loss_fn: &dyn Fn() -> Result<Tensor>,
504        var: &mut Tensor,
505        _var_name: &str,
506    ) -> Result<Tensor> {
507        const EPSILON: f32 = 1e-4;
508
509        let original_loss = loss_fn()?;
510
511        // Compute gradient for each element using finite differences
512        let var_data = var.data()?;
513        let mut grad_data = vec![0.0; var_data.len()];
514
515        for i in 0..var_data.len() {
516            // Forward difference: f(x + h) - f(x) / h
517            let mut var_plus = var_data.clone();
518            var_plus[i] += EPSILON;
519            *var = Tensor::from_vec(var_plus, &var.shape())?;
520
521            let loss_plus = loss_fn()?;
522            let loss_plus_scalar = loss_plus.data()?[0];
523            let original_loss_scalar = original_loss.data()?[0];
524
525            grad_data[i] = (loss_plus_scalar - original_loss_scalar) / EPSILON;
526
527            // Restore original value
528            let var_original = var_data.clone();
529            *var = Tensor::from_vec(var_original, &var.shape())?;
530        }
531
532        let grad = Tensor::from_vec(grad_data, &var.shape())?;
533        Ok(grad)
534    }
535}
536
537/// TensorFlow-compatible AdamW optimizer
538pub struct TensorFlowAdamW {
539    inner: AdamW,
540    config: TensorFlowOptimizerConfig,
541    variables: Arc<Mutex<HashMap<String, Tensor>>>,
542    lr_schedule: Option<Box<dyn TensorFlowLearningRateSchedule>>,
543    global_step: i64,
544}
545
546impl TensorFlowAdamW {
547    /// Create new TensorFlow-compatible AdamW optimizer
548    pub fn new(
549        learning_rate: f64,
550        beta_1: f64,
551        beta_2: f64,
552        epsilon: f64,
553        weight_decay: f64,
554        clipnorm: Option<f64>,
555        clipvalue: Option<f64>,
556        global_clipnorm: Option<f64>,
557        use_ema: bool,
558        ema_momentum: f64,
559        jit_compile: bool,
560        name: Option<String>,
561    ) -> Result<Self> {
562        let config = TensorFlowOptimizerConfig {
563            optimizer_type: "AdamW".to_string(),
564            learning_rate,
565            beta_1: Some(beta_1),
566            beta_2: Some(beta_2),
567            epsilon: Some(epsilon),
568            weight_decay: Some(weight_decay),
569            clipnorm,
570            clipvalue,
571            global_clipnorm,
572            use_ema: Some(use_ema),
573            ema_momentum: Some(ema_momentum),
574            ema_overwrite_frequency: None,
575            jit_compile: Some(jit_compile),
576            name,
577            parameters: HashMap::new(),
578        };
579
580        let _optimizer_config = TensorFlowOptimizerConfig {
581            learning_rate,
582            beta_1: Some(beta_1),
583            beta_2: Some(beta_2),
584            epsilon: Some(epsilon),
585            weight_decay: Some(weight_decay),
586            ..Default::default()
587        };
588
589        let inner = AdamW::new(
590            learning_rate as f32,
591            (beta_1 as f32, beta_2 as f32),
592            epsilon as f32,
593            weight_decay as f32,
594        );
595
596        Ok(Self {
597            inner,
598            config,
599            variables: Arc::new(Mutex::new(HashMap::new())),
600            lr_schedule: None,
601            global_step: 0,
602        })
603    }
604
605    /// Create with default parameters
606    pub fn with_defaults() -> Result<Self> {
607        Self::new(
608            0.001,
609            0.9,
610            0.999,
611            1e-7,
612            0.01,
613            None,
614            None,
615            None,
616            false,
617            0.99,
618            true,
619            Some("AdamW".to_string()),
620        )
621    }
622
623    /// Create with learning rate schedule
624    pub fn with_schedule(
625        schedule: Box<dyn TensorFlowLearningRateSchedule>,
626        beta_1: f64,
627        beta_2: f64,
628        epsilon: f64,
629        weight_decay: f64,
630        clipnorm: Option<f64>,
631        clipvalue: Option<f64>,
632        global_clipnorm: Option<f64>,
633        use_ema: bool,
634        ema_momentum: f64,
635        jit_compile: bool,
636        name: Option<String>,
637    ) -> Result<Self> {
638        let mut optimizer = Self::new(
639            schedule.get_lr(0),
640            beta_1,
641            beta_2,
642            epsilon,
643            weight_decay,
644            clipnorm,
645            clipvalue,
646            global_clipnorm,
647            use_ema,
648            ema_momentum,
649            jit_compile,
650            name,
651        )?;
652
653        optimizer.lr_schedule = Some(schedule);
654        Ok(optimizer)
655    }
656
657    /// Add variable to optimizer
658    pub fn add_variable(&mut self, name: String, var: Tensor) -> Result<()> {
659        let mut variables = self.variables.lock().map_err(|_| {
660            TrustformersError::lock_error(
661                "tensorflow optimizer variables mutex poisoned".to_string(),
662            )
663        })?;
664        variables.insert(name, var);
665        Ok(())
666    }
667
668    /// Update learning rate based on schedule
669    fn update_learning_rate(&mut self) -> Result<()> {
670        if let Some(ref schedule) = self.lr_schedule {
671            let new_lr = schedule.get_lr(self.global_step);
672            self.config.learning_rate = new_lr;
673
674            // Update inner optimizer learning rate
675            self.inner.set_lr(new_lr as f32);
676        }
677        Ok(())
678    }
679
680    /// Apply gradient clipping
681    fn clip_gradients(&self, gradients: &mut [Tensor]) -> Result<()> {
682        if let Some(clipnorm) = self.config.clipnorm {
683            // Clip by norm
684            for grad in gradients.iter_mut() {
685                let norm = grad.norm()?;
686                if norm > clipnorm as f32 {
687                    grad.mul_scalar((clipnorm as f32) / norm)?;
688                }
689            }
690        }
691
692        if let Some(clipvalue) = self.config.clipvalue {
693            // Clip by value
694            for grad in gradients.iter_mut() {
695                grad.clamp(-clipvalue as f32, clipvalue as f32)?;
696            }
697        }
698
699        if let Some(global_clipnorm) = self.config.global_clipnorm {
700            // Global gradient clipping
701            let global_norm: f64 = gradients
702                .iter()
703                .map(|g| g.norm().unwrap_or(0.0).powi(2) as f64)
704                .sum::<f64>()
705                .sqrt();
706
707            if global_norm > global_clipnorm {
708                let scale = global_clipnorm / global_norm;
709                for grad in gradients.iter_mut() {
710                    grad.mul_scalar(scale as f32)?;
711                }
712            }
713        }
714
715        Ok(())
716    }
717}
718
719impl TensorFlowOptimizer for TensorFlowAdamW {
720    fn apply_gradients(
721        &mut self,
722        grads_and_vars: &[(Tensor, String)],
723        global_step: Option<i64>,
724    ) -> Result<()> {
725        if let Some(step) = global_step {
726            self.global_step = step;
727        } else {
728            self.global_step += 1;
729        }
730
731        // Update learning rate if schedule is set
732        self.update_learning_rate()?;
733
734        let mut gradients: Vec<Tensor> = grads_and_vars.iter().map(|(g, _)| g.clone()).collect();
735
736        // Apply gradient clipping
737        self.clip_gradients(&mut gradients)?;
738
739        // Apply gradients using inner optimizer
740        let mut variables = self.variables.lock().map_err(|_| {
741            TrustformersError::lock_error(
742                "tensorflow optimizer variables mutex poisoned".to_string(),
743            )
744        })?;
745        for (grad, var_name) in grads_and_vars {
746            if let Some(var) = variables.get_mut(var_name) {
747                self.inner.update(var, grad)?;
748            }
749        }
750        self.inner.step();
751
752        Ok(())
753    }
754
755    fn minimize(
756        &mut self,
757        loss_fn: Box<dyn Fn() -> Result<Tensor>>,
758        var_list: &[String],
759        global_step: Option<i64>,
760    ) -> Result<Tensor> {
761        let loss = loss_fn()?;
762
763        // Compute gradients (this would normally be done by automatic differentiation)
764        let mut grads_and_vars = Vec::new();
765        {
766            let mut variables = self.variables.lock().map_err(|_| {
767                TrustformersError::lock_error(
768                    "tensorflow optimizer variables mutex poisoned".to_string(),
769                )
770            })?;
771
772            for var_name in var_list {
773                if let Some(var) = variables.get_mut(var_name) {
774                    // Compute numerical gradient using finite differences
775                    let grad = self.compute_numerical_gradient(loss_fn.as_ref(), var, var_name)?;
776                    grads_and_vars.push((grad, var_name.clone()));
777                }
778            }
779        } // variables lock is dropped here
780
781        self.apply_gradients(&grads_and_vars, global_step)?;
782        Ok(loss)
783    }
784
785    fn get_config(&self) -> TensorFlowOptimizerConfig {
786        self.config.clone()
787    }
788
789    fn variables(&self) -> Vec<String> {
790        let variables = self.variables.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
791        variables.keys().cloned().collect()
792    }
793
794    fn get_weights(&self) -> Vec<Tensor> {
795        let variables = self.variables.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
796        variables.values().cloned().collect()
797    }
798
799    fn set_weights(&mut self, weights: Vec<Tensor>) -> Result<()> {
800        let mut variables = self.variables.lock().map_err(|_| {
801            TrustformersError::lock_error(
802                "tensorflow optimizer variables mutex poisoned".to_string(),
803            )
804        })?;
805        let var_names: Vec<String> = variables.keys().cloned().collect();
806
807        if weights.len() != var_names.len() {
808            return Err(TrustformersError::invalid_argument(
809                "Number of weights must match number of variables".to_string(),
810            ));
811        }
812
813        for (weight, var_name) in weights.into_iter().zip(var_names) {
814            variables.insert(var_name, weight);
815        }
816
817        Ok(())
818    }
819
820    fn get_learning_rate(&self) -> f64 {
821        self.config.learning_rate
822    }
823
824    fn set_learning_rate(&mut self, lr: f64) -> Result<()> {
825        self.config.learning_rate = lr;
826
827        // Update inner optimizer
828        self.inner.set_lr(lr as f32);
829
830        Ok(())
831    }
832
833    fn get_name(&self) -> &str {
834        self.config.name.as_deref().unwrap_or("AdamW")
835    }
836}
837
838impl TensorFlowAdamW {
839    /// Compute numerical gradient using finite differences
840    fn compute_numerical_gradient(
841        &self,
842        loss_fn: &dyn Fn() -> Result<Tensor>,
843        var: &mut Tensor,
844        _var_name: &str,
845    ) -> Result<Tensor> {
846        const EPSILON: f32 = 1e-4;
847
848        let original_loss = loss_fn()?;
849
850        // Compute gradient for each element using finite differences
851        let var_data = var.data()?;
852        let mut grad_data = vec![0.0; var_data.len()];
853
854        for i in 0..var_data.len() {
855            // Forward difference: f(x + h) - f(x) / h
856            let mut var_plus = var_data.clone();
857            var_plus[i] += EPSILON;
858            *var = Tensor::from_vec(var_plus, &var.shape())?;
859
860            let loss_plus = loss_fn()?;
861            let loss_plus_scalar = loss_plus.data()?[0];
862            let original_loss_scalar = original_loss.data()?[0];
863
864            grad_data[i] = (loss_plus_scalar - original_loss_scalar) / EPSILON;
865
866            // Restore original value
867            let var_original = var_data.clone();
868            *var = Tensor::from_vec(var_original, &var.shape())?;
869        }
870
871        let grad = Tensor::from_vec(grad_data, &var.shape())?;
872        Ok(grad)
873    }
874}
875
876/// TensorFlow optimizer factory
877pub struct TensorFlowOptimizerFactory;
878
879impl TensorFlowOptimizerFactory {
880    /// Create Adam optimizer
881    pub fn adam(
882        learning_rate: f64,
883        beta_1: f64,
884        beta_2: f64,
885        epsilon: f64,
886        weight_decay: Option<f64>,
887        clipnorm: Option<f64>,
888        clipvalue: Option<f64>,
889        global_clipnorm: Option<f64>,
890        use_ema: bool,
891        ema_momentum: f64,
892        jit_compile: bool,
893        name: Option<String>,
894    ) -> Result<TensorFlowAdam> {
895        TensorFlowAdam::new(
896            learning_rate,
897            beta_1,
898            beta_2,
899            epsilon,
900            weight_decay,
901            clipnorm,
902            clipvalue,
903            global_clipnorm,
904            use_ema,
905            ema_momentum,
906            jit_compile,
907            name,
908        )
909    }
910
911    /// Create AdamW optimizer
912    pub fn adamw(
913        learning_rate: f64,
914        beta_1: f64,
915        beta_2: f64,
916        epsilon: f64,
917        weight_decay: f64,
918        clipnorm: Option<f64>,
919        clipvalue: Option<f64>,
920        global_clipnorm: Option<f64>,
921        use_ema: bool,
922        ema_momentum: f64,
923        jit_compile: bool,
924        name: Option<String>,
925    ) -> Result<TensorFlowAdamW> {
926        TensorFlowAdamW::new(
927            learning_rate,
928            beta_1,
929            beta_2,
930            epsilon,
931            weight_decay,
932            clipnorm,
933            clipvalue,
934            global_clipnorm,
935            use_ema,
936            ema_momentum,
937            jit_compile,
938            name,
939        )
940    }
941
942    /// Create exponential decay schedule
943    pub fn exponential_decay(
944        initial_learning_rate: f64,
945        decay_steps: i64,
946        decay_rate: f64,
947        staircase: bool,
948    ) -> TensorFlowExponentialDecay {
949        TensorFlowExponentialDecay::new(initial_learning_rate, decay_steps, decay_rate, staircase)
950    }
951
952    /// Create cosine decay schedule
953    pub fn cosine_decay(
954        initial_learning_rate: f64,
955        decay_steps: i64,
956        alpha: f64,
957    ) -> TensorFlowCosineDecay {
958        TensorFlowCosineDecay::new(initial_learning_rate, decay_steps, alpha)
959    }
960}
961
962#[cfg(test)]
963mod tests {
964    use super::*;
965    use trustformers_core::Tensor;
966
967    #[test]
968    fn test_tensorflow_adam_creation() {
969        let optimizer = TensorFlowAdam::with_defaults().expect("Operation failed in test");
970        assert_eq!(optimizer.get_learning_rate(), 0.001);
971        assert_eq!(optimizer.get_name(), "Adam");
972    }
973
974    #[test]
975    fn test_tensorflow_adamw_creation() {
976        let optimizer = TensorFlowAdamW::with_defaults().expect("Operation failed in test");
977        assert_eq!(optimizer.get_learning_rate(), 0.001);
978        assert_eq!(optimizer.get_name(), "AdamW");
979    }
980
981    #[test]
982    fn test_tensorflow_exponential_decay() {
983        let schedule = TensorFlowExponentialDecay::new(0.1, 100, 0.96, false);
984        assert_eq!(schedule.get_lr(0), 0.1);
985        assert!(schedule.get_lr(100) < 0.1);
986    }
987
988    #[test]
989    fn test_tensorflow_cosine_decay() {
990        let schedule = TensorFlowCosineDecay::new(0.1, 100, 0.0);
991        assert_eq!(schedule.get_lr(0), 0.1);
992        assert!(schedule.get_lr(50) < 0.1);
993        assert!(schedule.get_lr(100) < 0.1);
994    }
995
996    #[test]
997    fn test_tensorflow_optimizer_factory() {
998        let adam = TensorFlowOptimizerFactory::adam(
999            0.001,
1000            0.9,
1001            0.999,
1002            1e-7,
1003            None,
1004            None,
1005            None,
1006            None,
1007            false,
1008            0.99,
1009            true,
1010            Some("TestAdam".to_string()),
1011        )
1012        .expect("Operation failed in test");
1013        assert_eq!(adam.get_name(), "TestAdam");
1014
1015        let adamw = TensorFlowOptimizerFactory::adamw(
1016            0.001,
1017            0.9,
1018            0.999,
1019            1e-7,
1020            0.01,
1021            None,
1022            None,
1023            None,
1024            false,
1025            0.99,
1026            true,
1027            Some("TestAdamW".to_string()),
1028        )
1029        .expect("Operation failed in test");
1030        assert_eq!(adamw.get_name(), "TestAdamW");
1031    }
1032
1033    #[test]
1034    fn test_learning_rate_schedule_with_optimizer() {
1035        let schedule = Box::new(TensorFlowExponentialDecay::new(0.1, 100, 0.96, false));
1036        let optimizer = TensorFlowAdam::with_schedule(
1037            schedule,
1038            0.9,
1039            0.999,
1040            1e-7,
1041            None,
1042            None,
1043            None,
1044            None,
1045            false,
1046            0.99,
1047            true,
1048            Some("ScheduledAdam".to_string()),
1049        )
1050        .expect("Operation failed in test");
1051
1052        assert_eq!(optimizer.get_learning_rate(), 0.1);
1053    }
1054
1055    #[test]
1056    fn test_variable_management() {
1057        let mut optimizer = TensorFlowAdam::with_defaults().expect("Operation failed in test");
1058
1059        let var1 = Tensor::zeros(&[10, 10]).expect("Failed to create tensor");
1060        let var2 = Tensor::zeros(&[5, 5]).expect("Failed to create tensor");
1061
1062        optimizer
1063            .add_variable("var1".to_string(), var1)
1064            .expect("Operation failed in test");
1065        optimizer
1066            .add_variable("var2".to_string(), var2)
1067            .expect("Operation failed in test");
1068
1069        let variables = optimizer.variables();
1070        assert_eq!(variables.len(), 2);
1071        assert!(variables.contains(&"var1".to_string()));
1072        assert!(variables.contains(&"var2".to_string()));
1073    }
1074
1075    #[test]
1076    fn test_learning_rate_updates() {
1077        let mut optimizer = TensorFlowAdam::with_defaults().expect("Operation failed in test");
1078        assert_eq!(optimizer.get_learning_rate(), 0.001);
1079
1080        optimizer.set_learning_rate(0.01).expect("Operation failed in test");
1081        assert_eq!(optimizer.get_learning_rate(), 0.01);
1082    }
1083
1084    #[test]
1085    fn test_config_serialization() {
1086        let optimizer = TensorFlowAdam::with_defaults().expect("Operation failed in test");
1087        let config = optimizer.get_config();
1088
1089        assert_eq!(config.learning_rate, 0.001);
1090        assert_eq!(config.beta_1, Some(0.9));
1091        assert_eq!(config.beta_2, Some(0.999));
1092        assert_eq!(config.epsilon, Some(1e-7));
1093    }
1094}