torchnd 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.
- torchnd/__init__.py +14 -0
- torchnd/functional.py +478 -0
- torchnd/modules.py +635 -0
- torchnd-0.1.0.dist-info/METADATA +154 -0
- torchnd-0.1.0.dist-info/RECORD +7 -0
- torchnd-0.1.0.dist-info/WHEEL +5 -0
- torchnd-0.1.0.dist-info/top_level.txt +1 -0
torchnd/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
2
|
+
|
|
3
|
+
from torchnd.functional import adjoint_pad_nd, conv_nd, pad_nd, pad_or_crop_to_size
|
|
4
|
+
from torchnd.modules import ConvNd, ConvTransposeNd
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"adjoint_pad_nd",
|
|
8
|
+
"conv_nd",
|
|
9
|
+
"pad_nd",
|
|
10
|
+
"pad_or_crop_to_size",
|
|
11
|
+
"ConvNd",
|
|
12
|
+
"ConvTransposeNd",
|
|
13
|
+
"__version__",
|
|
14
|
+
]
|
torchnd/functional.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""N-dimensional convolution with flexible dimension specification."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
from einops import rearrange
|
|
7
|
+
from torch import Tensor
|
|
8
|
+
|
|
9
|
+
CONV_REGISTRY: dict[int, Callable[..., Tensor]] = {
|
|
10
|
+
1: torch.nn.functional.conv1d,
|
|
11
|
+
2: torch.nn.functional.conv2d,
|
|
12
|
+
3: torch.nn.functional.conv3d,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
CONV_TRANSPOSE_REGISTRY: dict[int, Callable[..., Tensor]] = {
|
|
16
|
+
1: torch.nn.functional.conv_transpose1d,
|
|
17
|
+
2: torch.nn.functional.conv_transpose2d,
|
|
18
|
+
3: torch.nn.functional.conv_transpose3d,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _normalize_tuple(value: int | tuple[int, ...], length: int) -> tuple[int, ...]:
|
|
23
|
+
return (value,) * length if isinstance(value, int) else tuple(value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def conv_nd(
|
|
27
|
+
x: Tensor,
|
|
28
|
+
weight: Tensor,
|
|
29
|
+
dim: tuple[int, ...] = (-2, -1),
|
|
30
|
+
channel_dim: int = 1,
|
|
31
|
+
stride: int | tuple[int, ...] = 1,
|
|
32
|
+
padding: int | tuple[int, ...] = 0,
|
|
33
|
+
dilation: int | tuple[int, ...] = 1,
|
|
34
|
+
groups: int = 1,
|
|
35
|
+
transposed: bool = False,
|
|
36
|
+
output_padding: int | tuple[int, ...] = 0,
|
|
37
|
+
) -> Tensor:
|
|
38
|
+
"""N-dimensional convolution with flexible dimension specification.
|
|
39
|
+
|
|
40
|
+
Supports arbitrary spatial dimensions via recursive decomposition when
|
|
41
|
+
native PyTorch implementations (1D, 2D, 3D) are unavailable.
|
|
42
|
+
|
|
43
|
+
Note: Strictly speaking, this implements correlation, not convolution. But it matches pytorch's naming convention.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
x
|
|
48
|
+
Input tensor with batch, channel, and spatial dimensions.
|
|
49
|
+
weight
|
|
50
|
+
Convolution kernel.
|
|
51
|
+
Forward: ``(C_out, C_in // groups, *kernel_sizes)``.
|
|
52
|
+
Transposed: ``(C_in, C_out // groups, *kernel_sizes)``.
|
|
53
|
+
dim
|
|
54
|
+
Spatial dimensions to convolve over.
|
|
55
|
+
channel_dim
|
|
56
|
+
Channel dimension index.
|
|
57
|
+
stride
|
|
58
|
+
Stride of the convolution; controls the step size the kernel moves along each spatial dimension. Can be a single integer or a tuple giving a value per dimension.
|
|
59
|
+
padding
|
|
60
|
+
Padding added to each spatial dimension. Can be a single integer or a tuple specifying padding for each dimension.
|
|
61
|
+
dilation
|
|
62
|
+
Spacing between kernel elements along each spatial dimension. Can be a single integer or per-dimension tuple.
|
|
63
|
+
groups
|
|
64
|
+
Grouped convolution groups.
|
|
65
|
+
transposed
|
|
66
|
+
If True, perform transposed (fractionally-strided) convolution.
|
|
67
|
+
output_padding
|
|
68
|
+
Additional size for transposed conv output (disambiguates shape).
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
Convolved output preserving input's dimension layout.
|
|
73
|
+
"""
|
|
74
|
+
num_spatial = len(dim)
|
|
75
|
+
stride = _normalize_tuple(stride, num_spatial)
|
|
76
|
+
padding = _normalize_tuple(padding, num_spatial)
|
|
77
|
+
dilation = _normalize_tuple(dilation, num_spatial)
|
|
78
|
+
output_padding = _normalize_tuple(output_padding, num_spatial)
|
|
79
|
+
|
|
80
|
+
def conv(a: Tensor, b: Tensor) -> Tensor:
|
|
81
|
+
return _dispatch(a, b, dim, channel_dim, stride, padding, dilation, groups, transposed, output_padding)
|
|
82
|
+
|
|
83
|
+
match (x.is_complex(), weight.is_complex()):
|
|
84
|
+
case (True, True):
|
|
85
|
+
return torch.complex(
|
|
86
|
+
conv(x.real, weight.real) - conv(x.imag, weight.imag),
|
|
87
|
+
conv(x.real, weight.imag) + conv(x.imag, weight.real),
|
|
88
|
+
)
|
|
89
|
+
case (True, False):
|
|
90
|
+
return torch.complex(conv(x.real, weight), conv(x.imag, weight))
|
|
91
|
+
case (False, True):
|
|
92
|
+
return torch.complex(conv(x, weight.real), conv(x, weight.imag))
|
|
93
|
+
case _:
|
|
94
|
+
return conv(x, weight)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _dispatch(
|
|
98
|
+
x: Tensor,
|
|
99
|
+
weight: Tensor,
|
|
100
|
+
dim: tuple[int, ...],
|
|
101
|
+
channel_dim: int,
|
|
102
|
+
stride: tuple[int, ...],
|
|
103
|
+
padding: tuple[int, ...],
|
|
104
|
+
dilation: tuple[int, ...],
|
|
105
|
+
groups: int,
|
|
106
|
+
transposed: bool,
|
|
107
|
+
output_padding: tuple[int, ...],
|
|
108
|
+
) -> Tensor:
|
|
109
|
+
"""Permute to standard layout, convolve, permute back."""
|
|
110
|
+
ndim = x.ndim
|
|
111
|
+
spatial_dims = tuple(d % ndim for d in dim)
|
|
112
|
+
channel_dim_normalized = channel_dim % ndim
|
|
113
|
+
batch_dims = tuple(d for d in range(ndim) if d not in set(spatial_dims) and d != channel_dim_normalized)
|
|
114
|
+
|
|
115
|
+
to_standard = batch_dims + (channel_dim_normalized,) + spatial_dims
|
|
116
|
+
x_perm = x.permute(to_standard)
|
|
117
|
+
|
|
118
|
+
num_batch = len(batch_dims)
|
|
119
|
+
batch_shape = x_perm.shape[:num_batch]
|
|
120
|
+
x_flat = x_perm.reshape(-1 if batch_shape else 1, *x_perm.shape[num_batch:])
|
|
121
|
+
|
|
122
|
+
out_flat = _conv_core(x_flat, weight, stride, padding, dilation, groups, transposed, output_padding)
|
|
123
|
+
|
|
124
|
+
out = out_flat.reshape(*batch_shape, *out_flat.shape[1:]) if batch_shape else out_flat.squeeze(0)
|
|
125
|
+
from_standard = torch.argsort(torch.tensor(to_standard)).tolist()
|
|
126
|
+
return out.permute(from_standard)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _conv_core(
|
|
130
|
+
x: Tensor,
|
|
131
|
+
weight: Tensor,
|
|
132
|
+
stride: tuple[int, ...],
|
|
133
|
+
padding: tuple[int, ...],
|
|
134
|
+
dilation: tuple[int, ...],
|
|
135
|
+
groups: int,
|
|
136
|
+
transposed: bool,
|
|
137
|
+
output_padding: tuple[int, ...],
|
|
138
|
+
) -> Tensor:
|
|
139
|
+
"""Dispatch to native implementation or recursive fallback."""
|
|
140
|
+
num_spatial = len(stride)
|
|
141
|
+
registry = CONV_TRANSPOSE_REGISTRY if transposed else CONV_REGISTRY
|
|
142
|
+
|
|
143
|
+
if num_spatial == 0:
|
|
144
|
+
return registry[1](x.unsqueeze(-1), weight.unsqueeze(-1), groups=groups).squeeze(-1)
|
|
145
|
+
|
|
146
|
+
if num_spatial in registry:
|
|
147
|
+
kwargs = {
|
|
148
|
+
"bias": None,
|
|
149
|
+
"stride": stride,
|
|
150
|
+
"padding": padding,
|
|
151
|
+
"dilation": dilation,
|
|
152
|
+
"groups": groups,
|
|
153
|
+
}
|
|
154
|
+
if transposed:
|
|
155
|
+
kwargs["output_padding"] = output_padding
|
|
156
|
+
return registry[num_spatial](x, weight, **kwargs)
|
|
157
|
+
|
|
158
|
+
if transposed:
|
|
159
|
+
return _conv_transpose_recursive(x, weight, stride, padding, dilation, groups, output_padding)
|
|
160
|
+
return _conv_recursive(x, weight, stride, padding, dilation, groups)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _conv_recursive(
|
|
164
|
+
x: Tensor,
|
|
165
|
+
weight: Tensor,
|
|
166
|
+
stride: tuple[int, ...],
|
|
167
|
+
padding: tuple[int, ...],
|
|
168
|
+
dilation: tuple[int, ...],
|
|
169
|
+
groups: int,
|
|
170
|
+
) -> Tensor:
|
|
171
|
+
"""Decompose N-D conv: sum over first kernel dim of (N-1)-D convs."""
|
|
172
|
+
if padding[0] > 0:
|
|
173
|
+
pad_spec = (0, 0) * (x.ndim - 3) + (padding[0], padding[0])
|
|
174
|
+
x = torch.nn.functional.pad(x, pad_spec)
|
|
175
|
+
|
|
176
|
+
kernel_size = weight.shape[2]
|
|
177
|
+
input_size = x.shape[2]
|
|
178
|
+
batch_size = x.shape[0]
|
|
179
|
+
output_size = (input_size - dilation[0] * (kernel_size - 1) - 1) // stride[0] + 1
|
|
180
|
+
|
|
181
|
+
accumulator: Tensor | None = None
|
|
182
|
+
for ki in range(kernel_size):
|
|
183
|
+
start = ki * dilation[0]
|
|
184
|
+
x_slice = x[:, :, start : start + (output_size - 1) * stride[0] + 1 : stride[0]]
|
|
185
|
+
|
|
186
|
+
out = _conv_core(
|
|
187
|
+
rearrange(x_slice, "n c d ... -> (n d) c ..."),
|
|
188
|
+
weight[:, :, ki],
|
|
189
|
+
stride[1:],
|
|
190
|
+
padding[1:],
|
|
191
|
+
dilation[1:],
|
|
192
|
+
groups,
|
|
193
|
+
False,
|
|
194
|
+
(),
|
|
195
|
+
)
|
|
196
|
+
out = rearrange(out, "(n d) c ... -> n c d ...", n=batch_size, d=output_size)
|
|
197
|
+
accumulator = out if accumulator is None else accumulator + out
|
|
198
|
+
|
|
199
|
+
assert accumulator is not None
|
|
200
|
+
return accumulator
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _conv_transpose_recursive(
|
|
204
|
+
x: Tensor,
|
|
205
|
+
weight: Tensor,
|
|
206
|
+
stride: tuple[int, ...],
|
|
207
|
+
padding: tuple[int, ...],
|
|
208
|
+
dilation: tuple[int, ...],
|
|
209
|
+
groups: int,
|
|
210
|
+
output_padding: tuple[int, ...],
|
|
211
|
+
) -> Tensor:
|
|
212
|
+
"""Decompose N-D transpose conv: scatter (N-1)-D results into output."""
|
|
213
|
+
kernel_size = weight.shape[2]
|
|
214
|
+
input_size = x.shape[2]
|
|
215
|
+
batch_size = x.shape[0]
|
|
216
|
+
out_channels = weight.shape[1] * groups
|
|
217
|
+
full_size = (input_size - 1) * stride[0] + dilation[0] * (kernel_size - 1) + 1
|
|
218
|
+
|
|
219
|
+
result: Tensor | None = None
|
|
220
|
+
for ki in range(kernel_size):
|
|
221
|
+
out = _conv_core(
|
|
222
|
+
rearrange(x, "n c d ... -> (n d) c ..."),
|
|
223
|
+
weight[:, :, ki],
|
|
224
|
+
stride[1:],
|
|
225
|
+
padding[1:],
|
|
226
|
+
dilation[1:],
|
|
227
|
+
groups,
|
|
228
|
+
True,
|
|
229
|
+
output_padding[1:],
|
|
230
|
+
)
|
|
231
|
+
out = rearrange(out, "(n d) c ... -> n c d ...", n=batch_size, d=input_size)
|
|
232
|
+
|
|
233
|
+
if result is None:
|
|
234
|
+
result = x.new_zeros(batch_size, out_channels, full_size, *out.shape[3:])
|
|
235
|
+
|
|
236
|
+
start = ki * dilation[0]
|
|
237
|
+
end = start + input_size * stride[0]
|
|
238
|
+
result[:, :, start : end : stride[0]] = result[:, :, start : end : stride[0]] + out
|
|
239
|
+
|
|
240
|
+
assert result is not None
|
|
241
|
+
|
|
242
|
+
if padding[0] > 0:
|
|
243
|
+
result = result[:, :, padding[0] :]
|
|
244
|
+
|
|
245
|
+
right_adjust = padding[0] - output_padding[0]
|
|
246
|
+
if right_adjust > 0:
|
|
247
|
+
result = result[:, :, :-right_adjust]
|
|
248
|
+
elif right_adjust < 0:
|
|
249
|
+
pad_spec = (0, 0) * (result.ndim - 3) + (0, -right_adjust)
|
|
250
|
+
result = torch.nn.functional.pad(result, pad_spec)
|
|
251
|
+
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def pad_nd(
|
|
256
|
+
input: Tensor,
|
|
257
|
+
pad: Sequence[int],
|
|
258
|
+
mode: str = "constant",
|
|
259
|
+
value: float = 0.0,
|
|
260
|
+
dims: Sequence[int] | None = None,
|
|
261
|
+
) -> Tensor:
|
|
262
|
+
"""
|
|
263
|
+
N-dimensional padding/cropping supporting arbitrary dimensions.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
input
|
|
268
|
+
Input tensor.
|
|
269
|
+
pad
|
|
270
|
+
Padding sizes as pairs (left, right) per dimension.
|
|
271
|
+
Negative values crop. Without `dims`: PyTorch convention.
|
|
272
|
+
mode
|
|
273
|
+
'constant', 'circular', 'reflect', or 'replicate'.
|
|
274
|
+
value
|
|
275
|
+
Fill value for 'constant' mode.
|
|
276
|
+
dims
|
|
277
|
+
Dimensions to pad. If None, pads last N dimensions.
|
|
278
|
+
|
|
279
|
+
Returns
|
|
280
|
+
-------
|
|
281
|
+
Padded/cropped tensor.
|
|
282
|
+
"""
|
|
283
|
+
if len(pad) % 2 != 0:
|
|
284
|
+
raise ValueError("pad must have even length")
|
|
285
|
+
|
|
286
|
+
n_dims = len(pad) // 2
|
|
287
|
+
if n_dims == 0:
|
|
288
|
+
return input
|
|
289
|
+
|
|
290
|
+
ndim = input.ndim
|
|
291
|
+
|
|
292
|
+
if dims is None:
|
|
293
|
+
target_dims = list(range(ndim - 1, ndim - n_dims - 1, -1))
|
|
294
|
+
pad_pairs = [(pad[2 * i], pad[2 * i + 1]) for i in range(n_dims)]
|
|
295
|
+
else:
|
|
296
|
+
if len(dims) != n_dims:
|
|
297
|
+
raise ValueError("len(dims) must equal len(pad) // 2")
|
|
298
|
+
target_dims = [d % ndim for d in dims]
|
|
299
|
+
pad_pairs = [(pad[2 * i], pad[2 * i + 1]) for i in range(n_dims)]
|
|
300
|
+
if len(set(target_dims)) != len(target_dims):
|
|
301
|
+
raise ValueError("duplicate dimensions not supported")
|
|
302
|
+
|
|
303
|
+
if mode == "constant":
|
|
304
|
+
return _pad_constant(input, target_dims, pad_pairs, value)
|
|
305
|
+
|
|
306
|
+
return _pad_non_constant(input, target_dims, pad_pairs, mode)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def pad_or_crop_to_size(
|
|
310
|
+
input: Tensor,
|
|
311
|
+
size: Sequence[int],
|
|
312
|
+
mode: str = "constant",
|
|
313
|
+
value: float = 0.0,
|
|
314
|
+
dims: Sequence[int] | None = None,
|
|
315
|
+
) -> Tensor:
|
|
316
|
+
"""
|
|
317
|
+
Pad or crop tensor to target size, centering the original content.
|
|
318
|
+
|
|
319
|
+
Parameters
|
|
320
|
+
----------
|
|
321
|
+
input
|
|
322
|
+
Input tensor.
|
|
323
|
+
size
|
|
324
|
+
Target sizes for each dimension to adjust.
|
|
325
|
+
mode
|
|
326
|
+
'constant', 'circular', 'reflect', or 'replicate'.
|
|
327
|
+
value
|
|
328
|
+
Fill value for 'constant' mode.
|
|
329
|
+
dims
|
|
330
|
+
Dimensions to adjust. If None, adjusts last len(size) dimensions.
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
Tensor with specified dimensions resized to target.
|
|
335
|
+
"""
|
|
336
|
+
ndim = input.ndim
|
|
337
|
+
|
|
338
|
+
if dims is None:
|
|
339
|
+
target_dims = list(range(ndim - len(size), ndim))
|
|
340
|
+
else:
|
|
341
|
+
if len(dims) != len(size):
|
|
342
|
+
raise ValueError("len(dims) must equal len(size)")
|
|
343
|
+
target_dims = [d % ndim for d in dims]
|
|
344
|
+
|
|
345
|
+
pad_list: list[int] = []
|
|
346
|
+
for dim, target in zip(target_dims, size):
|
|
347
|
+
diff = target - input.shape[dim]
|
|
348
|
+
pad_list.extend([diff // 2, diff - diff // 2])
|
|
349
|
+
|
|
350
|
+
return pad_nd(input, pad_list, mode=mode, value=value, dims=target_dims)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _pad_constant(
|
|
354
|
+
input: Tensor,
|
|
355
|
+
target_dims: list[int],
|
|
356
|
+
pad_pairs: list[tuple[int, int]],
|
|
357
|
+
value: float,
|
|
358
|
+
) -> Tensor:
|
|
359
|
+
dim_to_pad = dict(zip(target_dims, pad_pairs))
|
|
360
|
+
|
|
361
|
+
full_pad: list[int] = []
|
|
362
|
+
for d in range(input.ndim - 1, -1, -1):
|
|
363
|
+
full_pad.extend(dim_to_pad.get(d, (0, 0)))
|
|
364
|
+
|
|
365
|
+
while len(full_pad) > 2 and full_pad[-2:] == [0, 0]:
|
|
366
|
+
full_pad = full_pad[:-2]
|
|
367
|
+
|
|
368
|
+
return torch.nn.functional.pad(input, full_pad, mode="constant", value=value)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _pad_non_constant(
|
|
372
|
+
input: Tensor,
|
|
373
|
+
target_dims: list[int],
|
|
374
|
+
pad_pairs: list[tuple[int, int]],
|
|
375
|
+
mode: str,
|
|
376
|
+
) -> Tensor:
|
|
377
|
+
result = input
|
|
378
|
+
|
|
379
|
+
for i in range(0, len(target_dims), 3):
|
|
380
|
+
chunk_dims = target_dims[i : i + 3]
|
|
381
|
+
chunk_pads = pad_pairs[i : i + 3]
|
|
382
|
+
|
|
383
|
+
other_dims = [d for d in range(result.ndim) if d not in chunk_dims]
|
|
384
|
+
perm = other_dims + chunk_dims
|
|
385
|
+
|
|
386
|
+
inv_perm = [0] * len(perm)
|
|
387
|
+
for old_pos, new_pos in enumerate(perm):
|
|
388
|
+
inv_perm[new_pos] = old_pos
|
|
389
|
+
|
|
390
|
+
result = result.permute(perm)
|
|
391
|
+
batch_shape = result.shape[: len(other_dims)]
|
|
392
|
+
|
|
393
|
+
if len(batch_shape) == 0:
|
|
394
|
+
result = result.unsqueeze(0)
|
|
395
|
+
elif len(batch_shape) > 1:
|
|
396
|
+
result = result.flatten(0, len(batch_shape) - 1)
|
|
397
|
+
|
|
398
|
+
n_unsqueeze = max(0, len(chunk_dims) + 2 - result.ndim)
|
|
399
|
+
for _ in range(n_unsqueeze):
|
|
400
|
+
result = result.unsqueeze(1)
|
|
401
|
+
|
|
402
|
+
flat_pad = [p for lr in reversed(chunk_pads) for p in lr]
|
|
403
|
+
result = torch.nn.functional.pad(result, flat_pad, mode=mode)
|
|
404
|
+
|
|
405
|
+
for _ in range(n_unsqueeze):
|
|
406
|
+
result = result.squeeze(1)
|
|
407
|
+
|
|
408
|
+
if len(batch_shape) == 0:
|
|
409
|
+
result = result.squeeze(0)
|
|
410
|
+
elif len(batch_shape) > 1:
|
|
411
|
+
result = result.unflatten(0, batch_shape)
|
|
412
|
+
|
|
413
|
+
result = result.permute(inv_perm)
|
|
414
|
+
|
|
415
|
+
return result
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def adjoint_pad_nd(
|
|
419
|
+
input: Tensor,
|
|
420
|
+
pad: Sequence[int],
|
|
421
|
+
mode: str = "constant",
|
|
422
|
+
value: float = 0.0,
|
|
423
|
+
dims: Sequence[int] | None = None,
|
|
424
|
+
original_sizes: Sequence[int] | None = None,
|
|
425
|
+
) -> Tensor:
|
|
426
|
+
"""
|
|
427
|
+
Adjoint of pad_nd via autograd - computes backward of padding as forward op.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
input
|
|
432
|
+
Padded tensor (output space of pad_nd).
|
|
433
|
+
pad
|
|
434
|
+
Padding sizes as pairs (left, right) per dimension (same as pad_nd).
|
|
435
|
+
mode
|
|
436
|
+
'constant', 'circular', 'reflect', or 'replicate'.
|
|
437
|
+
value
|
|
438
|
+
Fill value for 'constant' mode.
|
|
439
|
+
dims
|
|
440
|
+
Dimensions that were padded. If None, assumes last N dimensions.
|
|
441
|
+
original_sizes
|
|
442
|
+
Original sizes of the padded dimensions before padding.
|
|
443
|
+
Required to reconstruct the unpadded shape.
|
|
444
|
+
|
|
445
|
+
Returns
|
|
446
|
+
-------
|
|
447
|
+
Tensor with padding adjoint applied (unpadded shape).
|
|
448
|
+
"""
|
|
449
|
+
if len(pad) % 2 != 0:
|
|
450
|
+
raise ValueError("pad must have even length")
|
|
451
|
+
|
|
452
|
+
n_dims = len(pad) // 2
|
|
453
|
+
if n_dims == 0:
|
|
454
|
+
return input
|
|
455
|
+
|
|
456
|
+
ndim = input.ndim
|
|
457
|
+
|
|
458
|
+
if dims is None:
|
|
459
|
+
target_dims = list(range(ndim - 1, ndim - n_dims - 1, -1))
|
|
460
|
+
else:
|
|
461
|
+
if len(dims) != n_dims:
|
|
462
|
+
raise ValueError("len(dims) must equal len(pad) // 2")
|
|
463
|
+
target_dims = [d % ndim for d in dims]
|
|
464
|
+
|
|
465
|
+
if original_sizes is None:
|
|
466
|
+
original_sizes = []
|
|
467
|
+
for i, d in enumerate(target_dims):
|
|
468
|
+
left, right = pad[2 * i], pad[2 * i + 1]
|
|
469
|
+
original_sizes.append(input.shape[d] - left - right)
|
|
470
|
+
|
|
471
|
+
unpadded_shape = list(input.shape)
|
|
472
|
+
for i, d in enumerate(target_dims):
|
|
473
|
+
unpadded_shape[d] = original_sizes[i]
|
|
474
|
+
|
|
475
|
+
x = torch.zeros(unpadded_shape, device=input.device, dtype=input.dtype, requires_grad=True)
|
|
476
|
+
y = pad_nd(x, pad, mode=mode, value=value, dims=dims)
|
|
477
|
+
(gx,) = torch.autograd.grad(y, x, grad_outputs=input, retain_graph=False, create_graph=True)
|
|
478
|
+
return gx
|
torchnd/modules.py
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
"""N-dimensional convolution modules wrapping conv_nd functional interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
from torch import Tensor
|
|
10
|
+
from torch.utils.checkpoint import checkpoint
|
|
11
|
+
|
|
12
|
+
from .functional import _normalize_tuple, adjoint_pad_nd, conv_nd, pad_nd
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _ConvNdBase(torch.nn.Module, ABC):
|
|
16
|
+
"""Base class for N-dimensional convolution modules."""
|
|
17
|
+
|
|
18
|
+
transposed: bool = False
|
|
19
|
+
in_channels: int
|
|
20
|
+
out_channels: int
|
|
21
|
+
kernel_size: tuple[int, ...]
|
|
22
|
+
dim: tuple[int, ...]
|
|
23
|
+
channel_dim: int
|
|
24
|
+
stride: tuple[int, ...]
|
|
25
|
+
padding: tuple[int, ...]
|
|
26
|
+
output_padding: tuple[int, ...]
|
|
27
|
+
dilation: tuple[int, ...]
|
|
28
|
+
groups: int
|
|
29
|
+
padding_mode: tuple[str, ...]
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
in_channels: int,
|
|
34
|
+
out_channels: int,
|
|
35
|
+
kernel_size: int | tuple[int, ...],
|
|
36
|
+
dim: tuple[int, ...] = (-2, -1),
|
|
37
|
+
channel_dim: int = 1,
|
|
38
|
+
stride: int | tuple[int, ...] = 1,
|
|
39
|
+
padding: int | tuple[int, ...] = 0,
|
|
40
|
+
output_padding: int | tuple[int, ...] = 0,
|
|
41
|
+
dilation: int | tuple[int, ...] = 1,
|
|
42
|
+
groups: int = 1,
|
|
43
|
+
bias: bool | Tensor = True,
|
|
44
|
+
padding_mode: str | tuple[str, ...] = "zeros",
|
|
45
|
+
device: torch.device | None = None,
|
|
46
|
+
dtype: torch.dtype | None = None,
|
|
47
|
+
weight: Tensor | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Initialize N-dimensional convolution.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
in_channels
|
|
54
|
+
Number of input channels.
|
|
55
|
+
out_channels
|
|
56
|
+
Number of output channels.
|
|
57
|
+
kernel_size
|
|
58
|
+
Size of the convolving kernel.
|
|
59
|
+
dim
|
|
60
|
+
Spatial dimensions to convolve over.
|
|
61
|
+
channel_dim
|
|
62
|
+
Channel dimension index.
|
|
63
|
+
stride
|
|
64
|
+
Stride of the convolution.
|
|
65
|
+
padding
|
|
66
|
+
Padding added to each spatial dimension.
|
|
67
|
+
output_padding
|
|
68
|
+
Additional size added to output shape (transposed conv only).
|
|
69
|
+
dilation
|
|
70
|
+
Spacing between kernel elements.
|
|
71
|
+
groups
|
|
72
|
+
Number of blocked connections from input to output channels.
|
|
73
|
+
bias
|
|
74
|
+
If True, adds learnable bias. If Tensor, uses as external bias.
|
|
75
|
+
padding_mode
|
|
76
|
+
Supported padding modes are 'zeros', 'reflect', 'circular', and 'replicate'.
|
|
77
|
+
Can be a single string (applied to all dims) or a tuple of strings per dim.
|
|
78
|
+
device
|
|
79
|
+
Device for parameters. External weights/bias are moved to this device.
|
|
80
|
+
dtype
|
|
81
|
+
Data type for parameters. Complex dtypes enable complex convolution.
|
|
82
|
+
External weights/bias are cast to this dtype.
|
|
83
|
+
weight
|
|
84
|
+
If provided, uses as external weight (enables hypernetwork usage).
|
|
85
|
+
Registered as buffer to be included in state_dict.
|
|
86
|
+
"""
|
|
87
|
+
super().__init__()
|
|
88
|
+
|
|
89
|
+
if in_channels % groups != 0:
|
|
90
|
+
raise ValueError(f"in_channels ({in_channels}) must be divisible by groups ({groups})")
|
|
91
|
+
if out_channels % groups != 0:
|
|
92
|
+
raise ValueError(f"out_channels ({out_channels}) must be divisible by groups ({groups})")
|
|
93
|
+
|
|
94
|
+
num_spatial = len(dim)
|
|
95
|
+
self.in_channels = in_channels
|
|
96
|
+
self.out_channels = out_channels
|
|
97
|
+
self.kernel_size = _normalize_tuple(kernel_size, num_spatial)
|
|
98
|
+
self.dim = dim
|
|
99
|
+
self.channel_dim = channel_dim
|
|
100
|
+
self.stride = _normalize_tuple(stride, num_spatial)
|
|
101
|
+
self.padding = _normalize_tuple(padding, num_spatial)
|
|
102
|
+
self.output_padding = _normalize_tuple(output_padding, num_spatial)
|
|
103
|
+
self.dilation = _normalize_tuple(dilation, num_spatial)
|
|
104
|
+
self.groups = groups
|
|
105
|
+
|
|
106
|
+
if isinstance(padding_mode, str):
|
|
107
|
+
self.padding_mode = (padding_mode,) * num_spatial
|
|
108
|
+
else:
|
|
109
|
+
self.padding_mode = tuple(padding_mode)
|
|
110
|
+
|
|
111
|
+
if self.transposed:
|
|
112
|
+
weight_shape = (in_channels, out_channels // groups, *self.kernel_size)
|
|
113
|
+
else:
|
|
114
|
+
weight_shape = (out_channels, in_channels // groups, *self.kernel_size)
|
|
115
|
+
|
|
116
|
+
for mode in self.padding_mode:
|
|
117
|
+
if mode not in ("zeros", "reflect", "circular", "replicate"):
|
|
118
|
+
raise ValueError(f"padding_mode '{mode}' is not supported")
|
|
119
|
+
|
|
120
|
+
if weight is not None:
|
|
121
|
+
weight = self._apply_device_dtype(weight, device, dtype)
|
|
122
|
+
self.register_buffer("weight", weight)
|
|
123
|
+
else:
|
|
124
|
+
self.weight = torch.nn.Parameter(torch.empty(weight_shape, device=device, dtype=dtype))
|
|
125
|
+
|
|
126
|
+
if isinstance(bias, Tensor):
|
|
127
|
+
bias = self._apply_device_dtype(bias, device, dtype)
|
|
128
|
+
self.register_buffer("bias", bias)
|
|
129
|
+
elif bias:
|
|
130
|
+
self.bias = torch.nn.Parameter(torch.empty(out_channels, device=device, dtype=dtype))
|
|
131
|
+
else:
|
|
132
|
+
self.register_buffer("bias", None)
|
|
133
|
+
|
|
134
|
+
self.reset_parameters()
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _apply_device_dtype(
|
|
138
|
+
tensor: Tensor,
|
|
139
|
+
device: torch.device | None,
|
|
140
|
+
dtype: torch.dtype | None,
|
|
141
|
+
) -> Tensor:
|
|
142
|
+
if device is not None or dtype is not None:
|
|
143
|
+
tensor = tensor.to(device=device, dtype=dtype)
|
|
144
|
+
return tensor
|
|
145
|
+
|
|
146
|
+
def reset_parameters(self) -> None:
|
|
147
|
+
"""Initialize learnable parameters using Kaiming uniform initialization."""
|
|
148
|
+
if not isinstance(self.weight, torch.nn.Parameter):
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
fan_in = self.weight.shape[1] * math.prod(self.weight.shape[2:])
|
|
152
|
+
|
|
153
|
+
if self.weight.is_complex():
|
|
154
|
+
std = 1.0 / math.sqrt(fan_in * 2)
|
|
155
|
+
bound = math.sqrt(3.0) * std
|
|
156
|
+
with torch.no_grad():
|
|
157
|
+
self.weight.real.uniform_(-bound, bound)
|
|
158
|
+
self.weight.imag.uniform_(-bound, bound)
|
|
159
|
+
else:
|
|
160
|
+
torch.nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))
|
|
161
|
+
|
|
162
|
+
if isinstance(self.bias, torch.nn.Parameter):
|
|
163
|
+
bound = 1.0 / math.sqrt(fan_in) if fan_in > 0 else 0
|
|
164
|
+
with torch.no_grad():
|
|
165
|
+
if self.bias.is_complex():
|
|
166
|
+
self.bias.real.uniform_(-bound, bound)
|
|
167
|
+
self.bias.imag.uniform_(-bound, bound)
|
|
168
|
+
else:
|
|
169
|
+
self.bias.uniform_(-bound, bound)
|
|
170
|
+
|
|
171
|
+
@abstractmethod
|
|
172
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
173
|
+
"""Forward pass of the convolution."""
|
|
174
|
+
raise NotImplementedError
|
|
175
|
+
|
|
176
|
+
@abstractmethod
|
|
177
|
+
def adjoint(self, x: Tensor) -> Tensor: # noqa: ARG002
|
|
178
|
+
"""Return the adjoint convolution.
|
|
179
|
+
|
|
180
|
+
The adjoint of a convolution is the transposed convolution with
|
|
181
|
+
conjugated weights.
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
x
|
|
186
|
+
Input tensor.
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
Adjoint convolution.
|
|
190
|
+
"""
|
|
191
|
+
raise NotImplementedError
|
|
192
|
+
|
|
193
|
+
def extra_repr(self) -> str:
|
|
194
|
+
parts = [
|
|
195
|
+
f"{self.in_channels}",
|
|
196
|
+
f"{self.out_channels}",
|
|
197
|
+
f"kernel_size={self.kernel_size}",
|
|
198
|
+
f"stride={self.stride}",
|
|
199
|
+
]
|
|
200
|
+
if self.padding != (0,) * len(self.padding):
|
|
201
|
+
parts.append(f"padding={self.padding}")
|
|
202
|
+
if self.dilation != (1,) * len(self.dilation):
|
|
203
|
+
parts.append(f"dilation={self.dilation}")
|
|
204
|
+
if self.transposed and self.output_padding != (0,) * len(self.output_padding):
|
|
205
|
+
parts.append(f"output_padding={self.output_padding}")
|
|
206
|
+
if self.groups != 1:
|
|
207
|
+
parts.append(f"groups={self.groups}")
|
|
208
|
+
if self.bias is None:
|
|
209
|
+
parts.append("bias=False")
|
|
210
|
+
if not all(m == "zeros" for m in self.padding_mode):
|
|
211
|
+
# Show single value if all same, otherwise show tuple
|
|
212
|
+
if len(set(self.padding_mode)) == 1:
|
|
213
|
+
parts.append(f"padding_mode={self.padding_mode[0]!r}")
|
|
214
|
+
else:
|
|
215
|
+
parts.append(f"padding_mode={self.padding_mode}")
|
|
216
|
+
if self.dim != (-2, -1):
|
|
217
|
+
parts.append(f"dim={self.dim}")
|
|
218
|
+
if self.channel_dim != 1:
|
|
219
|
+
parts.append(f"channel_dim={self.channel_dim}")
|
|
220
|
+
if not isinstance(self.weight, torch.nn.Parameter):
|
|
221
|
+
parts.append("weight=external")
|
|
222
|
+
if self.bias is not None and not isinstance(self.bias, torch.nn.Parameter):
|
|
223
|
+
parts.append("bias=external")
|
|
224
|
+
return ", ".join(parts)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class ConvNd(_ConvNdBase):
|
|
228
|
+
"""N-dimensional convolution with flexible dimension specification.
|
|
229
|
+
|
|
230
|
+
Supports arbitrary spatial dimensions and dimension layouts. When native
|
|
231
|
+
PyTorch implementations (1D, 2D, 3D) are unavailable, uses recursive
|
|
232
|
+
decomposition.
|
|
233
|
+
|
|
234
|
+
Examples
|
|
235
|
+
--------
|
|
236
|
+
>>> conv = ConvNd(3, 64, kernel_size=3, dim=(-2, -1))
|
|
237
|
+
>>> x = torch.randn(2, 3, 32, 32)
|
|
238
|
+
>>> y = conv(x)
|
|
239
|
+
|
|
240
|
+
>>> # Hypernetwork usage
|
|
241
|
+
>>> external_weight = hyper_net(z)
|
|
242
|
+
>>> conv = ConvNd(3, 64, kernel_size=3, weight=external_weight, bias=False)
|
|
243
|
+
>>> y = conv(x) # Gradients flow through hyper_net
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
transposed = False
|
|
247
|
+
|
|
248
|
+
def __init__(
|
|
249
|
+
self,
|
|
250
|
+
in_channels: int,
|
|
251
|
+
out_channels: int,
|
|
252
|
+
kernel_size: int | tuple[int, ...],
|
|
253
|
+
dim: tuple[int, ...] = (-2, -1),
|
|
254
|
+
channel_dim: int = 1,
|
|
255
|
+
stride: int | tuple[int, ...] = 1,
|
|
256
|
+
padding: int | tuple[int, ...] = 0,
|
|
257
|
+
dilation: int | tuple[int, ...] = 1,
|
|
258
|
+
groups: int = 1,
|
|
259
|
+
bias: bool | Tensor = True,
|
|
260
|
+
padding_mode: str | tuple[str, ...] = "zeros",
|
|
261
|
+
device: torch.device | None = None,
|
|
262
|
+
dtype: torch.dtype | None = None,
|
|
263
|
+
weight: Tensor | None = None,
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Initialize N-dimensional convolution.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
in_channels
|
|
270
|
+
Number of input channels.
|
|
271
|
+
out_channels
|
|
272
|
+
Number of output channels.
|
|
273
|
+
kernel_size
|
|
274
|
+
Size of the convolving kernel.
|
|
275
|
+
dim
|
|
276
|
+
Spatial dimensions to convolve over.
|
|
277
|
+
channel_dim
|
|
278
|
+
Channel dimension index.
|
|
279
|
+
stride
|
|
280
|
+
Stride of the convolution.
|
|
281
|
+
padding
|
|
282
|
+
Padding added to each spatial dimension.
|
|
283
|
+
dilation
|
|
284
|
+
Spacing between kernel elements.
|
|
285
|
+
groups
|
|
286
|
+
Number of blocked connections from input to output channels.
|
|
287
|
+
bias
|
|
288
|
+
If True, adds learnable bias. If Tensor, uses as external bias.
|
|
289
|
+
padding_mode
|
|
290
|
+
Supported padding modes are 'zeros', 'reflect', 'circular', and 'replicate'.
|
|
291
|
+
Can be a single string (applied to all dims) or a tuple of strings per dim.
|
|
292
|
+
device
|
|
293
|
+
Device for parameters. External weights/bias are moved to this device.
|
|
294
|
+
dtype
|
|
295
|
+
Data type for parameters. Complex dtypes enable complex convolution.
|
|
296
|
+
weight
|
|
297
|
+
If provided, uses as external weight. Registered as buffer for state_dict.
|
|
298
|
+
"""
|
|
299
|
+
super().__init__(
|
|
300
|
+
in_channels=in_channels,
|
|
301
|
+
out_channels=out_channels,
|
|
302
|
+
kernel_size=kernel_size,
|
|
303
|
+
dim=dim,
|
|
304
|
+
channel_dim=channel_dim,
|
|
305
|
+
stride=stride,
|
|
306
|
+
padding=padding,
|
|
307
|
+
output_padding=0,
|
|
308
|
+
dilation=dilation,
|
|
309
|
+
groups=groups,
|
|
310
|
+
bias=bias,
|
|
311
|
+
padding_mode=padding_mode,
|
|
312
|
+
device=device,
|
|
313
|
+
dtype=dtype,
|
|
314
|
+
weight=weight,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
318
|
+
"""Apply the forward convolution operator.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
x
|
|
323
|
+
Input tensor.
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
Output tensor.
|
|
328
|
+
"""
|
|
329
|
+
effective_padding = self.padding
|
|
330
|
+
|
|
331
|
+
if not all(m == "zeros" for m in self.padding_mode):
|
|
332
|
+
effective_padding = tuple(0 if m != "zeros" and p > 0 else p for m, p in zip(self.padding_mode, self.padding))
|
|
333
|
+
for mode in ("reflect", "replicate", "circular"):
|
|
334
|
+
dims_idx = [i for i, (m, p) in enumerate(zip(self.padding_mode, self.padding)) if m == mode and p > 0]
|
|
335
|
+
if dims_idx:
|
|
336
|
+
spatial_dims = tuple(self.dim[i] for i in dims_idx)
|
|
337
|
+
pad_spec = [v for i in dims_idx for v in (self.padding[i], self.padding[i])]
|
|
338
|
+
x = checkpoint(pad_nd, x, pad_spec, mode, 0.0, spatial_dims, use_reentrant=False)
|
|
339
|
+
|
|
340
|
+
out = conv_nd(
|
|
341
|
+
x,
|
|
342
|
+
self.weight,
|
|
343
|
+
dim=self.dim,
|
|
344
|
+
channel_dim=self.channel_dim,
|
|
345
|
+
stride=self.stride,
|
|
346
|
+
padding=effective_padding,
|
|
347
|
+
dilation=self.dilation,
|
|
348
|
+
groups=self.groups,
|
|
349
|
+
transposed=False,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if self.bias is not None:
|
|
353
|
+
bias_shape = [1] * out.ndim
|
|
354
|
+
bias_shape[self.channel_dim % out.ndim] = -1
|
|
355
|
+
out = out + self.bias.view(*bias_shape)
|
|
356
|
+
|
|
357
|
+
return out
|
|
358
|
+
|
|
359
|
+
def adjoint(self, x: Tensor, input_shape: tuple[int, ...] | None = None) -> Tensor:
|
|
360
|
+
"""Apply the adjoint convolution operator.
|
|
361
|
+
|
|
362
|
+
The adjoint of a convolution is the transposed convolution with
|
|
363
|
+
conjugated weights (for complex) or identical weights (for real).
|
|
364
|
+
|
|
365
|
+
This method applies the adjoint using the same weights (and does not
|
|
366
|
+
allocate a new module). Intended for iterative algorithms, not separate
|
|
367
|
+
training of an adjoint operator.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
x
|
|
372
|
+
Input tensor in the output space of the forward operator.
|
|
373
|
+
input_shape
|
|
374
|
+
Spatial shape of the input to the forward operator. Needed for
|
|
375
|
+
strided ConvNd to determine output_padding of the adjoint. For
|
|
376
|
+
stride > 1, multiple input shapes can map to the same output shape,
|
|
377
|
+
so the adjoint output size is ambiguous without this information.
|
|
378
|
+
|
|
379
|
+
Returns
|
|
380
|
+
-------
|
|
381
|
+
Result of applying the adjoint operator.
|
|
382
|
+
|
|
383
|
+
Raises
|
|
384
|
+
------
|
|
385
|
+
ValueError
|
|
386
|
+
If bias is present (adjoint undefined with bias).
|
|
387
|
+
If input_shape is missing when any stride != 1.
|
|
388
|
+
"""
|
|
389
|
+
if self.bias is not None:
|
|
390
|
+
raise ValueError("Adjoint is not defined for convolution with bias")
|
|
391
|
+
|
|
392
|
+
effective_padding = self.padding
|
|
393
|
+
if not all(m == "zeros" for m in self.padding_mode):
|
|
394
|
+
effective_padding = tuple(0 if m != "zeros" and p > 0 else p for m, p in zip(self.padding_mode, self.padding))
|
|
395
|
+
|
|
396
|
+
num_spatial = len(self.dim)
|
|
397
|
+
if input_shape is None:
|
|
398
|
+
if any(s != 1 for s in self.stride):
|
|
399
|
+
raise ValueError("input_shape is required for ConvNd.adjoint when any stride != 1")
|
|
400
|
+
out_pad = (0,) * num_spatial
|
|
401
|
+
else:
|
|
402
|
+
if len(input_shape) != num_spatial:
|
|
403
|
+
raise ValueError(f"input_shape must have length {num_spatial}, got {len(input_shape)}")
|
|
404
|
+
|
|
405
|
+
pads: list[int] = []
|
|
406
|
+
for i, (n_in, k, s, p, d) in enumerate(zip(input_shape, self.kernel_size, self.stride, self.padding, self.dilation)):
|
|
407
|
+
eff_p = effective_padding[i]
|
|
408
|
+
n_out = (n_in + 2 * p - d * (k - 1) - 1) // s + 1
|
|
409
|
+
base = (n_out - 1) * s - 2 * eff_p + d * (k - 1) + 1
|
|
410
|
+
op = (n_in + 2 * (p - eff_p)) - base
|
|
411
|
+
if op < 0 or op >= s:
|
|
412
|
+
raise RuntimeError(
|
|
413
|
+
"Computed output_padding is invalid. "
|
|
414
|
+
f"Got output_padding={op} for stride={s} "
|
|
415
|
+
f"(input={n_in}, kernel={k}, pad={p}, dilation={d})."
|
|
416
|
+
)
|
|
417
|
+
pads.append(op)
|
|
418
|
+
out_pad = tuple(pads)
|
|
419
|
+
|
|
420
|
+
w = self.weight.conj() if self.weight.is_complex() else self.weight
|
|
421
|
+
out = conv_nd(
|
|
422
|
+
x,
|
|
423
|
+
w,
|
|
424
|
+
dim=self.dim,
|
|
425
|
+
channel_dim=self.channel_dim,
|
|
426
|
+
stride=self.stride,
|
|
427
|
+
padding=effective_padding,
|
|
428
|
+
dilation=self.dilation,
|
|
429
|
+
groups=self.groups,
|
|
430
|
+
transposed=True,
|
|
431
|
+
output_padding=out_pad,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if not all(m == "zeros" for m in self.padding_mode):
|
|
435
|
+
for mode in ("reflect", "replicate", "circular"):
|
|
436
|
+
dims_idx = [i for i, (m, p) in enumerate(zip(self.padding_mode, self.padding)) if m == mode and p > 0]
|
|
437
|
+
if dims_idx:
|
|
438
|
+
spatial_dims = tuple(self.dim[i] for i in dims_idx)
|
|
439
|
+
pad_spec = [v for i in dims_idx for v in (self.padding[i], self.padding[i])]
|
|
440
|
+
original_sizes = [input_shape[i] for i in dims_idx] if input_shape else None
|
|
441
|
+
out = adjoint_pad_nd(out, pad_spec, mode=mode, dims=spatial_dims, original_sizes=original_sizes)
|
|
442
|
+
|
|
443
|
+
return out
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class ConvTransposeNd(_ConvNdBase):
|
|
447
|
+
"""N-dimensional transposed convolution with flexible dimension specification.
|
|
448
|
+
|
|
449
|
+
Also known as fractionally-strided convolution or deconvolution.
|
|
450
|
+
|
|
451
|
+
Examples
|
|
452
|
+
--------
|
|
453
|
+
>>> conv_t = ConvTransposeNd(64, 3, kernel_size=3, stride=2, dim=(-2, -1))
|
|
454
|
+
>>> x = torch.randn(2, 64, 16, 16)
|
|
455
|
+
>>> y = conv_t(x)
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
transposed = True
|
|
459
|
+
|
|
460
|
+
def __init__(
|
|
461
|
+
self,
|
|
462
|
+
in_channels: int,
|
|
463
|
+
out_channels: int,
|
|
464
|
+
kernel_size: int | tuple[int, ...],
|
|
465
|
+
dim: tuple[int, ...] = (-2, -1),
|
|
466
|
+
channel_dim: int = 1,
|
|
467
|
+
stride: int | tuple[int, ...] = 1,
|
|
468
|
+
padding: int | tuple[int, ...] = 0,
|
|
469
|
+
output_padding: int | tuple[int, ...] = 0,
|
|
470
|
+
dilation: int | tuple[int, ...] = 1,
|
|
471
|
+
groups: int = 1,
|
|
472
|
+
bias: bool | Tensor = True,
|
|
473
|
+
padding_mode: str | tuple[str, ...] = "zeros",
|
|
474
|
+
device: torch.device | None = None,
|
|
475
|
+
dtype: torch.dtype | None = None,
|
|
476
|
+
weight: Tensor | None = None,
|
|
477
|
+
) -> None:
|
|
478
|
+
"""Initialize N-dimensional transposed convolution.
|
|
479
|
+
|
|
480
|
+
Parameters
|
|
481
|
+
----------
|
|
482
|
+
in_channels
|
|
483
|
+
Number of input channels.
|
|
484
|
+
out_channels
|
|
485
|
+
Number of output channels.
|
|
486
|
+
kernel_size
|
|
487
|
+
Size of the convolving kernel.
|
|
488
|
+
dim
|
|
489
|
+
Spatial dimensions to convolve over.
|
|
490
|
+
channel_dim
|
|
491
|
+
Channel dimension index.
|
|
492
|
+
stride
|
|
493
|
+
Stride of the convolution.
|
|
494
|
+
padding
|
|
495
|
+
Padding added to each spatial dimension.
|
|
496
|
+
output_padding
|
|
497
|
+
Additional size added to output shape to disambiguate when stride > 1.
|
|
498
|
+
dilation
|
|
499
|
+
Spacing between kernel elements.
|
|
500
|
+
groups
|
|
501
|
+
Number of blocked connections from input to output channels.
|
|
502
|
+
bias
|
|
503
|
+
If True, adds learnable bias. If Tensor, uses as external bias.
|
|
504
|
+
padding_mode
|
|
505
|
+
Supported padding modes are 'zeros', 'reflect', 'circular', and 'replicate'.
|
|
506
|
+
Can be a single string (applied to all dims) or a tuple of strings per dim.
|
|
507
|
+
device
|
|
508
|
+
Device for parameters. External weights/bias are moved to this device.
|
|
509
|
+
dtype
|
|
510
|
+
Data type for parameters. Complex dtypes enable complex convolution.
|
|
511
|
+
weight
|
|
512
|
+
If provided, uses as external weight. Registered as buffer for state_dict.
|
|
513
|
+
"""
|
|
514
|
+
super().__init__(
|
|
515
|
+
in_channels=in_channels,
|
|
516
|
+
out_channels=out_channels,
|
|
517
|
+
kernel_size=kernel_size,
|
|
518
|
+
dim=dim,
|
|
519
|
+
channel_dim=channel_dim,
|
|
520
|
+
stride=stride,
|
|
521
|
+
padding=padding,
|
|
522
|
+
output_padding=output_padding,
|
|
523
|
+
dilation=dilation,
|
|
524
|
+
groups=groups,
|
|
525
|
+
bias=bias,
|
|
526
|
+
padding_mode=padding_mode,
|
|
527
|
+
device=device,
|
|
528
|
+
dtype=dtype,
|
|
529
|
+
weight=weight,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
533
|
+
"""Apply the forward convolution operator.
|
|
534
|
+
|
|
535
|
+
Parameters
|
|
536
|
+
----------
|
|
537
|
+
x
|
|
538
|
+
Input tensor.
|
|
539
|
+
|
|
540
|
+
Returns
|
|
541
|
+
-------
|
|
542
|
+
Output tensor.
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
effective_padding = self.padding
|
|
546
|
+
if not all(m == "zeros" for m in self.padding_mode):
|
|
547
|
+
effective_padding = tuple(0 if m != "zeros" and p > 0 else p for m, p in zip(self.padding_mode, self.padding))
|
|
548
|
+
|
|
549
|
+
out = conv_nd(
|
|
550
|
+
x,
|
|
551
|
+
self.weight,
|
|
552
|
+
dim=self.dim,
|
|
553
|
+
channel_dim=self.channel_dim,
|
|
554
|
+
stride=self.stride,
|
|
555
|
+
padding=effective_padding,
|
|
556
|
+
dilation=self.dilation,
|
|
557
|
+
groups=self.groups,
|
|
558
|
+
transposed=True,
|
|
559
|
+
output_padding=self.output_padding,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
if not all(m == "zeros" for m in self.padding_mode):
|
|
563
|
+
for mode in ("reflect", "replicate", "circular"):
|
|
564
|
+
dims_idx = [i for i, (m, p) in enumerate(zip(self.padding_mode, self.padding)) if m == mode and p > 0]
|
|
565
|
+
if dims_idx:
|
|
566
|
+
spatial_dims = tuple(self.dim[i] for i in dims_idx)
|
|
567
|
+
pad_spec = [v for i in dims_idx for v in (self.padding[i], self.padding[i])]
|
|
568
|
+
original_sizes = [out.shape[self.dim[i]] - 2 * self.padding[i] for i in dims_idx]
|
|
569
|
+
out = checkpoint(
|
|
570
|
+
adjoint_pad_nd,
|
|
571
|
+
out,
|
|
572
|
+
pad_spec,
|
|
573
|
+
mode,
|
|
574
|
+
0.0,
|
|
575
|
+
spatial_dims,
|
|
576
|
+
original_sizes,
|
|
577
|
+
use_reentrant=False,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
if self.bias is not None:
|
|
581
|
+
bias_shape = [1] * out.ndim
|
|
582
|
+
bias_shape[self.channel_dim % out.ndim] = -1
|
|
583
|
+
out = out + self.bias.view(*bias_shape)
|
|
584
|
+
|
|
585
|
+
return out
|
|
586
|
+
|
|
587
|
+
def adjoint(self, x: Tensor) -> Tensor:
|
|
588
|
+
"""Apply the adjoint convolution operator.
|
|
589
|
+
|
|
590
|
+
The adjoint of a transposed convolution is the (forward) convolution with
|
|
591
|
+
conjugated weights (for complex) or identical weights (for real).
|
|
592
|
+
|
|
593
|
+
This method applies the adjoint using the same weights (and does not
|
|
594
|
+
allocate a new module). Intended for iterative algorithms, not separate
|
|
595
|
+
training of an adjoint operator.
|
|
596
|
+
|
|
597
|
+
Parameters
|
|
598
|
+
----------
|
|
599
|
+
x
|
|
600
|
+
Input tensor in the output space of the forward operator.
|
|
601
|
+
|
|
602
|
+
Returns
|
|
603
|
+
-------
|
|
604
|
+
Result of applying the adjoint operator.
|
|
605
|
+
|
|
606
|
+
Raises
|
|
607
|
+
------
|
|
608
|
+
ValueError
|
|
609
|
+
If bias is present (adjoint undefined with bias).
|
|
610
|
+
"""
|
|
611
|
+
if self.bias is not None:
|
|
612
|
+
raise ValueError("Adjoint is not defined for convolution with bias")
|
|
613
|
+
|
|
614
|
+
effective_padding = self.padding
|
|
615
|
+
if not all(m == "zeros" for m in self.padding_mode):
|
|
616
|
+
effective_padding = tuple(0 if m != "zeros" and p > 0 else p for m, p in zip(self.padding_mode, self.padding))
|
|
617
|
+
for mode in ("reflect", "replicate", "circular"):
|
|
618
|
+
dims_idx = [i for i, (m, p) in enumerate(zip(self.padding_mode, self.padding)) if m == mode and p > 0]
|
|
619
|
+
if dims_idx:
|
|
620
|
+
spatial_dims = tuple(self.dim[i] for i in dims_idx)
|
|
621
|
+
pad_spec = [v for i in dims_idx for v in (self.padding[i], self.padding[i])]
|
|
622
|
+
x = pad_nd(x, pad_spec, mode=mode, dims=spatial_dims)
|
|
623
|
+
|
|
624
|
+
w = self.weight.conj() if self.weight.is_complex() else self.weight
|
|
625
|
+
return conv_nd(
|
|
626
|
+
x,
|
|
627
|
+
w,
|
|
628
|
+
dim=self.dim,
|
|
629
|
+
channel_dim=self.channel_dim,
|
|
630
|
+
stride=self.stride,
|
|
631
|
+
padding=effective_padding,
|
|
632
|
+
dilation=self.dilation,
|
|
633
|
+
groups=self.groups,
|
|
634
|
+
transposed=False,
|
|
635
|
+
)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: torchnd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ND Convolution for PyTorch
|
|
5
|
+
Author-email: Felix Zimmermann <fzimmermann89@gmail.com>
|
|
6
|
+
License: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/fzimmermann89/torch_conv_nd
|
|
8
|
+
Project-URL: Repository, https://github.com/fzimmermann89/torch_conv_nd
|
|
9
|
+
Project-URL: Issues, https://github.com/fzimmermann89/torch_conv_nd/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: torch>=2.0.0
|
|
23
|
+
Requires-Dist: einops>=0.6.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.5.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pre-commit>=3.4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: types-setuptools; extra == "dev"
|
|
31
|
+
|
|
32
|
+
# torchnd
|
|
33
|
+
|
|
34
|
+
N-dimensional convolution for PyTorch that works with any number of spatial dimensions, supporting groups, dilation, transposed convolution, complex numbers, and arbitrary dimension layouts.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install torchnd
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
**2D convolution:**
|
|
45
|
+
```python
|
|
46
|
+
import torch
|
|
47
|
+
from torchnd import conv_nd
|
|
48
|
+
|
|
49
|
+
x = torch.randn(2, 4, 16, 16)
|
|
50
|
+
weight = torch.randn(8, 4, 3, 3)
|
|
51
|
+
out = conv_nd(x, weight, dim=(-2, -1), padding=1, stride=2)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**4D convolution** (batch × time × height × width):
|
|
55
|
+
```python
|
|
56
|
+
x = torch.randn(2, 4, 10, 16, 16)
|
|
57
|
+
weight = torch.randn(8, 4, 3, 3, 3)
|
|
58
|
+
out = conv_nd(x, weight, dim=(-3, -2, -1), padding=1)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Channel-last layout:**
|
|
62
|
+
```python
|
|
63
|
+
x = torch.randn(2, 16, 16, 4)
|
|
64
|
+
weight = torch.randn(8, 4, 3, 3)
|
|
65
|
+
out = conv_nd(x, weight, dim=(1, 2), channel_dim=-1, padding=1)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Transposed convolution:**
|
|
69
|
+
```python
|
|
70
|
+
x = torch.randn(2, 4, 8, 8)
|
|
71
|
+
weight = torch.randn(4, 8, 3, 3)
|
|
72
|
+
out = conv_nd(x, weight, dim=(-2, -1), padding=1, stride=2, transposed=True)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Complex numbers:**
|
|
76
|
+
```python
|
|
77
|
+
x = torch.randn(2, 4, 16, 16, dtype=torch.complex64)
|
|
78
|
+
weight = torch.randn(8, 4, 3, 3, dtype=torch.complex64)
|
|
79
|
+
out = conv_nd(x, weight, dim=(-2, -1), padding=1)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Asymmetric parameters per dimension:**
|
|
83
|
+
```python
|
|
84
|
+
x = torch.randn(2, 4, 16, 16)
|
|
85
|
+
weight = torch.randn(8, 4, 3, 3)
|
|
86
|
+
out = conv_nd(
|
|
87
|
+
x, weight,
|
|
88
|
+
dim=(-2, -1),
|
|
89
|
+
stride=(2, 1),
|
|
90
|
+
padding=(1, 2),
|
|
91
|
+
dilation=(1, 2)
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Modules:**
|
|
96
|
+
```python
|
|
97
|
+
from torchnd import ConvNd, ConvTransposeNd
|
|
98
|
+
|
|
99
|
+
conv = ConvNd(4, 8, 3, dim=(-2, -1), padding=1)
|
|
100
|
+
out = conv(x)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Padding:**
|
|
104
|
+
```python
|
|
105
|
+
from torchnd import pad_nd, adjoint_pad_nd
|
|
106
|
+
|
|
107
|
+
# Pad or crop arbitrary dimensions
|
|
108
|
+
padded = pad_nd(x, pad=(1, 1, 2, 2), dims=(-2, -1), mode="reflect")
|
|
109
|
+
cropped = pad_nd(x, pad=(-1, -1), dims=(0,)) # Negative values crop
|
|
110
|
+
|
|
111
|
+
# Adjoint of padding (unpads)
|
|
112
|
+
unpadded = adjoint_pad_nd(padded, pad=(1, 1, 2, 2), dims=(-2, -1))
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Adjoint operators:**
|
|
116
|
+
```python
|
|
117
|
+
from torchnd import ConvNd
|
|
118
|
+
|
|
119
|
+
conv = ConvNd(4, 8, 3, dim=(-2, -1), padding=1, bias=False)
|
|
120
|
+
x = torch.randn(2, 4, 16, 16)
|
|
121
|
+
y = torch.randn_like(conv(x))
|
|
122
|
+
|
|
123
|
+
# Adjoint satisfies: <conv(x), y> = <x, conv.adjoint(y)>
|
|
124
|
+
# For ConvNd: adjoint is transposed convolution with conjugated weights
|
|
125
|
+
adj_y = conv.adjoint(y, input_shape=(16, 16))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Functions
|
|
129
|
+
|
|
130
|
+
### pad_nd
|
|
131
|
+
|
|
132
|
+
N-dimensional padding and cropping with flexible dimension specification. Supports arbitrary dimension layouts, not limited to the last N dimensions. Negative padding values crop the tensor.
|
|
133
|
+
|
|
134
|
+
Modes: `constant`, `reflect`, `replicate`, `circular`. Each dimension can use a different mode. For constant mode, negative padding crops symmetrically. For non-constant modes, negative padding is not supported.
|
|
135
|
+
|
|
136
|
+
The `dims` parameter specifies which dimensions to pad. If `None`, pads the last N dimensions where N = len(pad) // 2. Padding is specified as pairs (left, right) per dimension.
|
|
137
|
+
|
|
138
|
+
### adjoint_pad_nd
|
|
139
|
+
|
|
140
|
+
Computes the adjoint of `pad_nd` via autograd. For a padding operator P, the adjoint P* satisfies the inner product identity: `<Px, y> = <x, P*y>` for all x, y. The adjoint unpads the tensor, summing contributions from padded regions back into the original shape. For constant/zeros mode, the adjoint is equivalent to cropping. For other modes, it correctly handles the adjoint of the boundary extension.
|
|
141
|
+
|
|
142
|
+
### Adjoint convolution
|
|
143
|
+
|
|
144
|
+
The `ConvNd` and `ConvTransposeNd` modules provide `adjoint()` methods. For `ConvNd`, the adjoint is the transposed convolution with conjugated weights (for complex) or identical weights (for real). For `ConvTransposeNd`, the adjoint is the forward convolution with conjugated weights.
|
|
145
|
+
|
|
146
|
+
The adjoint satisfies the inner product identity: `<Ax, y> = <x, A*y>` where A is the convolution operator and A* is its adjoint. This is verified via dot product tests. The adjoint is undefined when bias is present.
|
|
147
|
+
|
|
148
|
+
## Implementation
|
|
149
|
+
|
|
150
|
+
For dimensions beyond 3D, `conv_nd` recursively decomposes the convolution into lower-dimensional operations. A 4D convolution becomes a sum of 3D convolutions applied to strided slices of the input.
|
|
151
|
+
|
|
152
|
+
Complex convolution is handled by decomposing into real operations: `(a+bi)*(c+di) = (ac-bd) + (ad+bc)i`.
|
|
153
|
+
|
|
154
|
+
The implementation uses native PyTorch operations when possible (1D, 2D, 3D) and falls back to recursion only for higher dimensions, minimizing memory overhead with strided views.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
torchnd/__init__.py,sha256=H3h4vZsjgSbFKvS0w9O5kXEFVs3zqsX8eM7GdIqRmiw,308
|
|
2
|
+
torchnd/functional.py,sha256=9tKawtVRUa3KFyve6pcjv603yeaVB831eXg98xio4uQ,15043
|
|
3
|
+
torchnd/modules.py,sha256=wyqD55PJLEH4LUvlB4Jp3EBH14nvMsqIxvn19TB28KM,23324
|
|
4
|
+
torchnd-0.1.0.dist-info/METADATA,sha256=DoVe_hcmKauvJnRBtLXQ-PoLHHCUsQoXezpzG-1k1Ko,5694
|
|
5
|
+
torchnd-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
torchnd-0.1.0.dist-info/top_level.txt,sha256=ZsJixSsEHaRPdRuRlYQcnFKvii3QUbRrtEvyK_F7uIg,8
|
|
7
|
+
torchnd-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
torchnd
|