Skip to content

Commit 32282be

Browse files
refactor: extract Trainable network class
1 parent fe25562 commit 32282be

5 files changed

Lines changed: 211 additions & 119 deletions

File tree

src/my_torch/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .network import Network
1+
from .network import Network, TrainableNetwork
22
from .optimizer import SGDOptimizer
33

4-
__all__ = ["Network", "SGDOptimizer"]
4+
__all__ = ("Network", "SGDOptimizer", "TrainableNetwork")

src/my_torch/network/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .core import Network
2+
from .trainable import TrainableNetwork
23

3-
__all__ = ["Network"]
4+
__all__ = ("Network", "TrainableNetwork")

src/my_torch/network/core.py

Lines changed: 48 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import json
44
import random
5-
from typing import Callable, List, Tuple
5+
import sys
6+
from pathlib import Path
7+
from typing import Callable, List, Self, Tuple
68

79
import numpy as np
810

@@ -13,19 +15,23 @@
1315
class Network:
1416
def __init__(
1517
self,
18+
weights,
19+
biases,
1620
layer_sizes: List[int],
1721
hidden_activation: str = "sigmoid",
1822
output_activation: str = "sigmoid",
1923
loss: str = "mse",
2024
optimizer: Optimizer | None = None,
2125
class_weights: List[float] | None = None,
22-
seed: int | None = None,
2326
):
2427
if len(layer_sizes) < 2:
2528
raise ValueError("Need at least input and output layer")
2629
if any(s <= 0 for s in layer_sizes):
2730
raise ValueError("All layer sizes must be > 0")
2831

32+
self.biases = biases
33+
self.weights = weights
34+
2935
self.layer_sizes = layer_sizes
3036
self.hidden_activation = hidden_activation
3137
self.output_activation = output_activation
@@ -38,26 +44,35 @@ def __init__(
3844
if loss == "ce" and output_activation not in ("softmax",):
3945
raise ValueError("Cross-entropy requires softmax output")
4046

47+
@classmethod
48+
def random_net(
49+
cls, layer_sizes: List[int], seed: int | None = None, **kwargs
50+
) -> Self:
4151
if seed is not None:
4252
random.seed(seed)
4353

44-
self.weights: List[List[List[float]]] = []
45-
self.biases: List[List[float]] = []
54+
weights: List[List[List[float]]] = []
55+
biases: List[List[float]] = []
4656

4757
for i in range(len(layer_sizes) - 1):
4858
in_size = layer_sizes[i]
4959
out_size = layer_sizes[i + 1]
5060

5161
scale = (2.0 / in_size) ** 0.5
52-
self.weights.append(
62+
weights.append(
5363
[
5464
[random.gauss(0, scale) for _ in range(in_size)]
5565
for _ in range(out_size)
5666
]
5767
)
58-
self.biases.append(
59-
[random.uniform(-0.1, 0.1) for _ in range(out_size)]
60-
)
68+
biases.append([random.uniform(-0.1, 0.1) for _ in range(out_size)])
69+
70+
return cls(
71+
layer_sizes=layer_sizes,
72+
weights=weights,
73+
biases=biases,
74+
**kwargs,
75+
)
6176

6277
def _compute_loss(self, output: List[float], target: List[float]) -> float:
6378
if self.loss_fn == "mse":
@@ -336,43 +351,6 @@ def train_epoch(
336351
accuracy = total_correct / m if m else 0.0
337352
return avg_loss, accuracy
338353

339-
def train(
340-
self,
341-
dataset: List[Tuple[List[float], List[float]]],
342-
epochs: int = 1000,
343-
target_accuracy: float = 1.0,
344-
batch_size: int = 32,
345-
validation_data: List[Tuple[List[float], List[float]]] | None = None,
346-
verbose: bool = True,
347-
) -> List[Tuple[int, float, float, float]]:
348-
history: List[Tuple[int, float, float, float]] = []
349-
350-
for epoch in range(1, epochs + 1):
351-
loss, acc = self.train_epoch(dataset, batch_size=batch_size)
352-
353-
val_acc = 0.0
354-
if validation_data:
355-
val_acc = self.evaluate(validation_data)
356-
357-
history.append((epoch, loss, acc, val_acc))
358-
359-
if verbose:
360-
val_str = (
361-
f" val_acc={val_acc*100:.1f}%" if validation_data else ""
362-
)
363-
print(
364-
f"Epoch {epoch}: loss={loss:.4f} train_acc={acc*100:.1f}%{val_str}"
365-
)
366-
367-
if acc >= target_accuracy:
368-
if verbose:
369-
print(
370-
f"Reached target accuracy {target_accuracy*100:.1f}% at epoch {epoch}"
371-
)
372-
break
373-
374-
return history
375-
376354
def to_dict(self) -> dict:
377355
return {
378356
"version": "1.0",
@@ -410,7 +388,9 @@ def from_dict(cls, data: dict):
410388
learning_rate=opt_data.get("learning_rate", 0.3)
411389
)
412390

413-
net = cls(
391+
return cls(
392+
weights=params["weights"],
393+
biases=params["biases"],
414394
layer_sizes=arch["layer_sizes"],
415395
hidden_activation=arch.get("hidden_activation", "sigmoid"),
416396
output_activation=arch.get("output_activation", "sigmoid"),
@@ -419,13 +399,27 @@ def from_dict(cls, data: dict):
419399
class_weights=hyper.get("class_weights"),
420400
)
421401

422-
net.weights = params["weights"]
423-
net.biases = params["biases"]
402+
@classmethod
403+
def load(cls, filepath: Path) -> Self | None:
404+
try:
405+
print(f"Loading network from {filepath}...", file=sys.stderr)
406+
raw_config = filepath.read_text()
407+
408+
except FileNotFoundError:
409+
print(
410+
f"Error: Network file '{filepath}' not found", file=sys.stderr
411+
)
412+
return None
413+
except Exception as e:
414+
print(f"Error loading network: {e}", file=sys.stderr)
415+
return None
424416

425-
return net
417+
data = json.loads(raw_config)
418+
self = cls.from_dict(data)
426419

427-
@classmethod
428-
def load(cls, filepath: str):
429-
with open(filepath, "r", encoding="utf-8") as f:
430-
data = json.load(f)
431-
return cls.from_dict(data)
420+
if self is None:
421+
print("Failed to load the network", file=sys.stderr)
422+
return None
423+
424+
print(f"Loaded network: {self.layer_sizes}", file=sys.stderr)
425+
return self

src/my_torch/network/trainable.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from __future__ import annotations
2+
3+
from typing import List, Tuple
4+
5+
from ..optimizer import Optimizer, SGDOptimizer
6+
from .core import Network
7+
8+
9+
class TrainableNetwork(Network):
10+
11+
def __init__(
12+
self,
13+
weights,
14+
biases,
15+
layer_sizes: List[int],
16+
hidden_activation: str = "sigmoid",
17+
output_activation: str = "sigmoid",
18+
loss: str = "mse",
19+
optimizer: Optimizer | None = None,
20+
class_weights: List[float] | None = None,
21+
):
22+
super().__init__(
23+
weights,
24+
biases,
25+
layer_sizes,
26+
hidden_activation,
27+
output_activation,
28+
loss,
29+
optimizer,
30+
class_weights,
31+
)
32+
33+
self.optimizer: Optimizer = optimizer or SGDOptimizer(
34+
learning_rate=0.3
35+
)
36+
37+
def train(
38+
self,
39+
dataset: List[Tuple[List[float], List[float]]],
40+
epochs: int = 1000,
41+
target_accuracy: float = 1.0,
42+
batch_size: int = 32,
43+
validation_data: List[Tuple[List[float], List[float]]] | None = None,
44+
verbose: bool = True,
45+
) -> List[Tuple[int, float, float, float]]:
46+
history: List[Tuple[int, float, float, float]] = []
47+
48+
for epoch in range(1, epochs + 1):
49+
loss, acc = self.train_epoch(dataset, batch_size=batch_size)
50+
51+
val_acc = 0.0
52+
if validation_data:
53+
val_acc = self.evaluate(validation_data)
54+
55+
history.append((epoch, loss, acc, val_acc))
56+
57+
if verbose:
58+
val_str = (
59+
f" val_acc={val_acc*100:.1f}%" if validation_data else ""
60+
)
61+
print(
62+
f"Epoch {epoch}: loss={loss:.4f} train_acc={acc*100:.1f}%{val_str}"
63+
)
64+
65+
if acc >= target_accuracy:
66+
if verbose:
67+
print(
68+
f"Reached target accuracy {target_accuracy*100:.1f}% at epoch {epoch}"
69+
)
70+
break
71+
72+
return history
73+
74+
@classmethod
75+
def from_dict(cls, data: dict):
76+
arch = data["architecture"]
77+
hyper = data.get("hyperparameters", {})
78+
params = data["parameters"]
79+
80+
opt_data = hyper.get("optimizer", {})
81+
if opt_data.get("type") == "sgd":
82+
optimizer = SGDOptimizer.from_dict(opt_data)
83+
else:
84+
optimizer = SGDOptimizer(
85+
learning_rate=opt_data.get("learning_rate", 0.3)
86+
)
87+
88+
net = cls(
89+
weights=params["weights"],
90+
biases=params["biases"],
91+
layer_sizes=arch["layer_sizes"],
92+
hidden_activation=arch.get("hidden_activation", "sigmoid"),
93+
output_activation=arch.get("output_activation", "sigmoid"),
94+
loss=hyper.get("loss", "mse"),
95+
optimizer=optimizer,
96+
class_weights=hyper.get("class_weights"),
97+
)
98+
99+
return net

0 commit comments

Comments
 (0)