825 lines
28 KiB
ReStructuredText
825 lines
28 KiB
ReStructuredText
Tutorial 4: continuous convolutional filter
|
||
===========================================
|
||
|
||
In this tutorial we will show how to use the Continouous Convolutional
|
||
Filter, and how to build common Deep Learning architectures with it. The
|
||
implementation of the filter follows the original work `A Continuous
|
||
Convolutional Trainable Filter for Modelling Unstructured
|
||
Data <https://arxiv.org/abs/2210.13416>`__ of Coscia Dario, Laura
|
||
Meneghetti, Nicola Demo, Giovanni Stabile, and Gianluigi Rozza.
|
||
|
||
First of all we import the modules needed for the tutorial, which
|
||
include:
|
||
|
||
- ``ContinuousConv`` class from ``pina.model.layers`` which implements
|
||
the continuous convolutional filter
|
||
- ``PyTorch`` and ``Matplotlib`` for tensorial operations and
|
||
visualization respectively
|
||
|
||
.. code:: ipython3
|
||
|
||
import torch
|
||
import matplotlib.pyplot as plt
|
||
from pina.model.layers import ContinuousConv
|
||
import torchvision # for MNIST dataset
|
||
from pina.model import FeedForward # for building AE and MNIST classification
|
||
|
||
The tutorial is structured as follow:
|
||
|
||
* `Continuous filter background <#continuous-filter-background>`__: understand how the convolutional filter works and how to use it.
|
||
|
||
* `Building a MNIST Classifier <#building-a-mnist-classifier>`__: show how to build a simple classifier using the MNIST dataset and how to combine a continuous convolutional layer with a feedforward neural network.
|
||
|
||
* `Building a Continuous Convolutional Autoencoder <#building-a-continuous-convolutional-autoencoder>`__: show how to use the continuous filter to work with unstructured data for autoencoding and up-sampling.
|
||
|
||
Continuous filter background
|
||
----------------------------
|
||
|
||
As reported by the authors in the original paper: in contrast to
|
||
discrete convolution, continuous convolution is mathematically defined
|
||
as:
|
||
|
||
.. math::
|
||
|
||
|
||
\mathcal{I}_{\rm{out}}(\mathbf{x}) = \int_{\mathcal{X}} \mathcal{I}(\mathbf{x} + \mathbf{\tau}) \cdot \mathcal{K}(\mathbf{\tau}) d\mathbf{\tau},
|
||
|
||
where :math:`\mathcal{K} : \mathcal{X} \rightarrow \mathbb{R}` is the
|
||
*continuous filter* function, and
|
||
:math:`\mathcal{I} : \Omega \subset \mathbb{R}^N \rightarrow \mathbb{R}`
|
||
is the input function. The continuous filter function is approximated
|
||
using a FeedForward Neural Network, thus trainable during the training
|
||
phase. The way in which the integral is approximated can be different,
|
||
currently on **PINA** we approximate it using a simple sum, as suggested
|
||
by the authors. Thus, given :math:`\{\mathbf{x}_i\}_{i=1}^{n}` points in
|
||
:math:`\mathbb{R}^N` of the input function mapped on the
|
||
:math:`\mathcal{X}` filter domain, we approximate the above equation as:
|
||
|
||
.. math::
|
||
|
||
|
||
\mathcal{I}_{\rm{out}}(\mathbf{\tilde{x}}_i) = \sum_{{\mathbf{x}_i}\in\mathcal{X}} \mathcal{I}(\mathbf{x}_i + \mathbf{\tau}) \cdot \mathcal{K}(\mathbf{x}_i),
|
||
|
||
where :math:`\mathbf{\tau} \in \mathcal{S}`, with :math:`\mathcal{S}`
|
||
the set of available strides, corresponds to the current stride position
|
||
of the filter, and :math:`\mathbf{\tilde{x}}_i` points are obtained by
|
||
taking the centroid of the filter position mapped on the :math:`\Omega`
|
||
domain.
|
||
|
||
We will now try to pratically see how to work with the filter. From the
|
||
above definition we see that what is needed is: 1. A domain and a
|
||
function defined on that domain (the input) 2. A stride, corresponding
|
||
to the positions where the filter needs to be :math:`\rightarrow`
|
||
``stride`` variable in ``ContinuousConv`` 3. The filter rectangular
|
||
domain :math:`\rightarrow` ``filter_dim`` variable in ``ContinuousConv``
|
||
|
||
Input function
|
||
~~~~~~~~~~~~~~
|
||
|
||
The input function for the continuous filter is defined as a tensor of
|
||
shape:
|
||
|
||
.. math:: [B \times N_{in} \times N \times D]
|
||
|
||
\ where :math:`B` is the batch_size, :math:`N_{in}` is the number of
|
||
input fields, :math:`N` the number of points in the mesh, :math:`D` the
|
||
dimension of the problem. In particular:
|
||
|
||
* :math:`D` is the number of spatial variables + 1. The last column must contain the field value. For example for 2D problems :math:`D=3` and the tensor will be something like ``[first coordinate, second coordinate, field value]``
|
||
|
||
* :math:`N_{in}` represents the number of vectorial function presented. For example a vectorial function :math:`f = [f_1, f_2]` will have math:`N_{in}=2`
|
||
|
||
Let’s see an example to clear the ideas. We will be verbose to explain
|
||
in details the input form. We wish to create the function:
|
||
|
||
.. math::
|
||
|
||
|
||
f(x, y) = [\sin(\pi x) \sin(\pi y), -\sin(\pi x) \sin(\pi y)] \quad (x,y)\in[0,1]\times[0,1]
|
||
|
||
using a batch size of one.
|
||
|
||
.. code:: ipython3
|
||
|
||
# batch size fixed to 1
|
||
batch_size = 1
|
||
|
||
# points in the mesh fixed to 200
|
||
N = 200
|
||
|
||
# vectorial 2 dimensional function, number_input_fileds=2
|
||
number_input_fileds = 2
|
||
|
||
# 2 dimensional spatial variables, D = 2 + 1 = 3
|
||
D = 3
|
||
|
||
# create the function f domain as random 2d points in [0, 1]
|
||
domain = torch.rand(size=(batch_size, number_input_fileds, N, D-1))
|
||
print(f"Domain has shape: {domain.shape}")
|
||
|
||
# create the functions
|
||
pi = torch.acos(torch.tensor([-1.])) # pi value
|
||
f1 = torch.sin(pi * domain[:, 0, :, 0]) * torch.sin(pi * domain[:, 0, :, 1])
|
||
f2 = - torch.sin(pi * domain[:, 1, :, 0]) * torch.sin(pi * domain[:, 1, :, 1])
|
||
|
||
# stacking the input domain and field values
|
||
data = torch.empty(size=(batch_size, number_input_fileds, N, D))
|
||
data[..., :-1] = domain # copy the domain
|
||
data[:, 0, :, -1] = f1 # copy first field value
|
||
data[:, 1, :, -1] = f1 # copy second field value
|
||
print(f"Filter input data has shape: {data.shape}")
|
||
|
||
|
||
.. parsed-literal::
|
||
|
||
Domain has shape: torch.Size([1, 2, 200, 2])
|
||
Filter input data has shape: torch.Size([1, 2, 200, 3])
|
||
|
||
|
||
Stride
|
||
~~~~~~
|
||
|
||
The stride is passed as a dictionary ``stride`` which tells the filter
|
||
where to go. Here is an example for the :math:`[0,1]\times[0,5]` domain:
|
||
|
||
.. code:: python
|
||
|
||
# stride definition
|
||
stride = {"domain": [1, 5],
|
||
"start": [0, 0],
|
||
"jump": [0.1, 0.3],
|
||
"direction": [1, 1],
|
||
}
|
||
|
||
This tells the filter:
|
||
|
||
1. ``domain``: square domain (the only implemented) :math:`[0,1]\times[0,5]`. The minimum value is always zero, while the maximum is specified by the user
|
||
|
||
2. ``start``: start position of the filter, coordinate :math:`(0, 0)`
|
||
|
||
3. ``jump``: the jumps of the centroid of the filter to the next position :math:`(0.1, 0.3)`
|
||
|
||
4. ``direction``: the directions of the jump, with ``1 = right``, ``0 = no jump``,\ ``-1 = left`` with respect to the current position
|
||
|
||
**Note**
|
||
|
||
We are planning to release the possibility to directly pass a list of
|
||
possible strides!
|
||
|
||
Filter definition
|
||
~~~~~~~~~~~~~~~~~
|
||
|
||
Having defined all the previous blocks we are able to construct the
|
||
continuous filter.
|
||
|
||
Suppose we would like to get an ouput with only one field, and let us
|
||
fix the filter dimension to be :math:`[0.1, 0.1]`.
|
||
|
||
.. code:: ipython3
|
||
|
||
# filter dim
|
||
filter_dim = [0.1, 0.1]
|
||
|
||
# stride
|
||
stride = {"domain": [1, 1],
|
||
"start": [0, 0],
|
||
"jump": [0.08, 0.08],
|
||
"direction": [1, 1],
|
||
}
|
||
|
||
# creating the filter
|
||
cConv = ContinuousConv(input_numb_field=number_input_fileds,
|
||
output_numb_field=1,
|
||
filter_dim=filter_dim,
|
||
stride=stride)
|
||
|
||
That’s it! In just one line of code we have created the continuous
|
||
convolutional filter. By default the ``pina.model.FeedForward`` neural
|
||
network is intitialised, more on the
|
||
`documentation <https://mathlab.github.io/PINA/_rst/fnn.html>`__. In
|
||
case the mesh doesn’t change during training we can set the ``optimize``
|
||
flag equals to ``True``, to exploit optimizations for finding the points
|
||
to convolve.
|
||
|
||
.. code:: ipython3
|
||
|
||
# creating the filter + optimization
|
||
cConv = ContinuousConv(input_numb_field=number_input_fileds,
|
||
output_numb_field=1,
|
||
filter_dim=filter_dim,
|
||
stride=stride,
|
||
optimize=True)
|
||
|
||
|
||
Let’s try to do a forward pass
|
||
|
||
.. code:: ipython3
|
||
|
||
print(f"Filter input data has shape: {data.shape}")
|
||
|
||
#input to the filter
|
||
output = cConv(data)
|
||
|
||
print(f"Filter output data has shape: {output.shape}")
|
||
|
||
|
||
.. parsed-literal::
|
||
|
||
Filter input data has shape: torch.Size([1, 2, 200, 3])
|
||
Filter output data has shape: torch.Size([1, 1, 169, 3])
|
||
|
||
|
||
If we don’t want to use the default ``FeedForward`` neural network, we
|
||
can pass a specified torch model in the ``model`` keyword as follow:
|
||
|
||
.. code:: ipython3
|
||
|
||
class SimpleKernel(torch.nn.Module):
|
||
def __init__(self) -> None:
|
||
super().__init__()
|
||
self. model = torch.nn.Sequential(
|
||
torch.nn.Linear(2, 20),
|
||
torch.nn.ReLU(),
|
||
torch.nn.Linear(20, 20),
|
||
torch.nn.ReLU(),
|
||
torch.nn.Linear(20, 1))
|
||
|
||
def forward(self, x):
|
||
return self.model(x)
|
||
|
||
|
||
cConv = ContinuousConv(input_numb_field=number_input_fileds,
|
||
output_numb_field=1,
|
||
filter_dim=filter_dim,
|
||
stride=stride,
|
||
optimize=True,
|
||
model=SimpleKernel)
|
||
|
||
|
||
Notice that we pass the class and not an already built object!
|
||
|
||
Building a MNIST Classifier
|
||
---------------------------
|
||
|
||
Let’s see how we can build a MNIST classifier using a continuous
|
||
convolutional filter. We will use the MNIST dataset from PyTorch. In
|
||
order to keep small training times we use only 6000 samples for training
|
||
and 1000 samples for testing.
|
||
|
||
.. code:: ipython3
|
||
|
||
from torch.utils.data import DataLoader, SubsetRandomSampler
|
||
|
||
numb_training = 6000 # get just 6000 images for training
|
||
numb_testing= 1000 # get just 1000 images for training
|
||
seed = 111 # for reproducibility
|
||
batch_size = 8 # setting batch size
|
||
|
||
# setting the seed
|
||
torch.manual_seed(seed)
|
||
|
||
# downloading the dataset
|
||
train_data = torchvision.datasets.MNIST('./data/', train=True, download=True,
|
||
transform=torchvision.transforms.Compose([
|
||
torchvision.transforms.ToTensor(),
|
||
torchvision.transforms.Normalize(
|
||
(0.1307,), (0.3081,))
|
||
]))
|
||
subsample_train_indices = torch.randperm(len(train_data))[:numb_training]
|
||
train_loader = DataLoader(train_data, batch_size=batch_size,
|
||
sampler=SubsetRandomSampler(subsample_train_indices))
|
||
|
||
test_data = torchvision.datasets.MNIST('./data/', train=False, download=True,
|
||
transform=torchvision.transforms.Compose([
|
||
torchvision.transforms.ToTensor(),
|
||
torchvision.transforms.Normalize(
|
||
(0.1307,), (0.3081,))
|
||
]))
|
||
subsample_test_indices = torch.randperm(len(train_data))[:numb_testing]
|
||
test_loader = DataLoader(train_data, batch_size=batch_size,
|
||
sampler=SubsetRandomSampler(subsample_train_indices))
|
||
|
||
Let’s now build a simple classifier. The MNIST dataset is composed by
|
||
vectors of shape ``[batch, 1, 28, 28]``, but we can image them as one
|
||
field functions where the pixels :math:`ij` are the coordinate
|
||
:math:`x=i, y=j` in a :math:`[0, 27]\times[0,27]` domain, and the pixels
|
||
value are the field values. We just need a function to transform the
|
||
regular tensor in a tensor compatible for the continuous filter:
|
||
|
||
.. code:: ipython3
|
||
|
||
def transform_input(x):
|
||
batch_size = x.shape[0]
|
||
dim_grid = tuple(x.shape[:-3:-1])
|
||
|
||
# creating the n dimensional mesh grid for a single channel image
|
||
values_mesh = [torch.arange(0, dim).float() for dim in dim_grid]
|
||
mesh = torch.meshgrid(values_mesh)
|
||
coordinates_mesh = [x.reshape(-1, 1) for x in mesh]
|
||
coordinates = torch.cat(coordinates_mesh, dim=1).unsqueeze(
|
||
0).repeat((batch_size, 1, 1)).unsqueeze(1)
|
||
|
||
return torch.cat((coordinates, x.flatten(2).unsqueeze(-1)), dim=-1)
|
||
|
||
|
||
# let's try it out
|
||
image, s = next(iter(train_loader))
|
||
print(f"Original MNIST image shape: {image.shape}")
|
||
|
||
image_transformed = transform_input(image)
|
||
print(f"Transformed MNIST image shape: {image_transformed.shape}")
|
||
|
||
|
||
|
||
.. parsed-literal::
|
||
|
||
Original MNIST image shape: torch.Size([8, 1, 28, 28])
|
||
Transformed MNIST image shape: torch.Size([8, 1, 784, 3])
|
||
|
||
|
||
We can now build a simple classifier! We will use just one convolutional
|
||
filter followed by a feedforward neural network
|
||
|
||
.. code:: ipython3
|
||
|
||
# setting the seed
|
||
torch.manual_seed(seed)
|
||
|
||
class ContinuousClassifier(torch.nn.Module):
|
||
def __init__(self):
|
||
super().__init__()
|
||
|
||
# number of classes for classification
|
||
numb_class = 10
|
||
|
||
# convolutional block
|
||
self.convolution = ContinuousConv(input_numb_field=1,
|
||
output_numb_field=4,
|
||
stride={"domain": [27, 27],
|
||
"start": [0, 0],
|
||
"jumps": [4, 4],
|
||
"direction": [1, 1.],
|
||
},
|
||
filter_dim=[4, 4],
|
||
optimize=True)
|
||
# feedforward net
|
||
self.nn = FeedForward(input_variables=196,
|
||
output_variables=numb_class,
|
||
layers=[120, 64],
|
||
func=torch.nn.ReLU)
|
||
|
||
def forward(self, x):
|
||
# transform input + convolution
|
||
x = transform_input(x)
|
||
x = self.convolution(x)
|
||
# feed forward classification
|
||
return self.nn(x[..., -1].flatten(1))
|
||
|
||
|
||
net = ContinuousClassifier()
|
||
|
||
Let’s try to train it using a simple pytorch training loop. We train for
|
||
juts 1 epoch using Adam optimizer with a :math:`0.001` learning rate.
|
||
|
||
.. code:: ipython3
|
||
|
||
# setting the seed
|
||
torch.manual_seed(seed)
|
||
|
||
# optimizer and loss function
|
||
optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
|
||
criterion = torch.nn.CrossEntropyLoss()
|
||
|
||
for epoch in range(1): # loop over the dataset multiple times
|
||
|
||
running_loss = 0.0
|
||
for i, data in enumerate(train_loader, 0):
|
||
# get the inputs; data is a list of [inputs, labels]
|
||
inputs, labels = data
|
||
|
||
# zero the parameter gradients
|
||
optimizer.zero_grad()
|
||
|
||
# forward + backward + optimize
|
||
outputs = net(inputs)
|
||
loss = criterion(outputs, labels)
|
||
loss.backward()
|
||
optimizer.step()
|
||
|
||
# print statistics
|
||
running_loss += loss.item()
|
||
if i % 50 == 49:
|
||
print(
|
||
f'epoch [{i + 1}/{numb_training//batch_size}] loss[{running_loss / 500:.3f}]')
|
||
running_loss = 0.0
|
||
|
||
|
||
|
||
.. parsed-literal::
|
||
|
||
epoch [50/750] loss[0.148]
|
||
epoch [100/750] loss[0.072]
|
||
epoch [150/750] loss[0.063]
|
||
epoch [200/750] loss[0.053]
|
||
epoch [250/750] loss[0.041]
|
||
epoch [300/750] loss[0.048]
|
||
epoch [350/750] loss[0.054]
|
||
epoch [400/750] loss[0.048]
|
||
epoch [450/750] loss[0.047]
|
||
epoch [500/750] loss[0.035]
|
||
epoch [550/750] loss[0.036]
|
||
epoch [600/750] loss[0.041]
|
||
epoch [650/750] loss[0.030]
|
||
epoch [700/750] loss[0.040]
|
||
epoch [750/750] loss[0.040]
|
||
|
||
|
||
Let’s see the performance on the train set!
|
||
|
||
.. code:: ipython3
|
||
|
||
correct = 0
|
||
total = 0
|
||
with torch.no_grad():
|
||
for data in test_loader:
|
||
images, labels = data
|
||
# calculate outputs by running images through the network
|
||
outputs = net(images)
|
||
# the class with the highest energy is what we choose as prediction
|
||
_, predicted = torch.max(outputs.data, 1)
|
||
total += labels.size(0)
|
||
correct += (predicted == labels).sum().item()
|
||
|
||
print(
|
||
f'Accuracy of the network on the 1000 test images: {(correct / total):.3%}')
|
||
|
||
|
||
|
||
.. parsed-literal::
|
||
|
||
Accuracy of the network on the 1000 test images: 93.017%
|
||
|
||
|
||
As we can see we have very good performance for having traing only for 1
|
||
epoch! Nevertheless, we are still using structured data… Let’s see how
|
||
we can build an autoencoder for unstructured data now.
|
||
|
||
Building a Continuous Convolutional Autoencoder
|
||
-----------------------------------------------
|
||
|
||
Just as toy problem, we will now build an autoencoder for the following
|
||
function :math:`f(x,y)=\sin(\pi x)\sin(\pi y)` on the unit circle domain
|
||
centered in :math:`(0.5, 0.5)`. We will also see the ability to
|
||
up-sample (once trained) the results without retraining. Let’s first
|
||
create the input and visualize it, we will use firstly a mesh of
|
||
:math:`100` points.
|
||
|
||
.. code:: ipython3
|
||
|
||
# create inputs
|
||
def circle_grid(N=100):
|
||
"""Generate points withing a unit 2D circle centered in (0.5, 0.5)
|
||
|
||
:param N: number of points
|
||
:type N: float
|
||
:return: [x, y] array of points
|
||
:rtype: torch.tensor
|
||
"""
|
||
|
||
PI = torch.acos(torch.zeros(1)).item() * 2
|
||
R = 0.5
|
||
centerX = 0.5
|
||
centerY = 0.5
|
||
|
||
r = R * torch.sqrt(torch.rand(N))
|
||
theta = torch.rand(N) * 2 * PI
|
||
|
||
x = centerX + r * torch.cos(theta)
|
||
y = centerY + r * torch.sin(theta)
|
||
|
||
return torch.stack([x, y]).T
|
||
|
||
# create the grid
|
||
grid = circle_grid(500)
|
||
|
||
# create input
|
||
input_data = torch.empty(size=(1, 1, grid.shape[0], 3))
|
||
input_data[0, 0, :, :-1] = grid
|
||
input_data[0, 0, :, -1] = torch.sin(pi * grid[:, 0]) * torch.sin(pi * grid[:, 1])
|
||
|
||
# visualize data
|
||
plt.title("Training sample with 500 points")
|
||
plt.scatter(grid[:, 0], grid[:, 1], c=input_data[0, 0, :, -1])
|
||
plt.colorbar()
|
||
plt.show()
|
||
|
||
|
||
|
||
|
||
.. image:: tutorial_files/tutorial_32_0.png
|
||
|
||
|
||
Let’s now build a simple autoencoder using the continuous convolutional
|
||
filter. The data is clearly unstructured and a simple convolutional
|
||
filter might not work without projecting or interpolating first. Let’s
|
||
first build and ``Encoder`` and ``Decoder`` class, and then a
|
||
``Autoencoder`` class that contains both.
|
||
|
||
.. code:: ipython3
|
||
|
||
class Encoder(torch.nn.Module):
|
||
def __init__(self, hidden_dimension):
|
||
super().__init__()
|
||
|
||
# convolutional block
|
||
self.convolution = ContinuousConv(input_numb_field=1,
|
||
output_numb_field=2,
|
||
stride={"domain": [1, 1],
|
||
"start": [0, 0],
|
||
"jumps": [0.05, 0.05],
|
||
"direction": [1, 1.],
|
||
},
|
||
filter_dim=[0.15, 0.15],
|
||
optimize=True)
|
||
# feedforward net
|
||
self.nn = FeedForward(input_variables=400,
|
||
output_variables=hidden_dimension,
|
||
layers=[240, 120])
|
||
|
||
def forward(self, x):
|
||
# convolution
|
||
x = self.convolution(x)
|
||
# feed forward pass
|
||
return self.nn(x[..., -1])
|
||
|
||
|
||
class Decoder(torch.nn.Module):
|
||
def __init__(self, hidden_dimension):
|
||
super().__init__()
|
||
|
||
# convolutional block
|
||
self.convolution = ContinuousConv(input_numb_field=2,
|
||
output_numb_field=1,
|
||
stride={"domain": [1, 1],
|
||
"start": [0, 0],
|
||
"jumps": [0.05, 0.05],
|
||
"direction": [1, 1.],
|
||
},
|
||
filter_dim=[0.15, 0.15],
|
||
optimize=True)
|
||
# feedforward net
|
||
self.nn = FeedForward(input_variables=hidden_dimension,
|
||
output_variables=400,
|
||
layers=[120, 240])
|
||
|
||
def forward(self, weights, grid):
|
||
# feed forward pass
|
||
x = self.nn(weights)
|
||
# transpose convolution
|
||
return torch.sigmoid(self.convolution.transpose(x, grid))
|
||
|
||
|
||
Very good! Notice that in the ``Decoder`` class in the ``forward`` pass
|
||
we have used the ``.transpose()`` method of the
|
||
``ContinuousConvolution`` class. This method accepts the ``weights`` for
|
||
upsampling and the ``grid`` on where to upsample. Let’s now build the
|
||
autoencoder! We set the hidden dimension in the ``hidden_dimension``
|
||
variable. We apply the sigmoid on the output since the field value is
|
||
between :math:`[0, 1]`.
|
||
|
||
.. code:: ipython3
|
||
|
||
class Autoencoder(torch.nn.Module):
|
||
def __init__(self, hidden_dimension=10):
|
||
super().__init__()
|
||
|
||
self.encoder = Encoder(hidden_dimension)
|
||
self.decoder = Decoder(hidden_dimension)
|
||
|
||
def forward(self, x):
|
||
# saving grid for later upsampling
|
||
grid = x.clone().detach()
|
||
# encoder
|
||
weights = self.encoder(x)
|
||
# decoder
|
||
out = self.decoder(weights, grid)
|
||
return out
|
||
|
||
|
||
net = Autoencoder()
|
||
|
||
Let’s now train the autoencoder, minimizing the mean square error loss
|
||
and optimizing using Adam.
|
||
|
||
.. code:: ipython3
|
||
|
||
# setting the seed
|
||
torch.manual_seed(seed)
|
||
|
||
# optimizer and loss function
|
||
optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
|
||
criterion = torch.nn.MSELoss()
|
||
max_epochs = 150
|
||
|
||
for epoch in range(max_epochs): # loop over the dataset multiple times
|
||
|
||
# zero the parameter gradients
|
||
optimizer.zero_grad()
|
||
|
||
# forward + backward + optimize
|
||
outputs = net(input_data)
|
||
loss = criterion(outputs[..., -1], input_data[..., -1])
|
||
loss.backward()
|
||
optimizer.step()
|
||
|
||
# print statistics
|
||
if epoch % 10 ==9:
|
||
print(f'epoch [{epoch + 1}/{max_epochs}] loss [{loss.item():.2}]')
|
||
|
||
|
||
|
||
.. parsed-literal::
|
||
|
||
epoch [10/150] loss [0.013]
|
||
epoch [20/150] loss [0.0029]
|
||
epoch [30/150] loss [0.0019]
|
||
epoch [40/150] loss [0.0014]
|
||
epoch [50/150] loss [0.0011]
|
||
epoch [60/150] loss [0.00094]
|
||
epoch [70/150] loss [0.00082]
|
||
epoch [80/150] loss [0.00074]
|
||
epoch [90/150] loss [0.00068]
|
||
epoch [100/150] loss [0.00064]
|
||
epoch [110/150] loss [0.00061]
|
||
epoch [120/150] loss [0.00058]
|
||
epoch [130/150] loss [0.00057]
|
||
epoch [140/150] loss [0.00056]
|
||
epoch [150/150] loss [0.00054]
|
||
|
||
|
||
Let’s visualize the two solutions side by side!
|
||
|
||
.. code:: ipython3
|
||
|
||
net.eval()
|
||
|
||
# get output and detach from computational graph for plotting
|
||
output = net(input_data).detach()
|
||
|
||
# visualize data
|
||
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 3))
|
||
pic1 = axes[0].scatter(grid[:, 0], grid[:, 1], c=input_data[0, 0, :, -1])
|
||
axes[0].set_title("Real")
|
||
fig.colorbar(pic1)
|
||
plt.subplot(1, 2, 2)
|
||
pic2 = axes[1].scatter(grid[:, 0], grid[:, 1], c=output[0, 0, :, -1])
|
||
axes[1].set_title("Autoencoder")
|
||
fig.colorbar(pic2)
|
||
plt.tight_layout()
|
||
plt.show()
|
||
|
||
|
||
|
||
|
||
.. image:: tutorial_files/tutorial_40_0.png
|
||
|
||
|
||
As we can see the two are really similar! We can compute the :math:`l_2`
|
||
error quite easily as well:
|
||
|
||
.. code:: ipython3
|
||
|
||
def l2_error(input_, target):
|
||
return torch.linalg.norm(input_-target, ord=2)/torch.linalg.norm(input_, ord=2)
|
||
|
||
|
||
print(f'l2 error: {l2_error(input_data[0, 0, :, -1], output[0, 0, :, -1]):.2%}')
|
||
|
||
|
||
.. parsed-literal::
|
||
|
||
l2 error: 4.10%
|
||
|
||
|
||
More or less :math:`4\%` in :math:`l_2` error, which is really low
|
||
considering the fact that we use just **one** convolutional layer and a
|
||
simple feedforward to decrease the dimension. Let’s see now some
|
||
peculiarity of the filter.
|
||
|
||
Filter for upsampling
|
||
~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
Suppose we have already the hidden dimension and we want to upsample on
|
||
a differen grid with more points. Let’s see how to do it:
|
||
|
||
.. code:: ipython3
|
||
|
||
# setting the seed
|
||
torch.manual_seed(seed)
|
||
|
||
grid2 = circle_grid(1500) # triple number of points
|
||
input_data2 = torch.zeros(size=(1, 1, grid2.shape[0], 3))
|
||
input_data2[0, 0, :, :-1] = grid2
|
||
input_data2[0, 0, :, -1] = torch.sin(pi *
|
||
grid2[:, 0]) * torch.sin(pi * grid2[:, 1])
|
||
|
||
# get the hidden dimension representation from original input
|
||
latent = net.encoder(input_data)
|
||
|
||
# upsample on the second input_data2
|
||
output = net.decoder(latent, input_data2).detach()
|
||
|
||
# show the picture
|
||
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 3))
|
||
pic1 = axes[0].scatter(grid2[:, 0], grid2[:, 1], c=input_data2[0, 0, :, -1])
|
||
axes[0].set_title("Real")
|
||
fig.colorbar(pic1)
|
||
plt.subplot(1, 2, 2)
|
||
pic2 = axes[1].scatter(grid2[:, 0], grid2[:, 1], c=output[0, 0, :, -1])
|
||
axes[1].set_title("Up-sampling")
|
||
fig.colorbar(pic2)
|
||
plt.tight_layout()
|
||
plt.show()
|
||
|
||
|
||
|
||
|
||
.. image:: tutorial_files/tutorial_45_0.png
|
||
|
||
|
||
As we can see we have a very good approximation of the original
|
||
function, even thought some noise is present. Let’s calculate the error
|
||
now:
|
||
|
||
.. code:: ipython3
|
||
|
||
print(f'l2 error: {l2_error(input_data2[0, 0, :, -1], output[0, 0, :, -1]):.2%}')
|
||
|
||
|
||
.. parsed-literal::
|
||
|
||
l2 error: 8.44%
|
||
|
||
|
||
Autoencoding at different resolution
|
||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
In the previous example we already had the hidden dimension (of original
|
||
input) and we used it to upsample. Sometimes however we have a more fine
|
||
mesh solution and we simply want to encode it. This can be done without
|
||
retraining! This procedure can be useful in case we have many points in
|
||
the mesh and just a smaller part of them are needed for training. Let’s
|
||
see the results of this:
|
||
|
||
.. code:: ipython3
|
||
|
||
# setting the seed
|
||
torch.manual_seed(seed)
|
||
|
||
grid2 = circle_grid(3500) # very fine mesh
|
||
input_data2 = torch.zeros(size=(1, 1, grid2.shape[0], 3))
|
||
input_data2[0, 0, :, :-1] = grid2
|
||
input_data2[0, 0, :, -1] = torch.sin(pi *
|
||
grid2[:, 0]) * torch.sin(pi * grid2[:, 1])
|
||
|
||
# get the hidden dimension representation from more fine mesh input
|
||
latent = net.encoder(input_data2)
|
||
|
||
# upsample on the second input_data2
|
||
output = net.decoder(latent, input_data2).detach()
|
||
|
||
# show the picture
|
||
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 3))
|
||
pic1 = axes[0].scatter(grid2[:, 0], grid2[:, 1], c=input_data2[0, 0, :, -1])
|
||
axes[0].set_title("Real")
|
||
fig.colorbar(pic1)
|
||
plt.subplot(1, 2, 2)
|
||
pic2 = axes[1].scatter(grid2[:, 0], grid2[:, 1], c=output[0, 0, :, -1])
|
||
axes[1].set_title("Autoencoder not re-trained")
|
||
fig.colorbar(pic2)
|
||
plt.tight_layout()
|
||
plt.show()
|
||
|
||
# calculate l2 error
|
||
print(
|
||
f'l2 error: {l2_error(input_data2[0, 0, :, -1], output[0, 0, :, -1]):.2%}')
|
||
|
||
|
||
|
||
|
||
.. image:: tutorial_files/tutorial_49_0.png
|
||
|
||
|
||
.. parsed-literal::
|
||
|
||
l2 error: 8.45%
|
||
|
||
|
||
What’s next?
|
||
------------
|
||
|
||
We have shown the basic usage of a convolutional filter. In the next
|
||
tutorials we will show how to combine the PINA framework with the
|
||
convolutional filter to train in few lines and efficiently a Neural
|
||
Network!
|