Skip to content

Commit ede326e

Browse files
CizniaSigmanificient
authored andcommitted
!
1 parent 798522d commit ede326e

1 file changed

Lines changed: 175 additions & 92 deletions

File tree

src/my_torch/network/core.py

Lines changed: 175 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import json
44
import random
5+
from functools import partial
6+
from multiprocessing import Pool, cpu_count
57
from typing import Callable, List, Tuple
68

79
import numpy as np
@@ -10,6 +12,33 @@
1012
from ..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+
1342
class 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

Comments
 (0)