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.
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/PKG-INFO +1 -1
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/_version.py +3 -3
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/activations.py +7 -0
- pytensor_ml-0.0.3/pytensor_ml/layers.py +236 -0
- pytensor_ml-0.0.3/pytensor_ml/model.py +161 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/optimizers.py +162 -9
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/pytensorf.py +22 -2
- pytensor_ml-0.0.3/pytensor_ml/rewriting/layers.py +85 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/PKG-INFO +1 -1
- pytensor_ml-0.0.2/pytensor_ml/layers.py +0 -103
- pytensor_ml-0.0.2/pytensor_ml/model.py +0 -108
- pytensor_ml-0.0.2/pytensor_ml/rewriting/layers.py +0 -37
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/LICENSE +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/README.md +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pyproject.toml +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/__init__.py +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/loss.py +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/rewriting/__init__.py +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml/util.py +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/SOURCES.txt +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/dependency_links.txt +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/requires.txt +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/pytensor_ml.egg-info/top_level.txt +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/setup.cfg +0 -0
- {pytensor_ml-0.0.2 → pytensor_ml-0.0.3}/setup.py +0 -0
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2025-
|
|
11
|
+
"date": "2025-06-18T22:13:49+0200",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.0.
|
|
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 [
|
|
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, *
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
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 /
|
|
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
|
-
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|