zeroth-learn 0.2.1__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {zeroth_learn-0.2.1/zeroth_learn.egg-info → zeroth_learn-0.2.3}/PKG-INFO +1 -1
  2. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/pyproject.toml +1 -1
  3. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/abstract/__init__.py +1 -1
  4. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/abstract/model.py +60 -31
  5. zeroth_learn-0.2.3/zeroth/abstract/neural_network.py +20 -0
  6. zeroth_learn-0.2.3/zeroth/abstract/summary.py +56 -0
  7. zeroth_learn-0.2.3/zeroth/all.py +12 -0
  8. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/data.py +5 -1
  9. zeroth_learn-0.2.3/zeroth/experiment.py +123 -0
  10. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/first_order/layer.py +1 -1
  11. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/first_order/model.py +7 -5
  12. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/first_order/neural_network.py +9 -7
  13. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/first_order/optimizers.py +1 -4
  14. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/plot_losses.py +22 -19
  15. zeroth_learn-0.2.3/zeroth/utils/__init__.py +4 -0
  16. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/utils/dataclasses_utils.py +10 -2
  17. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/zeroth_order/model.py +7 -5
  18. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/zeroth_order/neural_network/neural_network.py +9 -6
  19. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/zeroth_order/neural_network/parameter_manager.py +1 -1
  20. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/zeroth_order/optimizers.py +1 -2
  21. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3/zeroth_learn.egg-info}/PKG-INFO +1 -1
  22. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth_learn.egg-info/SOURCES.txt +1 -0
  23. zeroth_learn-0.2.1/zeroth/abstract/neural_network.py +0 -26
  24. zeroth_learn-0.2.1/zeroth/abstract/summary.py +0 -13
  25. zeroth_learn-0.2.1/zeroth/experiment.py +0 -151
  26. zeroth_learn-0.2.1/zeroth/zeroth_order/neural_network/__init__.py +0 -0
  27. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/MANIFEST.in +0 -0
  28. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/README.md +0 -0
  29. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/setup.cfg +0 -0
  30. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/__init__.py +0 -0
  31. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/abstract/activation.py +0 -0
  32. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/abstract/blackbox.py +0 -0
  33. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/abstract/data_creator.py +0 -0
  34. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/abstract/loss.py +0 -0
  35. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/abstract/metric.py +0 -0
  36. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/abstract/optimizer.py +0 -0
  37. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/abstract/perturbation_matrix.py +0 -0
  38. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/first_order/__init__.py +0 -0
  39. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/losses/__init__.py +0 -0
  40. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/losses/cross_entropy.py +0 -0
  41. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/losses/mse.py +0 -0
  42. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/paths.py +0 -0
  43. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/types.py +0 -0
  44. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/utils/activation_functions.py +0 -0
  45. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/utils/metrics.py +0 -0
  46. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/utils/perturbation_matrices.py +0 -0
  47. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/zeroth_order/__init__.py +0 -0
  48. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/zeroth_order/gradient_estimators.py +0 -0
  49. {zeroth_learn-0.2.1/zeroth/utils → zeroth_learn-0.2.3/zeroth/zeroth_order/neural_network}/__init__.py +0 -0
  50. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth/zeroth_order/zeroth_order_blackbox.py +0 -0
  51. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth_learn.egg-info/dependency_links.txt +0 -0
  52. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth_learn.egg-info/requires.txt +0 -0
  53. {zeroth_learn-0.2.1 → zeroth_learn-0.2.3}/zeroth_learn.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zeroth-learn
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Requires-Dist: numpy
5
5
  Requires-Dist: pandas
6
6
  Requires-Dist: matplotlib
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zeroth-learn"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  dependencies = [
9
9
  "numpy",
10
10
  "pandas",
@@ -4,7 +4,7 @@ from .data_creator import DataCreator
4
4
  from .loss import Loss
5
5
  from .metric import Metric
6
6
  from .model import Model, ModelConfig
7
- from .neural_network import NeuralNetworkConfig, LayerConfig, NeuralNetwork
7
+ from .neural_network import NeuralNetworkConfig, NeuralNetwork
8
8
  from .optimizer import Optimizer
9
9
  from .perturbation_matrix import PerturbationMatrix
10
10
  from .summary import Summary
@@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
7
7
  from dataclasses import dataclass
8
8
  from typing import Callable
9
9
 
10
+ import matplotlib.pyplot as plt
10
11
  import numpy as np
11
12
  import pandas as pd
12
13
 
@@ -17,9 +18,37 @@ from .summary import Summary
17
18
  from ..data import Data
18
19
  from ..plot_losses import plot_losses
19
20
  from ..types import Array
20
- from ..utils.dataclasses_utils import config_serializer
21
21
 
22
22
 
23
+ @dataclass
24
+ class ModelRecord:
25
+ name: str
26
+ id: dict
27
+ training_loss: Array
28
+
29
+ @classmethod
30
+ def load(cls, model_dir_path: str):
31
+ config_path = os.path.join(model_dir_path, "config.json")
32
+ with open(config_path, "r") as f:
33
+ config = json.load(f)
34
+
35
+ loss_path = os.path.join(model_dir_path, LOSS_FILE)
36
+ df_loss = pd.read_csv(loss_path)
37
+
38
+ return cls(
39
+ id=config.get("id", {}),
40
+ name=config.get("name", "Unknown"),
41
+ training_loss=df_loss["training_loss"].values
42
+ )
43
+
44
+ def plot_loss(self, smooth_fraction: float = 0.05) -> plt.Figure:
45
+ fig = plot_losses(dimension=0,
46
+ models=[self],
47
+ title=self.name,
48
+ smooth_fraction=smooth_fraction)
49
+ plt.close(fig)
50
+ return fig
51
+
23
52
  @dataclass(frozen=True, kw_only=True)
24
53
  class ModelConfig(ABC, Summary):
25
54
  """
@@ -38,11 +67,11 @@ class ModelConfig(ABC, Summary):
38
67
  nb_epochs: int = 1
39
68
 
40
69
  @abstractmethod
41
- def instantiate(self) -> Model:
70
+ def instantiate(self, data: Data) -> Model:
42
71
  ...
43
72
 
44
73
 
45
- class Model(ABC):
74
+ class Model(ABC, ModelRecord):
46
75
  """
47
76
  Base class orchestrating the training and testing loop.
48
77
 
@@ -50,14 +79,19 @@ class Model(ABC):
50
79
  regardless of the underlying engine (Backpropagation or zeroth_order).
51
80
  """
52
81
 
82
+ LOSS_FILE: str = "training_loss.csv"
83
+ WEIGHTS_FILE: str = "weights.pkl"
84
+ CONFIG_FILE: str = "config.json"
85
+
53
86
  neural_network: BlackBox
54
87
  optimizer: Optimizer
55
88
 
56
- def __init__(self, config: ModelConfig):
89
+ def __init__(self, config: ModelConfig, data: Data):
57
90
  self.config = config
58
91
 
59
92
  self.name: str = config.name
60
93
  self.id: dict = config.id
94
+ self.data: Data = data
61
95
  self.loss: Loss = config.loss
62
96
  self.metric: Callable = config.metric
63
97
  self.batch_size: int = config.batch_size
@@ -67,11 +101,10 @@ class Model(ABC):
67
101
  self.test_loss: float = float("nan")
68
102
  self.test_accuracy: float = float("nan")
69
103
 
70
- def train(self, data: Data, nb_print: int = 0) -> None:
104
+ def train(self, nb_print: int = 0) -> None:
71
105
  """Runs the training loop over the dataset.
72
106
 
73
107
  Args:
74
- data (Data): The dataset object containing train/test sets.
75
108
  nb_print (int): Number of progress updates to print per epoch.
76
109
 
77
110
  Returns:
@@ -79,30 +112,29 @@ class Model(ABC):
79
112
  """
80
113
  print(f" Training {self.id} Model")
81
114
 
82
- data.batch_size = self.batch_size
83
- nb_batches = len(data)
115
+ self.data.batch_size = self.batch_size
116
+ nb_batches = len(self.data)
84
117
 
85
118
  self.training_loss = np.zeros(self.nb_epochs * nb_batches, dtype=np.float64)
119
+
120
+ nb_print = nb_batches if nb_print == -1 else nb_print
86
121
  print_indexes = np.linspace(0, nb_batches - 1, nb_print).astype(int)
87
122
 
88
123
  for epoch_idx in range(self.nb_epochs):
89
124
  print(f" epoch n°{epoch_idx + 1} out of {self.nb_epochs}")
90
- data.permutation()
91
- data.batch_size = self.batch_size
92
- for batch_idx, (X_train, Y_train) in enumerate(data):
125
+ self.data.permutation()
126
+ self.data.batch_size = self.batch_size
127
+ for batch_idx, (X_train, Y_train) in enumerate(self.data):
93
128
  avg_loss = self.optimizer.do_descent(self.neural_network, self.loss, X_train, Y_train)
94
129
  self.training_loss[epoch_idx * nb_batches + batch_idx] = avg_loss
95
130
 
96
131
  if batch_idx in print_indexes:
97
132
  print(f" batch n°{batch_idx + 1} out of {nb_batches}, "
98
133
  f"loss : {np.round(self.training_loss[epoch_idx * nb_batches + batch_idx], 3)}")
99
- self.test(data)
100
-
101
- def plot_loss(self, save_path: str = None, smooth_fraction: float = 0) -> None:
102
- plot_losses(dimension=0, models=[self], title=self.name, save_path=save_path, smooth_fraction=smooth_fraction)
134
+ self.test()
103
135
 
104
- def test(self, data: Data) -> None:
105
- X_test, Y_true = data.X_test, data.Y_test # (in, batch), (out, batch)
136
+ def test(self) -> None:
137
+ X_test, Y_true = self.data.X_test, self.data.Y_test # (in, batch), (out, batch)
106
138
  Y_pred = self.neural_network(X_test) # (out, batch)
107
139
 
108
140
  self.test_accuracy = self.metric(Y_pred, Y_true)
@@ -110,26 +142,23 @@ class Model(ABC):
110
142
 
111
143
  print(f" {self.id} accuracy : {self.test_accuracy}, loss : {self.test_loss}")
112
144
 
113
- def save_weights(self, save_path: str) -> None:
114
- os.makedirs(os.path.dirname(save_path), exist_ok=True)
115
- params_dict = self.neural_network.get_params()
116
- with open(save_path, 'wb') as f:
117
- pickle.dump(params_dict, f)
118
-
119
- def save_config(self, save_path: str) -> None:
120
- os.makedirs(os.path.dirname(save_path), exist_ok=True)
121
- with open(save_path, "w") as f:
122
- json.dump(self.config, f, default=config_serializer, indent=4)
123
-
124
- def save_loss(self, save_path: str) -> None:
125
- os.makedirs(os.path.dirname(save_path), exist_ok=True)
145
+ def save_loss(self, save_dir: str) -> None:
146
+ save_path = os.path.join(save_dir, self.LOSS_FILE)
147
+ os.makedirs(os.path.dirname(save_dir), exist_ok=True)
126
148
  df = pd.DataFrame({
127
149
  'training_loss': self.training_loss
128
150
  })
129
151
  df.to_csv(save_path)
130
152
 
131
- def load_weights(self, load_path: str) -> None:
153
+ def save_weights(self, save_dir: str) -> None:
154
+ save_path = os.path.join(save_dir, self.WEIGHTS_FILE)
155
+ params_dict = self.neural_network.get_params()
156
+ with open(save_path, 'wb') as f:
157
+ pickle.dump(params_dict, f)
158
+
159
+ def load_weights(self, load_dir: str) -> None:
132
160
  """Restaure les paramètres depuis un fichier pickle."""
161
+ load_path = os.path.join(load_dir, self.WEIGHTS_FILE)
133
162
  with open(load_path, 'rb') as f:
134
163
  params_dict = pickle.load(f)
135
164
 
@@ -0,0 +1,20 @@
1
+ from abc import ABC
2
+ from dataclasses import dataclass
3
+
4
+ from .activation import Activation
5
+ from .summary import Summary
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class NeuralNetworkConfig(Summary):
10
+ name: str
11
+ hidden_dims: list[int]
12
+ activations: list[Activation]
13
+
14
+
15
+ class NeuralNetwork(ABC):
16
+ def __init__(self, config: NeuralNetworkConfig, input_dim: int, output_dim: int):
17
+ self.name: str = config.name
18
+ self.nb_layers: int = len(config.activations)
19
+ self.input_dim: int = input_dim
20
+ self.output_dim: int = output_dim
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import os
5
+ from typing import Any
6
+
7
+
8
+ class Summary:
9
+ def summary(self, file=None) -> None:
10
+ print(self._summary(self, indent=0), file=file)
11
+
12
+ def save(self, save_dir: str) -> None:
13
+
14
+ save_path = os.path.join(save_dir, "config.txt")
15
+
16
+ if os.path.dirname(save_path):
17
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
18
+
19
+ with open(save_path, "w", encoding="utf-8") as f:
20
+ print(self._summary(self, indent=0), file=f)
21
+
22
+ @classmethod
23
+ def load(cls, path: str, context: dict = None) -> Summary:
24
+ with open(path, "r") as f:
25
+ code = "".join(l for l in f)
26
+ return eval(code.strip(), {"__builtins__": __builtins__}, context)
27
+
28
+ def _summary(self, obj: Any, indent: int) -> str:
29
+ shift = " " * indent
30
+ next_shift = " " * (indent + 1)
31
+
32
+ if dataclasses.is_dataclass(obj):
33
+ cls_name = obj.__class__.__name__
34
+ items = []
35
+ for f in dataclasses.fields(obj):
36
+ val = getattr(obj, f.name)
37
+ formatted_val = self._summary(val, indent + 1)
38
+ items.append(f"{next_shift}{f.name}={formatted_val.lstrip()}")
39
+
40
+ content = ",\n".join(items)
41
+ return f"{cls_name}(\n{content}\n{shift})"
42
+
43
+ elif isinstance(obj, list):
44
+ if not obj: return "[]"
45
+ items = [self._summary(item, indent + 1) for item in obj]
46
+ content = ",\n".join([f"{next_shift}{item.lstrip()}" for item in items])
47
+ return f"[\n{content}\n{shift}]"
48
+
49
+ elif isinstance(obj, dict):
50
+ if not obj: return "{}"
51
+ items = [f"{next_shift}{repr(k)}: {self._summary(v, indent + 1).lstrip()}" for k, v in obj.items()]
52
+ content = ",\n".join(items)
53
+ return f"{{\n{content}\n{shift}}}"
54
+
55
+ else:
56
+ return repr(obj)
@@ -0,0 +1,12 @@
1
+ # noinspection PyUnresolvedReferences
2
+ from .losses import *
3
+ # noinspection PyUnresolvedReferences
4
+ from .abstract import *
5
+ # noinspection PyUnresolvedReferences
6
+ from .zeroth_order import *
7
+ # noinspection PyUnresolvedReferences
8
+ from .first_order import *
9
+ # noinspection PyUnresolvedReferences
10
+ from .experiment import *
11
+ # noinspection PyUnresolvedReferences
12
+ from .utils import *
@@ -6,13 +6,17 @@ from .types import Array
6
6
 
7
7
 
8
8
  class Data:
9
- def __init__(self, raw_X_train: Array, raw_Y_train: Array, raw_X_test: Array, raw_Y_test: Array) -> None:
9
+ def __init__(self, raw_X_train: Array, raw_Y_train: Array, raw_X_test: Array, raw_Y_test: Array,
10
+ nb_class: int = 0) -> None:
10
11
  self.raw_X_train: Array = raw_X_train
11
12
  self.raw_Y_train: Array = raw_Y_train
12
13
  self.X_test: Array = raw_X_test
13
14
  self.Y_test: Array = raw_Y_test
14
15
 
15
16
  self.nb_data: int = raw_X_train.shape[0]
17
+ self.input_dim: int = self.raw_X_train.shape[1]
18
+ self.output_dim: int = self.raw_Y_train.shape[1] if nb_class == 0 else nb_class
19
+
16
20
  self.batch_size: int | None = None
17
21
 
18
22
  self.indices: np.ndarray = np.arange(self.nb_data)
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import os
5
+ from dataclasses import dataclass, replace
6
+ from typing import Union
7
+
8
+ import matplotlib.pyplot as plt
9
+ import pandas as pd
10
+
11
+ from .abstract import Model, ModelConfig, DataCreator, Summary
12
+ from .data import Data
13
+ from .plot_losses import plot_losses
14
+ from .utils.dataclasses_utils import get_name, set_value_by_path
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class VariationConfig:
19
+ name: str
20
+ param: list[str]
21
+ values: Union[list, list[list]]
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class ExperimentConfig(Summary):
26
+ name: str
27
+ base_model: ModelConfig
28
+ data_creator: DataCreator
29
+ variations: list[VariationConfig]
30
+
31
+
32
+ def instantiate(self) -> Experiment:
33
+ return Experiment(self)
34
+
35
+
36
+ class Experiment:
37
+ """Manages the full lifecycle of a deep learning experiment.
38
+
39
+ It handles data loading, model instantiation, training loops, and results visualization.
40
+
41
+ Attributes:
42
+ name (str): Name of the experiment
43
+ models (list[Model]): List of models to train/compare
44
+ data (Data): The dataset wrapper.
45
+ """
46
+
47
+ ACCURACY_FILE: str = "models_accuracy.csv"
48
+ CONFIG_FILE: str = "config.txt"
49
+
50
+ def __init__(self, config: ExperimentConfig) -> None:
51
+ self.config = config
52
+ self.name: str = config.name
53
+ self.base_model_config: ModelConfig = config.base_model
54
+ self.data = config.data_creator()
55
+ self.models: list[Model] = generate_models(config.base_model, config.variations, self.data)
56
+
57
+ def train_models(self, nb_print: int) -> None:
58
+ print(f"Training Models")
59
+ for model in self.models:
60
+ model.train(nb_print)
61
+
62
+ def plot_losses(self, title: str, plot_dimension: int, smooth_fraction: float = 0) -> plt.Figure:
63
+ fig = plot_losses(title=title,
64
+ dimension=plot_dimension,
65
+ models=self.models,
66
+ smooth_fraction=smooth_fraction)
67
+
68
+ plt.close(fig)
69
+
70
+ return fig
71
+
72
+ def test_models(self) -> None:
73
+ print(f"Testing Models")
74
+ for model in self.models:
75
+ model.test()
76
+
77
+ def save_df(self, save_dir: str) -> None:
78
+ """
79
+ saves the models parameters and their args
80
+ """
81
+ os.makedirs(save_dir, exist_ok=True)
82
+ print(f" Saving results to: {save_dir}")
83
+
84
+ data = [model.id | {"test_loss": model.test_loss, "test_accuracy": model.test_accuracy}
85
+ for model in self.models]
86
+
87
+ df = pd.DataFrame(data)
88
+ df.to_csv(os.path.join(save_dir, self.ACCURACY_FILE), index_label="iteration")
89
+
90
+ def save_weights(self, save_dir: str) -> None:
91
+ for i, model in enumerate(self.models):
92
+ save_path = os.path.join(save_dir, model.name)
93
+ model.save_weights(save_path)
94
+
95
+ def save_configs(self, save_dir: str) -> None:
96
+
97
+ config_path = os.path.join(save_dir, self.CONFIG_FILE)
98
+ self.config.save(config_path)
99
+
100
+ for i, model in enumerate(self.models):
101
+ save_path = os.path.join(save_dir, model.name)
102
+ model.config.save(save_path)
103
+
104
+
105
+ def generate_models(base_model: ModelConfig, variations: list[VariationConfig], data: Data) -> list[Model]:
106
+ models = []
107
+
108
+ values_lists = [v.values for v in variations]
109
+
110
+ for combination in itertools.product(*values_lists):
111
+ id_ = {}
112
+ current_model = base_model
113
+
114
+ for var_config, current_vals in zip(variations, combination):
115
+ id_[var_config.name] = get_name(current_vals[0])
116
+
117
+ for path, val in zip(var_config.param, current_vals):
118
+ current_model = set_value_by_path(current_model, path, val)
119
+
120
+ current_model = replace(current_model, id=id_)
121
+ models.append(current_model.instantiate(data))
122
+
123
+ return models
@@ -15,7 +15,7 @@ class Layer:
15
15
  A (Array): Activated output. Shape (batch_size, output_dim).
16
16
  """
17
17
 
18
- def __init__(self, output_dim: int, input_dim: int, activation: Activation) -> None:
18
+ def __init__(self, input_dim: int, output_dim: int, activation: Activation) -> None:
19
19
  """Initializes the layer with random weights and zeros biases.
20
20
 
21
21
  Args:
@@ -5,6 +5,7 @@ from dataclasses import dataclass
5
5
  from .neural_network import FirstOrderNeuralNetwork
6
6
  from .optimizers import FirstOrderOptimizerConfig, FirstOrderOptimizer
7
7
  from ..abstract import ModelConfig, Model, NeuralNetworkConfig
8
+ from ..data import Data
8
9
 
9
10
 
10
11
  @dataclass(frozen=True, kw_only=True)
@@ -12,13 +13,14 @@ class FirstOrderModelConfig(ModelConfig):
12
13
  neural_network_config: NeuralNetworkConfig
13
14
  optimizer_config: FirstOrderOptimizerConfig
14
15
 
15
- def instantiate(self) -> FirstOrderModel:
16
- return FirstOrderModel(self)
16
+ def instantiate(self, data: Data) -> FirstOrderModel:
17
+ return FirstOrderModel(self, data)
17
18
 
18
19
 
19
20
  class FirstOrderModel(Model):
20
- def __init__(self, config: FirstOrderModelConfig) -> None:
21
- super().__init__(config)
21
+ def __init__(self, config: FirstOrderModelConfig, data: Data) -> None:
22
+ super().__init__(config, data)
22
23
 
23
- self.neural_network: FirstOrderNeuralNetwork = FirstOrderNeuralNetwork(config.neural_network_config)
24
+ self.neural_network: FirstOrderNeuralNetwork = FirstOrderNeuralNetwork(config.neural_network_config,
25
+ data.input_dim, data.output_dim)
24
26
  self.optimizer: FirstOrderOptimizer = config.optimizer_config.instantiate()
@@ -1,6 +1,5 @@
1
1
  from .layer import Layer
2
2
  from ..abstract.neural_network import NeuralNetwork, NeuralNetworkConfig
3
-
4
3
  from ..types import Array
5
4
 
6
5
 
@@ -12,13 +11,16 @@ class FirstOrderNeuralNetwork(NeuralNetwork):
12
11
  nb_layers (int): Number of layers.
13
12
  """
14
13
 
15
- def __init__(self, config: NeuralNetworkConfig) -> None:
16
- super().__init__(config)
14
+ def __init__(self, config: NeuralNetworkConfig, input_dim: int, output_dim: int) -> None:
15
+ super().__init__(config, input_dim, output_dim)
16
+
17
17
  self.layers: list[Layer] = []
18
- for layer_config in config.layers_config:
19
- self.layers.append(Layer(layer_config.output_dim,
20
- layer_config.input_dim,
21
- layer_config.activation))
18
+ network_dimensions = [input_dim] + config.hidden_dims + [output_dim]
19
+
20
+ for i in range(self.nb_layers):
21
+ self.layers.append(Layer(network_dimensions[i],
22
+ network_dimensions[i + 1],
23
+ config.activations[i]))
22
24
 
23
25
  def __call__(self, X: Array) -> Array:
24
26
  return self.forward(X)
@@ -30,6 +30,7 @@ class FirstOrderSGDConfig(FirstOrderOptimizerConfig):
30
30
 
31
31
  @dataclass(frozen=True)
32
32
  class FirstOrderAdamConfig(FirstOrderSGDConfig):
33
+ name = "Adam"
33
34
  beta1: float
34
35
  beta2: float
35
36
  epsilon: float
@@ -45,8 +46,6 @@ class FirstOrderOptimizer(Optimizer):
45
46
 
46
47
 
47
48
  class FirstOrderSGD(FirstOrderOptimizer):
48
- """Abstract base class for gradient descent optimizers using first_order."""
49
-
50
49
  def __init__(self, config: FirstOrderSGDConfig) -> None:
51
50
  self.learning_rate: float = config.learning_rate
52
51
 
@@ -97,13 +96,11 @@ class FirstOrderSGD(FirstOrderOptimizer):
97
96
 
98
97
 
99
98
  class FirstOrderAdam(FirstOrderSGD):
100
- name = "Adam"
101
99
  """Implements the Adam optimization algorithm.
102
100
 
103
101
  Adam (Adaptive Moment Estimation) stores moving averages of the gradients (m)
104
102
  and squared gradients (v) to adapt the learning rate for each parameter.
105
103
  """
106
-
107
104
  def __init__(self, config: FirstOrderAdamConfig) -> None:
108
105
  super().__init__(config)
109
106
  self.beta1: float = config.beta1
@@ -11,7 +11,7 @@ from matplotlib.axes import Axes
11
11
  from .types import Array
12
12
 
13
13
  if TYPE_CHECKING:
14
- from .abstract.model import Model
14
+ from .abstract.model import ModelRecord
15
15
 
16
16
 
17
17
  def set_style() -> None:
@@ -80,7 +80,7 @@ def smooth_curve(loss: Array, window_length: int) -> Array:
80
80
  return np.exp(pd.Series(np.log(loss)).ewm(span=window_length, adjust=True).mean())
81
81
 
82
82
 
83
- def plot_0d(models: list[Model], title: str, smooth_fraction: float = 50) -> None:
83
+ def plot_0d(models: list[ModelRecord], title: str, smooth_fraction: float = 50) -> plt.Figure:
84
84
  """
85
85
  Plots a single graph overlaying multiple models that share the same hyperparameters.
86
86
  """
@@ -105,14 +105,17 @@ def plot_0d(models: list[Model], title: str, smooth_fraction: float = 50) -> Non
105
105
  fig.legend(handles, labels, loc='lower center', ncol=len(handles),
106
106
  bbox_to_anchor=(0.5, 0), frameon=False, fontsize=9)
107
107
 
108
+ return fig
108
109
 
109
- def plot_1d(models: list[Model], title: str, key: str, smooth_fraction: float = 50) -> None:
110
+
111
+ def plot_1d(models: list[ModelRecord], title: str, key: str, smooth_fraction: float = 50) -> plt.Figure:
110
112
  """
111
113
  Plots a row of subplots, varying one hyperparameter (key) across columns.
112
114
  """
113
-
115
+ print(len(models), [model.id for model in models])
114
116
  cols = list(dict.fromkeys([m.id[key] for m in models]))
115
117
  n_models = len(cols)
118
+ print(cols, n_models)
116
119
  fig, axs = plt.subplots(1, n_models, figsize=(4.5 * n_models, 3.5), sharey=True)
117
120
 
118
121
  for i, val in enumerate(cols):
@@ -141,8 +144,10 @@ def plot_1d(models: list[Model], title: str, key: str, smooth_fraction: float =
141
144
  fig.legend(handles, labels, loc='lower center', ncol=len(handles),
142
145
  bbox_to_anchor=(0.5, 0), frameon=False, fontsize=9)
143
146
 
147
+ return fig
144
148
 
145
- def plot_2d(models: list[Model], title: str, row_key: str, col_key: str, smooth_fraction: float) -> None:
149
+
150
+ def plot_2d(models: list[ModelRecord], title: str, row_key: str, col_key: str, smooth_fraction: float) -> plt.Figure:
146
151
  """
147
152
  Plots a grid of subplots varying two hyperparameters: one across rows, one across columns.
148
153
 
@@ -191,32 +196,30 @@ def plot_2d(models: list[Model], title: str, row_key: str, col_key: str, smooth_
191
196
  fig.text(0.5, 0.07, "Training steps", ha='center', fontsize=10)
192
197
  fig.text(0.02, 0.5, "Training loss", va='center', rotation='vertical', fontsize=10)
193
198
 
199
+ return fig
200
+
194
201
 
195
- def plot_losses(dimension: int, models: list[Model], title: str, smooth_fraction: float, save_path: str = None) -> None:
202
+ def plot_losses(title: str, dimension: int, models: list[ModelRecord], smooth_fraction: float) -> plt.Figure:
196
203
  """
197
204
  Main entry point for plotting. Automatically detects if the plot should be 0D, 1D, or 2D
198
205
  based on the number of variation parameters.
199
206
 
200
207
  Args:
208
+ title (str): The title of the plot.
201
209
  dimension (int): dimension of the plot
202
210
  models (list): List of model objects.
203
- title (str): The title of the plot.
204
- save_path (str, optional): File path to save the figure (e.g., 'plot.png').
205
211
  smooth_fraction (int): span for smoothing.
206
212
  """
207
213
  set_style()
208
214
 
209
215
  keys = list(models[0].id.keys())
210
216
 
211
- if dimension == 0:
212
- plot_0d(models, title, smooth_fraction)
213
- elif dimension == 1:
214
- plot_1d(models, title, keys[0], smooth_fraction)
215
- else:
216
- plot_2d(models, title, keys[0], keys[1], smooth_fraction)
217
-
218
- if save_path:
219
- plt.savefig(save_path, dpi=300, bbox_inches="tight")
220
- print(f"Plot saved to {save_path}")
217
+ match dimension:
218
+ case 0:
219
+ fig = plot_0d(models, title, smooth_fraction)
220
+ case 1:
221
+ fig = plot_1d(models, title, keys[0], smooth_fraction)
222
+ case _:
223
+ fig = plot_2d(models, title, keys[0], keys[1], smooth_fraction)
221
224
 
222
- plt.show()
225
+ return fig
@@ -0,0 +1,4 @@
1
+ from .activation_functions import *
2
+ from .dataclasses_utils import *
3
+ from .metrics import *
4
+ from .perturbation_matrices import *
@@ -1,4 +1,4 @@
1
- from dataclasses import is_dataclass, asdict, replace
1
+ from dataclasses import replace
2
2
  from typing import Any
3
3
 
4
4
 
@@ -26,9 +26,17 @@ def set_value_by_path(obj: Any, path: str, value: Any) -> Any:
26
26
  return replace(obj, **{field: value})
27
27
 
28
28
 
29
+ from dataclasses import is_dataclass, fields
30
+
31
+
29
32
  def config_serializer(obj: Any):
30
33
  if is_dataclass(obj):
31
- return asdict(obj)
34
+ data = {f.name: getattr(obj, f.name) for f in fields(obj)}
35
+ return {obj.__class__.__name__: data}
36
+
37
+ if hasattr(obj, "__class__") and obj.__class__.__module__ != "builtins":
38
+ return repr(obj)
39
+
32
40
  if callable(obj):
33
41
  return getattr(obj, "__name__", str(obj))
34
42
 
@@ -6,6 +6,7 @@ from .gradient_estimators import GradientEstimatorConfig, GradientEstimator
6
6
  from .neural_network.neural_network import ZerothOrderNeuralNetwork
7
7
  from .optimizers import ZerothOrderOptimizerConfig, ZerothOrderOptimizer
8
8
  from ..abstract import Model, ModelConfig, NeuralNetworkConfig
9
+ from ..data import Data
9
10
 
10
11
 
11
12
  @dataclass(frozen=True)
@@ -14,15 +15,16 @@ class ZerothOrderModelConfig(ModelConfig):
14
15
  optimizer_config: ZerothOrderOptimizerConfig
15
16
  gradient_estimator_config: GradientEstimatorConfig
16
17
 
17
- def instantiate(self) -> ZerothOrderModel:
18
- return ZerothOrderModel(self)
18
+ def instantiate(self, data: Data) -> ZerothOrderModel:
19
+ return ZerothOrderModel(self, data)
19
20
 
20
21
 
21
22
  class ZerothOrderModel(Model):
22
- def __init__(self, config: ZerothOrderModelConfig) -> None:
23
- super().__init__(config)
23
+ def __init__(self, config: ZerothOrderModelConfig, data: Data) -> None:
24
+ super().__init__(config, data)
24
25
 
25
- self.neural_network: ZerothOrderNeuralNetwork = ZerothOrderNeuralNetwork(config.neural_network_config)
26
+ self.neural_network: ZerothOrderNeuralNetwork = ZerothOrderNeuralNetwork(config.neural_network_config,
27
+ data.input_dim, data.output_dim)
26
28
  nb_params = self.neural_network.params.nb_params
27
29
  self.gradient_estimator: GradientEstimator = config.gradient_estimator_config.instantiate(nb_params)
28
30
  self.optimizer: ZerothOrderOptimizer = config.optimizer_config.instantiate(self.gradient_estimator)
@@ -12,13 +12,16 @@ class ZerothOrderNeuralNetwork(NeuralNetwork, ZerothOrderBlackBox):
12
12
  params (ParameterManager): Handler for flattening/reshaping weights (Theta <-> Ws/Bs).
13
13
  """
14
14
 
15
- def __init__(self, config: NeuralNetworkConfig) -> None:
16
- super().__init__(config)
15
+ def __init__(self, config: NeuralNetworkConfig, input_dim: int, output_dim: int) -> None:
16
+ super().__init__(config, input_dim, output_dim)
17
17
  self.params: ParameterManager = ParameterManager()
18
- for layer_config in config.layers_config:
19
- self.params.push_layer(layer_config.output_dim,
20
- layer_config.input_dim,
21
- layer_config.activation)
18
+
19
+ network_dimensions = [input_dim] + config.hidden_dims + [output_dim]
20
+
21
+ for i in range(self.nb_layers):
22
+ self.params.push_layer(network_dimensions[i],
23
+ network_dimensions[i + 1],
24
+ config.activations[i])
22
25
 
23
26
  def __call__(self, X: Array) -> Array:
24
27
  return self.forward(X)
@@ -30,7 +30,7 @@ class ParameterManager:
30
30
  self.nb_params: int = 0
31
31
  self.Theta: Array = np.array([])
32
32
 
33
- def push_layer(self, output_dim: int, input_dim: int | None = None, f: Activation = ReLU()) -> None:
33
+ def push_layer(self, input_dim: int, output_dim: int, f: Activation = ReLU()) -> None:
34
34
  """Adds a layer to the structure and updates the flat Theta vector.
35
35
 
36
36
  Args:
@@ -97,14 +97,13 @@ class ZerothOrderSGD(ZerothOrderOptimizer):
97
97
 
98
98
 
99
99
  class ZerothOrderAdam(ZerothOrderSGD):
100
- name = "Adam"
101
100
  """Adaptive Moment Estimation (Adam) adapted for zeroth_order gradient estimates.
102
101
 
103
102
  Note:
104
103
  Since zeroth_order gradients are noisy approximations, Adam is often very effective
105
104
  as its momentum terms (m, v) help smooth out the noise over time.
106
105
  """
107
-
106
+ name = "Adam"
108
107
  def __init__(self, config: ZerothOrderAdamConfig, gradient_estimator: GradientEstimator) -> None:
109
108
  self.beta1: float = config.beta1
110
109
  self.beta2: float = config.beta2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zeroth-learn
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Requires-Dist: numpy
5
5
  Requires-Dist: pandas
6
6
  Requires-Dist: matplotlib
@@ -2,6 +2,7 @@ MANIFEST.in
2
2
  README.md
3
3
  pyproject.toml
4
4
  zeroth/__init__.py
5
+ zeroth/all.py
5
6
  zeroth/data.py
6
7
  zeroth/experiment.py
7
8
  zeroth/paths.py
@@ -1,26 +0,0 @@
1
- from abc import ABC
2
- from dataclasses import dataclass
3
-
4
- from .activation import Activation
5
- from .summary import Summary
6
-
7
-
8
- @dataclass(frozen=True)
9
- class LayerConfig(Summary):
10
- input_dim: int
11
- output_dim: int
12
- activation: Activation
13
-
14
-
15
- @dataclass(frozen=True)
16
- class NeuralNetworkConfig(Summary):
17
- name: str
18
- layers_config: list[LayerConfig]
19
-
20
-
21
- class NeuralNetwork(ABC):
22
- def __init__(self, config: NeuralNetworkConfig):
23
- self.name: str = config.name
24
- self.nb_layers: int = len(config.layers_config)
25
- self.input_dim: int = config.layers_config[0].input_dim
26
- self.output_dim: int = config.layers_config[-1].output_dim
@@ -1,13 +0,0 @@
1
- import json
2
-
3
- from ..utils.dataclasses_utils import config_serializer
4
-
5
-
6
- class Summary:
7
- def summary(self) -> None:
8
- summary = json.dumps(
9
- self,
10
- default=config_serializer,
11
- indent=4
12
- )
13
- print(summary)
@@ -1,151 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import itertools
4
- import os
5
- from dataclasses import dataclass, replace
6
- from typing import Union
7
-
8
- import pandas as pd
9
-
10
- from .abstract import Model, ModelConfig, DataCreator, Summary
11
- from .data import Data
12
- from .plot_losses import plot_losses
13
- from .utils.dataclasses_utils import get_name, set_value_by_path
14
-
15
-
16
- @dataclass(frozen=True)
17
- class VariationConfig:
18
- name: str
19
- param: list[str]
20
- values: Union[list, list[list]]
21
-
22
-
23
- @dataclass(frozen=True)
24
- class ExperimentConfig(Summary):
25
- name: str
26
- title: str
27
- base_model: ModelConfig
28
- variations: list[VariationConfig]
29
- data_creator: DataCreator
30
- plot_dimension: int
31
- smooth_fraction: float
32
-
33
- def instantiate(self) -> Experiment:
34
- return Experiment(self)
35
-
36
-
37
- class Experiment:
38
- """Manages the full lifecycle of a deep learning experiment.
39
-
40
- It handles data loading, model instantiation, training loops, and results visualization.
41
-
42
- Attributes:
43
- name (str): Name of the experiment
44
- title (str): Title of the graphs
45
- models (list[Model]): List of models to train/compare
46
- data (Data): The dataset wrapper.
47
- """
48
-
49
- def __init__(self, config: ExperimentConfig):
50
- self.config = config
51
- self.name: str = config.name
52
- self.title: str = config.title
53
- self.base_model_config: ModelConfig = config.base_model
54
- self.models: list[Model] = generate_models(config.base_model, config.variations)
55
- self.data: Data = config.data_creator()
56
- self.plot_dimension: int = config.plot_dimension
57
- self.smooth_fraction: float = config.smooth_fraction
58
-
59
- self.save_dir = os.path.join("experiments", self.name)
60
-
61
- def launch(self, do_train: bool, do_test: bool, nb_print_train: int, do_plot_train: bool, do_save: bool) -> None:
62
- """
63
- Executes the experiment pipeline.
64
-
65
- Args:
66
- do_train (bool): Whether to run the training loop.
67
- do_test (bool): Whether to run evaluation on test set.
68
- nb_print_train (int): Number of logs to print during training.
69
- do_plot_train (bool): If True, plots loss curves after training.
70
- do_save (bool): if True, saves the plots and dataframes
71
- """
72
- print(f"### Launching Experiment : {self.name} ###")
73
- if do_train:
74
- self.train(nb_print=nb_print_train, do_plot=do_plot_train, do_save=do_save)
75
- if do_test:
76
- self.test()
77
- if do_save:
78
- self.save_df()
79
- self.save_weights()
80
-
81
- def train(self, nb_print: int, do_plot: bool, do_save: bool):
82
- for model in self.models:
83
- model.train(self.data, nb_print)
84
-
85
- if do_plot:
86
- plot_path = None
87
- if do_save:
88
- os.makedirs(self.save_dir, exist_ok=True)
89
- plot_path = os.path.join(self.save_dir, "training_losses.png")
90
-
91
- plot_losses(dimension=self.plot_dimension,
92
- models=self.models,
93
- title=self.title,
94
- smooth_fraction=self.smooth_fraction,
95
- save_path=plot_path)
96
-
97
- def test(self) -> None:
98
- for model in self.models:
99
- model.test(self.data)
100
-
101
- def save_df(self) -> None:
102
- """
103
- saves the models parameters and their args
104
- """
105
- os.makedirs(self.save_dir, exist_ok=True)
106
- print(f" Saving results to: {self.save_dir}")
107
-
108
- data = [model.id | {"test_loss": model.test_loss, "test_accuracy": model.test_accuracy}
109
- for model in self.models]
110
-
111
- df = pd.DataFrame(data)
112
- df.to_csv(os.path.join(self.save_dir, "models_accuracy.csv"), index_label="iteration")
113
-
114
- def save_weights(self) -> None:
115
- weights_dir = os.path.join(self.save_dir, "models")
116
- os.makedirs(weights_dir, exist_ok=True)
117
- print(f" Saving weights to: {weights_dir}")
118
-
119
- for i, model in enumerate(self.models):
120
- model_dir = os.path.join(weights_dir, f"{model.get_folder_name()}")
121
-
122
- weights_path = os.path.join(model_dir, "weights.pkl")
123
- model.save_weights(weights_path)
124
-
125
- config_path = os.path.join(model_dir, "config.json")
126
- model.save_config(config_path)
127
-
128
- loss_path = os.path.join(model_dir, "training_loss.csv")
129
- model.save_loss(loss_path)
130
-
131
-
132
- def generate_models(base_model: ModelConfig, variations: list[VariationConfig]) -> list[Model]:
133
- models = []
134
-
135
- values_lists = [v.values for v in variations]
136
-
137
- for combination in itertools.product(*values_lists):
138
- id_ = {}
139
- current_model = base_model
140
-
141
- for var_config, current_vals in zip(variations, combination):
142
-
143
- id_[var_config.name] = get_name(current_vals[0])
144
-
145
- for path, val in zip(var_config.param, current_vals):
146
- current_model = set_value_by_path(current_model, path, val)
147
-
148
- current_model = replace(current_model, id=id_)
149
- models.append(current_model.instantiate())
150
-
151
- return models
File without changes
File without changes
File without changes