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.
Files changed (108) hide show
  1. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.github/workflows/run_tests.yml +53 -5
  2. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/PKG-INFO +5 -1
  3. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/conda_envs/pytensor_ml.yml +1 -0
  4. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/examples/mnist_feed_forward.ipynb +2 -3
  5. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pyproject.toml +5 -0
  6. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/_version.py +2 -2
  7. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/checkpoint.py +16 -8
  8. pytensor_ml-0.1.0/pytensor_ml/layers/combinators.py +34 -0
  9. pytensor_ml-0.1.0/pytensor_ml/layers/dropout.py +76 -0
  10. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/layers/embedding.py +15 -2
  11. pytensor_ml-0.1.0/pytensor_ml/layers/linear.py +101 -0
  12. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/layers/norm.py +61 -8
  13. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/model.py +6 -2
  14. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/optim/__init__.py +36 -3
  15. pytensor_ml-0.1.0/pytensor_ml/optim/alias.py +314 -0
  16. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/optim/base.py +79 -19
  17. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/optim/rules.py +63 -30
  18. pytensor_ml-0.1.0/pytensor_ml/optim/schedules.py +398 -0
  19. pytensor_ml-0.1.0/pytensor_ml/optim/train.py +135 -0
  20. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/optim/transform.py +3 -38
  21. pytensor_ml-0.1.0/pytensor_ml/params.py +115 -0
  22. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pytensorf/__init__.py +6 -0
  23. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pytensorf/collect.py +74 -3
  24. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pytensorf/compile.py +42 -15
  25. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pytensorf/rewrite.py +7 -2
  26. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pytensorf/rng.py +28 -0
  27. pytensor_ml-0.1.0/pytensor_ml/state.py +230 -0
  28. pytensor_ml-0.1.0/tests/optim/test_learning_rate.py +366 -0
  29. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/optim/test_rules.py +99 -22
  30. pytensor_ml-0.1.0/tests/optim/test_schedules.py +369 -0
  31. pytensor_ml-0.1.0/tests/optim/test_training.py +475 -0
  32. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/optim/test_transform.py +19 -18
  33. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_attention.py +2 -1
  34. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_checkpoint.py +38 -4
  35. pytensor_ml-0.1.0/tests/test_collect.py +185 -0
  36. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_layers.py +137 -1
  37. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_model.py +50 -1
  38. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_pretrained.py +13 -10
  39. pytensor_ml-0.1.0/tests/test_pytensorf.py +236 -0
  40. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_serialize.py +13 -4
  41. pytensor_ml-0.1.0/tests/test_state.py +219 -0
  42. pytensor_ml-0.1.0/tests/test_workflow_groups.py +69 -0
  43. pytensor_ml-0.0.7/pytensor_ml/layers/combinators.py +0 -35
  44. pytensor_ml-0.0.7/pytensor_ml/layers/dropout.py +0 -40
  45. pytensor_ml-0.0.7/pytensor_ml/layers/linear.py +0 -58
  46. pytensor_ml-0.0.7/pytensor_ml/optim/alias.py +0 -233
  47. pytensor_ml-0.0.7/pytensor_ml/optim/train.py +0 -77
  48. pytensor_ml-0.0.7/pytensor_ml/params.py +0 -56
  49. pytensor_ml-0.0.7/pytensor_ml/state.py +0 -118
  50. pytensor_ml-0.0.7/tests/optim/test_training.py +0 -239
  51. pytensor_ml-0.0.7/tests/test_collect.py +0 -82
  52. pytensor_ml-0.0.7/tests/test_pytensorf.py +0 -42
  53. pytensor_ml-0.0.7/tests/test_state.py +0 -93
  54. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.gitattributes +0 -0
  55. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.github/workflows/mypy.yml +0 -0
  56. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.github/workflows/pre-commit.yml +0 -0
  57. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.github/workflows/release.yml +0 -0
  58. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.gitignore +0 -0
  59. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/.pre-commit-config.yaml +0 -0
  60. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/LICENSE +0 -0
  61. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/README.md +0 -0
  62. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/conda_envs/pytensor_ml-gpu_jax.yml +0 -0
  63. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/__init__.py +0 -0
  64. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/activations.py +0 -0
  65. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/base.py +0 -0
  66. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/dispatch/__init__.py +0 -0
  67. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/dispatch/jax/__init__.py +0 -0
  68. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/dispatch/jax/attention.py +0 -0
  69. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/dispatch/mlx/__init__.py +0 -0
  70. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/dispatch/mlx/attention.py +0 -0
  71. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/json_serialize.py +0 -0
  72. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/layers/__init__.py +0 -0
  73. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/layers/attention.py +0 -0
  74. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/layers/transformer.py +0 -0
  75. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/loss.py +0 -0
  76. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/optim/clipping.py +0 -0
  77. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/pretrained.py +0 -0
  78. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/rewriting/__init__.py +0 -0
  79. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/rewriting/layers.py +0 -0
  80. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/__init__.py +0 -0
  81. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/base.py +0 -0
  82. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/blockwise.py +0 -0
  83. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/builders.py +0 -0
  84. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/elemwise.py +0 -0
  85. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/scalar.py +0 -0
  86. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/serialize/scan.py +0 -0
  87. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/pytensor_ml/util.py +0 -0
  88. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/scripts/run_mypy.py +0 -0
  89. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/__init__.py +0 -0
  90. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/conftest.py +0 -0
  91. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/__init__.py +0 -0
  92. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/jax/__init__.py +0 -0
  93. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/jax/test_attention.py +0 -0
  94. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/jax/test_basic.py +0 -0
  95. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/mlx/__init__.py +0 -0
  96. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/mlx/test_attention.py +0 -0
  97. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/dispatch/mlx/test_basic.py +0 -0
  98. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/optim/__init__.py +0 -0
  99. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/optim/conftest.py +0 -0
  100. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/optim/test_clipping.py +0 -0
  101. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/rewriting/__init__.py +0 -0
  102. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/rewriting/conftest.py +0 -0
  103. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/rewriting/test_layers.py +0 -0
  104. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_activations.py +0 -0
  105. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_base.py +0 -0
  106. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_loss.py +0 -0
  107. {pytensor_ml-0.0.7 → pytensor_ml-0.1.0}/tests/test_transformer.py +0 -0
  108. {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, windows-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
- tests/
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.7
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
@@ -18,6 +18,7 @@ dependencies:
18
18
  - pytest-xdist
19
19
  - pytest-benchmark
20
20
  - pytest-mock
21
+ - pyyaml # tests/test_workflow_groups.py reads the CI matrix
21
22
 
22
23
  # For building docs
23
24
  - sphinx
@@ -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
- "# X_in = Input('X_in', shape=(64, ))\n",
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
  {
@@ -52,6 +52,11 @@ dev = [
52
52
  "scikit-learn",
53
53
  "mypy",
54
54
  "jax",
55
+ "pyyaml",
56
+ ]
57
+ examples = [
58
+ "matplotlib",
59
+ "tqdm",
55
60
  ]
56
61
 
57
62
  [tool.hatch.version]
@@ -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.7'
22
- __version_tuple__ = version_tuple = (0, 0, 7)
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 = {name: _as_saveable_array(variable.get_value()) for name, variable in indexed.items()}
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 rank.
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
- # Through numpy: a backend array's own dtype object never compares equal to a numpy dtype.
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
- if value.shape != current.shape or value.dtype != current.dtype:
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 {current.dtype} of shape {current.shape}, "
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__(self, name: str | None, n_embeddings: int, n_features: int):
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(name: str, n_in: int) -> tuple[TrainableParameter, TrainableParameter]:
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
- loc = trainable(np.zeros(n_in, dtype=config.floatX), f"{name}_loc")
82
- scale = trainable(np.ones(n_in, dtype=config.floatX), f"{name}_scale")
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`. Default is True.
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(self.name, n_in)
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`. Default is True.
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(self.name, n_in)
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 scheme, or an
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
  """