pytensor-ml 0.0.7__tar.gz → 0.1.0__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.7 → pytensor_ml-0.1.0}/.github/workflows/run_tests.yml +53 -5
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/PKG-INFO +5 -1
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/conda_envs/pytensor_ml.yml +1 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/examples/mnist_feed_forward.ipynb +2 -3
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pyproject.toml +5 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/_version.py +2 -2
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/checkpoint.py +16 -8
- pytensor_ml-0.1.0/pytensor_ml/layers/combinators.py +34 -0
- pytensor_ml-0.1.0/pytensor_ml/layers/dropout.py +76 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/layers/embedding.py +15 -2
- pytensor_ml-0.1.0/pytensor_ml/layers/linear.py +101 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/layers/norm.py +61 -8
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/model.py +6 -2
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/optim/__init__.py +36 -3
- pytensor_ml-0.1.0/pytensor_ml/optim/alias.py +314 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/optim/base.py +79 -19
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/optim/rules.py +63 -30
- pytensor_ml-0.1.0/pytensor_ml/optim/schedules.py +398 -0
- pytensor_ml-0.1.0/pytensor_ml/optim/train.py +135 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/optim/transform.py +3 -38
- pytensor_ml-0.1.0/pytensor_ml/params.py +115 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pytensorf/__init__.py +6 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pytensorf/collect.py +74 -3
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pytensorf/compile.py +42 -15
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pytensorf/rewrite.py +7 -2
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pytensorf/rng.py +28 -0
- pytensor_ml-0.1.0/pytensor_ml/state.py +230 -0
- pytensor_ml-0.1.0/tests/optim/test_learning_rate.py +366 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/optim/test_rules.py +99 -22
- pytensor_ml-0.1.0/tests/optim/test_schedules.py +369 -0
- pytensor_ml-0.1.0/tests/optim/test_training.py +475 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/optim/test_transform.py +19 -18
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_attention.py +2 -1
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_checkpoint.py +38 -4
- pytensor_ml-0.1.0/tests/test_collect.py +185 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_layers.py +137 -1
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_model.py +50 -1
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_pretrained.py +13 -10
- pytensor_ml-0.1.0/tests/test_pytensorf.py +236 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_serialize.py +13 -4
- pytensor_ml-0.1.0/tests/test_state.py +219 -0
- pytensor_ml-0.1.0/tests/test_workflow_groups.py +69 -0
- pytensor_ml-0.0.7/pytensor_ml/layers/combinators.py +0 -35
- pytensor_ml-0.0.7/pytensor_ml/layers/dropout.py +0 -40
- pytensor_ml-0.0.7/pytensor_ml/layers/linear.py +0 -58
- pytensor_ml-0.0.7/pytensor_ml/optim/alias.py +0 -233
- pytensor_ml-0.0.7/pytensor_ml/optim/train.py +0 -77
- pytensor_ml-0.0.7/pytensor_ml/params.py +0 -56
- pytensor_ml-0.0.7/pytensor_ml/state.py +0 -118
- pytensor_ml-0.0.7/tests/optim/test_training.py +0 -239
- pytensor_ml-0.0.7/tests/test_collect.py +0 -82
- pytensor_ml-0.0.7/tests/test_pytensorf.py +0 -42
- pytensor_ml-0.0.7/tests/test_state.py +0 -93
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.gitattributes +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.github/workflows/mypy.yml +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.github/workflows/pre-commit.yml +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.github/workflows/release.yml +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.gitignore +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.pre-commit-config.yaml +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/LICENSE +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/README.md +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/conda_envs/pytensor_ml-gpu_jax.yml +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/activations.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/base.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/dispatch/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/dispatch/jax/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/dispatch/jax/attention.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/dispatch/mlx/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/dispatch/mlx/attention.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/json_serialize.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/layers/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/layers/attention.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/layers/transformer.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/loss.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/optim/clipping.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pretrained.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/rewriting/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/rewriting/layers.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/base.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/blockwise.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/builders.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/elemwise.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/scalar.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/scan.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/util.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/scripts/run_mypy.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/conftest.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/jax/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/jax/test_attention.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/jax/test_basic.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/mlx/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/mlx/test_attention.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/mlx/test_basic.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/optim/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/optim/conftest.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/optim/test_clipping.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/rewriting/__init__.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/rewriting/conftest.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/rewriting/test_layers.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_activations.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_base.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_loss.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_transformer.py +0 -0
- {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_util.py +0 -0
|
@@ -14,19 +14,57 @@ concurrency:
|
|
|
14
14
|
|
|
15
15
|
jobs:
|
|
16
16
|
unittest:
|
|
17
|
+
name: unittest (${{ matrix.os }}, ${{ matrix.python-version }}, ${{ matrix.test-subset.name }})
|
|
17
18
|
strategy:
|
|
18
19
|
fail-fast: false
|
|
19
20
|
matrix:
|
|
20
|
-
os: [ubuntu-latest
|
|
21
|
+
os: [ubuntu-latest]
|
|
21
22
|
python-version: ["3.12"]
|
|
23
|
+
# Groups are balanced on measured cold-compile-cache time, which is what a fresh runner pays.
|
|
24
|
+
# tests/test_workflow_groups.py fails if a test file is missing from every group or listed twice.
|
|
22
25
|
test-subset:
|
|
23
|
-
-
|
|
24
|
-
|
|
26
|
+
- name: attention
|
|
27
|
+
paths: |
|
|
28
|
+
tests/test_attention.py
|
|
29
|
+
tests/dispatch/
|
|
30
|
+
- name: layers
|
|
31
|
+
paths: |
|
|
32
|
+
tests/test_transformer.py
|
|
33
|
+
tests/test_layers.py
|
|
34
|
+
tests/rewriting/
|
|
35
|
+
- name: optim
|
|
36
|
+
paths: |
|
|
37
|
+
tests/optim/
|
|
38
|
+
- name: rest
|
|
39
|
+
paths: |
|
|
40
|
+
tests/test_serialize.py
|
|
41
|
+
tests/test_activations.py
|
|
42
|
+
tests/test_loss.py
|
|
43
|
+
tests/test_checkpoint.py
|
|
44
|
+
tests/test_model.py
|
|
45
|
+
tests/test_pretrained.py
|
|
46
|
+
tests/test_pytensorf.py
|
|
47
|
+
tests/test_state.py
|
|
48
|
+
tests/test_base.py
|
|
49
|
+
tests/test_collect.py
|
|
50
|
+
tests/test_util.py
|
|
51
|
+
tests/test_workflow_groups.py
|
|
52
|
+
# Windows runners are roughly twice as slow, and almost everything here is platform independent.
|
|
53
|
+
# It runs one job over the parts that are not: file IO, paths, and compiling a graph to train.
|
|
54
|
+
include:
|
|
55
|
+
- os: windows-latest
|
|
56
|
+
python-version: "3.12"
|
|
57
|
+
test-subset:
|
|
58
|
+
name: smoke
|
|
59
|
+
paths: |
|
|
60
|
+
tests/test_checkpoint.py
|
|
61
|
+
tests/test_serialize.py
|
|
62
|
+
tests/optim/test_training.py
|
|
25
63
|
|
|
26
64
|
runs-on: ${{ matrix.os }}
|
|
27
65
|
|
|
28
66
|
env:
|
|
29
|
-
TEST_SUBSET: ${{ matrix.test-subset }}
|
|
67
|
+
TEST_SUBSET: ${{ matrix.test-subset.paths }}
|
|
30
68
|
|
|
31
69
|
defaults:
|
|
32
70
|
run:
|
|
@@ -53,6 +91,16 @@ jobs:
|
|
|
53
91
|
~\AppData\Local\pip\Cache
|
|
54
92
|
key: ${{ runner.os }}-build-${{ matrix.python-version }}-${{ env.CACHE_NUMBER }}-${{
|
|
55
93
|
hashFiles('pyproject.toml') }}
|
|
94
|
+
- name: Cache the pytensor compile directory
|
|
95
|
+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
96
|
+
env:
|
|
97
|
+
CACHE_NUMBER: 0
|
|
98
|
+
with:
|
|
99
|
+
path: ~/.pytensor
|
|
100
|
+
key: ${{ runner.os }}-py${{ matrix.python-version }}-pytensor-${{ env.CACHE_NUMBER }}-${{
|
|
101
|
+
matrix.test-subset.name }}-${{ hashFiles('conda_envs/pytensor_ml.yml') }}
|
|
102
|
+
restore-keys: |
|
|
103
|
+
${{ runner.os }}-py${{ matrix.python-version }}-pytensor-${{ env.CACHE_NUMBER }}-${{ matrix.test-subset.name }}-
|
|
56
104
|
- uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4.0.1
|
|
57
105
|
with:
|
|
58
106
|
miniforge-variant: Miniforge3
|
|
@@ -75,7 +123,7 @@ jobs:
|
|
|
75
123
|
|
|
76
124
|
- name: Run tests
|
|
77
125
|
run: |
|
|
78
|
-
python -m pytest -vv --cov=pytensor_ml --cov-report=xml --no-cov-on-fail --cov-report term $TEST_SUBSET
|
|
126
|
+
python -m pytest -vv --durations=25 --cov=pytensor_ml --cov-report=xml --no-cov-on-fail --cov-report term $TEST_SUBSET
|
|
79
127
|
- name: Upload coverage to Codecov
|
|
80
128
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
|
81
129
|
with:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytensor_ml
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: A(nother) deep learning library, built on top of Pytensor
|
|
5
5
|
Project-URL: Homepage, https://github.com/pymc-devs/pytensor_ml
|
|
6
6
|
Project-URL: Source, https://github.com/pymc-devs/pytensor_ml
|
|
@@ -232,7 +232,11 @@ Requires-Dist: mypy; extra == 'dev'
|
|
|
232
232
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
233
233
|
Requires-Dist: pytest; extra == 'dev'
|
|
234
234
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
235
|
+
Requires-Dist: pyyaml; extra == 'dev'
|
|
235
236
|
Requires-Dist: scikit-learn; extra == 'dev'
|
|
237
|
+
Provides-Extra: examples
|
|
238
|
+
Requires-Dist: matplotlib; extra == 'examples'
|
|
239
|
+
Requires-Dist: tqdm; extra == 'examples'
|
|
236
240
|
Description-Content-Type: text/markdown
|
|
237
241
|
|
|
238
242
|
# pytensor_ml
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"from sklearn.preprocessing import MinMaxScaler, OneHotEncoder\n",
|
|
38
38
|
"\n",
|
|
39
39
|
"from pytensor_ml.activations import LeakyReLU\n",
|
|
40
|
-
"from pytensor_ml.layers import BatchNorm2D, Dropout, Linear, Sequential\n",
|
|
40
|
+
"from pytensor_ml.layers import BatchNorm2D, Dropout, Input, Linear, Sequential\n",
|
|
41
41
|
"from pytensor_ml.loss import CrossEntropy\n",
|
|
42
42
|
"from pytensor_ml.model import Model\n",
|
|
43
43
|
"from pytensor_ml.optim import adagrad\n",
|
|
@@ -63,8 +63,7 @@
|
|
|
63
63
|
"metadata": {},
|
|
64
64
|
"outputs": [],
|
|
65
65
|
"source": [
|
|
66
|
-
"
|
|
67
|
-
"X_in = pytensor.tensor.tensor(\"X_in\", shape=(None, 64))"
|
|
66
|
+
"X_in = Input(\"X_in\", shape=(None, 64))"
|
|
68
67
|
]
|
|
69
68
|
},
|
|
70
69
|
{
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.0
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -42,19 +42,24 @@ def save_state(shared_variables: Sequence[SharedVariable], path: str | Path) ->
|
|
|
42
42
|
Destination archive, written verbatim.
|
|
43
43
|
"""
|
|
44
44
|
indexed = _index_by_name(shared_variables)
|
|
45
|
-
tensors = {
|
|
45
|
+
tensors = {
|
|
46
|
+
name: _as_saveable_array(variable.get_value(), variable.type.dtype)
|
|
47
|
+
for name, variable in indexed.items()
|
|
48
|
+
}
|
|
46
49
|
save_file(tensors, fspath(path))
|
|
47
50
|
|
|
48
51
|
|
|
49
|
-
def _as_saveable_array(value: Any) -> np.ndarray:
|
|
52
|
+
def _as_saveable_array(value: Any, dtype: str) -> np.ndarray:
|
|
50
53
|
"""
|
|
51
|
-
Return a value as a C-contiguous numpy array safetensors will accept, preserving its
|
|
54
|
+
Return a value as a C-contiguous numpy array of ``dtype`` that safetensors will accept, preserving its
|
|
55
|
+
rank.
|
|
52
56
|
|
|
53
57
|
A JIT backend stores its own array type in the shared variables a compiled function updates, so a
|
|
54
58
|
trained model's values are ``jax.Array`` or ``mlx.core.array``; both convert through the array
|
|
55
|
-
protocol.
|
|
59
|
+
protocol. It may also narrow them -- JAX computes in 32-bit unless ``floatX`` is ``float64`` -- so the
|
|
60
|
+
archive records the declared dtype to stay portable across precisions.
|
|
56
61
|
"""
|
|
57
|
-
array = np.asarray(value)
|
|
62
|
+
array = np.asarray(value, dtype=dtype)
|
|
58
63
|
# Not np.ascontiguousarray: it forces ndim >= 1, reshaping a rank-0 array such as the step count.
|
|
59
64
|
return array if array.flags["C_CONTIGUOUS"] else np.array(array, order="C")
|
|
60
65
|
|
|
@@ -110,11 +115,14 @@ def load_state(
|
|
|
110
115
|
mismatches = []
|
|
111
116
|
for key, variable in target_by_key.items():
|
|
112
117
|
value = values[key]
|
|
113
|
-
#
|
|
118
|
+
# The declared dtype, not the stored value's, which a backend may have narrowed (see
|
|
119
|
+
# _as_saveable_array); set_value filters to the declaration anyway. Shape has to come from the
|
|
120
|
+
# value, whose runtime dimensions the declared type may leave unknown.
|
|
114
121
|
current = np.asarray(variable.get_value(borrow=True))
|
|
115
|
-
|
|
122
|
+
declared_dtype = np.dtype(variable.type.dtype)
|
|
123
|
+
if value.shape != current.shape or value.dtype != declared_dtype:
|
|
116
124
|
mismatches.append(
|
|
117
|
-
f" {variable.name!r}: target is {
|
|
125
|
+
f" {variable.name!r}: target is {declared_dtype} of shape {current.shape}, "
|
|
118
126
|
f"archive has {value.dtype} of shape {value.shape}"
|
|
119
127
|
)
|
|
120
128
|
if mismatches:
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
import pytensor.tensor as pt
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def Input(name: str, shape: tuple[int | None, ...], dtype: str | None = None) -> pt.TensorVariable:
|
|
7
|
+
"""
|
|
8
|
+
Create a named symbolic input tensor.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
name : str
|
|
13
|
+
Name of the input variable.
|
|
14
|
+
shape : tuple of int or None
|
|
15
|
+
Size of each dimension. Use None wherever the size varies between calls, such as a batch axis.
|
|
16
|
+
dtype : str, optional
|
|
17
|
+
Data type of the input. Default ``floatX``.
|
|
18
|
+
"""
|
|
19
|
+
return pt.tensor(name=name, shape=shape, dtype=dtype)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def Sequential(*layers: Callable) -> Callable:
|
|
23
|
+
"""Compose layers left to right into a single callable that threads its input through each in turn."""
|
|
24
|
+
|
|
25
|
+
def forward(x: pt.TensorLike) -> pt.TensorLike:
|
|
26
|
+
for layer in layers:
|
|
27
|
+
x = layer(x)
|
|
28
|
+
return x
|
|
29
|
+
|
|
30
|
+
return forward
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Squeeze = pt.squeeze
|
|
34
|
+
Concatenate = pt.concatenate
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytensor.tensor as pt
|
|
5
|
+
import pytensor.tensor.random as ptr
|
|
6
|
+
|
|
7
|
+
from pytensor import config
|
|
8
|
+
from pytensor.tensor.random.variable import RandomGeneratorSharedVariable, shared_rng
|
|
9
|
+
|
|
10
|
+
from pytensor_ml.base import Layer, UnaryLayerOp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DropoutLayer(UnaryLayerOp):
|
|
14
|
+
__props__ = ("p",)
|
|
15
|
+
|
|
16
|
+
def build_inner_graph(self, X, mask):
|
|
17
|
+
return [pt.where(mask, ift=X / (1 - self.p), iff=0)]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Dropout(Layer):
|
|
21
|
+
"""
|
|
22
|
+
Zero a random fraction of its input, rescaling the rest so the expected sum is unchanged.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
name : str, optional
|
|
27
|
+
Name of the layer, used to name its output and its generators. Default "Dropout".
|
|
28
|
+
p : float
|
|
29
|
+
Probability of zeroing each element. Default 0.5.
|
|
30
|
+
random_state : int, Generator, or other seed, optional
|
|
31
|
+
Seed for the layer's masks. Draws are reproducible under a given seed, including when the layer is
|
|
32
|
+
applied at several points in a network. Seeded from fresh entropy when omitted.
|
|
33
|
+
|
|
34
|
+
Attributes
|
|
35
|
+
----------
|
|
36
|
+
generators : list of RandomGeneratorSharedVariable
|
|
37
|
+
One generator per application of the layer, in the order they were applied. Set a value on these to
|
|
38
|
+
steer or restore the masks.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, name: str | None = None, p: float = 0.5, random_state: Any | None = None):
|
|
42
|
+
if p < 0.0 or p > 1.0:
|
|
43
|
+
raise ValueError(f"Dropout probability has to be between 0 and 1, but got {p}")
|
|
44
|
+
self.name = name if name else "Dropout"
|
|
45
|
+
self.p = p
|
|
46
|
+
self.generators: list[RandomGeneratorSharedVariable] = []
|
|
47
|
+
self._generator_source = np.random.default_rng(random_state)
|
|
48
|
+
|
|
49
|
+
def _own_generator(self) -> RandomGeneratorSharedVariable:
|
|
50
|
+
"""Return a generator for one application of this layer, spawned from its seed.
|
|
51
|
+
|
|
52
|
+
Each application draws its own mask, so each needs a generator of its own: a generator read by two
|
|
53
|
+
draws has no single next state, and nothing can advance it. Spawning keeps the whole layer
|
|
54
|
+
reproducible under ``random_state`` however many times it is applied.
|
|
55
|
+
"""
|
|
56
|
+
generator = shared_rng(
|
|
57
|
+
self._generator_source.spawn(1)[0], name=f"{self.name}/rng_{len(self.generators)}"
|
|
58
|
+
)
|
|
59
|
+
self.generators.append(generator)
|
|
60
|
+
return generator
|
|
61
|
+
|
|
62
|
+
def __call__(self, X: pt.TensorLike) -> pt.TensorVariable:
|
|
63
|
+
X = pt.as_tensor(X)
|
|
64
|
+
p = pt.as_tensor(self.p, dtype=config.floatX)
|
|
65
|
+
_, mask = ptr.bernoulli(
|
|
66
|
+
p=1 - p, size=X.shape, rng=self._own_generator(), return_next_rng=True
|
|
67
|
+
)
|
|
68
|
+
mask = mask.astype(config.floatX)
|
|
69
|
+
|
|
70
|
+
X_masked = DropoutLayer(
|
|
71
|
+
name=f"{self.name}[p = {self.p}]",
|
|
72
|
+
p=self.p,
|
|
73
|
+
)(X, mask)
|
|
74
|
+
X_masked.name = f"{self.name}_output"
|
|
75
|
+
|
|
76
|
+
return X_masked
|
|
@@ -5,6 +5,7 @@ from pytensor import config
|
|
|
5
5
|
|
|
6
6
|
from pytensor_ml.base import Layer, UnaryLayerOp
|
|
7
7
|
from pytensor_ml.params import trainable
|
|
8
|
+
from pytensor_ml.state import Initializer
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class EmbeddingLayer(UnaryLayerOp):
|
|
@@ -30,15 +31,27 @@ class Embedding(Layer):
|
|
|
30
31
|
Number of rows in the table -- the number of distinct indices it can map.
|
|
31
32
|
n_features : int
|
|
32
33
|
Size of each embedding row.
|
|
34
|
+
weight_initializer : Initializer, optional
|
|
35
|
+
How the table is drawn, in place of whatever scheme :meth:`~pytensor_ml.model.Model.initialize` is
|
|
36
|
+
given. Left to the scheme when omitted. A fan-scaled scheme puts the vocabulary size in the
|
|
37
|
+
denominator, which is correct Xavier and much tighter than the ``NormalInitializer(0.0, 0.02)`` that
|
|
38
|
+
reference implementations of GPT-2 use, so this is the keyword to reach for when matching one.
|
|
33
39
|
"""
|
|
34
40
|
|
|
35
|
-
def __init__(
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
name: str | None,
|
|
44
|
+
n_embeddings: int,
|
|
45
|
+
n_features: int,
|
|
46
|
+
*,
|
|
47
|
+
weight_initializer: Initializer | None = None,
|
|
48
|
+
):
|
|
36
49
|
self.name = name if name else "Embedding"
|
|
37
50
|
self.n_embeddings = n_embeddings
|
|
38
51
|
self.n_features = n_features
|
|
39
52
|
|
|
40
53
|
W_value = np.zeros((n_embeddings, n_features), dtype=config.floatX)
|
|
41
|
-
self.W = trainable(W_value, f"{self.name}_W")
|
|
54
|
+
self.W = trainable(W_value, f"{self.name}_W", initializer=weight_initializer)
|
|
42
55
|
|
|
43
56
|
def __call__(self, ids: pt.TensorLike) -> pt.TensorVariable:
|
|
44
57
|
ids = pt.as_tensor(ids)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytensor.tensor as pt
|
|
3
|
+
|
|
4
|
+
from pytensor import config
|
|
5
|
+
|
|
6
|
+
from pytensor_ml.base import Layer, UnaryLayerOp
|
|
7
|
+
from pytensor_ml.params import trainable
|
|
8
|
+
from pytensor_ml.state import Initializer, ZeroInitializer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def shape_to_str(shape):
|
|
12
|
+
inner = ",".join([str(st_dim) if st_dim is not None else "?" for st_dim in shape])
|
|
13
|
+
return f"({inner})"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LinearLayer(UnaryLayerOp):
|
|
17
|
+
__props__ = ("n_in", "n_out", "bias")
|
|
18
|
+
|
|
19
|
+
def build_inner_graph(self, X, W, b=None):
|
|
20
|
+
res = X @ W
|
|
21
|
+
if self.bias:
|
|
22
|
+
res = res + b
|
|
23
|
+
return [res]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Linear(Layer):
|
|
27
|
+
r"""
|
|
28
|
+
Affine map :math:`y = x W + b`.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
name : str or None
|
|
33
|
+
Name prefix for the layer's parameters. Defaults to "Linear" when None.
|
|
34
|
+
n_in : int
|
|
35
|
+
Size of the input feature axis.
|
|
36
|
+
n_out : int
|
|
37
|
+
Size of the output feature axis.
|
|
38
|
+
bias : bool, optional
|
|
39
|
+
Add the learned shift :math:`b`, which starts at zero and stays there under a network-wide
|
|
40
|
+
initialization scheme. Default is True.
|
|
41
|
+
weight_initializer : Initializer, optional
|
|
42
|
+
How :math:`W` is drawn, in place of whatever scheme :meth:`~pytensor_ml.model.Model.initialize` is
|
|
43
|
+
given. Left to the scheme when omitted, which is what makes a network-wide choice reach the weights.
|
|
44
|
+
bias_initializer : Initializer, optional
|
|
45
|
+
How :math:`b` is drawn. Zeros when omitted, following Keras and flax rather than torch, which draws
|
|
46
|
+
the bias from :math:`\mathcal{U}(\pm 1/\sqrt{\text{fan\_in}})`.
|
|
47
|
+
|
|
48
|
+
Notes
|
|
49
|
+
-----
|
|
50
|
+
The weight matrix :math:`W` starts at zero, so in a stack every activation below the first layer is
|
|
51
|
+
zero and every weight matrix receives a zero gradient: an uninitialized network can fit only its
|
|
52
|
+
output bias, and predicts a constant. Call :meth:`~pytensor_ml.model.Model.initialize`, or assign a
|
|
53
|
+
value yourself, before training.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
name: str | None,
|
|
59
|
+
n_in: int,
|
|
60
|
+
n_out: int,
|
|
61
|
+
bias: bool = True,
|
|
62
|
+
*,
|
|
63
|
+
weight_initializer: Initializer | None = None,
|
|
64
|
+
bias_initializer: Initializer | None = None,
|
|
65
|
+
):
|
|
66
|
+
self.name = name if name else "Linear"
|
|
67
|
+
self.n_in = n_in
|
|
68
|
+
self.n_out = n_out
|
|
69
|
+
self.bias = bias
|
|
70
|
+
|
|
71
|
+
W_value = np.zeros((n_in, n_out), dtype=config.floatX)
|
|
72
|
+
self.W = trainable(W_value, f"{self.name}_W", initializer=weight_initializer)
|
|
73
|
+
|
|
74
|
+
if self.bias:
|
|
75
|
+
b_value = np.zeros(n_out, dtype=config.floatX)
|
|
76
|
+
self.b = trainable(
|
|
77
|
+
b_value,
|
|
78
|
+
f"{self.name}_b",
|
|
79
|
+
initializer=ZeroInitializer() if bias_initializer is None else bias_initializer,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def __call__(self, X: pt.TensorLike) -> pt.TensorVariable:
|
|
83
|
+
X = pt.as_tensor(X)
|
|
84
|
+
|
|
85
|
+
inputs = [X, self.W]
|
|
86
|
+
if self.bias:
|
|
87
|
+
inputs.append(self.b)
|
|
88
|
+
|
|
89
|
+
input_shape = shape_to_str(X.type.shape)
|
|
90
|
+
output_shape = shape_to_str((X @ self.W).type.shape)
|
|
91
|
+
|
|
92
|
+
ofg = LinearLayer(
|
|
93
|
+
name=f"{self.name}[{input_shape} -> {output_shape}]",
|
|
94
|
+
n_in=self.n_in,
|
|
95
|
+
n_out=self.n_out,
|
|
96
|
+
bias=self.bias,
|
|
97
|
+
)
|
|
98
|
+
out = ofg(*inputs)
|
|
99
|
+
out.name = f"{self.name}_output"
|
|
100
|
+
|
|
101
|
+
return out
|
|
@@ -5,6 +5,7 @@ from pytensor import config
|
|
|
5
5
|
|
|
6
6
|
from pytensor_ml.base import Layer, LayerOp, UnaryLayerOp
|
|
7
7
|
from pytensor_ml.params import NonTrainableParameter, TrainableParameter, non_trainable, trainable
|
|
8
|
+
from pytensor_ml.state import Initializer, OneInitializer, ZeroInitializer
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def _standardize(X, epsilon, axis, keepdims=False):
|
|
@@ -75,11 +76,29 @@ def _resolve_n_in(name: str, n_in: int | None, X: pt.TensorVariable | None) -> i
|
|
|
75
76
|
return inferred
|
|
76
77
|
|
|
77
78
|
|
|
78
|
-
def _affine_parameters(
|
|
79
|
+
def _affine_parameters(
|
|
80
|
+
name: str,
|
|
81
|
+
n_in: int,
|
|
82
|
+
loc_initializer: Initializer | None = None,
|
|
83
|
+
scale_initializer: Initializer | None = None,
|
|
84
|
+
) -> tuple[TrainableParameter, TrainableParameter]:
|
|
79
85
|
"""Build the learned shift and scale. Returns them in the ``(loc, scale)`` order that every norm
|
|
80
|
-
op unpacks its inputs in, so the two cannot drift apart.
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
op unpacks its inputs in, so the two cannot drift apart.
|
|
87
|
+
|
|
88
|
+
Both declare their initializer, so a network-wide scheme leaves the identity transform in place --
|
|
89
|
+
normalizing and then rescaling by a random factor defeats the point of the layer. A caller who wants
|
|
90
|
+
something else says so, and their choice becomes the declaration.
|
|
91
|
+
"""
|
|
92
|
+
loc = trainable(
|
|
93
|
+
np.zeros(n_in, dtype=config.floatX),
|
|
94
|
+
f"{name}_loc",
|
|
95
|
+
initializer=ZeroInitializer() if loc_initializer is None else loc_initializer,
|
|
96
|
+
)
|
|
97
|
+
scale = trainable(
|
|
98
|
+
np.ones(n_in, dtype=config.floatX),
|
|
99
|
+
f"{name}_scale",
|
|
100
|
+
initializer=OneInitializer() if scale_initializer is None else scale_initializer,
|
|
101
|
+
)
|
|
83
102
|
return loc, scale
|
|
84
103
|
|
|
85
104
|
|
|
@@ -151,7 +170,14 @@ class BatchNorm2D(Layer):
|
|
|
151
170
|
Weight :math:`m` of the current batch statistic in the running-average update. Default is
|
|
152
171
|
0.1.
|
|
153
172
|
affine : bool, optional
|
|
154
|
-
Apply the learned scale :math:`\gamma` and shift :math:`\beta
|
|
173
|
+
Apply the learned scale :math:`\gamma` and shift :math:`\beta`, starting from the identity
|
|
174
|
+
transform :math:`\gamma = 1`, :math:`\beta = 0`, which a network-wide initialization scheme
|
|
175
|
+
leaves in place. Default is True.
|
|
176
|
+
scale_initializer : Initializer, optional
|
|
177
|
+
How :math:`\gamma` is drawn. Ones when omitted, which is the identity transform; a scheme that
|
|
178
|
+
rescaled a normalized activation by a random factor would defeat the layer.
|
|
179
|
+
loc_initializer : Initializer, optional
|
|
180
|
+
How :math:`\beta` is drawn. Zeros when omitted.
|
|
155
181
|
track_running_stats : bool, optional
|
|
156
182
|
Maintain running mean and variance for use at prediction time. Default is True.
|
|
157
183
|
|
|
@@ -175,6 +201,9 @@ class BatchNorm2D(Layer):
|
|
|
175
201
|
momentum: float = 0.1,
|
|
176
202
|
affine: bool = True,
|
|
177
203
|
track_running_stats: bool = True,
|
|
204
|
+
*,
|
|
205
|
+
scale_initializer: Initializer | None = None,
|
|
206
|
+
loc_initializer: Initializer | None = None,
|
|
178
207
|
):
|
|
179
208
|
self.name = name if name else "BatchNorm"
|
|
180
209
|
self.n_in = n_in
|
|
@@ -182,6 +211,8 @@ class BatchNorm2D(Layer):
|
|
|
182
211
|
self.momentum = momentum
|
|
183
212
|
self.affine = affine
|
|
184
213
|
self.track_running_stats = track_running_stats
|
|
214
|
+
self._scale_initializer = scale_initializer
|
|
215
|
+
self._loc_initializer = loc_initializer
|
|
185
216
|
|
|
186
217
|
self.scale: TrainableParameter | None = None
|
|
187
218
|
self.loc: TrainableParameter | None = None
|
|
@@ -202,7 +233,12 @@ class BatchNorm2D(Layer):
|
|
|
202
233
|
return
|
|
203
234
|
|
|
204
235
|
if self.affine:
|
|
205
|
-
self.loc, self.scale = _affine_parameters(
|
|
236
|
+
self.loc, self.scale = _affine_parameters(
|
|
237
|
+
self.name,
|
|
238
|
+
n_in,
|
|
239
|
+
loc_initializer=self._loc_initializer,
|
|
240
|
+
scale_initializer=self._scale_initializer,
|
|
241
|
+
)
|
|
206
242
|
|
|
207
243
|
if self.track_running_stats:
|
|
208
244
|
zeros = np.zeros(n_in, dtype=config.floatX)
|
|
@@ -283,7 +319,14 @@ class LayerNorm(Layer):
|
|
|
283
319
|
epsilon : float, optional
|
|
284
320
|
Constant :math:`\epsilon` added to the variance for numerical stability. Default is 1e-5.
|
|
285
321
|
affine : bool, optional
|
|
286
|
-
Apply the learned scale :math:`\gamma` and shift :math:`\beta
|
|
322
|
+
Apply the learned scale :math:`\gamma` and shift :math:`\beta`, starting from the identity
|
|
323
|
+
transform :math:`\gamma = 1`, :math:`\beta = 0`, which a network-wide initialization scheme
|
|
324
|
+
leaves in place. Default is True.
|
|
325
|
+
scale_initializer : Initializer, optional
|
|
326
|
+
How :math:`\gamma` is drawn. Ones when omitted, which is the identity transform; a scheme that
|
|
327
|
+
rescaled a normalized activation by a random factor would defeat the layer.
|
|
328
|
+
loc_initializer : Initializer, optional
|
|
329
|
+
How :math:`\beta` is drawn. Zeros when omitted.
|
|
287
330
|
"""
|
|
288
331
|
|
|
289
332
|
def __init__(
|
|
@@ -292,11 +335,16 @@ class LayerNorm(Layer):
|
|
|
292
335
|
n_in: int | None = None,
|
|
293
336
|
epsilon: float = 1e-5,
|
|
294
337
|
affine: bool = True,
|
|
338
|
+
*,
|
|
339
|
+
scale_initializer: Initializer | None = None,
|
|
340
|
+
loc_initializer: Initializer | None = None,
|
|
295
341
|
):
|
|
296
342
|
self.name = name if name else "LayerNorm"
|
|
297
343
|
self.n_in = n_in
|
|
298
344
|
self.epsilon = epsilon
|
|
299
345
|
self.affine = affine
|
|
346
|
+
self._scale_initializer = scale_initializer
|
|
347
|
+
self._loc_initializer = loc_initializer
|
|
300
348
|
|
|
301
349
|
self.scale: TrainableParameter | None = None
|
|
302
350
|
self.loc: TrainableParameter | None = None
|
|
@@ -313,7 +361,12 @@ class LayerNorm(Layer):
|
|
|
313
361
|
return
|
|
314
362
|
|
|
315
363
|
if self.affine:
|
|
316
|
-
self.loc, self.scale = _affine_parameters(
|
|
364
|
+
self.loc, self.scale = _affine_parameters(
|
|
365
|
+
self.name,
|
|
366
|
+
n_in,
|
|
367
|
+
loc_initializer=self._loc_initializer,
|
|
368
|
+
scale_initializer=self._scale_initializer,
|
|
369
|
+
)
|
|
317
370
|
|
|
318
371
|
self.initialized = True
|
|
319
372
|
|
|
@@ -41,11 +41,15 @@ class Model:
|
|
|
41
41
|
"""
|
|
42
42
|
Initialize the trainable weights in place and return self.
|
|
43
43
|
|
|
44
|
+
A parameter that declares its own initializer keeps it, so ``scheme`` reaches the weights whose
|
|
45
|
+
starting value is a free choice and leaves the rest alone: a batch norm layer stays at its
|
|
46
|
+
identity transform, and biases stay at zero.
|
|
47
|
+
|
|
44
48
|
Parameters
|
|
45
49
|
----------
|
|
46
50
|
scheme : str or Initializer
|
|
47
|
-
Initialization scheme for the weights: the name of a built-in
|
|
48
|
-
:class:`~pytensor_ml.state.Initializer` instance. Default 'xavier_normal'.
|
|
51
|
+
Initialization scheme for the weights that do not declare one: the name of a built-in
|
|
52
|
+
scheme, or an :class:`~pytensor_ml.state.Initializer` instance. Default 'xavier_normal'.
|
|
49
53
|
seed : int or numpy Generator, optional
|
|
50
54
|
Seed for reproducible initialization.
|
|
51
55
|
"""
|