From 95c678d992e4cf29dcd174c200af85458c13d480 Mon Sep 17 00:00:00 2001 From: Yannick DAYER <yannick.dayer@idiap.ch> Date: Fri, 22 Oct 2021 14:39:04 +0200 Subject: [PATCH] Using python GMM from bob.learn.em --- bob/bio/gmm/__init__.py | 1 + bob/bio/gmm/algorithm/__init__.py | 8 +- bob/bio/gmm/bioalgorithm/GMM.py | 385 ++++++++++++++++++ bob/bio/gmm/bioalgorithm/__init__.py | 25 ++ bob/bio/gmm/config/bioalgorithm/__init__.py | 0 bob/bio/gmm/config/bioalgorithm/gmm.py | 3 + .../gmm/config/bioalgorithm/gmm_regular.py | 5 + bob/bio/gmm/test/data/gmm_projected.hdf5 | Bin 5280 -> 6232 bytes bob/bio/gmm/test/data/gmm_projector.hdf5 | Bin 11176 -> 10256 bytes bob/bio/gmm/test/test_algorithms.py | 50 ++- setup.py | 4 + 11 files changed, 456 insertions(+), 25 deletions(-) create mode 100644 bob/bio/gmm/bioalgorithm/GMM.py create mode 100644 bob/bio/gmm/bioalgorithm/__init__.py create mode 100644 bob/bio/gmm/config/bioalgorithm/__init__.py create mode 100644 bob/bio/gmm/config/bioalgorithm/gmm.py create mode 100644 bob/bio/gmm/config/bioalgorithm/gmm_regular.py diff --git a/bob/bio/gmm/__init__.py b/bob/bio/gmm/__init__.py index c020e48..24f76d9 100644 --- a/bob/bio/gmm/__init__.py +++ b/bob/bio/gmm/__init__.py @@ -1,4 +1,5 @@ from . import algorithm # noqa: F401 +from . import bioalgorithm # noqa: F401 from . import test # noqa: F401 diff --git a/bob/bio/gmm/algorithm/__init__.py b/bob/bio/gmm/algorithm/__init__.py index 046970f..fc5f4fe 100644 --- a/bob/bio/gmm/algorithm/__init__.py +++ b/bob/bio/gmm/algorithm/__init__.py @@ -1,5 +1,5 @@ -from .GMM import GMM -from .GMM import GMMRegular +# from .GMM import GMM +# from .GMM import GMMRegular from .ISV import ISV from .IVector import IVector from .JFA import JFA @@ -22,8 +22,8 @@ def __appropriate__(*args): __appropriate__( - GMM, - GMMRegular, + # GMM, + # GMMRegular, JFA, ISV, IVector, diff --git a/bob/bio/gmm/bioalgorithm/GMM.py b/bob/bio/gmm/bioalgorithm/GMM.py new file mode 100644 index 0000000..23d9ebc --- /dev/null +++ b/bob/bio/gmm/bioalgorithm/GMM.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# Manuel Guenther <Manuel.Guenther@idiap.ch> + +"""Interface between the lower level GMM classes and the Algorithm Transformer. + +Implements the enroll and score methods using the low level GMM implementation. + +This adds the notions of models, probes, enrollment, and scores to GMM. +""" + + +import logging + +from typing import Callable + +import numpy + +from sklearn.base import BaseEstimator + +import bob.core +import bob.io.base + +from bob.bio.base.pipelines.vanilla_biometrics.abstract_classes import BioAlgorithm +from bob.learn.em.mixture import GMMMachine +from bob.learn.em.mixture import GMMStats +from bob.learn.em.mixture import linear_scoring + +logger = logging.getLogger(__name__) + + +class GMM(BioAlgorithm, BaseEstimator): + """Algorithm for computing UBM and Gaussian Mixture Models of the features. + + Features must be normalized to zero mean and unit standard deviation. + + Models are MAP GMM machines trained from a UBM on the enrollment feature set. + + The UBM is a ML GMM machine trained on the training feature set. + + Probes are GMM statistics of features projected on the UBM. + """ + + def __init__( + self, + # parameters for the GMM + number_of_gaussians: int, + # parameters of UBM training + kmeans_training_iterations: int = 25, # Maximum number of iterations for K-Means + ubm_training_iterations: int = 25, # Maximum number of iterations for GMM Training + training_threshold: float = 5e-4, # Threshold to end the ML training + variance_threshold: float = 5e-4, # Minimum value that a variance can reach + update_weights: bool = True, + update_means: bool = True, + update_variances: bool = True, + # parameters of the GMM enrollment + relevance_factor: float = 4, # Relevance factor as described in Reynolds paper + gmm_enroll_iterations: int = 1, # Number of iterations for the enrollment phase + responsibility_threshold: float = 0, # If set, the weight of a particular Gaussian will at least be greater than this threshold. In the case the real weight is lower, the prior mean value will be used to estimate the current mean and variance. + init_seed: int = 5489, + # scoring + scoring_function: Callable = linear_scoring, + # n_threads=None, + ): + """Initializes the local UBM-GMM tool chain. + + Parameters + ---------- + number_of_gaussians + The number of Gaussians used in the UBM and the models. + kmeans_training_iterations + Number of e-m iterations to train k-means initializing the UBM. + ubm_training_iterations + Number of e-m iterations for training the UBM. + training_threshold + Convergence threshold to halt the GMM training early. + variance_threshold + Minimum value a variance of the Gaussians can reach. + update_weights + Decides wether the weights of the Gaussians are updated while training. + update_means + Decides wether the means of the Gaussians are updated while training. + update_variances + Decides wether the variancess of the Gaussians are updated while training. + relevance_factor + Relevance factor as described in Reynolds paper. + gmm_enroll_iterations + Number of iterations for the MAP GMM used for enrollment. + responsibility_threshold + If set, the weight of a particular Gaussian will at least be greater than + this threshold. In the case where the real weight is lower, the prior mean + value will be used to estimate the current mean and variance. + init_seed + Seed for the random number generation. + scoring_function + Function returning a score from a model, a UBM, and a probe. + """ + + # call base class constructor and register that this tool performs projection + # super().__init__(score_reduction_operation=??) + + # copy parameters + self.number_of_gaussians = number_of_gaussians + self.kmeans_training_iterations = kmeans_training_iterations + self.ubm_training_iterations = ubm_training_iterations + self.training_threshold = training_threshold + self.variance_threshold = variance_threshold + self.update_weights = update_weights + self.update_means = update_means + self.update_variances = update_variances + self.relevance_factor = relevance_factor + self.gmm_enroll_iterations = gmm_enroll_iterations + self.init_seed = init_seed + self.rng = bob.core.random.mt19937(self.init_seed) # TODO + self.responsibility_threshold = responsibility_threshold + self.scoring_function = scoring_function + + self.ubm = None + + def _check_feature(self, feature): + """Checks that the features are appropriate""" + if ( + not isinstance(feature, numpy.ndarray) + or feature.ndim != 2 + or feature.dtype != numpy.float64 + ): + raise ValueError("The given feature is not appropriate") + if self.ubm is not None and feature.shape[1] != self.ubm.shape[1]: + raise ValueError( + "The given feature is expected to have %d elements, but it has %d" + % (self.ubm.shape[1], feature.shape[1]) + ) + + ####################################################### + # UBM training # + + def train_ubm(self, array): + + logger.debug(" .... Training UBM with %d feature vectors", array.shape[0]) + + logger.debug(" .... Creating UBM machine") + self.ubm = GMMMachine( + n_gaussians=self.number_of_gaussians, + trainer="ml", + max_fitting_steps=self.ubm_training_iterations, + convergence_threshold=self.training_threshold, + update_means=self.update_means, + update_variances=self.update_variances, + update_weights=self.update_weights, + # TODO more params? + ) + + # Trains the GMM + logger.info(" -> Training UBM GMM") + # Resetting the pseudo random number generator so we can have the same initialization for serial and parallel execution. + # self.rng = bob.core.random.mt19937(self.init_seed) + self.ubm.fit(array) + + def save_ubm(self, projector_file): + """Saves the projector to file""" + # Saves the UBM to file + logger.debug(" .... Saving model to file '%s'", projector_file) + + hdf5 = ( + projector_file + if isinstance(projector_file, bob.io.base.HDF5File) + else bob.io.base.HDF5File(projector_file, "w") + ) + self.ubm.save(hdf5) + + def train_projector(self, train_features, projector_file): + """Computes the Universal Background Model from the training ("world") data""" + [self._check_feature(feature) for feature in train_features] + + logger.info( + " -> Training UBM model with %d training files", len(train_features) + ) + + # Loads the data into an array + array = numpy.vstack(train_features) + + self.train_ubm(array) + + self.save_ubm(projector_file) + + ####################################################### + # GMM training using UBM # + + def load_ubm(self, ubm_file): + hdf5file = bob.io.base.HDF5File(ubm_file) + # read UBM + self.ubm = GMMMachine.from_hdf5(hdf5file) + self.ubm.variance_thresholds = self.variance_threshold + + def load_projector(self, projector_file): + """Reads the UBM model from file""" + # read UBM + self.load_ubm(projector_file) + # prepare MAP_GMM_Trainer + # kwargs = ( + # dict( + # mean_var_update_responsibilities_threshold=self.responsibility_threshold + # ) + # if self.responsibility_threshold > 0.0 + # else dict() + # ) + # self.enroll_trainer = bob.learn.em.MAP_GMMTrainer( + # self.ubm, + # relevance_factor=self.relevance_factor, + # update_means=True, + # update_variances=False, + # **kwargs + # ) + self.rng = bob.core.random.mt19937(self.init_seed) + + def project_ubm(self, array): + logger.debug(" .... Projecting %d feature vectors", array.shape[0]) + # Accumulates statistics + gmm_stats = GMMStats(self.ubm.shape[0], self.ubm.shape[1]) + self.ubm.acc_statistics(array, gmm_stats) + + # return the resulting statistics + return gmm_stats + + def project(self, feature): + """Computes GMM statistics against a UBM, given an input 2D numpy.ndarray of feature vectors""" + self._check_feature(feature) + return self.project_ubm(feature) + + def read_gmm_stats(self, gmm_stats_file): + """Reads GMM stats from file.""" + return GMMStats.from_hdf5(bob.io.base.HDF5File(gmm_stats_file)) + + def read_feature(self, feature_file): + """Read the type of features that we require, namely GMM_Stats""" + return self.read_gmm_stats(feature_file) + + def write_feature(self, feature, feature_file): + """Write the features (GMM_Stats)""" + return feature.save(feature_file) + + def enroll_gmm(self, array): + logger.debug(" .... Enrolling with %d feature vectors", array.shape[0]) + # TODO responsibility_threshold + gmm = GMMMachine( + n_gaussians=self.number_of_gaussians, + trainer="map", + ubm=self.ubm, + convergence_threshold=self.training_threshold, + max_fitting_steps=self.gmm_enroll_iterations, + random_state=self.rng, # TODO + update_means=True, + update_variances=True, # TODO default? + update_weights=True, # TODO default? + ) + gmm.variance_thresholds = self.variance_threshold + gmm = gmm.fit(array) + return gmm + + def enroll(self, data): + """Enrolls a GMM using MAP adaptation, given a list of 2D numpy.ndarray's of feature vectors""" + [self._check_feature(feature) for feature in data] + array = numpy.vstack(data) + # Use the array to train a GMM and return it + return self.enroll_gmm(array) + + ###################################################### + # Feature comparison # + def read_model(self, model_file): + """Reads the model, which is a GMM machine""" + return GMMMachine.from_hdf5(bob.io.base.HDF5File(model_file)) + + def score(self, biometric_reference: GMMMachine, data: GMMStats): + """Computes the score for the given model and the given probe. + + Uses the scoring function passed during initialization. + + Parameters + ---------- + biometric_reference: + The model to score against. + data: + The probe data to compare to the model. + """ + + assert isinstance(biometric_reference, GMMMachine) # TODO is it a list? + assert isinstance(data, GMMStats) + return self.scoring_function( + models_means=[biometric_reference], + ubm=self.ubm, + test_stats=data, + frame_length_normalisation=True, + )[0, 0] + + def score_multiple_biometric_references( + self, biometric_references: "list[GMMMachine]", data: GMMStats + ): + """Computes the score between multiple models and one probe. + + Uses the scoring function passed during initialization. + + Parameters + ---------- + biometric_references: + The models to score against. + data: + The probe data to compare to the models. + """ + + assert isinstance(biometric_references, GMMMachine) # TODO is it a list? + assert isinstance(data, GMMStats) + return self.scoring_function( + models_means=biometric_references, + ubm=self.ubm, + test_stats=data, + frame_length_normalisation=True, + ) + + # def score_for_multiple_probes(self, model, probes): + # """This function computes the score between the given model and several given probe files.""" + # assert isinstance(model, GMMMachine) + # for probe in probes: + # assert isinstance(probe, GMMStats) + # # logger.warn("Please verify that this function is correct") + # return self.probe_fusion_function( + # self.scoring_function( + # model.means, self.ubm, probes, [], frame_length_normalisation=True + # ) + # ) + + def fit(self, X, y=None, **kwargs): + """Trains the UBM.""" + self.train_ubm(X) + return self + + def transform(self, X, **kwargs): + """Passthrough. Enroll applies a different transform as score.""" + return X + + +class GMMRegular(GMM): + """Algorithm for computing Universal Background Models and Gaussian Mixture Models of the features""" + + def __init__(self, **kwargs): + """Initializes the local UBM-GMM tool chain with the given file selector object""" + # logger.warn("This class must be checked. Please verify that I didn't do any mistake here. I had to rename 'train_projector' into a 'train_enroller'!") + # initialize the UBMGMM base class + GMM.__init__(self, **kwargs) + # register a different set of functions in the Tool base class + BioAlgorithm.__init__( + self, requires_enroller_training=True, performs_projection=False + ) + + ####################################################### + # UBM training # + + def train_enroller(self, train_features, enroller_file): + """Computes the Universal Background Model from the training ("world") data""" + train_features = [feature for client in train_features for feature in client] + return self.train_projector(train_features, enroller_file) + + ####################################################### + # GMM training using UBM # + + def load_enroller(self, enroller_file): + """Reads the UBM model from file""" + return self.load_projector(enroller_file) + + ###################################################### + # Feature comparison # + def score(self, model, probe): + """Computes the score for the given model and the given probe. + The score are Log-Likelihood. + Therefore, the log of the likelihood ratio is obtained by computing the following difference.""" + + assert isinstance(model, GMMMachine) + self._check_feature(probe) + score = sum( + model.log_likelihood(probe[i, :]) - self.ubm.log_likelihood(probe[i, :]) + for i in range(probe.shape[0]) + ) + return score / probe.shape[0] + + def score_for_multiple_probes(self, model, probes): + raise NotImplementedError("Implement Me!") diff --git a/bob/bio/gmm/bioalgorithm/__init__.py b/bob/bio/gmm/bioalgorithm/__init__.py new file mode 100644 index 0000000..e1b44bc --- /dev/null +++ b/bob/bio/gmm/bioalgorithm/__init__.py @@ -0,0 +1,25 @@ +from .GMM import GMM +from .GMM import GMMRegular + + +# gets sphinx autodoc done right - don't remove it +def __appropriate__(*args): + """Says object was actually declared here, and not in the import module. + Fixing sphinx warnings of not being able to find classes, when path is shortened. + Parameters: + + *args: An iterable of objects to modify + + Resolves `Sphinx referencing issues + <https://github.com/sphinx-doc/sphinx/issues/3048>` + """ + + for obj in args: + obj.__module__ = __name__ + + +__appropriate__( + GMM, + GMMRegular, +) +__all__ = [_ for _ in dir() if not _.startswith("_")] diff --git a/bob/bio/gmm/config/bioalgorithm/__init__.py b/bob/bio/gmm/config/bioalgorithm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bob/bio/gmm/config/bioalgorithm/gmm.py b/bob/bio/gmm/config/bioalgorithm/gmm.py new file mode 100644 index 0000000..58aeddd --- /dev/null +++ b/bob/bio/gmm/config/bioalgorithm/gmm.py @@ -0,0 +1,3 @@ +import bob.bio.gmm + +bioalgorithm = bob.bio.gmm.bioalgorithm.GMM(number_of_gaussians=512) diff --git a/bob/bio/gmm/config/bioalgorithm/gmm_regular.py b/bob/bio/gmm/config/bioalgorithm/gmm_regular.py new file mode 100644 index 0000000..f7166b5 --- /dev/null +++ b/bob/bio/gmm/config/bioalgorithm/gmm_regular.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +import bob.bio.gmm + +bioalgorithm = bob.bio.gmm.bioalgorithm.GMMRegular(number_of_gaussians=512) diff --git a/bob/bio/gmm/test/data/gmm_projected.hdf5 b/bob/bio/gmm/test/data/gmm_projected.hdf5 index 31d930b955098e3ae990c1e2509d2c232d1a86be..ba17796e7d8d0a7374edc3a9ae067447043feedc 100644 GIT binary patch delta 2086 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 Io49cr0QyQXsQ>@~ delta 1834 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 diff --git a/bob/bio/gmm/test/data/gmm_projector.hdf5 b/bob/bio/gmm/test/data/gmm_projector.hdf5 index 4c47be97a009e963d25301904a7420eced1b55e9..d39d6920f1e7d335eddb7ce2c2be0ba892aa0541 100644 GIT binary patch literal 10256 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 literal 11176 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 M>nTFqIoQ%O0P^5SCIA2c diff --git a/bob/bio/gmm/test/test_algorithms.py b/bob/bio/gmm/test/test_algorithms.py index 7cb0bb5..e0375ec 100644 --- a/bob/bio/gmm/test/test_algorithms.py +++ b/bob/bio/gmm/test/test_algorithms.py @@ -24,6 +24,7 @@ import sys import numpy import pkg_resources +import pytest import bob.bio.gmm import bob.io.base @@ -32,9 +33,9 @@ import bob.learn.linear from bob.bio.base.test import utils -logger = logging.getLogger("bob.bio.gmm") +logger = logging.getLogger(__name__) -regenerate_refs = False +regenerate_refs = True seed_value = 5489 @@ -72,25 +73,30 @@ def _compare_complex( assert numpy.allclose(d, r, atol=1e-5) +@pytest.mark.isolated_gmm def test_gmm(): - temp_file = bob.io.base.test_utils.temporary_filename() + temp_file = ( + "./temptest/test_file" # TODO bob.io.base.test_utils.temporary_filename() + ) gmm1 = bob.bio.base.load_resource( - "gmm", "algorithm", preferred_package="bob.bio.gmm" + "gmm", "bioalgorithm", preferred_package="bob.bio.gmm" ) - assert isinstance(gmm1, bob.bio.gmm.algorithm.GMM) - assert isinstance(gmm1, bob.bio.base.algorithm.Algorithm) - assert gmm1.performs_projection - assert gmm1.requires_projector_training - assert not gmm1.use_projected_features_for_enrollment - assert not gmm1.split_training_features_by_client - assert not gmm1.requires_enroller_training + assert isinstance(gmm1, bob.bio.gmm.bioalgorithm.GMM) + assert isinstance( + gmm1, bob.bio.base.pipelines.vanilla_biometrics.abstract_classes.BioAlgorithm + ) + # assert gmm1.performs_projection + # assert gmm1.requires_projector_training + # assert not gmm1.use_projected_features_for_enrollment + # assert not gmm1.split_training_features_by_client + # assert not gmm1.requires_enroller_training # create smaller GMM object - gmm2 = bob.bio.gmm.algorithm.GMM( + gmm2 = bob.bio.gmm.bioalgorithm.GMM( number_of_gaussians=2, kmeans_training_iterations=1, - gmm_training_iterations=1, - INIT_SEED=seed_value, + ubm_training_iterations=1, + init_seed=seed_value, ) train_data = utils.random_training_set( @@ -120,7 +126,7 @@ def test_gmm(): # generate and project random feature feature = utils.random_array((20, 45), -5.0, 5.0, seed=84) projected = gmm1.project(feature) - assert isinstance(projected, bob.learn.em.GMMStats) + assert isinstance(projected, bob.learn.em.mixture.GMMStats) _compare( projected, pkg_resources.resource_filename("bob.bio.gmm.test", "data/gmm_projected.hdf5"), @@ -131,7 +137,7 @@ def test_gmm(): # enroll model from random features enroll = utils.random_training_set((20, 45), 5, -5.0, 5.0, seed=21) model = gmm1.enroll(enroll) - assert isinstance(model, bob.learn.em.GMMMachine) + assert isinstance(model, bob.learn.em.mixture.GMMMachine) _compare( model, pkg_resources.resource_filename("bob.bio.gmm.test", "data/gmm_model.hdf5"), @@ -159,16 +165,18 @@ def test_gmm_regular(): gmm1 = bob.bio.base.load_resource( "gmm-regular", "algorithm", preferred_package="bob.bio.gmm" ) - assert isinstance(gmm1, bob.bio.gmm.algorithm.GMMRegular) - assert isinstance(gmm1, bob.bio.gmm.algorithm.GMM) - assert isinstance(gmm1, bob.bio.base.algorithm.Algorithm) + assert isinstance(gmm1, bob.bio.gmm.bioalgorithm.GMMRegular) + assert isinstance(gmm1, bob.bio.gmm.bioalgorithm.GMM) + assert isinstance( + gmm1, bob.bio.base.pipelines.vanilla_biometrics.abstract_classes.BioAlgorithm + ) assert not gmm1.performs_projection assert not gmm1.requires_projector_training assert not gmm1.use_projected_features_for_enrollment assert gmm1.requires_enroller_training # create smaller GMM object - gmm2 = bob.bio.gmm.algorithm.GMMRegular( + gmm2 = bob.bio.gmm.bioalgorithm.GMMRegular( number_of_gaussians=2, kmeans_training_iterations=1, gmm_training_iterations=1, @@ -202,7 +210,7 @@ def test_gmm_regular(): # enroll model from random features enroll = utils.random_training_set((20, 45), 5, -5.0, 5.0, seed=21) model = gmm1.enroll(enroll) - assert isinstance(model, bob.learn.em.GMMMachine) + assert isinstance(model, bob.learn.em.mixture.GMMMachine) _compare( model, pkg_resources.resource_filename("bob.bio.gmm.test", "data/gmm_model.hdf5"), diff --git a/setup.py b/setup.py index 3b512f4..a69ee95 100644 --- a/setup.py +++ b/setup.py @@ -110,6 +110,10 @@ setup( "ivector-plda = bob.bio.gmm.config.algorithm.ivector_plda:algorithm", "ivector-lda-wccn-plda = bob.bio.gmm.config.algorithm.ivector_lda_wccn_plda:algorithm", ], + "bob.bio.bioalgorithm": [ + "gmm = bob.bio.gmm.config.bioalgorithm.gmm:bioalgorithm", + "gmm-regular = bob.bio.gmm.config.bioalgorithm.gmm_regular:bioalgorithm", + ], }, # Classifiers are important if you plan to distribute this package through # PyPI. You can find the complete list of classifiers that are valid and -- GitLab