"""Principle Component Analysis (PCA) with PyTorch.""" # Copyright (C) 2020 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions # and limitations under the License. from typing import Union import torch from torch import Tensor from anomalib.models.components.base import DynamicBufferModule class PCA(DynamicBufferModule): """Principle Component Analysis (PCA). Args: n_components (float): Number of components. Can be either integer number of components or a ratio between 0-1. """ def __init__(self, n_components: Union[float, int]): super().__init__() self.n_components = n_components self.register_buffer("singular_vectors", Tensor()) self.register_buffer("mean", Tensor()) self.register_buffer("num_components", Tensor()) self.singular_vectors: Tensor self.singular_values: Tensor self.mean: Tensor self.num_components: Tensor def fit(self, dataset: Tensor) -> None: """Fits the PCA model to the dataset. Args: dataset (Tensor): Input dataset to fit the model. """ mean = dataset.mean(dim=0) dataset -= mean _, sig, v_h = torch.linalg.svd(dataset.double()) num_components: int if self.n_components <= 1: variance_ratios = torch.cumsum(sig * sig, dim=0) / torch.sum(sig * sig) num_components = torch.nonzero(variance_ratios >= self.n_components)[0] else: num_components = int(self.n_components) self.num_components = Tensor([num_components]) self.singular_vectors = v_h.transpose(-2, -1)[:, :num_components].float() self.singular_values = sig[:num_components].float() self.mean = mean def fit_transform(self, dataset: Tensor) -> Tensor: """Fit and transform PCA to dataset. Args: dataset (Tensor): Dataset to which the PCA if fit and transformed Returns: Transformed dataset """ mean = dataset.mean(dim=0) dataset -= mean num_components = int(self.n_components) self.num_components = Tensor([num_components]) v_h = torch.linalg.svd(dataset)[-1] self.singular_vectors = v_h.transpose(-2, -1)[:, :num_components] self.mean = mean return torch.matmul(dataset, self.singular_vectors) def transform(self, features: Tensor) -> Tensor: """Transforms the features based on singular vectors calculated earlier. Args: features (Tensor): Input features Returns: Transformed features """ features -= self.mean return torch.matmul(features, self.singular_vectors) def inverse_transform(self, features: Tensor) -> Tensor: """Inverses the transformed features. Args: features (Tensor): Transformed features Returns: Inverse features """ inv_features = torch.matmul(features, self.singular_vectors.transpose(-2, -1)) return inv_features def forward(self, features: Tensor) -> Tensor: """Transforms the features. Args: features (Tensor): Input features Returns: Transformed features """ return self.transform(features)