1212from datetime import timedelta
1313import pickle
1414import os
15+ import numpy as np
16+
17+ try :
18+ from numba import njit
19+ NUMBA_AVAILABLE = True
20+ except ImportError :
21+ NUMBA_AVAILABLE = False
22+
23+ def njit (* args , ** kwargs ):
24+ """Identity decorator fallback when Numba is not installed."""
25+ def decorator (func ):
26+ return func
27+ if len (args ) == 1 and callable (args [0 ]):
28+ return args [0 ]
29+ return decorator
1530
1631# for unit tests
1732from numpy .testing import assert_almost_equal , assert_array_almost_equal
@@ -72,6 +87,48 @@ def fast_norm(x):
7287 return sqrt (dot (x , x .T ))
7388
7489
90+ @njit (fastmath = True , cache = True )
91+ def _accumulate_batch (data , weights , bmu_indices , xx , yy , sigma ):
92+ """JIT-compiled accumulation of numerator/denominator
93+ for the batch SOM update (Gaussian neighborhood only).
94+
95+ Parameters
96+ ----------
97+ data : 2D float64 array (n_samples, input_len)
98+ weights : 3D float64 array (map_x, map_y, input_len)
99+ bmu_indices : 1D int64 array (n_samples,) — flat BMU index per sample
100+ xx, yy : 2D float64 arrays — meshgrid coordinates
101+ sigma : float64
102+
103+ Returns
104+ -------
105+ numerator : 3D array (map_x, map_y, input_len)
106+ denominator : 2D array (map_x, map_y)
107+ """
108+ map_x , map_y , input_len = weights .shape
109+ numerator = np .zeros ((map_x , map_y , input_len ))
110+ denominator = np .zeros ((map_x , map_y ))
111+ d = 2.0 * sigma * sigma
112+
113+ for s in range (data .shape [0 ]):
114+ bi = bmu_indices [s ] // map_y
115+ bj = bmu_indices [s ] % map_y
116+
117+ cx = xx [bj , bi ]
118+ cy = yy [bj , bi ]
119+
120+ for i in range (map_x ):
121+ for j in range (map_y ):
122+ dx = xx [j , i ] - cx
123+ dy = yy [j , i ] - cy
124+ g = np .exp (- (dx * dx + dy * dy ) / d )
125+ denominator [i , j ] += g
126+ for k in range (input_len ):
127+ numerator [i , j , k ] += g * data [s , k ]
128+
129+ return numerator , denominator
130+
131+
75132class MiniSom (object ):
76133 Y_HEX_CONV_FACTOR = (3.0 / 2.0 ) / sqrt (3 )
77134
@@ -595,6 +652,7 @@ def train_batch_offline(self, data, num_iteration, verbose=False):
595652 """
596653 self ._check_iteration_number (num_iteration )
597654 self ._check_input_len (data )
655+ data = array (data , dtype = float )
598656 iterations = range (num_iteration )
599657 if verbose :
600658 iterations = _wrap_index__in_verbose (iterations )
@@ -608,19 +666,29 @@ def train_batch_offline(self, data, num_iteration, verbose=False):
608666 iteration ,
609667 num_iteration )
610668
611- # Initialize accumulators
612- numerator = zeros_like (self ._weights )
613- denominator = zeros ((self ._weights .shape [0 ],
614- self ._weights .shape [1 ]))
615-
616- # Process all samples
617- for sample in data :
618- bmu = self .winner (sample )
619- g = self .neighborhood (bmu , sigma )
620- # Vectorized accumulation
621- g_expanded = g [:, :, newaxis ]
622- numerator += g_expanded * sample
623- denominator += g
669+ if NUMBA_AVAILABLE :
670+ # Fast path: vectorized BMU search + Numba JIT accumulation
671+ dists = self ._distance_from_weights (data )
672+ bmu_indices = argmin (dists , axis = 1 )
673+
674+ numerator , denominator = _accumulate_batch (
675+ data , self ._weights ,
676+ bmu_indices .astype (np .int64 ),
677+ self ._xx .astype (np .float64 ),
678+ self ._yy .astype (np .float64 ),
679+ float (sigma ))
680+ else :
681+ # Fallback: original pure-NumPy implementation
682+ numerator = zeros_like (self ._weights )
683+ denominator = zeros ((self ._weights .shape [0 ],
684+ self ._weights .shape [1 ]))
685+
686+ for sample in data :
687+ bmu = self .winner (sample )
688+ g = self .neighborhood (bmu , sigma )
689+ g_expanded = g [:, :, newaxis ]
690+ numerator += g_expanded * sample
691+ denominator += g
624692
625693 # Batch update with safety check
626694 denominator_safe = where (denominator [:, :, newaxis ] > 0 ,
0 commit comments