pytensor-ml 0.0.2__tar.gz → 0.0.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 (25) hide show
  1. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/PKG-INFO +1 -1
  2. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/_version.py +3 -3
  3. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/activations.py +7 -0
  4. pytensor_ml-0.0.3/pytensor_ml/layers.py +236 -0
  5. pytensor_ml-0.0.3/pytensor_ml/model.py +161 -0
  6. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/optimizers.py +162 -9
  7. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/pytensorf.py +22 -2
  8. pytensor_ml-0.0.3/pytensor_ml/rewriting/layers.py +85 -0
  9. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/PKG-INFO +1 -1
  10. pytensor_ml-0.0.2/pytensor_ml/layers.py +0 -103
  11. pytensor_ml-0.0.2/pytensor_ml/model.py +0 -108
  12. pytensor_ml-0.0.2/pytensor_ml/rewriting/layers.py +0 -37
  13. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/LICENSE +0 -0
  14. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/README.md +0 -0
  15. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pyproject.toml +0 -0
  16. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/__init__.py +0 -0
  17. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/loss.py +0 -0
  18. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/rewriting/__init__.py +0 -0
  19. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/util.py +0 -0
  20. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/SOURCES.txt +0 -0
  21. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/dependency_links.txt +0 -0
  22. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/requires.txt +0 -0
  23. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/top_level.txt +0 -0
  24. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/setup.cfg +0 -0
  25. {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytensor_ml
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: A(nother) deep learning library, built on top of Pytensor
5
5
  Author: Ricardo Vieira
6
6
  Author-email: Jesse Grabowski <jessegrabowski@gmail.com>
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-01-26T00:24:12+0800",
11
+ "date": "2025-06-18T22:13:49+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "75eda5beff184dd32a4fe21444ec961a27c8eca3",
15
- "version": "0.0.2"
14
+ "full-revisionid": "56994236bd76d029096eb5ad6ca3767a893bb4fa",
15
+ "version": "0.0.3"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -37,6 +37,13 @@ class Sigmoid(Activation):
37
37
  return out
38
38
 
39
39
 
40
+ class SoftPlus(Activation):
41
+ def __call__(self, x: pt.TensorLike) -> pt.TensorVariable:
42
+ out = pt.softplus(x)
43
+ out.name = "SoftPlus"
44
+ return out
45
+
46
+
40
47
  class Softmax(Activation):
41
48
  def __init__(self, axis: int = -1):
42
49
  self.axis = axis
@@ -0,0 +1,236 @@
1
+ from abc import ABC
2
+ from collections.abc import Callable
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ import pytensor.tensor as pt
7
+ import pytensor.tensor.random as ptr
8
+
9
+ from pytensor import config
10
+ from pytensor.compile.sharedvalue import shared
11
+
12
+ from pytensor_ml.pytensorf import LayerOp
13
+
14
+
15
+ def shape_to_str(shape):
16
+ inner = ",".join([str(st_dim) if st_dim is not None else "?" for st_dim in shape])
17
+ return f"({inner})"
18
+
19
+
20
+ class Layer(ABC):
21
+ def __call__(self, x: pt.TensorLike) -> pt.TensorVariable: ...
22
+
23
+
24
+ class LinearLayer(LayerOp):
25
+ __props__ = ("n_in", "n_out", "bias")
26
+
27
+
28
+ class Linear(Layer):
29
+ def __init__(self, name: str | None, n_in: int, n_out: int, bias: bool = True):
30
+ self.name = name if name else "Linear"
31
+ self.n_in = n_in
32
+ self.n_out = n_out
33
+ self.bias = bias
34
+
35
+ self.W = pt.tensor(f"{self.name}_W", shape=(n_in, self.n_out))
36
+
37
+ if self.bias:
38
+ self.b = pt.tensor(f"{self.name}_b", shape=(self.n_out,))
39
+
40
+ def __call__(self, X: pt.TensorLike) -> pt.TensorLike:
41
+ X = pt.as_tensor(X)
42
+
43
+ init_st_shape = shape_to_str(X.type.shape)
44
+
45
+ res = X @ self.W
46
+ inputs = [X, self.W]
47
+ if self.bias:
48
+ res += self.b
49
+ inputs.append(self.b)
50
+
51
+ final_st_shape = shape_to_str(res.type.shape)
52
+
53
+ ofg = LinearLayer(
54
+ inputs=inputs,
55
+ outputs=[res],
56
+ name=f"{self.name}[{init_st_shape} -> {final_st_shape}]",
57
+ n_in=self.n_in,
58
+ n_out=self.n_out,
59
+ bias=self.bias,
60
+ )
61
+ out = ofg(*inputs)
62
+ out.name = f"{self.name}_output"
63
+
64
+ return out
65
+
66
+
67
+ class DropoutLayer(LayerOp):
68
+ __props__ = ("p",)
69
+
70
+
71
+ class Dropout(Layer):
72
+ def __init__(self, name: str | None = None, p: float = 0.5, random_state: Any | None = None):
73
+ if p < 0.0 or p > 1.0:
74
+ raise ValueError(f"Dropout probability has to be between 0 and 1, but got {p}")
75
+ self.name = name if name else "Dropout"
76
+ self.p = p
77
+ self.rng = shared(np.random.default_rng(random_state))
78
+
79
+ def __call__(self, X: pt.TensorLike) -> pt.TensorLike:
80
+ X = pt.as_tensor(X)
81
+ p = pt.as_tensor(self.p, dtype=config.floatX)
82
+ new_rng, mask = ptr.bernoulli(p=1 - p, size=X.shape, rng=self.rng).owner.outputs
83
+ mask = mask.astype(config.floatX)
84
+
85
+ X_masked = DropoutLayer(
86
+ inputs=[X, mask],
87
+ outputs=[pt.where(mask, ift=X / (1 - p), iff=0)],
88
+ name=f"{self.name}[p = {self.p}]",
89
+ p=self.p,
90
+ )(X, mask)
91
+ X_masked.name = f"{self.name}_output"
92
+
93
+ return X_masked
94
+
95
+
96
+ class BatchNormLayer(LayerOp):
97
+ __props__ = ("n_in", "epsilon", "momentum", "affine")
98
+
99
+ def update_map(self):
100
+ return {1: 3, 2: 4}
101
+
102
+
103
+ class NoRunningStatsBatchNormLayer(LayerOp):
104
+ __props__ = ("n_in", "epsilon", "momentum", "affine")
105
+
106
+
107
+ class PredictionBatchNormLayer(LayerOp):
108
+ __props__ = ("n_in", "epsilon", "momentum", "affine")
109
+
110
+
111
+ class BatchNorm2D(Layer):
112
+ def __init__(
113
+ self,
114
+ name: str | None = None,
115
+ n_in: int | None = None,
116
+ epsilon: float = 1e-5,
117
+ momentum: float = 0.1,
118
+ affine: bool = True,
119
+ track_running_stats: bool = True,
120
+ ):
121
+ self.name = name if name else "BatchNorm"
122
+ self.n_in = n_in
123
+ self.epsilon = epsilon
124
+ self.momentum = momentum
125
+ self.affine = affine
126
+ self.track_running_stats = track_running_stats
127
+
128
+ self.loc = None
129
+ self.scale = None
130
+
131
+ self.running_mean = None
132
+ self.running_var = None
133
+
134
+ self.initialized = False
135
+ self._initialize_params(None)
136
+
137
+ def _initialize_params(self, X: pt.TensorLike | None):
138
+ if self.initialized:
139
+ return
140
+
141
+ if self.n_in is None and X is None:
142
+ self.scale = None
143
+ self.loc = None
144
+ return
145
+
146
+ if X is not None:
147
+ n_in = X.type.shape[-1]
148
+ else:
149
+ n_in = self.n_in
150
+
151
+ if self.affine:
152
+ self.scale = pt.tensor(f"{self.name}_scale", shape=(n_in,))
153
+ self.loc = pt.tensor(f"{self.name}_loc", shape=(n_in,))
154
+ self.initialized = True
155
+
156
+ else:
157
+ self.initialized = True
158
+
159
+ def __call__(self, X: pt.TensorLike) -> pt.TensorLike:
160
+ X = pt.as_tensor(X)
161
+ inputs = [X]
162
+
163
+ self._initialize_params(X)
164
+
165
+ mu = X.mean(axis=0)
166
+ sigma_sq = X.var(axis=0)
167
+
168
+ X_normalized = (X - mu) / pt.sqrt(sigma_sq + self.epsilon)
169
+
170
+ if self.affine:
171
+ X_rescaled = X_normalized * self.scale + self.loc
172
+ inputs.extend([self.loc, self.scale])
173
+
174
+ else:
175
+ X_rescaled = X_normalized
176
+
177
+ if self.track_running_stats:
178
+ self.running_mean = mu.type(name=f"{self.name}_running_mean")
179
+ self.running_var = sigma_sq.type(name=f"{self.name}_running_var")
180
+
181
+ new_running_mean = self.momentum * mu + (1 - self.momentum) * self.running_mean
182
+ new_running_var = self.momentum * sigma_sq + (1 - self.momentum) * self.running_var
183
+
184
+ batch_norm_op = BatchNormLayer(
185
+ inputs=[*inputs, self.running_mean, self.running_var],
186
+ outputs=[X_rescaled, new_running_mean, new_running_var],
187
+ name=f"{self.name}",
188
+ n_in=self.n_in,
189
+ epsilon=self.epsilon,
190
+ momentum=self.momentum,
191
+ affine=self.affine,
192
+ )
193
+
194
+ X_transformed, self.new_running_mean, self.new_running_var = batch_norm_op(
195
+ *inputs, self.running_mean, self.running_var
196
+ )
197
+
198
+ else:
199
+ batch_norm_op = NoRunningStatsBatchNormLayer(
200
+ inputs=inputs,
201
+ outputs=[X_rescaled],
202
+ name=f"{self.name}",
203
+ n_in=self.n_in,
204
+ epsilon=self.epsilon,
205
+ momentum=self.momentum,
206
+ affine=self.affine,
207
+ )
208
+
209
+ X_transformed = batch_norm_op(*inputs)
210
+
211
+ X_transformed.name = f"{self.name}_output"
212
+
213
+ return X_transformed
214
+
215
+
216
+ def Input(name: str, shape: tuple[int]) -> pt.TensorLike:
217
+ if not all(isinstance(dim, int) for dim in shape):
218
+ raise ValueError("All dimensions must be integers")
219
+
220
+ return pt.tensor(name=name, shape=shape)
221
+
222
+
223
+ def Sequential(*layers: Callable) -> Callable:
224
+ def forward(x: pt.TensorLike) -> pt.TensorLike:
225
+ for layer in layers:
226
+ x = layer(x)
227
+ return x
228
+
229
+ return forward
230
+
231
+
232
+ Squeeze = pt.squeeze
233
+ Concatenate = pt.concatenate
234
+
235
+
236
+ __all__ = ["BatchNorm2D", "Concatenate", "Dropout", "Input", "Linear", "Sequential", "Squeeze"]
@@ -0,0 +1,161 @@
1
+ from collections.abc import Generator
2
+ from functools import partial
3
+ from typing import Any, Literal, cast
4
+
5
+ import numpy as np
6
+
7
+ from pytensor import config
8
+ from pytensor.compile.sharedvalue import SharedVariable
9
+ from pytensor.graph import graph_inputs
10
+ from pytensor.graph.basic import Constant, ancestors
11
+ from pytensor.printing import debugprint
12
+ from pytensor.tensor import TensorVariable
13
+
14
+ from pytensor_ml.pytensorf import LayerOp, function, rewrite_for_prediction
15
+
16
+ InitializationSchemes = Literal["zeros", "xavier_uniform", "xavier_normal"]
17
+
18
+
19
+ def required_graph_inputs(tensor: TensorVariable) -> Generator[TensorVariable, None, None]:
20
+ return (
21
+ cast(TensorVariable, var)
22
+ for var in graph_inputs([tensor])
23
+ if not isinstance(var, Constant | SharedVariable)
24
+ )
25
+
26
+
27
+ class Model:
28
+ def __init__(
29
+ self,
30
+ X: TensorVariable,
31
+ y: TensorVariable,
32
+ random_state: Any | None = None,
33
+ compile_kwargs: dict | None = None,
34
+ ):
35
+ self.X = X
36
+ self.y = y
37
+
38
+ self.rng = np.random.default_rng(random_state)
39
+
40
+ self._compile_kwargs = compile_kwargs if compile_kwargs else {}
41
+ self._weight_values: list[np.ndarray[float]] | None = None
42
+ self._non_trainable_values: list[np.ndarray[float]] | None = None
43
+ self._predict_fn = None
44
+
45
+ def initialize_weights(
46
+ self,
47
+ scheme: InitializationSchemes = "xavier_normal",
48
+ random_seed: int | str | np.random.Generator | None = None,
49
+ ):
50
+ self._weight_values = initialize_weights(self, scheme, random_seed)
51
+
52
+ def initialize_non_trainable_values(self) -> list[np.ndarray]:
53
+ if self.updates[0]:
54
+ values = [
55
+ self.rng.uniform(0, 1, param.type.shape).astype(param.type.dtype)
56
+ for param in self.updates[0]
57
+ ]
58
+ self._non_trainable_values = values
59
+ else:
60
+ self._non_trainable_values = []
61
+
62
+ @property
63
+ def weights(self) -> list[TensorVariable]:
64
+ not_trainable = {self.X}
65
+ for ancestor in ancestors([self.y]):
66
+ node = ancestor.owner
67
+ if node is not None and isinstance(node.op, LayerOp):
68
+ for input_idx in node.op.update_map().values():
69
+ not_trainable.add(node.inputs[input_idx])
70
+
71
+ return [var for var in required_graph_inputs(self.y) if var not in not_trainable]
72
+
73
+ @property
74
+ def updates(self) -> tuple[list[TensorVariable], list[TensorVariable]]:
75
+ updates = {}
76
+ for ancestor in ancestors([self.y]):
77
+ node = ancestor.owner
78
+ if node is not None and isinstance(node.op, LayerOp):
79
+ for output_idx, input_idx in node.op.update_map().items():
80
+ new_value = node.outputs[output_idx]
81
+ old_value = node.inputs[input_idx]
82
+ assert old_value.type.dtype == new_value.type.dtype
83
+ updates[old_value] = new_value
84
+
85
+ if updates:
86
+ return list(updates.keys()), list(updates.values())
87
+
88
+ return [], []
89
+
90
+ @property
91
+ def weight_values(self) -> list[np.ndarray[float]]:
92
+ if self._weight_values is None:
93
+ self.initialize_weights()
94
+ return self._weight_values
95
+
96
+ @weight_values.setter
97
+ def weight_values(self, values: list[np.ndarray[float]]):
98
+ for i, new_value in enumerate(values):
99
+ self._weight_values[i][:] = new_value
100
+
101
+ @property
102
+ def non_trainable_values(self) -> list[np.ndarray[float]]:
103
+ if self._non_trainable_values is None:
104
+ self.initialize_non_trainable_values()
105
+ return self._non_trainable_values
106
+
107
+ @non_trainable_values.setter
108
+ def non_trainable_values(self, values: list[np.ndarray[float]]):
109
+ for i, new_value in enumerate(values):
110
+ self._non_trainable_values[i][:] = new_value
111
+
112
+ def predict(self, X_values: np.ndarray) -> np.ndarray:
113
+ y_pred = rewrite_for_prediction(self.y)
114
+ if self._predict_fn is None:
115
+ f = function(
116
+ [*self.weights, *self.updates[0], self.X],
117
+ y_pred,
118
+ **self._compile_kwargs,
119
+ )
120
+ self._predict_fn = partial(f, *self.weight_values, *self.non_trainable_values)
121
+
122
+ return cast(np.ndarray, self._predict_fn(X_values))
123
+
124
+ def __str__(self):
125
+ return debugprint(self.y, file="str")
126
+
127
+
128
+ def _zero_init(shape: tuple[int], *args) -> np.ndarray:
129
+ return np.zeros(shape, dtype=config.floatX)
130
+
131
+
132
+ def _xavier_uniform_init(shape: tuple[int], dtype, rng: np.random.Generator) -> np.ndarray:
133
+ scale = np.sqrt(6.0 / np.sum([x for x in shape if x is not None]))
134
+ return rng.uniform(-scale, scale, size=shape).astype(dtype)
135
+
136
+
137
+ def _xavier_normal_init(shape: tuple[int], dtype, rng: np.random.Generator) -> np.ndarray:
138
+ scale = np.sqrt(2.0 / np.sum([x for x in shape if x is not None]))
139
+ return rng.normal(0, scale, size=shape).astype(dtype)
140
+
141
+
142
+ initialization_factory = {
143
+ "zeros": _zero_init,
144
+ "xavier_uniform": _xavier_uniform_init,
145
+ "xavier_normal": _xavier_normal_init,
146
+ }
147
+
148
+
149
+ def initialize_weights(
150
+ model, scheme: InitializationSchemes, random_seed: int | str | np.random.Generator | None
151
+ ):
152
+ rng = np.random.default_rng(random_seed)
153
+
154
+ initial_values = []
155
+ for var in model.weights:
156
+ shape = var.type.shape
157
+ dtype = var.type.dtype
158
+ f_initialize = initialization_factory[scheme]
159
+ initial_values.append(f_initialize(shape, dtype, rng))
160
+
161
+ return initial_values
@@ -1,7 +1,11 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Callable, Sequence
3
+ from functools import wraps
4
+ from typing import Any
3
5
 
4
6
  import numpy as np
7
+ import pytensor
8
+ import pytensor.tensor as pt
5
9
 
6
10
  from pytensor import config
7
11
  from pytensor.compile.function.types import Function
@@ -12,6 +16,37 @@ from pytensor_ml.model import Model
12
16
  from pytensor_ml.pytensorf import function, rewrite_pregrad
13
17
 
14
18
 
19
+ def split_output_wrapper(fn: Callable, *splits) -> Callable:
20
+ """
21
+ Wraps a function to return the weights and optimizer weights separately.
22
+
23
+ Parameters
24
+ ----------
25
+ fn: Function
26
+ The function to wrap.
27
+ splits: int
28
+
29
+ Returns
30
+ -------
31
+ Function
32
+ A wrapped function that returns the model weights and optimizer weights separately.
33
+ """
34
+ splits = np.cumsum(splits)
35
+
36
+ @wraps(fn)
37
+ def wrapped_fn(*args, **kwargs):
38
+ result = fn(*args, **kwargs)
39
+ output = []
40
+ cursor = 0
41
+ for split in splits:
42
+ output.append(result[cursor:split])
43
+ cursor = split
44
+ output.append(result[cursor:])
45
+ return output
46
+
47
+ return wrapped_fn
48
+
49
+
15
50
  class Optimizer(ABC):
16
51
  def __init__(
17
52
  self,
@@ -20,7 +55,9 @@ class Optimizer(ABC):
20
55
  ndim_out: int = 1,
21
56
  optimizer_weights: list[TensorVariable] | None = None,
22
57
  compile_kwargs: dict | None = None,
58
+ random_state: Any | None = None,
23
59
  ):
60
+ self.rng = np.random.default_rng(random_state)
24
61
  self.model = model
25
62
  self.loss_fn = loss_fn
26
63
  self.ndim_out = ndim_out
@@ -43,7 +80,10 @@ class Optimizer(ABC):
43
80
 
44
81
  def _initialize_weights(self) -> list[np.ndarray]:
45
82
  if self.optimizer_weights:
46
- return [np.zeros(param.type.shape) for param in self.optimizer_weights]
83
+ return [
84
+ np.zeros(param.type.shape, dtype=param.type.dtype)
85
+ for param in self.optimizer_weights
86
+ ]
47
87
  return []
48
88
 
49
89
  def _split_weights(
@@ -78,19 +118,25 @@ class Optimizer(ABC):
78
118
 
79
119
  weights = self.model.weights
80
120
  optimizer_weights = self.optimizer_weights
121
+ update_inputs, update_outputs = self.model.updates
81
122
 
82
123
  loss = self.loss_fn(y, y_hat)
83
124
  loss = rewrite_pregrad(loss)
84
125
 
85
126
  all_weights = self.update_parameters(weights + optimizer_weights, loss)
127
+
128
+ weights_fn_inputs = [pytensor.In(w, mutable=True) for w in self.model.weights]
129
+ optimizer_weights_fn_inputs = [pytensor.In(w, mutable=True) for w in self.optimizer_weights]
130
+ non_trainable_fn_inputs = [pytensor.In(w, mutable=True) for w in update_inputs]
131
+
86
132
  fn = function(
87
- [x, y, *weights, *optimizer_weights],
88
- [*all_weights, loss],
133
+ [x, y, *weights_fn_inputs, *optimizer_weights_fn_inputs, *non_trainable_fn_inputs],
134
+ [*all_weights, *update_outputs, loss],
135
+ trust_input=True,
89
136
  **compile_kwargs,
90
137
  )
91
- fn.trust_input = True
92
138
 
93
- return fn
139
+ return split_output_wrapper(fn, len(weights), len(optimizer_weights), len(update_outputs))
94
140
 
95
141
  def step(self, x_values, y_values) -> np.ndarray:
96
142
  """
@@ -100,11 +146,19 @@ class Optimizer(ABC):
100
146
  -------
101
147
  loss
102
148
  """
103
- *new_weights, loss_values = self.update_fn(
104
- x_values, y_values, *self.model.weight_values, *self.optimizer_weights_values
149
+
150
+ new_weights, new_optimizer_weights, new_non_trainable_values, loss_values = self.update_fn(
151
+ x_values,
152
+ y_values,
153
+ *self.model.weight_values,
154
+ *self.optimizer_weights_values,
155
+ *self.model.non_trainable_values,
105
156
  )
106
157
 
107
- self.model.weight_values, self.optimizer_weights_values = self._split_weights(new_weights)
158
+ self.model.weight_values = new_weights
159
+ self.model.non_trainable_values = new_non_trainable_values
160
+
161
+ self.optimizer_weights_values = new_optimizer_weights
108
162
 
109
163
  return loss_values
110
164
 
@@ -168,13 +222,57 @@ class ADAGrad(Optimizer):
168
222
 
169
223
  for param, d_loss_d_param, g2 in zip(weights, grads, optimizer_weights):
170
224
  new_g2 = g2 + d_loss_d_param**2
171
- weight_update = d_loss_d_param / np.sqrt(new_g2 + self.epsilon)
225
+ weight_update = d_loss_d_param / pt.sqrt(new_g2 + self.epsilon)
172
226
  new_weights.append(param - self.learning_rate * weight_update)
173
227
  new_optimizer_weights.append(new_g2)
174
228
 
175
229
  return new_weights + new_optimizer_weights
176
230
 
177
231
 
232
+ class Adadelta(Optimizer):
233
+ def __init__(
234
+ self,
235
+ model,
236
+ loss_fn,
237
+ *,
238
+ ndim_out: int = 1,
239
+ learning_rate: TensorLike = 1.0,
240
+ rho: TensorLike = 0.9,
241
+ epsilon: TensorLike = 1e-8,
242
+ ):
243
+ self.learning_rate = learning_rate
244
+ self.rho = rho
245
+ self.epsilon = epsilon
246
+
247
+ u_weights = [param.type() for param in model.weights]
248
+ v_weights = [param.type() for param in model.weights]
249
+ optimizer_weights = u_weights + v_weights
250
+ super().__init__(model, loss_fn, ndim_out=ndim_out, optimizer_weights=optimizer_weights)
251
+
252
+ def update_parameters(
253
+ self, weights: list[TensorVariable], loss: TensorVariable
254
+ ) -> list[TensorVariable]:
255
+ weights, optimizer_weights = self._split_weights(weights)
256
+ u_weights, v_weights = self._split_weights(optimizer_weights)
257
+
258
+ grads = grad(loss, weights)
259
+
260
+ new_weights = []
261
+ new_u_weights = []
262
+ new_v_weights = []
263
+
264
+ for param, d_loss_d_param, u, v in zip(weights, grads, u_weights, v_weights):
265
+ new_v = v * self.rho + d_loss_d_param**2 * (1 - self.rho)
266
+ weight_update = (sqrt(u + self.epsilon) / sqrt(new_v + self.epsilon)) * d_loss_d_param
267
+ new_u = u * self.rho + weight_update**2 * (1 - self.rho)
268
+
269
+ new_weights.append(param - self.learning_rate * weight_update)
270
+ new_u_weights.append(new_u)
271
+ new_v_weights.append(new_v)
272
+
273
+ return new_weights + new_u_weights + new_v_weights
274
+
275
+
178
276
  class Adam(Optimizer):
179
277
  def __init__(
180
278
  self,
@@ -232,3 +330,58 @@ class Adam(Optimizer):
232
330
  new_v_weights.append(new_v)
233
331
 
234
332
  return new_weights + new_m_weights + new_v_weights + [new_t]
333
+
334
+
335
+ class AdamW(Optimizer):
336
+ def __init__(
337
+ self,
338
+ model,
339
+ loss_fn,
340
+ *,
341
+ ndim_out: int = 1,
342
+ learning_rate: TensorLike = 0.01,
343
+ beta1: TensorLike = 0.9,
344
+ beta2: TensorLike = 0.999,
345
+ epsilon: TensorLike = 1e-8,
346
+ weight_decay: TensorLike = 0.01,
347
+ ):
348
+ self.learning_rate = learning_rate
349
+ self.beta1 = beta1
350
+ self.beta2 = beta2
351
+ self.epsilon = epsilon
352
+ self.weight_decay = weight_decay
353
+
354
+ m_weights = [param.type() for param in model.weights]
355
+ v_weights = [param.type() for param in model.weights]
356
+ t = tensor("t", shape=(1,), dtype=config.floatX)
357
+
358
+ optimizer_weights = m_weights + v_weights + [t]
359
+ super().__init__(model, loss_fn, ndim_out=ndim_out, optimizer_weights=optimizer_weights)
360
+
361
+ def update_parameters(
362
+ self, weights: list[TensorVariable], loss: TensorVariable
363
+ ) -> list[TensorVariable]:
364
+ weights, optimizer_weights = self._split_weights(weights)
365
+ t = optimizer_weights.pop(-1)
366
+ m_weights, v_weights = self._split_weights(optimizer_weights)
367
+
368
+ grads = grad(loss, weights)
369
+
370
+ new_weights = []
371
+ new_m_weights = []
372
+ new_v_weights = []
373
+
374
+ new_t = t + 1
375
+ a_t = sqrt(1 - self.beta2**new_t) / (1 - self.beta1**new_t)
376
+
377
+ for param, d_loss_d_param, m, v in zip(weights, grads, m_weights, v_weights):
378
+ decayed_param = (1 - self.weight_decay * self.learning_rate) * param
379
+ weight_update = a_t * m / (sqrt(v) + self.epsilon)
380
+ new_weights.append(decayed_param - self.learning_rate * weight_update)
381
+
382
+ new_m = self.beta1 * m + (1 - self.beta1) * d_loss_d_param
383
+ new_v = self.beta2 * v + (1 - self.beta2) * d_loss_d_param**2
384
+ new_m_weights.append(new_m)
385
+ new_v_weights.append(new_v)
386
+
387
+ return new_weights + new_m_weights + new_v_weights + [new_t]
@@ -5,10 +5,28 @@ import pytensor
5
5
  from pymc.pytensorf import SeedSequenceSeed, collect_default_updates, reseed_rngs
6
6
  from pytensor import Mode
7
7
  from pytensor.compile import Function, SharedVariable, get_mode
8
+ from pytensor.compile.builders import OpFromGraph
8
9
  from pytensor.graph import FunctionGraph, RewriteDatabaseQuery, rewrite_graph
9
10
  from pytensor.tensor.variable import Variable
10
11
 
11
- from pytensor_ml.rewriting.layers import predict_db
12
+
13
+ class LayerOp(OpFromGraph):
14
+ # This can be removed once https://github.com/pymc-devs/pytensor/issues/1114 is fixed
15
+
16
+ __props__ = ()
17
+
18
+ def __init__(self, *args, **kwargs):
19
+ prop_kwargs = {key: value for key, value in kwargs.items() if key in self.__props__}
20
+ kwargs = {key: value for key, value in kwargs.items() if key not in self.__props__}
21
+
22
+ for key, value in prop_kwargs.items():
23
+ setattr(self, key, value)
24
+
25
+ super().__init__(*args, **kwargs)
26
+
27
+ def update_map(self) -> dict[int, int]:
28
+ """Return a mapping of output indexes to input indexes"""
29
+ return {}
12
30
 
13
31
 
14
32
  def atleast_list(x):
@@ -75,11 +93,13 @@ def rewrite_pregrad(graph):
75
93
 
76
94
  def rewrite_for_prediction(graph):
77
95
  """Apply rewrites to specialize a graph for forward passes (e.g. removing Dropout layers)"""
96
+ from pytensor_ml.rewriting.layers import predict_db
97
+
78
98
  if isinstance(graph, FunctionGraph):
79
99
  fgraph = graph
80
100
  else:
81
101
  outputs = [graph] if isinstance(graph, Variable) else graph
82
- fgraph = FunctionGraph(outputs=outputs, clone=False)
102
+ fgraph = FunctionGraph(outputs=outputs, clone=True, copy_inputs=False)
83
103
 
84
104
  rewriter = predict_db.query(RewriteDatabaseQuery(include=["basic"]))
85
105
  rewriter.rewrite(fgraph)
@@ -0,0 +1,85 @@
1
+ import pytensor.tensor as pt
2
+
3
+ from pytensor.graph.basic import Apply
4
+ from pytensor.graph.fg import FunctionGraph
5
+ from pytensor.graph.rewriting.basic import node_rewriter
6
+ from pytensor.graph.rewriting.db import EquilibriumDB
7
+ from pytensor.tensor.variable import Variable
8
+
9
+ from pytensor_ml.layers import BatchNormLayer, DropoutLayer, PredictionBatchNormLayer
10
+
11
+ predict_db = EquilibriumDB()
12
+
13
+
14
+ @node_rewriter([DropoutLayer])
15
+ def remove_dropout_for_prediction(fgraph: FunctionGraph, node: Apply) -> list[Variable] | None:
16
+ """
17
+ Set dropout probability to zero for all dropout layers.
18
+
19
+ Parameters
20
+ ----------
21
+ fgraph: FunctionGraph
22
+ Graph being rewritten
23
+ node: Node
24
+ Node being rewritten
25
+
26
+ Returns
27
+ -------
28
+ X: Variable
29
+ The input to the dropout layer, removing the dropout from the graph
30
+ """
31
+ X, rng = node.inputs
32
+ return [X]
33
+
34
+
35
+ predict_db.register(
36
+ "remove_dropout_for_prediction",
37
+ remove_dropout_for_prediction,
38
+ "basic",
39
+ )
40
+
41
+
42
+ @node_rewriter([BatchNormLayer])
43
+ def rewrite_batch_stats_to_running_average_stats(
44
+ fgraph: FunctionGraph, node: Apply
45
+ ) -> list[Variable] | None:
46
+ """
47
+ Replace usage of batch mean and variance with running mean and variance.
48
+
49
+ Parameters
50
+ ----------
51
+ fgraph: FunctionGraph
52
+ Graph being rewritten
53
+ node: Node
54
+ Node being rewritten
55
+
56
+ Returns
57
+ -------
58
+ X_normalized: Variable
59
+
60
+ """
61
+ X, loc, scale, running_mean, running_var = node.inputs
62
+
63
+ res = (X - running_mean) / pt.sqrt(running_var + node.op.epsilon)
64
+ res = loc + res * scale
65
+
66
+ batch_norm_op = PredictionBatchNormLayer(
67
+ inputs=[X, loc, scale, running_mean, running_var],
68
+ outputs=[res],
69
+ name=f"{node.op.name}",
70
+ n_in=node.op.n_in,
71
+ momentum=node.op.momentum,
72
+ epsilon=node.op.epsilon,
73
+ affine=node.op.affine,
74
+ )
75
+
76
+ X_normalized = batch_norm_op(X, loc, scale, running_mean, running_var)
77
+
78
+ return [X_normalized, None, None]
79
+
80
+
81
+ predict_db.register(
82
+ "rewrite_batch_stats_to_running_average_stats",
83
+ rewrite_batch_stats_to_running_average_stats,
84
+ "basic",
85
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytensor-ml
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: A(nother) deep learning library, built on top of Pytensor
5
5
  Author: Ricardo Vieira
6
6
  Author-email: Jesse Grabowski <jessegrabowski@gmail.com>
@@ -1,103 +0,0 @@
1
- from abc import ABC
2
- from collections.abc import Callable
3
-
4
- import numpy as np
5
- import pytensor.tensor as pt
6
- import pytensor.tensor.random as ptr
7
-
8
- from pytensor import config
9
- from pytensor.compile.builders import OpFromGraph
10
- from pytensor.compile.sharedvalue import shared
11
-
12
-
13
- def shape_to_str(shape):
14
- inner = ",".join([str(st_dim) if st_dim is not None else "?" for st_dim in shape])
15
- return f"({inner})"
16
-
17
-
18
- class Layer(ABC):
19
- def __call__(self, x: pt.TensorLike) -> pt.TensorLike: ...
20
-
21
-
22
- class LinearLayer(OpFromGraph): ...
23
-
24
-
25
- class Linear(Layer):
26
- __props__ = ("name", "n_in", "n_out")
27
-
28
- def __init__(self, name: str | None, n_in: int, n_out: int):
29
- self.name = name if name else "Linear"
30
- self.n_in = n_in
31
- self.n_out = n_out
32
-
33
- self.W = pt.tensor(f"{self.name}_W", shape=(n_in, self.n_out))
34
- self.b = pt.tensor(f"{self.name}_b", shape=(self.n_out,))
35
-
36
- def __call__(self, X: pt.TensorLike) -> pt.TensorLike:
37
- X = pt.as_tensor(X)
38
-
39
- init_st_shape = shape_to_str(X.type.shape)
40
-
41
- res = X @ self.W + self.b
42
-
43
- final_st_shape = shape_to_str(res.type.shape)
44
-
45
- ofg = LinearLayer(
46
- inputs=[X, self.W, self.b],
47
- outputs=[res],
48
- inline=True,
49
- name=f"{self.name}[{init_st_shape} -> {final_st_shape}]",
50
- )
51
- out = ofg(X, self.W, self.b)
52
- out.name = f"{self.name}_output"
53
-
54
- return out
55
-
56
-
57
- class DropoutLayer(OpFromGraph): ...
58
-
59
-
60
- class Dropout(Layer):
61
- __props__ = ("name", "p")
62
-
63
- def __init__(self, name: str | None, p: float = 0.5):
64
- if p < 0.0 or p > 1.0:
65
- raise ValueError(f"Dropout probability has to be between 0 and 1, but got {p}")
66
- self.name = name if name else "Dropout"
67
- self.p = p
68
- self.rng = shared(np.random.default_rng())
69
-
70
- def __call__(self, X: pt.TensorLike) -> pt.TensorLike:
71
- X = pt.as_tensor(X)
72
- new_rng, mask = ptr.bernoulli(p=1 - self.p, size=X.shape, rng=self.rng).owner.outputs
73
- mask = mask.astype(config.floatX)
74
-
75
- X_masked = DropoutLayer(
76
- inputs=[X, mask], outputs=[pt.where(mask, ift=X / (1 - self.p), iff=0)], inline=True
77
- )(X, mask)
78
- X_masked.name = f"{self.name}[p = {self.p}]"
79
-
80
- return X_masked
81
-
82
-
83
- def Input(name: str, shape: tuple[int]) -> pt.TensorLike:
84
- if not all(isinstance(dim, int) for dim in shape):
85
- raise ValueError("All dimensions must be integers")
86
-
87
- return pt.tensor(name=name, shape=shape)
88
-
89
-
90
- def Sequential(*layers: Callable) -> Callable:
91
- def forward(x: pt.TensorLike) -> pt.TensorLike:
92
- for layer in layers:
93
- x = layer(x)
94
- return x
95
-
96
- return forward
97
-
98
-
99
- Squeeze = pt.squeeze
100
- Concatenate = pt.concatenate
101
-
102
-
103
- __all__ = ["Concatenate", "Input", "Linear", "Sequential", "Squeeze"]
@@ -1,108 +0,0 @@
1
- from collections.abc import Generator
2
- from functools import partial
3
- from typing import Literal, cast
4
-
5
- import numpy as np
6
-
7
- from pytensor import config
8
- from pytensor.compile.sharedvalue import SharedVariable
9
- from pytensor.graph import graph_inputs
10
- from pytensor.graph.basic import Constant
11
- from pytensor.printing import debugprint
12
- from pytensor.tensor import TensorVariable
13
-
14
- from pytensor_ml.pytensorf import function, rewrite_for_prediction
15
-
16
- InitializationSchemes = Literal["zeros", "xavier_uniform", "xavier_normal"]
17
-
18
-
19
- def required_graph_inputs(tensor: TensorVariable) -> Generator[TensorVariable, None, None]:
20
- return (
21
- cast(TensorVariable, var)
22
- for var in graph_inputs([tensor])
23
- if not isinstance(var, Constant | SharedVariable)
24
- )
25
-
26
-
27
- class Model:
28
- def __init__(self, X: TensorVariable, y: TensorVariable, compile_kwargs: dict | None = None):
29
- self.X = X
30
- self.y = y
31
-
32
- self._compile_kwargs = compile_kwargs if compile_kwargs else {}
33
- self._weight_values: list[np.ndarray[float]] | None = None
34
- self._predict_fn = None
35
-
36
- def initialize_weights(
37
- self,
38
- scheme: InitializationSchemes = "xavier_normal",
39
- random_seed: int | str | np.random.Generator | None = None,
40
- ):
41
- self._weight_values = initialize_weights(self, scheme, random_seed)
42
-
43
- @property
44
- def weights(self) -> list[TensorVariable]:
45
- return [var for var in required_graph_inputs(self.y) if var is not self.X]
46
-
47
- @property
48
- def weight_values(self) -> list[np.ndarray[float]]:
49
- if self._weight_values is None:
50
- raise ValueError("Weights have not been initialized")
51
- return self._weight_values
52
-
53
- @weight_values.setter
54
- def weight_values(self, values: list[np.ndarray[float]]):
55
- for i, new_value in enumerate(values):
56
- self._weight_values[i][:] = new_value
57
-
58
- def predict(self, X_values: np.ndarray) -> np.ndarray:
59
- if self._predict_fn is None:
60
- f = function(
61
- [*self.weights, self.X],
62
- rewrite_for_prediction(self.y),
63
- **self._compile_kwargs,
64
- )
65
- self._predict_fn = partial(f, *self.weight_values)
66
-
67
- return cast(np.ndarray, self._predict_fn(X_values))
68
-
69
- def __str__(self):
70
- return debugprint(self.y, file="str")
71
-
72
-
73
- def _zero_init(shape: tuple[int], *args) -> np.ndarray:
74
- return np.zeros(shape, dtype=config.floatX)
75
-
76
-
77
- def _xavier_uniform_init(shape: tuple[int], rng: np.random.Generator) -> np.ndarray:
78
- scale = np.sqrt(6.0 / np.sum([x for x in shape if x is not None]))
79
- return rng.uniform(-scale, scale, size=shape).astype(config.floatX)
80
-
81
-
82
- def _xavier_normal_init(shape: tuple[int], rng: np.random.Generator) -> np.ndarray:
83
- scale = np.sqrt(2.0 / np.sum([x for x in shape if x is not None]))
84
- return rng.normal(0, scale, size=shape).astype(config.floatX)
85
-
86
-
87
- initialization_factory = {
88
- "zeros": _zero_init,
89
- "xavier_uniform": _xavier_uniform_init,
90
- "xavier_normal": _xavier_normal_init,
91
- }
92
-
93
-
94
- def initialize_weights(
95
- model, scheme: InitializationSchemes, random_seed: int | str | np.random.Generator | None
96
- ):
97
- if isinstance(random_seed, str):
98
- random_seed = sum(map(ord, random_seed))
99
- if isinstance(random_seed, int) or random_seed is None:
100
- random_seed = np.random.default_rng(random_seed)
101
-
102
- initial_values = []
103
- for var in model.weights:
104
- shape = var.type.shape
105
- f_initialize = initialization_factory[scheme]
106
- initial_values.append(f_initialize(shape, random_seed))
107
-
108
- return initial_values
@@ -1,37 +0,0 @@
1
- from pytensor.graph.basic import Apply
2
- from pytensor.graph.fg import FunctionGraph
3
- from pytensor.graph.rewriting.basic import node_rewriter
4
- from pytensor.graph.rewriting.db import EquilibriumDB
5
- from pytensor.tensor.variable import Variable
6
-
7
- from pytensor_ml.layers import DropoutLayer
8
-
9
- predict_db = EquilibriumDB()
10
-
11
-
12
- @node_rewriter([DropoutLayer])
13
- def remove_dropout_for_prediction(fgraph: FunctionGraph, node: Apply) -> list[Variable] | None:
14
- """
15
- Set dropout probability to zero for all dropout layers.
16
-
17
- Parameters
18
- ----------
19
- fgraph: FunctionGraph
20
- Graph being rewritten
21
- node: Node
22
- Node being rewritten
23
-
24
- Returns
25
- -------
26
- X: Variable
27
- The input to the dropout layer, removing the dropout from the graph
28
- """
29
- X, rng = node.inputs
30
- return [X]
31
-
32
-
33
- predict_db.register(
34
- "remove_dropout_for_prediction",
35
- remove_dropout_for_prediction,
36
- "basic",
37
- )
File without changes
File without changes
File without changes
File without changes
File without changes