Source code for peal.sparse_dictionaries.singular_value_decomposition
"""Truncated SVD (PCA) as the simplest PEAL sparse dictionary.
The principal directions of a feature matrix serve as a dense, non-sparse
baseline dictionary that adaptors such as DiDAE can search for class-relevant
directions in the same way they search an SAE or MSAE dictionary.
"""
from typing import Union
import torch
from peal.sparse_dictionaries.interfaces import SparseDictionary, SparseDictionaryConfig
[docs]
class SVDDictionaryConfig(SparseDictionaryConfig):
"""Config for :class:`SVDDictionary`.
Parameters
----------
n_components : int or None, optional
Number of singular vectors to keep; ``None`` keeps all. Default 10.
sparse_dictionaries_type : str
Factory key, fixed to ``"SVDDictionary"``.
ending : str
File ending used for the saved weights. Default ``".npz"``.
"""
n_components: Union[int, None] = 10
sparse_dictionaries_type: str = "SVDDictionary"
ending: str = ".npz"
[docs]
class SVDDictionary(SparseDictionary):
"""Dictionary whose components are the top right-singular vectors of the data.
``fit`` stores ``U``, ``S`` and ``Vt`` of the (already centred) feature
matrix truncated to ``config.n_components``; ``fit_from_dataloaders``
additionally records the feature mean ``mu`` that was subtracted.
Parameters
----------
config : SVDDictionaryConfig, optional
Number of components and factory metadata.
Attributes
----------
U, S, Vt : torch.Tensor or None
Truncated SVD factors, ``None`` until fitted.
mu : torch.Tensor or None
Feature mean subtracted before the SVD (only set by
``fit_from_dataloaders``).
"""
[docs]
def __init__(self, config=SVDDictionaryConfig()):
"""Store the config and leave the SVD factors unfitted (``None``)."""
self.config = config
self.U = None
self.S = None
self.Vt = None
self.mu = None
[docs]
def fit(self, X):
"""Compute the truncated SVD of a feature matrix.
Parameters
----------
X : torch.Tensor
Feature matrix of shape ``(n_samples, n_features)``; no centring
is applied here.
"""
# Perform SVD on the input data matrix X
U, S, Vt = torch.linalg.svd(X, full_matrices=False)
if self.config.n_components is None:
n_components = S.shape[0]
else:
n_components = self.config.n_components
self.U = U[:, :n_components]
self.S = S[:n_components]
self.Vt = Vt[:n_components, :]
[docs]
def fit_from_dataloaders(self, dataloaders, feature_extractor):
"""Extract features for every batch, centre them and call ``fit``.
Parameters
----------
dataloaders : iterable of torch.utils.data.DataLoader
Loaders whose first batch element is the input image; it is moved
to CUDA before feature extraction.
feature_extractor : callable
Maps a batch of inputs to a ``(batch, n_features)`` tensor.
"""
X_list = []
# derive which device to use from feature extractor
for dataloader in dataloaders:
for batch in dataloader:
X_list.append(feature_extractor(batch[0].cuda()).cpu())
X = torch.cat(X_list, dim=0)
self.mu = torch.mean(X, dim=0)
self.fit(X - self.mu)
[docs]
def get_components(self):
"""Return the dictionary as a ``(n_features, n_components)`` matrix."""
return self.Vt.transpose(0, 1)
[docs]
def save_on_disk(self, path):
"""Save ``U``, ``S``, ``Vt`` and ``mu`` with ``torch.save`` to ``path``."""
torch.save({"U": self.U, "S": self.S, "Vt": self.Vt, "mu": self.mu}, path)
[docs]
def load_from_disk(self, path):
"""Restore the factors written by :meth:`save_on_disk` from ``path``."""
checkpoint = torch.load(path)
self.U = checkpoint["U"]
self.S = checkpoint["S"]
self.Vt = checkpoint["Vt"]
self.mu = checkpoint["mu"]