diff --git a/deel/torchlip/functional.py b/deel/torchlip/functional.py index 9b120ea..bb8e5b2 100644 --- a/deel/torchlip/functional.py +++ b/deel/torchlip/functional.py @@ -212,7 +212,9 @@ def max_min(input: torch.Tensor, dim: Optional[int] = None) -> torch.Tensor: return torch.cat((F.relu(input), F.relu(-input)), dim=dim) -def group_sort(input: torch.Tensor, group_size: Optional[int] = None) -> torch.Tensor: +def group_sort( + input: torch.Tensor, group_size: Optional[int] = None, dim: int = 1 +) -> torch.Tensor: r""" Applies GroupSort activation on the given tensor. @@ -220,22 +222,28 @@ def group_sort(input: torch.Tensor, group_size: Optional[int] = None) -> torch.T :py:func:`group_sort_2` :py:func:`full_sort` """ - if group_size is None or group_size > input.shape[1]: - group_size = input.shape[1] - if input.shape[1] % group_size != 0: + if group_size is None or group_size > input.shape[dim]: + group_size = input.shape[dim] + + if input.shape[dim] % group_size != 0: raise ValueError("The input size must be a multiple of the group size.") - fv = input.reshape([-1, group_size]) + new_shape = ( + input.shape[:dim] + + (input.shape[dim] // group_size, group_size) + + input.shape[dim + 1 :] + ) if group_size == 2: - sfv = torch.chunk(fv, 2, 1) - b = sfv[0] - c = sfv[1] - newv = torch.cat((torch.min(b, c), torch.max(b, c)), dim=1) - newv = newv.reshape(input.shape) - return newv + resh_input = input.view(new_shape) + a, b = ( + torch.min(resh_input, dim + 1, keepdim=True)[0], + torch.max(resh_input, dim + 1, keepdim=True)[0], + ) + return torch.cat([a, b], dim=dim + 1).view(input.shape) + fv = input.reshape(new_shape) - return torch.sort(fv)[0].reshape(input.shape) + return torch.sort(fv, dim=dim + 1)[0].reshape(input.shape) def group_sort_2(input: torch.Tensor) -> torch.Tensor: @@ -568,3 +576,42 @@ def process_labels_for_multi_gpu(labels: torch.Tensor) -> torch.Tensor: # Since element-wise KR terms are averaged by loss reduction later on, it is needed # to multiply by batch_size here. return torch.where(labels > 0, pos_factor, neg_factor) + + +class SymmetricPad(torch.nn.Module): + """ + Pads a 2D tensor symmetrically. + + Args: + pad (tuple): A tuple (pad_left, pad_right, pad_top, pad_bottom) specifying + the number of pixels to pad on each side. (or single int if + common padding). + + onedim: False for conv2d, True for conv1d. + + """ + + def __init__(self, pad, onedim=False): + super().__init__() + self.onedim = onedim + num_dim = 2 if onedim else 4 + if isinstance(pad, int): + self.pad = (pad,) * num_dim + else: + self.pad = torch.nn.modules.utils._reverse_repeat_tuple(pad, 2) + assert len(self.pad) == num_dim, f"Pad must be a tuple of {num_dim} integers" + + def forward(self, x): + + # Horizontal padding + left = x[:, ..., : self.pad[0]].flip(dims=[-1]) + right = x[:, ..., -self.pad[1] :].flip(dims=[-1]) + x = torch.cat([left, x, right], dim=-1) + if self.onedim: + return x + # Vertical padding + top = x[:, :, : self.pad[2], :].flip(dims=[-2]) + bottom = x[:, :, -self.pad[3] :, :].flip(dims=[-2]) + x = torch.cat([top, x, bottom], dim=-2) + + return x diff --git a/deel/torchlip/modules/__init__.py b/deel/torchlip/modules/__init__.py index af8406c..6326bc4 100644 --- a/deel/torchlip/modules/__init__.py +++ b/deel/torchlip/modules/__init__.py @@ -48,10 +48,13 @@ from .activation import FullSort from .activation import GroupSort from .activation import GroupSort2 +from .activation import HouseHolder from .activation import LPReLU from .activation import MaxMin from .conv import FrobeniusConv2d from .conv import SpectralConv2d +from .conv import SpectralConv1d +from .conv import SpectralConvTranspose2d from .downsampling import InvertibleDownSampling from .linear import FrobeniusLinear from .linear import SpectralLinear @@ -72,4 +75,10 @@ from .pooling import ScaledAdaptiveAvgPool2d from .pooling import ScaledAvgPool2d from .pooling import ScaledL2NormPool2d +from .pooling import ScaledAdaptativeL2NormPool2d from .upsampling import InvertibleUpSampling +from .normalization import LayerCentering +from .normalization import BatchCentering +from .unconstrained import PadConv2d +from .unconstrained import PadConv1d +from .residual import LipResidual diff --git a/deel/torchlip/modules/activation.py b/deel/torchlip/modules/activation.py index 370c395..4f976ae 100644 --- a/deel/torchlip/modules/activation.py +++ b/deel/torchlip/modules/activation.py @@ -33,6 +33,7 @@ import torch import torch.nn as nn +import numpy as np from .. import functional as F from .module import LipschitzModule @@ -211,3 +212,55 @@ def vanilla_export(self): layer = LPReLU(num_parameters=self.num_parameters) layer.weight.data = self.weight.data return layer + + +class HouseHolder(nn.Module, LipschitzModule): + def __init__(self, channels, k_coef_lip: float = 1.0, theta_initializer=None): + """ + Householder activation: + [this review](https://openreview.net/pdf?id=tD7eCtaSkR) + Adapted from [this repository](https://github.com/singlasahil14/SOC) + """ + nn.Module.__init__(self) + LipschitzModule.__init__(self, k_coef_lip) + assert (channels % 2) == 0 + eff_channels = channels // 2 + + if isinstance(theta_initializer, float): + coef_theta = theta_initializer + else: + coef_theta = 0.5 * np.pi + self.theta = nn.Parameter( + coef_theta * torch.ones(eff_channels), requires_grad=True + ) + if theta_initializer is not None: + if isinstance(theta_initializer, str): + name2init = { + "zeros": torch.nn.init.zeros_, + "ones": torch.nn.init.ones_, + "normal": torch.nn.init.normal_, + } + assert ( + theta_initializer in name2init + ), f"Unknown initializer {theta_initializer}" + name2init[theta_initializer](self.theta) + elif isinstance(theta_initializer, float): + pass + else: + raise ValueError(f"Unknown initializer {theta_initializer}") + + def forward(self, z, axis=1): + theta_shape = (1, -1) + (1,) * (len(z.shape) - 2) + theta = self.theta.view(theta_shape) + x, y = z.split(z.shape[axis] // 2, axis) + selector = (x * torch.sin(0.5 * theta)) - (y * torch.cos(0.5 * theta)) + + a_2 = x * torch.cos(theta) + y * torch.sin(theta) + b_2 = x * torch.sin(theta) - y * torch.cos(theta) + + a = x * (selector <= 0) + a_2 * (selector > 0) + b = y * (selector <= 0) + b_2 * (selector > 0) + return torch.cat([a, b], dim=axis) + + def vanilla_export(self): + return self diff --git a/deel/torchlip/modules/conv.py b/deel/torchlip/modules/conv.py index 2e3e06e..31b7766 100644 --- a/deel/torchlip/modules/conv.py +++ b/deel/torchlip/modules/conv.py @@ -34,10 +34,11 @@ from ..normalizers import DEFAULT_EPS_SPECTRAL from ..utils import frobenius_norm from ..utils import lconv_norm +from .unconstrained import PadConv1d, PadConv2d from .module import LipschitzModule -class SpectralConv2d(torch.nn.Conv2d, LipschitzModule): +class SpectralConv1d(PadConv1d, LipschitzModule): def __init__( self, in_channels: int, @@ -54,7 +55,7 @@ def __init__( eps_bjorck: int = DEFAULT_EPS_BJORCK, ): """ - This class is a Conv2d Layer constrained such that all singular of it's kernel + This class is a Conv1d Layer constrained such that all singular of it's kernel are 1. The computation based on BjorckNormalizer algorithm. As this is not enough to ensure 1-Lipschitz a coercive coefficient is applied on the output. @@ -82,14 +83,14 @@ def __init__( eps_spectral: stopping criterion for the iterative power algorithm. eps_bjorck: stopping criterion Bjorck algorithm. - This documentation reuse the body of the original torch.nn.Conv2D doc. + This documentation reuse the body of the original torch.nn.Conv1D doc. """ # if not ((dilation == (1, 1)) or (dilation == [1, 1]) or (dilation == 1)): # raise RuntimeError("NormalizedConv does not support dilation rate") # if padding_mode != "same": # raise RuntimeError("NormalizedConv only support padding='same'") - torch.nn.Conv2d.__init__( + PadConv1d.__init__( self, in_channels=in_channels, out_channels=out_channels, @@ -97,6 +98,8 @@ def __init__( stride=stride, padding=padding, bias=bias, + dilation=dilation, + groups=groups, padding_mode=padding_mode, ) LipschitzModule.__init__(self, k_coef_lip) @@ -111,28 +114,98 @@ def __init__( eps=eps_spectral, ) bjorck_norm(self, name="weight", eps=eps_bjorck) - lconv_norm(self, name="weight") + lconv_norm(self) self.apply_lipschitz_factor() def vanilla_export(self): - layer = torch.nn.Conv2d( - in_channels=self.in_channels, - out_channels=self.out_channels, - kernel_size=self.kernel_size, - stride=self.stride, - padding=self.padding, - dilation=self.dilation, - groups=self.groups, - bias=self.bias is not None, - padding_mode=self.padding_mode, + return PadConv1d.vanilla_export(self) + + +class SpectralConv2d(PadConv2d, LipschitzModule): + def __init__( + self, + in_channels: int, + out_channels: int, + kernel_size: _size_2_t, + stride: _size_2_t = 1, + padding: _size_2_t = 0, + dilation: _size_2_t = 1, + groups: int = 1, + bias: bool = True, + padding_mode: str = "zeros", + k_coef_lip: float = 1.0, + eps_spectral: int = DEFAULT_EPS_SPECTRAL, + eps_bjorck: int = DEFAULT_EPS_BJORCK, + ): + """ + This class is a Conv2d Layer constrained such that all singular of it's kernel + are 1. The computation based on BjorckNormalizer algorithm. As this is not + enough to ensure 1-Lipschitz a coercive coefficient is applied on the + output. + The computation is done in three steps: + + 1. reduce the largest singular value to 1, using iterated power method. + 2. increase other singular values to 1, using BjorckNormalizer algorithm. + 3. divide the output by the Lipschitz bound to ensure k-Lipschitz. + + Args: + in_channels (int): Number of channels in the input image + out_channels (int): Number of channels produced by the convolution + kernel_size (int or tuple): Size of the convolving kernel + stride (int or tuple, optional): Stride of the convolution. + padding (int or tuple, optional): Zero-padding added to both sides of + the input. + padding_mode (string, optional): ``'zeros'``, ``'reflect'``, + ``'replicate'`` or ``'circular'``. Default: ``'zeros'`` + dilation (int or tuple, optional): Spacing between kernel elements. + Has to be one + groups (int, optional): Number of blocked connections from input + channels to output channels. Has to be one + bias (bool, optional): If ``True``, adds a learnable bias to the + output. + k_coef_lip: Lipschitz constant to ensure. + eps_spectral: stopping criterion for the iterative power algorithm. + eps_bjorck: stopping criterion Bjorck algorithm. + + This documentation reuse the body of the original torch.nn.Conv2D doc. + """ + # if not ((dilation == (1, 1)) or (dilation == [1, 1]) or (dilation == 1)): + # raise RuntimeError("NormalizedConv does not support dilation rate") + # if padding_mode != "same": + # raise RuntimeError("NormalizedConv only support padding='same'") + + PadConv2d.__init__( + self, + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + bias=bias, + dilation=dilation, + groups=groups, + padding_mode=padding_mode, ) - layer.weight.data = self.weight.detach() + LipschitzModule.__init__(self, k_coef_lip) + + torch.nn.init.orthogonal_(self.weight) if self.bias is not None: - layer.bias.data = self.bias.detach() - return layer + self.bias.data.fill_(0.0) + + spectral_norm( + self, + name="weight", + eps=eps_spectral, + ) + bjorck_norm(self, name="weight", eps=eps_bjorck) + lconv_norm(self, name="weight") + self.apply_lipschitz_factor() + + def vanilla_export(self): + return PadConv2d.vanilla_export(self) -class FrobeniusConv2d(torch.nn.Conv2d, LipschitzModule): +class FrobeniusConv2d(PadConv2d, LipschitzModule): """ Same as SpectralConv2d but in the case of a single output. """ @@ -155,14 +228,17 @@ def __init__( # if padding_mode != "same": # raise RuntimeError("NormalizedConv only support padding='same'") - torch.nn.Conv2d.__init__( + PadConv2d.__init__( self, in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=stride, padding=padding, + padding_mode=padding_mode, bias=bias, + dilation=dilation, + groups=groups, ) LipschitzModule.__init__(self, k_coef_lip) @@ -175,12 +251,98 @@ def __init__( self.apply_lipschitz_factor() def vanilla_export(self): - layer = torch.nn.Conv2d( + return PadConv2d.vanilla_export(self) + + +class SpectralConvTranspose2d(torch.nn.ConvTranspose2d, LipschitzModule): + r"""Applies a 2D transposed convolution operator over an input image + such that all singular of it's kernel are 1. + The computation are the same as for SpectralConv2d layer + + Args: + in_channels (int): Number of channels in the input image + out_channels (int): Number of channels produced by the convolution + kernel_size (int or tuple): Size of the convolving kernel + stride (int or tuple, optional): Stride of the convolution. + padding (int or tuple, optional): Zero-padding added to both sides of + the input. + output_padding: only 0 or none are supported + padding_mode (string, optional): ``'zeros'``, ``'reflect'``, + ``'replicate'`` or ``'circular'``. Default: ``'zeros'`` + dilation (int or tuple, optional): Spacing between kernel elements. + Has to be one. + groups (int, optional): Number of blocked connections from input + channels to output channels. Has to be one. + bias (bool, optional): If ``True``, adds a learnable bias to the + output. + k_coef_lip: Lipschitz constant to ensure. + eps_spectral: stopping criterion for the iterative power algorithm. + eps_bjorck: stopping criterion Bjorck algorithm. + + This documentation reuse the body of the original torch.nn.ConvTranspose2d + doc. + """ + + def __init__( + self, + in_channels: int, + out_channels: int, + kernel_size: _size_2_t, + stride: _size_2_t = 1, + padding: _size_2_t = 0, + output_padding: _size_2_t = 0, + groups: int = 1, + bias: bool = True, + dilation: _size_2_t = 1, + padding_mode: str = "zeros", + device=None, + dtype=None, + k_coef_lip: float = 1.0, + eps_spectral: int = DEFAULT_EPS_SPECTRAL, + eps_bjorck: int = DEFAULT_EPS_BJORCK, + ) -> None: + if dilation != 1: + raise ValueError("SpectralConvTranspose2d does not support dilation rate") + if output_padding not in [0, None]: + raise ValueError("SpectralConvTranspose2d only supports output_padding=0") + torch.nn.ConvTranspose2d.__init__( + self, + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + output_padding=output_padding, + groups=groups, + bias=bias, + dilation=dilation, + padding_mode=padding_mode, + device=device, + dtype=dtype, + ) + LipschitzModule.__init__(self, k_coef_lip) + + torch.nn.init.orthogonal_(self.weight) + if self.bias is not None: + self.bias.data.fill_(0.0) + + spectral_norm( + self, + name="weight", + eps=eps_spectral, + ) + bjorck_norm(self, name="weight", eps=eps_bjorck) + lconv_norm(self, name="weight") + self.apply_lipschitz_factor() + + def vanilla_export(self): + layer = torch.nn.ConvTranspose2d( in_channels=self.in_channels, out_channels=self.out_channels, kernel_size=self.kernel_size, stride=self.stride, padding=self.padding, + output_padding=self.output_padding, dilation=self.dilation, groups=self.groups, bias=self.bias is not None, diff --git a/deel/torchlip/modules/downsampling.py b/deel/torchlip/modules/downsampling.py index 7c00fc8..d6c4127 100644 --- a/deel/torchlip/modules/downsampling.py +++ b/deel/torchlip/modules/downsampling.py @@ -24,22 +24,18 @@ # rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, # CRIAQ and ANITI - https://www.deel.ai/ # ===================================================================================== -from typing import Tuple - import torch -from .. import functional as F from .module import LipschitzModule -class InvertibleDownSampling(torch.nn.Module, LipschitzModule): - def __init__(self, kernel_size: Tuple[int, int], k_coef_lip: float = 1.0): - torch.nn.Module.__init__(self) +class InvertibleDownSampling(torch.nn.PixelUnshuffle, LipschitzModule): + def __init__(self, kernel_size: int, k_coef_lip: float = 1.0): + torch.nn.PixelUnshuffle.__init__(self, downscale_factor=kernel_size) LipschitzModule.__init__(self, k_coef_lip) - self.kernel_size = kernel_size - - def forward(self, input: torch.Tensor) -> torch.Tensor: - return F.invertible_downsample(input, self.kernel_size) * self._coefficient_lip def vanilla_export(self): - return self + if self._coefficient_lip == 1.0: + return torch.nn.PixelUnshuffle(self.downscale_factor) + else: + return self diff --git a/deel/torchlip/modules/module.py b/deel/torchlip/modules/module.py index a951a61..c295426 100644 --- a/deel/torchlip/modules/module.py +++ b/deel/torchlip/modules/module.py @@ -72,13 +72,12 @@ def vanilla_model(model: nn.Module): model (nn.Module): Lipschitz neural network """ for n, module in model.named_children(): - if len(list(module.children())) > 0: - # compound module, go inside it - vanilla_model(module) - if isinstance(module, LipschitzModule): # simple module setattr(model, n, module.vanilla_export()) + elif len(list(module.children())) > 0: + # compound module, go inside it + vanilla_model(module) class _LipschitzCoefMultiplication(nn.Module): diff --git a/deel/torchlip/modules/normalization.py b/deel/torchlip/modules/normalization.py new file mode 100644 index 0000000..d1df822 --- /dev/null +++ b/deel/torchlip/modules/normalization.py @@ -0,0 +1,130 @@ +from typing import Optional +import torch +import torch.nn as nn +import torch.distributed as dist + + +class LayerCentering(nn.Module): + r""" + Applies Layer centering over a mini-batch of inputs. + + This layer implements the operation as described in + .. math:: + y = x - \mathrm{E}[x] + \beta + The mean is calculated over the last `D` dimensions + given in the `dim` parameter. + `\beta` is learnable bias parameter. that can be + applied after the mean subtraction. + Unlike Layer Normalization, this layer is 1-Lipschitz + This layer uses statistics computed from input data in + both training and evaluation modes. + + Args: + size: number of features in the input tensor + dim: dimensions over which to compute the mean + (default ``input.mean((-2, -1))`` for a 4D tensor). + bias: if `True`, adds a learnable bias to the output + of shape (size,). Default: `True` + + Shape: + - Input: :math:`(N, size, *)` + - Output: :math:`(N, size, *)` (same shape as input) + + """ + + def __init__(self, size: int = 1, dim: tuple = [-2, -1], bias=True): + super(LayerCentering, self).__init__() + if bias: + self.bias = nn.Parameter(torch.zeros((size,)), requires_grad=True) + else: + self.register_parameter("bias", None) + self.dim = dim + + def forward(self, x): + mean = x.mean(dim=self.dim, keepdim=True) + if self.bias is not None: + bias_shape = (1, -1) + (1,) * (len(x.shape) - 2) + return x - mean + self.bias.view(bias_shape) + else: + return x - mean + + +LayerCentering2d = LayerCentering + + +class BatchCentering(nn.Module): + r""" + Applies Batch Centering over a 2D, 3D, 4D input. + + .. math:: + + y = x - \mathrm{E}[x] + \beta + + The mean is calculated per-dimension over the mini-batchesa and + other dimensions excepted the feature/channel dimension. + This layer uses statistics computed from input data in + training mode and a constant in evaluation mode computed as + the running mean on training samples. + :math:`\beta` is a learnable parameter vectors + of size `C` (where `C` is the number of features or channels of the input). + that can be applied after the mean subtraction. + Unlike Batch Normalization, this layer is 1-Lipschitz + + Args: + size: number of features in the input tensor + dim: dimensions over which to compute the mean + (default ``input.mean((0, -2, -1))`` for a 4D tensor). + momentum: the value used for the running mean computation + bias: if `True`, adds a learnable bias to the output + of shape (size,). Default: `True` + + Shape: + - Input: :math:`(N, size, *)` + - Output: :math:`(N, size, *)` (same shape as input) + + """ + + def __init__( + self, + size: int = 1, + dim: Optional[tuple] = None, + momentum: float = 0.05, + bias: bool = True, + ): + super(BatchCentering, self).__init__() + self.dim = dim + self.momentum = momentum + self.register_buffer("running_mean", torch.zeros((size,))) + if bias: + self.bias = nn.Parameter(torch.zeros((size,)), requires_grad=True) + else: + self.register_parameter("bias", None) + + self.first = True + + def forward(self, x): + if self.dim is None: # (0,2,3) for 4D tensor; (0,) for 2D tensor + self.dim = (0,) + tuple(range(2, len(x.shape))) + mean_shape = (1, -1) + (1,) * (len(x.shape) - 2) + if self.training: + mean = x.mean(dim=self.dim) + with torch.no_grad(): + if self.first: + self.running_mean = mean + self.first = False + else: + self.running_mean = ( + 1 - self.momentum + ) * self.running_mean + self.momentum * mean + if dist.is_initialized(): + dist.all_reduce(self.running_mean, op=dist.ReduceOp.SUM) + self.running_mean /= dist.get_world_size() + else: + mean = self.running_mean + if self.bias is not None: + return x - mean.view(mean_shape) + self.bias.view(mean_shape) + else: + return x - mean.view(mean_shape) + + +BatchCentering2d = BatchCentering diff --git a/deel/torchlip/modules/pooling.py b/deel/torchlip/modules/pooling.py index 3467e5a..d7421cd 100644 --- a/deel/torchlip/modules/pooling.py +++ b/deel/torchlip/modules/pooling.py @@ -29,9 +29,9 @@ import numpy as np import torch +import torch.nn.functional as F from torch.nn.common_types import _size_2_t -from ..utils import sqrt_with_gradeps from .module import LipschitzModule @@ -125,6 +125,11 @@ def __init__( This documentation reuse the body of the original nn.AdaptiveAvgPool2d doc. """ + if not isinstance(output_size, tuple) or len(output_size) != 2: + raise RuntimeError("output_size must be a tuple of 2 integers") + else: + if output_size[0] != 1 or output_size[1] != 1: + raise RuntimeError("output_size must be (1, 1) for Lipschitz constant") torch.nn.AdaptiveAvgPool2d.__init__(self, output_size) LipschitzModule.__init__(self, k_coef_lip) @@ -136,17 +141,13 @@ def vanilla_export(self): return self -class ScaledL2NormPool2d(torch.nn.AvgPool2d, LipschitzModule): +class ScaledL2NormPool2d(torch.nn.LPPool2d, LipschitzModule): def __init__( self, kernel_size: _size_2_t, stride: Optional[_size_2_t] = None, - padding: _size_2_t = 0, ceil_mode: bool = False, - count_include_pad: bool = True, - divisor_override: bool = None, k_coef_lip: float = 1.0, - eps_grad_sqrt: float = 1e-6, ): """ Average pooling operation for spatial data, with a lipschitz bound. This @@ -159,48 +160,74 @@ def __init__( kernel_size: The size of the window. stride: The stride of the window. Must be None or equal to ``kernel_size``. Default value is ``kernel_size``. - padding: Implicit zero-padding to be added on both sides. Must - be zero. ceil_mode: When True, will use ceil instead of floor to compute the output shape. - count_include_pad: When True, will include the zero-padding in the averaging - calculation. - divisor_override: If specified, it will be used as divisor, otherwise - ``kernel_size`` will be used. k_coef_lip: The lipschitz factor to ensure. The output will be scaled by this factor. - eps_grad_sqrt: Epsilon value to avoid numerical instability - due to non-defined gradient at 0 in the sqrt function """ - torch.nn.AvgPool2d.__init__( + torch.nn.LPPool2d.__init__( self, + 2, # Norm 2 kernel_size=kernel_size, stride=stride, - padding=padding, ceil_mode=ceil_mode, - count_include_pad=count_include_pad, - divisor_override=divisor_override, ) LipschitzModule.__init__(self, k_coef_lip) - self.eps_grad_sqrt = eps_grad_sqrt - self.scalingFactor = computePoolScalingFactor(self.kernel_size) - if self.stride != self.kernel_size: + if (self.stride is not None) and (self.stride != self.kernel_size): raise RuntimeError("stride must be equal to kernel_size.") - if np.sum(self.padding) != 0: - raise RuntimeError("ScaledL2NormPooling2D does not support padding.") - if eps_grad_sqrt < 0.0: - raise RuntimeError("eps_grad_sqrt must be positive") def forward(self, input: torch.Tensor) -> torch.Tensor: - coeff = self._coefficient_lip * self.scalingFactor - return ( # type: ignore - sqrt_with_gradeps( - torch.nn.AvgPool2d.forward(self, torch.square(input)), - self.eps_grad_sqrt, + coeff = self._coefficient_lip + return torch.nn.LPPool2d.forward(self, input) * coeff + + def vanilla_export(self): + if self._coefficient_lip == 1.0: + return torch.nn.LPPool2d( + 2, # Norm 2 + kernel_size=self.kernel_size, + stride=self.stride, + ceil_mode=self.ceil_mode, ) - * coeff - ) + else: + return self + + +class ScaledAdaptativeL2NormPool2d( + torch.nn.modules.pooling._AdaptiveAvgPoolNd, LipschitzModule +): + def __init__( + self, + output_size: _size_2_t = (1, 1), + k_coef_lip: float = 1.0, + ): + """ + Average pooling operation for spatial data, with a lipschitz bound. This + pooling operation is norm preserving (aka gradient=1 almost everywhere). + + [1]Y.-L.Boureau, J.Ponce, et Y.LeCun, « A Theoretical Analysis of Feature + Pooling in Visual Recognition »,p.8. + + Arguments: + output_size: the target output size has to be (1,1) + k_coef_lip: the lipschitz factor to ensure + + Input shape: + 4D tensor with shape `(batch_size, channels, rows, cols)`. + + Output shape: + 4D tensor with shape `(batch_size, channels, 1, 1)`. + """ + if not isinstance(output_size, tuple) or len(output_size) != 2: + raise RuntimeError("output_size must be a tuple of 2 integers") + else: + if output_size[0] != 1 or output_size[1] != 1: + raise RuntimeError("output_size must be (1, 1)") + torch.nn.modules.pooling._AdaptiveAvgPoolNd.__init__(self, output_size) + LipschitzModule.__init__(self, k_coef_lip) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + return F.lp_pool2d(input, 2, input.shape[-2:]) * self._coefficient_lip def vanilla_export(self): return self diff --git a/deel/torchlip/modules/residual.py b/deel/torchlip/modules/residual.py new file mode 100644 index 0000000..30e1c11 --- /dev/null +++ b/deel/torchlip/modules/residual.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# ===================================================================================== + +import torch +from torch import nn + + +class LipResidual(nn.Module): + """ + This class is a 1-Lipschitz residual connection + With a learnable parameter alpha that give a tradeoff + between the x and the layer y=l(x) + + Args: + """ + + def __init__(self): + super().__init__() + self.alpha = nn.Parameter(torch.tensor(0.0), requires_grad=True) + + def forward(self, x, y): + alpha = torch.sigmoid(self.alpha) + return alpha * x + (1 - alpha) * y diff --git a/deel/torchlip/modules/unconstrained.py b/deel/torchlip/modules/unconstrained.py new file mode 100644 index 0000000..d420d09 --- /dev/null +++ b/deel/torchlip/modules/unconstrained.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# ===================================================================================== + +from typing import Union +import torch +from torch.nn.common_types import _size_1_t, _size_2_t +from ..functional import SymmetricPad + + +class PadConv1d(torch.nn.Conv1d): + def __init__( + self, + in_channels: int, + out_channels: int, + kernel_size: _size_1_t, + stride: _size_1_t = 1, + padding: Union[str, _size_1_t] = 0, + dilation: _size_1_t = 1, + groups: int = 1, + bias: bool = True, + padding_mode: str = "zeros", + ): + """ + This class is a Conv1d Layer with additional padding modes + + Args: + in_channels (int): Number of channels in the input image + out_channels (int): Number of channels produced by the convolution + kernel_size (int or tuple): Size of the convolving kernel + stride (int or tuple, optional): Stride of the convolution. + padding (int or tuple, optional): Zero-padding added to both sides of + the input. + padding_mode (string, optional): ``'zeros'``, ``'reflect'``, + ``'replicate'``,``'symmetric'`` or ``'circular'``. + Default: ``'zeros'`` + dilation (int or tuple, optional): Spacing between kernel elements. + Has to be one + groups (int, optional): Number of blocked connections from input + channels to output channels. Has to be one + bias (bool, optional): If ``True``, adds a learnable bias to the + output. + + This documentation reuse the body of the original torch.nn.Conv1d doc. + """ + + self.old_padding = padding + self.old_padding_mode = padding_mode + if padding_mode.lower() == "symmetric": + padding_mode = "zeros" + padding = "valid" + + super(PadConv1d, self).__init__( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + bias=bias, + dilation=dilation, + groups=groups, + padding_mode=padding_mode, + ) + + if self.old_padding_mode.lower() == "symmetric": + self.pad = SymmetricPad(self.old_padding, onedim=True) + else: + self.pad = lambda x: x + + def forward(self, input: torch.Tensor) -> torch.Tensor: + return super(PadConv1d, self).forward(self.pad(input)) + + def vanilla_export(self): + if self.old_padding_mode.lower() == "symmetric": + next_layer_type = PadConv1d + else: + next_layer_type = torch.nn.Conv1d + + layer = next_layer_type( + in_channels=self.in_channels, + out_channels=self.out_channels, + kernel_size=self.kernel_size, + stride=self.stride, + padding=self.old_padding, + dilation=self.dilation, + groups=self.groups, + bias=self.bias is not None, + padding_mode=self.old_padding_mode, + ) + layer.weight.data = self.weight.detach() + if self.bias is not None: + layer.bias.data = self.bias.detach() + return layer + + +class PadConv2d(torch.nn.Conv2d): + def __init__( + self, + in_channels: int, + out_channels: int, + kernel_size: _size_2_t, + stride: _size_2_t = 1, + padding: Union[str, _size_2_t] = 0, + dilation: _size_2_t = 1, + groups: int = 1, + bias: bool = True, + padding_mode: str = "zeros", + ): + """ + This class is a Conv2d Layer with additional padding modes + + Args: + in_channels (int): Number of channels in the input image + out_channels (int): Number of channels produced by the convolution + kernel_size (int or tuple): Size of the convolving kernel + stride (int or tuple, optional): Stride of the convolution. + padding (int or tuple, optional): Zero-padding added to both sides of + the input. + padding_mode (string, optional): ``'zeros'``, ``'reflect'``, + ``'replicate'``,``'symmetric'`` or ``'circular'``. + Default: ``'zeros'`` + dilation (int or tuple, optional): Spacing between kernel elements. + Has to be one + groups (int, optional): Number of blocked connections from input + channels to output channels. Has to be one + bias (bool, optional): If ``True``, adds a learnable bias to the + output. + + This documentation reuse the body of the original torch.nn.Conv2D doc. + """ + + self.old_padding = padding + self.old_padding_mode = padding_mode + if padding_mode.lower() == "symmetric": + # symmetric padding of one pixel can be replaced by replicate + if (isinstance(padding, int) and padding <= 1) or ( + isinstance(padding, tuple) and padding[0] <= 1 and padding[1] <= 1 + ): + self.old_padding_mode = padding_mode = "replicate" + else: + padding_mode = "zeros" + padding = "valid" + + super(PadConv2d, self).__init__( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + bias=bias, + dilation=dilation, + groups=groups, + padding_mode=padding_mode, + ) + + if self.old_padding_mode.lower() == "symmetric": + self.pad = SymmetricPad(self.old_padding) + else: + self.pad = lambda x: x + + def forward(self, input: torch.Tensor) -> torch.Tensor: + return super(PadConv2d, self).forward(self.pad(input)) + + def vanilla_export(self): + if self.old_padding_mode.lower() == "symmetric": + next_layer_type = PadConv2d + else: + next_layer_type = torch.nn.Conv2d + + layer = next_layer_type( + in_channels=self.in_channels, + out_channels=self.out_channels, + kernel_size=self.kernel_size, + stride=self.stride, + padding=self.old_padding, + dilation=self.dilation, + groups=self.groups, + bias=self.bias is not None, + padding_mode=self.old_padding_mode, + ) + layer.weight.data = self.weight.detach() + if self.bias is not None: + layer.bias.data = self.bias.detach() + return layer diff --git a/deel/torchlip/modules/upsampling.py b/deel/torchlip/modules/upsampling.py index 7f40354..262e3eb 100644 --- a/deel/torchlip/modules/upsampling.py +++ b/deel/torchlip/modules/upsampling.py @@ -24,25 +24,19 @@ # rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, # CRIAQ and ANITI - https://www.deel.ai/ # ===================================================================================== -from typing import Tuple -from typing import Union import torch -from .. import functional as F from .module import LipschitzModule -class InvertibleUpSampling(torch.nn.Module, LipschitzModule): - def __init__( - self, kernel_size: Union[int, Tuple[int, ...]], k_coef_lip: float = 1.0 - ): - torch.nn.Module.__init__(self) +class InvertibleUpSampling(torch.nn.PixelShuffle, LipschitzModule): + def __init__(self, kernel_size: int, k_coef_lip: float = 1.0): + torch.nn.PixelShuffle.__init__(self, upscale_factor=kernel_size) LipschitzModule.__init__(self, k_coef_lip) - self.kernel_size = kernel_size - - def forward(self, input: torch.Tensor) -> torch.Tensor: - return F.invertible_upsample(input, self.kernel_size) * self._coefficient_lip def vanilla_export(self): - return self + if self._coefficient_lip == 1.0: + return torch.nn.PixelShuffle(self.upscale_factor) + else: + return self diff --git a/deel/torchlip/utils/lconv_norm.py b/deel/torchlip/utils/lconv_norm.py index 7ba0ef3..0101fdf 100644 --- a/deel/torchlip/utils/lconv_norm.py +++ b/deel/torchlip/utils/lconv_norm.py @@ -24,7 +24,7 @@ # rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, # CRIAQ and ANITI - https://www.deel.ai/ # ===================================================================================== -from typing import Tuple +from typing import Tuple, Union import numpy as np import torch @@ -32,16 +32,38 @@ import torch.nn.utils.parametrize as parametrize +def compute_lconv_coef_1d( + kernel_size: Tuple[int], + input_shape: Tuple[int] = None, + strides: Tuple[int] = (1,), + padding_mode: str = "zeros", +) -> float: + stride = strides[0] + k1 = kernel_size[0] + + if (padding_mode in ["zeros"]) and (stride == 1) and (input_shape is not None): + # See https://arxiv.org/abs/2006.06520 + in_l = input_shape[-1] + k1_div2 = (k1 - 1) / 2 + coefLip = in_l / (k1 * in_l - k1_div2 * (k1_div2 + 1)) + else: + sn1 = strides[0] + coefLip = 1.0 / np.ceil(k1 / sn1) + + return coefLip # type: ignore + + def compute_lconv_coef( kernel_size: Tuple[int, ...], input_shape: Tuple[int, ...] = None, strides: Tuple[int, ...] = (1, 1), + padding_mode: str = "zeros", ) -> float: # See https://arxiv.org/abs/2006.06520 stride = np.prod(strides) k1, k2 = kernel_size - if stride == 1 and input_shape is not None: + if (padding_mode in ["zeros"]) and (stride == 1) and (input_shape is not None): h, w = input_shape[-2:] k1_div2 = (k1 - 1) / 2 k2_div2 = (k2 - 1) / 2 @@ -68,7 +90,10 @@ def forward(self, weight: torch.Tensor) -> torch.Tensor: return weight * self.lconv_coefficient -def lconv_norm(module: torch.nn.Conv2d, name: str = "weight") -> torch.nn.Conv2d: +ConvType = Union[torch.nn.Conv2d, torch.nn.Conv1d] + + +def lconv_norm(module: ConvType, name: str = "weight") -> ConvType: r""" Applies Lipschitz normalization to a kernel in the given convolutional. This is implemented via a hook that multiplies the kernel by a value computed @@ -91,7 +116,11 @@ def lconv_norm(module: torch.nn.Conv2d, name: str = "weight") -> torch.nn.Conv2d Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1)) """ - coefficient = compute_lconv_coef(module.kernel_size, None, module.stride) + onedim = isinstance(module, torch.nn.Conv1d) + if onedim: + coefficient = compute_lconv_coef_1d(module.kernel_size, None, module.stride) + else: + coefficient = compute_lconv_coef(module.kernel_size, None, module.stride) parametrize.register_parametrization(module, name, _LConvNorm(coefficient)) return module diff --git a/docs/notebooks/wasserstein_classification_MNIST08.ipynb b/docs/notebooks/wasserstein_classification_MNIST08.ipynb index d6bf23a..81f8b3d 100644 --- a/docs/notebooks/wasserstein_classification_MNIST08.ipynb +++ b/docs/notebooks/wasserstein_classification_MNIST08.ipynb @@ -36,13 +36,6 @@ "execution_count": 2, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - }, { "name": "stdout", "output_type": "stream", @@ -200,7 +193,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -208,25 +201,25 @@ "output_type": "stream", "text": [ "Epoch 1/10\n", - "loss: -0.0655 - KR: 3.3978 - acc: 0.9913 - val_loss: -0.0769 - val_KR: 4.2157 - val_acc: 0.9933\n", + "loss: -0.0340 - KR: 1.2288 - acc: 0.8649 - val_loss: -0.0363 - val_KR: 2.3215 - val_acc: 0.9928\n", "Epoch 2/10\n", - "loss: -0.1013 - KR: 4.7773 - acc: 0.9945 - val_loss: -0.0989 - val_KR: 5.3608 - val_acc: 0.9928\n", + "loss: -0.0630 - KR: 2.8186 - acc: 0.9943 - val_loss: -0.0607 - val_KR: 3.4102 - val_acc: 0.9939\n", "Epoch 3/10\n", - "loss: -0.0946 - KR: 5.6133 - acc: 0.9951 - val_loss: -0.1112 - val_KR: 5.9211 - val_acc: 0.9949\n", + "loss: -0.0901 - KR: 3.8766 - acc: 0.9960 - val_loss: -0.0805 - val_KR: 4.4241 - val_acc: 0.9939\n", "Epoch 4/10\n", - "loss: -0.1145 - KR: 6.0779 - acc: 0.9963 - val_loss: -0.1180 - val_KR: 6.2546 - val_acc: 0.9939\n", + "loss: -0.0964 - KR: 4.7411 - acc: 0.9965 - val_loss: -0.0957 - val_KR: 5.1178 - val_acc: 0.9933\n", "Epoch 5/10\n", - "loss: -0.1133 - KR: 6.2920 - acc: 0.9962 - val_loss: -0.1206 - val_KR: 6.3919 - val_acc: 0.9944\n", + "loss: -0.1084 - KR: 5.3850 - acc: 0.9957 - val_loss: -0.1036 - val_KR: 5.7095 - val_acc: 0.9923\n", "Epoch 6/10\n", - "loss: -0.1371 - KR: 6.5019 - acc: 0.9965 - val_loss: -0.1255 - val_KR: 6.6471 - val_acc: 0.9939\n", + "loss: -0.1095 - KR: 5.8155 - acc: 0.9954 - val_loss: -0.1126 - val_KR: 6.0285 - val_acc: 0.9944\n", "Epoch 7/10\n", - "loss: -0.1226 - KR: 6.6214 - acc: 0.9969 - val_loss: -0.1261 - val_KR: 6.7408 - val_acc: 0.9939\n", + "loss: -0.1090 - KR: 6.1108 - acc: 0.9960 - val_loss: -0.1178 - val_KR: 6.3084 - val_acc: 0.9933\n", "Epoch 8/10\n", - "loss: -0.1395 - KR: 6.7325 - acc: 0.9967 - val_loss: -0.1280 - val_KR: 6.7204 - val_acc: 0.9944\n", + "loss: -0.1266 - KR: 6.3128 - acc: 0.9959 - val_loss: -0.1192 - val_KR: 6.4553 - val_acc: 0.9923\n", "Epoch 9/10\n", - "loss: -0.1271 - KR: 6.7927 - acc: 0.9971 - val_loss: -0.1255 - val_KR: 6.8759 - val_acc: 0.9898\n", + "loss: -0.1263 - KR: 6.4460 - acc: 0.9966 - val_loss: -0.1208 - val_KR: 6.4837 - val_acc: 0.9939\n", "Epoch 10/10\n", - "loss: -0.1316 - KR: 6.8134 - acc: 0.9970 - val_loss: -0.1286 - val_KR: 6.8696 - val_acc: 0.9928\n" + "loss: -0.1316 - KR: 6.5416 - acc: 0.9967 - val_loss: -0.1240 - val_KR: 6.6313 - val_acc: 0.9933\n" ] } ], @@ -326,14 +319,14 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "tensor(0.1439)\n" + "tensor(0.1420)\n" ] } ], @@ -361,14 +354,14 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "tensor(0.8923, dtype=torch.float64)\n" + "tensor(0.8950, dtype=torch.float64)\n" ] } ], @@ -403,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -428,7 +421,7 @@ " (1): _BjorckNorm()\n", " )\n", " )\n", - "), min=0.9999998211860657, max=1.0000001192092896\n", + "), min=0.9999998211860657, max=1.000000238418579\n", "ParametrizedSpectralLinear(\n", " in_features=64, out_features=32, bias=True\n", " (parametrizations): ModuleDict(\n", @@ -437,7 +430,7 @@ " (1): _BjorckNorm()\n", " )\n", " )\n", - "), min=0.9999998211860657, max=1.0\n", + "), min=0.9999998807907104, max=1.0\n", "ParametrizedFrobeniusLinear(\n", " in_features=32, out_features=1, bias=True\n", " (parametrizations): ModuleDict(\n", @@ -445,7 +438,7 @@ " (0): _FrobeniusNorm()\n", " )\n", " )\n", - "), min=0.9999999403953552, max=0.9999999403953552\n" + "), min=0.9999998807907104, max=0.9999998807907104\n" ] } ], @@ -459,9 +452,47 @@ " print(f\"{layer}, min={s.min()}, max={s.max()}\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.2 Model export\n", + "\n", + "Once training is finished, the model can be optimized for inference by using the\n", + "`vanilla_export()` method. The `torchlip` layers are converted to their PyTorch\n", + "counterparts, e.g. `SpectralConv2d` layers will be converted into `torch.nn.Conv2d`\n", + "layers." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Warnings:\n", + "vanilla_export method modifies the model in-place.\n", + "\n", + "In order to build and export a new model while keeping the reference one, it is required to follow these steps:\n", + "\n", + "\\# Build e new mode for instance with torchlip.Sequential( torchlip.SpectralConv2d(...), ...)\n", + "\n", + "`wexport = ()`\n", + "\n", + "\\# Copy the parameters from the reference t the new model\n", + "\n", + "`wexport.load_state_dict(wass.state_dict())`\n", + "\n", + "\\# one forward required to initialize pamatrizations\n", + "\n", + "`vanilla_model(one_input)`\n", + "\n", + "\\# vanilla_export the new model\n", + "\n", + "`wexport = wexport.vanilla_export()`" + ] + }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -470,9 +501,9 @@ "text": [ "=== After export ===\n", "Linear(in_features=784, out_features=128, bias=True), min=0.9999998211860657, max=1.0\n", - "Linear(in_features=128, out_features=64, bias=True), min=0.9999998211860657, max=1.0000001192092896\n", - "Linear(in_features=64, out_features=32, bias=True), min=0.9999998211860657, max=1.0\n", - "Linear(in_features=32, out_features=1, bias=True), min=0.9999999403953552, max=0.9999999403953552\n" + "Linear(in_features=128, out_features=64, bias=True), min=0.9999998211860657, max=1.000000238418579\n", + "Linear(in_features=64, out_features=32, bias=True), min=0.9999998807907104, max=1.0\n", + "Linear(in_features=32, out_features=1, bias=True), min=0.9999998807907104, max=0.9999998807907104\n" ] } ], @@ -505,7 +536,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "deel-pt1.10", "language": "python", "name": "python3" }, diff --git a/docs/notebooks/wasserstein_classification_fashionMNIST.ipynb b/docs/notebooks/wasserstein_classification_fashionMNIST.ipynb index 4a25601..596513f 100644 --- a/docs/notebooks/wasserstein_classification_fashionMNIST.ipynb +++ b/docs/notebooks/wasserstein_classification_fashionMNIST.ipynb @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -94,7 +94,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -147,7 +147,7 @@ ")" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -213,79 +213,119 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Epoch 1/30\n", - "loss: 0.0318 - acc: 0.8132 - KR: 0.1755 - val_loss: 0.0330 - val_acc: 0.8626 - val_KR: 0.1893\n", - "Epoch 2/30\n", - "loss: 0.0258 - acc: 0.8736 - KR: 0.1943 - val_loss: 0.0288 - val_acc: 0.8715 - val_KR: 0.1913\n", - "Epoch 3/30\n", - "loss: 0.0295 - acc: 0.8873 - KR: 0.2057 - val_loss: 0.0261 - val_acc: 0.8870 - val_KR: 0.2080\n", - "Epoch 4/30\n", - "loss: 0.0181 - acc: 0.8968 - KR: 0.2127 - val_loss: 0.0260 - val_acc: 0.8861 - val_KR: 0.2187\n", - "Epoch 5/30\n", - "loss: 0.0307 - acc: 0.9029 - KR: 0.2183 - val_loss: 0.0249 - val_acc: 0.8914 - val_KR: 0.2235\n", - "Epoch 6/30\n", - "loss: 0.0253 - acc: 0.9062 - KR: 0.2224 - val_loss: 0.0253 - val_acc: 0.8868 - val_KR: 0.2149\n", - "Epoch 7/30\n", - "loss: 0.0229 - acc: 0.9100 - KR: 0.2261 - val_loss: 0.0239 - val_acc: 0.8979 - val_KR: 0.2227\n", - "Epoch 8/30\n", - "loss: 0.0203 - acc: 0.9122 - KR: 0.2300 - val_loss: 0.0215 - val_acc: 0.9028 - val_KR: 0.2220\n", - "Epoch 9/30\n", - "loss: 0.0185 - acc: 0.9154 - KR: 0.2319 - val_loss: 0.0234 - val_acc: 0.8999 - val_KR: 0.2294\n", - "Epoch 10/30\n", - "loss: 0.0228 - acc: 0.9186 - KR: 0.2350 - val_loss: 0.0207 - val_acc: 0.9089 - val_KR: 0.2314\n", - "Epoch 11/30\n", - "loss: 0.0238 - acc: 0.9199 - KR: 0.2366 - val_loss: 0.0224 - val_acc: 0.8980 - val_KR: 0.2299\n", - "Epoch 12/30\n", - "loss: 0.0224 - acc: 0.9224 - KR: 0.2403 - val_loss: 0.0214 - val_acc: 0.9062 - val_KR: 0.2262\n", - "Epoch 13/30\n", - "loss: 0.0134 - acc: 0.9231 - KR: 0.2393 - val_loss: 0.0199 - val_acc: 0.9126 - val_KR: 0.2427\n", - "Epoch 14/30\n", - "loss: 0.0174 - acc: 0.9249 - KR: 0.2425 - val_loss: 0.0204 - val_acc: 0.9099 - val_KR: 0.2434\n", - "Epoch 15/30\n", - "loss: 0.0227 - acc: 0.9272 - KR: 0.2449 - val_loss: 0.0198 - val_acc: 0.9147 - val_KR: 0.2449\n", - "Epoch 16/30\n", - "loss: 0.0194 - acc: 0.9270 - KR: 0.2463 - val_loss: 0.0196 - val_acc: 0.9120 - val_KR: 0.2427\n", - "Epoch 17/30\n", - "loss: 0.0120 - acc: 0.9298 - KR: 0.2483 - val_loss: 0.0199 - val_acc: 0.9098 - val_KR: 0.2441\n", - "Epoch 18/30\n", - "loss: 0.0091 - acc: 0.9321 - KR: 0.2514 - val_loss: 0.0193 - val_acc: 0.9112 - val_KR: 0.2418\n", - "Epoch 19/30\n", - "loss: 0.0117 - acc: 0.9317 - KR: 0.2559 - val_loss: 0.0195 - val_acc: 0.9163 - val_KR: 0.2483\n", - "Epoch 20/30\n", - "loss: 0.0091 - acc: 0.9340 - KR: 0.2564 - val_loss: 0.0190 - val_acc: 0.9144 - val_KR: 0.2537\n", - "Epoch 21/30\n", - "loss: 0.0127 - acc: 0.9336 - KR: 0.2609 - val_loss: 0.0182 - val_acc: 0.9179 - val_KR: 0.2638\n", - "Epoch 22/30\n", - "loss: 0.0171 - acc: 0.9361 - KR: 0.2641 - val_loss: 0.0185 - val_acc: 0.9146 - val_KR: 0.2613\n", - "Epoch 23/30\n", - "loss: 0.0143 - acc: 0.9362 - KR: 0.2662 - val_loss: 0.0187 - val_acc: 0.9136 - val_KR: 0.2625\n", - "Epoch 24/30\n", - "loss: 0.0209 - acc: 0.9380 - KR: 0.2683 - val_loss: 0.0184 - val_acc: 0.9173 - val_KR: 0.2586\n", - "Epoch 25/30\n", - "loss: 0.0136 - acc: 0.9382 - KR: 0.2726 - val_loss: 0.0190 - val_acc: 0.9126 - val_KR: 0.2634\n", - "Epoch 26/30\n", - "loss: 0.0127 - acc: 0.9387 - KR: 0.2742 - val_loss: 0.0188 - val_acc: 0.9149 - val_KR: 0.2712\n", - "Epoch 27/30\n", - "loss: 0.0076 - acc: 0.9404 - KR: 0.2787 - val_loss: 0.0181 - val_acc: 0.9162 - val_KR: 0.2704\n", - "Epoch 28/30\n", - "loss: 0.0211 - acc: 0.9417 - KR: 0.2790 - val_loss: 0.0187 - val_acc: 0.9137 - val_KR: 0.2715\n", - "Epoch 29/30\n", - "loss: 0.0174 - acc: 0.9414 - KR: 0.2829 - val_loss: 0.0185 - val_acc: 0.9161 - val_KR: 0.2804\n", - "Epoch 30/30\n", - "loss: 0.0186 - acc: 0.9423 - KR: 0.2820 - val_loss: 0.0187 - val_acc: 0.9128 - val_KR: 0.2860\n" + "Epoch 1/50\n", + "loss: 0.0257 - acc: 0.7874 - KR: 0.8125 - val_loss: 0.0219 - val_acc: 0.8306 - val_KR: 1.0971\n", + "Epoch 2/50\n", + "loss: 0.0257 - acc: 0.8382 - KR: 1.2778 - val_loss: 0.0160 - val_acc: 0.8530 - val_KR: 1.3746\n", + "Epoch 3/50\n", + "loss: 0.0111 - acc: 0.8485 - KR: 1.5971 - val_loss: 0.0162 - val_acc: 0.8232 - val_KR: 1.7986\n", + "Epoch 4/50\n", + "loss: 0.0066 - acc: 0.8521 - KR: 1.8986 - val_loss: 0.0143 - val_acc: 0.8511 - val_KR: 1.9778\n", + "Epoch 5/50\n", + "loss: 0.0030 - acc: 0.8551 - KR: 2.1034 - val_loss: 0.0092 - val_acc: 0.8579 - val_KR: 2.1881\n", + "Epoch 6/50\n", + "loss: 0.0028 - acc: 0.8607 - KR: 2.2412 - val_loss: 0.0070 - val_acc: 0.8605 - val_KR: 2.2559\n", + "Epoch 7/50\n", + "loss: 0.0103 - acc: 0.8644 - KR: 2.3199 - val_loss: 0.0076 - val_acc: 0.8485 - val_KR: 2.3628\n", + "Epoch 8/50\n", + "loss: 0.0062 - acc: 0.8661 - KR: 2.3732 - val_loss: 0.0057 - val_acc: 0.8596 - val_KR: 2.3941\n", + "Epoch 9/50\n", + "loss: -0.0055 - acc: 0.8677 - KR: 2.4145 - val_loss: 0.0055 - val_acc: 0.8491 - val_KR: 2.4343\n", + "Epoch 10/50\n", + "loss: -0.0086 - acc: 0.8708 - KR: 2.4599 - val_loss: 0.0049 - val_acc: 0.8613 - val_KR: 2.4484\n", + "Epoch 11/50\n", + "loss: 0.0052 - acc: 0.8703 - KR: 2.4972 - val_loss: 0.0038 - val_acc: 0.8529 - val_KR: 2.5537\n", + "Epoch 12/50\n", + "loss: -0.0062 - acc: 0.8740 - KR: 2.5305 - val_loss: 0.0020 - val_acc: 0.8677 - val_KR: 2.5299\n", + "Epoch 13/50\n", + "loss: -0.0046 - acc: 0.8753 - KR: 2.5532 - val_loss: 0.0027 - val_acc: 0.8694 - val_KR: 2.5189\n", + "Epoch 14/50\n", + "loss: -0.0004 - acc: 0.8765 - KR: 2.5746 - val_loss: 0.0058 - val_acc: 0.8594 - val_KR: 2.5631\n", + "Epoch 15/50\n", + "loss: -0.0013 - acc: 0.8765 - KR: 2.6024 - val_loss: -0.0003 - val_acc: 0.8766 - val_KR: 2.6008\n", + "Epoch 16/50\n", + "loss: 0.0091 - acc: 0.8801 - KR: 2.6371 - val_loss: 0.0021 - val_acc: 0.8668 - val_KR: 2.6268\n", + "Epoch 17/50\n", + "loss: -0.0033 - acc: 0.8811 - KR: 2.6631 - val_loss: 0.0012 - val_acc: 0.8717 - val_KR: 2.6742\n", + "Epoch 18/50\n", + "loss: -0.0064 - acc: 0.8809 - KR: 2.6901 - val_loss: 0.0006 - val_acc: 0.8657 - val_KR: 2.6784\n", + "Epoch 19/50\n", + "loss: -0.0005 - acc: 0.8820 - KR: 2.7062 - val_loss: 0.0007 - val_acc: 0.8744 - val_KR: 2.6597\n", + "Epoch 20/50\n", + "loss: -0.0035 - acc: 0.8820 - KR: 2.7165 - val_loss: -0.0002 - val_acc: 0.8775 - val_KR: 2.7445\n", + "Epoch 21/50\n", + "loss: -0.0086 - acc: 0.8847 - KR: 2.7422 - val_loss: -0.0014 - val_acc: 0.8745 - val_KR: 2.7166\n", + "Epoch 22/50\n", + "loss: 0.0000 - acc: 0.8859 - KR: 2.7515 - val_loss: -0.0007 - val_acc: 0.8739 - val_KR: 2.7656\n", + "Epoch 23/50\n", + "loss: 0.0035 - acc: 0.8825 - KR: 2.7664 - val_loss: -0.0022 - val_acc: 0.8794 - val_KR: 2.8049\n", + "Epoch 24/50\n", + "loss: -0.0041 - acc: 0.8831 - KR: 2.7871 - val_loss: -0.0013 - val_acc: 0.8790 - val_KR: 2.7953\n", + "Epoch 25/50\n", + "loss: -0.0167 - acc: 0.8853 - KR: 2.7945 - val_loss: -0.0010 - val_acc: 0.8720 - val_KR: 2.7675\n", + "Epoch 26/50\n", + "loss: -0.0061 - acc: 0.8860 - KR: 2.7932 - val_loss: -0.0016 - val_acc: 0.8730 - val_KR: 2.8115\n", + "Epoch 27/50\n", + "loss: -0.0107 - acc: 0.8864 - KR: 2.8091 - val_loss: -0.0019 - val_acc: 0.8766 - val_KR: 2.7849\n", + "Epoch 28/50\n", + "loss: -0.0062 - acc: 0.8851 - KR: 2.8195 - val_loss: -0.0020 - val_acc: 0.8735 - val_KR: 2.8323\n", + "Epoch 29/50\n", + "loss: -0.0084 - acc: 0.8885 - KR: 2.8274 - val_loss: -0.0021 - val_acc: 0.8776 - val_KR: 2.8002\n", + "Epoch 30/50\n", + "loss: -0.0007 - acc: 0.8870 - KR: 2.8297 - val_loss: -0.0017 - val_acc: 0.8751 - val_KR: 2.7982\n", + "Epoch 31/50\n", + "loss: -0.0050 - acc: 0.8870 - KR: 2.8472 - val_loss: -0.0024 - val_acc: 0.8793 - val_KR: 2.8330\n", + "Epoch 32/50\n", + "loss: -0.0029 - acc: 0.8877 - KR: 2.8435 - val_loss: -0.0021 - val_acc: 0.8709 - val_KR: 2.8541\n", + "Epoch 33/50\n", + "loss: 0.0057 - acc: 0.8897 - KR: 2.8467 - val_loss: -0.0028 - val_acc: 0.8798 - val_KR: 2.8615\n", + "Epoch 34/50\n", + "loss: -0.0048 - acc: 0.8887 - KR: 2.8576 - val_loss: -0.0026 - val_acc: 0.8786 - val_KR: 2.8566\n", + "Epoch 35/50\n", + "loss: 0.0002 - acc: 0.8895 - KR: 2.8640 - val_loss: -0.0029 - val_acc: 0.8759 - val_KR: 2.8470\n", + "Epoch 36/50\n", + "loss: -0.0103 - acc: 0.8884 - KR: 2.8747 - val_loss: -0.0030 - val_acc: 0.8795 - val_KR: 2.8565\n", + "Epoch 37/50\n", + "loss: -0.0098 - acc: 0.8893 - KR: 2.8683 - val_loss: -0.0015 - val_acc: 0.8761 - val_KR: 2.8617\n", + "Epoch 38/50\n", + "loss: 0.0033 - acc: 0.8902 - KR: 2.8757 - val_loss: -0.0030 - val_acc: 0.8746 - val_KR: 2.8755\n", + "Epoch 39/50\n", + "loss: -0.0073 - acc: 0.8897 - KR: 2.8798 - val_loss: -0.0039 - val_acc: 0.8789 - val_KR: 2.8708\n", + "Epoch 40/50\n", + "loss: -0.0086 - acc: 0.8902 - KR: 2.8815 - val_loss: -0.0034 - val_acc: 0.8787 - val_KR: 2.8560\n", + "Epoch 41/50\n", + "loss: 0.0121 - acc: 0.8913 - KR: 2.8832 - val_loss: -0.0019 - val_acc: 0.8798 - val_KR: 2.8585\n", + "Epoch 42/50\n", + "loss: -0.0059 - acc: 0.8899 - KR: 2.8782 - val_loss: -0.0026 - val_acc: 0.8775 - val_KR: 2.8855\n", + "Epoch 43/50\n", + "loss: -0.0010 - acc: 0.8930 - KR: 2.8835 - val_loss: -0.0036 - val_acc: 0.8829 - val_KR: 2.8955\n", + "Epoch 44/50\n", + "loss: -0.0095 - acc: 0.8925 - KR: 2.8908 - val_loss: -0.0038 - val_acc: 0.8767 - val_KR: 2.8424\n", + "Epoch 45/50\n", + "loss: -0.0097 - acc: 0.8933 - KR: 2.8954 - val_loss: -0.0030 - val_acc: 0.8799 - val_KR: 2.8613\n", + "Epoch 46/50\n", + "loss: -0.0166 - acc: 0.8931 - KR: 2.8888 - val_loss: -0.0037 - val_acc: 0.8801 - val_KR: 2.8815\n", + "Epoch 47/50\n", + "loss: -0.0128 - acc: 0.8917 - KR: 2.8968 - val_loss: -0.0036 - val_acc: 0.8782 - val_KR: 2.8733\n", + "Epoch 48/50\n", + "loss: -0.0061 - acc: 0.8927 - KR: 2.8994 - val_loss: -0.0038 - val_acc: 0.8793 - val_KR: 2.8937\n", + "Epoch 49/50\n", + "loss: -0.0024 - acc: 0.8923 - KR: 2.9055 - val_loss: -0.0042 - val_acc: 0.8803 - val_KR: 2.8879\n", + "Epoch 50/50\n", + "loss: -0.0143 - acc: 0.8936 - KR: 2.9024 - val_loss: -0.0038 - val_acc: 0.8826 - val_KR: 2.9081\n" ] } ], "source": [ - "loss_choice = \"SoftHKRMulticlassLoss\" # \"HKRMulticlassLoss\" or \"SoftHKRMulticlassLoss\"\n", - "epochs = 30\n", + "loss_choice = \"HKRMulticlassLoss\" # \"HKRMulticlassLoss\" or \"SoftHKRMulticlassLoss\"\n", + "epochs = 50\n", "\n", "optimizer = torch.optim.Adam(lr=1e-3, params=model.parameters())\n", "hkr_loss = None\n", @@ -371,9 +411,35 @@ "layers.\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Warnings:\n", + "vanilla_export method modifies the model in-place.\n", + "\n", + "In order to build and export a new model while keeping the reference one, it is required to follow these steps:\n", + "\n", + "\\# Build e new mode for instance with torchlip.Sequential( torchlip.SpectralConv2d(...), ...)\n", + "\n", + "`vanilla_model = ()`\n", + "\n", + "\\# Copy the parameters from the reference t the new model\n", + "\n", + "`vanilla_model.load_state_dict(model.state_dict())`\n", + "\n", + "\\# one forward required to initialize pamatrizations\n", + "\n", + "`vanilla_model(one_input)`\n", + "\n", + "\\# vanilla_export the new model\n", + "\n", + "`vanilla_model = vanilla_model.vanilla_export()`" + ] + }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -393,7 +459,7 @@ ")" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -421,14 +487,14 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "# Select only the first batch from the test set\n", - "sub_data, sub_targets = iter(test_loader).next()\n", + "sub_data, sub_targets = next(iter(test_loader))\n", "sub_data, sub_targets = sub_data.to(device), sub_targets.to(device)\n", "\n", "# Drop misclassified elements\n", @@ -474,7 +540,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -483,16 +549,16 @@ "text": [ "Image # Certificate Distance to adversarial\n", "---------------------------------------------------\n", - "Image 0 0.538 1.61\n", - "Image 1 1.519 3.63\n", - "Image 2 0.444 1.51\n", - "Image 3 0.695 1.85\n", - "Image 4 0.284 0.88\n", - "Image 5 0.272 0.70\n", - "Image 6 0.181 0.65\n", - "Image 7 0.544 1.13\n", - "Image 8 1.061 2.94\n", - "Image 9 0.214 0.62\n" + "Image 0 0.349 1.43\n", + "Image 1 1.783 4.59\n", + "Image 2 0.368 1.47\n", + "Image 3 0.647 2.16\n", + "Image 4 0.166 0.56\n", + "Image 5 0.244 0.99\n", + "Image 6 0.108 0.55\n", + "Image 7 0.362 1.31\n", + "Image 8 1.514 3.90\n", + "Image 9 0.217 0.91\n" ] } ], @@ -537,14 +603,14 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAdYAAAXnCAYAAADxc1hBAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydd3gWxdr/v0lIIwmEJCQkQBIMkNCRDpZQLIgIx2N5EZEmiiCivpaDKFIseMBylFeaCNiwi6BSRAigh1As9C7dkFCDIRDS5vcHv0TmniHPk7iEJHw/18V1cc8zszu7O7uT3fue7+2hlFIghBBCiCN4Xu4OEEIIIRUJTqyEEEKIg3BiJYQQQhyEEyshhBDiIJxYCSGEEAfhxEoIIYQ4CCdWQgghxEE4sRJCCCEOwomVEEIIcZBSn1g9PDzc+rd8+fIS7yM2Nhbdu3d3WW/58uXF2tecOXPwn//8p8g6//u//4tmzZoBAFatWoUxY8YgPT3dre2Tss2aNWtw++23Izo6Gr6+voiIiED79u3xxBNPFNa5nGOPACkpKRgzZgzWr19v/DZmzBh4eHhoZdnZ2XjooYcQGRkJLy8vNG/evNj7PHLkCPr374+wsDBUrlwZ7du3x9KlS0vU/z59+sDDw8M6hjIyMjB8+HDUrFkTvr6+qF+/PiZMmIC8vLwS7as4dOzYEY0bN77k+7Hx3HPPoXv37qhZsyY8PDzQv3//Em9rxowZ8PDwQGBgoPHbW2+9hXbt2iEsLAy+vr6Ijo5Gr169sGXLluLvSJUyycnJ2r9u3bopf39/o/zUqVMl3kdMTIy69dZbXdY7depUsfZ16623qpiYGJf7Hjt2rFJKqYkTJyoAau/evW5tn5Rdvv32W+Xp6ak6d+6sPv74Y7V8+XL18ccfqyeeeELVrFmzsN7lHHtEqXXr1ikAatasWcZvBw8eVMnJyVrZf/7zHwVATZo0Sa1atUpt3LixWPvLyspSjRs3VrVq1VIffvih+v7771XPnj1VpUqV1PLly4u1rW+//VYFBASoKlWqGGMoJydHtW3bVlWrVk393//9n/r+++/V//7v/yoPDw/1yCOPFGs/JSExMVE1atToku/HRuXKlVW7du3UQw89pHx8fFS/fv1KtJ1Dhw6pqlWrqqioKBUQEGD8/vzzz6sxY8aouXPnquXLl6uZM2eq+vXrq4CAALV9+/Zi7avUJ1ZJv379rAf5d3D34eYumZmZSinXD7e1a9cqAGrz5s1KKU6sFYnrr79excXFqZycHOO3vLy8wv9frrF3pZObm6uysrKKnFhtDBo0SPn7+5d4v2+//bYCoFatWlVYlpOToxo2bKjatGnj9nbS09NVzZo11euvv24dQx9//LECoL788kut/MEHH1Senp7FfvAXl8s5sV54fwUEBJR4Yu3evbu67bbbijXnbN26VQFQo0aNKta+yp2Pdc+ePejVqxeioqIKP8d16dLF+uln0aJFaNGiBfz9/ZGQkICZM2dqv9s+x/Xv3x+BgYHYtGkTbrrpJgQFBaFLly7o2LEjvvvuO+zfv1/7ZH0hX375JeLj49GoUSOMGTMGTz31FACgTp06xifu/Px8TJgwAQkJCfD19UV4eDj69u2LQ4cOadss+ATz448/ol27dvD390fNmjUxatSoUvkERM5z/PhxhIWFoVKlSsZvnp7mbVTaY688sH37dtxzzz2IiIgo/NTWt29fnDt3rrBOamoqBg8ejFq1asHHxwd16tTB2LFjkZubW1hn37598PDwwIQJE/Diiy+iTp068PX1RVJSElq3bg0AGDBgQOF5GjNmDADzU7CHhwdmzJiBs2fPFtadPXt2sY5p7ty5iI+PR/v27QvLKlWqhD59+mDt2rX4448/3NrOE088gcjISAwfPtz6+3//+194eHjglltu0cq7d++O/Px8zJ07t1j9LsDd51AB7jyHpkyZgmbNmiEwMBBBQUFISEjAyJEjS9Q/wH5/FZcPP/wQK1aswOTJk4vVrnr16gBgve+Loni1ywDdunVDXl4eJkyYgOjoaBw7dgyrVq0y/JgbNmzAE088gREjRiAiIgIzZszA/fffj7p16+L6668vch/Z2dno0aMHBg8ejBEjRiA3Nxe1atXCgw8+iN9///2ig/jLL7/E3XffDQAYNGgQTpw4gUmTJuGrr75CZGQkAKBhw4YAgCFDhmD69OkYNmwYunfvjn379mHUqFFYvnw5fv31V4SFhRVuNzU1Fb169cKIESMwbtw4fPfdd3jxxRdx8uRJ/N///V9JTyUpBu3bt8eMGTMwfPhw3HvvvWjRogW8vb2tdS/H2CvrbNiwAddeey3CwsIwbtw41KtXD4cPH8b8+fORnZ0NX19fpKamok2bNvD09MTzzz+PuLg4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PTWXMXJzc1XlypVV27Zt3ar/4IMPKgBq2LBhatGiRWrq1KmqevXqqnbt2uro0aOF9aZMmaLGjx+v5s+fr1asWKHee+891axZMxUfH6+ys7OVUkodPHhQPfLIIwqA+uqrr1RycrJKTk5Wp06duiTHSpzD9saamJioQkJCVO3atdWkSZNUUlKSWrFihTpy5IiqWbOmql69upo6dapatGiRGjZsmAKghgwZUti+4K/lpKQkbbt79+413qri4+NV3bp11QcffKBWrFihvvzyS/XEE09obV966SXl4eGhBg4cqL799lv11Vdfqfbt26uAgAC1ZcsW7Vi8vb1VbGysGj9+vFq6dKlavHixo+eruKxfv14FBgaq2NhYNXXqVLV06VL14Ycfqrvvvlv9+eefSimlDh8+rGrXrq1iYmLUtGnT1A8//KBeeOEF5evrq/r371+4rYLzV7NmTdWpUyf1xRdfqO+//15t2LBBzZo1SwFQzz33XOH9d/DgQaWUUqNHj1YXPvKSk5NVt27dlL+/f2HdI0eOFG6/X79+Lo+rRo0a6q7/x957h2dRrP//75A8CWkQkkAgQBIMJYAgIkVACUVRiihSDnCUJogionw8RxRUiiCKKJavYsV6UMEGIlINWEDAgyBNESVICyUQSggkIfP7g1+ew9wz5NnETUjw/bourot7npnd2d3Zney+77nv3r2N8gULFigARTrv+ePm+eeft/6ek5OjTp48qb777juVlJSkrrnmGpWbm1vo/Sjl/PmXnJysoqKiVGxsrHrhhRfU4sWL1ahRoxQAdc8993jrffDBBwqAuvfee9WSJUvUsmXL1CuvvKJGjRql7Tc+Pl7Fx8f77F/fvn1V5cqVjfKTJ08qAOrhhx8usH3+uaxSpYry9/dX4eHhqlOnTurbb7/V6v3555/efj/88MPe+g0aNFBvv/22z35KLrmJNS0tTQFQffv29Vl327ZtCoAaMWKEVr5mzRoFQI0dO9baLi8vT+Xk5Khdu3YpANpnZX4KLptcaGKF5TPbQw89pACoNWvWaOV333238vPzU7/++qtSyvnEevjwYQVAPffccxfs359//qkCAgLUvffeq5WfOHFCVa1aVfXp00c7FgBq1qxZjo69JOjQoYOKiIhQBw8evGCd4cOHq7CwMLVr1y6tfPr06QqA94+H/POXmJjo/aM2n4I+BcuJVSn7dU9NTVX+/v5qyJAhPo/L4/Go4cOHG+WrVq1SANTs2bN9buN8jh8/rurXr69q1qypTpw4Yfy+evVqBcD7r0uXLt4/TApLYZ5/+feClNCGDRumypUr571mI0eOVBERET73nZiYqBITE33Wu/7661W9evWsvwUGBqo777yzwPbr169X9913n/rss8/UN998o2bNmqXq16+v/P391aJFi7z18s9rhQoVVIMGDdScOXPU4sWLVa9evRQA9dprr/ns6/lckp+CnZKSkgLg3Kez82nRogXq16+vefYdPHgQd911F2rWrImAgAB4PB7Ex8cDALZt21ZifSYlS6VKldChQwet7Ouvv0aDBg3QokULrXzQoEFQShkOUL6IjIxEYmIinn76aTz77LP46aeftE/KALB48WLk5uZiwIAByM3N9f4rX748kpOTrXJDz549C9WP4uLUqVNYuXIl+vTp4/10aWPBggVo3749YmNjtWPs3LkzAGDlypVa/e7du8Pj8bje3/j4eOTm5uLNN990VF96Gjv9TXL69Gnceuut2LVrF+bOnWt1pGrUqBHWrVuHlStX4vnnn8dPP/2E66+/HqdOnXK8n3wK8/wDgPDwcK+Elk///v2Rl5eHb775xts2IyMD/fr1w7x58y7osbxjxw7s2LHDUT//yvm98sor8dxzz+GWW27Btddei8GDB2PVqlWoVq0aHnzwQW+9/Pvt9OnTWLhwIXr37o1OnTphzpw5aNq0KSZNmuSor/lcchNrvsv7zp07fdZNT08HAFSrVs34LTY21vt7Xl4eOnXqhE8//RQPPvggli9fjrVr13rdtbOyslw8AlKasI2N9PT0C46Z/N8Lg5+fH5YvX44bbrgB06ZNQ9OmTVG5cmWMGjUKJ06cAAAcOHAAANC8eXN4PB7t30cffWQ8wEJCQlChQoVC9aO4OHr0KM6ePYsaNWoUWO/AgQP44osvjONr2LAhABjHaLsGJU1UVJT1eucv54uMjHS0nTNnzng9UufPn4+WLVta64WGhqJZs2Zo27YtRo0ahc8++wxr1qzBq6++Wui+O33+5XO+h3I+VatW1bZ1++23Y9asWdi1axd69uyJKlWqoGXLlli6dGmh+wdc+PxmZmYiOzvb8fk9n4iICHTr1g0///yz99kdFRUFAEhKSvK+MAHn7s0bbrgBe/bswcGDBx3v45JbbuPv74+OHTviq6++wp49ewq8mfNP5v79+416+/btQ3R0NABg8+bN2LhxI95++20MHDjQW8fpX1yk7GL7izgqKsq6znDfvn0A4B035cuXBwDNOQcwJwjg3FtS/hvS9u3bMWfOHEyYMAHZ2dl45ZVXvNv8+OOPtRu/MP2+WERGRsLf3/+CDjH5REdHo3HjxpgyZYr19/w/XPIpDcfYqFEjbNq0ySjPL3Oy9vPMmTO45ZZbkJKSgnnz5hXK0axZs2YoV64ctm/f7rzT/z9On3/55P9xdz5paWnatoBzjkWDBw9GZmYmvvnmG4wfPx7dunXD9u3bHY3d82nUqBE+/PBDpKWleSdxoHDn14ZSCsD/xlBiYiJCQkIKrFuunPP30EvujRUAHn74YSilMGzYMGRnZxu/5+Tk4IsvvvB+4nv//fe139etW4dt27Z5B3j+yZcexra/EvPr8C320qVjx47YunUr1q9fr5W/++678PPz866XS0hIAHAuUMn5zJ8/v8Dt161bF4888ggaNWrk3ccNN9yAgIAA/P7772jWrJn1X2klODgYycnJmDt3boHBDLp164bNmzcjMTHRenxyYrVR0vdfjx498Msvv2DNmjXestzcXLz//vto2bKlzz7nv6l+/fXX+OSTT3DDDTcUav8rV65EXl4eateuXei+O33+5XPixAlj7M6ePRvlypVD27Ztje2Hhoaic+fOGDduHLKzs7Fly5ZC9/Hmm2+Gn58f3nnnHa387bffRnBwsLEu2QlHjx7FggUL0KRJE+8fvwEBAbj55puxbds2pKameusqpbBo0SIkJiYaf2gUSKEU2TLEa6+9pgICAtTll1+uXnrpJbVixQq1dOlSNW3aNFW7dm11yy23KKXOecX5+fmp+++/Xy1evFi9+uqrqkqVKqpmzZrq8OHDSimlsrOzVWJiooqPj1ezZ89WixYtUvfcc4+qW7euAqDGjx/v3W++w8rw4cPVqlWr1Lp164rsXEBKjgs5L9nW7uV7BVetWlW99tprXg9JPz8/wxHkuuuuU5UqVVKvv/66WrJkiRozZoyqU6eO5mCzceNGde2116oXXnhBffXVV2r58uVq3Lhxqly5cpoDyRNPPKECAgLU8OHD1WeffaZWrFihPvroI/XAAw+oxx57rMBjudjkewVfdtll6rXXXlNff/21+uCDD1S/fv2898e+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eLEaNWqUAqDuueeewpxqR2zYsEGFhYWphIQE9corr6jly5er999/X/Xp00cdP35cKaXU/v37Vc2aNVV8fLx69dVX1bJly9Tjjz+ugoKC1KBBg7zb2rlzpwKgqlevrtq3b68+/vhjtWTJErVx40b11ltvKQDqkUce8Z6b3bt3K6WUGj9+vDr/kbd69WrVpUsXFRwc7K178OBB7/YHDhzo87iqVq2qevfubZQvWLBAAVCLFy92fI6cvLGGh4ersLAwFRAQoOrXr6+mT5+ucnNzHe9D4vZ4+eCDD7zP3yVLlqhly5apV155RY0aNco4Fidf9vr27asqV65slJ88eVIBUA8//HCB7fPvxSpVqih/f38VHh6uOnXqpL799luj7sCBA1W5cuVUTEyMKleunKpevbq65557fL4V27jkJtbDhw+ra665RgFQAJTH41GtW7dWU6dOVSdOnPDWi4+PV+XLl1e7du3ylmVlZanIyEg1fPhwb9mFHpIA1KxZs4z981Nw6aAwE+vhw4cVAPXcc88VWO9ijJkNGzYoAOq///2vt+xCn4K3bdumAKgRI0Zo5WvWrFEAvJOzUucelADUvHnztLrDhg1T5cqV047RDTp06KAiIiLUwYMHL1hn+PDhKiwszNj39OnTFQC1ZcsWpdT/JtbExETjD+eCPgXLiVUp+zhJTU1V/v7+asiQIT6Py+PxaNc+n1WrVikAavbs2T63kY+viXXEiBFq1qxZauXKlerzzz9X//znPxUAddtttznex/kUx3gZOXKkioiI8LnvxMRElZiY6LPe9ddfr+rVq2f9LTAwUN15550Ftl+/fr2677771Geffaa++eYbNWvWLFW/fn3l7++vFi1apNV99tln1bPPPquWLFmilixZosaNG6dCQkJUUlKSNnc44ZL7FBwVFYVvv/0W69atw5NPPombb74Z27dvx8MPP4xGjRrh8OHD3rpNmjRBXFyc1y5fvjzq1q2LXbt2OdpXz549Xe8/KT6UUsjNzdX+AUBkZCQSExPx9NNP49lnn8VPP/2EvLw86zZKesx88sknSEhIQNOmTX3WTUlJAXDus/P5tGjRAvXr1zc8VcPDw9G9e3etrH///sjLy8M333xTqH4WxKlTp7By5Ur06dMHlStXvmC9BQsWoH379oiNjdWuUefOnQEAK1eu1Op3794dHo/HtX7mEx8f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FxITEzFw4ECfOuuQIUPw0ksvoXfv3njyySdRpUoVvPzyy/j111+xbNkyrW7Hjh2xcuVKbenQFVdcgdtuuw3169dH+fLlsXbtWjz99NOoWrUqHnzwQa391q1bsXXrVgDn3hRPnTqFjz/+GADQoEEDNGjQwFvXz88PycnJBQY5qFevHu688068+OKLKFeuHDp37ozU1FQ8+uijqFmzJkaPHq3Vj4qKwt13340///wTdevWxcKFC/H666/j7rvv9n5mHTZsGIKDg9GmTRtUq1YNaWlpmDp1KipWrKhN3rVr1wYARzrrypUrcejQIQDA2bNnsWvXLu9xJycne984J02ahEmTJmH58uXer0p33nknKlSogBYtWiAmJgaHDx/G3Llz8dFHH+Hf//639rZ66NAhr46f/8Xhq6++QuXKlVG5cmXHwT4AlF6v4I8++kj1799f1alTR4WFhSmPx6Pi4uLU7bffrrZu3eqtd6EgDcnJySo5OVkrO3TokBo1apSqVauW8ng8KjIyUl111VVq3Lhx6uTJk956s2bNUvXq1VNBQUHqsssuU1OnTlVvvvmm4SVm23dKSooKCwtTN954ozp16pRSSqmVK1eqrl27qsjISOXxeFT16tVV165d1dy5c73tCrNQnLiL07G2atUq1apVKxUSEqIqV66shg4dqtavX294odo8TZWye6Xu2bNH9ezZU4WFhanw8HDVs2dPr0fp+dvMr1epUiUVHh6ubrzxRrV582YVHx+veUmWVq9gpZTaunWr6t27t4qKilKBgYEqLi5ODRo0SJ0+fdpbx8k9mu8V/PTTT1v388EHH6ikpCTl8XgUADV+/HillHOv4PztO/U+TUtLUwMGDFCRkZGqfPny6uqrr1ZLly416uV71p5P3759Ve3atVVoaKjyeDwqPj5e3XXXXWrfvn1G+/z+2/7lH6NSSp04cUIBUH379vXZ97Nnz6qnnnpK1a1bV3k8HhUdHa1uu+02tXv3bqPvDRs2VCtWrFDNmjVTQUFBqlq1amrs2LFaYJV33nlHtW/fXsXExKjAwEAVGxur+vTpo37++Wdte/Hx8Y69mvPPm+3f+eM8//ycXzZr1ix17bXXqujoaBUQEKAiIiJUcnKyeu+994z95N87tn9yLvGFn1IOoh4QQggpEyxcuBDdunXDxo0b0ahRo4vdnb8lpXa5DSGEkMKT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bsG8efOsY88//zxeeOEFPPDAA/jss88wfvz4k7rejz/+iJ49e2LGjBl46qmn8I9//AM1a9ZE7969MXv27Kj18/Pz0aVLFzz33HOYOnUqnnzySWzbtg3dunVz6o8fPx7Lli1Dhw4dkJvLqWHOXn79619j1KhRuP322/HRRx/hyiuvxJ133ok//OEPUesuXLgQ3bp1w8GDBzF+/HiMHz8ehw4dQs+ePZ2xMGPGDEyfPh3Z2dno3LnzqTXalAELFy40AEzfvn0NAHPrrbee0vkGDRpkkpKSopbLy8szeXl5p3Stklz3dNC3b1+Tk5NTJtc+k+nWrZvJzc01hw8fdv525MiRMmiRS05Ojunbt+9pO//+/ftP27nLiqKiInPo0KHI3PK3v/0tUL1bbrnFJCYmlvi6f/7znw0AM3fu3Mixw4cPm+bNm5sOHTqU6JwFBQUmLi7OXH/99dbxY8dnab//eXl5pkWLFqV2vWK+/vprExMTY/7whz9Yx2+99VaTmJho8vPzT1i/V69epmbNmtaY37Nnj8nIyDCdO3e2yh7bv3/84x8NALNmzZoStbtMvljHjRsHABg9ejQ6d+6MN954AwcOHLDKrF27FjExMXjiiSfw5JNPon79+khOTsaFF16Izz//POo15syZg4yMDPTr1w/79+8/brnCwkI8+uijaNq0KRISElC9enXceOON2LFjR+D7WbZsGXr27ImkpCRUr14dd9xxh3M/hw4dwn333Yf69esjPj4etWvXxu23346CggKr3NGjR/H4449H2lOjRg3ccMMN2LhxY6RM9+7d8c9//hPr1q1DTExM5D8Rnfz8fGRkZCA2lpNZAhUq/Pt1KP45dsqUKbjggguQmJiIpk2bRn5pOZatW7fitttuQ506dRAfH4/69etjxIgRTg7SESNGoGPHjkhLS0OVKlVwwQUXYNy4cYFyrT733HOIjY3F8OHDI8emT5+Onj17okqVKqhcuTK6dOni/ARZ/PPnl19+if79+6NatWpl9rXz7bff4pprron87JmdnY0bbrgBP/74Y6RMkL4snhsef/xxPProo6hfvz4SEhIwc+ZMtG/fHgBw4403Rt6Lhx9+GID7U3BMTAz++te/4uDBg5Gyf//730/qnt599100adIEF154YeRYbGwsrrvuOixYsACbNm066X5KSUlBpUqVnDF67PgMiyDzzbF8+umn6NSpExITE1G7dm08+OCDOHLkiFXm+eefR+vWrZGcnIyUlBQ0bdoU999/f4na995778EYgxtvvNE6fuONN+LgwYOYMmXKCevPmTMH3bt3t3IRp6SkoFu3bpg7dy62bNkSOR5q/5ZoOT4FDhw4YFJTU0379u2NMcb89a9/NQDM3//+d6vcmjVrDABTr14907t3b/Pee++Z9957z7Rq1cpUq1bNFBQURMryl+PEiRNNQkKCGTp0qCkqKooc5y/WI0eOmN69e5ukpCQzYsQIM23aNPPXv/7V1K5d2zRv3twcOHDghPcyaNAgEx8fb7Kzs81jjz1mpk6dah5++GETGxtr+vXrFyl39OhR06tXLxMbG2sefPBBM3XqVPPEE0+YpKQkc/7555tDhw5Fyg4ZMsQAMHfccYeZMmWKGTt2rKlevbqpW7eu2bFjhzHGmGXLlpkuXbqYWrVqmXnz5kX+E9G55ZZbDADzm9/8xnz++eemsLDQWy4nJ8fUqVPHNG/e3Lzyyivmo48+MgMGDDAAzOzZsyPltmzZYurWrWtycnLMCy+8YKZPn24eeeQRk5CQYAYPHmydc/DgwWbcuHFm2rRpZtq0aeaRRx4xiYmJZsSIEc61i79Yjx49au6++24TFxdnfYWNHz/exMTEmCuuuMK888475oMPPjD9+vUzFStWNNOnT4+UGz58uAFgcnJyzL333mumTZtm3nvvvVPtxpNm0aJFJjk52dSrV8+MHTvWzJgxw7z66qvm6quvNnv27DHGBO/L4rmhdu3apkePHuatt94yU6dONYsXLzZ/+9vfDADz+9//PvJebNiwweqLYubNm2cuvfRSk5iYGCm7ffv2yPkHDRoU9b5q1aplBgwY4Bz/8MMPDQDz0UcfBeqfoqIiU1hYaNasWWOGDBlikpOTzRdffHHc8mF9sQaZb4z5ae5MT083WVlZ5umnnzYfffSRGTZsmAFgbr/99ki5119/PfJ+TZ061UyfPt2MHTvWDBs2zLpuTk5OoPYPHDjQVK9e3Tm+b98+A8Dcd999J6wfHx9vbrjhBuf4Nddcc8Lnc6pfrKW+sL7yyisGgBk7dqwxxpi9e/ea5ORk87Of/cwqVzy4W7VqZS2OCxYsMADM66+/Hjl27MI6evRoU7FiRTNmzBjn2rywFg+Ct99+2ypX/HPSc889d8J7GTRokAFgnnrqKev4Y489ZgCYzz77zBhjzJQpUwwA8/jjj1vlJk6caACYv/zlL8YYY7755hsDwPz617+2ys2fP98AMPfff3/kmH4KLhk7d+40Xbt2NQAMABMXF2c6d+5sRo0aZfbu3Rspl5OTYypVqmTWrVsXOXbw4EGTlpZmbrvttsix2267zSQnJ1vljDHmiSeeMADMsmXLvO04cuSIOXz4sBk5cqRJT083R48eta7dt29fc+DAAXPVVVeZ1NRUa7Hcv3+/SUtLM5dddplzztatW1s/QRYvJg899NBJ9lS4XHTRRaZq1apm+/btxy0TtC+L54bc3FznH0Yn+imYF1Zj/HLO2rVrTcWKFc1NN90U9b7i4uKs8VDM3LlzDQAzYcKEqOcwxpgmTZpExmRmZmZk7jgeYbz/JzPf5OXlGQDmH//4h1X21ltvNRUqVIg8szvuuMNUrVo16rVzc3NNbm5u1HI///nPTZMmTbx/i4+PN0OGDDlh/TZt2pjGjRtbP/MePnzYNGjQ4ITP54z7KXjcuHFITEzEwIEDAQDJyckYMGAAPv30U6xatcop37dvX1SsWDFin3feeQCAdevWWeWMMbjtttswfPhwTJgwAf/93/8dtS0ffvghqlatissuuwxFRUWR/9q0aYNatWph1qxZge7pP/7jPyz72muvBQDMnDkTAPDxxx8D+MmL+FgGDBiApKSkyM93xeW5XIcOHdCsWbPAnobi+KSnp+PTTz/FwoULMXr0aPziF7/AypUrcd9996FVq1bYuXNnpGybNm2QnZ0dsStVqoTGjRtbY+/DDz9Ejx49kJWVZY2hPn36AIDlgPLxxx/j4osvRmpqKipWrIi4uDg89NBDyM/Px/bt26125ufn46KLLsKCBQvw2WefoWfPnpG/zZ07F7t27cKgQYOsax49ehS9e/fGwoULHfnjqquuCqcDS8CBAwcwe/ZsXH311ahevfpxy51MXwLA5Zdfjri4uNDbm5OTg6KioohkFY0TyTBBJZq3334b8+fPx5tvvonmzZujT58+geefknKy801KSgouv/xy69i1116Lo0eP4pNPPonULSgowDXXXIN//OMf1vt0LKtXr8bq1asDtfNU+vc3v/kNVq5ciTvuuAObNm3Chg0b8Ktf/SryDp+On9eBUvYKXr16NT755BP07dsXxhgUFBSgoKAA/fv3BwCvfpWenm7ZCQkJAICDBw9axwsLCzFx4kS0aNEi8iJGY9u2bSgoKEB8fDzi4uKs/7Zu3XrcQXEssbGxThtr1aoF4KfJsfj/sbGxzqQSExODWrVqWeUAIDMz07lOVlZW5O/i1GnXrh3uvfdevPnmm9i8eTPuuusurF27Fo8//nikDD9X4Kfxd+zY27ZtGz744ANn/LRo0QIAImNowYIFuOSSSwAAL774IubMmYOFCxfigQceAOCO55UrV2L+/Pno06ePs81h27ZtAID+/fs71x0zZgyMMdi1a5dVxzemSovdu3fjyJEjqFOnzgnLBe3LYsrynopJT0/3vpfF/Z+WlhboPC1atECHDh3Qv39/TJkyBTk5ObjzzjtDbStzsvNNzZo1nXI8111//fV46aWXsG7dOlx11VWoUaMGOnbsiGnTppWojcfr3/3796OwsDBq/950000YPXo0xo8fjzp16iA7OxvLly/HPffcAwCoXbt2idoVDdeD4zTy0ksvwRiDt956C2+99Zbz95dffhmPPvqo9YUalGLnhV69euHiiy/GlClTUK1atRPWycjIQHp6+nEF8JSUlKjXLSoqQn5+vjUJb926FcC/J+b09HQUFRVhx44d1uJqjMHWrVsjDhfF5bds2eJMQps3b0ZGRkbU9oiTJy4uDsOHD8f//M//4Ouvvz6puhkZGTjvvPPw2GOPef+elZUFAHjjjTcQFxeHDz/8EJUqVYr8/b333vPWu/DCCzFgwADcfPPNAH5yCCn+13XxOHjmmWfQqVMnb32eBMvSuS0tLQ0VK1Y8rkNMMUH7spjy4LDXqlUrLF261DlefKwkez9jY2NxwQUX4P/+7/9OuX0n4mTnm+J/0B0Lz3XAT45FN954I/bv349PPvkEw4cPR79+/bBy5Urk5OScVBtbtWqFN954A1u3bo0s4sDJ9e+9996L3/72t1i1ahVSUlKQk5OD2267DUlJSWjbtu1JtScopfbFeuTIEbz88svIzc3FzJkznf/uvvtubNmyBZMnTy7xNc4//3zMnj0bGzduRPfu3Z2f15h+/fohPz8fR44cQbt27Zz/mjRpEui6r732mmVPmDABACLBKIp/xnv11Vetcm+//Tb2798f+ftFF13kLbdw4UJ888031s+B/OUkgnGsF+CxfPPNNwDcyTsa/fr1w9dff43c3FzvGCo+X0xMDGJjY61/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", "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -611,7 +677,7 @@ ], "metadata": { "kernelspec": { - "display_name": "deel-pt1.10", + "display_name": "deel-pt1.13.1", "language": "python", "name": "python3" }, @@ -625,7 +691,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.10.15" } }, "nbformat": 4, diff --git a/docs/source/basic_example.rst b/docs/source/basic_example.rst index 9404021..10b3fcc 100644 --- a/docs/source/basic_example.rst +++ b/docs/source/basic_example.rst @@ -54,21 +54,32 @@ The following table indicates which module are safe to use in a Lipschitz networ - * - :class:`torch.nn.AvgPool2d`\ :raw-html-m2r:`
`\ :class:`torch.nn.AdaptiveAvgPool2d` - no - - :class:`.ScaledAvgPool2d`\ :raw-html-m2r:`
`\ :class:`.ScaledAdaptiveAvgPool2d` \ :raw-html-m2r:`
` \ :class:`.ScaledL2NormPool2d` + - :class:`.ScaledAvgPool2d`\ :raw-html-m2r:`
`\ :class:`.ScaledAdaptiveAvgPool2d` \ :raw-html-m2r:`
` \ :class:`.ScaledL2NormPool2d` \ :raw-html-m2r:`
` \ :class:`.ScaledAdaptativeL2NormPool2d` - The Lipschitz constant is bounded by ``sqrt(pool_h * pool_w)``. * - :class:`Flatten` - yes - n/a - - - * - :class:`torch.nn.Dropout` + - + * - :class:`torch.nn.ConvTranspose2d` - no - - None - - The Lipschitz constant is bounded by the dropout factor. + - :class:`.SpectralConvTranspose2d` + - :class:`.SpectralConvTranspose2d` also implements Björck normalization. * - :class:`torch.nn.BatchNorm1d` \ :raw-html-m2r:`
` \ :class:`torch.nn.BatchNorm2d` \ :raw-html-m2r:`
` \ :class:`torch.nn.BatchNorm3d` + - no + - :class:`.BatchCentering` + - This layer apply a bias based on statistics on batch, but no normalization factor (1-Lipschitz). + * - :class:`torch.nn.LayerNorm` + - no + - :class:`.LayerCentering` + - This layer apply a bias based on statistics on each sample, but no normalization factor (1-Lipschitz). + * - Residual connections + - no + - :class:`.LipResidual` + - Learn a factor for mixing residual and a 1-Lipschitz branch . + * - :class:`torch.nn.Dropout` - no - None - - We suspect that layer normalization already limits internal covariate shift. - + - The Lipschitz constant is bounded by the dropout factor. How to use it? -------------- diff --git a/docs/source/deel.torchlip.functional.rst b/docs/source/deel.torchlip.functional.rst index 287d76a..3c6af39 100644 --- a/docs/source/deel.torchlip.functional.rst +++ b/docs/source/deel.torchlip.functional.rst @@ -9,12 +9,6 @@ deel.torchlip.functional Non-linear activation functions ------------------------------- -:hidden:`invertible down/up sample` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autofunction:: invertible_downsample -.. autofunction:: invertible_upsample - :hidden:`max_min` ~~~~~~~~~~~~~~~~~ @@ -32,6 +26,7 @@ Non-linear activation functions .. autofunction:: lipschitz_prelu + Loss functions -------------- diff --git a/docs/source/deel.torchlip.rst b/docs/source/deel.torchlip.rst index d75bd4d..2447432 100644 --- a/docs/source/deel.torchlip.rst +++ b/docs/source/deel.torchlip.rst @@ -31,6 +31,7 @@ Convolution Layers .. autoclass:: SpectralConv2d .. autoclass:: FrobeniusConv2d +.. autoclass:: SpectralConvTranspose2d Pooling Layers -------------- @@ -38,12 +39,13 @@ Pooling Layers .. autoclass:: ScaledAdaptiveAvgPool2d .. autoclass:: ScaledAvgPool2d .. autoclass:: ScaledL2NormPool2d +.. autoclass:: ScaledAdaptativeL2NormPool2d +.. autoclass:: InvertibleDownSampling +.. autoclass:: InvertibleUpSampling Non-linear Activations ---------------------- -.. autoclass:: InvertibleDownSampling -.. autoclass:: InvertibleUpSampling .. autoclass:: MaxMin .. autoclass:: GroupSort .. autoclass:: GroupSort2 @@ -63,3 +65,8 @@ Loss Functions .. autoclass:: NegKRLoss .. autoclass:: HingeMarginLoss .. autoclass:: HKRLoss +.. autoclass:: HKRMulticlassLoss +.. autoclass:: SoftHKRMulticlassLoss +.. autoclass:: TauCrossEntropyLoss +.. autoclass:: TauBCEWithLogitsLoss +.. autoclass:: CategoricalHingeLoss diff --git a/docs/source/index.rst b/docs/source/index.rst index 94eba83..c5a5382 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,7 +16,7 @@ This library provides implementation of **k-Lispchitz layers for PyTorch**. Content of the library ---------------------- -* k-Lipschitz variant of PyTorch layers such as ``Linear``, ``Conv2d`` and ``AvgPool2d``, +* k-Lipschitz variant of PyTorch layers such as ``Linear``, ``Conv2d`` and ``AvgPool2d``, ... * activation functions compatible with ``pytorch``, * initializers for ``pytorch``, * loss functions to work with Wasserstein distance estimations. diff --git a/docs/source/wasserstein_classification_MNIST08.rst b/docs/source/wasserstein_classification_MNIST08.rst index af627f8..9c3f8b1 100644 --- a/docs/source/wasserstein_classification_MNIST08.rst +++ b/docs/source/wasserstein_classification_MNIST08.rst @@ -24,11 +24,11 @@ For this task we will select two classes: 0 and 8. Labels are changed to import torch from torchvision import datasets - + # First we select the two classes selected_classes = [0, 8] # must be two classes as we perform binary classification - - + + def prepare_data(dataset, class_a=0, class_b=8): """ This function converts the MNIST data to make it suitable for our binary @@ -42,25 +42,25 @@ For this task we will select two classes: 0 and 8. Labels are changed to ) # mask to select only items from class_a or class_b x = x[mask] y = y[mask] - + # convert from range int[0,255] to float32[-1,1] x = x.float() / 255 x = x.reshape((-1, 28, 28, 1)) # change label to binary classification {-1,1} - + y_ = torch.zeros_like(y).float() y_[y == class_a] = 1.0 y_[y == class_b] = -1.0 return torch.utils.data.TensorDataset(x, y_) - - + + train = datasets.MNIST("./data", train=True, download=True) test = datasets.MNIST("./data", train=False, download=True) - + # Prepare the data train = prepare_data(train, selected_classes[0], selected_classes[1]) test = prepare_data(test, selected_classes[0], selected_classes[1]) - + # Display infos about dataset print( f"Train set size: {len(train)} samples, classes proportions: " @@ -70,7 +70,7 @@ For this task we will select two classes: 0 and 8. Labels are changed to f"Test set size: {len(test)} samples, classes proportions: " f"{100 * (test.tensors[1] == 1).numpy().mean():.2f} %" ) - + @@ -91,9 +91,9 @@ convolutional layers. import torch from deel import torchlip - + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - + ninputs = 28 * 28 wass = torchlip.Sequential( torch.nn.Flatten(), @@ -105,29 +105,55 @@ convolutional layers. torchlip.FullSort(), torchlip.FrobeniusLinear(32, 1), ).to(device) - + wass -.. parsed-literal:: - - Sequential model contains a layer which is not a Lipschitz layer: Flatten(start_dim=1, end_dim=-1) - - .. parsed-literal:: Sequential( (0): Flatten(start_dim=1, end_dim=-1) - (1): SpectralLinear(in_features=784, out_features=128, bias=True) + (1): ParametrizedSpectralLinear( + in_features=784, out_features=128, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ) (2): FullSort() - (3): SpectralLinear(in_features=128, out_features=64, bias=True) + (3): ParametrizedSpectralLinear( + in_features=128, out_features=64, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ) (4): FullSort() - (5): SpectralLinear(in_features=64, out_features=32, bias=True) + (5): ParametrizedSpectralLinear( + in_features=64, out_features=32, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ) (6): FullSort() - (7): FrobeniusLinear(in_features=32, out_features=1, bias=True) + (7): ParametrizedFrobeniusLinear( + in_features=32, out_features=1, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _FrobeniusNorm() + ) + ) + ) ) @@ -137,42 +163,45 @@ convolutional layers. .. code:: ipython3 - from deel.torchlip.functional import kr_loss, hkr_loss, hinge_margin_loss - + from deel.torchlip import KRLoss, HKRLoss, HingeMarginLoss + # training parameters epochs = 10 batch_size = 128 - + # loss parameters min_margin = 1 - alpha = 10 - + alpha = 0.98 + + kr_loss = KRLoss() + hkr_loss = HKRLoss(alpha=alpha, min_margin=min_margin) + hinge_margin_loss =HingeMarginLoss(min_margin=min_margin) optimizer = torch.optim.Adam(lr=0.001, params=wass.parameters()) - + train_loader = torch.utils.data.DataLoader(train, batch_size=batch_size, shuffle=True) test_loader = torch.utils.data.DataLoader(test, batch_size=32, shuffle=False) - + for epoch in range(epochs): - + m_kr, m_hm, m_acc = 0, 0, 0 wass.train() - + for step, (data, target) in enumerate(train_loader): - + data, target = data.to(device), target.to(device) optimizer.zero_grad() output = wass(data) - loss = hkr_loss(output, target, alpha=alpha, min_margin=min_margin) + loss = hkr_loss(output, target) loss.backward() optimizer.step() - + # Compute metrics on batch - m_kr += kr_loss(output, target, (1, -1)) - m_hm += hinge_margin_loss(output, target, min_margin) + m_kr += kr_loss(output, target) + m_hm += hinge_margin_loss(output, target) m_acc += (torch.sign(output).flatten() == torch.sign(target)).sum() / len( target ) - + # Train metrics for the current epoch metrics = [ f"{k}: {v:.04f}" @@ -182,7 +211,7 @@ convolutional layers. "acc": m_acc / (step + 1), }.items() ] - + # Compute test loss for the current epoch wass.eval() testo = [] @@ -190,21 +219,21 @@ convolutional layers. data, target = data.to(device), target.to(device) testo.append(wass(data).detach().cpu()) testo = torch.cat(testo).flatten() - + # Validation metrics for the current epoch metrics += [ f"val_{k}: {v:.04f}" for k, v in { "loss": hkr_loss( - testo, test.tensors[1], alpha=alpha, min_margin=min_margin + testo, test.tensors[1] ), - "KR": kr_loss(testo.flatten(), test.tensors[1], (1, -1)), + "KR": kr_loss(testo.flatten(), test.tensors[1]), "acc": (torch.sign(testo).flatten() == torch.sign(test.tensors[1])) .float() .mean(), }.items() ] - + print(f"Epoch {epoch + 1}/{epochs}") print(" - ".join(metrics)) @@ -213,25 +242,61 @@ convolutional layers. .. parsed-literal:: Epoch 1/10 - loss: -2.5269 - KR: 1.6177 - acc: 0.8516 - val_loss: -2.7241 - val_KR: 3.0157 - val_acc: 0.9939 + loss: -0.0302 - KR: 1.5302 - acc: 0.9242 - val_loss: -0.0375 - val_KR: 2.4426 - val_acc: 0.9923 + + +.. parsed-literal:: + Epoch 2/10 - loss: -3.6040 - KR: 3.8627 - acc: 0.9918 - val_loss: -4.5285 - val_KR: 4.7897 - val_acc: 0.9918 + loss: -0.0479 - KR: 2.8884 - acc: 0.9900 - val_loss: -0.0575 - val_KR: 3.4451 - val_acc: 0.9923 + + +.. parsed-literal:: + Epoch 3/10 - loss: -5.7646 - KR: 5.4015 - acc: 0.9922 - val_loss: -5.7246 - val_KR: 6.0067 - val_acc: 0.9898 + loss: -0.0459 - KR: 3.7795 - acc: 0.9895 - val_loss: -0.0713 - val_KR: 4.1205 - val_acc: 0.9923 + + +.. parsed-literal:: + Epoch 4/10 - loss: -6.6268 - KR: 6.2105 - acc: 0.9921 - val_loss: -6.2183 - val_KR: 6.4874 - val_acc: 0.9893 + loss: -0.0534 - KR: 4.4300 - acc: 0.9898 - val_loss: -0.0829 - val_KR: 4.6154 - val_acc: 0.9923 + + +.. parsed-literal:: + Epoch 5/10 - loss: -6.4072 - KR: 6.5715 - acc: 0.9931 - val_loss: -6.4530 - val_KR: 6.7446 - val_acc: 0.9887 + loss: -0.0940 - KR: 4.9912 - acc: 0.9917 - val_loss: -0.0908 - val_KR: 5.2786 - val_acc: 0.9893 + + +.. parsed-literal:: + Epoch 6/10 - loss: -6.7689 - KR: 6.7803 - acc: 0.9926 - val_loss: -6.6342 - val_KR: 6.8849 - val_acc: 0.9898 + loss: -0.1041 - KR: 5.4511 - acc: 0.9940 - val_loss: -0.1060 - val_KR: 5.7054 - val_acc: 0.9928 + + +.. parsed-literal:: + Epoch 7/10 - loss: -6.2389 - KR: 6.8948 - acc: 0.9932 - val_loss: -6.7603 - val_KR: 6.9643 - val_acc: 0.9933 + loss: -0.1136 - KR: 5.8117 - acc: 0.9947 - val_loss: -0.1105 - val_KR: 5.9891 - val_acc: 0.9918 + + +.. parsed-literal:: + Epoch 8/10 - loss: -6.9207 - KR: 6.9642 - acc: 0.9933 - val_loss: -6.8199 - val_KR: 7.0147 - val_acc: 0.9918 + loss: -0.1200 - KR: 6.0296 - acc: 0.9954 - val_loss: -0.1156 - val_KR: 6.1311 - val_acc: 0.9944 + + +.. parsed-literal:: + Epoch 9/10 - loss: -6.9446 - KR: 7.0211 - acc: 0.9936 - val_loss: -6.8038 - val_KR: 7.0666 - val_acc: 0.9887 + loss: -0.1236 - KR: 6.1587 - acc: 0.9953 - val_loss: -0.1139 - val_KR: 6.2823 - val_acc: 0.9918 + + +.. parsed-literal:: + Epoch 10/10 - loss: -6.5403 - KR: 7.0694 - acc: 0.9942 - val_loss: -6.9136 - val_KR: 7.1086 - val_acc: 0.9933 + loss: -0.1198 - KR: 6.3513 - acc: 0.9964 - val_loss: -0.1207 - val_KR: 6.3622 - val_acc: 0.9944 4. Evaluate the Lipschitz constant of our networks @@ -245,7 +310,7 @@ We can estimate the Lipschitz constant by evaluating .. math:: - \frac{\Vert{}F(x_2) - F(x_1)\Vert{}}{\Vert{}x_2 - x_1\Vert{}} \quad\text{or}\quad + \frac{\Vert{}F(x_2) - F(x_1)\Vert{}}{\Vert{}x_2 - x_1\Vert{}} \quad\text{or}\quad \frac{\Vert{}F(x + \epsilon) - F(x)\Vert{}}{\Vert{}\epsilon\Vert{}} for various inputs. @@ -253,9 +318,9 @@ for various inputs. .. code:: ipython3 from scipy.spatial.distance import pdist - + wass.eval() - + p = [] for _ in range(64): eps = 1e-3 @@ -263,7 +328,7 @@ for various inputs. dist = torch.distributions.Uniform(-eps, +eps).sample(batch.shape) y1 = wass(batch.to(device)).detach().cpu() y2 = wass((batch + dist).to(device)).detach().cpu() - + p.append( torch.max( torch.norm(y2 - y1, dim=1) @@ -275,7 +340,7 @@ for various inputs. .. parsed-literal:: - tensor(0.1349) + tensor(0.1312) .. code:: ipython3 @@ -286,14 +351,14 @@ for various inputs. y = wass(batch.to(device)).detach().cpu().numpy() xd = pdist(x.reshape(batch.shape[0], -1)) yd = pdist(y.reshape(batch.shape[0], -1)) - + p.append((yd / xd).max()) print(torch.tensor(p).max()) .. parsed-literal:: - tensor(0.9038, dtype=torch.float64) + tensor(0.8606, dtype=torch.float64) As we can see, using the :math:`\epsilon`-version, we greatly @@ -323,16 +388,80 @@ are 1. .. parsed-literal:: === Before export === - SpectralLinear(in_features=784, out_features=128, bias=True), min=0.9999998807907104, max=1.0 - SpectralLinear(in_features=128, out_features=64, bias=True), min=0.9999998807907104, max=1.0000001192092896 - SpectralLinear(in_features=64, out_features=32, bias=True), min=0.9999998807907104, max=1.0 - FrobeniusLinear(in_features=32, out_features=1, bias=True), min=0.9999999403953552, max=0.9999999403953552 + ParametrizedSpectralLinear( + in_features=784, out_features=128, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ), min=0.9999998807907104, max=1.0 + ParametrizedSpectralLinear( + in_features=128, out_features=64, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ), min=0.9999998211860657, max=1.000000238418579 + ParametrizedSpectralLinear( + in_features=64, out_features=32, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ), min=0.9999998807907104, max=1.0 + ParametrizedFrobeniusLinear( + in_features=32, out_features=1, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _FrobeniusNorm() + ) + ) + ), min=0.9999999403953552, max=0.9999999403953552 + + +4.2 Model export +~~~~~~~~~~~~~~~~ + +Once training is finished, the model can be optimized for inference by +using the ``vanilla_export()`` method. The ``torchlip`` layers are +converted to their PyTorch counterparts, e.g. ``SpectralConv2d`` +layers will be converted into ``torch.nn.Conv2d`` layers. +Warnings: +^^^^^^^^^ + +vanilla_export method modifies the model in-place. + +In order to build and export a new model while keeping the reference +one, it is required to follow these steps: + +# Build e new mode for instance with torchlip.Sequential( +torchlip.SpectralConv2d(…), …) + +``wexport = ()`` + +# Copy the parameters from the reference t the new model + +``wexport.load_state_dict(wass.state_dict())`` + +# one forward required to initialize pamatrizations + +``vanilla_model(one_input)`` + +# vanilla_export the new model + +``wexport = wexport.vanilla_export()`` .. code:: ipython3 wexport = wass.vanilla_export() - + print("=== After export ===") layers = list(wexport.children()) for layer in layers: @@ -346,7 +475,7 @@ are 1. === After export === Linear(in_features=784, out_features=128, bias=True), min=0.9999998807907104, max=1.0 - Linear(in_features=128, out_features=64, bias=True), min=0.9999998807907104, max=1.0000001192092896 + Linear(in_features=128, out_features=64, bias=True), min=0.9999998211860657, max=1.000000238418579 Linear(in_features=64, out_features=32, bias=True), min=0.9999998807907104, max=1.0 Linear(in_features=32, out_features=1, bias=True), min=0.9999999403953552, max=0.9999999403953552 diff --git a/docs/source/wasserstein_classification_fashionMNIST.rst b/docs/source/wasserstein_classification_fashionMNIST.rst index 94e6d2a..ee2510a 100644 --- a/docs/source/wasserstein_classification_fashionMNIST.rst +++ b/docs/source/wasserstein_classification_fashionMNIST.rst @@ -29,22 +29,22 @@ keep things simple, no data augmentation is performed. import torch from torchvision import datasets, transforms - + train_set = datasets.FashionMNIST( root="./data", download=True, train=True, transform=transforms.ToTensor(), ) - + test_set = datasets.FashionMNIST( root="./data", download=True, train=False, transform=transforms.ToTensor(), ) - - batch_size = 4096 + + batch_size = 100 train_loader = torch.utils.data.DataLoader(train_set, batch_size, shuffle=True) test_loader = torch.utils.data.DataLoader(test_set, batch_size) @@ -55,9 +55,9 @@ keep things simple, no data augmentation is performed. The original one-vs-all setup would require 10 different networks (1 per class). However, we use in practice a network with a common body and a Lipschitz head (linear layer) containing 10 output neurons, like any -standard network for multiclass classification. Note that each head -neuron is not a 1-Lipschitz function; however the overall head with the -10 outputs is 1-Lipschitz. +standard network for multiclass classification. Note that we use +torchlip.FrobeniusLinear disjoint_neurons=True to enforce each head +neuron to be a 1-Lipschitz function; Notes about constraint enforcement ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -80,7 +80,7 @@ implemented in ``torchlip``. .. code:: ipython3 from deel import torchlip - + # Sequential has the same properties as any Lipschitz layer. It only acts as a # container, with features specific to Lipschitz functions (condensation, # vanilla_exportation, ...) @@ -104,37 +104,65 @@ implemented in ``torchlip``. torch.nn.Flatten(), torchlip.SpectralLinear(1568, 64), torchlip.GroupSort2(), - torchlip.SpectralLinear(64, 10, bias=False), + torchlip.FrobeniusLinear(64, 10, bias=True, disjoint_neurons=True), # Similarly, model has a parameter to set the Lipschitz constant that automatically # sets the constant of each layer. k_coef_lip=1.0, ) - + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) -.. parsed-literal:: - - Sequential model contains a layer which is not a Lipschitz layer: Flatten(start_dim=1, end_dim=-1) - - .. parsed-literal:: Sequential( - (0): SpectralConv2d(1, 16, kernel_size=(3, 3), stride=(1, 1), padding=same) + (0): ParametrizedSpectralConv2d( + 1, 16, kernel_size=(3, 3), stride=(1, 1), padding=same + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + (2): _LConvNorm() + ) + ) + ) (1): GroupSort2() (2): ScaledL2NormPool2d(kernel_size=(2, 2), stride=(2, 2), padding=0) - (3): SpectralConv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=same) + (3): ParametrizedSpectralConv2d( + 16, 32, kernel_size=(3, 3), stride=(1, 1), padding=same + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + (2): _LConvNorm() + ) + ) + ) (4): GroupSort2() (5): ScaledL2NormPool2d(kernel_size=(2, 2), stride=(2, 2), padding=0) (6): Flatten(start_dim=1, end_dim=-1) - (7): SpectralLinear(in_features=1568, out_features=64, bias=True) + (7): ParametrizedSpectralLinear( + in_features=1568, out_features=64, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ) (8): GroupSort2() - (9): SpectralLinear(in_features=64, out_features=10, bias=False) + (9): ParametrizedFrobeniusLinear( + in_features=64, out_features=10, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _FrobeniusNorm() + ) + ) + ) ) @@ -142,46 +170,59 @@ implemented in ``torchlip``. 3. HKR loss and training ------------------------ -The multiclass HKR loss can be found in the ``hkr_multiclass_loss`` -function or in the ``HKRMulticlassLoss`` class. The loss has two -parameters: ``alpha`` and ``min_margin``. Decreasing ``alpha`` and -increasing ``min_margin`` improve robustness (at the cost of accuracy). -Note also in the case of Lipschitz networks, more robustness requires -more parameters. For more information, see `our +The multiclass HKR loss can be found in the\ ``HKRMulticlassLoss`` +class. The loss has two parameters: ``alpha`` and ``min_margin``. +Decreasing ``alpha`` and increasing ``min_margin`` improve robustness +(at the cost of accuracy). Note also in the case of Lipschitz networks, +more robustness requires more parameters. For more information, see `our paper `__. -In this setup, choosing ``alpha=100`` and ``min_margin=.25`` provides -good robustness without hurting the accuracy too much. - -Finally the ``kr_multiclass_loss`` gives an indication on the robustness -of the network (proxy of the average certificate). +In this setup, choosing ``alpha=0.99`` and ``min_margin=.25`` provides +good robustness without hurting the accuracy too much. An accurate +network can be obtained using ``alpha=0.999`` and ``min_margin=.1`` We +also propose the ``SoftHKRMulticlassLoss`` proposed in `this +paper `__ that can achieve equivalent +performance to unconstrianed networks (92% validation accuracy with +``alpha=0.995``, ``min_margin=0.10``, ``temperature=50.0``). Finally the +``KRMulticlassLoss`` gives an indication on the robustness of the +network (proxy of the average certificate). .. code:: ipython3 - epochs = 100 - optimizer = torch.optim.Adam(lr=1e-4, params=model.parameters()) - hkr_loss = torchlip.HKRMulticlassLoss(alpha=100, min_margin=0.25) - + loss_choice = "HKRMulticlassLoss" # "HKRMulticlassLoss" or "SoftHKRMulticlassLoss" + epochs = 50 + + optimizer = torch.optim.Adam(lr=1e-3, params=model.parameters()) + hkr_loss = None + if loss_choice == "HKRMulticlassLoss": + hkr_loss = torchlip.HKRMulticlassLoss(alpha=0.99, min_margin=0.25) #Robust + #hkr_loss = torchlip.HKRMulticlassLoss(alpha=0.999, min_margin=0.10) #Accurate + if loss_choice == "SoftHKRMulticlassLoss": + hkr_loss = torchlip.SoftHKRMulticlassLoss(alpha=0.995, min_margin=0.10, temperature=50.0) + assert hkr_loss is not None, "Please choose a valid loss function" + + kr_multiclass_loss = torchlip.KRMulticlassLoss() + for epoch in range(epochs): m_kr, m_acc = 0, 0 - + for step, (data, target) in enumerate(train_loader): - + # For multiclass HKR loss, the targets must be one-hot encoded target = torch.nn.functional.one_hot(target, num_classes=10) data, target = data.to(device), target.to(device) - + # Forward + backward pass optimizer.zero_grad() output = model(data) loss = hkr_loss(output, target) loss.backward() optimizer.step() - + # Compute metrics on batch - m_kr += torchlip.functional.kr_multiclass_loss(output, target) + m_kr += kr_multiclass_loss(output, target) m_acc += (output.argmax(dim=1) == target.argmax(dim=1)).sum() / len(target) - + # Train metrics for the current epoch metrics = [ f"{k}: {v:.04f}" @@ -191,7 +232,7 @@ of the network (proxy of the average certificate). "KR": m_kr / (step + 1), }.items() ] - + # Compute validation loss for the current epoch test_output, test_targets = [], [] for data, target in test_loader: @@ -202,11 +243,11 @@ of the network (proxy of the average certificate). ) test_output = torch.cat(test_output) test_targets = torch.cat(test_targets) - + val_loss = hkr_loss(test_output, test_targets) - val_kr = torchlip.functional.kr_multiclass_loss(test_output, test_targets) + val_kr = kr_multiclass_loss(test_output, test_targets) val_acc = (test_output.argmax(dim=1) == test_targets.argmax(dim=1)).float().mean() - + # Validation metrics for the current epoch metrics += [ f"val_{k}: {v:.04f}" @@ -215,10 +256,10 @@ of the network (proxy of the average certificate). "acc": (test_output.argmax(dim=1) == test_targets.argmax(dim=1)) .float() .mean(), - "KR": torchlip.functional.kr_multiclass_loss(test_output, test_targets), + "KR": kr_multiclass_loss(test_output, test_targets), }.items() ] - + print(f"Epoch {epoch + 1}/{epochs}") print(" - ".join(metrics)) @@ -226,206 +267,302 @@ of the network (proxy of the average certificate). .. parsed-literal:: - Epoch 1/100 - loss: 29.8065 - acc: 0.2169 - KR: 0.1004 - val_loss: 28.8107 - val_acc: 0.4582 - val_KR: 0.1890 - Epoch 2/100 - loss: 19.8997 - acc: 0.5137 - KR: 0.2591 - val_loss: 19.6618 - val_acc: 0.5694 - val_KR: 0.3345 - Epoch 3/100 - loss: 15.5582 - acc: 0.6162 - KR: 0.3930 - val_loss: 15.7906 - val_acc: 0.6218 - val_KR: 0.4501 - Epoch 4/100 - loss: 13.6293 - acc: 0.6692 - KR: 0.4945 - val_loss: 13.8149 - val_acc: 0.6832 - val_KR: 0.5319 - Epoch 5/100 - loss: 12.3328 - acc: 0.7009 - KR: 0.5630 - val_loss: 12.3709 - val_acc: 0.7038 - val_KR: 0.5904 - Epoch 6/100 - loss: 11.2218 - acc: 0.7248 - KR: 0.6149 - val_loss: 11.3854 - val_acc: 0.7161 - val_KR: 0.6349 - Epoch 7/100 - loss: 10.5164 - acc: 0.7351 - KR: 0.6575 - val_loss: 10.7304 - val_acc: 0.7312 - val_KR: 0.6749 - Epoch 8/100 - loss: 9.9036 - acc: 0.7458 - KR: 0.6955 - val_loss: 10.2040 - val_acc: 0.7389 - val_KR: 0.7098 - Epoch 9/100 - loss: 9.4456 - acc: 0.7515 - KR: 0.7283 - val_loss: 9.7864 - val_acc: 0.7461 - val_KR: 0.7404 - Epoch 10/100 - loss: 9.4395 - acc: 0.7565 - KR: 0.7562 - val_loss: 9.4458 - val_acc: 0.7488 - val_KR: 0.7644 - Epoch 11/100 - loss: 8.6899 - acc: 0.7621 - KR: 0.7809 - val_loss: 9.1339 - val_acc: 0.7584 - val_KR: 0.7878 - Epoch 12/100 - loss: 8.8400 - acc: 0.7660 - KR: 0.8033 - val_loss: 8.8585 - val_acc: 0.7603 - val_KR: 0.8114 - Epoch 13/100 - loss: 8.4524 - acc: 0.7698 - KR: 0.8280 - val_loss: 8.6265 - val_acc: 0.7615 - val_KR: 0.8348 - Epoch 14/100 - loss: 8.2200 - acc: 0.7728 - KR: 0.8497 - val_loss: 8.4014 - val_acc: 0.7684 - val_KR: 0.8576 - Epoch 15/100 - loss: 7.5585 - acc: 0.7771 - KR: 0.8733 - val_loss: 8.1770 - val_acc: 0.7731 - val_KR: 0.8779 - Epoch 16/100 - loss: 7.7402 - acc: 0.7789 - KR: 0.8954 - val_loss: 7.9923 - val_acc: 0.7737 - val_KR: 0.9000 - Epoch 17/100 - loss: 7.8116 - acc: 0.7828 - KR: 0.9146 - val_loss: 7.8163 - val_acc: 0.7774 - val_KR: 0.9193 - Epoch 18/100 - loss: 7.3096 - acc: 0.7854 - KR: 0.9364 - val_loss: 7.6657 - val_acc: 0.7784 - val_KR: 0.9392 - Epoch 19/100 - loss: 7.1890 - acc: 0.7892 - KR: 0.9548 - val_loss: 7.5001 - val_acc: 0.7822 - val_KR: 0.9597 - Epoch 20/100 - loss: 7.1856 - acc: 0.7899 - KR: 0.9761 - val_loss: 7.3783 - val_acc: 0.7815 - val_KR: 0.9803 - Epoch 21/100 - loss: 6.8862 - acc: 0.7927 - KR: 0.9959 - val_loss: 7.2480 - val_acc: 0.7829 - val_KR: 1.0005 - Epoch 22/100 - loss: 6.7167 - acc: 0.7966 - KR: 1.0154 - val_loss: 7.1030 - val_acc: 0.7862 - val_KR: 1.0169 - Epoch 23/100 - loss: 6.6035 - acc: 0.7978 - KR: 1.0321 - val_loss: 6.9949 - val_acc: 0.7894 - val_KR: 1.0359 - Epoch 24/100 - loss: 6.5261 - acc: 0.8007 - KR: 1.0522 - val_loss: 6.8867 - val_acc: 0.7925 - val_KR: 1.0526 - Epoch 25/100 - loss: 6.3522 - acc: 0.8023 - KR: 1.0674 - val_loss: 6.7934 - val_acc: 0.7946 - val_KR: 1.0706 - Epoch 26/100 - loss: 6.3714 - acc: 0.8036 - KR: 1.0867 - val_loss: 6.7136 - val_acc: 0.7960 - val_KR: 1.0874 - Epoch 27/100 - loss: 6.2562 - acc: 0.8060 - KR: 1.1034 - val_loss: 6.6595 - val_acc: 0.7958 - val_KR: 1.1038 - Epoch 28/100 - loss: 6.1618 - acc: 0.8081 - KR: 1.1197 - val_loss: 6.5398 - val_acc: 0.7991 - val_KR: 1.1196 - Epoch 29/100 - loss: 6.0123 - acc: 0.8094 - KR: 1.1373 - val_loss: 6.4722 - val_acc: 0.7979 - val_KR: 1.1350 - Epoch 30/100 - loss: 6.1670 - acc: 0.8111 - KR: 1.1519 - val_loss: 6.3815 - val_acc: 0.8038 - val_KR: 1.1519 - Epoch 31/100 - loss: 5.8678 - acc: 0.8132 - KR: 1.1682 - val_loss: 6.2972 - val_acc: 0.8038 - val_KR: 1.1675 - Epoch 32/100 - loss: 5.8205 - acc: 0.8150 - KR: 1.1839 - val_loss: 6.2579 - val_acc: 0.8025 - val_KR: 1.1849 - Epoch 33/100 - loss: 5.8555 - acc: 0.8149 - KR: 1.2006 - val_loss: 6.1964 - val_acc: 0.8069 - val_KR: 1.2005 - Epoch 34/100 - loss: 5.8581 - acc: 0.8176 - KR: 1.2147 - val_loss: 6.1072 - val_acc: 0.8088 - val_KR: 1.2144 - Epoch 35/100 - loss: 5.7316 - acc: 0.8187 - KR: 1.2302 - val_loss: 6.0802 - val_acc: 0.8062 - val_KR: 1.2290 - Epoch 36/100 - loss: 5.9217 - acc: 0.8187 - KR: 1.2449 - val_loss: 5.9837 - val_acc: 0.8122 - val_KR: 1.2463 - Epoch 37/100 - loss: 5.4302 - acc: 0.8219 - KR: 1.2589 - val_loss: 5.9178 - val_acc: 0.8151 - val_KR: 1.2556 - Epoch 38/100 - loss: 5.5795 - acc: 0.8219 - KR: 1.2732 - val_loss: 5.8836 - val_acc: 0.8157 - val_KR: 1.2725 - Epoch 39/100 - loss: 5.5917 - acc: 0.8238 - KR: 1.2878 - val_loss: 5.8426 - val_acc: 0.8138 - val_KR: 1.2899 - Epoch 40/100 - loss: 5.2440 - acc: 0.8242 - KR: 1.3040 - val_loss: 5.7798 - val_acc: 0.8190 - val_KR: 1.2982 - Epoch 41/100 - loss: 5.4507 - acc: 0.8244 - KR: 1.3157 - val_loss: 5.7328 - val_acc: 0.8176 - val_KR: 1.3134 - Epoch 42/100 - loss: 5.2139 - acc: 0.8272 - KR: 1.3277 - val_loss: 5.7118 - val_acc: 0.8166 - val_KR: 1.3298 - Epoch 43/100 - loss: 5.4277 - acc: 0.8277 - KR: 1.3446 - val_loss: 5.6266 - val_acc: 0.8203 - val_KR: 1.3391 - Epoch 44/100 - loss: 5.3023 - acc: 0.8291 - KR: 1.3555 - val_loss: 5.5880 - val_acc: 0.8214 - val_KR: 1.3558 - Epoch 45/100 - loss: 5.3210 - acc: 0.8296 - KR: 1.3705 - val_loss: 5.5427 - val_acc: 0.8206 - val_KR: 1.3683 - Epoch 46/100 - loss: 5.1909 - acc: 0.8298 - KR: 1.3833 - val_loss: 5.4947 - val_acc: 0.8214 - val_KR: 1.3806 - Epoch 47/100 - loss: 4.7530 - acc: 0.8308 - KR: 1.3961 - val_loss: 5.4601 - val_acc: 0.8256 - val_KR: 1.3949 - Epoch 48/100 - loss: 5.3041 - acc: 0.8325 - KR: 1.4094 - val_loss: 5.4323 - val_acc: 0.8238 - val_KR: 1.4044 - Epoch 49/100 - loss: 4.8817 - acc: 0.8327 - KR: 1.4206 - val_loss: 5.3684 - val_acc: 0.8263 - val_KR: 1.4190 - Epoch 50/100 - loss: 5.2699 - acc: 0.8324 - KR: 1.4354 - val_loss: 5.3517 - val_acc: 0.8294 - val_KR: 1.4300 - Epoch 51/100 - loss: 4.8224 - acc: 0.8347 - KR: 1.4470 - val_loss: 5.3209 - val_acc: 0.8250 - val_KR: 1.4453 - Epoch 52/100 - loss: 4.7981 - acc: 0.8358 - KR: 1.4586 - val_loss: 5.2608 - val_acc: 0.8266 - val_KR: 1.4562 - Epoch 53/100 - loss: 4.7855 - acc: 0.8353 - KR: 1.4731 - val_loss: 5.2477 - val_acc: 0.8254 - val_KR: 1.4662 - Epoch 54/100 - loss: 5.4214 - acc: 0.8368 - KR: 1.4807 - val_loss: 5.1947 - val_acc: 0.8286 - val_KR: 1.4792 - Epoch 55/100 - loss: 4.4762 - acc: 0.8385 - KR: 1.4953 - val_loss: 5.1617 - val_acc: 0.8304 - val_KR: 1.4877 - Epoch 56/100 - loss: 5.0611 - acc: 0.8384 - KR: 1.5048 - val_loss: 5.1164 - val_acc: 0.8301 - val_KR: 1.5023 - Epoch 57/100 - loss: 4.7158 - acc: 0.8379 - KR: 1.5154 - val_loss: 5.1140 - val_acc: 0.8283 - val_KR: 1.5128 - Epoch 58/100 - loss: 4.7872 - acc: 0.8389 - KR: 1.5301 - val_loss: 5.0908 - val_acc: 0.8317 - val_KR: 1.5246 - Epoch 59/100 - loss: 4.7114 - acc: 0.8403 - KR: 1.5377 - val_loss: 5.0289 - val_acc: 0.8358 - val_KR: 1.5359 - Epoch 60/100 - loss: 4.8055 - acc: 0.8409 - KR: 1.5506 - val_loss: 5.0150 - val_acc: 0.8308 - val_KR: 1.5439 - Epoch 61/100 - loss: 4.5613 - acc: 0.8413 - KR: 1.5563 - val_loss: 4.9887 - val_acc: 0.8373 - val_KR: 1.5536 - Epoch 62/100 - loss: 4.3678 - acc: 0.8413 - KR: 1.5695 - val_loss: 4.9495 - val_acc: 0.8366 - val_KR: 1.5621 - Epoch 63/100 - loss: 4.8015 - acc: 0.8436 - KR: 1.5788 - val_loss: 4.9201 - val_acc: 0.8368 - val_KR: 1.5737 - Epoch 64/100 - loss: 4.6411 - acc: 0.8445 - KR: 1.5881 - val_loss: 4.8899 - val_acc: 0.8352 - val_KR: 1.5844 - Epoch 65/100 - loss: 4.4301 - acc: 0.8446 - KR: 1.5971 - val_loss: 4.8566 - val_acc: 0.8344 - val_KR: 1.5953 - Epoch 66/100 - loss: 4.5307 - acc: 0.8449 - KR: 1.6088 - val_loss: 4.8410 - val_acc: 0.8358 - val_KR: 1.6009 - Epoch 67/100 - loss: 5.0502 - acc: 0.8443 - KR: 1.6166 - val_loss: 4.8211 - val_acc: 0.8378 - val_KR: 1.6097 - Epoch 68/100 - loss: 4.3426 - acc: 0.8459 - KR: 1.6251 - val_loss: 4.7964 - val_acc: 0.8401 - val_KR: 1.6198 - Epoch 69/100 - loss: 4.2726 - acc: 0.8468 - KR: 1.6320 - val_loss: 4.7703 - val_acc: 0.8373 - val_KR: 1.6263 - Epoch 70/100 - loss: 4.5685 - acc: 0.8464 - KR: 1.6417 - val_loss: 4.7610 - val_acc: 0.8339 - val_KR: 1.6356 - Epoch 71/100 - loss: 4.3319 - acc: 0.8467 - KR: 1.6507 - val_loss: 4.7237 - val_acc: 0.8395 - val_KR: 1.6403 - Epoch 72/100 - loss: 4.8462 - acc: 0.8471 - KR: 1.6573 - val_loss: 4.7196 - val_acc: 0.8406 - val_KR: 1.6531 - Epoch 73/100 - loss: 4.4542 - acc: 0.8485 - KR: 1.6657 - val_loss: 4.6709 - val_acc: 0.8391 - val_KR: 1.6599 - Epoch 74/100 - loss: 4.1947 - acc: 0.8483 - KR: 1.6750 - val_loss: 4.6740 - val_acc: 0.8391 - val_KR: 1.6628 - Epoch 75/100 - loss: 4.1425 - acc: 0.8494 - KR: 1.6824 - val_loss: 4.6660 - val_acc: 0.8394 - val_KR: 1.6738 - Epoch 76/100 - loss: 4.8530 - acc: 0.8501 - KR: 1.6894 - val_loss: 4.6159 - val_acc: 0.8396 - val_KR: 1.6850 - Epoch 77/100 - loss: 4.4014 - acc: 0.8496 - KR: 1.6972 - val_loss: 4.5799 - val_acc: 0.8404 - val_KR: 1.6898 - Epoch 78/100 - loss: 4.1155 - acc: 0.8490 - KR: 1.7033 - val_loss: 4.5703 - val_acc: 0.8428 - val_KR: 1.6942 - Epoch 79/100 - loss: 3.9704 - acc: 0.8494 - KR: 1.7123 - val_loss: 4.5954 - val_acc: 0.8427 - val_KR: 1.6996 - Epoch 80/100 - loss: 4.4123 - acc: 0.8509 - KR: 1.7168 - val_loss: 4.5463 - val_acc: 0.8435 - val_KR: 1.7092 - Epoch 81/100 - loss: 3.9522 - acc: 0.8505 - KR: 1.7240 - val_loss: 4.5268 - val_acc: 0.8438 - val_KR: 1.7153 - Epoch 82/100 - loss: 4.0600 - acc: 0.8513 - KR: 1.7326 - val_loss: 4.4986 - val_acc: 0.8445 - val_KR: 1.7214 - Epoch 83/100 - loss: 4.0133 - acc: 0.8522 - KR: 1.7343 - val_loss: 4.4688 - val_acc: 0.8435 - val_KR: 1.7248 - Epoch 84/100 - loss: 4.1254 - acc: 0.8529 - KR: 1.7452 - val_loss: 4.4479 - val_acc: 0.8444 - val_KR: 1.7376 - Epoch 85/100 - loss: 3.7917 - acc: 0.8542 - KR: 1.7499 - val_loss: 4.4521 - val_acc: 0.8440 - val_KR: 1.7433 - Epoch 86/100 - loss: 4.2524 - acc: 0.8534 - KR: 1.7584 - val_loss: 4.4099 - val_acc: 0.8434 - val_KR: 1.7509 - Epoch 87/100 - loss: 4.1529 - acc: 0.8541 - KR: 1.7622 - val_loss: 4.4031 - val_acc: 0.8439 - val_KR: 1.7507 - Epoch 88/100 - loss: 3.8418 - acc: 0.8545 - KR: 1.7675 - val_loss: 4.3966 - val_acc: 0.8436 - val_KR: 1.7644 - Epoch 89/100 - loss: 4.3602 - acc: 0.8543 - KR: 1.7753 - val_loss: 4.3608 - val_acc: 0.8429 - val_KR: 1.7700 - Epoch 90/100 - loss: 3.6240 - acc: 0.8537 - KR: 1.7835 - val_loss: 4.3561 - val_acc: 0.8455 - val_KR: 1.7732 - Epoch 91/100 - loss: 4.0434 - acc: 0.8542 - KR: 1.7886 - val_loss: 4.3595 - val_acc: 0.8481 - val_KR: 1.7735 - Epoch 92/100 - loss: 4.0609 - acc: 0.8565 - KR: 1.7890 - val_loss: 4.3036 - val_acc: 0.8479 - val_KR: 1.7824 - Epoch 93/100 - loss: 4.3047 - acc: 0.8554 - KR: 1.7950 - val_loss: 4.2832 - val_acc: 0.8496 - val_KR: 1.7867 - Epoch 94/100 - loss: 3.9837 - acc: 0.8569 - KR: 1.8023 - val_loss: 4.2719 - val_acc: 0.8475 - val_KR: 1.7916 - Epoch 95/100 - loss: 4.1019 - acc: 0.8563 - KR: 1.8050 - val_loss: 4.3060 - val_acc: 0.8465 - val_KR: 1.7944 - Epoch 96/100 - loss: 3.8759 - acc: 0.8571 - KR: 1.8111 - val_loss: 4.2724 - val_acc: 0.8479 - val_KR: 1.8052 - Epoch 97/100 - loss: 3.8682 - acc: 0.8564 - KR: 1.8185 - val_loss: 4.2375 - val_acc: 0.8492 - val_KR: 1.8049 - Epoch 98/100 - loss: 3.9488 - acc: 0.8580 - KR: 1.8201 - val_loss: 4.2446 - val_acc: 0.8471 - val_KR: 1.8083 - Epoch 99/100 - loss: 3.8166 - acc: 0.8579 - KR: 1.8258 - val_loss: 4.2073 - val_acc: 0.8481 - val_KR: 1.8168 - Epoch 100/100 - loss: 3.6867 - acc: 0.8586 - KR: 1.8287 - val_loss: 4.1908 - val_acc: 0.8482 - val_KR: 1.8212 + Epoch 1/50 + loss: 0.0193 - acc: 0.7896 - KR: 0.8442 - val_loss: 0.0213 - val_acc: 0.8244 - val_KR: 1.2169 + + +.. parsed-literal:: + + Epoch 2/50 + loss: 0.0124 - acc: 0.8482 - KR: 1.4474 - val_loss: 0.0186 - val_acc: 0.8342 - val_KR: 1.6805 + + +.. parsed-literal:: + + Epoch 3/50 + loss: 0.0109 - acc: 0.8542 - KR: 1.8511 - val_loss: 0.0118 - val_acc: 0.8538 - val_KR: 2.0030 + + +.. parsed-literal:: + + Epoch 4/50 + loss: 0.0060 - acc: 0.8587 - KR: 2.1384 - val_loss: 0.0072 - val_acc: 0.8534 - val_KR: 2.2039 + + +.. parsed-literal:: + + Epoch 5/50 + loss: 0.0019 - acc: 0.8619 - KR: 2.2898 - val_loss: 0.0088 - val_acc: 0.8419 - val_KR: 2.3712 + + +.. parsed-literal:: + + Epoch 6/50 + loss: 0.0062 - acc: 0.8658 - KR: 2.3825 - val_loss: 0.0049 - val_acc: 0.8675 - val_KR: 2.4397 + + +.. parsed-literal:: + + Epoch 7/50 + loss: 0.0162 - acc: 0.8681 - KR: 2.4547 - val_loss: 0.0041 - val_acc: 0.8647 - val_KR: 2.4717 + + +.. parsed-literal:: + + Epoch 8/50 + loss: 0.0046 - acc: 0.8709 - KR: 2.4912 - val_loss: 0.0042 - val_acc: 0.8645 - val_KR: 2.4645 + + +.. parsed-literal:: + + Epoch 9/50 + loss: -0.0095 - acc: 0.8717 - KR: 2.5289 - val_loss: 0.0027 - val_acc: 0.8713 - val_KR: 2.5118 + + +.. parsed-literal:: + + Epoch 10/50 + loss: 0.0066 - acc: 0.8751 - KR: 2.5463 - val_loss: 0.0048 - val_acc: 0.8578 - val_KR: 2.6126 + + +.. parsed-literal:: + + Epoch 11/50 + loss: 0.0102 - acc: 0.8746 - KR: 2.5673 - val_loss: 0.0039 - val_acc: 0.8673 - val_KR: 2.5540 + + +.. parsed-literal:: + + Epoch 12/50 + loss: -0.0033 - acc: 0.8756 - KR: 2.5913 - val_loss: 0.0020 - val_acc: 0.8648 - val_KR: 2.5890 + + +.. parsed-literal:: + + Epoch 13/50 + loss: -0.0091 - acc: 0.8775 - KR: 2.6237 - val_loss: 0.0025 - val_acc: 0.8708 - val_KR: 2.5836 + + +.. parsed-literal:: + + Epoch 14/50 + loss: -0.0021 - acc: 0.8780 - KR: 2.6263 - val_loss: 0.0030 - val_acc: 0.8583 - val_KR: 2.6685 + + +.. parsed-literal:: + + Epoch 15/50 + loss: 0.0211 - acc: 0.8785 - KR: 2.6446 - val_loss: 0.0027 - val_acc: 0.8595 - val_KR: 2.6300 + + +.. parsed-literal:: + + Epoch 16/50 + loss: 0.0062 - acc: 0.8789 - KR: 2.6743 - val_loss: 0.0016 - val_acc: 0.8634 - val_KR: 2.6763 + + +.. parsed-literal:: + + Epoch 17/50 + loss: -0.0101 - acc: 0.8805 - KR: 2.7005 - val_loss: -0.0009 - val_acc: 0.8766 - val_KR: 2.6881 + + +.. parsed-literal:: + + Epoch 18/50 + loss: 0.0014 - acc: 0.8831 - KR: 2.7211 - val_loss: -0.0007 - val_acc: 0.8783 - val_KR: 2.7363 + + +.. parsed-literal:: + + Epoch 19/50 + loss: -0.0027 - acc: 0.8812 - KR: 2.7439 - val_loss: -0.0001 - val_acc: 0.8708 - val_KR: 2.7713 + + +.. parsed-literal:: + + Epoch 20/50 + loss: -0.0044 - acc: 0.8835 - KR: 2.7603 - val_loss: -0.0002 - val_acc: 0.8716 - val_KR: 2.7494 + + +.. parsed-literal:: + + Epoch 21/50 + loss: -0.0117 - acc: 0.8837 - KR: 2.7681 - val_loss: 0.0012 - val_acc: 0.8702 - val_KR: 2.7200 + + +.. parsed-literal:: + + Epoch 22/50 + loss: -0.0140 - acc: 0.8844 - KR: 2.7766 - val_loss: -0.0014 - val_acc: 0.8782 - val_KR: 2.8377 + + +.. parsed-literal:: + + Epoch 23/50 + loss: -0.0074 - acc: 0.8863 - KR: 2.7910 - val_loss: 0.0004 - val_acc: 0.8747 - val_KR: 2.7969 + + +.. parsed-literal:: + + Epoch 24/50 + loss: -0.0056 - acc: 0.8868 - KR: 2.7963 - val_loss: -0.0002 - val_acc: 0.8682 - val_KR: 2.7982 + + +.. parsed-literal:: + + Epoch 25/50 + loss: -0.0092 - acc: 0.8870 - KR: 2.7979 - val_loss: -0.0025 - val_acc: 0.8808 - val_KR: 2.8081 + + +.. parsed-literal:: + + Epoch 26/50 + loss: 0.0144 - acc: 0.8869 - KR: 2.8073 - val_loss: -0.0016 - val_acc: 0.8783 - val_KR: 2.8037 + + +.. parsed-literal:: + + Epoch 27/50 + loss: -0.0063 - acc: 0.8887 - KR: 2.8083 - val_loss: -0.0020 - val_acc: 0.8793 - val_KR: 2.7780 + + +.. parsed-literal:: + + Epoch 28/50 + loss: -0.0097 - acc: 0.8886 - KR: 2.8210 - val_loss: -0.0003 - val_acc: 0.8742 - val_KR: 2.7555 + + +.. parsed-literal:: + + Epoch 29/50 + loss: -0.0036 - acc: 0.8873 - KR: 2.8288 - val_loss: -0.0017 - val_acc: 0.8802 - val_KR: 2.8015 + + +.. parsed-literal:: + + Epoch 30/50 + loss: -0.0130 - acc: 0.8888 - KR: 2.8301 - val_loss: -0.0019 - val_acc: 0.8792 - val_KR: 2.8037 + + +.. parsed-literal:: + + Epoch 31/50 + loss: -0.0001 - acc: 0.8898 - KR: 2.8378 - val_loss: -0.0025 - val_acc: 0.8800 - val_KR: 2.7789 + + +.. parsed-literal:: + + Epoch 32/50 + loss: -0.0027 - acc: 0.8893 - KR: 2.8273 - val_loss: -0.0017 - val_acc: 0.8735 - val_KR: 2.8077 + + +.. parsed-literal:: + + Epoch 33/50 + loss: 0.0239 - acc: 0.8908 - KR: 2.8385 - val_loss: -0.0013 - val_acc: 0.8770 - val_KR: 2.8136 + + +.. parsed-literal:: + + Epoch 34/50 + loss: -0.0139 - acc: 0.8910 - KR: 2.8461 - val_loss: -0.0029 - val_acc: 0.8792 - val_KR: 2.8236 + + +.. parsed-literal:: + + Epoch 35/50 + loss: -0.0040 - acc: 0.8901 - KR: 2.8543 - val_loss: -0.0013 - val_acc: 0.8740 - val_KR: 2.8225 + + +.. parsed-literal:: + + Epoch 36/50 + loss: -0.0020 - acc: 0.8919 - KR: 2.8619 - val_loss: -0.0025 - val_acc: 0.8800 - val_KR: 2.8071 + + +.. parsed-literal:: + + Epoch 37/50 + loss: -0.0067 - acc: 0.8925 - KR: 2.8522 - val_loss: -0.0032 - val_acc: 0.8812 - val_KR: 2.8336 + + +.. parsed-literal:: + + Epoch 38/50 + loss: -0.0063 - acc: 0.8916 - KR: 2.8582 - val_loss: -0.0036 - val_acc: 0.8812 - val_KR: 2.8604 + + +.. parsed-literal:: + + Epoch 39/50 + loss: -0.0087 - acc: 0.8927 - KR: 2.8672 - val_loss: -0.0033 - val_acc: 0.8846 - val_KR: 2.8692 + + +.. parsed-literal:: + + Epoch 40/50 + loss: -0.0147 - acc: 0.8942 - KR: 2.8641 - val_loss: -0.0014 - val_acc: 0.8832 - val_KR: 2.8150 + + +.. parsed-literal:: + + Epoch 41/50 + loss: 0.0033 - acc: 0.8928 - KR: 2.8696 - val_loss: -0.0033 - val_acc: 0.8830 - val_KR: 2.8585 + + +.. parsed-literal:: + + Epoch 42/50 + loss: -0.0066 - acc: 0.8934 - KR: 2.8735 - val_loss: -0.0030 - val_acc: 0.8809 - val_KR: 2.8260 + + +.. parsed-literal:: + + Epoch 43/50 + loss: -0.0146 - acc: 0.8952 - KR: 2.8766 - val_loss: -0.0031 - val_acc: 0.8852 - val_KR: 2.8403 + + +.. parsed-literal:: + + Epoch 44/50 + loss: -0.0086 - acc: 0.8950 - KR: 2.8773 - val_loss: -0.0018 - val_acc: 0.8787 - val_KR: 2.9115 + + +.. parsed-literal:: + + Epoch 45/50 + loss: -0.0000 - acc: 0.8957 - KR: 2.8799 - val_loss: -0.0040 - val_acc: 0.8863 - val_KR: 2.8622 + + +.. parsed-literal:: + + Epoch 46/50 + loss: -0.0104 - acc: 0.8961 - KR: 2.8910 - val_loss: -0.0038 - val_acc: 0.8843 - val_KR: 2.8445 + + +.. parsed-literal:: + + Epoch 47/50 + loss: -0.0022 - acc: 0.8953 - KR: 2.8878 - val_loss: -0.0036 - val_acc: 0.8823 - val_KR: 2.8444 + + +.. parsed-literal:: + + Epoch 48/50 + loss: -0.0157 - acc: 0.8951 - KR: 2.8893 - val_loss: -0.0044 - val_acc: 0.8867 - val_KR: 2.8650 + + +.. parsed-literal:: + + Epoch 49/50 + loss: -0.0080 - acc: 0.8945 - KR: 2.8897 - val_loss: -0.0042 - val_acc: 0.8851 - val_KR: 2.8629 + + +.. parsed-literal:: + + Epoch 50/50 + loss: -0.0060 - acc: 0.8966 - KR: 2.8937 - val_loss: -0.0038 - val_acc: 0.8845 - val_KR: 2.8673 4. Model export @@ -433,9 +570,34 @@ of the network (proxy of the average certificate). Once training is finished, the model can be optimized for inference by using the ``vanilla_export()`` method. The ``torchlip`` layers are -converted to their PyTorch counterparts, e.g. \ ``SpectralConv2d`` +converted to their PyTorch counterparts, e.g. ``SpectralConv2d`` layers will be converted into ``torch.nn.Conv2d`` layers. +Warnings: +~~~~~~~~~ + +vanilla_export method modifies the model in-place. + +In order to build and export a new model while keeping the reference +one, it is required to follow these steps: + +# Build e new mode for instance with torchlip.Sequential( +torchlip.SpectralConv2d(…), …) + +``vanilla_model = ()`` + +# Copy the parameters from the reference t the new model + +``vanilla_model.load_state_dict(model.state_dict())`` + +# one forward required to initialize pamatrizations + +``vanilla_model(one_input)`` + +# vanilla_export the new model + +``vanilla_model = vanilla_model.vanilla_export()`` + .. code:: ipython3 vanilla_model = model.vanilla_export() @@ -458,7 +620,7 @@ layers will be converted into ``torch.nn.Conv2d`` layers. (6): Flatten(start_dim=1, end_dim=-1) (7): Linear(in_features=1568, out_features=64, bias=True) (8): GroupSort2() - (9): Linear(in_features=64, out_features=10, bias=False) + (9): Linear(in_features=64, out_features=10, bias=True) ) @@ -478,17 +640,17 @@ perform adversarial attacks. .. code:: ipython3 import numpy as np - + # Select only the first batch from the test set - sub_data, sub_targets = iter(test_loader).next() + sub_data, sub_targets = next(iter(test_loader)) sub_data, sub_targets = sub_data.to(device), sub_targets.to(device) - + # Drop misclassified elements output = vanilla_model(sub_data) well_classified_mask = output.argmax(dim=-1) == sub_targets sub_data = sub_data[well_classified_mask] sub_targets = sub_targets[well_classified_mask] - + # Retrieve one image per class images_list, targets_list = [], [] for i in range(10): @@ -496,10 +658,10 @@ perform adversarial attacks. label_mask = sub_targets == i x = sub_data[label_mask][0] y = sub_targets[label_mask][0] - + images_list.append(x) targets_list.append(y) - + images = torch.stack(images_list) targets = torch.stack(targets_list) @@ -507,13 +669,13 @@ perform adversarial attacks. In order to build a certificate :math:`\mathcal{M}` for a given sample, we take the top-2 output and apply the following formula: -.. math:: \mathcal{M} = \frac{\text{top}_1 - \text{top}_2}{\sqrt{2}} +.. math:: \mathcal{M} = \frac{\text{top}_1 - \text{top}_2}{2} This certificate is a guarantee that no L2 attack can defeat the given image sample with a robustness radius :math:`\epsilon` lower than the certificate, i.e. -.. math:: \epsilon \geq \mathcal{M} +.. math:: \epsilon \geq \mathcal{M} In the following cell, we attack the model on the ten selected images and compare the obtained radius :math:`\epsilon` with the certificates @@ -524,17 +686,18 @@ gradient norm preserving, other attacks gives very similar results. .. code:: ipython3 import foolbox as fb - + # Compute certificates values, _ = vanilla_model(images).topk(k=2) - certificates = (values[:, 0] - values[:, 1]) / np.sqrt(2) - + #The factor is 2.0 when using disjoint_neurons==True + certificates = (values[:, 0] - values[:, 1]) / 2. + # Run Carlini & Wagner attack fmodel = fb.PyTorchModel(vanilla_model, bounds=(0.0, 1.0), device=device) attack = fb.attacks.L2CarliniWagnerAttack(binary_search_steps=6, steps=8000) _, advs, success = attack(fmodel, images, targets, epsilons=None) dist_to_adv = (images - advs).square().sum(dim=(1, 2, 3)).sqrt() - + # Print results print("Image # Certificate Distance to adversarial") print("---------------------------------------------------") @@ -547,16 +710,16 @@ gradient norm preserving, other attacks gives very similar results. Image # Certificate Distance to adversarial --------------------------------------------------- - Image 0 0.485 1.33 - Image 1 1.510 3.46 - Image 2 0.593 1.79 - Image 3 0.903 2.00 - Image 4 0.090 0.26 - Image 5 0.288 0.73 - Image 6 0.212 0.75 - Image 7 0.520 1.16 - Image 8 1.042 3.03 - Image 9 0.269 0.73 + Image 0 0.246 1.04 + Image 1 1.863 4.57 + Image 2 0.475 1.78 + Image 3 0.601 2.71 + Image 4 0.108 0.43 + Image 5 0.214 0.83 + Image 6 0.104 0.45 + Image 7 0.447 1.61 + Image 8 1.564 3.89 + Image 9 0.135 0.59 Finally, we can take a visual look at the obtained images. When looking @@ -577,7 +740,7 @@ properties: .. code:: ipython3 import matplotlib.pyplot as plt - + def adversarial_viz(model, images, advs, class_names): """ This functions shows for each image sample: @@ -588,18 +751,18 @@ properties: """ scale = 1.5 nb_imgs = images.shape[0] - + # Compute certificates values, _ = model(images).topk(k=2) certificates = (values[:, 0] - values[:, 1]) / np.sqrt(2) - + # Compute distance between image and its adversarial dist_to_adv = (images - advs).square().sum(dim=(1, 2, 3)).sqrt() - + # Find predicted classes for images and their adversarials orig_classes = [class_names[i] for i in model(images).argmax(dim=-1)] advs_classes = [class_names[i] for i in model(advs).argmax(dim=-1)] - + # Compute difference maps advs = advs.detach().cpu() images = images.detach().cpu() @@ -608,14 +771,14 @@ properties: diff_map = np.concatenate( [diff_neg, diff_pos, np.zeros_like(diff_neg)], axis=1 ).transpose((0, 2, 3, 1)) - + # Create plot def _set_ax(ax, title): ax.set_title(title) ax.set_xticks([]) ax.set_yticks([]) ax.axis("off") - + figsize = (3 * scale, nb_imgs * scale) _, axes = plt.subplots( ncols=3, nrows=nb_imgs, figsize=figsize, squeeze=False, constrained_layout=True @@ -627,11 +790,12 @@ properties: axes[i][1].imshow(advs[i].squeeze(), cmap="gray") _set_ax(axes[i][2], f"certif: {certificates[i]:.2f}, obs: {dist_to_adv[i]:.2f}") axes[i][2].imshow(diff_map[i] / diff_map[i].max()) - - + + adversarial_viz(vanilla_model, images, advs, test_set.classes) -.. image:: wasserstein_classification_fashionMNIST_files/wasserstein_classification_fashionMNIST_15_0.png +.. image:: wasserstein_classification_fashionMNIST_files/wasserstein_classification_fashionMNIST_16_0.png + diff --git a/docs/source/wasserstein_classification_fashionMNIST_files/wasserstein_classification_fashionMNIST_15_0.png b/docs/source/wasserstein_classification_fashionMNIST_files/wasserstein_classification_fashionMNIST_15_0.png deleted file mode 100644 index 7f09f30..0000000 Binary files a/docs/source/wasserstein_classification_fashionMNIST_files/wasserstein_classification_fashionMNIST_15_0.png and /dev/null differ diff --git a/docs/source/wasserstein_classification_fashionMNIST_files/wasserstein_classification_fashionMNIST_16_0.png b/docs/source/wasserstein_classification_fashionMNIST_files/wasserstein_classification_fashionMNIST_16_0.png new file mode 100644 index 0000000..02d393c Binary files /dev/null and b/docs/source/wasserstein_classification_fashionMNIST_files/wasserstein_classification_fashionMNIST_16_0.png differ diff --git a/docs/source/wasserstein_toy.rst b/docs/source/wasserstein_toy.rst index 2562d87..178d194 100644 --- a/docs/source/wasserstein_toy.rst +++ b/docs/source/wasserstein_toy.rst @@ -56,18 +56,18 @@ The two distributions are .. code:: ipython3 from typing import Tuple - + import matplotlib.pyplot as plt import numpy as np - + size = (64, 64) frac = 0.3 # proportion of the center square - - + + def generate_toy_images(shape: Tuple[int, int], frac: float = 0, value: float = 1): """ Generates a single image. - + Args: shape: Shape of the output image. frac: Proportion of the center rectangle. @@ -76,42 +76,42 @@ The two distributions are img = np.zeros(shape) if frac == 0: return img - + frac = frac ** 0.5 - + l = int(shape[0] * frac) ldec = (shape[0] - l) // 2 w = int(shape[1] * frac) wdec = (shape[1] - w) // 2 - + img[ldec : ldec + l, wdec : wdec + w] = value - + return img - - + + def generator(batch_size: int, shape: Tuple[int, int], frac: float): """ Creates an infinite generator that generates batch of images. Half of the batch comes from the first distribution (only black images), while the remaining half comes from the second distribution. - + Args: batch_size: Number of images in each batch. shape: Shape of the image. frac: Fraction of the square to set "white". - + Returns: An infinite generator that yield batch of the given size. """ - + pwhite = generate_toy_images(shape, frac=frac, value=1) nwhite = generate_toy_images(shape, frac=frac, value=-1) - + nblack = batch_size // 2 nsquares = batch_size - nblack npwhite = nsquares // 2 nnwhite = nsquares - npwhite - + batch_x = np.concatenate( ( np.zeros((nblack,) + shape), @@ -121,11 +121,11 @@ The two distributions are axis=0, ) batch_y = np.concatenate((np.zeros((nblack, 1)), np.ones((nsquares, 1))), axis=0) - + while True: yield batch_x, batch_y - - + + def display_image(ax, image, title: str = ""): ax.imshow(image, cmap="gray") ax.set_xticks([]) @@ -141,13 +141,13 @@ between the two sets. img1 = generate_toy_images(size, 0) img2 = generate_toy_images(size, frac, value=-1) img3 = generate_toy_images(size, frac, value=1) - + fig, axs = plt.subplots(1, 3, figsize=(21, 7)) - + display_image(axs[0], img1, "black (label = -1)") display_image(axs[1], img2, "'negative' white (label = 1)") display_image(axs[2], img3, "'positive' white (label = 1)") - + print("L2-Norm, black vs. 'negative' white -> {}".format(np.linalg.norm(img2 - img1))) print("L2-Norm, black vs. 'positive' white -> {}".format(np.linalg.norm(img3 - img1))) @@ -177,7 +177,7 @@ distance is W_1(\mu, \nu) = \sup_{f \in Lip_1(\Omega)} \underset{\textbf{x} \sim \mu}{\mathbb{E}} \left[f(\textbf{x} )\right] -\underset{\textbf{x} \sim \nu}{\mathbb{E}} - \left[f(\textbf{x} )\right]. + \left[f(\textbf{x} )\right]. This states the problem as an optimization problem over the space of 1-Lipschitz functions. We can estimate this by optimizing over the space @@ -212,9 +212,9 @@ function: import torch from deel import torchlip - + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - + wass = torchlip.Sequential( torch.nn.Flatten(), torchlip.SpectralLinear(np.prod(size), 128), @@ -225,28 +225,54 @@ function: torchlip.FullSort(), torchlip.FrobeniusLinear(32, 1), ).to(device) - + wass -.. parsed-literal:: - - Sequential model contains a layer which is not a Lipschitz layer: Flatten(start_dim=1, end_dim=-1) - - .. parsed-literal:: Sequential( (0): Flatten(start_dim=1, end_dim=-1) - (1): SpectralLinear(in_features=4096, out_features=128, bias=True) + (1): ParametrizedSpectralLinear( + in_features=4096, out_features=128, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ) (2): FullSort() - (3): SpectralLinear(in_features=128, out_features=64, bias=True) + (3): ParametrizedSpectralLinear( + in_features=128, out_features=64, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ) (4): FullSort() - (5): SpectralLinear(in_features=64, out_features=32, bias=True) + (5): ParametrizedSpectralLinear( + in_features=64, out_features=32, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ) (6): FullSort() - (7): FrobeniusLinear(in_features=32, out_features=1, bias=True) + (7): ParametrizedFrobeniusLinear( + in_features=32, out_features=1, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _FrobeniusNorm() + ) + ) + ) ) @@ -259,22 +285,23 @@ formulation for the Wasserstein distance. .. code:: ipython3 - from deel.torchlip.functional import kr_loss + from deel.torchlip import KRLoss from tqdm import trange - + batch_size = 16 n_epochs = 10 steps_per_epoch = 256 - + # Create the image generator: g = generator(batch_size, size, frac) - + + kr_loss = KRLoss() optimizer = torch.optim.Adam(lr=0.01, params=wass.parameters()) - + n_steps = steps_per_epoch // batch_size - + for epoch in range(n_epochs): - + tsteps = trange(n_steps, desc=f"Epoch {epoch + 1}/{n_epochs}") for _ in tsteps: data, target = next(g) @@ -293,16 +320,776 @@ formulation for the Wasserstein distance. .. parsed-literal:: - Epoch 1/10: 100%|███████████████████████████████████████████████████████████████████████| 16/16 [00:00<00:00, 16.40it/s, loss=-29.041878] - Epoch 2/10: 100%|███████████████████████████████████████████████████████████████████████| 16/16 [00:00<00:00, 16.53it/s, loss=-34.570045] - Epoch 3/10: 100%|███████████████████████████████████████████████████████████████████████| 16/16 [00:00<00:00, 16.14it/s, loss=-34.912281] - Epoch 4/10: 100%|███████████████████████████████████████████████████████████████████████| 16/16 [00:00<00:00, 16.11it/s, loss=-34.984196] - Epoch 5/10: 100%|███████████████████████████████████████████████████████████████████████| 16/16 [00:00<00:00, 16.57it/s, loss=-34.992695] - Epoch 6/10: 100%|███████████████████████████████████████████████████████████████████████| 16/16 [00:00<00:00, 16.14it/s, loss=-34.993195] - Epoch 7/10: 100%|███████████████████████████████████████████████████████████████████████| 16/16 [00:00<00:00, 16.36it/s, loss=-34.994316] - Epoch 8/10: 100%|███████████████████████████████████████████████████████████████████████| 16/16 [00:00<00:00, 16.62it/s, loss=-34.994377] - Epoch 9/10: 100%|███████████████████████████████████████████████████████████████████████| 16/16 [00:00<00:00, 16.47it/s, loss=-34.993877] - Epoch 10/10: 100%|██████████████████████████████████████████████████████████████████████| 16/16 [00:00<00:00, 16.35it/s, loss=-34.994080] + Epoch 1/10: 0%| | 0/16 [00:00 + @@ -147,9 +147,9 @@ sub-class of functions. import torch from deel import torchlip - + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - + # Other Lipschitz activations are ReLU, MaxMin, GroupSort2, GroupSort. wass = torchlip.Sequential( torchlip.SpectralLinear(2, 256), @@ -160,7 +160,7 @@ sub-class of functions. torchlip.FullSort(), torchlip.FrobeniusLinear(64, 1), ).to(device) - + wass @@ -169,13 +169,44 @@ sub-class of functions. .. parsed-literal:: Sequential( - (0): SpectralLinear(in_features=2, out_features=256, bias=True) + (0): ParametrizedSpectralLinear( + in_features=2, out_features=256, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ) (1): FullSort() - (2): SpectralLinear(in_features=256, out_features=128, bias=True) + (2): ParametrizedSpectralLinear( + in_features=256, out_features=128, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ) (3): FullSort() - (4): SpectralLinear(in_features=128, out_features=64, bias=True) + (4): ParametrizedSpectralLinear( + in_features=128, out_features=64, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _SpectralNorm() + (1): _BjorckNorm() + ) + ) + ) (5): FullSort() - (6): FrobeniusLinear(in_features=64, out_features=1, bias=True) + (6): ParametrizedFrobeniusLinear( + in_features=64, out_features=1, bias=True + (parametrizations): ModuleDict( + (weight): ParametrizationList( + (0): _FrobeniusNorm() + ) + ) + ) ) @@ -195,40 +226,43 @@ dataset. .. code:: ipython3 - from deel.torchlip.functional import kr_loss, hkr_loss, hinge_margin_loss - + from deel.torchlip import KRLoss, HKRLoss, HingeMarginLoss + batch_size = 256 n_epochs = 10 - - alpha = 10 + + alpha = 0.98 min_margin = 0.29 # minimum margin to enforce between the values of F for each class - + + kr_loss = KRLoss() + hkr_loss = HKRLoss(alpha=alpha, min_margin=min_margin) + hinge_margin_loss =HingeMarginLoss(min_margin=min_margin) optimizer = torch.optim.Adam(lr=0.01, params=wass.parameters()) - + loader = torch.utils.data.DataLoader( torch.utils.data.TensorDataset(torch.tensor(X).float(), torch.tensor(Y).float()), batch_size=batch_size, shuffle=True, ) - + for epoch in range(n_epochs): - + m_kr, m_hm, m_acc = 0, 0, 0 - + for step, (data, target) in enumerate(loader): data, target = data.to(device), target.to(device) optimizer.zero_grad() output = wass(data) - loss = hkr_loss(output, target, alpha=alpha, min_margin=min_margin) + loss = hkr_loss(output, target) loss.backward() optimizer.step() - - m_kr += kr_loss(output, target, (1, -1)) - m_hm += hinge_margin_loss(output, target, min_margin) + + m_kr += kr_loss(output, target) + m_hm += hinge_margin_loss(output, target) m_acc += ( torch.sign(output.view(target.shape)) == torch.sign(target) ).sum() / len(target) - + print(f"Epoch {epoch + 1}/{n_epochs}") print( f"loss: {loss:.04f} - " @@ -242,25 +276,41 @@ dataset. .. parsed-literal:: Epoch 1/10 - loss: 1.7240 - KR: 0.0837 - hinge: 0.2519 - accuracy: 0.5387 + loss: 0.1045 - KR: 0.0198 - hinge: 0.1362 - accuracy: 0.5065 Epoch 2/10 - loss: -0.3211 - KR: 0.5286 - hinge: 0.0969 - accuracy: 0.8665 + loss: 0.0195 - KR: 0.2597 - hinge: 0.0510 - accuracy: 0.8651 + + +.. parsed-literal:: + Epoch 3/10 - loss: -0.7250 - KR: 0.8928 - hinge: 0.0484 - accuracy: 0.9253 + loss: 0.0021 - KR: 0.4625 - hinge: 0.0193 - accuracy: 0.9495 Epoch 4/10 - loss: -0.6545 - KR: 0.9257 - hinge: 0.0328 - accuracy: 0.9552 + loss: -0.0094 - KR: 0.4755 - hinge: 0.0046 - accuracy: 0.9947 + + +.. parsed-literal:: + Epoch 5/10 - loss: -0.5023 - KR: 0.9287 - hinge: 0.0262 - accuracy: 0.9696 + loss: -0.0107 - KR: 0.5690 - hinge: 0.0014 - accuracy: 0.9996 Epoch 6/10 - loss: -0.5727 - KR: 0.9217 - hinge: 0.0223 - accuracy: 0.9785 + loss: -0.0135 - KR: 0.6430 - hinge: 0.0011 - accuracy: 0.9998 + + +.. parsed-literal:: + Epoch 7/10 - loss: -0.6651 - KR: 0.9306 - hinge: 0.0202 - accuracy: 0.9862 + loss: -0.0129 - KR: 0.6983 - hinge: 0.0014 - accuracy: 0.9990 Epoch 8/10 - loss: -0.5247 - KR: 0.9454 - hinge: 0.0208 - accuracy: 0.9810 + loss: -0.0119 - KR: 0.7164 - hinge: 0.0012 - accuracy: 0.9994 + + +.. parsed-literal:: + Epoch 9/10 - loss: -0.6442 - KR: 0.9496 - hinge: 0.0205 - accuracy: 0.9811 + loss: -0.0149 - KR: 0.7620 - hinge: 0.0014 - accuracy: 0.9994 Epoch 10/10 - loss: -0.7998 - KR: 0.9713 - hinge: 0.0211 - accuracy: 0.9791 + loss: -0.0152 - KR: 0.7569 - hinge: 0.0012 - accuracy: 0.9992 2.6. Plot output countour line @@ -274,29 +324,30 @@ draw a countour plot to visualize :math:`F`. import matplotlib.pyplot as plt import numpy as np - + x = np.linspace(X[:, 0].min() - 0.2, X[:, 0].max() + 0.2, 120) y = np.linspace(X[:, 1].min() - 0.2, X[:, 1].max() + 0.2, 120) xx, yy = np.meshgrid(x, y, sparse=False) X_pred = np.stack((xx.ravel(), yy.ravel()), axis=1) - + # Make predictions from F: Y_pred = wass(torch.tensor(X_pred).float().to(device)) Y_pred = Y_pred.reshape(x.shape[0], y.shape[0]).detach().cpu().numpy() - + # We are also going to check the exported version: vwass = wass.vanilla_export() + Y_predv = vwass(torch.tensor(X_pred).float().to(device)) Y_predv = Y_predv.reshape(x.shape[0], y.shape[0]).detach().cpu().numpy() - + # Plot the results: fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 6)) - + sns.scatterplot(x=X[Y == 1, 0], y=X[Y == 1, 1], alpha=0.1, ax=ax1) sns.scatterplot(x=X[Y == -1, 0], y=X[Y == -1, 1], alpha=0.1, ax=ax1) cset = ax1.contour(xx, yy, Y_pred, cmap="twilight", levels=np.arange(-1.2, 1.2, 0.4)) ax1.clabel(cset, inline=1, fontsize=10) - + sns.scatterplot(x=X[Y == 1, 0], y=X[Y == 1, 1], alpha=0.1, ax=ax2) sns.scatterplot(x=X[Y == -1, 0], y=X[Y == -1, 1], alpha=0.1, ax=ax2) cset = ax2.contour(xx, yy, Y_predv, cmap="twilight", levels=np.arange(-1.2, 1.2, 0.4)) @@ -307,7 +358,7 @@ draw a countour plot to visualize :math:`F`. .. parsed-literal:: - + diff --git a/docs/source/wasserstein_toy_classification_files/wasserstein_toy_classification_12_1.png b/docs/source/wasserstein_toy_classification_files/wasserstein_toy_classification_12_1.png index 42fc17e..e5c99e2 100644 Binary files a/docs/source/wasserstein_toy_classification_files/wasserstein_toy_classification_12_1.png and b/docs/source/wasserstein_toy_classification_files/wasserstein_toy_classification_12_1.png differ diff --git a/docs/source/wasserstein_toy_classification_files/wasserstein_toy_classification_5_1.png b/docs/source/wasserstein_toy_classification_files/wasserstein_toy_classification_5_1.png index 102c415..5aba553 100644 Binary files a/docs/source/wasserstein_toy_classification_files/wasserstein_toy_classification_5_1.png and b/docs/source/wasserstein_toy_classification_files/wasserstein_toy_classification_5_1.png differ diff --git a/docs/source/wasserstein_toy_files/wasserstein_toy_5_1.png b/docs/source/wasserstein_toy_files/wasserstein_toy_5_1.png index 566ac81..8b4f91d 100644 Binary files a/docs/source/wasserstein_toy_files/wasserstein_toy_5_1.png and b/docs/source/wasserstein_toy_files/wasserstein_toy_5_1.png differ diff --git a/tests/test_activations.py b/tests/test_activations.py index 0232d8a..eadb144 100644 --- a/tests/test_activations.py +++ b/tests/test_activations.py @@ -31,7 +31,7 @@ from .utils_framework import ( CategoricalCrossentropy, GroupSort, - Householder, + HouseHolder, ) @@ -39,7 +39,7 @@ def check_serialization(layer_type, layer_params): m = uft.generate_k_lip_model(layer_type, layer_params, input_shape=(10,), k=1) if m is None: return - optimizer, loss, _ = uft.compile_model( + loss, optimizer, _ = uft.compile_model( m, optimizer=uft.get_instance_framework(uft.SGD, inst_params={"model": m}), loss=CategoricalCrossentropy(from_logits=True), @@ -59,7 +59,7 @@ def check_serialization(layer_type, layer_params): k=1, ) y2 = m2(x) - np.testing.assert_allclose(y1.numpy(), y2.numpy()) + np.testing.assert_allclose(uft.to_numpy(y1), uft.to_numpy(y2)) def test_group_sort_simple(): @@ -100,6 +100,16 @@ def test_group_sort_simple(): [1.0, 2.0, 1.0, 2.0], ], ), + ( + 4, + True, + [ + [1.0, 2.0, 3.0, 4.0], + [1.0, 2.0, 3.0, 4.0], + [1.0, 1.0, 2.0, 2.0], + [1.0, 1.0, 2.0, 2.0], + ], + ), ], ) def test_GroupSort(group_size, img, expected): @@ -119,8 +129,9 @@ def test_GroupSort(group_size, img, expected): else: xn = np.asarray(x) xnp = np.repeat( - np.expand_dims(np.repeat(np.expand_dims(xn, 1), 28, 1), 1), 28, 1 + np.expand_dims(np.repeat(np.expand_dims(xn, -1), 28, -1), -1), 28, -1 ) + xnp = uft.to_NCHW_inv(xnp) # move channel if needed (TF) x = uft.to_tensor(xnp) uft.build_layer(gs, (28, 28, 4)) y = gs(x).numpy() @@ -128,8 +139,9 @@ def test_GroupSort(group_size, img, expected): if img: y_tnp = np.asarray(y_t) y_t = np.repeat( - np.expand_dims(np.repeat(np.expand_dims(y_tnp, 1), 28, 1), 1), 28, 1 + np.expand_dims(np.repeat(np.expand_dims(y_tnp, -1), 28, -1), -1), 28, -1 ) + y_t = uft.to_NCHW_inv(y_t) # move channel if needed (TF) np.testing.assert_equal(y, y_t) @@ -146,7 +158,7 @@ def test_GroupSort_idempotence(group_size): np.testing.assert_equal(y1.numpy(), y2.numpy()) -"""Tests for Householder activation: +"""Tests for HouseHolder activation: - instantiation of layer - check outputs on dense (bs, n) tensor, with three thetas: 0, pi/2 and pi - check outputs on dense (bs, h, w, n) tensor, with three thetas: 0, pi/2 and pi @@ -155,162 +167,190 @@ def test_GroupSort_idempotence(group_size): @pytest.mark.skipif( - hasattr(Householder, "unavailable_class"), reason="Householder not available" + hasattr(HouseHolder, "unavailable_class"), reason="HouseHolder not available" ) @pytest.mark.parametrize( "params,shape,len_shape,expected", [ - ({}, (28, 28, 10), (5,), np.pi / 2), # Instantiation without argument + ( + {"channels": 10}, + (10, 28, 28), + (5,), + np.pi / 2, + ), # Instantiation without argument ( { "data_format": "channels_last", + "channels": 16, "k_coef_lip": 2.5, "theta_initializer": "ones", }, - (32, 32, 16), + (16, 32, 32), (8,), 1, ), # Instantiation with arguments ], ) -def test_Householder_instantiation(params, shape, len_shape, expected): - hh = uft.get_instance_framework(Householder, params) +def test_HouseHolder_instantiation(params, shape, len_shape, expected): + shape = uft.to_framework_channel(shape) + hh = uft.get_instance_framework(HouseHolder, params) uft.build_layer(hh, shape) - assert hh.theta.shape == len_shape - np.testing.assert_equal(hh.theta.numpy(), expected) + theta = np.squeeze(uft.to_numpy(hh.theta)) + assert theta.shape == len_shape + np.testing.assert_equal(theta, expected) @pytest.mark.skipif( - hasattr(Householder, "unavailable_class"), reason="Householder not available" + hasattr(HouseHolder, "unavailable_class"), reason="HouseHolder not available" ) -def test_Householder_serialization(): +def test_HouseHolder_serialization(): # Check serialization check_serialization( - Householder, layer_params={"theta_initializer": "glorot_uniform"} + HouseHolder, layer_params={"channels": 10, "theta_initializer": "normal"} ) + if uft.framework == "torch": + pytest.skip("data format skipped in torch") # Instantiation error because of wrong data format with pytest.raises(RuntimeError): - _ = uft.get_instance_framework(Householder, {"data_format": "channels_first"}) + _ = uft.get_instance_framework( + HouseHolder, {"channels": 4, "data_format": "channels_first"} + ) @pytest.mark.skipif( - hasattr(Householder, "unavailable_class"), reason="Householder not available" + hasattr(HouseHolder, "unavailable_class"), reason="HouseHolder not available" ) @pytest.mark.parametrize("dense", [(True,), (False,)]) -def test_Householder_theta_zero(dense): - """Householder with theta=0 on 2-D tensor (bs, n). +def test_HouseHolder_theta_zero(dense): + """HouseHolder with theta=0 on 2-D tensor (bs, n). Theta=0 means Id if z2 > 0, and reflection if z2 < 0. """ - hh = uft.get_instance_framework(Householder, {"theta_initializer": "zeros"}) if dense: bs = np.random.randint(64, 512) n = np.random.randint(1, 1024) * 2 size = (bs, n // 2) + ch = n else: # convolutional bs = np.random.randint(32, 128) h, w = np.random.randint(1, 64), np.random.randint(1, 64) c = np.random.randint(1, 64) * 2 - size = (bs, h, w, c // 2) + size = (bs,) + uft.to_framework_channel((c // 2, h, w)) + ch = c + + hh = uft.get_instance_framework( + HouseHolder, {"channels": ch, "theta_initializer": "zeros"} + ) # Case 1: hh(x) = x (identity case, z2 > 0) z1 = np.random.normal(size=size) z2 = np.random.uniform(size=size) x = np.concatenate([z1, z2], axis=-1) - np.testing.assert_allclose(hh(uft.to_tensor(x)), x) + y = uft.to_numpy(hh(uft.to_tensor(x))) + np.testing.assert_allclose(y, x) # Case 2: hh(x) = [z1, -z2] (reflection across z1 axis, z2 < 0) z1 = np.random.normal(size=size) z2 = -np.random.uniform(size=size) x = np.concatenate([z1, z2], axis=-1) expected_output = np.concatenate([z1, -z2], axis=-1) - np.testing.assert_allclose(hh(uft.to_tensor(x)), expected_output) + y = uft.to_numpy(hh(uft.to_tensor(x))) + np.testing.assert_allclose(y, expected_output) @pytest.mark.skipif( - hasattr(Householder, "unavailable_class"), reason="Householder not available" + hasattr(HouseHolder, "unavailable_class"), reason="HouseHolder not available" ) @pytest.mark.parametrize("dense", [(True,), (False,)]) -def test_Householder_theta_pi(dense): - """Householder with theta=pi on 2-D tensor (bs, n). +def test_HouseHolder_theta_pi(dense): + """HouseHolder with theta=pi on 2-D tensor (bs, n). Theta=pi means Id if z1 < 0, and reflection if z1 > 0. """ - hh = uft.get_instance_framework( - Householder, {"theta_initializer": uft.initializers_Constant(np.pi)} - ) if dense: bs = np.random.randint(64, 512) n = np.random.randint(1, 1024) * 2 size = (bs, n // 2) + ch = n else: # convolutional bs = np.random.randint(32, 128) h, w = np.random.randint(1, 64), np.random.randint(1, 64) c = np.random.randint(1, 64) * 2 - size = (bs, h, w, c // 2) + size = (bs,) + uft.to_framework_channel((c // 2, h, w)) + ch = c + hh = uft.get_instance_framework( + HouseHolder, + {"channels": ch, "theta_initializer": uft.initializers_Constant(np.pi)}, + ) # Case 1: hh(x) = x (identity case, z1 < 0) z1 = -np.random.uniform(size=size) z2 = np.random.normal(size=size) x = np.concatenate([z1, z2], axis=-1) - np.testing.assert_allclose(hh(uft.to_tensor(x)), x, atol=1e-6) + y = uft.to_numpy(hh(uft.to_tensor(x))) + np.testing.assert_allclose(y, x, atol=1e-6) # Case 2: hh(x) = [z1, -z2] (reflection across z2 axis, z1 > 0) z1 = np.random.uniform(size=size) z2 = np.random.normal(size=size) x = np.concatenate([z1, z2], axis=-1) expected_output = np.concatenate([-z1, z2], axis=-1) - np.testing.assert_allclose(hh(uft.to_tensor(x)), expected_output, atol=1e-6) + y = uft.to_numpy(hh(uft.to_tensor(x))) + np.testing.assert_allclose(y, expected_output, atol=1e-6) @pytest.mark.skipif( - hasattr(Householder, "unavailable_class"), reason="Householder not available" + hasattr(HouseHolder, "unavailable_class"), reason="HouseHolder not available" ) @pytest.mark.parametrize("dense", [(True,), (False,)]) -def test_Householder_theta_90(dense): - """Householder with theta=pi/2 on 2-D tensor (bs, n). +def test_HouseHolder_theta_90(dense): + """HouseHolder with theta=pi/2 on 2-D tensor (bs, n). Theta=pi/2 is equivalent to GroupSort2: Id if z1 < z2, and reflection if z1 > z2 """ - hh = uft.get_instance_framework(Householder, {}) if dense: bs = np.random.randint(64, 512) n = np.random.randint(1, 1024) * 2 size = (bs, n // 2) + ch = n else: # convolutional bs = np.random.randint(32, 128) h, w = np.random.randint(1, 64), np.random.randint(1, 64) c = np.random.randint(1, 64) * 2 - size = (bs, h, w, c // 2) + size = (bs,) + uft.to_framework_channel((c // 2, h, w)) + ch = c + hh = uft.get_instance_framework(HouseHolder, {"channels": ch}) # Case 1: hh(x) = x (identity case, z1 < z2) z1 = -np.random.normal(size=size) z2 = z1 + np.random.uniform(size=size) x = np.concatenate([z1, z2], axis=-1) - np.testing.assert_allclose(hh(uft.to_tensor(x)), x) + y = uft.to_numpy(hh(uft.to_tensor(x))) + np.testing.assert_allclose(y, x) # Case 2: hh(x) = reflection(x) (if z1 > z2) z1 = np.random.normal(size=size) z2 = z1 - np.random.uniform(size=size) x = np.concatenate([z1, z2], axis=-1) expected_output = np.concatenate([z2, z1], axis=-1) - np.testing.assert_allclose(hh(uft.to_tensor(x)), expected_output, atol=1e-6) + y = uft.to_numpy(hh(uft.to_tensor(x))) + np.testing.assert_allclose(y, expected_output, atol=1e-6) @pytest.mark.skipif( - hasattr(Householder, "unavailable_class"), reason="Householder not available" + hasattr(HouseHolder, "unavailable_class"), reason="HouseHolder not available" ) -def test_Householder_idempotence(): - """Assert idempotence of Householder activation: hh(hh(x)) = hh(x)""" - hh = uft.get_instance_framework( - Householder, {"theta_initializer": "glorot_uniform"} - ) +def test_HouseHolder_idempotence(): + """Assert idempotence of HouseHolder activation: hh(hh(x)) = hh(x)""" bs = np.random.randint(32, 128) h, w = np.random.randint(1, 64), np.random.randint(1, 64) c = np.random.randint(1, 32) * 2 - x = np.random.normal(size=(bs, h, w, c)) + hh = uft.get_instance_framework( + HouseHolder, {"channels": c, "theta_initializer": "normal"} + ) + x = np.random.normal(size=(bs,) + uft.to_framework_channel((c, h, w))) x = uft.to_tensor(x) # Run two times the HH activation and compare both outputs y = hh(x) z = hh(y) - np.testing.assert_allclose(y, z) + np.testing.assert_allclose(uft.to_numpy(y), uft.to_numpy(z)) diff --git a/tests/test_condense.py b/tests/test_condense.py index 8930640..be802e4 100644 --- a/tests/test_condense.py +++ b/tests/test_condense.py @@ -33,9 +33,6 @@ from . import utils_framework as uft from tests.utils_framework import ( - vanillaModel, - vanilla_require_a_copy, - copy_model_parameters, Sequential, tModel, ) @@ -43,7 +40,7 @@ from tests.utils_framework import ( SpectralLinear, SpectralConv2d, - SpectralConv2dTranspose, + SpectralConvTranspose2d, FrobeniusLinear, FrobeniusConv2d, ScaledL2NormPool2d, @@ -75,7 +72,7 @@ def sequential_layers(input_shape): {"in_channels": 2, "out_channels": 2, "kernel_size": (3, 3), "padding": 1}, ), uft.get_instance_framework( - SpectralConv2dTranspose, + SpectralConvTranspose2d, {"in_channels": 2, "out_channels": 5, "kernel_size": (3, 3), "padding": 1}, ), uft.get_instance_framework(Flatten, {}), @@ -120,7 +117,7 @@ def get_functional_tensors(input_shape): }, ) dict_functional_tensors["convt2"] = uft.get_instance_framework( - SpectralConv2dTranspose, + SpectralConvTranspose2d, {"in_channels": 2, "out_channels": 5, "kernel_size": (3, 3), "padding": 1}, ) dict_functional_tensors["flatten"] = uft.get_instance_framework(Flatten, {}) @@ -157,8 +154,8 @@ def functional_input_output_tensors(dict_functional_tensors, x): # return x -def get_model(layer_type, layer_params, input_shape, k_coef_lip): - if layer_type == tModel: +def get_model(model_type, layer_params, input_shape, k_coef_lip): + if model_type == tModel: return uft.get_functional_model( tModel, layer_params["dict_tensors"], @@ -166,22 +163,23 @@ def get_model(layer_type, layer_params, input_shape, k_coef_lip): ) else: return uft.generate_k_lip_model( - layer_type, layer_params, input_shape=input_shape, k=k_coef_lip + model_type, layer_params, input_shape=input_shape, k=k_coef_lip ) @pytest.mark.skipif( - hasattr(SpectralConv2dTranspose, "unavailable_class"), - reason="SpectralConv2dTranspose not available", + hasattr(SpectralConvTranspose2d, "unavailable_class"), + reason="SpectralConvTranspose2d not available", ) @pytest.mark.parametrize( - "layer_type, layer_params, k_coef_lip, input_shape", + "model_type, params_type, param_fct, dict_other_params, k_coef_lip, input_shape", [ - (Sequential, {"layers": sequential_layers((3, 8, 8))}, 5.0, (3, 8, 8)), + (Sequential, "layers", sequential_layers, {}, 5.0, (3, 8, 8)), ( tModel, + "dict_tensors", + get_functional_tensors, { - "dict_tensors": get_functional_tensors((3, 8, 8)), "functional_input_output_tensors": functional_input_output_tensors, }, 5.0, @@ -189,7 +187,9 @@ def get_model(layer_type, layer_params, input_shape, k_coef_lip): ), ], ) -def test_model(layer_type, layer_params, k_coef_lip, input_shape): +def test_model( + model_type, params_type, param_fct, dict_other_params, k_coef_lip, input_shape +): batch_size = 250 epochs = 1 steps_per_epoch = 125 @@ -198,9 +198,11 @@ def test_model(layer_type, layer_params, k_coef_lip, input_shape): # clear session to avoid side effects from previous train uft.init_session() # K.clear_session() np.random.seed(42) + input_shape_CHW = input_shape input_shape = uft.to_framework_channel(input_shape) - - model = get_model(layer_type, layer_params, input_shape, k_coef_lip) + layer_params = {params_type: param_fct(input_shape_CHW)} + layer_params.update(dict_other_params) + model = get_model(model_type, layer_params, input_shape, k_coef_lip) # create the model, defin opt, and compile it optimizer = uft.get_instance_framework( @@ -242,12 +244,14 @@ def test_model(layer_type, layer_params, k_coef_lip, input_shape): test_dl = linear_generator(batch_size, input_shape, kernel) loss, mse = uft.run_test(model, test_dl, loss_fn, metrics, steps=10) # generate vanilla - if vanilla_require_a_copy(): - model2 = get_model(layer_type, layer_params, input_shape, k_coef_lip) - copy_model_parameters(model, model2) - vanilla_model = vanillaModel(model2) + if uft.vanilla_require_a_copy(): + layer_params = {params_type: param_fct(input_shape_CHW)} + layer_params.update(dict_other_params) + model2 = get_model(model_type, layer_params, input_shape, k_coef_lip) + uft.copy_model_parameters(model, model2) + vanilla_model = uft.vanillaModel(model2) else: - vanilla_model = vanillaModel(model) + vanilla_model = uft.vanillaModel(model) # vanilla_model = model.vanilla_export() loss_fn, optimizer, metrics = uft.compile_model( vanilla_model, @@ -265,12 +269,11 @@ def test_model(layer_type, layer_params, k_coef_lip, input_shape): vanilla_loss, vanilla_mse = uft.run_test( vanilla_model, test_dl, loss_fn, metrics, steps=10 ) - model.summary() - vanilla_model.summary() - np.testing.assert_equal( + np.testing.assert_almost_equal( mse, vanilla_mse, + 3, "the exported vanilla model must have same behaviour as original", ) np.testing.assert_equal( diff --git a/tests/test_layers.py b/tests/test_layers.py index 5ca4008..4017de0 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -35,7 +35,8 @@ from .utils_framework import ( SpectralLinear, SpectralConv2d, - SpectralConv2dTranspose, + SpectralConv1d, + SpectralConvTranspose2d, FrobeniusLinear, FrobeniusConv2d, ScaledAvgPool2d, @@ -43,7 +44,7 @@ ScaledL2NormPool2d, InvertibleDownSampling, InvertibleUpSampling, - ScaledGlobalL2NormPool2d, + ScaledAdaptativeL2NormPool2d, Flatten, Sequential, ) @@ -148,7 +149,7 @@ def train_k_lip_model( input_shape: tuple, k_lip_model: float, k_lip_data: float, - **kwargs + **kwargs, ): """ Create a generator, create a model, train it and return the results. @@ -251,7 +252,6 @@ def train_k_lip_model( def _check_mse_results(mse, from_disk_mse, test_params): - print("aaaaa", mse, from_disk_mse) assert from_disk_mse == pytest.approx( mse, 1e-5 ), "serialization must not change the performance of a layer" @@ -276,7 +276,7 @@ def _apply_tests_bank(test_params): ) = train_k_lip_model(**test_params) print("test mse: %f" % mse) print( - "empirical lip const: %f ( expected %s )" + "empirical lip const: %f ( expected min data and model %s )" % ( emp_lip_const, min(test_params["k_lip_model"], test_params["k_lip_data"]), @@ -455,227 +455,245 @@ def test_constraints_frobenius(test_params): @pytest.mark.parametrize( - "test_params", + "layer_type", [ - dict( - layer_type=SpectralLinear, - layer_params={"bias": False, "in_features": 4, "out_features": 3}, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(4,), - k_lip_data=1.0, - k_lip_model=1.0, - callbacks=[], + SpectralLinear, + ], +) +@pytest.mark.parametrize( + "layer_params,k_lip_data,k_lip_model", + [ + ( + {"bias": False, "in_features": 4, "out_features": 3}, + 1.0, + 1.0, ), - dict( - layer_type=SpectralLinear, - layer_params={"in_features": 4, "out_features": 4}, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(4,), - k_lip_data=5.0, - k_lip_model=1.0, - callbacks=[], + ( + {"in_features": 4, "out_features": 4}, + 5.0, + 1.0, ), - dict( - layer_type=SpectralLinear, - layer_params={"in_features": 4, "out_features": 4}, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(4,), - k_lip_data=1.0, - k_lip_model=5.0, - callbacks=[], + ( + {"in_features": 4, "out_features": 4}, + 1.0, + 5.0, ), ], ) -def test_spectral_dense(test_params): +def test_spectral_linear(layer_type, layer_params, k_lip_data, k_lip_model): + test_params = dict( + layer_type=layer_type, + layer_params=layer_params, + batch_size=250, + steps_per_epoch=125, + epochs=5, + input_shape=(4,), + k_lip_data=k_lip_data, + k_lip_model=k_lip_model, + callbacks=[], + ) _apply_tests_bank(test_params) @pytest.mark.parametrize( - "test_params", + "layer_type", [ - dict( - layer_type=FrobeniusLinear, - layer_params={"in_features": 4, "out_features": 1}, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(4,), - k_lip_data=1.0, - k_lip_model=1.0, - callbacks=[], + FrobeniusLinear, + ], +) +@pytest.mark.parametrize( + "layer_params,k_lip_data,k_lip_model", + [ + ( + {"bias": False, "in_features": 4, "out_features": 1}, + 1.0, + 1.0, ), - dict( - layer_type=FrobeniusLinear, - layer_params={"in_features": 4, "out_features": 1}, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(4,), - k_lip_data=5.0, - k_lip_model=1.0, - callbacks=[], + ( + {"in_features": 4, "out_features": 1}, + 5.0, + 1.0, ), - dict( - layer_type=FrobeniusLinear, - layer_params={"in_features": 4, "out_features": 1}, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(4,), - k_lip_data=1.0, - k_lip_model=5.0, - callbacks=[], + ( + {"in_features": 4, "out_features": 1}, + 1.0, + 5.0, ), ], ) -def test_frobenius_dense(test_params): +def test_frobenius_linear(layer_type, layer_params, k_lip_data, k_lip_model): + test_params = dict( + layer_type=layer_type, + layer_params=layer_params, + batch_size=250, + steps_per_epoch=125, + epochs=5, + input_shape=(4,), + k_lip_data=k_lip_data, + k_lip_model=k_lip_model, + callbacks=[], + ) _apply_tests_bank(test_params) @pytest.mark.parametrize( - "test_params", + "layer_type", + [SpectralConv2d, FrobeniusConv2d, SpectralConvTranspose2d], +) +@pytest.mark.parametrize( + "layer_params,k_lip_data,k_lip_model", [ - dict( - layer_type=SpectralConv2d, - layer_params={ + ( + { "in_channels": 1, "out_channels": 2, "kernel_size": (3, 3), "bias": False, }, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(1, 5, 5), - k_lip_data=1.0, - k_lip_model=1.0, - callbacks=[], + 1.0, + 1.0, ), - dict( - layer_type=SpectralConv2d, - layer_params={"in_channels": 1, "out_channels": 2, "kernel_size": (3, 3)}, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(1, 5, 5), - k_lip_data=5.0, - k_lip_model=1.0, - callbacks=[], + ( + {"in_channels": 1, "out_channels": 2, "kernel_size": (3, 3)}, + 5.0, + 1.0, ), - dict( - layer_type=SpectralConv2d, - layer_params={"in_channels": 1, "out_channels": 2, "kernel_size": (3, 3)}, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(1, 5, 5), - k_lip_data=1.0, - k_lip_model=5.0, - callbacks=[], + ( + {"in_channels": 1, "out_channels": 2, "kernel_size": (3, 3)}, + 1.0, + 5.0, ), ], ) -def test_spectralconv2d(test_params): +def test_conv2d(layer_type, layer_params, k_lip_data, k_lip_model): + if hasattr(layer_type, "unavailable_class"): + pytest.skip("layer not available") + test_params = dict( + layer_type=layer_type, + layer_params=layer_params, + batch_size=250, + steps_per_epoch=125, + epochs=5, + input_shape=(1, 5, 5), + k_lip_data=k_lip_data, + k_lip_model=k_lip_model, + callbacks=[], + ) _apply_tests_bank(test_params) -@pytest.mark.skipif( - hasattr(SpectralConv2dTranspose, "unavailable_class"), - reason="SpectralConv2dTranspose not available", +@pytest.mark.parametrize( + "pad_mode", + [ + "zeros", + "reflect", + "circular", + "symmetric", + ], ) @pytest.mark.parametrize( - "test_params", + "pad, kernel_size", [ - dict( - layer_type=SpectralConv2dTranspose, - layer_params={ + (1, (3, 3)), + ((1, 1), (3, 3)), + (2, (5, 5)), + ((2, 2), (5, 5)), + ], +) +@pytest.mark.parametrize( + "layer_params,k_lip_data,k_lip_model", + [ + ( + { "in_channels": 1, "out_channels": 2, - "kernel_size": (3, 3), "bias": False, }, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(1, 5, 5), - k_lip_data=1.0, - k_lip_model=1.0, - callbacks=[], + 1.0, + 1.0, ), - dict( - layer_type=SpectralConv2dTranspose, - layer_params={"in_channels": 1, "out_channels": 2, "kernel_size": (3, 3)}, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(1, 5, 5), - k_lip_data=5.0, - k_lip_model=1.0, - callbacks=[], + ( + {"in_channels": 1, "out_channels": 2}, + 1.0, + 1.0, ), - dict( - layer_type=SpectralConv2dTranspose, - layer_params={"in_channels": 1, "out_channels": 2, "kernel_size": (3, 3)}, - batch_size=250, - steps_per_epoch=125, - epochs=5, - input_shape=(1, 5, 5), - k_lip_data=1.0, - k_lip_model=5.0, - callbacks=[], + ( + {"in_channels": 1, "out_channels": 2}, + 1.0, + 5.0, ), ], ) -def test_SpectralConv2dTranspose(test_params): +def test_spectralconv2d_pad( + pad, pad_mode, kernel_size, layer_params, k_lip_data, k_lip_model +): + layer_params["padding"] = pad + layer_params["padding_mode"] = pad_mode + layer_params["kernel_size"] = kernel_size + if not uft.is_supported_padding(pad_mode, SpectralConv2d): + pytest.skip(f"SpectralConv2d: Padding {pad_mode} not supported") + test_params = dict( + layer_type=SpectralConv2d, + layer_params=layer_params, + batch_size=250, + steps_per_epoch=125, + epochs=5, + input_shape=(1, 5, 5), + k_lip_data=k_lip_data, + k_lip_model=k_lip_model, + callbacks=[], + ) _apply_tests_bank(test_params) +@pytest.mark.skipif( + hasattr(SpectralConv1d, "unavailable_class"), + reason="SpectralConv1d not available", +) @pytest.mark.parametrize( "test_params", [ dict( - layer_type=FrobeniusConv2d, - layer_params={"in_channels": 1, "out_channels": 2, "kernel_size": (3, 3)}, + layer_type=SpectralConv1d, + layer_params={ + "in_channels": 1, + "out_channels": 2, + "kernel_size": 3, + "bias": False, + }, batch_size=250, steps_per_epoch=125, epochs=5, - input_shape=(1, 5, 5), + input_shape=(1, 5), k_lip_data=1.0, k_lip_model=1.0, callbacks=[], ), dict( - layer_type=FrobeniusConv2d, - layer_params={"in_channels": 1, "out_channels": 2, "kernel_size": (3, 3)}, + layer_type=SpectralConv1d, + layer_params={"in_channels": 1, "out_channels": 2, "kernel_size": 3}, batch_size=250, steps_per_epoch=125, epochs=5, - input_shape=(1, 5, 5), + input_shape=(1, 5), k_lip_data=5.0, k_lip_model=1.0, callbacks=[], ), dict( - layer_type=FrobeniusConv2d, - layer_params={"in_channels": 1, "out_channels": 2, "kernel_size": (3, 3)}, + layer_type=SpectralConv1d, + layer_params={"in_channels": 1, "out_channels": 2, "kernel_size": 3}, batch_size=250, steps_per_epoch=125, epochs=5, - input_shape=(1, 5, 5), + input_shape=(1, 5), k_lip_data=1.0, k_lip_model=5.0, callbacks=[], ), ], ) -def test_frobeniusconv2d(test_params): - # tests only checks that lip cons is enforced +def test_spectralconv1d(test_params): _apply_tests_bank(test_params) @@ -859,7 +877,7 @@ def test_scaledl2normPool2d(test_params): @pytest.mark.skipif( - hasattr(ScaledGlobalL2NormPool2d, "unavailable_class"), + hasattr(ScaledAdaptativeL2NormPool2d, "unavailable_class"), reason="compute_layer_sv not available", ) @pytest.mark.parametrize( @@ -872,7 +890,7 @@ def test_scaledl2normPool2d(test_params): "layers": [ tInput(uft.to_framework_channel((1, 5, 5))), uft.get_instance_framework( - ScaledGlobalL2NormPool2d, {"data_format": "channels_last"} + ScaledAdaptativeL2NormPool2d, {"data_format": "channels_last"} ), ] }, @@ -890,7 +908,7 @@ def test_scaledl2normPool2d(test_params): "layers": [ tInput(uft.to_framework_channel((1, 5, 5))), uft.get_instance_framework( - ScaledGlobalL2NormPool2d, {"data_format": "channels_last"} + ScaledAdaptativeL2NormPool2d, {"data_format": "channels_last"} ), ] }, @@ -908,7 +926,7 @@ def test_scaledl2normPool2d(test_params): "layers": [ tInput(uft.to_framework_channel((1, 5, 5))), uft.get_instance_framework( - ScaledGlobalL2NormPool2d, {"data_format": "channels_last"} + ScaledAdaptativeL2NormPool2d, {"data_format": "channels_last"} ), ] }, @@ -1125,7 +1143,7 @@ def test_callbacks(test_params): [ dict( layer_type=InvertibleDownSampling, - layer_params={"kernel_size": (2, 3)}, + layer_params={"kernel_size": 3}, batch_size=250, steps_per_epoch=1, epochs=5, @@ -1146,7 +1164,7 @@ def test_invertibledownsampling(test_params): [ dict( layer_type=InvertibleUpSampling, - layer_params={"kernel_size": (2, 3)}, + layer_params={"kernel_size": 3}, batch_size=250, steps_per_epoch=1, epochs=5, @@ -1163,15 +1181,15 @@ def test_invertibleupsampling(test_params): @pytest.mark.skipif( - hasattr(SpectralConv2dTranspose, "unavailable_class"), - reason="SpectralConv2dTranspose not available", + hasattr(SpectralConvTranspose2d, "unavailable_class"), + reason="SpectralConvTranspose2d not available", ) @pytest.mark.parametrize( "test_params,msg", [ (dict(in_channels=1, out_channels=5, kernel_size=3), ""), ( - dict(in_channels=1, out_channels=12, kernel_size=5, strides=2, bias=False), + dict(in_channels=1, out_channels=12, kernel_size=5, stride=2, bias=False), "", ), ( @@ -1180,7 +1198,7 @@ def test_invertibleupsampling(test_params): out_channels=3, kernel_size=3, padding="same", - dilation_rate=1, + dilation=1, ), "", ), @@ -1214,7 +1232,7 @@ def test_invertibleupsampling(test_params): "Wrong padding", ), ( - dict(in_channels=1, out_channels=10, kernel_size=3, dilation_rate=2), + dict(in_channels=1, out_channels=10, kernel_size=3, dilation=2), "Wrong dilation rate", ), ( @@ -1223,34 +1241,193 @@ def test_invertibleupsampling(test_params): ), ], ) -def test_SpectralConv2dTranspose_instantiation(test_params, msg): +def test_SpectralConvTranspose2d_instantiation(test_params, msg): if msg == "": - uft.get_instance_framework(SpectralConv2dTranspose, test_params) + uft.get_instance_framework(SpectralConvTranspose2d, test_params) else: with pytest.raises(ValueError): - uft.get_instance_framework(SpectralConv2dTranspose, test_params) + uft.get_instance_framework(SpectralConvTranspose2d, test_params) + + +@pytest.mark.skipif( + hasattr(SpectralConv1d, "unavailable_class"), + reason="SpectralConv1d not available", +) +@pytest.mark.parametrize( + "pad_mode", + [ + "zeros", + "reflect", + "circular", + "symmetric", + ], +) +@pytest.mark.parametrize( + "pad, kernel_size", + [ + (1, (3,)), + (2, (5,)), + ], +) +@pytest.mark.parametrize( + "layer_type", + [ + SpectralConv1d, + ], +) +@pytest.mark.parametrize( + "layer_params", + [ + { + "in_channels": 1, + "out_channels": 2, + "bias": False, + }, + {"in_channels": 1, "out_channels": 2}, + ], +) +def test_SpectralConv1d_vanilla_export( + pad, pad_mode, kernel_size, layer_params, layer_type +): + layer_params["padding"] = pad + layer_params["padding_mode"] = pad_mode + layer_params["kernel_size"] = kernel_size + layer_type = layer_type + input_shape = (1, 5) + + model = uft.generate_k_lip_model(layer_type, layer_params, input_shape, 1.0) + + # lay = SpectralConvTranspose2d(**kwargs) + # model = Sequential([lay]) + x = np.random.normal(size=(5,) + input_shape) + + x = uft.to_tensor(x) + y1 = model(x) + + # Test vanilla export inference comparison + if uft.vanilla_require_a_copy(): + model2 = uft.generate_k_lip_model(layer_type, layer_params, input_shape, 1.0) + uft.copy_model_parameters(model, model2) + vanilla_model = uft.vanillaModel(model2) + else: + vanilla_model = uft.vanillaModel(model) # .vanilla_export() + y2 = vanilla_model(x) + np.testing.assert_allclose(uft.to_numpy(y1), uft.to_numpy(y2), atol=1e-6) + + # Test saving/loading model + with tempfile.TemporaryDirectory() as tmpdir: + uft.MODEL_PATH = os.path.join(tmpdir, uft.MODEL_PATH) + uft.save_model(model, uft.MODEL_PATH, overwrite=True) + uft.load_model( + uft.MODEL_PATH, + layer_type=layer_type, + layer_params=layer_params, + input_shape=input_shape, + k=1.0, + ) + + +@pytest.mark.skipif( + hasattr(SpectralConv2d, "unavailable_class"), + reason="SpectralConv2d not available", +) +@pytest.mark.parametrize( + "pad_mode", + [ + "zeros", + "reflect", + "circular", + "symmetric", + ], +) +@pytest.mark.parametrize( + "pad, kernel_size", + [ + (1, (3, 3)), + ((1, 1), (3, 3)), + (2, (5, 5)), + ((2, 2), (5, 5)), + ], +) +@pytest.mark.parametrize( + "layer_type", + [ + SpectralConv2d, + FrobeniusConv2d, + ], +) +@pytest.mark.parametrize( + "layer_params", + [ + { + "in_channels": 1, + "out_channels": 2, + "bias": False, + }, + {"in_channels": 1, "out_channels": 2}, + ], +) +def test_Conv2d_vanilla_export(pad, pad_mode, kernel_size, layer_params, layer_type): + layer_params["padding"] = pad + layer_params["padding_mode"] = pad_mode + layer_params["kernel_size"] = kernel_size + layer_type = layer_type + input_shape = (1, 5, 5) + + if not uft.is_supported_padding(pad_mode, layer_type): + pytest.skip(f"{layer_type}: Padding {pad_mode} not supported") + model = uft.generate_k_lip_model(layer_type, layer_params, input_shape, 1.0) + + # lay = SpectralConvTranspose2d(**kwargs) + # model = Sequential([lay]) + x = np.random.normal(size=(5,) + input_shape) + + x = uft.to_tensor(x) + y1 = model(x) + + # Test vanilla export inference comparison + if uft.vanilla_require_a_copy(): + model2 = uft.generate_k_lip_model(layer_type, layer_params, input_shape, 1.0) + uft.copy_model_parameters(model, model2) + vanilla_model = uft.vanillaModel(model2) + else: + vanilla_model = uft.vanillaModel(model) # .vanilla_export() + y2 = vanilla_model(x) + np.testing.assert_allclose(uft.to_numpy(y1), uft.to_numpy(y2), atol=1e-6) + + # Test saving/loading model + with tempfile.TemporaryDirectory() as tmpdir: + uft.MODEL_PATH = os.path.join(tmpdir, uft.MODEL_PATH) + uft.save_model(model, uft.MODEL_PATH, overwrite=True) + uft.load_model( + uft.MODEL_PATH, + layer_type=layer_type, + layer_params=layer_params, + input_shape=input_shape, + k=1.0, + ) @pytest.mark.skipif( - hasattr(SpectralConv2dTranspose, "unavailable_class"), - reason="SpectralConv2dTranspose not available", + hasattr(SpectralConvTranspose2d, "unavailable_class"), + reason="SpectralConvTranspose2d not available", ) -def test_SpectralConv2dTranspose_vanilla_export(): +def test_SpectralConvTranspose2d_vanilla_export(): kwargs = dict( in_channels=3, out_channels=16, kernel_size=5, - strides=2, + stride=2, activation="relu", data_format="channels_first", input_shape=(3, 28, 28), ) model = uft.generate_k_lip_model( - SpectralConv2dTranspose, kwargs, kwargs["input_shape"], 1.0 + SpectralConvTranspose2d, kwargs, kwargs["input_shape"], 1.0 ) - # lay = SpectralConv2dTranspose(**kwargs) + # lay = SpectralConvTranspose2d(**kwargs) # model = Sequential([lay]) x = np.random.normal(size=(5,) + kwargs["input_shape"]) @@ -1258,17 +1435,24 @@ def test_SpectralConv2dTranspose_vanilla_export(): y1 = model(x) # Test vanilla export inference comparison - vanilla_model = model.vanilla_export() + if uft.vanilla_require_a_copy(): + model2 = uft.generate_k_lip_model( + SpectralConvTranspose2d, kwargs, kwargs["input_shape"], 1.0 + ) + uft.copy_model_parameters(model, model2) + vanilla_model = uft.vanillaModel(model2) + else: + vanilla_model = uft.vanillaModel(model) # .vanilla_export() y2 = vanilla_model(x) - np.testing.assert_allclose(y1, y2, atol=1e-6) + np.testing.assert_allclose(uft.to_numpy(y1), uft.to_numpy(y2), atol=1e-6) # Test saving/loading model with tempfile.TemporaryDirectory() as tmpdir: uft.MODEL_PATH = os.path.join(tmpdir, uft.MODEL_PATH) - model.save(uft.MODEL_PATH) + uft.save_model(model, uft.MODEL_PATH, overwrite=True) uft.load_model( uft.MODEL_PATH, - layer_type=SpectralConv2dTranspose, + layer_type=SpectralConvTranspose2d, layer_params=kwargs, input_shape=kwargs["input_shape"], k=1.0, diff --git a/tests/test_losses.py b/tests/test_losses.py index 12e9746..b0cd03d 100644 --- a/tests/test_losses.py +++ b/tests/test_losses.py @@ -228,7 +228,6 @@ def test_loss_generic_value( y_true, y_pred = uft.to_tensor(y_true_np), uft.to_tensor(y_pred_np) loss_val = uft.compute_loss(loss, y_pred, y_true).numpy() - print("loss_val", loss_val, expected_loss) np.testing.assert_allclose( loss_val, np.float32(expected_loss), diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 0be592c..62946a6 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -180,7 +180,6 @@ def test_provable_vs_adjusted(loss, loss_params, nb_class): l1 = pr(y, x).numpy() l2 = ar(y, x).numpy() - print(l1, l2) diff = np.min(np.abs(l1 - l2)) assert ( diff > 1e-4 diff --git a/tests/test_normalization.py b/tests/test_normalization.py new file mode 100644 index 0000000..a6ffbed --- /dev/null +++ b/tests/test_normalization.py @@ -0,0 +1,269 @@ +# -*- coding: utf-8 -*- +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# ===================================================================================== +import os +import pytest + +import numpy as np + +from . import utils_framework as uft + +from .utils_framework import BatchCentering, LayerCentering + + +def check_serialization(layer_type, layer_params, input_shape=(10,)): + m = uft.generate_k_lip_model(layer_type, layer_params, input_shape=input_shape, k=1) + if m is None: + pytest.skip() + loss, optimizer, _ = uft.compile_model( + m, + optimizer=uft.get_instance_framework(uft.SGD, inst_params={"model": m}), + loss=uft.CategoricalCrossentropy(from_logits=True), + ) + name = layer_type.__class__.__name__ + path = os.path.join("logs", "normalization", name) + xnp = np.random.uniform(-10, 10, (255,) + input_shape) + x = uft.to_tensor(xnp) + y1 = m(x) + uft.save_model(m, path) + m2 = uft.load_model( + path, + compile=True, + layer_type=layer_type, + layer_params=layer_params, + input_shape=input_shape, + k=1, + ) + y2 = m2(x) + np.testing.assert_allclose(uft.to_numpy(y1), uft.to_numpy(y2)) + + +@pytest.mark.skipif( + hasattr(LayerCentering, "unavailable_class"), + reason="LayerCentering not available", +) +@pytest.mark.parametrize( + "size, input_shape, bias", + [ + (4, (3, 4, 8, 8), False), + (4, (3, 4, 8, 8), True), + ], +) +def test_LayerCentering(size, input_shape, bias): + """evaluate layerbatch centering""" + input_shape = uft.to_framework_channel(input_shape) + x = np.arange(np.prod(input_shape)).reshape(input_shape) + bn = uft.get_instance_framework(LayerCentering, {"size": size, "bias": bias}) + + mean_x = np.mean(x, axis=(2, 3)) + mean_shape = (-1, size, 1, 1) + x = uft.to_tensor(x) + y = bn(x) + np.testing.assert_allclose( + uft.to_numpy(y), x - np.reshape(mean_x, mean_shape), atol=1e-5 + ) + y = bn(2 * x) + np.testing.assert_allclose( + uft.to_numpy(y), 2 * x - 2 * np.reshape(mean_x, mean_shape), atol=1e-5 + ) # keep substract batch mean + bn.eval() + y = bn(2 * x) + np.testing.assert_allclose( + uft.to_numpy(y), 2 * x - 2 * np.reshape(mean_x, mean_shape), atol=1e-5 + ) # eval mode use running_mean + + +@pytest.mark.skipif( + hasattr(BatchCentering, "unavailable_class"), + reason="BatchCentering not available", +) +@pytest.mark.parametrize( + "size, input_shape, bias", + [ + (4, (3, 4), False), + (4, (3, 4), True), + (4, (3, 4, 8, 8), False), + (4, (3, 4, 8, 8), True), + ], +) +def test_BatchCentering(size, input_shape, bias): + """evaluate layerbatch centering""" + input_shape = uft.to_framework_channel(input_shape) + x = np.arange(np.prod(input_shape)).reshape(input_shape) + bn = uft.get_instance_framework(BatchCentering, {"size": size, "bias": bias}) + bn_mom = bn.momentum + if len(input_shape) == 2: + mean_x = np.mean(x, axis=0) + mean_shape = (1, size) + else: + mean_x = np.mean(x, axis=(0, 2, 3)) + mean_shape = (1, size, 1, 1) + x = uft.to_tensor(x) + y = bn(x) + np.testing.assert_allclose(bn.running_mean, mean_x, atol=1e-5) + np.testing.assert_allclose( + uft.to_numpy(y), x - np.reshape(mean_x, mean_shape), atol=1e-5 + ) + y = bn(2 * x) + new_runningmean = mean_x * (1 - bn_mom) + 2 * mean_x * bn_mom + np.testing.assert_allclose(bn.running_mean, new_runningmean, atol=1e-5) + np.testing.assert_allclose( + uft.to_numpy(y), 2 * x - 2 * np.reshape(mean_x, mean_shape), atol=1e-5 + ) # keep substract batch mean + bn.eval() + y = bn(2 * x) + np.testing.assert_allclose( + bn.running_mean, new_runningmean, atol=1e-5 + ) # eval mode running mean freezed + np.testing.assert_allclose( + uft.to_numpy(y), 2 * x - np.reshape(new_runningmean, mean_shape), atol=1e-5 + ) # eval mode use running_mean + + +@pytest.mark.parametrize( + "norm_type", + [LayerCentering, BatchCentering], +) +@pytest.mark.parametrize( + "size, input_shape, bias", + [ + (10, (10,), False), + (10, (10,), True), + (7, (7, 8, 8), False), + (7, (7, 8, 8), True), + ], +) +def test_Normalization_serialization(norm_type, size, input_shape, bias): + # Check serialization + if hasattr(norm_type, "unavailable_class"): + pytest.skip(f"{norm_type} not available") + check_serialization( + norm_type, layer_params={"size": size, "bias": bias}, input_shape=input_shape + ) + + +def linear_generator(batch_size, input_shape: tuple): + """ + Generate data according to a linear kernel + Args: + batch_size: size of each batch + input_shape: shape of the desired input + + Returns: + a generator for the data + + """ + input_shape = tuple(input_shape) + while True: + # pick random sample in [0, 1] with the input shape + batch_x = np.array( + np.random.uniform(-10, 10, (batch_size,) + input_shape), dtype=np.float16 + ) + # apply the k lip linear transformation + batch_y = batch_x + yield batch_x, batch_y + + +@pytest.mark.parametrize( + "norm_type", + [LayerCentering, BatchCentering], +) +@pytest.mark.parametrize( + "size, input_shape, bias", + [ + (10, (10,), True), + (7, (7, 8, 8), True), + ], +) +def test_Normalization_bias(norm_type, size, input_shape, bias): + if hasattr(norm_type, "unavailable_class"): + pytest.skip(f"{norm_type} not available") + m = uft.generate_k_lip_model( + norm_type, + layer_params={"size": size, "bias": bias}, + input_shape=input_shape, + k=1, + ) + if m is None: + pytest.skip() + loss, optimizer, _ = uft.compile_model( + m, + optimizer=uft.get_instance_framework(uft.SGD, inst_params={"model": m}), + loss=uft.CategoricalCrossentropy(from_logits=True), + ) + batch_size = 10 + bb = uft.to_numpy(uft.get_layer_by_index(m, 0).bias) + np.testing.assert_allclose(bb, np.zeros((size,)), atol=1e-5) + + traind_ds = linear_generator(batch_size, input_shape) + uft.train( + traind_ds, + m, + loss, + optimizer, + 2, + batch_size, + steps_per_epoch=10, + ) + + bb = uft.to_numpy(uft.get_layer_by_index(m, 0).bias) + assert np.linalg.norm(bb) != 0.0 + + +@pytest.mark.skipif( + hasattr(BatchCentering, "unavailable_class"), + reason="BatchCentering not available", +) +@pytest.mark.parametrize( + "size, input_shape, bias", + [ + (4, (3, 4), False), + (4, (3, 4), True), + (4, (3, 4, 8, 8), False), + (4, (3, 4, 8, 8), True), + ], +) +def test_BatchCentering_runningmean(size, input_shape, bias): + """evaluate batch centering convergence of running mean""" + input_shape = uft.to_framework_channel(input_shape) + # start with 0 to set up running mean to zero + x = np.zeros(input_shape) + bn = uft.get_instance_framework(BatchCentering, {"size": size, "bias": bias}) + x = uft.to_tensor(x) + y = bn(x) + + np.testing.assert_allclose(bn.running_mean, 0.0, atol=1e-5) + + x = np.random.normal(0.0, 1.0, input_shape) + if len(input_shape) == 2: + mean_x = np.mean(x, axis=0) + else: + mean_x = np.mean(x, axis=(0, 2, 3)) + x = uft.to_tensor(x) + for _ in range(1000): + y = bn(x) # noqa: F841 + + np.testing.assert_allclose(bn.running_mean, mean_x, atol=1e-5) diff --git a/tests/test_normalizers.py b/tests/test_normalizers.py index 4294a4b..26d35e6 100644 --- a/tests/test_normalizers.py +++ b/tests/test_normalizers.py @@ -64,7 +64,6 @@ ) def test_kernel_svd(kernel_shape): """Compare max singular value using power iteration and np.linalg.svd""" - print(kernel_shape) kernel = rng.normal(size=kernel_shape).astype("float32") sigmas_svd = np.linalg.svd( np.reshape(kernel, (np.prod(kernel.shape[:-1]), kernel.shape[-1])), diff --git a/tests/test_pooling.py b/tests/test_pooling.py new file mode 100644 index 0000000..2635c92 --- /dev/null +++ b/tests/test_pooling.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# ===================================================================================== +import pytest +import os +import math + +import numpy as np +from . import utils_framework as uft +from .utils_framework import ( + CategoricalCrossentropy, + ScaledAvgPool2d, + ScaledAdaptiveAvgPool2d, + ScaledL2NormPool2d, + ScaledAdaptativeL2NormPool2d, +) + + +def check_serialization(layer_type, layer_params): + input_shape = (4, 10, 10) + input_shape = uft.to_framework_channel(input_shape) + m = uft.generate_k_lip_model(layer_type, layer_params, input_shape=input_shape, k=1) + if m is None: + return + loss, optimizer, _ = uft.compile_model( + m, + optimizer=uft.get_instance_framework(uft.SGD, inst_params={"model": m}), + loss=CategoricalCrossentropy(from_logits=True), + ) + name = layer_type.__class__.__name__ + path = os.path.join("logs", "pooling", name) + xnp = np.random.uniform(-10, 10, (255,) + input_shape) + x = uft.to_tensor(xnp) + y1 = m(x) + uft.save_model(m, path) + m2 = uft.load_model( + path, + compile=True, + layer_type=layer_type, + layer_params=layer_params, + input_shape=input_shape, + k=1, + ) + y2 = m2(x) + np.testing.assert_allclose(uft.to_numpy(y1), uft.to_numpy(y2)) + + +@pytest.mark.parametrize( + "layer_type", + [ + ScaledAvgPool2d, + ScaledL2NormPool2d, + ], +) +@pytest.mark.parametrize( + "layer_params", + [ + {"kernel_size": 2}, + {"kernel_size": (5, 5)}, + {"kernel_size": 2, "k_coef_lip": 2.5}, + {"kernel_size": (2, 2), "stride": (2, 2)}, + ], +) +def test_pooling_simple(layer_type, layer_params): + check_serialization(layer_type, layer_params) + + +@pytest.mark.parametrize( + "layer_type", + [ + ScaledAdaptiveAvgPool2d, + ScaledAdaptativeL2NormPool2d, + ], +) +@pytest.mark.parametrize( + "layer_params", + [ + {"output_size": (1, 1)}, + {"output_size": (1, 1), "k_coef_lip": 2.5}, + ], +) +def test_pooling_global(layer_type, layer_params): + check_serialization(layer_type, layer_params) + + +@pytest.mark.parametrize( + "layer_type,layer_params", + [ + ( + ScaledAvgPool2d, + {"kernel_size": 2}, + ), + ( + ScaledAvgPool2d, + {"kernel_size": (5, 5)}, + ), + ( + ScaledAvgPool2d, + {"kernel_size": 2, "k_coef_lip": 2.5}, + ), + (ScaledAvgPool2d, {"kernel_size": (2, 2), "stride": (2, 2)}), + ( + ScaledL2NormPool2d, + {"kernel_size": 2}, + ), + ( + ScaledL2NormPool2d, + {"kernel_size": (5, 5)}, + ), + ( + ScaledL2NormPool2d, + {"kernel_size": 2, "k_coef_lip": 2.5}, + ), + (ScaledL2NormPool2d, {"kernel_size": (2, 2), "stride": (2, 2)}), + (ScaledAdaptiveAvgPool2d, {"output_size": (1, 1)}), + (ScaledAdaptiveAvgPool2d, {"output_size": (1, 1), "k_coef_lip": 2.5}), + (ScaledAdaptativeL2NormPool2d, {"output_size": (1, 1)}), + (ScaledAdaptativeL2NormPool2d, {"output_size": (1, 1), "k_coef_lip": 2.5}), + ], +) +def test_pool_vanilla_export(layer_type, layer_params): + + input_shape = (4, 10, 10) + input_shape = uft.to_framework_channel(input_shape) + model = uft.generate_k_lip_model(layer_type, layer_params, input_shape, 1.0) + + # lay = SpectralConvTranspose2d(**kwargs) + # model = Sequential([lay]) + x = np.random.normal(size=(5,) + input_shape) + + x = uft.to_tensor(x) + y1 = model(x) + + # Test vanilla export inference comparison + if uft.vanilla_require_a_copy(): + model2 = uft.generate_k_lip_model(layer_type, layer_params, input_shape, 1.0) + uft.copy_model_parameters(model, model2) + vanilla_model = uft.vanillaModel(model2) + else: + vanilla_model = uft.vanillaModel(model) # .vanilla_export() + y2 = vanilla_model(x) + np.testing.assert_allclose(uft.to_numpy(y1), uft.to_numpy(y2), atol=1e-6) + + +@pytest.mark.parametrize( + "layer_type, layer_params, expected", + [ + ( + ScaledAvgPool2d, + {"kernel_size": 2}, + [ + [ + [ + [10.0 / math.sqrt(2.0 * 2.0), 10.0 / math.sqrt(2.0 * 2.0)], + [10.0 / math.sqrt(2.0 * 2.0), 10.0 / math.sqrt(2.0 * 2.0)], + ], + [ + [10.0 / math.sqrt(2.0 * 2.0), -2.0 / math.sqrt(2.0 * 2.0)], + [1.0 / math.sqrt(2.0 * 2.0), 9.0 / math.sqrt(2.0 * 2.0)], + ], + ] + ], + ), + ( + ScaledL2NormPool2d, + {"kernel_size": 2}, + [ + [ + [ + [math.sqrt(30.0), math.sqrt(30.0)], + [math.sqrt(30.0), math.sqrt(30.0)], + ], + [ + [math.sqrt(74.0), math.sqrt(22.0)], + [math.sqrt(129.0), math.sqrt(95.0)], + ], + ] + ], + ), + ( + ScaledAdaptiveAvgPool2d, + {"output_size": (1, 1)}, + [[[[40.0 / math.sqrt(4.0 * 4.0)]], [[18.0 / math.sqrt(4.0 * 4.0)]]]], + ), + ( + ScaledAdaptativeL2NormPool2d, + {"output_size": (1, 1)}, + [[[[math.sqrt(120.0)]], [[math.sqrt(320.0)]]]], + ), + ], +) +def test_AvgPooling(layer_type, layer_params, expected): + pool = uft.get_instance_framework(layer_type, layer_params) + if pool is None: + return + input = [ + [ # input shape (bc,c,h,w) = (1,2,4,4) + [ + [1.0, 2.0, 3.0, 4.0], + [3.0, 4.0, 1.0, 2.0], + [1.0, 2.0, 3.0, 4.0], + [3.0, 4.0, 1.0, 2.0], + ], + [ + [6.0, 2.0, 1.0, -4.0], + [5.0, -3.0, -1.0, 2.0], + [10.0, -2.0, 3.0, 9.0], + [-3.0, -4.0, -1.0, -2.0], + ], + ] + ] + xnp = np.asarray(input) + xnp = uft.to_NCHW_inv(xnp) # move channel if needed (TF) + x = uft.to_tensor(xnp) + print(x.shape) + uft.build_layer(pool, x.shape[1:]) + + y = pool(x).numpy() + y = np.squeeze(y) # yorch keep dim whereas tf not + y_tnp = np.asarray(expected) + y_t = uft.to_NCHW_inv(y_tnp) # move channel if needed (TF) + y_t = np.squeeze(y_t) + np.testing.assert_almost_equal(y, y_t, decimal=5) diff --git a/tests/test_residual.py b/tests/test_residual.py new file mode 100644 index 0000000..b8fa117 --- /dev/null +++ b/tests/test_residual.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# ===================================================================================== +import os +import pytest + +import numpy as np + +from . import utils_framework as uft + +from .utils_framework import LipResidual +from .utils_framework import tInput, tSplit, tModel + + +def get_functional_tensors(input_shape): + dict_functional_tensors = {} + dict_functional_tensors["inputs"] = uft.get_instance_framework( + tInput, {"shape": input_shape} + ) + dict_functional_tensors["split"] = uft.get_instance_framework( + tSplit, {"chunks": 2, "dim": 1} + ) + dict_functional_tensors["residual"] = uft.get_instance_framework(LipResidual, {}) + return dict_functional_tensors + + +def functional_input_output_tensors(dict_functional_tensors, x): + """Return input and output tensor of a Functional (hard-coded) model""" + if dict_functional_tensors["inputs"] is None: + inputs = x + else: + inputs = dict_functional_tensors["inputs"] + x = dict_functional_tensors["split"](inputs) + outputs = dict_functional_tensors["residual"](x[0], x[1]) + if dict_functional_tensors["inputs"] is None: + return outputs + else: + return inputs, outputs + # return x + + +def check_serialization(layer_type, layer_params, input_shape=(10,)): + + dict_tensors = get_functional_tensors(input_shape) + m = uft.get_functional_model(tModel, dict_tensors, functional_input_output_tensors) + if m is None: + pytest.skip() + loss, optimizer, _ = uft.compile_model( + m, + optimizer=uft.get_instance_framework(uft.SGD, inst_params={"model": m}), + loss=uft.MeanSquaredError(), + ) + name = layer_type.__class__.__name__ + path = os.path.join("logs", "residual", name) + xnp = np.random.uniform(-10, 10, (255,) + input_shape) + x = uft.to_tensor(xnp) + y1 = m(x) + uft.save_model(m, path) + + # build and generate the model + if uft.vanilla_require_a_copy(): + dict_tensors2 = get_functional_tensors(input_shape) + m2 = uft.get_functional_model( + tModel, dict_tensors2, functional_input_output_tensors + ) + m2 = uft.load_state_dict(path, m2) + else: + m2 = uft.load_model( + path, + compile=True, + layer_type=layer_type, + layer_params=layer_params, + input_shape=input_shape, + k=1, + ) + y2 = m2(x) + np.testing.assert_allclose(uft.to_numpy(y1), uft.to_numpy(y2)) + + +@pytest.mark.skipif( + hasattr(LipResidual, "unavailable_class"), + reason="LipResidual not available", +) +@pytest.mark.parametrize( + "input_shape", + [ + (3, 4, 8, 8), + ], +) +def test_initLipResidual(input_shape): + """evaluate layerbatch centering""" + input_shape = uft.to_framework_channel(input_shape) + x1 = np.arange(np.prod(input_shape)).reshape(input_shape) + x2 = np.zeros(input_shape) + res = uft.get_instance_framework(LipResidual, {}) + + alpha_res = uft.to_numpy(res.alpha) + assert alpha_res == 0.0 + z = res(uft.to_tensor(x1), uft.to_tensor(x1)) + np.testing.assert_allclose(uft.to_numpy(z), x1, atol=1e-5) + z = res(uft.to_tensor(x1), uft.to_tensor(x2)) + np.testing.assert_allclose(uft.to_numpy(z), x1 / 2.0, atol=1e-5) + + +@pytest.mark.skipif( + hasattr(LipResidual, "unavailable_class"), + reason="LipResidual not available", +) +@pytest.mark.parametrize( + "input_shape", + [ + (14, 8, 8), + ], +) +def test_Normalization_serialization(input_shape): + # Check serialization + check_serialization(LipResidual, layer_params={}, input_shape=input_shape) + + +def linear_generator(batch_size, input_shape: tuple, input_type: str): + """ + Generate data according to a linear kernel + Args: + batch_size: size of each batch + input_shape: shape of the desired input + input_type: duplication type for residual + + Returns: + a generator for the data + + """ + input_shape = tuple( + [sh // 2 if id == 0 else sh for id, sh in enumerate(input_shape)] + ) + while True: + # pick random sample in [0, 1] with the input shape + batch_x = np.array( + np.random.uniform(-10, 10, (batch_size,) + input_shape), dtype=np.float16 + ) + # same output as input + batch_y = batch_x + # concatenate to use split + if input_type == "zeros": + batch_x = np.concatenate([batch_x, np.zeros_like(batch_x)], axis=1) + if input_type == "invert": + batch_x = np.concatenate([np.zeros_like(batch_x), batch_x], axis=1) + if input_type == "copy": + batch_x = np.concatenate([batch_x, batch_x], axis=1) + if input_type == "random": + batch_x = np.concatenate( + [ + batch_x, + np.array( + np.random.uniform(-10, 10, (batch_size,) + input_shape), + dtype=np.float16, + ), + ], + axis=1, + ) + yield batch_x, batch_y + + +def sigmoid(z): + return 1 / (1 + np.exp(-z)) + + +@pytest.mark.skipif( + hasattr(LipResidual, "unavailable_class"), + reason="LipResidual not available", +) +@pytest.mark.parametrize( + "input_shape, input_type, learnt_alpha", + [ + ((14, 8, 8), "zeros", 1.0), # x1=x x2=0 + ((14, 8, 8), "copy", 0.5), # x1=x2=x + ((14, 8, 8), "invert", 0.0), # x1=0 x2=x + ((14, 8, 8), "random", None), # x1=x1 x2=x2 + ], +) +def test_learntResidual(input_shape, input_type, learnt_alpha): + dict_tensors = get_functional_tensors(input_shape) + m = uft.get_functional_model(tModel, dict_tensors, functional_input_output_tensors) + if m is None: + pytest.skip() + loss, optimizer, _ = uft.compile_model( + m, + optimizer=uft.get_instance_framework(uft.SGD, inst_params={"model": m}), + loss=uft.MeanSquaredError(), + ) + batch_size = 10 + + traind_ds = linear_generator(batch_size, input_shape, input_type) + uft.train( + traind_ds, + m, + loss, + optimizer, + 5, + batch_size, + steps_per_epoch=100, + ) + + alpha = uft.to_numpy(m.get_module_by_name("residual").alpha) + if learnt_alpha is not None: + np.testing.assert_allclose(sigmoid(alpha), learnt_alpha, atol=1e-1) + else: + assert alpha != 0.0 diff --git a/tests/test_unconstrained_layers.py b/tests/test_unconstrained_layers.py index 69b829d..cd99255 100644 --- a/tests/test_unconstrained_layers.py +++ b/tests/test_unconstrained_layers.py @@ -37,8 +37,8 @@ def compare(x, x_ref, index_x=[], index_x_ref=[]): """Compare a tensor and its padded version, based on index_x and ref.""" - x = uft.to_numpy(uft.to_NCHW(x)) - x_ref = uft.to_numpy(uft.to_NCHW(x_ref)) + x = uft.to_NCHW(uft.to_numpy(x)) + x_ref = uft.to_NCHW(uft.to_numpy(x_ref)) x_cropped = x[:, :, index_x[0] : index_x[1], index_x[3] : index_x[4]][ :, :, :: index_x[2], :: index_x[5] ] @@ -46,17 +46,18 @@ def compare(x, x_ref, index_x=[], index_x_ref=[]): np.testing.assert_allclose(x_cropped, np.zeros(x_cropped.shape), 1e-2, 0) else: np.testing.assert_allclose( - x_cropped, - x_ref[ + x_cropped + - x_ref[ :, :, index_x_ref[0] : index_x_ref[1], index_x_ref[3] : index_x_ref[4] ][:, :, :: index_x_ref[2], :: index_x_ref[5]], + np.zeros(x_cropped.shape), 1e-2, 0, ) @pytest.mark.parametrize( - "padding_tested", ["circular", "constant", "symmetric", "reflect"] + "padding_tested", ["circular", "constant", "symmetric", "reflect", "replicate"] ) @pytest.mark.parametrize( "input_shape, batch_size, kernel_size, filters", @@ -70,7 +71,7 @@ def compare(x, x_ref, index_x=[], index_x_ref=[]): def test_padding(padding_tested, input_shape, batch_size, kernel_size, filters): """Test different padding types: assert values in original and padded tensors""" input_shape = uft.to_framework_channel(input_shape) - if not uft.is_supported_padding(padding_tested): + if not uft.is_supported_padding(padding_tested, PadConv2d): pytest.skip(f"Padding {padding_tested} not supported") kernel_size_list = kernel_size if isinstance(kernel_size, (int, float)): @@ -90,15 +91,19 @@ def test_padding(padding_tested, input_shape, batch_size, kernel_size, filters): right_x_pad = [p_vert, -p_vert, 1, -p_hor, x_pad_NCHW[3], 1, "right"] all_x = [0, x_NCHW[2], 1, 0, x_NCHW[3], 1] upper_x = [0, p_vert, 1, 0, x_NCHW[3], 1] + upper_x_first = [0, 1, 1, 0, x_NCHW[3], 1] upper_x_rev = [0, p_vert, -1, 0, x_NCHW[3], 1] upper_x_refl = [1, p_vert + 1, -1, 0, x_NCHW[3], 1] lower_x = [-p_vert, x_NCHW[2], 1, 0, x_NCHW[3], 1] + lower_x_last = [-1, x_NCHW[2], 1, 0, x_NCHW[3], 1] lower_x_rev = [-p_vert, x_NCHW[2], -1, 0, x_NCHW[3], 1] lower_x_refl = [-p_vert - 1, x_NCHW[2] - 1, -1, 0, x_NCHW[3], 1] left_x = [0, x_NCHW[2], 1, 0, p_hor, 1] + left_x_first = [0, x_NCHW[2], 1, 0, 1, 1] left_x_rev = [0, x_NCHW[2], 1, 0, p_hor, -1] left_x_refl = [0, x_NCHW[2], 1, 1, p_hor + 1, -1] right_x = [0, x_NCHW[2], 1, -p_hor, x_NCHW[3], 1] + right_x_last = [0, x_NCHW[2], 1, -1, x_NCHW[3], 1] right_x_rev = [0, x_NCHW[2], 1, -p_hor, x_NCHW[3], -1] right_x_refl = [0, x_NCHW[2], 1, -p_hor - 1, x_NCHW[3] - 1, -1] zero_pad = [None, None, None, None] @@ -108,30 +113,35 @@ def test_padding(padding_tested, input_shape, batch_size, kernel_size, filters): "constant": [center_x_pad, all_x], "symmetric": [center_x_pad, all_x], "reflect": [center_x_pad, all_x], + "replicate": [center_x_pad, all_x], }, { "circular": [upper_x_pad, lower_x], "constant": [upper_x_pad, zero_pad], "symmetric": [upper_x_pad, upper_x_rev], "reflect": [upper_x_pad, upper_x_refl], + "replicate": [upper_x_pad, upper_x_first], }, { "circular": [lower_x_pad, upper_x], "constant": [lower_x_pad, zero_pad], "symmetric": [lower_x_pad, lower_x_rev], "reflect": [lower_x_pad, lower_x_refl], + "replicate": [lower_x_pad, lower_x_last], }, { "circular": [left_x_pad, right_x], "constant": [left_x_pad, zero_pad], "symmetric": [left_x_pad, left_x_rev], "reflect": [left_x_pad, left_x_refl], + "replicate": [left_x_pad, left_x_first], }, { "circular": [right_x_pad, left_x], "constant": [right_x_pad, zero_pad], "symmetric": [right_x_pad, right_x_rev], "reflect": [right_x_pad, right_x_refl], + "replicate": [right_x_pad, right_x_last], }, ] @@ -149,7 +159,8 @@ def test_padding(padding_tested, input_shape, batch_size, kernel_size, filters): reason="PadConv2d not available", ) @pytest.mark.parametrize( - "padding_tested", ["circular", "constant", "symmetric", "reflect", "same", "valid"] + "padding_tested", + ["circular", "constant", "symmetric", "reflect", "replicate", "same", "valid"], ) @pytest.mark.parametrize( "input_shape, batch_size, kernel_size, filters", @@ -165,7 +176,7 @@ def test_predict(padding_tested, input_shape, batch_size, kernel_size, filters): in_ch = input_shape[0] input_shape = uft.to_framework_channel(input_shape) - if not uft.is_supported_padding(padding_tested): + if not uft.is_supported_padding(padding_tested, PadConv2d): pytest.skip(f"Padding {padding_tested} not supported") layer_params = { "out_channels": 2, @@ -222,7 +233,8 @@ def test_predict(padding_tested, input_shape, batch_size, kernel_size, filters): reason="PadConv2d not available", ) @pytest.mark.parametrize( - "padding_tested", ["circular", "constant", "symmetric", "reflect", "same", "valid"] + "padding_tested", + ["circular", "constant", "symmetric", "reflect", "replicate", "same", "valid"], ) @pytest.mark.parametrize( "input_shape, batch_size, kernel_size, filters", @@ -238,7 +250,7 @@ def test_vanilla(padding_tested, input_shape, batch_size, kernel_size, filters): in_ch = input_shape[0] input_shape = uft.to_framework_channel(input_shape) - if not uft.is_supported_padding(padding_tested): + if not uft.is_supported_padding(padding_tested, PadConv2d): pytest.skip(f"Padding {padding_tested} not supported") layer_params = { "out_channels": 2, diff --git a/tests/test_updownsampling.py b/tests/test_updownsampling.py index 7f0dedb..a674f7d 100644 --- a/tests/test_updownsampling.py +++ b/tests/test_updownsampling.py @@ -29,69 +29,106 @@ import numpy as np from . import utils_framework as uft -from .utils_framework import invertible_downsample, invertible_upsample +from .utils_framework import InvertibleDownSampling, InvertibleUpSampling + + +def check_downsample(x, y, kernel_size): + index = 0 + for dx in range(kernel_size): + for dy in range(kernel_size): + xx = x[:, :, dx::kernel_size, dy::kernel_size] + yy = y[:, index :: (kernel_size * kernel_size), :, :] + np.testing.assert_almost_equal(xx, yy, decimal=6) + index += 1 @pytest.mark.skipif( - hasattr(invertible_downsample, "unavailable_class"), - reason="invertible_downsample not available", + hasattr(InvertibleDownSampling, "unavailable_class"), + reason="InvertibleDownSampling not available", ) def test_invertible_downsample(): - # 1D input - x = uft.to_tensor([[[1, 2, 3, 4], [5, 6, 7, 8]]]) - x = uft.get_instance_framework( - invertible_downsample, {"input": x, "kernel_size": (2,)} - ) - assert x.shape == (1, 4, 2) - - # TODO: Check this. - np.testing.assert_equal(uft.to_numpy(x), [[[1, 2], [3, 4], [5, 6], [7, 8]]]) + x_np = np.arange(32).reshape(1, 2, 4, 4) + x = uft.to_NCHW_inv(x_np) + x = uft.to_tensor(x) + dw_layer = uft.get_instance_framework(InvertibleDownSampling, {"kernel_size": 2}) + y = dw_layer(x) + y_np = uft.to_numpy(y) + y_np = uft.to_NCHW(y_np) + assert y_np.shape == (1, 8, 2, 2) + check_downsample(x_np, y_np, 2) # 2D input - x = np.random.rand(10, 1, 128, 128) # torch.rand(10, 1, 128, 128) + x_np = np.random.rand(10, 1, 128, 128) # torch.rand(10, 1, 128, 128) + x = uft.to_NCHW_inv(x_np) x = uft.to_tensor(x) - assert invertible_downsample(x, (4, 4)).shape == (10, 16, 32, 32) - x = np.random.rand(10, 4, 64, 64) - x = uft.to_tensor(x) - assert invertible_downsample(x, (2, 2)).shape == (10, 16, 32, 32) + dw_layer = uft.get_instance_framework(InvertibleDownSampling, {"kernel_size": 4}) + y = dw_layer(x) + y_np = uft.to_numpy(y) + y_np = uft.to_NCHW(y_np) + assert y_np.shape == (10, 16, 32, 32) + check_downsample(x_np, y_np, 4) - # 3D input - x = np.random.rand(10, 2, 128, 64, 64) + x_np = np.random.rand(10, 4, 64, 64) + x = uft.to_NCHW_inv(x_np) x = uft.to_tensor(x) - assert invertible_downsample(x, 2).shape == (10, 16, 64, 32, 32) + dw_layer = uft.get_instance_framework(InvertibleDownSampling, {"kernel_size": 2}) + y = dw_layer(x) + y_np = uft.to_numpy(y) + y_np = uft.to_NCHW(y_np) + assert y_np.shape == (10, 16, 32, 32) + check_downsample(x_np, y_np, 2) @pytest.mark.skipif( - hasattr(invertible_upsample, "unavailable_class"), - reason="invertible_upsample not available", + hasattr(InvertibleUpSampling, "unavailable_class"), + reason="InvertibleUpSampling not available", ) def test_invertible_upsample(): - # 1D input - x = uft.to_tensor([[[1, 2], [3, 4], [5, 6], [7, 8]]]) - x = uft.get_instance_framework( - invertible_upsample, {"input": x, "kernel_size": (2,)} - ) - assert x.shape == (1, 2, 4) + # 2D input + x_np = np.random.rand(10, 16, 32, 32) + x = uft.to_NCHW_inv(x_np) + x = uft.to_tensor(x) + dw_layer = uft.get_instance_framework(InvertibleUpSampling, {"kernel_size": 4}) + y = dw_layer(x) + y_np = uft.to_numpy(y) + y_np = uft.to_NCHW(y_np) + assert y_np.shape == (10, 1, 128, 128) + check_downsample(y_np, x_np, 4) + + dw_layer = uft.get_instance_framework(InvertibleUpSampling, {"kernel_size": 2}) + y = dw_layer(x) + y_np = uft.to_numpy(y) + y_np = uft.to_NCHW(y_np) + assert y_np.shape == (10, 4, 64, 64) + check_downsample(y_np, x_np, 2) - # Check output. - np.testing.assert_equal(uft.to_numpy(x), [[[1, 2, 3, 4], [5, 6, 7, 8]]]) - # 2D input - x = np.random.rand(10, 16, 32, 32) +@pytest.mark.skipif( + hasattr(InvertibleUpSampling, "unavailable_class") + or hasattr(InvertibleDownSampling, "unavailable_class"), + reason="InvertibleUpSampling not available", +) +def test_invertible_upsample_downsample(): + x_np = np.random.rand(10, 16, 32, 32) + x = uft.to_NCHW_inv(x_np) x = uft.to_tensor(x) - y = uft.get_instance_framework( - invertible_upsample, {"input": x, "kernel_size": (4, 4)} - ) - assert y.shape == (10, 1, 128, 128) - y = uft.get_instance_framework( - invertible_upsample, {"input": x, "kernel_size": (2, 2)} - ) - assert y.shape == (10, 4, 64, 64) - - # 3D input - x = np.random.rand(10, 16, 64, 32, 32) + up_layer = uft.get_instance_framework(InvertibleUpSampling, {"kernel_size": 4}) + y = up_layer(x) + + dw_layer = uft.get_instance_framework(InvertibleDownSampling, {"kernel_size": 4}) + z = dw_layer(y) + assert z.shape == x.shape + np.testing.assert_array_equal(x, z) + + x_np = np.random.rand(10, 1, 128, 128) # torch.rand(10, 1, 128, 128) + x = uft.to_NCHW_inv(x_np) x = uft.to_tensor(x) - y = uft.get_instance_framework(invertible_upsample, {"input": x, "kernel_size": 2}) - assert y.shape == (10, 2, 128, 64, 64) + + dw_layer = uft.get_instance_framework(InvertibleDownSampling, {"kernel_size": 4}) + y = dw_layer(x) + up_layer = uft.get_instance_framework(InvertibleUpSampling, {"kernel_size": 4}) + z = up_layer(y) + assert z.shape == x.shape + np.testing.assert_array_equal(x, z) diff --git a/tests/utils_framework.py b/tests/utils_framework.py index 980fee8..a070176 100644 --- a/tests/utils_framework.py +++ b/tests/utils_framework.py @@ -21,7 +21,6 @@ from torch.nn import Softmax as tSoftmax from torch.nn import MaxPool2d as tMaxPool2d from torch.nn import Conv2d as tConv2d -from torch.nn import Conv2d as PadConv2d from torch.nn import Upsample as tUpSampling2d from torch.nn import Unflatten as tReshape from torch import int32 as type_int32 @@ -30,18 +29,27 @@ from deel.torchlip import GroupSort from deel.torchlip import GroupSort2 +from deel.torchlip import HouseHolder + from deel.torchlip import Sequential from deel.torchlip.modules import LipschitzModule as LipschitzLayer from deel.torchlip.modules import SpectralLinear from deel.torchlip.modules import SpectralConv2d +from deel.torchlip.modules import SpectralConv1d +from deel.torchlip.modules import SpectralConvTranspose2d from deel.torchlip.modules import FrobeniusLinear from deel.torchlip.modules import FrobeniusConv2d from deel.torchlip.modules import ScaledAvgPool2d from deel.torchlip.modules import ScaledAdaptiveAvgPool2d from deel.torchlip.modules import ScaledL2NormPool2d +from deel.torchlip.modules import ScaledAdaptativeL2NormPool2d from deel.torchlip.modules import InvertibleDownSampling from deel.torchlip.modules import InvertibleUpSampling +from deel.torchlip.modules import LayerCentering +from deel.torchlip.modules import BatchCentering from deel.torchlip.utils import evaluate_lip_const +from deel.torchlip.modules import PadConv2d +from deel.torchlip.modules import LipResidual from deel.torchlip.modules import ( KRLoss, @@ -64,6 +72,7 @@ from deel.torchlip.functional import invertible_downsample from deel.torchlip.functional import invertible_upsample from deel.torchlip.functional import process_labels_for_multi_gpu +from deel.torchlip.functional import SymmetricPad from deel.torchlip.utils.bjorck_norm import bjorck_norm, remove_bjorck_norm from deel.torchlip.utils.frobenius_norm import ( @@ -77,6 +86,7 @@ ) from torch.nn import Module as Loss +framework = "torch" # to avoid linter F401 __all__ = [ @@ -93,11 +103,14 @@ "type_int32", "GroupSort", "GroupSort2", + "HouseHolder", "Sequential", "FrobeniusLinear", "FrobeniusConv2d", "InvertibleDownSampling", "InvertibleUpSampling", + "LayerCentering", + "BatchCentering", "evaluate_lip_const", "DEFAULT_EPS_SPECTRAL", "invertible_downsample", @@ -113,6 +126,8 @@ "tReshape", "CategoricalHingeLoss", "process_labels_for_multi_gpu", + "SpectralConv1d", + "LipResidual", ] @@ -137,18 +152,16 @@ def __call__(self, **kwargs): return None +TauCategoricalCrossentropyLoss = TauCrossEntropyLoss +TauSparseCategoricalCrossentropyLoss = TauCrossEntropyLoss +TauBinaryCrossentropyLoss = TauBCEWithLogitsLoss + tInput = module_Unavailable_foo -Householder = module_Unavailable_class -SpectralConv2dTranspose = module_Unavailable_class -ScaledGlobalL2NormPool2d = module_Unavailable_class AutoWeightClipConstraint = module_Unavailable_class SpectralConstraint = module_Unavailable_class FrobeniusConstraint = module_Unavailable_class CondenseCallback = module_Unavailable_class MonitorCallback = module_Unavailable_class -TauCategoricalCrossentropyLoss = TauCrossEntropyLoss -TauSparseCategoricalCrossentropyLoss = TauCrossEntropyLoss -TauBinaryCrossentropyLoss = TauBCEWithLogitsLoss CategoricalProvableRobustAccuracy = module_Unavailable_class BinaryProvableRobustAccuracy = module_Unavailable_class CategoricalProvableAvgRobustness = module_Unavailable_class @@ -176,9 +189,7 @@ def replace_key_params(inst_params, dict_keys_replace): if k in layp: val = layp.pop(k) if v is None: - warnings.warn( - UserWarning("Warning key is not used", k, " in tensorflow") - ) + warnings.warn(UserWarning("Warning key is not used", k, " in pytorch")) else: if isinstance(v, tuple): layp[v[0]] = v[1](val) @@ -197,11 +208,18 @@ def get_instance_withcheck( instance_type, inst_params, dict_keys_replace={}, list_keys_notimplemented=[] ): for k in list_keys_notimplemented: - if k in inst_params: - warnings.warn( - UserWarning("Warning key is not implemented", k, " in pytorch") - ) - return None + if isinstance(k, tuple): + kk = k[0] + kv = k[1] + else: + kk = k + kv = None + if kk in inst_params: + if (kv is None) or inst_params[kk] in kv: + warnings.warn( + UserWarning("Warning key is not implemented", kk, " in tensorflow") + ) + return None layp = replace_key_params(inst_params, dict_keys_replace) return instance_type(**layp) @@ -213,9 +231,21 @@ def get_instance_withcheck( ScaledL2NormPool2d: partial( get_instance_withreplacement, dict_keys_replace={"data_format": None} ), + ScaledAdaptativeL2NormPool2d: partial( + get_instance_withreplacement, dict_keys_replace={"data_format": None} + ), SpectralConv2d: partial( get_instance_withreplacement, dict_keys_replace={"name": None} ), + SpectralConvTranspose2d: partial( + get_instance_withreplacement, + dict_keys_replace={ + "name": None, + "data_format": None, + "activation": None, + "input_shape": None, + }, + ), SpectralLinear: partial( get_instance_withreplacement, dict_keys_replace={"name": None} ), @@ -223,35 +253,31 @@ def get_instance_withcheck( get_instance_withreplacement, dict_keys_replace={"data_format": None} ), KRLoss: partial( - get_instance_withcheck, + get_instance_withreplacement, dict_keys_replace={"name": None}, - list_keys_notimplemented=[], ), - HingeMarginLoss: partial(get_instance_withcheck, dict_keys_replace={"name": None}), + HingeMarginLoss: partial( + get_instance_withreplacement, dict_keys_replace={"name": None} + ), HKRLoss: partial( - get_instance_withcheck, + get_instance_withreplacement, dict_keys_replace={"name": None}, - list_keys_notimplemented=[], ), HingeMulticlassLoss: partial( - get_instance_withcheck, + get_instance_withreplacement, dict_keys_replace={"name": None}, - list_keys_notimplemented=[], ), HKRMulticlassLoss: partial( - get_instance_withcheck, + get_instance_withreplacement, dict_keys_replace={"name": None}, - list_keys_notimplemented=[], ), KRMulticlassLoss: partial( - get_instance_withcheck, + get_instance_withreplacement, dict_keys_replace={"name": None}, - list_keys_notimplemented=[], ), SoftHKRMulticlassLoss: partial( - get_instance_withcheck, + get_instance_withreplacement, dict_keys_replace={"name": None}, - list_keys_notimplemented=[], ), tLinear: partial( get_instance_withcheck, @@ -269,6 +295,9 @@ def get_instance_withcheck( ), }, ), + HouseHolder: partial( + get_instance_withreplacement, dict_keys_replace={"data_format": None} + ), } @@ -320,6 +349,9 @@ def __init__(self, dict_tensors={}, functional_input_output_tensors={}): self.functional_input_output_tensors = functional_input_output_tensors self.modList = torch.nn.ModuleList([dict_tensors[key] for key in dict_tensors]) + def get_module_by_name(self, name): + return self.dict_tensors[name] + def forward(self, x): x = self.functional_input_output_tensors(self.dict_tensors, x) return x @@ -459,10 +491,19 @@ def load_model( return model +def load_state_dict(path, model): + model.load_state_dict(torch.load(path)) + return model + + def get_layer_weights_by_index(model, layer_idx): return get_layer_weights(model[layer_idx]) +def get_layer_by_index(model, layer_idx): + return model[layer_idx] + + # .weight.detach().cpu().numpy() @@ -484,7 +525,7 @@ def initialize_kernel(model, layer_idx, kernel_initializer): def initializers_Constant(value): - return None + return value def check_parametrization(m, is_parametrized): @@ -528,8 +569,12 @@ def to_NCHW(x): return x +def to_NCHW_inv(x): + return x + + def get_NCHW(x): - return (x.shape[0], x.shape[1], x.shape[2], x.shape[3]) + return (x.shape[-4], x.shape[-3], x.shape[-2], x.shape[-1]) def scaleAlpha(alpha): @@ -581,8 +626,42 @@ def vanillaModel(model): return model -def is_supported_padding(padding): - return padding.lower() in ["same", "valid", "reflect", "circular"] # "constant", +def is_supported_padding(padding, layer_type): + layertype2padding = { + SpectralConv2d: [ + "same", + "zeros", + "valid", + "reflect", + "circular", + "symmetric", + "replicate", + ], + FrobeniusConv2d: [ + "same", + "zeros", + "valid", + "reflect", + "circular", + "symmetric", + "replicate", + ], + PadConv2d: [ + "same", + "zeros", + "valid", + "reflect", + "circular", + "symmetric", + "replicate", + ], + } + if layer_type in layertype2padding: + return padding.lower() in layertype2padding[layer_type] + else: + assert False + warnings.warn(f"layer {layer_type} type not supported for padding") + return False def pad_input(x, padding, kernel_size): @@ -591,7 +670,7 @@ def pad_input(x, padding, kernel_size): kernel_size = [kernel_size, kernel_size] if padding.lower() in ["same", "valid"]: return x - elif padding.lower() in ["constant", "reflect", "circular"]: + elif padding.lower() in ["constant", "reflect", "circular", "replicate"]: p_vert, p_hor = kernel_size[0] // 2, kernel_size[1] // 2 pad_sizes = [ p_hor, @@ -600,6 +679,10 @@ def pad_input(x, padding, kernel_size): p_vert, ] # [[0, 0], [p_vert, p_vert], [p_hor, p_hor], [0, 0]] return pad(x, tuple(pad_sizes), padding) + elif padding.lower() == "symmetric": + p_vert, p_hor = kernel_size[0] // 2, kernel_size[1] // 2 + sym_pad = SymmetricPad([p_hor, p_vert]) + return sym_pad(x) class MultiMarginLoss(tMultiMarginLoss): @@ -619,3 +702,13 @@ def __init__(self, dim=-1): def forward(self, x): return torch.cat(x, dim=self.dim) + + +class tSplit(torch.nn.Module): + def __init__(self, chunks, dim=-1): + super(tSplit, self).__init__() + self.chunks = chunks + self.dim = dim + + def forward(self, x): + return torch.chunk(x, self.chunks, dim=self.dim)