|
| 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