22
33import json
44import random
5+ from functools import partial
6+ from multiprocessing import Pool , cpu_count
57from typing import Callable , List , Tuple
68
79import numpy as np
1012from ..optimizer import Optimizer , SGDOptimizer
1113
1214
15+ def _compute_neuron_forward (args ):
16+ """Compute forward pass for a single neuron."""
17+ weights , bias , inputs , activation_fn_name , is_softmax = args
18+ z = sum (w * inp for w , inp in zip (weights , inputs )) + bias
19+ return z
20+
21+
22+ def _compute_neuron_gradient (args ):
23+ """Compute gradients for a single neuron."""
24+ neuron_idx , weights , delta , prev_activations = args
25+ grad_w = [delta * a for a in prev_activations ]
26+ grad_b = delta
27+ return neuron_idx , grad_w , grad_b
28+
29+
30+ def _compute_neuron_backprop (args ):
31+ """Compute backpropagation delta for a single neuron in hidden layer."""
32+ neuron_idx , next_weights , next_deltas , activation , act_deriv = args
33+ # Sum contributions from all neurons in next layer
34+ s = sum (
35+ next_weights [j ][neuron_idx ] * next_deltas [j ]
36+ for j in range (len (next_deltas ))
37+ )
38+ delta = s * act_deriv (activation )
39+ return neuron_idx , delta
40+
41+
1342class Network :
1443 def __init__ (
1544 self ,
@@ -20,6 +49,7 @@ def __init__(
2049 optimizer : Optimizer | None = None ,
2150 class_weights : List [float ] | None = None ,
2251 seed : int | None = None ,
52+ n_processes : int | None = None ,
2353 ):
2454 if len (layer_sizes ) < 2 :
2555 raise ValueError ("Need at least input and output layer" )
@@ -32,6 +62,7 @@ def __init__(
3262 self .loss_fn = loss
3363 self .class_weights = class_weights
3464 self .optimizer = optimizer or SGDOptimizer (learning_rate = 0.3 )
65+ self .n_processes = n_processes or max (1 , cpu_count () - 1 )
3566
3667 if output_activation == "softmax" and layer_sizes [- 1 ] == 1 :
3768 raise ValueError ("Softmax requires >1 output neurons" )
@@ -93,7 +124,6 @@ def evaluate(
93124
94125 return correct / len (dataset )
95126
96- # Method stubs for dynamically attached methods (implemented in submodules)
97127 @staticmethod
98128 def get_activation (name : str ) -> Callable [[float ], float ]:
99129 """Get activation function by name."""
@@ -115,75 +145,73 @@ def get_activation_derivative(name: str) -> Callable[[float], float]:
115145 }[name ]
116146
117147 def forward (
118- self , inputs : List [float ]
148+ self , inputs : List [float ], pool : Pool | None = None
119149 ) -> Tuple [List [List [float ]], List [List [float ]]]:
120- """Forward pass through the network."""
150+ """Forward pass through the network with optional multiprocessing ."""
121151 if len (inputs ) != self .layer_sizes [0 ]:
122152 raise ValueError (
123153 f"Input size { len (inputs )} doesn't match network input { self .layer_sizes [0 ]} "
124154 )
125155
126- # Convert inputs to numpy array
127- a = np .array (inputs )
128-
129- # Store activations and z values for each layer
130- activations_list = [a ]
156+ activations_list = [inputs ]
131157 zs = []
158+ current_input = inputs
132159
133160 for layer_idx in range (len (self .weights )):
134- # Extract weight matrix and bias vector for current layer
135161 w_mat = self .weights [layer_idx ]
136162 b_vec = self .biases [layer_idx ]
163+ is_output = layer_idx == len (self .weights ) - 1
164+ is_softmax = is_output and self .output_activation == "softmax"
165+
166+ # Parallel computation of z values for all neurons in this layer
167+ if (
168+ pool is not None and len (w_mat ) > 10
169+ ): # Use MP for larger layers
170+ args = [
171+ (
172+ w_mat [i ],
173+ b_vec [i ],
174+ current_input ,
175+ (
176+ self .output_activation
177+ if is_output
178+ else self .hidden_activation
179+ ),
180+ is_softmax ,
181+ )
182+ for i in range (len (w_mat ))
183+ ]
184+ z_layer = pool .map (_compute_neuron_forward , args )
185+ else :
186+ z_layer = [
187+ sum (w * inp for w , inp in zip (w_mat [i ], current_input ))
188+ + b_vec [i ]
189+ for i in range (len (w_mat ))
190+ ]
137191
138- # Compute z values: z = W * a + b
139- z_layer = np .dot (w_mat , a ) + b_vec
140192 zs .append (z_layer )
141193
142- # Determine if it's the output layer
143- is_output = layer_idx == len (self .weights ) - 1
144-
145- # Activation function for output layer (e.g., softmax)
146- if is_output and self .output_activation == "softmax" :
147- a_next = self .softmax (z_layer )
194+ # Apply activation function
195+ if is_softmax :
196+ z_array = np .array (z_layer )
197+ exp_z = np .exp (z_array - np .max (z_array ))
198+ a_next = (exp_z / np .sum (exp_z )).tolist ()
148199 else :
149- # Use appropriate activation function for hidden layers
150200 act_fn = self .get_activation (
151201 self .output_activation
152202 if is_output
153203 else self .hidden_activation
154204 )
155- a_next = act_fn (z_layer )
205+ a_next = [ act_fn (z ) for z in z_layer ]
156206
157207 activations_list .append (a_next )
158- a = a_next
159-
160- # Convert activations back to list of lists if needed
161- activations_list = [a .tolist () for a in activations_list ]
162- zs = [z .tolist () for z in zs ]
208+ current_input = a_next
163209
164210 return activations_list , zs
165211
166- def softmax (self , z : np .ndarray ) -> np .ndarray :
167- """Softmax activation function."""
168- exp_z = np .exp (z - np .max (z )) # Numerical stability
169- return exp_z / np .sum (exp_z , axis = - 1 , keepdims = True )
170-
171- def get_activation (self , activation_type : str ):
172- """Returns the activation function based on the type."""
173- if activation_type == "sigmoid" :
174- return lambda x : 1 / (1 + np .exp (- x ))
175- elif activation_type == "tanh" :
176- return np .tanh
177- elif activation_type == "relu" :
178- return lambda x : np .maximum (0 , x )
179- else :
180- raise ValueError (
181- f"Activation function { activation_type } not recognized"
182- )
183-
184212 def predict (self , inputs : List [float ]) -> List [float ]:
185213 """Make a prediction for given inputs."""
186- activations_list , _ = self .forward (inputs )
214+ activations_list , _ = self .forward (inputs , pool = None )
187215 return activations_list [- 1 ]
188216
189217 def compute_output_delta (
@@ -212,52 +240,102 @@ def compute_output_delta(
212240 ]
213241
214242 def compute_hidden_deltas (
215- self , activations_list : List [List [float ]], output_delta : List [float ]
243+ self ,
244+ activations_list : List [List [float ]],
245+ output_delta : List [float ],
246+ pool : Pool | None = None ,
216247 ) -> List [List [float ]]:
217- """Backpropagate delta through hidden layers."""
248+ """Backpropagate delta through hidden layers with multiprocessing ."""
218249 deltas : List [List [float ]] = [
219250 [] for _ in range (len (self .layer_sizes ) - 1 )
220251 ]
221252 last_layer_idx = len (self .layer_sizes ) - 2
222253 deltas [last_layer_idx ] = output_delta
223254
224255 act_deriv = self .get_activation_derivative (self .hidden_activation )
256+
225257 for layer_idx in range (last_layer_idx - 1 , - 1 , - 1 ):
226- layer_deltas = []
227- for i in range (self .layer_sizes [layer_idx + 1 ]):
228- s = sum (
229- self .weights [layer_idx + 1 ][j ][i ]
230- * deltas [layer_idx + 1 ][j ]
231- for j in range (self .layer_sizes [layer_idx + 2 ])
232- )
233- a_val = activations_list [layer_idx + 1 ][i ]
234- layer_deltas .append (s * act_deriv (a_val ))
258+ layer_size = self .layer_sizes [layer_idx + 1 ]
259+ next_weights = self .weights [layer_idx + 1 ]
260+ next_deltas = deltas [layer_idx + 1 ]
261+ layer_activations = activations_list [layer_idx + 1 ]
262+
263+ # Parallel computation of deltas for all neurons in this layer
264+ if pool is not None and layer_size > 10 :
265+ args = [
266+ (
267+ i ,
268+ next_weights ,
269+ next_deltas ,
270+ layer_activations [i ],
271+ act_deriv ,
272+ )
273+ for i in range (layer_size )
274+ ]
275+ results = pool .map (_compute_neuron_backprop , args )
276+ # Sort by neuron index to maintain order
277+ results .sort (key = lambda x : x [0 ])
278+ layer_deltas = [delta for _ , delta in results ]
279+ else :
280+ layer_deltas = []
281+ for i in range (layer_size ):
282+ s = sum (
283+ next_weights [j ][i ] * next_deltas [j ]
284+ for j in range (len (next_deltas ))
285+ )
286+ layer_deltas .append (s * act_deriv (layer_activations [i ]))
287+
235288 deltas [layer_idx ] = layer_deltas
236289
237290 return deltas
238291
239292 def compute_gradients (
240- self , activations_list : List [List [float ]], deltas : List [List [float ]]
293+ self ,
294+ activations_list : List [List [float ]],
295+ deltas : List [List [float ]],
296+ pool : Pool | None = None ,
241297 ) -> Tuple [List [List [List [float ]]], List [List [float ]]]:
242- """Compute weight and bias gradients from deltas ."""
298+ """Compute weight and bias gradients with multiprocessing ."""
243299 grad_w = [
244300 [[0.0 for _ in row ] for row in layer ] for layer in self .weights
245301 ]
246302 grad_b = [[0.0 for _ in layer ] for layer in self .biases ]
247303
248304 for layer_idx in range (len (self .weights )):
249- for neuron_idx in range (len (self .weights [layer_idx ])):
250- for w_idx in range (len (self .weights [layer_idx ][neuron_idx ])):
251- grad_w [layer_idx ][neuron_idx ][w_idx ] = (
252- deltas [layer_idx ][neuron_idx ]
253- * activations_list [layer_idx ][w_idx ]
305+ layer_size = len (self .weights [layer_idx ])
306+ prev_activations = activations_list [layer_idx ]
307+
308+ # Parallel computation of gradients for all neurons in this layer
309+ if pool is not None and layer_size > 10 :
310+ args = [
311+ (
312+ i ,
313+ self .weights [layer_idx ][i ],
314+ deltas [layer_idx ][i ],
315+ prev_activations ,
254316 )
255- grad_b [layer_idx ][neuron_idx ] = deltas [layer_idx ][neuron_idx ]
317+ for i in range (layer_size )
318+ ]
319+ results = pool .map (_compute_neuron_gradient , args )
320+
321+ for neuron_idx , gw , gb in results :
322+ grad_w [layer_idx ][neuron_idx ] = gw
323+ grad_b [layer_idx ][neuron_idx ] = gb
324+ else :
325+ for neuron_idx in range (layer_size ):
326+ delta = deltas [layer_idx ][neuron_idx ]
327+ grad_w [layer_idx ][neuron_idx ] = [
328+ delta * a for a in prev_activations
329+ ]
330+ grad_b [layer_idx ][neuron_idx ] = delta
256331
257332 return grad_w , grad_b
258333
259334 def backprop_sample (
260- self , activations_list : List [List [float ]], target : List [float ]
335+ self ,
336+ activations_list : List [List [float ]],
337+ target : List [float ],
338+ pool : Pool | None = None ,
261339 ) -> Tuple [List [List [List [float ]]], List [List [float ]], float , bool ]:
262340 """Full backpropagation for a single sample."""
263341 output = activations_list [- 1 ]
@@ -275,10 +353,12 @@ def backprop_sample(
275353
276354 # Compute deltas
277355 output_delta = self .compute_output_delta (activations_list , target )
278- deltas = self .compute_hidden_deltas (activations_list , output_delta )
356+ deltas = self .compute_hidden_deltas (
357+ activations_list , output_delta , pool
358+ )
279359
280360 # Compute gradients
281- grad_w , grad_b = self .compute_gradients (activations_list , deltas )
361+ grad_w , grad_b = self .compute_gradients (activations_list , deltas , pool )
282362
283363 return grad_w , grad_b , loss , is_correct
284364
@@ -294,40 +374,43 @@ def train_epoch(
294374 total_correct = 0
295375 random .shuffle (dataset )
296376
297- for start in range (0 , len (dataset ), batch_size ):
298- batch = dataset [start : start + batch_size ]
377+ # Create process pool for the entire epoch
378+ with Pool (processes = self .n_processes ) as pool :
379+ for start in range (0 , len (dataset ), batch_size ):
380+ batch = dataset [start : start + batch_size ]
299381
300- acc_grad_w = [
301- [[0.0 for _ in row ] for row in layer ] for layer in self .weights
302- ]
303- acc_grad_b = [[0.0 for _ in layer ] for layer in self .biases ]
382+ acc_grad_w = [
383+ [[0.0 for _ in row ] for row in layer ]
384+ for layer in self .weights
385+ ]
386+ acc_grad_b = [[0.0 for _ in layer ] for layer in self .biases ]
304387
305- for x , target in batch :
306- activations_list , _ = self .forward (x )
307- grad_w , grad_b , loss , is_correct = self .backprop_sample (
308- activations_list , target
309- )
388+ for x , target in batch :
389+ activations_list , _ = self .forward (x , pool )
390+ grad_w , grad_b , loss , is_correct = self .backprop_sample (
391+ activations_list , target , pool
392+ )
393+
394+ total_loss += loss
395+ if is_correct :
396+ total_correct += 1
310397
311- total_loss += loss
312- if is_correct :
313- total_correct += 1
398+ for li in range (len (self .weights )):
399+ for ni in range (len (self .weights [li ])):
400+ for wi in range (len (self .weights [li ][ni ])):
401+ acc_grad_w [li ][ni ][wi ] += grad_w [li ][ni ][wi ]
402+ acc_grad_b [li ][ni ] += grad_b [li ][ni ]
314403
404+ bsz = len (batch )
315405 for li in range (len (self .weights )):
316406 for ni in range (len (self .weights [li ])):
317407 for wi in range (len (self .weights [li ][ni ])):
318- acc_grad_w [li ][ni ][wi ] += grad_w [li ][ni ][wi ]
319- acc_grad_b [li ][ni ] += grad_b [li ][ni ]
320-
321- bsz = len (batch )
322- for li in range (len (self .weights )):
323- for ni in range (len (self .weights [li ])):
324- for wi in range (len (self .weights [li ][ni ])):
325- acc_grad_w [li ][ni ][wi ] /= bsz
326- acc_grad_b [li ][ni ] /= bsz
327-
328- self .optimizer .update (
329- self .weights , self .biases , acc_grad_w , acc_grad_b
330- )
408+ acc_grad_w [li ][ni ][wi ] /= bsz
409+ acc_grad_b [li ][ni ] /= bsz
410+
411+ self .optimizer .update (
412+ self .weights , self .biases , acc_grad_w , acc_grad_b
413+ )
331414
332415 self .optimizer .decay_lr ()
333416
0 commit comments