Skip to main content

torsh_nn/layers/
efficientnet.rs

1//! EfficientNet Architecture Components
2//!
3//! This module provides comprehensive EfficientNet building blocks and utilities including:
4//! - EfficientNet compound scaling
5//! - Advanced MBConv variants
6//! - Stochastic depth for training
7//! - Complete EfficientNet models (B0-B7)
8
9use crate::container::Sequential;
10use crate::layers::activation::SiLU;
11use crate::layers::blocks::MBConvBlock;
12use crate::layers::{BatchNorm2d, Conv2d, Dropout, Linear};
13use crate::{Module, ModuleBase, Parameter};
14use torsh_core::device::DeviceType;
15
16// Conditional imports for std/no_std compatibility
17#[cfg(feature = "std")]
18use std::collections::HashMap;
19
20#[cfg(not(feature = "std"))]
21use hashbrown::HashMap;
22use torsh_core::error::Result;
23use torsh_tensor::Tensor;
24
25/// EfficientNet compound scaling configuration
26#[derive(Debug, Clone)]
27pub struct EfficientNetConfig {
28    /// Width multiplier (channels)
29    pub width_multiplier: f32,
30    /// Depth multiplier (layers)
31    pub depth_multiplier: f32,
32    /// Input resolution
33    pub input_resolution: usize,
34    /// Dropout rate
35    pub dropout_rate: f32,
36    /// Stochastic depth rate
37    pub stochastic_depth_rate: f32,
38    /// Number of classes for classification
39    pub num_classes: usize,
40}
41
42impl EfficientNetConfig {
43    /// EfficientNet-B0 configuration
44    pub fn b0() -> Self {
45        Self {
46            width_multiplier: 1.0,
47            depth_multiplier: 1.0,
48            input_resolution: 224,
49            dropout_rate: 0.2,
50            stochastic_depth_rate: 0.2,
51            num_classes: 1000,
52        }
53    }
54
55    /// EfficientNet-B1 configuration
56    pub fn b1() -> Self {
57        Self {
58            width_multiplier: 1.0,
59            depth_multiplier: 1.1,
60            input_resolution: 240,
61            dropout_rate: 0.2,
62            stochastic_depth_rate: 0.2,
63            num_classes: 1000,
64        }
65    }
66
67    /// EfficientNet-B2 configuration
68    pub fn b2() -> Self {
69        Self {
70            width_multiplier: 1.1,
71            depth_multiplier: 1.2,
72            input_resolution: 260,
73            dropout_rate: 0.3,
74            stochastic_depth_rate: 0.2,
75            num_classes: 1000,
76        }
77    }
78
79    /// EfficientNet-B3 configuration
80    pub fn b3() -> Self {
81        Self {
82            width_multiplier: 1.2,
83            depth_multiplier: 1.4,
84            input_resolution: 300,
85            dropout_rate: 0.3,
86            stochastic_depth_rate: 0.2,
87            num_classes: 1000,
88        }
89    }
90
91    /// EfficientNet-B4 configuration
92    pub fn b4() -> Self {
93        Self {
94            width_multiplier: 1.4,
95            depth_multiplier: 1.8,
96            input_resolution: 380,
97            dropout_rate: 0.4,
98            stochastic_depth_rate: 0.2,
99            num_classes: 1000,
100        }
101    }
102
103    /// EfficientNet-B5 configuration
104    pub fn b5() -> Self {
105        Self {
106            width_multiplier: 1.6,
107            depth_multiplier: 2.2,
108            input_resolution: 456,
109            dropout_rate: 0.4,
110            stochastic_depth_rate: 0.2,
111            num_classes: 1000,
112        }
113    }
114
115    /// EfficientNet-B6 configuration
116    pub fn b6() -> Self {
117        Self {
118            width_multiplier: 1.8,
119            depth_multiplier: 2.6,
120            input_resolution: 528,
121            dropout_rate: 0.5,
122            stochastic_depth_rate: 0.2,
123            num_classes: 1000,
124        }
125    }
126
127    /// EfficientNet-B7 configuration
128    pub fn b7() -> Self {
129        Self {
130            width_multiplier: 2.0,
131            depth_multiplier: 3.1,
132            input_resolution: 600,
133            dropout_rate: 0.5,
134            stochastic_depth_rate: 0.2,
135            num_classes: 1000,
136        }
137    }
138
139    /// Calculate scaled width for given base width
140    pub fn scale_width(&self, base_width: usize) -> usize {
141        let scaled = (base_width as f32 * self.width_multiplier) as usize;
142        // Round to nearest multiple of 8 for efficient computation
143        ((scaled + 4) / 8) * 8
144    }
145
146    /// Calculate scaled depth for given base depth
147    pub fn scale_depth(&self, base_depth: usize) -> usize {
148        (base_depth as f32 * self.depth_multiplier).ceil() as usize
149    }
150}
151
152/// Advanced MBConv block with Stochastic Depth
153pub struct MBConvWithStochasticDepth {
154    base: ModuleBase,
155    mbconv: MBConvBlock,
156    stochastic_depth_prob: f32,
157    use_shortcut: bool,
158}
159
160impl MBConvWithStochasticDepth {
161    /// Create a new MBConv block with stochastic depth
162    pub fn new(
163        in_channels: usize,
164        out_channels: usize,
165        kernel_size: usize,
166        stride: usize,
167        expansion_ratio: usize,
168        se_ratio: f32,
169        stochastic_depth_prob: f32,
170    ) -> Result<Self> {
171        let mbconv = MBConvBlock::new(
172            in_channels,
173            out_channels,
174            kernel_size,
175            stride,
176            expansion_ratio,
177            Some(se_ratio),
178            0.0, // drop_rate
179        )?;
180
181        let use_shortcut = stride == 1 && in_channels == out_channels;
182
183        Ok(Self {
184            base: ModuleBase::new(),
185            mbconv,
186            stochastic_depth_prob,
187            use_shortcut,
188        })
189    }
190
191    /// Apply stochastic depth during training
192    fn apply_stochastic_depth(&self, x: &Tensor, residual: &Tensor) -> Result<Tensor> {
193        if !self.training() || self.stochastic_depth_prob == 0.0 {
194            return x.add_op(residual);
195        }
196
197        // During training, randomly drop the entire block
198        let keep_prob = 1.0 - self.stochastic_depth_prob;
199
200        // Simplified stochastic depth - in practice would use proper random sampling
201        // For now, we'll apply a scaling factor
202        let scaled_x = x.mul_scalar(keep_prob)?;
203        scaled_x.add_op(residual)
204    }
205}
206
207impl Module for MBConvWithStochasticDepth {
208    fn forward(&self, input: &Tensor) -> Result<Tensor> {
209        let output = self.mbconv.forward(input)?;
210
211        if self.use_shortcut {
212            self.apply_stochastic_depth(&output, input)
213        } else {
214            Ok(output)
215        }
216    }
217
218    fn parameters(&self) -> HashMap<String, Parameter> {
219        self.mbconv.parameters()
220    }
221
222    fn named_parameters(&self) -> HashMap<String, Parameter> {
223        self.mbconv.named_parameters()
224    }
225
226    fn training(&self) -> bool {
227        self.base.training()
228    }
229
230    fn train(&mut self) {
231        self.base.set_training(true);
232        // Note: mbconv.train() would need to be called if it's mutable
233    }
234
235    fn eval(&mut self) {
236        self.base.set_training(false);
237        // Note: mbconv.eval() would need to be called if it's mutable
238    }
239
240    fn set_training(&mut self, training: bool) {
241        self.base.set_training(training);
242        // Note: mbconv.set_training(training) would need to be called if it's mutable
243    }
244
245    fn to_device(&mut self, device: DeviceType) -> Result<()> {
246        self.base.to_device(device)
247        // Note: mbconv.to_device(device) would need to be called if it's mutable
248    }
249}
250
251/// EfficientNet block configuration
252#[derive(Debug, Clone)]
253pub struct BlockConfig {
254    /// Input channels
255    pub input_filters: usize,
256    /// Output channels
257    pub output_filters: usize,
258    /// Kernel size
259    pub kernel_size: usize,
260    /// Stride
261    pub stride: usize,
262    /// Expansion ratio
263    pub expansion_ratio: usize,
264    /// SE ratio
265    pub se_ratio: f32,
266    /// Number of repeats
267    pub num_repeat: usize,
268}
269
270impl BlockConfig {
271    /// Scale the block configuration according to EfficientNet scaling
272    pub fn scale(&self, config: &EfficientNetConfig) -> Self {
273        Self {
274            input_filters: config.scale_width(self.input_filters),
275            output_filters: config.scale_width(self.output_filters),
276            kernel_size: self.kernel_size,
277            stride: self.stride,
278            expansion_ratio: self.expansion_ratio,
279            se_ratio: self.se_ratio,
280            num_repeat: config.scale_depth(self.num_repeat),
281        }
282    }
283}
284
285/// EfficientNet backbone architecture
286pub struct EfficientNetBackbone {
287    base: ModuleBase,
288    stem: Sequential,
289    blocks: Vec<MBConvWithStochasticDepth>,
290    head: Sequential,
291    #[allow(dead_code)]
292    config: EfficientNetConfig,
293}
294
295impl EfficientNetBackbone {
296    /// Create a new EfficientNet backbone
297    pub fn new(config: EfficientNetConfig) -> Result<Self> {
298        let base_blocks = Self::get_base_block_configs();
299        let scaled_blocks: Vec<BlockConfig> = base_blocks
300            .iter()
301            .map(|block| block.scale(&config))
302            .collect();
303
304        let base = ModuleBase::new();
305
306        // Stem: Conv2d + BatchNorm + Swish
307        let stem_channels = config.scale_width(32);
308        let stem = Sequential::new()
309            .add(Conv2d::new(
310                3,
311                stem_channels,
312                (3, 3),
313                (2, 2),
314                (1, 1),
315                (1, 1),
316                false,
317                1,
318            ))
319            .add(BatchNorm2d::new(stem_channels)?)
320            .add(SiLU::new());
321
322        // Build MBConv blocks
323        let mut blocks = Vec::new();
324        let mut total_blocks = 0;
325        for block_config in &scaled_blocks {
326            total_blocks += block_config.num_repeat;
327        }
328
329        let mut block_index = 0;
330        for block_config in &scaled_blocks {
331            for i in 0..block_config.num_repeat {
332                let input_filters = if i == 0 {
333                    block_config.input_filters
334                } else {
335                    block_config.output_filters
336                };
337
338                let stride = if i == 0 { block_config.stride } else { 1 };
339
340                // Calculate stochastic depth probability
341                let stochastic_depth_prob =
342                    config.stochastic_depth_rate * (block_index as f32 / total_blocks as f32);
343
344                let block = MBConvWithStochasticDepth::new(
345                    input_filters,
346                    block_config.output_filters,
347                    block_config.kernel_size,
348                    stride,
349                    block_config.expansion_ratio,
350                    block_config.se_ratio,
351                    stochastic_depth_prob,
352                );
353
354                blocks.push(block?);
355                block_index += 1;
356            }
357        }
358
359        // Head: Conv2d + BatchNorm + Swish + GlobalAvgPool
360        let head_channels = config.scale_width(1280);
361        let last_block_channels = scaled_blocks
362            .last()
363            .expect("scaled_blocks should not be empty")
364            .output_filters;
365        let head = Sequential::new()
366            .add(Conv2d::new(
367                last_block_channels,
368                head_channels,
369                (1, 1),
370                (1, 1),
371                (0, 0),
372                (1, 1),
373                false,
374                1,
375            ))
376            .add(BatchNorm2d::new(head_channels)?)
377            .add(SiLU::new());
378
379        Ok(Self {
380            base,
381            stem,
382            blocks,
383            head,
384            config,
385        })
386    }
387
388    /// Get base block configurations for EfficientNet-B0
389    fn get_base_block_configs() -> Vec<BlockConfig> {
390        vec![
391            BlockConfig {
392                input_filters: 32,
393                output_filters: 16,
394                kernel_size: 3,
395                stride: 1,
396                expansion_ratio: 1,
397                se_ratio: 0.25,
398                num_repeat: 1,
399            },
400            BlockConfig {
401                input_filters: 16,
402                output_filters: 24,
403                kernel_size: 3,
404                stride: 2,
405                expansion_ratio: 6,
406                se_ratio: 0.25,
407                num_repeat: 2,
408            },
409            BlockConfig {
410                input_filters: 24,
411                output_filters: 40,
412                kernel_size: 5,
413                stride: 2,
414                expansion_ratio: 6,
415                se_ratio: 0.25,
416                num_repeat: 2,
417            },
418            BlockConfig {
419                input_filters: 40,
420                output_filters: 80,
421                kernel_size: 3,
422                stride: 2,
423                expansion_ratio: 6,
424                se_ratio: 0.25,
425                num_repeat: 3,
426            },
427            BlockConfig {
428                input_filters: 80,
429                output_filters: 112,
430                kernel_size: 5,
431                stride: 1,
432                expansion_ratio: 6,
433                se_ratio: 0.25,
434                num_repeat: 3,
435            },
436            BlockConfig {
437                input_filters: 112,
438                output_filters: 192,
439                kernel_size: 5,
440                stride: 2,
441                expansion_ratio: 6,
442                se_ratio: 0.25,
443                num_repeat: 4,
444            },
445            BlockConfig {
446                input_filters: 192,
447                output_filters: 320,
448                kernel_size: 3,
449                stride: 1,
450                expansion_ratio: 6,
451                se_ratio: 0.25,
452                num_repeat: 1,
453            },
454        ]
455    }
456
457    /// Extract features from the backbone
458    pub fn extract_features(&self, input: &Tensor) -> Result<Tensor> {
459        let mut x = self.stem.forward(input)?;
460
461        for block in &self.blocks {
462            x = block.forward(&x)?;
463        }
464
465        self.head.forward(&x)
466    }
467}
468
469impl Module for EfficientNetBackbone {
470    fn forward(&self, input: &Tensor) -> Result<Tensor> {
471        self.extract_features(input)
472    }
473
474    fn parameters(&self) -> HashMap<String, Parameter> {
475        let mut params = HashMap::new();
476
477        // Stem parameters
478        for (name, param) in self.stem.parameters() {
479            params.insert(format!("stem.{}", name), param);
480        }
481
482        // Block parameters
483        for (i, block) in self.blocks.iter().enumerate() {
484            for (name, param) in block.parameters() {
485                params.insert(format!("blocks.{}.{}", i, name), param);
486            }
487        }
488
489        // Head parameters
490        for (name, param) in self.head.parameters() {
491            params.insert(format!("head.{}", name), param);
492        }
493
494        params
495    }
496
497    fn named_parameters(&self) -> HashMap<String, Parameter> {
498        self.parameters()
499    }
500
501    fn training(&self) -> bool {
502        self.base.training()
503    }
504
505    fn train(&mut self) {
506        self.base.set_training(true);
507    }
508
509    fn eval(&mut self) {
510        self.base.set_training(false);
511    }
512
513    fn set_training(&mut self, training: bool) {
514        self.base.set_training(training);
515    }
516
517    fn to_device(&mut self, device: DeviceType) -> Result<()> {
518        self.base.to_device(device)
519    }
520}
521
522/// Complete EfficientNet model with classification head
523pub struct EfficientNet {
524    base: ModuleBase,
525    backbone: EfficientNetBackbone,
526    classifier: Sequential,
527    #[allow(dead_code)]
528    config: EfficientNetConfig,
529}
530
531impl EfficientNet {
532    /// Create a new EfficientNet model
533    pub fn new(config: EfficientNetConfig) -> Result<Self> {
534        let backbone = EfficientNetBackbone::new(config.clone())?;
535
536        // Classification head
537        let head_channels = config.scale_width(1280);
538        let classifier = Sequential::new()
539            .add(Dropout::new(config.dropout_rate))
540            .add(Linear::new(head_channels, config.num_classes, true));
541
542        Ok(Self {
543            base: ModuleBase::new(),
544            backbone,
545            classifier,
546            config,
547        })
548    }
549
550    /// Create EfficientNet-B0
551    pub fn b0(num_classes: usize) -> Result<Self> {
552        let mut config = EfficientNetConfig::b0();
553        config.num_classes = num_classes;
554        Self::new(config)
555    }
556
557    /// Create EfficientNet-B1
558    pub fn b1(num_classes: usize) -> Result<Self> {
559        let mut config = EfficientNetConfig::b1();
560        config.num_classes = num_classes;
561        Self::new(config)
562    }
563
564    /// Create EfficientNet-B2
565    pub fn b2(num_classes: usize) -> Result<Self> {
566        let mut config = EfficientNetConfig::b2();
567        config.num_classes = num_classes;
568        Self::new(config)
569    }
570
571    /// Create EfficientNet-B3
572    pub fn b3(num_classes: usize) -> Result<Self> {
573        let mut config = EfficientNetConfig::b3();
574        config.num_classes = num_classes;
575        Self::new(config)
576    }
577
578    /// Create EfficientNet-B4
579    pub fn b4(num_classes: usize) -> Result<Self> {
580        let mut config = EfficientNetConfig::b4();
581        config.num_classes = num_classes;
582        Self::new(config)
583    }
584
585    /// Create EfficientNet-B5
586    pub fn b5(num_classes: usize) -> Result<Self> {
587        let mut config = EfficientNetConfig::b5();
588        config.num_classes = num_classes;
589        Self::new(config)
590    }
591
592    /// Create EfficientNet-B6
593    pub fn b6(num_classes: usize) -> Result<Self> {
594        let mut config = EfficientNetConfig::b6();
595        config.num_classes = num_classes;
596        Self::new(config)
597    }
598
599    /// Create EfficientNet-B7
600    pub fn b7(num_classes: usize) -> Result<Self> {
601        let mut config = EfficientNetConfig::b7();
602        config.num_classes = num_classes;
603        Self::new(config)
604    }
605
606    /// Get the backbone for feature extraction
607    pub fn backbone(&self) -> &EfficientNetBackbone {
608        &self.backbone
609    }
610
611    /// Extract features without classification
612    pub fn extract_features(&self, input: &Tensor) -> Result<Tensor> {
613        self.backbone.extract_features(input)
614    }
615}
616
617impl Module for EfficientNet {
618    fn forward(&self, input: &Tensor) -> Result<Tensor> {
619        let features = self.backbone.forward(input)?;
620
621        // Apply global average pooling to reduce spatial dimensions
622        let features_shape = features.shape();
623        let shape = features_shape.dims();
624        let batch_size = shape[0];
625        let channels = shape[1];
626        let height = shape[2];
627        let width = shape[3];
628
629        // Manual global average pooling: sum over spatial dimensions and divide
630        let mut pooled_data = vec![0.0f32; batch_size * channels];
631        let features_data = features.to_vec()?;
632
633        for b in 0..batch_size {
634            for c in 0..channels {
635                let mut sum = 0.0f32;
636                for h in 0..height {
637                    for w in 0..width {
638                        let idx =
639                            b * (channels * height * width) + c * (height * width) + h * width + w;
640                        if idx < features_data.len() {
641                            sum += features_data[idx];
642                        }
643                    }
644                }
645                pooled_data[b * channels + c] = sum / (height * width) as f32;
646            }
647        }
648
649        // Create pooled tensor with shape [batch_size, channels]
650        let pooled = Tensor::from_vec(pooled_data, &[batch_size, channels])?;
651
652        self.classifier.forward(&pooled)
653    }
654
655    fn parameters(&self) -> HashMap<String, Parameter> {
656        let mut params = HashMap::new();
657
658        // Backbone parameters
659        for (name, param) in self.backbone.parameters() {
660            params.insert(format!("backbone.{}", name), param);
661        }
662
663        // Classifier parameters
664        for (name, param) in self.classifier.parameters() {
665            params.insert(format!("classifier.{}", name), param);
666        }
667
668        params
669    }
670
671    fn named_parameters(&self) -> HashMap<String, Parameter> {
672        self.parameters()
673    }
674
675    fn training(&self) -> bool {
676        self.base.training()
677    }
678
679    fn train(&mut self) {
680        self.base.set_training(true);
681    }
682
683    fn eval(&mut self) {
684        self.base.set_training(false);
685    }
686
687    fn set_training(&mut self, training: bool) {
688        self.base.set_training(training);
689    }
690
691    fn to_device(&mut self, device: DeviceType) -> Result<()> {
692        self.base.to_device(device)
693    }
694}
695
696/// Utilities for EfficientNet training and deployment
697pub mod utils {
698    use super::*;
699
700    /// Calculate model parameters for different EfficientNet variants
701    pub fn calculate_model_params(config: &EfficientNetConfig) -> (usize, f32) {
702        // Rough parameter count estimation
703        let base_params = 5_300_000; // EfficientNet-B0 approximate params
704        let width_factor = config.width_multiplier.powi(2);
705        let depth_factor = config.depth_multiplier;
706
707        let estimated_params = (base_params as f32 * width_factor * depth_factor) as usize;
708        let estimated_flops = estimated_params as f32 * config.input_resolution as f32 * 0.1;
709
710        (estimated_params, estimated_flops)
711    }
712
713    /// Get recommended training settings for different model sizes
714    pub fn get_training_config(config: &EfficientNetConfig) -> TrainingConfig {
715        match config.width_multiplier {
716            w if w <= 1.0 => TrainingConfig {
717                batch_size: 128,
718                learning_rate: 0.256,
719                weight_decay: 1e-5,
720                epochs: 350,
721            },
722            w if w <= 1.4 => TrainingConfig {
723                batch_size: 64,
724                learning_rate: 0.128,
725                weight_decay: 1e-5,
726                epochs: 350,
727            },
728            _ => TrainingConfig {
729                batch_size: 32,
730                learning_rate: 0.064,
731                weight_decay: 1e-5,
732                epochs: 350,
733            },
734        }
735    }
736
737    /// Create custom EfficientNet with specific scaling
738    pub fn create_custom_efficientnet(
739        width_multiplier: f32,
740        depth_multiplier: f32,
741        resolution: usize,
742        num_classes: usize,
743    ) -> Result<EfficientNet> {
744        let config = EfficientNetConfig {
745            width_multiplier,
746            depth_multiplier,
747            input_resolution: resolution,
748            dropout_rate: 0.2 + (width_multiplier - 1.0) * 0.3,
749            stochastic_depth_rate: 0.2,
750            num_classes,
751        };
752
753        EfficientNet::new(config)
754    }
755}
756
757/// Training configuration recommendations
758#[derive(Debug, Clone)]
759pub struct TrainingConfig {
760    pub batch_size: usize,
761    pub learning_rate: f32,
762    pub weight_decay: f32,
763    pub epochs: usize,
764}
765
766#[cfg(test)]
767mod tests {
768    use super::*;
769    use torsh_tensor::creation::*;
770
771    #[test]
772    fn test_efficientnet_config() {
773        let config = EfficientNetConfig::b0();
774        assert_eq!(config.width_multiplier, 1.0);
775        assert_eq!(config.depth_multiplier, 1.0);
776        assert_eq!(config.input_resolution, 224);
777    }
778
779    #[test]
780    fn test_scaling() {
781        let config = EfficientNetConfig::b1();
782        assert_eq!(config.scale_width(32), 32); // 32 * 1.0 = 32
783        assert_eq!(config.scale_depth(3), 4); // ceil(3 * 1.1) = 4
784    }
785
786    #[test]
787    fn test_block_config_scaling() {
788        let block = BlockConfig {
789            input_filters: 32,
790            output_filters: 64,
791            kernel_size: 3,
792            stride: 1,
793            expansion_ratio: 6,
794            se_ratio: 0.25,
795            num_repeat: 2,
796        };
797
798        let config = EfficientNetConfig::b2();
799        let scaled = block.scale(&config);
800
801        assert!(scaled.input_filters >= 32); // Should be scaled up
802        assert!(scaled.output_filters >= 64); // Should be scaled up
803        assert!(scaled.num_repeat >= 2); // Should be scaled up
804    }
805
806    #[test]
807    fn test_efficientnet_creation() -> Result<()> {
808        let model = EfficientNet::b0(1000)?;
809        assert_eq!(model.config.num_classes, 1000);
810        Ok(())
811    }
812
813    #[test]
814    #[ignore]
815    fn test_efficientnet_forward() -> Result<()> {
816        let model = EfficientNet::b0(10)?;
817        let input = ones(&[1, 3, 224, 224])?;
818
819        let output = model.forward(&input)?;
820        assert_eq!(output.shape().dims(), &[1, 10]);
821
822        Ok(())
823    }
824
825    #[test]
826    fn test_mbconv_with_stochastic_depth() -> Result<()> {
827        let block = MBConvWithStochasticDepth::new(32, 32, 3, 1, 6, 0.25, 0.1)?;
828        let input = ones(&[1, 32, 56, 56])?;
829
830        let output = block.forward(&input)?;
831        assert_eq!(output.shape().dims(), &[1, 32, 56, 56]);
832
833        Ok(())
834    }
835
836    #[test]
837    fn test_utils_calculate_params() {
838        let config = EfficientNetConfig::b0();
839        let (params, flops) = utils::calculate_model_params(&config);
840
841        assert!(params > 1_000_000); // Should have reasonable number of parameters
842        assert!(flops > 1_000_000.0); // Should have reasonable FLOPS
843    }
844
845    #[test]
846    fn test_custom_efficientnet() -> Result<()> {
847        let model = utils::create_custom_efficientnet(1.2, 1.4, 256, 100)?;
848        assert_eq!(model.config.num_classes, 100);
849        assert_eq!(model.config.input_resolution, 256);
850        Ok(())
851    }
852}