Source code for iv_scoring

# -*- coding: utf-8 -*-
#
# This file is part of SIDEKIT.
#
# SIDEKIT is a python package for speaker verification.
# Home page: http://www-lium.univ-lemans.fr/sidekit/
#
# SIDEKIT is a python package for speaker verification.
# Home page: http://www-lium.univ-lemans.fr/sidekit/
#    
# SIDEKIT is free software: you can redistribute it and/or modify
# it under the terms of the GNU LLesser General Public License as 
# published by the Free Software Foundation, either version 3 of the License, 
# or (at your option) any later version.
#
# SIDEKIT is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with SIDEKIT.  If not, see <http://www.gnu.org/licenses/>.
"""
Copyright 2014-2019 Anthony Larcher and Sylvain Meignier

    :mod:`iv_scoring` provides methods to compare i-vectors
"""
import copy
import logging
import numpy
import scipy
from sidekit.bosaris import Ndx
from sidekit.bosaris import Scores
from sidekit.statserver import StatServer

import sys
if sys.version_info.major > 2:
    from functools import reduce


__license__ = "LGPL"
__author__ = "Anthony Larcher"
__copyright__ = "Copyright 2014-2019 Anthony Larcher"
__maintainer__ = "Anthony Larcher"
__email__ = "anthony.larcher@univ-lemans.fr"
__status__ = "Production"
__docformat__ = 'reStructuredText'


def _check_missing_model(enroll, test, ndx):
    # Remove missing models and test segments
    clean_ndx = ndx.filter(enroll.modelset, test.segset, True)

    # Align StatServers to match the clean_ndx
    enroll.align_models(clean_ndx.modelset)
    test.align_segments(clean_ndx.segset)

    return clean_ndx


[docs]def cosine_scoring(enroll, test, ndx, wccn=None, check_missing=True): """Compute the cosine similarities between to sets of vectors. The list of trials to perform is given in an Ndx object. :param enroll: a StatServer in which stat1 are i-vectors :param test: a StatServer in which stat1 are i-vectors :param ndx: an Ndx object defining the list of trials to perform :param wccn: numpy.ndarray, if provided, the i-vectors are normalized by using a Within Class Covariance Matrix :param check_missing: boolean, if True, check that all models and segments exist :return: a score object """ assert isinstance(enroll, StatServer), 'First parameter should be a StatServer' assert isinstance(test, StatServer), 'Second parameter should be a StatServer' assert isinstance(ndx, Ndx), 'Third parameter should be an Ndx' enroll_copy = copy.deepcopy(enroll) test_copy = copy.deepcopy(test) # If models are not unique, compute the mean per model, display a warning #if not numpy.unique(enroll_copy.modelset).shape == enroll_copy.modelset.shape: # logging.warning("Enrollment models are not unique, average i-vectors") # enroll_copy = enroll_copy.mean_stat_per_model() # Remove missing models and test segments if check_missing: clean_ndx = _check_missing_model(enroll_copy, test_copy, ndx) else: clean_ndx = ndx if wccn is not None: enroll_copy.rotate_stat1(wccn) if enroll_copy != test_copy: test_copy.rotate_stat1(wccn) # Cosine scoring enroll_copy.norm_stat1() if enroll_copy != test_copy: test_copy.norm_stat1() s = numpy.dot(enroll_copy.stat1, test_copy.stat1.transpose()) score = Scores() score.scoremat = s score.modelset = clean_ndx.modelset score.segset = clean_ndx.segset score.scoremask = clean_ndx.trialmask return score
[docs]def mahalanobis_scoring(enroll, test, ndx, m, check_missing=True): """Compute the mahalanobis distance between to sets of vectors. The list of trials to perform is given in an Ndx object. :param enroll: a StatServer in which stat1 are i-vectors :param test: a StatServer in which stat1 are i-vectors :param ndx: an Ndx object defining the list of trials to perform :param m: mahalanobis matrix as a ndarray :param check_missing: boolean, default is True, set to False not to check missing models :return: a score object """ assert isinstance(enroll, StatServer), 'First parameter should be a StatServer' assert isinstance(test, StatServer), 'Second parameter should be a StatServer' assert isinstance(ndx, Ndx), 'Third parameter should be an Ndx' assert enroll.stat1.shape[1] == test.stat1.shape[1], 'I-vectors dimension mismatch' assert enroll.stat1.shape[1] == m.shape[0], 'I-vectors and Mahalanobis matrix dimension mismatch' # If models are not unique, compute the mean per model, display a warning if not numpy.unique(enroll.modelset).shape == enroll.modelset.shape: logging.warning("Enrollment models are not unique, average i-vectors") enroll = enroll.mean_stat_per_model() # Remove missing models and test segments if check_missing: clean_ndx = _check_missing_model(enroll, test, ndx) else: clean_ndx = ndx # Mahalanobis scoring s = numpy.zeros((enroll.modelset.shape[0], test.segset.shape[0])) for i in range(enroll.modelset.shape[0]): diff = enroll.stat1[i, :] - test.stat1 s[i, :] = -0.5 * numpy.sum(numpy.dot(diff, m) * diff, axis=1) score = Scores() score.scoremat = s score.modelset = clean_ndx.modelset score.segset = clean_ndx.segset score.scoremask = clean_ndx.trialmask return score
[docs]def two_covariance_scoring(enroll, test, ndx, W, B, check_missing=True): """Compute the 2-covariance scores between to sets of vectors. The list of trials to perform is given in an Ndx object. Within and between class co-variance matrices have to be pre-computed. :param enroll: a StatServer in which stat1 are i-vectors :param test: a StatServer in which stat1 are i-vectors :param ndx: an Ndx object defining the list of trials to perform :param W: the within-class co-variance matrix to consider :param B: the between-class co-variance matrix to consider :param check_missing: boolean, default is True, set to False not to check missing models :return: a score object """ assert isinstance(enroll, StatServer), 'First parameter should be a directory' assert isinstance(test, StatServer), 'Second parameter should be a StatServer' assert isinstance(ndx, Ndx), 'Third parameter should be an Ndx' assert enroll.stat1.shape[1] == test.stat1.shape[1], 'I-vectors dimension mismatch' assert enroll.stat1.shape[1] == W.shape[0], 'I-vectors and co-variance matrix dimension mismatch' assert enroll.stat1.shape[1] == B.shape[0], 'I-vectors and co-variance matrix dimension mismatch' # If models are not unique, compute the mean per model, display a warning if not numpy.unique(enroll.modelset).shape == enroll.modelset.shape: logging.warning("Enrollment models are not unique, average i-vectors") enroll = enroll.mean_stat_per_model() # Remove missing models and test segments if check_missing: clean_ndx = _check_missing_model(enroll, test, ndx) else: clean_ndx = ndx # Two covariance scoring scoring S = numpy.zeros((enroll.modelset.shape[0], test.segset.shape[0])) iW = scipy.linalg.inv(W) iB = scipy.linalg.inv(B) G = reduce(numpy.dot, [iW, scipy.linalg.inv(iB + 2*iW), iW]) H = reduce(numpy.dot, [iW, scipy.linalg.inv(iB + iW), iW]) s2 = numpy.sum(numpy.dot(enroll.stat1, H) * enroll.stat1, axis=1) s3 = numpy.sum(numpy.dot(test.stat1, H) * test.stat1, axis=1) for ii in range(enroll.modelset.shape[0]): A = enroll.stat1[ii, :] + test.stat1 s1 = numpy.sum(numpy.dot(A, G) * A, axis=1) S[ii, :] = s1 - s3 - s2[ii] score = Scores() score.scoremat = S score.modelset = clean_ndx.modelset score.segset = clean_ndx.segset score.scoremask = clean_ndx.trialmask return score
[docs]def PLDA_scoring(enroll, test, ndx, mu, F, G, Sigma, test_uncertainty=None, Vtrans=None, p_known=0.0, scaling_factor=1., full_model=False): """Compute the PLDA scores between two sets of vectors. The list of trials to perform is given in an Ndx object. PLDA matrices have to be pre-computed. i-vectors are supposed to be whitened before. Implements the approach described in [Lee13]_ including scoring for partially open-set identification :param enroll: a StatServer in which stat1 are i-vectors :param test: a StatServer in which stat1 are i-vectors :param ndx: an Ndx object defining the list of trials to perform :param mu: the mean vector of the PLDA gaussian :param F: the between-class co-variance matrix of the PLDA :param G: the within-class co-variance matrix of the PLDA :param Sigma: the residual covariance matrix :param p_known: probability of having a known speaker for open-set identification case (=1 for the verification task and =0 for the closed-set case) :param scaling_factor: scaling factor to be multiplied by the sufficient statistics :param full_model: boolean, set to True when using a complete PLDA model (including within class covariance matrix) :return: a score object """ assert isinstance(enroll, StatServer), 'First parameter should be a StatServer' assert isinstance(test, StatServer), 'Second parameter should be a StatServer' assert isinstance(ndx, Ndx), 'Third parameter should be an Ndx' assert enroll.stat1.shape[1] == test.stat1.shape[1], 'I-vectors dimension mismatch' assert enroll.stat1.shape[1] == F.shape[0], 'I-vectors and co-variance matrix dimension mismatch' assert enroll.stat1.shape[1] == G.shape[0], 'I-vectors and co-variance matrix dimension mismatch' if not full_model: return fast_PLDA_scoring(enroll, test, ndx, mu, F, Sigma, test_uncertainty, Vtrans, p_known=p_known, scaling_factor=scaling_factor, check_missing=True) else: return full_PLDA_scoring(enroll, test, ndx, mu, F, G, Sigma, p_known=p_known, scaling_factor=scaling_factor)
[docs]def full_PLDA_scoring(enroll, test, ndx, mu, F, G, Sigma, p_known=0.0, scaling_factor=1., check_missing=True): """Compute PLDA scoring :param enroll: a StatServer in which stat1 are i-vectors :param test: a StatServer in which stat1 are i-vectors :param ndx: an Ndx object defining the list of trials to perform :param mu: the mean vector of the PLDA gaussian :param F: the between-class co-variance matrix of the PLDA :param G: the within-class co-variance matrix of the PLDA :param Sigma: the residual covariance matrix :param p_known: probability of having a known speaker for open-set identification case (=1 for the verification task and =0 for the closed-set case) :param check_missing: boolean, default is True, set to False not to check missing models """ enroll_copy = copy.deepcopy(enroll) test_copy = copy.deepcopy(test) # If models are not unique, compute the mean per model, display a warning # if not numpy.unique(enroll_copy.modelset).shape == enroll_copy.modelset.shape: # logging.warning("Enrollment models are not unique, average i-vectors") # enroll_copy = enroll_copy.mean_stat_per_model() # Remove missing models and test segments if check_missing: clean_ndx = _check_missing_model(enroll_copy, test_copy, ndx) else: clean_ndx = ndx # Center the i-vectors around the PLDA mean enroll_copy.center_stat1(mu) test_copy.center_stat1(mu) # Compute temporary matrices invSigma = scipy.linalg.inv(Sigma) I_iv = numpy.eye(mu.shape[0], dtype='float') I_ch = numpy.eye(G.shape[1], dtype='float') I_spk = numpy.eye(F.shape[1], dtype='float') A = numpy.linalg.inv(G.T.dot(invSigma * scaling_factor).dot(G) + I_ch) # keep numpy as interface are different B = F.T.dot(invSigma * scaling_factor).dot(I_iv - G.dot(A).dot(G.T).dot(invSigma * scaling_factor)) K = B.dot(F) K1 = scipy.linalg.inv(K + I_spk) K2 = scipy.linalg.inv(2 * K + I_spk) # Compute the Gaussian distribution constant alpha1 = numpy.linalg.slogdet(K1)[1] alpha2 = numpy.linalg.slogdet(K2)[1] constant = alpha2 / 2.0 - alpha1 # Compute verification scores score = Scores() score.scoremat = numpy.zeros(clean_ndx.trialmask.shape) score.modelset = clean_ndx.modelset score.segset = clean_ndx.segset score.scoremask = clean_ndx.trialmask # Project data in the space that maximizes the speaker separability test_tmp = B.dot(test_copy.stat1.T) enroll_tmp = B.dot(enroll_copy.stat1.T) # score qui ne dépend que du segment tmp1 = test_tmp.T.dot(K1) # Compute the part of the score that is only dependent on the test segment S1 = numpy.empty(test_copy.segset.shape[0]) for seg_idx in range(test_copy.segset.shape[0]): S1[seg_idx] = tmp1[seg_idx, :].dot(test_tmp[:, seg_idx])/2. # Compute the part of the score that depends only on the model (S2) and on both model and test segment S2 = numpy.empty(enroll_copy.modelset.shape[0]) for model_idx in range(enroll_copy.modelset.shape[0]): mod_plus_test_seg = test_tmp + numpy.atleast_2d(enroll_tmp[:, model_idx]).T tmp2 = mod_plus_test_seg.T.dot(K2) S2[model_idx] = enroll_tmp[:, model_idx].dot(K1).dot(enroll_tmp[:, model_idx])/2. score.scoremat[model_idx, :] = numpy.einsum("ij, ji->i", tmp2, mod_plus_test_seg)/2. score.scoremat += constant - (S1 + S2[:, numpy.newaxis]) score.scoremat *= scaling_factor # Case of open-set identification, we compute the log-likelihood # by taking into account the probability of having a known impostor # or an out-of set class if p_known != 0: N = score.scoremat.shape[0] open_set_scores = numpy.empty(score.scoremat.shape) tmp = numpy.exp(score.scoremat) for ii in range(N): # open-set term open_set_scores[ii, :] = score.scoremat[ii, :] \ - numpy.log(p_known * tmp[~(numpy.arange(N) == ii)].sum(axis=0) / (N - 1) + (1 - p_known)) score.scoremat = open_set_scores return score
[docs]def fast_PLDA_scoring(enroll, test, ndx, mu, F, Sigma, test_uncertainty=None, Vtrans=None, p_known=0.0, scaling_factor=1., check_missing=True): """Compute the PLDA scores between to sets of vectors. The list of trials to perform is given in an Ndx object. PLDA matrices have to be pre-computed. i-vectors are supposed to be whitened before. :param enroll: a StatServer in which stat1 are i-vectors :param test: a StatServer in which stat1 are i-vectors :param ndx: an Ndx object defining the list of trials to perform :param mu: the mean vector of the PLDA gaussian :param F: the between-class co-variance matrix of the PLDA :param Sigma: the residual covariance matrix :param p_known: probability of having a known speaker for open-set identification case (=1 for the verification task and =0 for the closed-set case) :param check_missing: boolean, if True, check that all models and segments exist :return: a score object """ # assert isinstance(enroll, StatServer), 'First parameter should be a StatServer' # assert isinstance(test, StatServer), 'Second parameter should be a StatServer' # assert isinstance(ndx, Ndx), 'Third parameter should be an Ndx' # assert enroll.stat1.shape[1] == test.stat1.shape[1], 'I-vectors dimension mismatch' # assert enroll.stat1.shape[1] == F.shape[0], 'I-vectors and co-variance matrix dimension mismatch' # assert enroll.stat1.shape[1] == G.shape[0], 'I-vectors and co-variance matrix dimension mismatch' enroll_ctr = copy.deepcopy(enroll) test_ctr = copy.deepcopy(test) # If models are not unique, compute the mean per model, display a warning if not numpy.unique(enroll_ctr.modelset).shape == enroll_ctr.modelset.shape: logging.warning("Enrollment models are not unique, average i-vectors") enroll_ctr = enroll_ctr.mean_stat_per_model() # Remove missing models and test segments if check_missing: clean_ndx = _check_missing_model(enroll_ctr, test_ctr, ndx) else: clean_ndx = ndx # Center the i-vectors around the PLDA mean enroll_ctr.center_stat1(mu) test_ctr.center_stat1(mu) # If models are not unique, compute the mean per model, display a warning if not numpy.unique(enroll_ctr.modelset).shape == enroll_ctr.modelset.shape: logging.warning("Enrollment models are not unique, average i-vectors") enroll_ctr = enroll_ctr.mean_stat_per_model() # Compute constant component of the PLDA distribution invSigma = scipy.linalg.inv(Sigma) I_spk = numpy.eye(F.shape[1], dtype='float') K = F.T.dot(invSigma * scaling_factor).dot(F) K1 = scipy.linalg.inv(K + I_spk) K2 = scipy.linalg.inv(2 * K + I_spk) # Compute the Gaussian distribution constant alpha1 = numpy.linalg.slogdet(K1)[1] alpha2 = numpy.linalg.slogdet(K2)[1] plda_cst = alpha2 / 2.0 - alpha1 # Compute intermediate matrices Sigma_ac = numpy.dot(F, F.T) Sigma_tot = Sigma_ac + Sigma Sigma_tot_inv = scipy.linalg.inv(Sigma_tot) Tmp = numpy.linalg.inv(Sigma_tot - Sigma_ac.dot(Sigma_tot_inv).dot(Sigma_ac)) Phi = Sigma_tot_inv - Tmp Psi = Sigma_tot_inv.dot(Sigma_ac).dot(Tmp) # Compute the different parts of PLDA score model_part = 0.5 * numpy.einsum('ij, ji->i', enroll_ctr.stat1.dot(Phi), enroll_ctr.stat1.T) seg_part = 0.5 * numpy.einsum('ij, ji->i', test_ctr.stat1.dot(Phi), test_ctr.stat1.T) # Compute verification scores score = Scores() score.modelset = clean_ndx.modelset score.segset = clean_ndx.segset score.scoremask = clean_ndx.trialmask score.scoremat = model_part[:, numpy.newaxis] + seg_part + plda_cst score.scoremat += enroll_ctr.stat1.dot(Psi).dot(test_ctr.stat1.T) score.scoremat *= scaling_factor # Case of open-set identification, we compute the log-likelihood # by taking into account the probability of having a known impostor # or an out-of set class if p_known != 0: N = score.scoremat.shape[0] open_set_scores = numpy.empty(score.scoremat.shape) tmp = numpy.exp(score.scoremat) for ii in range(N): # open-set term open_set_scores[ii, :] = score.scoremat[ii, :] \ - numpy.log(p_known * tmp[~(numpy.arange(N) == ii)].sum(axis=0) / (N - 1) + (1 - p_known)) score.scoremat = open_set_scores return score
#IL FAUT RAJOUTER LA GESTION DES MULTI-SESSIONS (voir fonction PLDA_scoring_with_test_uncertainty_by_the_book de Themos) #IMPLEMENTER LA VERSION "BY THE BOOK" pour ne pas utiliser la moyenne des i-vecteurs
[docs]def PLDA_scoring_uncertainty(enroll, test, ndx, mu, F, Sigma, p_known=0.0, scaling_factor=1., test_uncertainty=None, Vtrans=None, check_missing=True): """ :param enroll: :param test: :param ndx: :param mu: :param F: :param Sigma: :param p_known: :param scaling_factor: :param test_uncertainty: :param Vtrans: :param check_missing: :return: """ assert isinstance(enroll, StatServer), 'First parameter should be a StatServer' assert isinstance(test, StatServer), 'Second parameter should be a StatServer' assert isinstance(ndx, Ndx), 'Third parameter should be an Ndx' assert enroll.stat1.shape[1] == test.stat1.shape[1], 'I-vectors dimension mismatch' assert enroll.stat1.shape[1] == F.shape[0], 'I-vectors and co-variance matrix dimension mismatch' assert enroll.stat1.shape[1] == G.shape[0], 'I-vectors and co-variance matrix dimension mismatch' enroll_ctr = copy.deepcopy(enroll) test_ctr = copy.deepcopy(test) # Remove missing models and test segments if check_missing: clean_ndx = _check_missing_model(enroll_ctr, test_ctr, ndx) else: clean_ndx = ndx # Center the i-vectors around the PLDA mean enroll_ctr.center_stat1(mu) test_ctr.center_stat1(mu) # Align StatServers to match the clean_ndx enroll_ctr.align_models_average(clean_ndx.modelset) test_ctr.align_segments(clean_ndx.segset) # Compute constant component of the PLDA distribution scoremat = numpy.zeros((enroll_ctr.stat1.shape[0], test_ctr.stat1.shape[0]), dtype='float') invSigma = scipy.linalg.inv(Sigma) K1 = scipy.linalg.inv(numpy.eye(F.shape[1]) + F.T.dot(invSigma * scaling_factor).dot(F)) FK1Ft = F.dot(K1).dot(F.T) Xtilda_e = FK1Ft.dot(invSigma * scaling_factor).dot(enroll_ctr.stat1.T).T for t in range(test_ctr.stat1.shape[0]): xt = test_ctr.stat1[t,:] Pr = numpy.eye(F.shape[1]) - numpy.outer(test.stat1[t, :],test.stat1[t, :]) Cunc = Pr.dot(Vtrans.transpose()).dot(numpy.diag(test_uncertainty[t, :]).dot(Vtrans)).dot(Pr) prec_den = scipy.linalg.inv(F.dot(F.T) + Sigma + Cunc) denom = -0.5 * xt.dot(prec_den).dot(xt) +0.5 * numpy.linalg.slogdet(prec_den)[1] prec_num = scipy.linalg.inv(FK1Ft+Sigma+Cunc) Xec = Xtilda_e - xt numer = -0.5 * numpy.einsum('ij, ji->i', Xec.dot(prec_num), Xec.T) + 0.5 * numpy.linalg.slogdet(prec_num)[1] scoremat[:, t] = scaling_factor * (numer - denom) # Compute verification scores score = Scores() score.modelset = clean_ndx.modelset score.segset = clean_ndx.segset score.scoremask = clean_ndx.trialmask score.scoremat = scoremat # Case of open-set identification, we compute the log-likelihood # by taking into account the probability of having a known impostor # or an out-of set class if p_known != 0: N = score.scoremat.shape[0] open_set_scores = numpy.empty(score.scoremat.shape) tmp = numpy.exp(score.scoremat) for ii in range(N): # open-set term open_set_scores[ii, :] = score.scoremat[ii, :] \ - numpy.log(p_known * tmp[~(numpy.arange(N) == ii)].sum(axis=0) / (N - 1) + (1 - p_known)) score.scoremat = open_set_scores return score