From b8f12e22d3299a930421e2aaa3f86be99dd4466a Mon Sep 17 00:00:00 2001
From: Manuel Guenther <manuel.guenther@idiap.ch>
Date: Mon, 1 Jun 2015 11:09:30 +0200
Subject: [PATCH] Added PLDA; improved build system

---
 bob/bio/base/algorithm/PLDA.py            | 163 +++++++
 bob/bio/base/algorithm/__init__.py        |   1 +
 bob/bio/base/config/algorithm/pca_plda.py |   9 +
 bob/bio/base/config/algorithm/plda.py     |   8 +
 bob/bio/base/test/data/plda_enroller.hdf5 | Bin 0 -> 34248 bytes
 bob/bio/base/test/data/plda_model.hdf5    | Bin 0 -> 5280 bytes
 bob/bio/base/test/test_algorithms.py      | 505 +++-------------------
 buildout.cfg                              |  37 +-
 requirements.txt                          |  15 +
 setup.py                                  |  14 +-
 10 files changed, 296 insertions(+), 456 deletions(-)
 create mode 100644 bob/bio/base/algorithm/PLDA.py
 create mode 100644 bob/bio/base/config/algorithm/pca_plda.py
 create mode 100644 bob/bio/base/config/algorithm/plda.py
 create mode 100644 bob/bio/base/test/data/plda_enroller.hdf5
 create mode 100644 bob/bio/base/test/data/plda_model.hdf5
 create mode 100644 requirements.txt

diff --git a/bob/bio/base/algorithm/PLDA.py b/bob/bio/base/algorithm/PLDA.py
new file mode 100644
index 00000000..d8661b7d
--- /dev/null
+++ b/bob/bio/base/algorithm/PLDA.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+# Laurent El Shafey <Laurent.El-Shafey@idiap.ch>
+
+import bob.core
+import bob.io.base
+import bob.learn.linear
+import bob.learn.em
+
+import numpy
+
+from .Algorithm import Algorithm
+import logging
+logger = logging.getLogger("bob.bio.base")
+
+
+class PLDA (Algorithm):
+  """Tool chain for computing PLDA (over PCA-dimensionality reduced) features"""
+
+  def __init__(
+      self,
+      subspace_dimension_of_f, # Size of subspace F
+      subspace_dimension_of_g, # Size of subspace G
+      subspace_dimension_pca = None,  # if given, perform PCA on data and reduce the PCA subspace to the given dimension
+      plda_training_iterations = 200, # Maximum number of iterations for the EM loop
+      # TODO: refactor the remaining parameters!
+      INIT_SEED = 5489, # seed for initializing
+      INIT_F_METHOD = 'BETWEEN_SCATTER',
+      INIT_G_METHOD = 'WITHIN_SCATTER',
+      INIT_S_METHOD = 'VARIANCE_DATA',
+      multiple_probe_scoring = 'joint_likelihood'
+  ):
+
+    """Initializes the local (PCA-)PLDA tool chain with the given file selector object"""
+    # call base class constructor and register that this class requires training for enrollment
+    Algorithm.__init__(
+        self,
+        requires_enroller_training = True,
+
+        subspace_dimension_of_f = subspace_dimension_of_f, # Size of subspace F
+        subspace_dimension_of_g = subspace_dimension_of_g, # Size of subspace G
+        subspace_dimension_pca = subspace_dimension_pca,  # if given, perform PCA on data and reduce the PCA subspace to the given dimension
+        plda_training_iterations = plda_training_iterations, # Maximum number of iterations for the EM loop
+        # TODO: refactor the remaining parameters!
+        INIT_SEED = INIT_SEED, # seed for initializing
+        INIT_F_METHOD = str(INIT_F_METHOD),
+        INIT_G_METHOD = str(INIT_G_METHOD),
+        INIT_S_METHOD =str(INIT_S_METHOD),
+        multiple_probe_scoring = multiple_probe_scoring,
+        multiple_model_scoring = None
+    )
+
+    self.subspace_dimension_of_f = subspace_dimension_of_f
+    self.subspace_dimension_of_g = subspace_dimension_of_g
+    self.subspace_dimension_pca = subspace_dimension_pca
+    self.plda_training_iterations = plda_training_iterations
+    self.score_set = {'joint_likelihood': 'joint_likelihood', 'average':numpy.average, 'min':min, 'max':max}[multiple_probe_scoring]
+
+    # TODO: refactor
+    self.plda_trainer = bob.learn.em.PLDATrainer()
+    self.plda_trainer.init_f_method = INIT_F_METHOD
+    self.plda_trainer.init_g_method = INIT_G_METHOD
+    self.plda_trainer.init_sigma_method = INIT_S_METHOD
+    self.rng = bob.core.random.mt19937(INIT_SEED)
+    self.pca_machine = None
+    self.plda_base = None
+
+
+
+  def _train_pca(self, training_set):
+    """Trains and returns a LinearMachine that is trained using PCA"""
+    data = numpy.vstack([feature for client in training_set for feature in client])
+
+    logger.info("  -> Training LinearMachine using PCA ")
+    trainer = bob.learn.linear.PCATrainer()
+    machine, _ = trainer.train(data)
+    # limit number of pcs
+    machine.resize(machine.shape[0], self.subspace_dimension_pca)
+    return machine
+
+  def _perform_pca_client(self, client):
+    """Perform PCA on an array"""
+    return numpy.vstack([self.pca_machine(feature) for feature in client])
+
+  def _perform_pca(self, training_set):
+    """Perform PCA on data"""
+    return [self._perform_pca_client(client) for client in training_set]
+
+
+  def train_enroller(self, training_features, projector_file):
+    """Generates the PLDA base model from a list of arrays (one per identity),
+       and a set of training parameters. If PCA is requested, it is trained on the same data.
+       Both the trained PLDABase and the PCA machine are written."""
+
+
+    # train PCA and perform PCA on training data
+    if self.subspace_dimension_pca is not None:
+      self.pca_machine = self._train_pca(training_features)
+      training_features = self._perform_pca(training_features)
+
+    input_dimension = training_features[0].shape[1]
+    logger.info("  -> Training PLDA base machine")
+
+    # train machine
+    self.plda_base = bob.learn.em.PLDABase(input_dimension, self.subspace_dimension_of_f, self.subspace_dimension_of_g)
+    bob.learn.em.train(self.plda_trainer, self.plda_base, training_features, self.plda_training_iterations, self.rng)
+
+    # write machines to file
+    proj_hdf5file = bob.io.base.HDF5File(str(projector_file), "w")
+    if self.subspace_dimension_pca is not None:
+      proj_hdf5file.create_group('/pca')
+      proj_hdf5file.cd('/pca')
+      self.pca_machine.save(proj_hdf5file)
+    proj_hdf5file.create_group('/plda')
+    proj_hdf5file.cd('/plda')
+    self.plda_base.save(proj_hdf5file)
+
+
+  def load_enroller(self, projector_file):
+    """Reads the PCA projection matrix and the PLDA model from file"""
+    # read enroller (PCA and PLDA matrix)
+    hdf5 = bob.io.base.HDF5File(projector_file)
+    if hdf5.has_group("/pca"):
+      hdf5.cd('/pca')
+      self.pca_machine = bob.learn.linear.Machine(hdf5)
+    hdf5.cd('/plda')
+    self.plda_base = bob.learn.em.PLDABase(hdf5)
+
+
+  def enroll(self, enroll_features):
+    """Enrolls the model by computing an average of the given input vectors"""
+    plda_machine = bob.learn.em.PLDAMachine(self.plda_base)
+    # project features, if enabled
+    if self.pca_machine is not None:
+      enroll_features = self._perform_pca_client(enroll_features)
+    # enroll
+    self.plda_trainer.enroll(plda_machine, enroll_features)
+    return plda_machine
+
+
+  def read_model(self, model_file):
+    """Reads the model, which in this case is a PLDA-Machine"""
+    # read machine and attach base machine
+    plda_machine = bob.learn.em.PLDAMachine(bob.io.base.HDF5File(model_file), self.plda_base)
+    return plda_machine
+
+
+  def score(self, model, probe):
+    """Computes the PLDA score for the given model and probe"""
+    return self.score_for_multiple_probes(model, [probe])
+
+
+  def score_for_multiple_probes(self, model, probes):
+    """This function computes the score between the given model and several given probe files.
+    In this base class implementation, it computes the scores for each probe file using the 'score' method,
+    and fuses the scores using the fusion method specified in the constructor of this class."""
+    if self.pca_machine is not None:
+      probes = [self.pca_machine(probe) for probe in probes]
+    # forward
+    if self.score_set == 'joint_likelihood':
+      return model.log_likelihood_ratio(numpy.vstack(probes))
+    else:
+      return self.score_set([model.log_likelihood_ratio(probe) for probe in probes])
diff --git a/bob/bio/base/algorithm/__init__.py b/bob/bio/base/algorithm/__init__.py
index d22a30f2..6baf97fd 100644
--- a/bob/bio/base/algorithm/__init__.py
+++ b/bob/bio/base/algorithm/__init__.py
@@ -1,4 +1,5 @@
 from .Algorithm import Algorithm
 from .PCA import PCA
 from .LDA import LDA
+from .PLDA import PLDA
 from .BIC import BIC
diff --git a/bob/bio/base/config/algorithm/pca_plda.py b/bob/bio/base/config/algorithm/pca_plda.py
new file mode 100644
index 00000000..2ff3a8e6
--- /dev/null
+++ b/bob/bio/base/config/algorithm/pca_plda.py
@@ -0,0 +1,9 @@
+#!/usr/bin/env python
+
+import bob.bio.base
+
+algorithm = bob.bio.base.algorithm.PLDA(
+    subspace_dimension_of_f = 16, # Size of subspace F
+    subspace_dimension_of_g = 16, # Size of subspace G
+    subspace_dimension_pca = 150   # Size of the PCA subspace
+)
diff --git a/bob/bio/base/config/algorithm/plda.py b/bob/bio/base/config/algorithm/plda.py
new file mode 100644
index 00000000..c3d362fb
--- /dev/null
+++ b/bob/bio/base/config/algorithm/plda.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+
+import bob.bio.base
+
+algorithm = bob.bio.base.algorithm.PLDA(
+    subspace_dimension_of_f = 16, # Size of subspace F
+    subspace_dimension_of_g = 16 # Size of subspace G
+)
diff --git a/bob/bio/base/test/data/plda_enroller.hdf5 b/bob/bio/base/test/data/plda_enroller.hdf5
new file mode 100644
index 0000000000000000000000000000000000000000..c242d7151b2b01a09bfd38940b4387c41af945b1
GIT binary patch
literal 34248
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

literal 0
HcmV?d00001

diff --git a/bob/bio/base/test/data/plda_model.hdf5 b/bob/bio/base/test/data/plda_model.hdf5
new file mode 100644
index 0000000000000000000000000000000000000000..d2d6285d520c1cfef63c46ca520ee4549a5bb652
GIT binary patch
literal 5280
zcmeD5aB<`1lHy_j0S*oZ76t(@6Gr@pf(0TF5f~pPp8#brLg@}Dy@CnCU}OM61_lYJ
zxFFPgbaf#?uC5F~l`!*RG*lad0Skm>023IcM^p%SxH<-aJiGzw>jo%2gB{AC5yi;B
z22Q^a0+d1_DVdQGlHy_M9!4_*(>I6>%0(PtH#vYwRv-@~&cwt7R>}c12gG1zOn@kZ
zss<|t5<*ZuNR6CgPACII159c_>0m#97e)qlh61Q7CJ5tq4mi?)N(uz=tBZh2HwfWZ
z=Kz&X00l4}01Z?I1!g>ofKsq>se%>1Is>S52OEBM#LAMMt{~|}P{AIWG8`C8xEW0T
zL;Z(4ePs}tzAA`yI!)9O3J+*?!T_V0q4|gO@F;+W2WkSs%x?o59<V^pi!V;hEyzhN
zh6?0m#21(5##dyP#3!YeBmx-_iSpFU^o)|!6p$=PBqu*TCo?-WCo>~IKLyB7Pt46t
zj5mUbC+FuCmz1Oy<pPDkATd5OFC{Y>ZV$MS0?NSBJwz4hB+Q%}Y#Px@y}$Q+eqwR|
zGD~6q+D|<vKi%@(pCFK?;O*{vU~5>e!O#Ei?dSA2q&7BAw3`j3d#ZQL`T-PYNR$+O
zlofLT7XPHXWK_p!2#kinXb6mkz-S1Jh5)@nfKa;*);@sI<hSb@hG;ui0~{5MkZvKo
zeG6;npwr}sPX{!76i}ibv)!A4B2Q}${nE#ZA(}oQJyc@G8z4hukod*jAKU;92k3wr
PG=eeHK?AL$o+$GH;R}|Q

literal 0
HcmV?d00001

diff --git a/bob/bio/base/test/test_algorithms.py b/bob/bio/base/test/test_algorithms.py
index b732b1c9..9d7f0bb1 100644
--- a/bob/bio/base/test/test_algorithms.py
+++ b/bob/bio/base/test/test_algorithms.py
@@ -27,11 +27,7 @@ import pkg_resources
 
 regenerate_refs = False
 
-#seed_value = 5489
-
-import sys
-_mac_os = sys.platform == 'darwin'
-
+seed_value = 5489
 
 import scipy.spatial
 
@@ -279,446 +275,59 @@ def test_bic():
   assert abs(bic1.score_for_multiple_probes(model, [probe, probe]) - reference_score) < 1e-5
 
 
-"""
-  def test01_gabor_jet(self):
-    # read input
-    extractor = facereclib.utils.tests.configuration_file('grid-graph', 'feature_extractor', 'features')
-    feature = extractor.read_feature(self.input_dir('graph_regular.hdf5'))
-    tool = self.config('gabor-jet')
-    self.assertFalse(tool.performs_projection)
-    self.assertFalse(tool.requires_enroller_training)
-
-    # enroll
-    model = tool.enroll([feature])
-    # execute the preprocessor
-    if regenerate_refs:
-      tool.save_model(model, self.reference_dir('graph_model.hdf5'))
-    reference = tool.read_model(self.reference_dir('graph_model.hdf5'))
-    self.assertEqual(len(model), 1)
-    for n in range(len(model[0])):
-      self.assertTrue((numpy.abs(model[0][n].abs - reference[0][n].abs) < 1e-5).all())
-      self.assertTrue((numpy.abs(model[0][n].phase - reference[0][n].phase) < 1e-5).all())
-
-    # score
-    sim = tool.score(model, feature)
-    self.assertAlmostEqual(sim, 1.)
-    self.assertAlmostEqual(tool.score_for_multiple_probes(model, [feature, feature]), 1.)
-
-    # test averaging
-    tool = facereclib.tools.GaborJets(
-      "PhaseDiffPlusCanberra",
-      gabor_sigma = math.sqrt(2.) * math.pi,
-      multiple_feature_scoring = "average_model"
-    )
-    model = tool.enroll([feature, feature])
-
-    # absoulte values must be identical
-    for n in range(len(model)):
-      self.assertTrue((numpy.abs(model[n].abs - reference[0][n].abs) < 1e-5).all())
-    # phases might differ with 2 Pi
-    for n in range(len(model)):
-      for j in range(len(model[n].phase)):
-        self.assertTrue(abs(model[n].phase[j] - reference[0][n].phase[j]) < 1e-5 or abs(model[n].phase[j] - reference[0][n].phase[j] + 2*math.pi) < 1e-5 or abs(model[n].phase[j] - reference[0][n].phase[j] - 2*math.pi) < 1e-5)
-
-    sim = tool.score(model, feature)
-    self.assertAlmostEqual(sim, 1.)
-    self.assertAlmostEqual(tool.score_for_multiple_probes(model, [feature, feature]), 1.)
-
-
-
-  def test02_lgbphs(self):
-    # read input
-    feature1 = facereclib.utils.load(self.input_dir('lgbphs_sparse.hdf5'))
-    feature2 = facereclib.utils.load(self.input_dir('lgbphs_no_phase.hdf5'))
-    tool = self.config('lgbphs')
-    self.assertFalse(tool.performs_projection)
-    self.assertFalse(tool.requires_enroller_training)
-
-    # enroll model
-    model = tool.enroll([feature1])
-    self.compare(model, 'lgbphs_model.hdf5')
-
-    # score
-    sim = tool.score(model, feature2)
-    self.assertAlmostEqual(sim, 40960.)
-    self.assertAlmostEqual(tool.score_for_multiple_probes(model, [feature2, feature2]), sim)
-
-
-
-
-
-
-
-  def test06_gmm(self):
-    # read input
-    feature = facereclib.utils.load(self.input_dir('dct_blocks.hdf5'))
-    # assure that the config file is readable
-    tool = self.config('gmm')
-    self.assertTrue(isinstance(tool, facereclib.tools.UBMGMM))
-
-    # here, we use a reduced complexity for test purposes
-    tool = facereclib.tools.UBMGMM(
-        number_of_gaussians = 2,
-        k_means_training_iterations = 1,
-        gmm_training_iterations = 1,
-        INIT_SEED = seed_value,
-    )
-    self.assertTrue(tool.performs_projection)
-    self.assertTrue(tool.requires_projector_training)
-    self.assertFalse(tool.use_projected_features_for_enrollment)
-    self.assertFalse(tool.split_training_features_by_client)
-
-    # train the projector
-    t = tempfile.mkstemp('ubm.hdf5', prefix='frltest_')[1]
-    tool.train_projector(facereclib.utils.tests.random_training_set(feature.shape, count=5, minimum=-5., maximum=5.), t)
-    if regenerate_refs:
-      import shutil
-      shutil.copy2(t, self.reference_dir('gmm_projector.hdf5'))
-
-    # load the projector file
-    tool.load_projector(self.reference_dir('gmm_projector.hdf5'))
-    # compare GMM projector with reference
-    new_machine = bob.learn.em.GMMMachine(bob.io.base.HDF5File(t))
-    self.assertTrue(tool.m_ubm.is_similar_to(new_machine))
-    os.remove(t)
-
-    # project the feature
-    projected = tool.project(feature)
-    if regenerate_refs:
-      projected.save(bob.io.base.HDF5File(self.reference_dir('gmm_feature.hdf5'), 'w'))
-    probe = tool.read_probe(self.reference_dir('gmm_feature.hdf5'))
-    self.assertTrue(projected.is_similar_to(probe))
-
-    # enroll model with the unprojected feature
-    model = tool.enroll([feature])
-    if regenerate_refs:
-      model.save(bob.io.base.HDF5File(self.reference_dir('gmm_model.hdf5'), 'w'))
-    reference_model = tool.read_model(self.reference_dir('gmm_model.hdf5'))
-    self.assertTrue(model.is_similar_to(reference_model))
-
-    # score with projected feature and compare to the weird reference score ...
-    sim = tool.score(reference_model, probe)
-    self.assertAlmostEqual(sim, 0.25472347774)
-    self.assertAlmostEqual(tool.score_for_multiple_probes(model, [probe, probe]), sim)
-
-
-  def test06a_gmm_regular(self):
-    # read input
-    feature = facereclib.utils.load(self.input_dir('dct_blocks.hdf5'))
-    # assure that the config file is readable
-    tool = self.config('ubm_gmm_regular_scoring')
-    self.assertTrue(isinstance(tool, facereclib.tools.UBMGMMRegular))
-
-    # here, we use a reduced complexity for test purposes
-    tool = facereclib.tools.UBMGMMRegular(
-        number_of_gaussians = 2,
-        k_means_training_iterations = 1,
-        gmm_training_iterations = 1,
-        INIT_SEED = seed_value
-    )
-    self.assertFalse(tool.performs_projection)
-    self.assertTrue(tool.requires_enroller_training)
-
-    # train the enroller
-    t = tempfile.mkstemp('ubm.hdf5', prefix='frltest_')[1]
-    tool.train_enroller(facereclib.utils.tests.random_training_set(feature.shape, count=5, minimum=-5., maximum=5.), t)
-    # assure that it is identical to the normal UBM projector
-    tool.load_enroller(self.reference_dir('gmm_projector.hdf5'))
-
-    # enroll model with the unprojected feature
-    model = tool.enroll([feature])
-    reference_model = tool.read_model(self.reference_dir('gmm_model.hdf5'))
-    self.assertTrue(model.is_similar_to(reference_model))
-
-    # score with unprojected feature and compare to the weird reference score ...
-    probe = tool.read_probe(self.input_dir('dct_blocks.hdf5'))
-    sim = tool.score(reference_model, probe)
-
-    self.assertAlmostEqual(sim, 0.143875716)
-
-
-  def test07_isv(self):
-    # read input
-    feature = facereclib.utils.load(self.input_dir('dct_blocks.hdf5'))
-    # assure that the config file is readable
-    tool = self.config('isv')
-    self.assertTrue(isinstance(tool, facereclib.tools.ISV))
-
-    # Here, we use a reduced complexity for test purposes
-    tool = facereclib.tools.ISV(
-        number_of_gaussians = 2,
-        subspace_dimension_of_u = 160,
-        k_means_training_iterations = 1,
-        gmm_training_iterations = 1,
-        isv_training_iterations = 1,
-        INIT_SEED = seed_value
-    )
-    self.assertTrue(tool.performs_projection)
-    self.assertTrue(tool.requires_projector_training)
-    self.assertTrue(tool.use_projected_features_for_enrollment)
-    self.assertTrue(tool.split_training_features_by_client)
-    self.assertFalse(tool.requires_enroller_training)
-
-    # train the projector
-    t = tempfile.mkstemp('ubm.hdf5', prefix='frltest_')[1]
-    tool.train_projector(facereclib.utils.tests.random_training_set_by_id(feature.shape, count=5, minimum=-5., maximum=5.), t)
-    if regenerate_refs:
-      import shutil
-      shutil.copy2(t, self.reference_dir('isv_projector.hdf5'))
-
-    # load the projector file
-    tool.load_projector(self.reference_dir('isv_projector.hdf5'))
-
-    # compare ISV projector with reference
-    hdf5file = bob.io.base.HDF5File(t)
-    hdf5file.cd('Projector')
-    projector_reference = bob.learn.em.GMMMachine(hdf5file)
-    self.assertTrue(tool.m_ubm.is_similar_to(projector_reference))
-
-    # compare ISV enroller with reference
-    hdf5file.cd('/')
-    hdf5file.cd('Enroller')
-    enroller_reference = bob.learn.em.ISVBase(hdf5file)
-    enroller_reference.ubm = projector_reference
-    if not _mac_os:
-      self.assertTrue(tool.m_isvbase.is_similar_to(enroller_reference))
-    os.remove(t)
-
-    # project the feature
-    projected = tool.project(feature)
-    if regenerate_refs:
-      tool.save_feature(projected, self.reference_dir('isv_feature.hdf5'))
-
-    # compare the projected feature with the reference
-    projected_reference = tool.read_feature(self.reference_dir('isv_feature.hdf5'))
-    self.assertTrue(projected[0].is_similar_to(projected_reference))
-
-    # enroll model with the projected feature
-    model = tool.enroll([projected[0]])
-    if regenerate_refs:
-      model.save(bob.io.base.HDF5File(self.reference_dir('isv_model.hdf5'), 'w'))
-    reference_model = tool.read_model(self.reference_dir('isv_model.hdf5'))
-    # compare the ISV model with the reference
-    self.assertTrue(model.is_similar_to(reference_model))
-
-    # check that the read_probe function reads the correct values
-    probe = tool.read_probe(self.reference_dir('isv_feature.hdf5'))
-    self.assertTrue(probe[0].is_similar_to(projected[0]))
-    self.assertEqual(probe[1].any(), projected[1].any())
-
-    # score with projected feature and compare to the weird reference score ...
-    sim = tool.score(model, probe)
-    self.assertAlmostEqual(sim, 0.002739667184506023)
-
-    # score with a concatenation of the probe
-    self.assertAlmostEqual(tool.score_for_multiple_probes(model, [probe, probe]), sim, places=5)
-
-
-  def test08_jfa(self):
-    # read input
-    feature = facereclib.utils.load(self.input_dir('dct_blocks.hdf5'))
-    # assure that the config file is readable
-    tool = self.config('jfa')
-    self.assertTrue(isinstance(tool, facereclib.tools.JFA))
-
-    # here, we use a reduced complexity for test purposes
-    tool = facereclib.tools.JFA(
-        number_of_gaussians = 2,
-        subspace_dimension_of_u = 2,
-        subspace_dimension_of_v = 2,
-        k_means_training_iterations = 1,
-        gmm_training_iterations = 1,
-        jfa_training_iterations = 1,
-        INIT_SEED = seed_value
-    )
-    self.assertTrue(tool.performs_projection)
-    self.assertTrue(tool.requires_projector_training)
-    self.assertTrue(tool.use_projected_features_for_enrollment)
-    self.assertFalse(tool.split_training_features_by_client)
-    self.assertTrue(tool.requires_enroller_training)
-
-    # train the projector
-    t = tempfile.mkstemp('ubm.hdf5', prefix='frltest_')[1]
-    tool.train_projector(facereclib.utils.tests.random_training_set(feature.shape, count=5, minimum=-5., maximum=5.), t)
-    if regenerate_refs:
-      import shutil
-      shutil.copy2(t, self.reference_dir('jfa_projector.hdf5'))
-
-    # load the projector file
-    tool.load_projector(self.reference_dir('jfa_projector.hdf5'))
-    # compare JFA projector with reference
-    new_machine = bob.learn.em.GMMMachine(bob.io.base.HDF5File(t))
-    self.assertTrue(tool.m_ubm.is_similar_to(new_machine))
-    os.remove(t)
-
-    # project the feature
-    projected = tool.project(feature)
-    if regenerate_refs:
-      projected.save(bob.io.base.HDF5File(self.reference_dir('jfa_feature.hdf5'), 'w'))
-    # compare the projected feature with the reference
-    projected_reference = tool.read_feature(self.reference_dir('jfa_feature.hdf5'))
-    self.assertTrue(projected.is_similar_to(projected_reference))
-
-    # train the enroller
-    t = tempfile.mkstemp('enroll.hdf5', prefix='frltest_')[1]
-    tool.train_enroller(self.train_gmm_stats(self.reference_dir('jfa_feature.hdf5'), count=5, minimum=-5., maximum=5.), t)
-    if regenerate_refs:
-      import shutil
-      shutil.copy2(t, self.reference_dir('jfa_enroller.hdf5'))
-    tool.load_enroller(self.reference_dir('jfa_enroller.hdf5'))
-    # compare JFA enroller with reference
-    enroller_reference = bob.learn.em.JFABase(bob.io.base.HDF5File(t))
-    enroller_reference.ubm = new_machine
-    if not _mac_os:
-      self.assertTrue(tool.m_jfabase.is_similar_to(enroller_reference))
-    os.remove(t)
-
-    # enroll model with the projected feature
-    model = tool.enroll([projected])
-    if regenerate_refs:
-      model.save(bob.io.base.HDF5File(self.reference_dir('jfa_model.hdf5'), 'w'))
-    # assert that the model is ok
-    reference_model = tool.read_model(self.reference_dir('jfa_model.hdf5'))
-    self.assertTrue(model.is_similar_to(reference_model))
-
-    # check that the read_probe function reads the requested data
-    probe = tool.read_probe(self.reference_dir('jfa_feature.hdf5'))
-    self.assertTrue(probe.is_similar_to(projected))
-
-    # score with projected feature and compare to the weird reference score ...
-    sim = tool.score(model, probe)
-    self.assertAlmostEqual(sim, 0.25473213400211353)
-    # score with a concatenation of the probe
-    # self.assertAlmostEqual(tool.score_for_multiple_probes(model, [probe, probe]), sim)
-
-
-  def test09_plda(self):
-    # read input
-    feature = facereclib.utils.load(self.input_dir('linearize.hdf5'))
-    # assure that the config file is readable
-    tool = self.config('pca+plda')
-    self.assertTrue(isinstance(tool, facereclib.tools.PLDA))
-
-    # here, we use a reduced complexity for test purposes
-    tool = facereclib.tools.PLDA(
-        subspace_dimension_of_f = 2,
-        subspace_dimension_of_g = 2,
-        subspace_dimension_pca = 10,
-        plda_training_iterations = 1,
-        INIT_SEED = seed_value,
-    )
-    self.assertFalse(tool.performs_projection)
-    self.assertTrue(tool.requires_enroller_training)
-
-    # train the projector
-    t = tempfile.mkstemp('pca+plda.hdf5', prefix='frltest_')[1]
-    tool.train_enroller(facereclib.utils.tests.random_training_set_by_id(feature.shape, count=20, minimum=0., maximum=255.), t)
-    if regenerate_refs:
-      import shutil
-      shutil.copy2(t, self.reference_dir('pca+plda_enroller.hdf5'))
-
-    # load the projector file
-    tool.load_enroller(self.reference_dir('pca+plda_enroller.hdf5'))
-    # compare the resulting machines
-    test_file = bob.io.base.HDF5File(t)
-    test_file.cd('/pca')
-    pca_machine = bob.learn.linear.Machine(test_file)
-    test_file.cd('/plda')
-    plda_machine = bob.learn.em.PLDABase(test_file)
-    # TODO: compare the PCA machines
-    #self.assertEqual(pca_machine, tool.m_pca_machine)
-    # TODO: compare the PLDA machines
-    #self.assertEqual(plda_machine, tool.m_plda_base_machine)
-    os.remove(t)
-
-    # enroll model
-    model = tool.enroll([feature])
-    if regenerate_refs:
-      model.save(bob.io.base.HDF5File(self.reference_dir('pca+plda_model.hdf5'), 'w'))
-    # TODO: compare the models with the reference
-    #reference_model = tool.read_model(self.reference_dir('pca+plda_model.hdf5'))
-    #self.assertEqual(model, reference_model)
-
-    # score
-    sim = tool.score(model, feature)
-    self.assertAlmostEqual(sim, 0.)
-    # score with a concatenation of the probe
-    self.assertAlmostEqual(tool.score_for_multiple_probes(model, [feature, feature]), 0.)
-
-
-  def test10_ivector(self):
-    # NOTE: This test will fail when it is run solely. Please always run all Tool tests in order to assure that they work.
-    # read input
-    feature = facereclib.utils.load(self.input_dir('dct_blocks.hdf5'))
-    # assure that the config file is readable
-    tool = self.config('ivector')
-    self.assertTrue(isinstance(tool, facereclib.tools.IVector))
-
-    # here, we use a reduced complexity for test purposes
-    tool = facereclib.tools.IVector(
-        number_of_gaussians = 2,
-        subspace_dimension_of_t=2,       # T subspace dimension
-        update_sigma = False, # TODO Do another test with True
-        tv_training_iterations = 1,  # Number of EM iterations for the JFA training
-        variance_threshold = 1e-5,
-        INIT_SEED = seed_value
-    )
-    self.assertTrue(tool.performs_projection)
-    self.assertTrue(tool.requires_projector_training)
-    self.assertTrue(tool.use_projected_features_for_enrollment)
-    self.assertFalse(tool.split_training_features_by_client)
-    self.assertFalse(tool.requires_enroller_training)
-
-    # train the projector
-    t = tempfile.mkstemp('ubm.hdf5', prefix='frltest_')[1]
-    tool.train_projector(facereclib.utils.tests.random_training_set(feature.shape, count=5, minimum=-5., maximum=5.), t)
-    if regenerate_refs:
-      import shutil
-      shutil.copy2(t, self.reference_dir('ivector_projector.hdf5'))
-
-    # load the projector file
-    tool.load_projector(self.reference_dir('ivector_projector.hdf5'))
-
-    # compare ISV projector with reference
-    hdf5file = bob.io.base.HDF5File(t)
-    hdf5file.cd('Projector')
-    projector_reference = bob.learn.em.GMMMachine(hdf5file)
-    self.assertTrue(tool.m_ubm.is_similar_to(projector_reference))
-
-    # compare ISV enroller with reference
-    hdf5file.cd('/')
-    hdf5file.cd('Enroller')
-    enroller_reference = bob.learn.em.IVectorMachine(hdf5file)
-    enroller_reference.ubm = projector_reference
-    if not _mac_os:
-      self.assertTrue(tool.m_tv.is_similar_to(enroller_reference))
-    os.remove(t)
-
-    # project the feature
-    projected = tool.project(feature)
-    if regenerate_refs:
-      tool.save_feature(projected, self.reference_dir('ivector_feature.hdf5'))
-
-    # compare the projected feature with the reference
-    projected_reference = tool.read_feature(self.reference_dir('ivector_feature.hdf5'))
-    self.assertTrue(numpy.allclose(projected,projected_reference))
-
-    # enroll model with the projected feature
-    # This is not yet supported
-    # model = tool.enroll([projected[0]])
-    # if regenerate_refs:
-    #  model.save(bob.io.HDF5File(self.reference_dir('ivector_model.hdf5'), 'w'))
-    #reference_model = tool.read_model(self.reference_dir('ivector_model.hdf5'))
-    # compare the IVector model with the reference
-    #self.assertTrue(model.is_similar_to(reference_model))
-
-    # check that the read_probe function reads the correct values
-    probe = tool.read_probe(self.reference_dir('ivector_feature.hdf5'))
-    self.assertTrue(numpy.allclose(probe,projected))
-
-    # score with projected feature and compare to the weird reference score ...
-    # This in not implemented yet
-
-    # score with a concatenation of the probe
-    # This is not implemented yet
-"""
+def test_plda():
+  temp_file = bob.io.base.test_utils.temporary_filename()
+  # assure that the configurations are loadable
+  plda1 = bob.bio.base.load_resource("plda", "algorithm")
+  assert isinstance(plda1, bob.bio.base.algorithm.PLDA)
+  assert isinstance(plda1, bob.bio.base.algorithm.Algorithm)
+  plda2 = bob.bio.base.load_resource("pca+plda", "algorithm")
+  assert isinstance(plda2, bob.bio.base.algorithm.PLDA)
+  assert isinstance(plda2, bob.bio.base.algorithm.Algorithm)
+
+  assert not plda1.performs_projection
+  assert not plda1.requires_projector_training
+  assert not plda1.use_projected_features_for_enrollment
+  assert not plda1.split_training_features_by_client
+  assert plda1.requires_enroller_training
+
+  # generate a smaller PCA subspcae
+  plda3 = bob.bio.base.algorithm.PLDA(subspace_dimension_of_f = 2, subspace_dimension_of_g = 2, subspace_dimension_pca = 10, plda_training_iterations = 1, INIT_SEED = seed_value)
+
+  # create random training set
+  train_set = utils.random_training_set_by_id(200, count=20, minimum=0., maximum=255.)
+  # train the projector
+  reference_file = pkg_resources.resource_filename('bob.bio.base.test', 'data/plda_enroller.hdf5')
+  try:
+    # train projector
+    plda3.train_enroller(train_set, temp_file)
+    assert os.path.exists(temp_file)
+
+    if regenerate_refs: shutil.copy(temp_file, reference_file)
+
+    # check projection matrix
+    plda1.load_enroller(reference_file)
+    plda3.load_enroller(temp_file)
+
+    assert plda1.pca_machine.is_similar_to(plda3.pca_machine)
+    assert plda1.plda_base.is_similar_to(plda3.plda_base)
+
+  finally:
+    if os.path.exists(temp_file): os.remove(temp_file)
+
+  # generate and project random feature
+  feature = utils.random_array(200, 0., 255., seed=84)
+
+  # enroll model from random features
+  reference = pkg_resources.resource_filename('bob.bio.base.test', 'data/plda_model.hdf5')
+  model = plda1.enroll([feature])
+  # execute the preprocessor
+  if regenerate_refs:
+    plda1.write_model(model, reference)
+  reference = plda1.read_model(reference)
+  assert model.is_similar_to(reference)
+
+  # compare model with probe
+  reference_score = 0.
+  assert abs(plda1.score(model, feature) - reference_score) < 1e-5, "The scores differ: %3.8f, %3.8f" % (plda1.score(model, feature), reference_score)
+  assert abs(plda1.score_for_multiple_probes(model, [feature, feature]) - reference_score) < 1e-5
diff --git a/buildout.cfg b/buildout.cfg
index 6b701057..ea8f7790 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -5,15 +5,46 @@
 [buildout]
 parts = scripts
 eggs = bob.bio.base
-extensions = bob.buildout
-
-develop = .
+       gridtk
 
+extensions = bob.buildout
+             mr.developer
+auto-checkout = *
+develop = src/bob.extension
+          src/bob.blitz
+          src/bob.core
+          src/bob.io.base
+          src/bob.learn.activation
+          src/bob.math
+          src/bob.learn.linear
+          src/bob.sp
+          src/bob.learn.em
+          src/bob.measure
+          src/bob.db.verification.utils
+          src/bob.db.atnt
+          src/bob.io.image
+          .
+         
 ; options for bob.buildout
 debug = true
 verbose = true
 newest = false
 
+[sources]
+bob.extension = git https://github.com/bioidiap/bob.extension
+bob.blitz = git https://github.com/bioidiap/bob.blitz
+bob.core = git https://github.com/bioidiap/bob.core
+bob.io.base = git https://github.com/bioidiap/bob.io.base
+bob.learn.activation = git https://github.com/bioidiap/bob.learn.activation
+bob.math = git https://github.com/bioidiap/bob.math
+bob.sp = git https://github.com/bioidiap/bob.sp
+bob.learn.linear = git https://github.com/bioidiap/bob.learn.linear
+bob.learn.em = git https://github.com/bioidiap/bob.learn.em
+bob.measure = git https://github.com/bioidiap/bob.measure
+bob.db.verification.utils = git https://github.com/bioidiap/bob.db.verification.utils
+bob.db.atnt = git https://github.com/bioidiap/bob.db.atnt
+bob.io.image = git https://github.com/bioidiap/bob.io.image
+
 [scripts]
 recipe = bob.buildout:scripts
 dependent-scripts = true
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..754f8e27
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,15 @@
+setuptools
+bob.extension
+bob.blitz
+bob.core
+bob.io.base
+bob.learn.activation
+bob.math
+bob.learn.linear
+bob.sp
+bob.learn.em
+bob.measure
+bob.db.verification.utils
+bob.db.atnt  # for test purposes
+bob.io.image # for test purposes
+matplotlib   # for plotting
diff --git a/setup.py b/setup.py
index fbe07a67..f0d76157 100644
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,11 @@
 # allows you to test your package with new python dependencies w/o requiring
 # administrative interventions.
 
-from setuptools import setup, find_packages
+from setuptools import setup, find_packages, dist
+dist.Distribution(dict(setup_requires=['bob.extension']))
+
+from bob.extension.utils import load_requirements
+install_requires = load_requirements()
 
 # The only thing we do in this file is to call the setup() function with all
 # parameters that define our package.
@@ -64,9 +68,7 @@ setup(
     # on the current system will be installed locally and only visible to the
     # scripts of this package. Don't worry - You won't need administrative
     # privileges when using buildout.
-    install_requires = [
-      'setuptools',
-    ],
+    install_requires = install_requires,
 
     # Your project should be called something like 'bob.<foo>' or
     # 'bob.<foo>.<bar>'. To implement this correctly and still get all your
@@ -122,7 +124,9 @@ setup(
         'dummy             = bob.bio.base.test.dummy.algorithm:algorithm', # for test purposes only
         'pca               = bob.bio.base.config.algorithm.pca:algorithm',
         'lda               = bob.bio.base.config.algorithm.lda:algorithm',
-        'pca+lda           = bob.bio.base.config.algorithm.lda:algorithm',
+        'pca+lda           = bob.bio.base.config.algorithm.pca_lda:algorithm',
+        'plda              = bob.bio.base.config.algorithm.plda:algorithm',
+        'pca+plda          = bob.bio.base.config.algorithm.pca_plda:algorithm',
         'bic               = bob.bio.base.config.algorithm.bic:algorithm',
       ],
 
-- 
GitLab