regnets 0.1.0__py3-none-any.whl

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.
_kernel_ops.py ADDED
@@ -0,0 +1,144 @@
1
+ """Packed tensor operations used by neural kernel descriptions."""
2
+
3
+ from dataclasses import dataclass
4
+ from math import isfinite
5
+
6
+ import torch
7
+ from torch import Tensor, nn
8
+ from torch.nn import functional as F
9
+
10
+
11
+ def positive_int(value: int, name: str) -> None:
12
+ """Отклонить размерности, смысл которых изменился бы при приведении типа."""
13
+ if type(value) is not int or value < 1:
14
+ raise ValueError(f"{name} must be a positive integer.")
15
+
16
+
17
+ def boolean(value: bool, name: str) -> None:
18
+ """Требовать явно заданное логическое значение."""
19
+ if type(value) is not bool:
20
+ raise ValueError(f"{name} must be a boolean.")
21
+
22
+
23
+ def positive_real(value: float, name: str) -> None:
24
+ """Проверить, что числовой параметр конечен и положителен."""
25
+ if type(value) not in (int, float) or not isfinite(value) or value <= 0:
26
+ raise ValueError(f"{name} must be positive and finite.")
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class KernelShape:
31
+ """Размерности и правила работы с параметрами для метода build ядра."""
32
+
33
+ in_features: int
34
+ out_features: int
35
+ positions: int
36
+ shared: bool
37
+ device: torch.device | str | None = None
38
+ dtype: torch.dtype | None = None
39
+
40
+ def __post_init__(self) -> None:
41
+ for name in ("in_features", "out_features", "positions"):
42
+ positive_int(getattr(self, name), name)
43
+ boolean(self.shared, "shared")
44
+
45
+
46
+ class PackedLinear(nn.Module):
47
+ """Применить одно аффинное преобразование параллельно ко всем позициям модулей."""
48
+
49
+ def __init__(self, shape: KernelShape, bias: bool = True) -> None:
50
+ super().__init__()
51
+ self.in_features = shape.in_features
52
+ self.out_features = shape.out_features
53
+ self.positions = shape.positions
54
+ self.shared = shape.shared
55
+ prefix = () if shape.shared else (shape.positions,)
56
+ self.weight = nn.Parameter(
57
+ torch.empty(
58
+ (*prefix, shape.out_features, shape.in_features),
59
+ device=shape.device,
60
+ dtype=shape.dtype,
61
+ )
62
+ )
63
+ self.bias = (
64
+ nn.Parameter(
65
+ torch.empty(
66
+ (*prefix, shape.out_features),
67
+ device=shape.device,
68
+ dtype=shape.dtype,
69
+ )
70
+ )
71
+ if bias
72
+ else None
73
+ )
74
+ self.reset_parameters()
75
+
76
+ def reset_parameters(self) -> None:
77
+ """Инициализировать параметры по локальному числу входов независимо от числа позиций."""
78
+ bound = self.in_features**-0.5
79
+ nn.init.uniform_(self.weight, -bound, bound)
80
+ if self.bias is not None:
81
+ nn.init.uniform_(self.bias, -bound, bound)
82
+
83
+ def forward(self, x: Tensor) -> Tensor:
84
+ """Преобразовать [..., positions, in_features] без цикла по позициям."""
85
+ if self.shared:
86
+ return F.linear(x, self.weight, self.bias)
87
+ batch_shape = x.shape[:-2]
88
+ matrices = x.reshape(-1, self.positions, self.in_features).transpose(0, 1)
89
+ y = torch.bmm(matrices, self.weight.transpose(1, 2)).transpose(0, 1)
90
+ y = y.reshape(*batch_shape, self.positions, self.out_features)
91
+ return y if self.bias is None else y + self.bias
92
+
93
+ def extra_repr(self) -> str:
94
+ return (
95
+ f"in_features={self.in_features}, out_features={self.out_features}, "
96
+ f"positions={self.positions}, shared={self.shared}, bias={self.bias is not None}"
97
+ )
98
+
99
+
100
+ class PackedNorm(nn.Module):
101
+ """Нормализовать локальные признаки независимо для позиций и элементов пакета."""
102
+
103
+ def __init__(
104
+ self, shape: KernelShape, *, rms: bool, eps: float, affine: bool, bias: bool
105
+ ) -> None:
106
+ super().__init__()
107
+ self.features = shape.in_features
108
+ self.rms = rms
109
+ self.eps = eps
110
+ dimensions = (
111
+ (shape.in_features,)
112
+ if shape.shared
113
+ else (shape.positions, shape.in_features)
114
+ )
115
+ self.weight = (
116
+ nn.Parameter(torch.ones(dimensions, device=shape.device, dtype=shape.dtype))
117
+ if affine
118
+ else None
119
+ )
120
+ self.bias = (
121
+ nn.Parameter(
122
+ torch.zeros(dimensions, device=shape.device, dtype=shape.dtype)
123
+ )
124
+ if affine and bias
125
+ else None
126
+ )
127
+
128
+ def forward(self, x: Tensor) -> Tensor:
129
+ """Сохранить тип активации, если параметры имеют большую точность."""
130
+ if self.rms:
131
+ y = F.rms_norm(x, (self.features,), eps=self.eps)
132
+ else:
133
+ y = F.layer_norm(x, (self.features,), eps=self.eps)
134
+ if self.weight is not None:
135
+ y = y * self.weight.to(dtype=y.dtype)
136
+ if self.bias is not None:
137
+ y = y + self.bias.to(dtype=y.dtype)
138
+ return y
139
+
140
+ def extra_repr(self) -> str:
141
+ return (
142
+ f"features={self.features}, rms={self.rms}, eps={self.eps}, "
143
+ f"affine={self.weight is not None}, bias={self.bias is not None}"
144
+ )
kernels.py ADDED
@@ -0,0 +1,270 @@
1
+ """Immutable descriptions of vectorized neural kernels and their basic blocks."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable, Sequence
5
+ from dataclasses import dataclass, replace
6
+ from math import isfinite
7
+
8
+ from torch import Tensor, nn
9
+
10
+ from _kernel_ops import (
11
+ KernelShape as KernelShape,
12
+ )
13
+ from _kernel_ops import (
14
+ PackedLinear,
15
+ PackedNorm,
16
+ boolean,
17
+ positive_int,
18
+ positive_real,
19
+ )
20
+
21
+ type Activation = Callable[[], nn.Module] | None
22
+
23
+ __all__ = [
24
+ "Kernel",
25
+ "KernelShape",
26
+ "Dense",
27
+ "MLP",
28
+ "Sequential",
29
+ "Identity",
30
+ "Dropout",
31
+ "LayerNorm",
32
+ "RMSNorm",
33
+ "GatedMLP",
34
+ ]
35
+
36
+
37
+ def _activation(factory: Activation) -> nn.Module:
38
+ if factory is None:
39
+ return nn.Identity()
40
+ if not callable(factory):
41
+ raise ValueError("activation must be a factory such as nn.SiLU.")
42
+ module = factory()
43
+ if not isinstance(module, nn.Module):
44
+ raise ValueError("activation must create an nn.Module.")
45
+ if list(module.parameters()) or list(module.buffers()):
46
+ raise ValueError(
47
+ "Use a Kernel block for activations with parameters or buffers."
48
+ )
49
+ return module
50
+
51
+
52
+ def _same_features(shape: KernelShape) -> None:
53
+ if shape.in_features != shape.out_features:
54
+ raise ValueError("This block preserves its number of features.")
55
+
56
+
57
+ class Kernel(ABC):
58
+ """Описать операцию над [..., positions, features] с сохранением позиций.
59
+
60
+ Подклассы создают новые экземпляры nn.Module с параметрами согласно shape.shared.
61
+ Они могут смешивать локальные признаки, но не элементы пакета и не позиции.
62
+ """
63
+
64
+ def output_features(self, in_features: int, default: int) -> int:
65
+ """Определить размерность выхода, когда блок находится внутри Sequential."""
66
+ return default
67
+
68
+ @abstractmethod
69
+ def build(self, shape: KernelShape) -> nn.Module:
70
+ """Создать все регистрируемые параметры до создания оптимизатора."""
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class Dense(Kernel):
75
+ """Описать аффинное преобразование с необязательной выходной активацией.
76
+
77
+ Args:
78
+ out_features: Явная локальная размерность или размерность выхода слоя.
79
+ bias: Добавить обучаемое смещение.
80
+ activation: Фабрика активации без состояния, например nn.SiLU.
81
+ """
82
+
83
+ out_features: int | None = None
84
+ bias: bool = True
85
+ activation: Activation = None
86
+
87
+ def __post_init__(self) -> None:
88
+ if self.out_features is not None:
89
+ positive_int(self.out_features, "out_features")
90
+ boolean(self.bias, "bias")
91
+
92
+ def output_features(self, in_features: int, default: int) -> int:
93
+ return default if self.out_features is None else self.out_features
94
+
95
+ def build(self, shape: KernelShape) -> nn.Module:
96
+ if self.out_features is not None and self.out_features != shape.out_features:
97
+ raise ValueError("Dense out_features does not match the layer output.")
98
+ linear = PackedLinear(shape, self.bias)
99
+ return (
100
+ linear
101
+ if self.activation is None
102
+ else nn.Sequential(linear, _activation(self.activation))
103
+ )
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class MLP(Kernel):
108
+ """Описать локальный многослойный перцептрон с определяемыми размерностями входа и выхода."""
109
+
110
+ hidden: int | Sequence[int] = (64,)
111
+ activation: Activation = nn.SiLU
112
+ output_activation: Activation = None
113
+ bias: bool = True
114
+
115
+ def __post_init__(self) -> None:
116
+ if type(self.hidden) is int:
117
+ hidden = (self.hidden,)
118
+ elif isinstance(self.hidden, Sequence) and not isinstance(
119
+ self.hidden, (str, bytes)
120
+ ):
121
+ hidden = tuple(self.hidden)
122
+ else:
123
+ raise ValueError(
124
+ "hidden must be an integer or a sequence of hidden widths."
125
+ )
126
+ for width in hidden:
127
+ positive_int(width, "hidden width")
128
+ object.__setattr__(self, "hidden", hidden)
129
+ boolean(self.bias, "bias")
130
+
131
+ def build(self, shape: KernelShape) -> nn.Module:
132
+ hidden = (self.hidden,) if isinstance(self.hidden, int) else self.hidden
133
+ widths = (shape.in_features, *hidden, shape.out_features)
134
+ modules = []
135
+ for i, (source, target) in enumerate(zip(widths, widths[1:])):
136
+ activation = (
137
+ self.output_activation if i == len(widths) - 2 else self.activation
138
+ )
139
+ local = replace(shape, in_features=source, out_features=target)
140
+ modules.append(Dense(bias=self.bias, activation=activation).build(local))
141
+ return nn.Sequential(*modules)
142
+
143
+
144
+ @dataclass(frozen=True, init=False)
145
+ class Sequential(Kernel):
146
+ """Объединить блоки; блоки без изменения размерности сохраняют прежнюю локальную размерность."""
147
+
148
+ blocks: tuple[Kernel, ...]
149
+
150
+ def __init__(self, *blocks: Kernel) -> None:
151
+ if any(not isinstance(block, Kernel) for block in blocks):
152
+ raise ValueError("Sequential expects Kernel descriptions.")
153
+ object.__setattr__(self, "blocks", tuple(blocks))
154
+
155
+ def output_features(self, in_features: int, default: int) -> int:
156
+ for block in self.blocks:
157
+ in_features = block.output_features(in_features, default)
158
+ return in_features
159
+
160
+ def build(self, shape: KernelShape) -> nn.Module:
161
+ current = shape.in_features
162
+ modules = []
163
+ for block in self.blocks:
164
+ target = block.output_features(current, shape.out_features)
165
+ modules.append(
166
+ block.build(replace(shape, in_features=current, out_features=target))
167
+ )
168
+ current = target
169
+ if current != shape.out_features:
170
+ raise ValueError("Sequential output width does not match the layer output.")
171
+ return nn.Sequential(*modules)
172
+
173
+
174
+ class _Preserving(Kernel):
175
+ def output_features(self, in_features: int, default: int) -> int:
176
+ return in_features
177
+
178
+
179
+ @dataclass(frozen=True)
180
+ class Identity(_Preserving):
181
+ """Сохранить локальные признаки без использования параметров."""
182
+
183
+ def build(self, shape: KernelShape) -> nn.Module:
184
+ _same_features(shape)
185
+ return nn.Identity()
186
+
187
+
188
+ @dataclass(frozen=True)
189
+ class Dropout(_Preserving):
190
+ """Исключать отдельные локальные признаки при обучении с независимыми масками."""
191
+
192
+ p: float = 0.1
193
+
194
+ def __post_init__(self) -> None:
195
+ if (
196
+ type(self.p) not in (int, float)
197
+ or not isfinite(self.p)
198
+ or not 0 <= self.p <= 1
199
+ ):
200
+ raise ValueError("Dropout p must be finite and between 0 and 1.")
201
+
202
+ def build(self, shape: KernelShape) -> nn.Module:
203
+ _same_features(shape)
204
+ return nn.Dropout(self.p)
205
+
206
+
207
+ @dataclass(frozen=True)
208
+ class LayerNorm(_Preserving):
209
+ """Нормализовать весь локальный вектор признаков, не смешивая позиции и элементы пакета."""
210
+
211
+ eps: float = 1e-5
212
+ affine: bool = True
213
+ bias: bool = True
214
+
215
+ def __post_init__(self) -> None:
216
+ positive_real(self.eps, "eps")
217
+ boolean(self.affine, "affine")
218
+ boolean(self.bias, "bias")
219
+
220
+ def build(self, shape: KernelShape) -> nn.Module:
221
+ _same_features(shape)
222
+ return PackedNorm(
223
+ shape, rms=False, eps=self.eps, affine=self.affine, bias=self.bias
224
+ )
225
+
226
+
227
+ @dataclass(frozen=True)
228
+ class RMSNorm(_Preserving):
229
+ """Нормализовать локальную среднеквадратичную величину с необязательным обучаемым масштабом."""
230
+
231
+ eps: float = 1e-6
232
+ affine: bool = True
233
+
234
+ def __post_init__(self) -> None:
235
+ positive_real(self.eps, "eps")
236
+ boolean(self.affine, "affine")
237
+
238
+ def build(self, shape: KernelShape) -> nn.Module:
239
+ _same_features(shape)
240
+ return PackedNorm(shape, rms=True, eps=self.eps, affine=self.affine, bias=False)
241
+
242
+
243
+ class _Gated(nn.Module):
244
+ def __init__(
245
+ self, shape: KernelShape, hidden: int, activation: Activation, bias: bool
246
+ ):
247
+ super().__init__()
248
+ self.input = PackedLinear(replace(shape, out_features=2 * hidden), bias)
249
+ self.output = PackedLinear(replace(shape, in_features=hidden), bias)
250
+ self.activation = _activation(activation)
251
+
252
+ def forward(self, x: Tensor) -> Tensor:
253
+ gate, value = self.input(x).chunk(2, dim=-1)
254
+ return self.output(self.activation(gate) * value)
255
+
256
+
257
+ @dataclass(frozen=True)
258
+ class GatedMLP(Kernel):
259
+ """Вычислить проекции затвора и значения одной упакованной аффинной операцией."""
260
+
261
+ hidden: int = 64
262
+ activation: Activation = nn.SiLU
263
+ bias: bool = True
264
+
265
+ def __post_init__(self) -> None:
266
+ positive_int(self.hidden, "hidden")
267
+ boolean(self.bias, "bias")
268
+
269
+ def build(self, shape: KernelShape) -> nn.Module:
270
+ return _Gated(shape, self.hidden, self.activation, self.bias)
modular.py ADDED
@@ -0,0 +1,272 @@
1
+ """Self-similar PyTorch networks built from backend-independent topologies."""
2
+
3
+ from collections.abc import Sequence
4
+ from copy import deepcopy
5
+ from dataclasses import dataclass, field
6
+ from math import prod
7
+ from typing import cast
8
+
9
+ import torch
10
+ from torch import Tensor, nn
11
+
12
+ from _kernel_ops import KernelShape, PackedLinear, boolean, positive_int
13
+ from kernels import Dense, Kernel
14
+ from topology import Topology
15
+ from topology.io import TopologySpec
16
+
17
+ __all__ = ["ModularNet", "Layer", "Residual"]
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Residual:
22
+ """Обойти один слой, при необходимости обучая локальную проекцию без смещения."""
23
+
24
+ project: bool = False
25
+
26
+ def __post_init__(self) -> None:
27
+ boolean(self.project, "project")
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Layer:
32
+ """Описать ядро слоя, правила работы с параметрами и размеры векторов контактов.
33
+
34
+ Args:
35
+ kernel: Неизменяемое описание, по которому создаётся векторизованное ядро.
36
+ shared: Использовать общие параметры для позиций внутри слоя.
37
+ residual: Обойти слой, сохранив его порядок сгруппированных контактов.
38
+ in_size: Размер вектора входного контакта или None для скалярных контактов.
39
+ out_size: Размер вектора выходного контакта или размер входа, если не задан.
40
+ """
41
+
42
+ kernel: Kernel = field(default_factory=Dense)
43
+ shared: bool = True
44
+ residual: bool | Residual = False
45
+ in_size: int | None = field(default=None, kw_only=True)
46
+ out_size: int | None = field(default=None, kw_only=True)
47
+
48
+ def __post_init__(self) -> None:
49
+ if self.in_size is not None:
50
+ positive_int(self.in_size, "in_size")
51
+ if self.out_size is not None:
52
+ positive_int(self.out_size, "out_size")
53
+ if self.in_size is None:
54
+ raise ValueError(
55
+ "out_size requires vector input contacts with an in_size."
56
+ )
57
+ if not isinstance(self.kernel, Kernel):
58
+ raise ValueError("kernel must be a Kernel description.")
59
+ boolean(self.shared, "shared")
60
+ if type(self.residual) is not bool and not isinstance(self.residual, Residual):
61
+ raise ValueError("residual must be a boolean or Residual description.")
62
+
63
+
64
+ class _Layer(nn.Module):
65
+ def __init__(self, description: Layer, shape: KernelShape, *, identity_skip: bool):
66
+ super().__init__()
67
+ self.out_features = shape.out_features
68
+ project = (
69
+ isinstance(description.residual, Residual) and description.residual.project
70
+ )
71
+ if description.residual and not project and not identity_skip:
72
+ raise ValueError(
73
+ "Identity residual requires p == g and equal vector sizes; use Residual(project=True)."
74
+ )
75
+ self.kernel = description.kernel.build(shape)
76
+ if not isinstance(self.kernel, nn.Module):
77
+ raise ValueError("Kernel.build must return an nn.Module.")
78
+ self.skip: nn.Module | None = None
79
+ if project:
80
+ self.skip = PackedLinear(shape, bias=False)
81
+ elif description.residual:
82
+ self.skip = nn.Identity()
83
+
84
+ def forward(self, x: Tensor) -> Tensor:
85
+ y = self.kernel(x)
86
+ if not isinstance(y, Tensor) or y.shape != (*x.shape[:-1], self.out_features):
87
+ raise ValueError(
88
+ "Kernel returned an incompatible batch, position or feature shape."
89
+ )
90
+ return y if self.skip is None else self.skip(x) + y
91
+
92
+
93
+ def _sizes(descriptions: tuple[Layer, ...]) -> tuple[tuple[int, ...], bool]:
94
+ """Определить и проверить общие поля контактов до создания ядер."""
95
+ previous = descriptions[0].in_size
96
+ scalar = previous is None
97
+ values = [1 if previous is None else previous]
98
+ for m, layer in enumerate(descriptions):
99
+ if m and previous != layer.in_size:
100
+ raise ValueError(
101
+ f"Layer {m - 1} out_size={previous} does not match "
102
+ f"Layer {m} in_size={layer.in_size}."
103
+ )
104
+ previous = layer.in_size if layer.out_size is None else layer.out_size
105
+ values.append(1 if previous is None else previous)
106
+ return tuple(values), scalar
107
+
108
+
109
+ class ModularNet(nn.Module):
110
+ """Применить компактную топологию с упакованными ядрами и остаточными связями.
111
+
112
+ Остаточные связи необязательны.
113
+
114
+ Args:
115
+ topology: Соединения, не зависящие от PyTorch.
116
+ layers: Одно описание, повторяемое с новыми параметрами, или n описаний.
117
+ Размеры соседних контактов и скалярный или векторный режим должны совпадать.
118
+ device: Начальное устройство для параметров и индексов.
119
+ dtype: Начальный тип параметров.
120
+ """
121
+
122
+ def __init__(
123
+ self,
124
+ topology: Topology,
125
+ layers: Layer | Sequence[Layer],
126
+ *,
127
+ device: torch.device | str | None = None,
128
+ dtype: torch.dtype | None = None,
129
+ ) -> None:
130
+ """Применить компактную топологию с упакованными ядрами и остаточными связями.
131
+
132
+ Остаточные связи необязательны.
133
+
134
+ Args:
135
+ topology: Соединения, не зависящие от PyTorch.
136
+ layers: Одно описание, повторяемое с новыми параметрами, или n описаний.
137
+ Размеры соседних контактов и скалярный или векторный режим должны совпадать.
138
+ device: Начальное устройство для параметров и индексов.
139
+ dtype: Начальный тип параметров.
140
+ """
141
+ super().__init__()
142
+ if not isinstance(topology, Topology):
143
+ raise ValueError("topology must be a Topology object.")
144
+ self.topology = topology
145
+ n = topology.n_layers
146
+ if isinstance(layers, Layer):
147
+ descriptions = (layers,) * n
148
+ elif isinstance(layers, Sequence) and not isinstance(layers, (str, bytes)):
149
+ descriptions = tuple(layers)
150
+ else:
151
+ raise ValueError(
152
+ "layers must be a Layer or a sequence of Layer descriptions."
153
+ )
154
+ if len(descriptions) != n or any(
155
+ not isinstance(item, Layer) for item in descriptions
156
+ ):
157
+ raise ValueError(f"layers must contain {n} Layer descriptions.")
158
+ self.sizes, self.scalar = _sizes(descriptions)
159
+ self.n_inputs, self.n_outputs = prod(self.p), prod(self.g)
160
+ self.positions = tuple(topology.n_modules(m) for m in range(n))
161
+ self.layers = nn.ModuleList()
162
+ for m, description in enumerate(descriptions):
163
+ shape = KernelShape(
164
+ self.p[m] * self.sizes[m],
165
+ self.g[m] * self.sizes[m + 1],
166
+ self.positions[m],
167
+ description.shared,
168
+ device,
169
+ dtype,
170
+ )
171
+ try:
172
+ layer = _Layer(
173
+ description,
174
+ shape,
175
+ identity_skip=self.p[m] == self.g[m]
176
+ and self.sizes[m] == self.sizes[m + 1],
177
+ )
178
+ except (ValueError, TypeError) as error:
179
+ raise ValueError(f"Layer {m}: {error}") from error
180
+ self.layers.append(layer)
181
+ self._permutation_names = []
182
+ for m, indices in enumerate(topology.permutations()):
183
+ name = f"_permutation_{m}"
184
+ identity = all(i == value for i, value in enumerate(indices))
185
+ tensor = (
186
+ None
187
+ if identity
188
+ else torch.tensor(indices, dtype=torch.long, device=device)
189
+ )
190
+ self.register_buffer(name, tensor, persistent=False)
191
+ self._permutation_names.append(name)
192
+ self._configuration = self._describe(descriptions)
193
+
194
+ @property
195
+ def p(self) -> tuple[int, ...]:
196
+ """Вернуть числа входных контактов топологии по слоям."""
197
+ return self.topology.p
198
+
199
+ @property
200
+ def g(self) -> tuple[int, ...]:
201
+ """Вернуть числа выходных контактов топологии по слоям."""
202
+ return self.topology.g
203
+
204
+ def _describe(self, descriptions: tuple[Layer, ...]) -> dict:
205
+ """Описать топологию и действующие настройки каждого слоя."""
206
+ return {
207
+ "topology": TopologySpec("", self.topology).to_dict(),
208
+ "layers": [
209
+ {
210
+ "kernel": [
211
+ [
212
+ f"{type(module).__module__}.{type(module).__qualname__}",
213
+ module.extra_repr(),
214
+ ]
215
+ for module in cast(_Layer, self.layers[m]).kernel.modules()
216
+ ],
217
+ "shared": layer.shared,
218
+ "residual": (
219
+ "project"
220
+ if isinstance(layer.residual, Residual)
221
+ and layer.residual.project
222
+ else "identity"
223
+ if layer.residual
224
+ else "none"
225
+ ),
226
+ "in_size": None if self.scalar else self.sizes[m],
227
+ "out_size": None if self.scalar else self.sizes[m + 1],
228
+ }
229
+ for m, layer in enumerate(descriptions)
230
+ ],
231
+ }
232
+
233
+ def get_extra_state(self) -> dict:
234
+ """Сохранить метаданные соединений и построения вместе с зарегистрированными параметрами."""
235
+ return deepcopy(self._configuration)
236
+
237
+ def set_extra_state(self, state: dict) -> None:
238
+ """Запретить загрузку весов при другой конфигурации соединений или ядер."""
239
+ if state != self._configuration:
240
+ raise RuntimeError(
241
+ "Checkpoint topology or layer configuration does not match this network."
242
+ )
243
+
244
+ def _route(self, x: Tensor, m: int) -> Tensor:
245
+ indices = getattr(self, self._permutation_names[m])
246
+ return x if indices is None else x.index_select(-2, indices)
247
+
248
+ def forward(self, x: Tensor) -> Tensor:
249
+ """Сохранить начальные размерности пакета и применить все n+1 перестановок."""
250
+ if not isinstance(x, Tensor) or not x.is_floating_point():
251
+ raise ValueError("Input must be a floating-point tensor.")
252
+ if self.scalar:
253
+ if x.ndim < 1 or x.shape[-1] != self.n_inputs:
254
+ raise ValueError(f"Expected scalar input [..., {self.n_inputs}].")
255
+ x = x.unsqueeze(-1)
256
+ elif x.ndim < 2 or x.shape[-2:] != (self.n_inputs, self.sizes[0]):
257
+ raise ValueError(
258
+ f"Expected vector input [..., {self.n_inputs}, {self.sizes[0]}]."
259
+ )
260
+ batch_shape = x.shape[:-2]
261
+ for m, layer in enumerate(self.layers):
262
+ grouped = self._route(x, m).reshape(
263
+ *batch_shape, self.positions[m], self.p[m] * self.sizes[m]
264
+ )
265
+ x = layer(grouped).reshape(
266
+ *batch_shape, self.positions[m] * self.g[m], self.sizes[m + 1]
267
+ )
268
+ x = self._route(x, len(self.layers))
269
+ return x.squeeze(-1) if self.scalar else x
270
+
271
+ def extra_repr(self) -> str:
272
+ return f"n_inputs={self.n_inputs}, n_outputs={self.n_outputs}, sizes={self.sizes}, scalar={self.scalar}"