Skip to main content

torsh_sparse/layers/
conv.rs

1//! Sparse convolution layers
2//!
3//! This module provides convolution operations optimized for sparse tensors,
4//! including 1D convolution, 2D convolution, and graph convolution layers.
5
6use crate::{CooTensor, CsrTensor, SparseTensor, TorshResult};
7
8use torsh_core::{Shape, TorshError};
9use torsh_tensor::{
10    creation::{randn, zeros},
11    Tensor,
12};
13
14/// Graph Convolutional Network (GCN) layer
15///
16/// Implements the GCN operation: H^(l+1) = σ(D^(-1/2) A D^(-1/2) H^(l) W^(l))
17/// where A is the adjacency matrix, D is the degree matrix, H is node features, W is weights
18pub struct GraphConvolution {
19    /// Weight matrix for feature transformation
20    weight: Tensor,
21    /// Optional bias vector
22    bias: Option<Tensor>,
23    /// Input feature dimension
24    in_features: usize,
25    /// Output feature dimension
26    out_features: usize,
27    /// Whether to add self-loops to adjacency matrix
28    add_self_loops: bool,
29    /// Whether to normalize adjacency matrix
30    normalize: bool,
31}
32
33impl GraphConvolution {
34    /// Create a new graph convolution layer
35    pub fn new(
36        in_features: usize,
37        out_features: usize,
38        use_bias: bool,
39        add_self_loops: bool,
40        normalize: bool,
41    ) -> TorshResult<Self> {
42        // Initialize weight matrix with Xavier/Glorot initialization
43        let _std_dev = (2.0 / (in_features + out_features) as f32).sqrt();
44        let weight = randn::<f32>(&[in_features, out_features])?;
45
46        // Initialize bias if requested
47        let bias = if use_bias {
48            Some(zeros::<f32>(&[out_features])?)
49        } else {
50            None
51        };
52
53        Ok(Self {
54            weight,
55            bias,
56            in_features,
57            out_features,
58            add_self_loops,
59            normalize,
60        })
61    }
62
63    /// Forward pass through the graph convolution layer
64    pub fn forward(&self, node_features: &Tensor, adjacency: &CsrTensor) -> TorshResult<Tensor> {
65        // Validate input dimensions
66        let feature_shape = node_features.shape();
67        if feature_shape.ndim() != 2 {
68            return Err(TorshError::InvalidArgument(
69                "Node features must be 2D tensor (num_nodes x in_features)".to_string(),
70            ));
71        }
72
73        let num_nodes = feature_shape.dims()[0];
74        let input_features = feature_shape.dims()[1];
75
76        if input_features != self.in_features {
77            return Err(TorshError::InvalidArgument(format!(
78                "Input features {} don't match layer input features {}",
79                input_features, self.in_features
80            )));
81        }
82
83        // Validate adjacency matrix
84        let adj_shape = adjacency.shape();
85        if adj_shape.dims() != [num_nodes, num_nodes] {
86            return Err(TorshError::InvalidArgument(
87                "Adjacency matrix must be square and match number of nodes".to_string(),
88            ));
89        }
90
91        // Prepare adjacency matrix (add self-loops if requested)
92        let adj_processed = if self.add_self_loops {
93            self.add_self_loops_to_adjacency(adjacency)?
94        } else {
95            adjacency.clone()
96        };
97
98        // Normalize adjacency matrix if requested
99        let adj_normalized = if self.normalize {
100            self.normalize_adjacency(&adj_processed)?
101        } else {
102            adj_processed
103        };
104
105        // Apply linear transformation: H * W
106        let transformed_features = zeros::<f32>(&[num_nodes, self.out_features])?;
107        for i in 0..num_nodes {
108            for j in 0..self.out_features {
109                let mut sum = 0.0;
110                for k in 0..self.in_features {
111                    sum += node_features.get(&[i, k])? * self.weight.get(&[k, j])?;
112                }
113                transformed_features.set(&[i, j], sum)?;
114            }
115        }
116
117        // Apply graph convolution: A_norm * (H * W)
118        let output = zeros::<f32>(&[num_nodes, self.out_features])?;
119        for i in 0..num_nodes {
120            let (neighbors, weights) = adj_normalized.get_row(i)?;
121            for j in 0..self.out_features {
122                let mut sum = 0.0;
123                for (&neighbor, &weight) in neighbors.iter().zip(weights.iter()) {
124                    sum += weight * transformed_features.get(&[neighbor, j])?;
125                }
126                output.set(&[i, j], sum)?;
127            }
128        }
129
130        // Add bias if present
131        if let Some(ref bias) = self.bias {
132            for i in 0..num_nodes {
133                for j in 0..self.out_features {
134                    let current = output.get(&[i, j])?;
135                    output.set(&[i, j], current + bias.get(&[j])?)?;
136                }
137            }
138        }
139
140        Ok(output)
141    }
142
143    /// Add self-loops to adjacency matrix
144    fn add_self_loops_to_adjacency(&self, adjacency: &CsrTensor) -> TorshResult<CsrTensor> {
145        let coo = adjacency.to_coo()?;
146        let mut triplets = coo.triplets();
147        let num_nodes = adjacency.shape().dims()[0];
148
149        // Add self-loops (diagonal entries with value 1.0)
150        let mut self_loop_set = std::collections::HashSet::new();
151        for (row, col, _) in &triplets {
152            if row == col {
153                self_loop_set.insert(*row);
154            }
155        }
156
157        // Add missing self-loops
158        for i in 0..num_nodes {
159            if !self_loop_set.contains(&i) {
160                triplets.push((i, i, 1.0));
161            }
162        }
163
164        let (row_indices, col_indices, values): (Vec<_>, Vec<_>, Vec<_>) =
165            triplets.into_iter().fold(
166                (Vec::new(), Vec::new(), Vec::new()),
167                |(mut rows, mut cols, mut vals), (r, c, v)| {
168                    rows.push(r);
169                    cols.push(c);
170                    vals.push(v);
171                    (rows, cols, vals)
172                },
173            );
174
175        let new_coo = CooTensor::new(row_indices, col_indices, values, adjacency.shape().clone())?;
176        CsrTensor::from_coo(&new_coo)
177    }
178
179    /// Normalize adjacency matrix using D^(-1/2) * A * D^(-1/2)
180    fn normalize_adjacency(&self, adjacency: &CsrTensor) -> TorshResult<CsrTensor> {
181        let num_nodes = adjacency.shape().dims()[0];
182
183        // Calculate degree for each node
184        let mut degrees = vec![0.0; num_nodes];
185        let coo = adjacency.to_coo()?;
186        let triplets = coo.triplets();
187
188        for (row, _col, val) in &triplets {
189            degrees[*row] += val;
190        }
191
192        // Calculate D^(-1/2)
193        let inv_sqrt_degrees: Vec<f32> = degrees
194            .iter()
195            .map(|&d| if d > 0.0 { 1.0 / d.sqrt() } else { 0.0 })
196            .collect();
197
198        // Apply normalization: D^(-1/2) * A * D^(-1/2)
199        let normalized_triplets: Vec<_> = triplets
200            .into_iter()
201            .map(|(row, col, val)| {
202                let normalized_val = inv_sqrt_degrees[row] * val * inv_sqrt_degrees[col];
203                (row, col, normalized_val)
204            })
205            .collect();
206
207        let (row_indices, col_indices, values): (Vec<_>, Vec<_>, Vec<_>) =
208            normalized_triplets.into_iter().fold(
209                (Vec::new(), Vec::new(), Vec::new()),
210                |(mut rows, mut cols, mut vals), (r, c, v)| {
211                    rows.push(r);
212                    cols.push(c);
213                    vals.push(v);
214                    (rows, cols, vals)
215                },
216            );
217
218        let normalized_coo =
219            CooTensor::new(row_indices, col_indices, values, adjacency.shape().clone())?;
220        CsrTensor::from_coo(&normalized_coo)
221    }
222
223    /// Get the number of parameters
224    pub fn num_parameters(&self) -> usize {
225        let weight_params = self.in_features * self.out_features;
226        let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
227        weight_params + bias_params
228    }
229
230    /// Get input feature dimension
231    pub fn in_features(&self) -> usize {
232        self.in_features
233    }
234
235    /// Get output feature dimension
236    pub fn out_features(&self) -> usize {
237        self.out_features
238    }
239}
240
241/// Sparse 2D Convolution layer
242///
243/// Implements 2D convolution operations optimized for sparse inputs and/or sparse kernels.
244/// This can significantly reduce computation when either the input or kernel has many zeros.
245pub struct SparseConv2d {
246    /// Convolution kernel in sparse format (out_channels, in_channels, kernel_height, kernel_width)
247    kernel: CsrTensor,
248    /// Optional bias vector (out_channels,)
249    bias: Option<Tensor>,
250    /// Number of input channels
251    in_channels: usize,
252    /// Number of output channels
253    out_channels: usize,
254    /// Kernel size (height, width)
255    kernel_size: (usize, usize),
256    /// Stride (height, width)
257    stride: (usize, usize),
258    /// Padding (height, width)
259    padding: (usize, usize),
260    /// Dilation (height, width)
261    dilation: (usize, usize),
262}
263
264impl SparseConv2d {
265    /// Create a new sparse 2D convolution layer
266    #[allow(clippy::too_many_arguments)]
267    pub fn new(
268        in_channels: usize,
269        out_channels: usize,
270        kernel_size: (usize, usize),
271        stride: Option<(usize, usize)>,
272        padding: Option<(usize, usize)>,
273        dilation: Option<(usize, usize)>,
274        sparsity: f32,
275        use_bias: bool,
276    ) -> TorshResult<Self> {
277        if !(0.0..=1.0).contains(&sparsity) {
278            return Err(TorshError::InvalidArgument(
279                "Sparsity must be between 0.0 and 1.0".to_string(),
280            ));
281        }
282
283        let stride = stride.unwrap_or((1, 1));
284        let padding = padding.unwrap_or((0, 0));
285        let dilation = dilation.unwrap_or((1, 1));
286
287        // Generate sparse kernel
288        let kernel =
289            Self::generate_sparse_kernel(out_channels, in_channels, kernel_size, sparsity)?;
290
291        // Generate bias if requested
292        let bias = if use_bias {
293            Some(zeros::<f32>(&[out_channels])?)
294        } else {
295            None
296        };
297
298        Ok(Self {
299            kernel,
300            bias,
301            in_channels,
302            out_channels,
303            kernel_size,
304            stride,
305            padding,
306            dilation,
307        })
308    }
309
310    /// Forward pass for sparse convolution
311    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
312        // Validate input shape: (batch_size, in_channels, height, width)
313        let input_shape = input.shape();
314        if input_shape.ndim() != 4 {
315            return Err(TorshError::InvalidArgument(
316                "Input must be 4D tensor (batch_size, in_channels, height, width)".to_string(),
317            ));
318        }
319
320        let batch_size = input_shape.dims()[0];
321        let input_channels = input_shape.dims()[1];
322        let input_height = input_shape.dims()[2];
323        let input_width = input_shape.dims()[3];
324
325        if input_channels != self.in_channels {
326            return Err(TorshError::InvalidArgument(format!(
327                "Input channels {} don't match layer input channels {}",
328                input_channels, self.in_channels
329            )));
330        }
331
332        // Calculate output dimensions
333        let output_height =
334            (input_height + 2 * self.padding.0 - self.dilation.0 * (self.kernel_size.0 - 1) - 1)
335                / self.stride.0
336                + 1;
337        let output_width =
338            (input_width + 2 * self.padding.1 - self.dilation.1 * (self.kernel_size.1 - 1) - 1)
339                / self.stride.1
340                + 1;
341
342        let mut output =
343            zeros::<f32>(&[batch_size, self.out_channels, output_height, output_width])?;
344
345        // Perform sparse convolution for each batch item
346        for b in 0..batch_size {
347            self.conv2d_single(
348                input,
349                &mut output,
350                b,
351                input_height,
352                input_width,
353                output_height,
354                output_width,
355            )?;
356        }
357
358        // Add bias if present
359        if let Some(ref bias) = self.bias {
360            for b in 0..batch_size {
361                for c in 0..self.out_channels {
362                    let bias_val = bias.get(&[c])?;
363                    for h in 0..output_height {
364                        for w in 0..output_width {
365                            let current = output.get(&[b, c, h, w])?;
366                            output.set(&[b, c, h, w], current + bias_val)?;
367                        }
368                    }
369                }
370            }
371        }
372
373        Ok(output)
374    }
375
376    /// Perform convolution for a single batch item
377    #[allow(clippy::too_many_arguments)]
378    fn conv2d_single(
379        &self,
380        input: &Tensor,
381        output: &mut Tensor,
382        batch_idx: usize,
383        input_height: usize,
384        input_width: usize,
385        output_height: usize,
386        output_width: usize,
387    ) -> TorshResult<()> {
388        // Get sparse kernel triplets (we need to interpret the flat CSR as 4D)
389        let kernel_coo = self.kernel.to_coo()?;
390        let kernel_triplets = kernel_coo.triplets();
391
392        // Perform sparse convolution by iterating over non-zero kernel elements
393        for (kernel_flat_idx, _, kernel_val) in kernel_triplets {
394            // Convert flat index back to 4D coordinates
395            let (out_c, in_c, kh, kw) = self.flat_to_4d_kernel(kernel_flat_idx);
396
397            // For each output position
398            for out_h in 0..output_height {
399                for out_w in 0..output_width {
400                    // Calculate input position
401                    let in_h = out_h * self.stride.0 + kh * self.dilation.0;
402                    let in_w = out_w * self.stride.1 + kw * self.dilation.1;
403
404                    // Apply padding offset
405                    if in_h >= self.padding.0 && in_w >= self.padding.1 {
406                        let padded_in_h = in_h - self.padding.0;
407                        let padded_in_w = in_w - self.padding.1;
408
409                        // Check bounds
410                        if padded_in_h < input_height && padded_in_w < input_width {
411                            let input_val =
412                                input.get(&[batch_idx, in_c, padded_in_h, padded_in_w])?;
413                            let current_out = output.get(&[batch_idx, out_c, out_h, out_w])?;
414                            output.set(
415                                &[batch_idx, out_c, out_h, out_w],
416                                current_out + kernel_val * input_val,
417                            )?;
418                        }
419                    }
420                }
421            }
422        }
423
424        Ok(())
425    }
426
427    /// Convert flat kernel index to 4D coordinates (out_c, in_c, kh, kw)
428    fn flat_to_4d_kernel(&self, flat_idx: usize) -> (usize, usize, usize, usize) {
429        let kernel_size_total = self.kernel_size.0 * self.kernel_size.1;
430        let channel_size = self.in_channels * kernel_size_total;
431
432        let out_c = flat_idx / channel_size;
433        let remaining = flat_idx % channel_size;
434        let in_c = remaining / kernel_size_total;
435        let remaining = remaining % kernel_size_total;
436        let kh = remaining / self.kernel_size.1;
437        let kw = remaining % self.kernel_size.1;
438
439        (out_c, in_c, kh, kw)
440    }
441
442    /// Convert 4D kernel coordinates to flat index
443    #[allow(dead_code)]
444    fn kernel_4d_to_flat(&self, out_c: usize, in_c: usize, kh: usize, kw: usize) -> usize {
445        let kernel_size_total = self.kernel_size.0 * self.kernel_size.1;
446        let channel_size = self.in_channels * kernel_size_total;
447
448        out_c * channel_size + in_c * kernel_size_total + kh * self.kernel_size.1 + kw
449    }
450
451    /// Generate sparse convolution kernel
452    fn generate_sparse_kernel(
453        out_channels: usize,
454        in_channels: usize,
455        kernel_size: (usize, usize),
456        sparsity: f32,
457    ) -> TorshResult<CsrTensor> {
458        let total_elements = out_channels * in_channels * kernel_size.0 * kernel_size.1;
459        let nnz = ((total_elements as f32) * (1.0 - sparsity)) as usize;
460
461        let mut row_indices = Vec::with_capacity(nnz);
462        let mut col_indices = Vec::with_capacity(nnz);
463        let mut values = Vec::with_capacity(nnz);
464
465        // Generate random sparse pattern
466        let mut positions = std::collections::HashSet::new();
467        while positions.len() < nnz {
468            let mut rng = scirs2_core::random::thread_rng();
469            let flat_idx = rng.gen_range(0..total_elements);
470            positions.insert(flat_idx);
471        }
472
473        // For CSR representation, we'll use a 2D matrix where:
474        // - rows represent output channels
475        // - columns represent (in_channels * kernel_height * kernel_width)
476        let kernel_size_total = kernel_size.0 * kernel_size.1;
477        let channel_size = in_channels * kernel_size_total;
478
479        for flat_idx in positions {
480            let out_c = flat_idx / channel_size;
481            let col_idx = flat_idx % channel_size; // This represents (in_c, kh, kw) flattened
482
483            row_indices.push(out_c);
484            col_indices.push(col_idx);
485
486            // He initialization for convolution layers
487            let fan_in = in_channels * kernel_size.0 * kernel_size.1;
488            let std_dev = (2.0 / fan_in as f32).sqrt();
489            let mut rng = scirs2_core::random::thread_rng();
490            values.push((rng.random::<f32>() * 2.0 - 1.0) * std_dev);
491        }
492
493        let shape = Shape::new(vec![out_channels, channel_size]);
494        let coo = CooTensor::new(row_indices, col_indices, values, shape)?;
495        CsrTensor::from_coo(&coo)
496    }
497
498    /// Get the number of parameters
499    pub fn num_parameters(&self) -> usize {
500        let kernel_params = self.kernel.nnz();
501        let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
502        kernel_params + bias_params
503    }
504
505    /// Get kernel sparsity
506    pub fn kernel_sparsity(&self) -> f32 {
507        let total_elements =
508            self.out_channels * self.in_channels * self.kernel_size.0 * self.kernel_size.1;
509        let nnz = self.kernel.nnz();
510        1.0 - (nnz as f32 / total_elements as f32)
511    }
512}
513
514/// Sparse 1D Convolution layer
515///
516/// Implements 1D convolution operations optimized for sparse inputs and/or sparse kernels.
517pub struct SparseConv1d {
518    /// Convolution kernel in sparse format (out_channels, in_channels, kernel_length)
519    kernel: CsrTensor,
520    /// Optional bias vector (out_channels,)
521    bias: Option<Tensor>,
522    /// Number of input channels
523    in_channels: usize,
524    /// Number of output channels
525    out_channels: usize,
526    /// Kernel size
527    kernel_size: usize,
528    /// Stride
529    stride: usize,
530    /// Padding
531    padding: usize,
532    /// Dilation
533    dilation: usize,
534}
535
536impl SparseConv1d {
537    /// Create a new sparse 1D convolution layer
538    #[allow(clippy::too_many_arguments)]
539    pub fn new(
540        in_channels: usize,
541        out_channels: usize,
542        kernel_size: usize,
543        stride: Option<usize>,
544        padding: Option<usize>,
545        dilation: Option<usize>,
546        sparsity: f32,
547        use_bias: bool,
548    ) -> TorshResult<Self> {
549        if !(0.0..=1.0).contains(&sparsity) {
550            return Err(TorshError::InvalidArgument(
551                "Sparsity must be between 0.0 and 1.0".to_string(),
552            ));
553        }
554
555        let stride = stride.unwrap_or(1);
556        let padding = padding.unwrap_or(0);
557        let dilation = dilation.unwrap_or(1);
558
559        // Generate sparse kernel
560        let kernel =
561            Self::generate_sparse_kernel_1d(out_channels, in_channels, kernel_size, sparsity)?;
562
563        // Generate bias if requested
564        let bias = if use_bias {
565            Some(zeros::<f32>(&[out_channels])?)
566        } else {
567            None
568        };
569
570        Ok(Self {
571            kernel,
572            bias,
573            in_channels,
574            out_channels,
575            kernel_size,
576            stride,
577            padding,
578            dilation,
579        })
580    }
581
582    /// Forward pass for sparse 1D convolution
583    pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
584        // Validate input shape: (batch_size, in_channels, length)
585        let input_shape = input.shape();
586        if input_shape.ndim() != 3 {
587            return Err(TorshError::InvalidArgument(
588                "Input must be 3D tensor (batch_size, in_channels, length)".to_string(),
589            ));
590        }
591
592        let batch_size = input_shape.dims()[0];
593        let input_channels = input_shape.dims()[1];
594        let input_length = input_shape.dims()[2];
595
596        if input_channels != self.in_channels {
597            return Err(TorshError::InvalidArgument(format!(
598                "Input channels {} don't match layer input channels {}",
599                input_channels, self.in_channels
600            )));
601        }
602
603        // Calculate output length
604        let output_length =
605            (input_length + 2 * self.padding - self.dilation * (self.kernel_size - 1) - 1)
606                / self.stride
607                + 1;
608
609        let mut output = zeros::<f32>(&[batch_size, self.out_channels, output_length])?;
610
611        // Perform sparse convolution for each batch item
612        for b in 0..batch_size {
613            self.conv1d_single(input, &mut output, b, input_length, output_length)?;
614        }
615
616        // Add bias if present
617        if let Some(ref bias) = self.bias {
618            for b in 0..batch_size {
619                for c in 0..self.out_channels {
620                    let bias_val = bias.get(&[c])?;
621                    for l in 0..output_length {
622                        let current = output.get(&[b, c, l])?;
623                        output.set(&[b, c, l], current + bias_val)?;
624                    }
625                }
626            }
627        }
628
629        Ok(output)
630    }
631
632    /// Perform 1D convolution for a single batch item
633    fn conv1d_single(
634        &self,
635        input: &Tensor,
636        output: &mut Tensor,
637        batch_idx: usize,
638        input_length: usize,
639        output_length: usize,
640    ) -> TorshResult<()> {
641        // Get sparse kernel triplets
642        let kernel_coo = self.kernel.to_coo()?;
643        let kernel_triplets = kernel_coo.triplets();
644
645        // Perform sparse convolution by iterating over non-zero kernel elements
646        for (kernel_flat_idx, _, kernel_val) in kernel_triplets {
647            // Convert flat index back to 3D coordinates
648            let (out_c, in_c, k_pos) = self.flat_to_3d_kernel(kernel_flat_idx);
649
650            // For each output position
651            for out_pos in 0..output_length {
652                // Calculate input position
653                let in_pos = out_pos * self.stride + k_pos * self.dilation;
654
655                // Apply padding offset
656                if in_pos >= self.padding {
657                    let padded_in_pos = in_pos - self.padding;
658
659                    // Check bounds
660                    if padded_in_pos < input_length {
661                        let input_val = input.get(&[batch_idx, in_c, padded_in_pos])?;
662                        let current_out = output.get(&[batch_idx, out_c, out_pos])?;
663                        output.set(
664                            &[batch_idx, out_c, out_pos],
665                            current_out + kernel_val * input_val,
666                        )?;
667                    }
668                }
669            }
670        }
671
672        Ok(())
673    }
674
675    /// Convert flat kernel index to 3D coordinates (out_c, in_c, k_pos)
676    fn flat_to_3d_kernel(&self, flat_idx: usize) -> (usize, usize, usize) {
677        let channel_size = self.in_channels * self.kernel_size;
678
679        let out_c = flat_idx / channel_size;
680        let remaining = flat_idx % channel_size;
681        let in_c = remaining / self.kernel_size;
682        let k_pos = remaining % self.kernel_size;
683
684        (out_c, in_c, k_pos)
685    }
686
687    /// Generate sparse 1D convolution kernel
688    fn generate_sparse_kernel_1d(
689        out_channels: usize,
690        in_channels: usize,
691        kernel_size: usize,
692        sparsity: f32,
693    ) -> TorshResult<CsrTensor> {
694        let total_elements = out_channels * in_channels * kernel_size;
695        let nnz = ((total_elements as f32) * (1.0 - sparsity)) as usize;
696
697        let mut row_indices = Vec::with_capacity(nnz);
698        let mut col_indices = Vec::with_capacity(nnz);
699        let mut values = Vec::with_capacity(nnz);
700
701        // Generate random sparse pattern
702        let mut positions = std::collections::HashSet::new();
703        while positions.len() < nnz {
704            let mut rng = scirs2_core::random::thread_rng();
705            let flat_idx = rng.gen_range(0..total_elements);
706            positions.insert(flat_idx);
707        }
708
709        // For CSR representation, we'll use a 2D matrix where:
710        // - rows represent output channels
711        // - columns represent (in_channels * kernel_size)
712        let channel_size = in_channels * kernel_size;
713
714        for flat_idx in positions {
715            let out_c = flat_idx / channel_size;
716            let col_idx = flat_idx % channel_size; // This represents (in_c, k_pos) flattened
717
718            row_indices.push(out_c);
719            col_indices.push(col_idx);
720
721            // He initialization for convolution layers
722            let fan_in = in_channels * kernel_size;
723            let std_dev = (2.0 / fan_in as f32).sqrt();
724            let mut rng = scirs2_core::random::thread_rng();
725            values.push((rng.random::<f32>() * 2.0 - 1.0) * std_dev);
726        }
727
728        let shape = Shape::new(vec![out_channels, channel_size]);
729        let coo = CooTensor::new(row_indices, col_indices, values, shape)?;
730        CsrTensor::from_coo(&coo)
731    }
732
733    /// Get the number of parameters
734    pub fn num_parameters(&self) -> usize {
735        let kernel_params = self.kernel.nnz();
736        let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
737        kernel_params + bias_params
738    }
739
740    /// Get kernel sparsity
741    pub fn kernel_sparsity(&self) -> f32 {
742        let total_elements = self.out_channels * self.in_channels * self.kernel_size;
743        let nnz = self.kernel.nnz();
744        1.0 - (nnz as f32 / total_elements as f32)
745    }
746}