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 +144 -0
- kernels.py +270 -0
- modular.py +272 -0
- regnets-0.1.0.dist-info/METADATA +232 -0
- regnets-0.1.0.dist-info/RECORD +14 -0
- regnets-0.1.0.dist-info/WHEEL +5 -0
- regnets-0.1.0.dist-info/licenses/LICENSE +21 -0
- regnets-0.1.0.dist-info/top_level.txt +4 -0
- topology/__init__.py +5 -0
- topology/core.py +176 -0
- topology/graph.py +445 -0
- topology/io.py +125 -0
- topology/lib.py +90 -0
- topology/viz.py +304 -0
_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}"
|