Skip to content

Commit e161502

Browse files
authored
Added custom and learnable filters support (#11)
Signed-off-by: Nicola VIGANÒ <nicola.vigano@cea.fr>
1 parent dd45487 commit e161502

5 files changed

Lines changed: 909 additions & 0 deletions

File tree

src/autoden/losses.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from torch.nn.modules.loss import _Loss
99

1010
from autoden.transforms._wavelets import dwtn, swtn, wavelet_norm
11+
from autoden.transforms.custom_filters import CustomFilterDecomposition
1112

1213

1314
def _differentiate(inp: pt.Tensor, dim: int, position: str) -> pt.Tensor:
@@ -239,3 +240,52 @@ def forward(self, img: pt.Tensor) -> pt.Tensor:
239240
return loss_vals.sum()
240241
else:
241242
return loss_vals
243+
244+
245+
class LossCCF(LossRegularizer):
246+
"""Custom convolutional filter decomposition loss function."""
247+
248+
def __init__(
249+
self,
250+
lambda_val: float,
251+
filters: pt.Tensor,
252+
weights: pt.Tensor,
253+
size_average=None,
254+
reduce=None,
255+
reduction: str = "mean",
256+
min_approx: bool = False,
257+
) -> None:
258+
super().__init__(size_average, reduce, reduction)
259+
self.lambda_val = lambda_val
260+
self.filters = filters
261+
self.weights = weights
262+
self.min_approx = min_approx
263+
264+
self.n_dims = filters.ndim - 2
265+
266+
if filters.shape[0] != weights.numel():
267+
raise ValueError(
268+
f"The number of convolution kernels ({filters.shape[0]}) does"
269+
f" not match the number of weights ({weights.numel()})"
270+
)
271+
272+
self.weights = weights.reshape([1, -1, *(1,) * self.n_dims])
273+
274+
def forward(self, img: pt.Tensor) -> pt.Tensor:
275+
"""Compute decomposition on current batch."""
276+
_check_input_tensor(img, self.n_dims)
277+
axes = list(range(-(self.n_dims + 1), 0))
278+
279+
decomp = CustomFilterDecomposition(kernels=self.filters[not self.min_approx :, ...], device=img.device)
280+
weights = self.weights[:, not self.min_approx :, ...].to(img.device)
281+
282+
coeffs = decomp.analyze(img)
283+
284+
loss_vals: pt.Tensor = self.lambda_val * (weights * coeffs).abs().sum(1).sum(axes)
285+
286+
if self.reduction.lower() == "mean":
287+
return loss_vals.mean()
288+
elif self.reduction.lower() == "sum":
289+
return loss_vals.sum()
290+
else:
291+
return loss_vals

src/autoden/transforms/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
"""Transforms sub-package."""
3+
4+
__author__ = """Nicola Vigano"""
5+
__email__ = "nicola.vigano@cea.fr"
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""
2+
Custom and learnable filters decompositions.
3+
"""
4+
5+
import math
6+
from abc import ABC, abstractmethod
7+
from typing import Callable, Literal
8+
9+
import torch as pt
10+
import torch.nn as nn
11+
import torch.nn.functional as F
12+
from numpy.typing import NDArray
13+
14+
15+
class ConvolutionalDecompositionBase(ABC, nn.Module):
16+
"""Base class for all decompositions."""
17+
18+
m: int
19+
k: int
20+
in_ch: int
21+
n_dims: int
22+
norm: Literal["backward", "forward", "ortho"] | None
23+
24+
_ndconvs_d: dict[int, Callable[..., pt.Tensor]] = {1: F.conv1d, 2: F.conv2d, 3: F.conv3d}
25+
_ndconvs_t: dict[int, Callable[..., pt.Tensor]] = {1: F.conv_transpose1d, 2: F.conv_transpose2d, 3: F.conv_transpose3d}
26+
27+
def __init__(
28+
self, k: int, n_dims: int, in_ch: int, m: int, norm: Literal["backward", "forward", "ortho"] | None = "backward"
29+
) -> None:
30+
"""Initialize the ConvolutionalDecomposition.
31+
32+
Parameters
33+
----------
34+
k : int
35+
Kernel size.
36+
n_dims : int
37+
Number of dimensions for the convolution.
38+
in_ch : int
39+
Number of input channels.
40+
m : int
41+
Number of output channels.
42+
norm : Literal["backward", "forward", "ortho"] | None, optional
43+
Normalization type. Defaults to "backward".
44+
"""
45+
super().__init__()
46+
self.k = k
47+
self.n_dims = n_dims
48+
self.in_ch = in_ch
49+
self.m = m
50+
self.norm = norm
51+
52+
@abstractmethod
53+
def get_kernels(self) -> pt.Tensor:
54+
"""Return the kernels to be used for the convolutions.
55+
56+
Returns
57+
-------
58+
pt.Tensor
59+
The kernels for the convolutions.
60+
"""
61+
62+
def analyze(self, x: pt.Tensor) -> pt.Tensor:
63+
"""Apply the analysis (forward) transform using the kernels.
64+
65+
Parameters
66+
----------
67+
x : pt.Tensor
68+
Input tensor of shape (B, in_ch, [D, H], W).
69+
70+
Returns
71+
-------
72+
pt.Tensor
73+
Output tensor of shape (B, m, [D, H], W).
74+
"""
75+
w = self.get_kernels()
76+
c = self._ndconvs_d[self.n_dims](x, w, padding=self.k // 2)
77+
if self.norm is not None:
78+
if self.norm.lower() == "ortho":
79+
c = c / math.sqrt(self.m) * math.sqrt(self.in_ch)
80+
elif self.norm.lower() == "forward":
81+
c = c / float(self.m) * float(self.in_ch)
82+
return c
83+
84+
def synthesize(self, c: pt.Tensor) -> pt.Tensor:
85+
"""Apply the synthesis (inverse) transform using the kernels.
86+
87+
Parameters
88+
----------
89+
c : pt.Tensor
90+
Input tensor of shape (B, m, [D, H], W).
91+
92+
Returns
93+
-------
94+
pt.Tensor
95+
Output tensor of shape (B, in_ch, [D, H], W).
96+
"""
97+
w = self.get_kernels()
98+
x = self._ndconvs_t[self.n_dims](c, w, padding=self.k // 2)
99+
if self.norm is not None:
100+
if self.norm.lower() == "ortho":
101+
x = x / math.sqrt(self.m) * math.sqrt(self.in_ch)
102+
elif self.norm.lower() == "backward":
103+
x = x / float(self.m) * float(self.in_ch)
104+
return x
105+
106+
107+
class CustomFilterDecomposition(ConvolutionalDecompositionBase):
108+
"""Decomposition using custom filters (kernels)."""
109+
110+
kernels: pt.Tensor
111+
112+
def __init__(
113+
self,
114+
kernels: pt.Tensor | NDArray,
115+
device: str = "cuda" if pt.cuda.is_available() else "cpu",
116+
norm: Literal["backward", "forward", "ortho"] | None = "backward",
117+
) -> None:
118+
"""Initialize the CustomFilterDecomposition.
119+
120+
Parameters
121+
----------
122+
kernels : pt.Tensor | NDArray
123+
The kernels to be used for the convolutions. Should have shape (m, in_ch, *((k,) * n_dims)).
124+
device : str, optional
125+
The device to use for the kernels. Defaults to "cuda" if available, otherwise "cpu".
126+
norm : Literal["backward", "forward", "ortho"] | None, optional
127+
Normalization type. Defaults to "backward".
128+
"""
129+
m = kernels.shape[0]
130+
in_ch = kernels.shape[1]
131+
n_dims = kernels.ndim - 2
132+
if n_dims < 1:
133+
raise ValueError(f"Kernels should have shape (m, in_ch, *((k,) * n_dims)), but {kernels.shape} was passed")
134+
k = kernels.shape[-1]
135+
if any(s != k for s in kernels.shape[-n_dims:-1]):
136+
raise ValueError(
137+
f"Kernels should have the same size `k` in all directions, but {kernels.shape[-n_dims]} was passed."
138+
f" Complete shape: {kernels.shape}"
139+
)
140+
super().__init__(k=k, in_ch=in_ch, n_dims=n_dims, m=m, norm=norm)
141+
142+
if not isinstance(kernels, pt.Tensor):
143+
kernels = pt.tensor(kernels)
144+
kernels = kernels.detach().to(device).clone()
145+
self.register_buffer("kernels", kernels)
146+
147+
self.device = device
148+
149+
def get_kernels(self) -> pt.Tensor:
150+
"""Return the kernels to be used for the convolutions.
151+
152+
Returns
153+
-------
154+
pt.Tensor
155+
The kernels for the convolutions.
156+
"""
157+
return self.kernels

0 commit comments

Comments
 (0)