Datasets & Data Formats#
PyTorch Network Loader provides the BaseDataset class for creating custom datasets in a format
compatible with NetLoader architectures, along with additional useful classes and functions for data formatting.
Data Requirements for NetLoader Architectures#
NetLoader follows the standard PyTorch dataset conventions (see the PyTorch Datasets tutorial for more information), but with some specific requirements for compatibility with NetLoader architectures. The basic requirements for a custom dataset are:
__len__method that returns the number of samples in the dataset.__getitem__method that returns a tuple of: ID, low-dimensional data (or None), high-dimensional data, and extra (or None) for a single sample.
High-dimensional data should always be set, if there is only one set of data (such as in unsupervised learning) then the low-dimensional data can be set to None. The ID is a unique identifier for each sample which can be used for tracking samples in the training and validation data loaders. The extra field can be used to store any additional information about the sample that can be used in the loss function of the architecture.
Custom Datasets from BaseDataset#
BaseDataset provides a convenient base class for creating custom datasets compatible with
BaseArchitecture architectures.
The attributes of the BaseDataset object include:
- class BaseDataset(*args, **kwargs)[source]
Bases:
Dataset[tuple[int,DataListT,DataListT,Any]],Generic[DataListT]Base dataset class for use with BaseNetwork.
- Attributes:
- extra
list[Any] |ArrayLike|NoneType Additional data for each sample in the dataset of length N with shape (N,…) and type Any
- idxs
ndarray Index for each sample in the dataset with shape (N) and type int
- low_dim
DataListT|NoneType Low dimensional data for each sample in the dataset with shape (N,…)
- high_dim
DataListT|NoneType High dimensional data for each sample in the dataset with shape (N,…), this is required
- extra
The minimum requirement to use BaseDataset is to inherit BaseDataset
and define the attribute high_dim, which if an architecture only uses one set of data, it should use the
high_dim attribute.
However, depending on the architecture, the dataset may also need to define the low_dim and extra attributes.
The idxs attribute is set to a list of indices for the samples in the dataset by default, but can be overridden if
needed.
The BaseDataset class also has the following methods:
- BaseDataset.get_extra(idx)[source]
Gets extra data for the sample of the given index
- Parameters:
- idx
int Sample index
- idx
- Returns:
AnySample extra data
- BaseDataset.get_high_dim(idx)[source]
Gets a high dimensional sample of the given index
- BaseDataset.get_low_dim(idx)[source]
Gets a low dimensional sample of the given index
- BaseDataset.step(epoch)[source]
Step the dataset for each training iteration.
- Parameters:
- epoch
float Current epoch number
- epoch
Along with the magic methods:
- BaseDataset.__getitem__(idx)[source]
- BaseDataset.__len__()[source]
Returns the number of samples in the dataset
- Returns:
intNumber of samples in the dataset
If pre-processing of the low-dimensional, high-dimensional, or extra data is needed for each time a sample is accessed
(such as data augmentation), the methods get_low_dim, get_high_dim, and get_extra can be overridden to
perform the necessary pre-processing, static pre-processing should be applied to the saved data, in the __init__
method, or after the dataset has been initialised and transformed with transforms from transforms
(see Transforms for more information).
To create a custom dataset, inherit from BaseDataset and define the necessary attributes and
methods based on the requirements of the architecture.
For example, if using the Encoder architecture, the dataset should define
the low_dim attribute as the labels and the high_dim attribute as the input data:
import pickle
import torch
from torchvision.transforms import v2
from netloader.data import BaseDataset
class CustomDataset(BaseDataset):
def __init__(self, data_dir: str) -> None:
super().__init__()
with open(data_dir, 'rb') as file:
self.low_dim, self.high_dim = pickle.load(file) # e.g. labels & images
# Data augmentations
self.aug = v2.Compose([
v2.RandomHorizontalFlip(),
v2.RandomVerticalFlip(),
])
def get_high_dim(self, idx: int) -> torch.Tensor:
# Apply augmentations to the high-dimensional data
return self.aug(self.high_dim[idx])
If any parameters in the dataset should be updated during training, such as the level of noise induced in the dataset,
the method step can be implemented to update the necessary parameters at the end of each batch iteration during the
training of the architecture:
from multiprocessing import Value
from multiprocessing.sharedctypes import Synchronized
class CustomDataset(BaseDataset):
max_epochs: int = 100
max_noise: float = 0.1
def __init__(self, data_dir: str) -> None:
super().__init__()
# Shared value for the noise level so that all workers in the data loader can access and
# update it
self.noise: Synchronized[float] = Value('f', 0.0)
with open(data_dir, 'rb') as file:
self.low_dim, self.high_dim = pickle.load(file)
def step(self, epoch: float) -> None:
# Update the noise level at the end of each batch iteration
self.noise.value = min(self.max_noise, self.max_noise * epoch / self.max_epochs)
def get_high_dim(self, idx: int) -> torch.Tensor:
# Add noise to the high-dimensional data
return self.high_dim[idx] + torch.randn_like(high_dim_data) * self.noise_level.value
Data with Uncertainties#
If the dataset contains uncertainties, the data can be stored as a Data object, which contains
the data and the corresponding uncertainties that can be naturally transformed using transforms (see
Transforms for more information) and passed to the architectures; however, currently the architectures do not
fully support the use of Data objects and will instead concatenate the data and uncertainties
as a Tensor.
The Data class can be initialised with the following parameters:
- Data.__init__(data, uncertainty=None)[source]
The attributes of the BaseDataset object include:
- class Data(data, uncertainty=None)[source]
-
Stores data with uncertainties for uncertainty handling in BaseArchitecture.
See Data for the methods of Data class.
import torch
from netloader.data import Data
from netloader import transforms
values: torch.Tensor = torch.tensor([1.0, 2.0, 3.0])
uncertainties: torch.Tensor = torch.tensor([0.1, 0.2, 0.3])
data: Data = Data(values, uncertainty=uncertainties)
trans_data: Data = transforms.Log()(data)
Multiple Inputs/Outputs#
If the architecture has multiple inputs or outputs, the dataset can store the multi-data as a
DataList object.
DataList objects can store ndarray, Tensor, or
Data objects where each array or Data object has the same number of
samples (i.e. the same length along the first dimension).
Some network layers from layers support DataList objects as inputs and/or
outputs, such as Unpack (see Layers for more information).
The DataList class can be initialised with the following parameters:
The DataList class also has the following magic methods:
- DataList.__getitem__(idx)[source]
Gets a subset of each element in the DataList.
- DataList.__iter__()[source]
Iterates over each element in the DataList.
- DataList.__len__()[source]
Returns the number of samples in each element.
- Returns:
intNumber of samples in each element
Along with matching methods:
- DataList.get(idx, list_=False)[source]
Gets a subset of the DataList or a subset of each element in the DataList.
- Overloads:
- DataList.iter(list_=True)[source]
Iterates over each element in the DataList.
- Overloads:
- iter(self, list_: Literal[True]) Iterator[DataT]
- DataList.len(list_=True)[source]
Gets the length of the DataList or the length of each element in the DataList.
The methods above have the list_ argument, which specifies if the method should act on the list containing the data,
or the data itself.
For example, DataList.len(True) will return the number of data arrays in the list, while DataList.len(False)
will return the number of samples in each data array.
For the magic methods, the list_ argument cannot be specified, so the default behaviour follows:
__getitem__:list_=False, returns a DataList with each data array indexed at the specified index.__iter__:list_=True, iterates over the data arrays in the list.__len__:list_=False, returns the number of samples in each data array.
See Data for the methods of DataList class.
import torch
import numpy as np
from netloader.data import Data, DataList
data1: np.ndarray = np.random.rand(4, 3, 10)
data2: torch.Tensor = torch.arange(12).view(4, 3)
data3: Data = Data(torch.ones((4, 3)), uncertainty=torch.zeros((4, 3)))
data_list: DataList = DataList([data1, data2, data3])
assert len(data_list) == 4
assert [tuple(data.shape) for data in data_list] == [(4, 3, 10), (4, 3), (4, 3)]
assert [tuple(data.shape) for data in data_list[0]] == [(3, 10), (3,), (3,)]