pytensor-ml 0.0.1__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.1 → pytensor_ml-0.0.3}/PKG-INFO +1 -1
  2. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/pyproject.toml +4 -1
  3. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/pytensor_ml/__init__.py +3 -0
  4. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/pytensor_ml/_version.py +3 -3
  5. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/pytensor_ml/activations.py +7 -0
  6. pytensor_ml-0.0.3/pytensor_ml/layers.py +236 -0
  7. pytensor_ml-0.0.3/pytensor_ml/model.py +161 -0
  8. pytensor_ml-0.0.3/pytensor_ml/optimizers.py +387 -0
  9. pytensor_ml-0.0.3/pytensor_ml/pytensorf.py +115 -0
  10. pytensor_ml-0.0.3/pytensor_ml/rewriting/__init__.py +0 -0
  11. pytensor_ml-0.0.3/pytensor_ml/rewriting/layers.py +85 -0
  12. pytensor_ml-0.0.3/pytensor_ml/util.py +49 -0
  13. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/PKG-INFO +1 -1
  14. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/SOURCES.txt +5 -1
  15. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/requires.txt +1 -0
  16. pytensor_ml-0.0.1/pytensor_ml/layers.py +0 -53
  17. pytensor_ml-0.0.1/pytensor_ml/model.py +0 -102
  18. pytensor_ml-0.0.1/pytensor_ml/optimizers.py +0 -199
  19. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/LICENSE +0 -0
  20. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/README.md +0 -0
  21. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/pytensor_ml/loss.py +0 -0
  22. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/dependency_links.txt +0 -0
  23. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/top_level.txt +0 -0
  24. {pytensor_ml-0.0.1 → pytensor_ml-0.0.3}/setup.cfg +0 -0
  25. {pytensor_ml-0.0.1 → 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.1
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>
@@ -33,7 +33,8 @@ keywords = [
33
33
 
34
34
  dependencies = [
35
35
  "pytensor",
36
- "numpy"
36
+ "numpy",
37
+ "pymc"
37
38
  ]
38
39
 
39
40
  [project.optional-dependencies]
@@ -96,6 +97,8 @@ ignore = [
96
97
  "D415",
97
98
  "D417",
98
99
  ]
100
+ fixable = ["ALL"]
101
+
99
102
 
100
103
  [tool.ruff.lint.isort]
101
104
  lines-between-types = 1
@@ -2,6 +2,7 @@ import logging
2
2
  import sys
3
3
 
4
4
  from pytensor_ml._version import get_versions
5
+ from pytensor_ml.pytensorf import function
5
6
 
6
7
  _log = logging.getLogger(__name__)
7
8
 
@@ -13,3 +14,5 @@ if not logging.root.handlers:
13
14
 
14
15
 
15
16
  __version__ = get_versions()["version"]
17
+
18
+ __all__ = ["function"]
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-12-30T10:40:07+0800",
11
+ "date": "2025-06-18T22:13:49+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "599c7aacb627660af38618e6095d8762a8d49a0a",
15
- "version": "0.0.1"
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