Commit c2a2f4b8 authored by Guillaume HEUSCH's avatar Guillaume HEUSCH

Merge branch 'mlp_train' into 'master'

MLP class and config to train it

See merge request !14
parents 9c130f65 bd511041
Pipeline #26670 passed with stages
in 10 minutes and 34 seconds
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
@author: Olegs Nikisins
"""
#==============================================================================
# Import here:
from torch import nn
import torch.nn.functional as F
#==============================================================================
# Define the network:
class TwoLayerMLP(nn.Module):
"""
A simple two-layer MLP for binary classification. The output activation
function is sigmoid.
Attributes
----------
in_features : int
Dimensionality of the input feature vectors.
n_hidden_relu : int
Number of ReLU units in the hidden layer of the MLP.
apply_sigmoid : bool
If set to ``True`` the sigmoid will be applied to the output of the
hidden FC layer. If ``False`` the sigmoid is not applied.
"""
def __init__(self, in_features, n_hidden_relu, apply_sigmoid = True):
super(TwoLayerMLP, self).__init__()
"""
Init method.
Parameters
----------
in_features : int
Dimensionality of the input feature vectors.
n_hidden_relu : int
Number of ReLU units in the hidden layer of the MLP.
apply_sigmoid : bool
If set to ``True`` the sigmoid will be applied to the output of the
hidden FC layer. If ``False`` the sigmoid is not applied.
Default: ``True``.
"""
self.in_features = in_features
self.n_hidden_relu = n_hidden_relu
self.apply_sigmoid = apply_sigmoid
self.fc1 = nn.Linear(in_features = self.in_features, out_features = self.n_hidden_relu, bias=True)
self.fc2 = nn.Linear(in_features = self.n_hidden_relu, out_features = 1, bias=True)
def forward(self, x):
"""
The forward method.
Parameters
----------
x : :py:class:`torch.Tensor`
The batch to forward through the network. Size of the input batch
is [batch_size, 1, self.in_features].
Returns
-------
x : :py:class:`torch.Tensor`
Output of the MLP, class probability.
"""
# input is a batch of the size: [batch_size, 1, self.in_features],
# convert it to the size [batch_size, self.in_features] as expected by FC layer:
x = x.squeeze()
# first fully connected activated by ReLu:
x = self.fc1(x)
x = F.relu(x)
# second fully connected activated by sigmoid:
x = self.fc2(x)
if not self.apply_sigmoid:
return x
x = F.sigmoid(x)
return x
......@@ -12,6 +12,7 @@ from .ConditionalGAN import ConditionalGAN_generator
from .ConditionalGAN import ConditionalGAN_discriminator
from .ConvAutoencoder import ConvAutoencoder
from .TwoLayerMLP import TwoLayerMLP
from .utils import weights_init
......
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
@author: Olegs Nikisins
"""
#==============================================================================
# Import here:
from bob.pad.face.database import BatlPadDatabase
from torch import nn
#==============================================================================
# Define parameters here:
"""
Note: do not change names of the below constants.
"""
NUM_EPOCHS = 100 # Maximum number of epochs
BATCH_SIZE = 64 # Size of the batch
LEARNING_RATE = 1e-4 # Learning rate
NUM_WORKERS = 8 # The number of workers for the DataLoader
"""
Set the kwargs of the "dataset" instance of the DataFolder class.
Note: do not change the name ``kwargs``.
"""
# Initialize HLDI for data folder:
ORIGINAL_DIRECTORY = "" # this arguments is not important in this case
ORIGINAL_EXTENSION = ".h5" # extension of the data files
ANNOTATIONS_TEMP_DIR = "" # this argument is not used here
PROTOCOL = 'grandtest-color*infrared*depth-10' # 3 channels are used here, 10 frames
bob_hldi_instance = BatlPadDatabase(protocol=PROTOCOL,
original_directory=ORIGINAL_DIRECTORY,
original_extension=ORIGINAL_EXTENSION,
annotations_temp_dir=ANNOTATIONS_TEMP_DIR, # annotations computed here will not be saved because ANNOTATIONS_TEMP_DIR is empty string
landmark_detect_method="mtcnn", # detect annotations using mtcnn
exclude_attacks_list=['makeup'],
exclude_pai_all_sets=True, # exclude makeup from all the sets, which is the default behavior for grandtest protocol
append_color_face_roi_annot=False) # annotations defining ROI in the cropped face image are not important here
kwargs = {}
kwargs["data_folder"] = "NO NEED TO SET HERE, WILL BE SET IN THE TRAINING SCRIPT"
# NOTE: ``kwargs["transform"] = transform`` is re-defined below, after ``transform()`` method is defined
kwargs["transform"] = None # keep None for now, re-define below
kwargs["extension"] = '.hdf5'
kwargs["bob_hldi_instance"] = bob_hldi_instance
kwargs["hldi_type"] = "pad"
kwargs["groups"] = ['train']
kwargs["protocol"] = 'grandtest'
kwargs["purposes"] = ['real', 'attack']
kwargs["allow_missing_files"] = True
"""
Transformations to be applied to the input data sample.
Note: the variable or function name ``transform`` must be the same in
all configuration files. This transformation is handled in DataFolder.
"""
from bob.learn.pytorch.utils import MeanStdNormalizer
transform = MeanStdNormalizer(kwargs)
"""
Set the kwargs of the "dataset" instance of the DataFolder class.
Note: do not change the name ``kwargs``.
"""
# NOTE: re-define ``transform`` parameter, after we defined the ``transform`` method / object
# In this case transformation is mean-std normalization, given mean-std for each feature:
kwargs["transform"] = transform
"""
Define the network to be trained as a class, named ``Network``.
Note: Do not change the name of the below class.
"""
from bob.learn.pytorch.architectures import TwoLayerMLP as Network
"""
Define the kwargs to be used for ``Network`` in the dictionary namely
``network_kwargs``.
"""
network_kwargs = {}
network_kwargs['in_features'] = 1296
network_kwargs['n_hidden_relu'] = 10
network_kwargs['apply_sigmoid'] = True
"""
Define the loss to be used for training.
Note: do not change the name of the below variable.
"""
loss_type = nn.BCELoss()
"""
OPTIONAL: if not defined **above** loss will be computed in the training script.
See training script for details
Define the function to compute the loss. Don't change the signature of this
function: ``loss_function(output, input, target)``
"""
from bob.learn.pytorch.utils import weighted_bce_loss as loss_function
......@@ -41,7 +41,7 @@ def test_architectures():
output, emdedding = net.forward(t)
assert output.shape == torch.Size([1, 79077])
assert emdedding.shape == torch.Size([1, 256])
# LightCNN29
a = numpy.random.rand(1, 1, 128, 128).astype("float32")
t = torch.from_numpy(a)
......@@ -119,7 +119,7 @@ def test_transforms():
tt = ToTensor()
tt(sample)
assert isinstance(sample['image'], torch.Tensor)
# grayscale
# grayscale
image_gray = numpy.random.rand(128, 128).astype("uint8")
sample_gray = {'image': image_gray}
tt(sample_gray)
......@@ -253,7 +253,7 @@ def test_conv_autoencoder():
Test the ConvAutoencoder class.
"""
from bob.learn.pytorch.architectures import ConvAutoencoder
batch = torch.randn(1, 3, 64, 64)
model = ConvAutoencoder()
output = model(batch)
......@@ -290,3 +290,23 @@ def test_extractors():
output = extractor(data)
assert output.shape[0] == 256
def test_two_layer_mlp():
"""
Test the TwoLayerMLP class.
"""
from bob.learn.pytorch.architectures import TwoLayerMLP
batch = torch.randn(10, 1, 100)
model = TwoLayerMLP(in_features = 100,
n_hidden_relu = 10,
apply_sigmoid = True)
output = model(batch)
assert list(output.shape) == [10, 1]
model.apply_sigmoid = False
output = model(batch)
assert list(output.shape) == [10, 1]
#!/usr/bin/env python
# encoding: utf-8
import numpy as np
import torch
from bob.learn.pytorch.datasets import DataFolder
from torch.utils.data import DataLoader
from torch import nn
import logging
logger = logging.getLogger("bob.learn.pytorch")
def get_parameter(args, configuration, keyword, default):
""" Get the right value for a parameter
......@@ -23,7 +32,7 @@ def get_parameter(args, configuration, keyword, default):
The arguments given by the configuration file.
keyword: string
the keyword for the parameter to process (in the "configuration" style)
default:
default:
The default value of the parameter
Returns
......@@ -32,23 +41,23 @@ def get_parameter(args, configuration, keyword, default):
The right value for the given keyword argument
"""
# get the keyword in a "docopt" friendly format
args_kw = '--' + keyword.replace('_', '-')
# get the type of this argument
_type = type(default)
# get the default value
arg_default = default
# get the default value
arg_default = default
# get the argument in the configuration file
# get the argument in the configuration file
if hasattr(configuration, keyword):
arg_config = getattr(configuration, keyword)
# get the argument from the command-line
arg_command = _type(args[args_kw])
# if the argument was not specified in the config file
if not hasattr(configuration, keyword):
return arg_command
......@@ -58,3 +67,244 @@ def get_parameter(args, configuration, keyword, default):
else:
return arg_command
# =============================================================================
def comp_bce_loss_weights(target):
"""
Compute the balancing weights for the BCE loss function.
Arguments
---------
target : Tensor
Tensor containing class labels for each sample.
Tensor of the size: ``[num_patches]``
Returns
-------
weights : Tensor
A tensor containing the weights for each sample.
"""
weights = np.copy(target.cpu().numpy())
pos_weight = 1-np.sum(weights)/len(weights)
neg_weight = 1-pos_weight
weights[weights==1]=pos_weight
weights[weights==0]=neg_weight
# weights = weights/np.sum(weights)
weights = torch.Tensor(weights)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
weights = weights.to(device)
return weights
# =============================================================================
def mean_std_normalize(features,
features_mean=None,
features_std=None):
"""
The features in the input 2D array are mean-std normalized.
The rows are samples, the columns are features. If ``features_mean``
and ``features_std`` are provided, then these vectors will be used for
normalization. Otherwise, the mean and std of the features is
computed on the fly.
Parameters
----------
features : 2D :py:class:`numpy.ndarray`
Array of features to be normalized.
features_mean : 1D :py:class:`numpy.ndarray`
Mean of the features. Default: None.
features_std : 2D :py:class:`numpy.ndarray`
Standart deviation of the features. Default: None.
Returns
-------
features_norm : 2D :py:class:`numpy.ndarray`
Normalized array of features.
features_mean : 1D :py:class:`numpy.ndarray`
Mean of the features.
features_std : 1D :py:class:`numpy.ndarray`
Standart deviation of the features.
"""
features = np.copy(features)
# Compute mean and std if not given:
if features_mean is None:
features_mean = np.mean(features, axis=0)
features_std = np.std(features, axis=0)
features_std[features_std==0.0]=1.0
row_norm_list = []
for row in features: # row is a sample
row_norm = (row - features_mean) / features_std
row_norm_list.append(row_norm)
features_norm = np.vstack(row_norm_list)
return features_norm, features_mean, features_std
# =============================================================================
def compute_mean_std_bf_class(kwargs):
"""
Compute mean-std normalization parameters using samples of the real class.
The mean-std parameters are computed sample-wise / each feature has
individual mean-std normalizers.
Parameters
----------
kwargs : dict
Kwargs to initialize the DataFolder class.
Returns
-------
features_mean: numpy array
1D numpy array containing mean of the features computed using bona-fide
samples of the training set.
features_std: numpy array
1D numpy array containing std of the features computed using bona-fide
samples of the training set.
"""
kwargs_copy = kwargs.copy()
def transform(x): return x # don't do any transformationson on data
kwargs_copy["transform"] = transform # no transformation
kwargs_copy["purposes"] = ['real'] # use only real samples
data_folder = DataFolder(**kwargs_copy) # initialize the DataFolder with new arguments
dataloader = DataLoader(dataset = data_folder,
batch_size = data_folder.__len__(), # load all samples
shuffle = False)
all_real_data = next(iter(dataloader)) # get all data as tensor
all_real_data_np = all_real_data[0].numpy().squeeze() # get data in numpy format
# the dimensions of "all_real_data_np" now is (n_samples x n_features)
features_norm, features_mean, features_std = mean_std_normalize(all_real_data_np)
return features_mean, features_std
# =============================================================================
class MeanStdNormalizer():
"""
The functionality of this class can be split into sub-tasks:
1. When **first** called, the mean-std normalization parameters are
pre-computed using **bona-fide** samples from the training set of the
database defined above.
2. In the next calls, the pre-computed mean-std normalizers are used
for normalization of the of the input training feature vectors.
Arguments
---------
kwargs : dict
The kwargs used to inintialize an instance of the DataFolder class.
"""
def __init__(self, kwargs):
self.kwargs = kwargs
self.features_mean = None
self.features_std = None
def __call__(self, x):
"""
Pre-compute normalizers and use them for mean-std normalization.
Also, converts normalized features to Tensors.
Arguments
---------
x : 1D :py:class:`numpy.ndarray`
Feature vector to be normalizaed. The size is ``(n_features, )``
Returns
-------
x_norm : :py:class:`torch.Tensor`
Normalized feature vector of the size ``(1, n_features)``
"""
if self.features_mean is None or self.features_std is None: # pre-compute normalization parameters
logger.info ("Computing mean-std normalization parameters using real samples of the training set")
# compute the normalization parameters on the fly:
features_mean, features_std = compute_mean_std_bf_class(self.kwargs)
# save normalization parameters:
logger.info ("Setting the normalization parameters")
self.features_mean = features_mean
self.features_std = features_std
# normalize the sample
x_norm, _, _ = mean_std_normalize(features = np.expand_dims(x, axis=0),
features_mean = self.features_mean,
features_std = self.features_std)
x_norm.squeeze()
return torch.Tensor(x_norm).unsqueeze(0)
# =============================================================================
def weighted_bce_loss(output, img, target):
"""
Returns a weighted BCE loss.
Parameters
----------
output : :py:class:`torch.Tensor`
Tensor of the size: ``[num_patches, 1]``
img : :py:class:`torch.Tensor`
This argument is not used in current loss function, but is here to
match the signature expected by the training script.
target : :py:class:`torch.Tensor`
Tensor containing class labels for each sample in ``img``.
Tensor of the size: ``[num_patches]``
Returns
-------
loss : :py:class:`torch.Tensor`
Tensor containing loss value.
"""
loss_type = nn.BCELoss()
target = target.float() # make sure the target is float, not int
# convert "target" tensor from size [num_patches] to [num_patches, 1], to match "output" dimensions:
target = target.view(-1, 1)
weight = comp_bce_loss_weights(target)
loss_type.weight = weight
loss = loss_type(output, target)
return loss
......@@ -14,7 +14,7 @@ As an example, to train an autoencoder on facial images extracted from the Celeb
.. code-block:: sh
./bin/train_autoencoder.py \ # script used for autoencoders training, can be used for other networks as-well
./bin/train_network.py \ # script used for autoencoders training, can be used for other networks as-well
<FOLDER_CONTAINING_TRAINING_DATA> \ # substitute the path pointing to training data
<FOLDER_TO_SAVE_THE_RESULTS>/ \ # substitute the path to save the results to
-c autoencoder/net1_celeba.py \ # configuration file defining the AE, database, and training parameters
......@@ -29,7 +29,7 @@ People in Idiap can benefit from GPU cluster, running the training as follows:
--name <NAME_OF_EXPERIMENT> \ # define the name of th job (Idiap only)
--log-dir <FOLDER_TO_SAVE_THE_RESULTS>/logs/ \ # substitute the path to save the logs to (Idiap only)
--environment="PYTHONUNBUFFERED=1" -- \ #
./bin/train_autoencoder.py \ # script used for autoencoders training, cand be used for other networks as-well
./bin/train_network.py \ # script used for autoencoders training, cand be used for other networks as-well
<FOLDER_CONTAINING_TRAINING_DATA> \ # substitute the path pointing to training data
<FOLDER_TO_SAVE_THE_RESULTS>/ \ # substitute the path to save the results to
-c autoencoder/net1_celeba.py \ # configuration file defining the AE, database, and training parameters
......@@ -42,7 +42,7 @@ For a more detailed documentation of functionality available in the training scr
.. code-block:: sh
./bin/train_autoencoder.py --help # note: remove ./bin/ if buildout is not used
./bin/train_network.py --help # note: remove ./bin/ if buildout is not used
Please inspect the corresponding configuration file, ``net1_celeba.py`` for example, for more details on how to define the database, network architecture and training parameters.
......@@ -61,7 +61,7 @@ the following reconstructions produced by an autoencoder:
Autoencoder fine-tuning on the multi-channel facial data
===========================================================
This section is useful for those trying to reproduce the results form [NGM19]_, or for demonstrative purposes showing the capabilities of ``train_autoencoder.py`` script.
This section is useful for those trying to reproduce the results form [NGM19]_, or for demonstrative purposes showing the capabilities of ``train_network.py`` script.
Following the training procedure of [NGM19]_, one might want to fine-tune the pre-trained autoencoder on the multi-channel (**MC**) facial data.
In this example, MC training data is a stack of gray-scale, NIR, and Depth (BW-NIR-D) facial images extracted from WMCA face PAD database.
......@@ -74,7 +74,7 @@ autoencoder are fine-tuned.
.. code-block:: sh
./bin/train_autoencoder.py \ # script used for autoencoders training, can be used for other networks as-well
./bin/train_network.py \ # script used for autoencoders training, can be used for other networks as-well
<FOLDER_CONTAINING_TRAINING_DATA> \ # substitute the path pointing to training data
<FOLDER_TO_SAVE_THE_RESULTS>/ \ # substitute the path to save the results to
-p <FOLDER_CONTAINING_RGB_AE_MODELS>/model_70.pth \ # initialize the AE with the model obtained during RGB pre-training
......@@ -87,7 +87,7 @@ Below is the command allowing to fine-tine just **one layer of encoder**, which
.. code-block:: sh
./bin/train_autoencoder.py \ # script used for autoencoders training, can be used for other networks as-well
./bin/train_network.py \ # script used for autoencoders training, can be used for other networks as-well
<FOLDER_CONTAINING_TRAINING_DATA> \ # substitute the path pointing to training data
<FOLDER_TO_SAVE_THE_RESULTS>/ \ # substitute the path to save the results to
-p <FOLDER_CONTAINING_RGB_AE_MODELS>/model_70.pth \ # initialize the AE with the model obtained during RGB pre-training
......
.. py:currentmodule:: bob.learn.pytorch
=============================
Multilayer perceptron
=============================
Multilayer perceptron training
===========================================================
This section introduces a work-flow for training a Multilayer perceptron (MLP). Training details, as well as the structure of the MLP are identical to the ones introduced the following publication [NGM19]_. It is recommended to check the publication for better understanding of the below discussions.
In [NGM19]_, an MLP is trained on concatenation of latent embeddings of multiple autoencoders. For an explicit example on how to compute latent embeddings for MLP training, please refer to the section entitled **Multi-channel face PAD using autoencoders** in the documentation of ``bob.pad.face`` package. In general, latent embeddings of a sample is just a 1D numpy array.
As an example, to train an autoencoder on latent embeddings extracted from an entire multi-channel (BW-NIR-D) faces of WMCA database, one can use the following command:
.. code-block:: sh
./bin/train_network.py \ # script used for MLP training, can be used for other networks as-well
<FOLDER_CONTAINING_TRAINING_DATA> \ # substitute the path pointing to training data
<FOLDER_TO_SAVE_THE_RESULTS>/ \ # substitute the path to save the results to
-c mlp/batl_db_1296x10_relu_mlp.py \ # configuration file defining the database, training parameters, transformation to be applied to training data, and an MLP architecture
-cg bob.learn.pytorch.config \ # name of the group containing the configuration file
-cv \ # compute the loss on the cross-validation set as-well
-vv # set verbosity level
Please inspect the corresponding configuration file, ``batl_db_1296x10_relu_mlp.py``, for more details on how to define the database, network architecture and training parameters. Additionally, ``batl_db_1296x10_relu_mlp.py`` defines a transformation function, applying mean-std normalization to the input training samples.
After above script is completed, models are saved in ``<FOLDER_TO_SAVE_THE_RESULTS>`` directory, and one can select the "best" model having the lowest loss on the cross-validation set, by inspecting the log file located in the same directory.
.. note::
People in Idiap can benefit from GPU cluster, running training commands similar to an example from section on **Convolutional autoencoder**.
......@@ -16,6 +16,8 @@ Users Guide
guide_dcgan.rst
guide_conditionalgan.rst
guide_conv_autoencoder.rst
guide_mlp.rst
================
Reference Manual
......
......@@ -72,7 +72,7 @@ setup(
'train_cnn.py = bob.learn.pytorch.scripts.train_cnn:main',
'train_dcgan.py = bob.learn.pytorch.scripts.train_dcgan:main',
'train_conditionalgan.py = bob.learn.pytorch.scripts.train_conditionalgan:main',
'train_autoencoder.py = bob.learn.pytorch.scripts.train_autoencoder:main',
'train_network.py = bob.learn.pytorch.scripts.train_network:main',
],
},
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment