// gmm/mle-full-gmm-test.cc // Copyright 2009-2011 Jan Silovsky; Saarland University; // Microsoft Corporation; Yanmin Qian; Georg Stemmer // See ../../COPYING for clarification regarding multiple authors // // 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 // // THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED // WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, // MERCHANTABLITY OR NON-INFRINGEMENT. // See the Apache 2 License for the specific language governing permissions and // limitations under the License. #include "gmm/full-gmm.h" #include "gmm/diag-gmm.h" #include "gmm/model-common.h" #include "gmm/mle-full-gmm.h" #include "gmm/mle-diag-gmm.h" #include "util/stl-utils.h" #include "util/kaldi-io.h" using namespace kaldi; void TestComponentAcc(const FullGmm &gmm, const Matrix &feats) { MleFullGmmOptions config; AccumFullGmm est_atonce; // updates all components AccumFullGmm est_compwise; // updates single components // Initialize estimators est_atonce.Resize(gmm.NumGauss(), gmm.Dim(), kGmmAll); est_atonce.SetZero(kGmmAll); est_compwise.Resize(gmm.NumGauss(), gmm.Dim(), kGmmAll); est_compwise.SetZero(kGmmAll); // accumulate estimators for (int32 i = 0; i < feats.NumRows(); i++) { est_atonce.AccumulateFromFull(gmm, feats.Row(i), 1.0F); Vector post(gmm.NumGauss()); gmm.ComponentPosteriors(feats.Row(i), &post); for (int32 m = 0; m < gmm.NumGauss(); m++) { est_compwise.AccumulateForComponent(feats.Row(i), m, post(m)); } } FullGmm gmm_atonce; // model with all components accumulated together FullGmm gmm_compwise; // model with each component accumulated separately gmm_atonce.Resize(gmm.NumGauss(), gmm.Dim()); gmm_compwise.Resize(gmm.NumGauss(), gmm.Dim()); MleFullGmmUpdate(config, est_atonce, kGmmAll, &gmm_atonce, NULL, NULL); MleFullGmmUpdate(config, est_compwise, kGmmAll, &gmm_compwise, NULL, NULL); // the two ways of updating should result in the same model double loglike0 = 0.0; double loglike1 = 0.0; double loglike2 = 0.0; for (int32 i = 0; i < feats.NumRows(); i++) { loglike0 += static_cast(gmm.LogLikelihood(feats.Row(i))); loglike1 += static_cast(gmm_atonce.LogLikelihood(feats.Row(i))); loglike2 += static_cast(gmm_compwise.LogLikelihood(feats.Row(i))); } std::cout << "Per-frame log-likelihood before update = " << (loglike0/feats.NumRows()) << '\n'; std::cout << "Per-frame log-likelihood (accumulating at once) = " << (loglike1/feats.NumRows()) << '\n'; std::cout << "Per-frame log-likelihood (accumulating component-wise) = " << (loglike2/feats.NumRows()) << '\n'; AssertEqual(loglike1, loglike2, 1.0e-6); if (est_atonce.NumGauss() != gmm.NumGauss()) { KALDI_WARN << "Unable to pass test_update_flags() test because of " "component removal during Update() call (this is normal)"; return; } else { KALDI_ASSERT(loglike1 >= loglike0 - (std::abs(loglike1)+std::abs(loglike0))*1.0e-06); KALDI_ASSERT(loglike2 >= loglike0 - (std::abs(loglike2)+std::abs(loglike0))*1.0e-06); } } void rand_posdef_spmatrix(size_t dim, SpMatrix *matrix, TpMatrix *matrix_sqrt = NULL, BaseFloat *logdet = NULL) { // generate random (non-singular) matrix Matrix tmp(dim, dim); while (1) { tmp.SetRandn(); if (tmp.Cond() < 100) break; std::cout << "Condition number of random matrix large " << static_cast(tmp.Cond()) << ", trying again (this is normal)" << '\n'; } // tmp * tmp^T will give positive definite matrix matrix->AddMat2(1.0, tmp, kNoTrans, 0.0); if (matrix_sqrt != NULL) matrix_sqrt->Cholesky(*matrix); if (logdet != NULL) *logdet = matrix->LogPosDefDet(); if ((matrix_sqrt == NULL) && (logdet == NULL)) { TpMatrix sqrt(dim); sqrt.Cholesky(*matrix); } } BaseFloat GetLogLikeTest(const FullGmm &gmm, const VectorBase &feats, bool print_eigs) { BaseFloat log_like_sum = -1.0e+10; Matrix means; gmm.GetMeans(&means); const std::vector > inv_covars = gmm.inv_covars(); if (print_eigs) for (size_t i = 0; i < inv_covars.size(); i++) { SpMatrix cov(inv_covars[i]); size_t dim = cov.NumRows(); cov.Invert(); std::cout << i << "'th component eigs are: "; Vector s(dim); Matrix P(dim, dim); cov.SymPosSemiDefEig(&s, &P); std::cout << s; } for (int32 i = 0; i < gmm.NumGauss(); i++) { BaseFloat logdet = -(inv_covars[i].LogPosDefDet()); BaseFloat log_like = Log(gmm.weights()(i)) -0.5 * (gmm.Dim() * M_LOG_2PI + logdet); Vector offset(feats); offset.AddVec(-1.0, means.Row(i)); log_like -= 0.5 * VecSpVec(offset, inv_covars[i], offset); log_like_sum = LogAdd(log_like_sum, log_like); } return log_like_sum; } void test_flags_driven_update(const FullGmm &gmm, const Matrix &feats, GmmFlagsType flags) { MleFullGmmOptions config; AccumFullGmm est_gmm_allp; // updates all params // let's trust that all-params update works AccumFullGmm est_gmm_somep; // updates params indicated by flags // warm-up estimators est_gmm_allp.Resize(gmm.NumGauss(), gmm.Dim(), kGmmAll); est_gmm_allp.SetZero(kGmmAll); est_gmm_somep.Resize(gmm.NumGauss(), gmm.Dim(), flags); est_gmm_somep.SetZero(flags); // accumulate estimators for (int32 i = 0; i < feats.NumRows(); i++) { est_gmm_allp.AccumulateFromFull(gmm, feats.Row(i), 1.0F); est_gmm_somep.AccumulateFromFull(gmm, feats.Row(i), 1.0F); } FullGmm gmm_all_update; // model with all params updated FullGmm gmm_some_update; // model with some params updated gmm_all_update.CopyFromFullGmm(gmm); // init with orig. model gmm_some_update.CopyFromFullGmm(gmm); // init with orig. model MleFullGmmUpdate(config, est_gmm_allp, kGmmAll, &gmm_all_update, NULL, NULL); MleFullGmmUpdate(config, est_gmm_somep, flags, &gmm_some_update, NULL, NULL); if (gmm_all_update.NumGauss() != gmm.NumGauss()) { KALDI_WARN << "Unable to pass test_update_flags() test because of " "component removal during Update() call (this is normal)"; return; } // now back-off the gmm_all_update params that were not updated // in gmm_some_update to orig. if (~flags & kGmmWeights) gmm_all_update.SetWeights(gmm.weights()); if (~flags & kGmmMeans) { Matrix means(gmm.NumGauss(), gmm.Dim()); gmm.GetMeans(&means); gmm_all_update.SetMeans(means); } if (~flags & kGmmVariances) { std::vector > vars(gmm.NumGauss()); for (int32 i = 0; i < gmm.NumGauss(); i++) vars[i].Resize(gmm.Dim()); gmm.GetCovars(&vars); for (int32 i = 0; i < gmm.NumGauss(); i++) vars[i].InvertDouble(); gmm_all_update.SetInvCovars(vars); } gmm_some_update.ComputeGconsts(); gmm_all_update.ComputeGconsts(); // now both models gmm_all_update, gmm_all_update have the same params updated // compute loglike for models for check double loglike0 = 0.0; double loglike1 = 0.0; double loglike2 = 0.0; for (int32 i = 0; i < feats.NumRows(); i++) { loglike0 += static_cast( gmm.LogLikelihood(feats.Row(i))); loglike1 += static_cast( gmm_all_update.LogLikelihood(feats.Row(i))); loglike2 += static_cast( gmm_some_update.LogLikelihood(feats.Row(i))); } KALDI_LOG << "loglike1 = " << loglike1 << " loglike2 = " << loglike2; AssertEqual(loglike1, loglike2, 0.01); } void test_io(const FullGmm &gmm, const AccumFullGmm &est_gmm, bool binary, const Matrix &feats) { std::cout << "Testing I/O, binary = " << binary << '\n'; est_gmm.Write(Output("tmp_stats", binary).Stream(), binary); bool binary_in; AccumFullGmm est_gmm2; est_gmm2.Resize(gmm.NumGauss(), gmm.Dim(), kGmmAll); Input ki("tmp_stats", &binary_in); est_gmm2.Read(ki.Stream(), binary_in, false); // not adding Input ki2("tmp_stats", &binary_in); est_gmm2.Read(ki2.Stream(), binary_in, true); // adding est_gmm2.Scale(0.5, kGmmAll); // 0.5 -> make it same as what it would have been if we read just once. // [may affect it due to removal of components with small counts]. MleFullGmmOptions config; FullGmm gmm1; FullGmm gmm2; gmm1.CopyFromFullGmm(gmm); gmm2.CopyFromFullGmm(gmm); MleFullGmmUpdate(config, est_gmm, est_gmm.Flags(), &gmm1, NULL, NULL); MleFullGmmUpdate(config, est_gmm2, est_gmm2.Flags(), &gmm2, NULL, NULL); BaseFloat loglike1 = 0.0; BaseFloat loglike2 = 0.0; for (int32 i = 0; i < feats.NumRows(); i++) { loglike1 += gmm1.LogLikelihood(feats.Row(i)); loglike2 += gmm2.LogLikelihood(feats.Row(i)); } AssertEqual(loglike1, loglike2, 0.01); unlink("tmp_stats"); } void UnitTestEstimateFullGmm() { // using namespace kaldi; // dimension of the gmm int32 dim = 10; // number of mixtures in the data int32 nMix = 7; // number of iterations for estimation int32 maxiterations = 20; // maximum number of densities in the GMM // larger than the number of mixtures in the data // so that we can test the removal of unseen components int32 maxcomponents = 50; // generate random feature vectors // first, generate parameters of vectors distribution // (mean and covariance matrices) Matrix means_f(nMix, dim); std::vector > vars_f(nMix); std::vector > vars_f_sqrt(nMix); for (int32 mix = 0; mix < nMix; mix++) { vars_f[mix].Resize(dim); vars_f_sqrt[mix].Resize(dim); } for (int32 m = 0; m < nMix; m++) { for (int32 d = 0; d < dim; d++) { means_f(m, d) = kaldi::RandGauss(); } rand_posdef_spmatrix(dim, &vars_f[m], &vars_f_sqrt[m], NULL); } // second, generate 1000 feature vectors for each of the mixture components int32 counter = 0, multiple = 200; Matrix feats(nMix*200, dim); Vector rnd_vec(dim); for (int32 m = 0; m < nMix; m++) { for (int32 i = 0; i < multiple; i++) { for (int32 d = 0; d < dim; d++) { rnd_vec(d) = RandGauss(); } feats.Row(counter).CopyFromVec(means_f.Row(m)); feats.Row(counter).AddTpVec(1.0, vars_f_sqrt[m], kNoTrans, rnd_vec, 1.0); ++counter; } } { // Work out "perfect" log-like w/ one component. Matrix cov(dim, dim); Vector mean(dim); cov.AddMatMat(1.0, feats, kTrans, feats, kNoTrans, 0.0); cov.Scale(1.0 / feats.NumRows()); mean.AddRowSumMat(1.0, feats); mean.Scale(1.0 / feats.NumRows()); cov.AddVecVec(-1.0, mean, mean); BaseFloat logdet = cov.LogDet(); BaseFloat avg_log = -0.5*(logdet + dim*(M_LOG_2PI + 1)); std::cout << "Avg log-like per frame [full-cov, 1-mix] should be: " << avg_log << '\n'; std::cout << "Total log-like [full-cov, 1-mix] should be: " << (feats.NumRows()*avg_log) << '\n'; Vector s(dim); Matrix P(dim, dim); cov.SymPosSemiDefEig(&s, &P); std::cout << "Cov eigs are " << s; } // write the feature vectors to a file // std::ofstream of("tmpfeats"); // of.precision(10); // of << feats; // of.close(); // now generate randomly initial values for the GMM Vector weights(1); Matrix means(1, dim); std::vector > invcovars(1); invcovars[0].Resize(dim); for (int32 d = 0; d < dim; d++) { means(0, d) = kaldi::RandGauss()*5.0F; } SpMatrix covar(dim); rand_posdef_spmatrix(dim, &covar, NULL, NULL); covar.AddToDiag(0.1); // Ensure the condition is reasonable, otherwise // we can get arbitrarily large inverse. invcovars[0].CopyFromSp(covar); invcovars[0].InvertDouble(); weights(0) = 1.0F; // new GMM FullGmm *gmm = new FullGmm(); gmm->Resize(1, dim); gmm->SetWeights(weights); gmm->SetInvCovarsAndMeans(invcovars, means); gmm->ComputeGconsts(); { KALDI_LOG << "Testing natural<>normal conversion"; FullGmmNormal ngmm(*gmm); FullGmm rgmm; rgmm.Resize(1, dim); ngmm.CopyToFullGmm(&rgmm, kGmmAll); // check contents KALDI_ASSERT(ApproxEqual(weights(0), 1.0F, 1e-6)); KALDI_ASSERT(ApproxEqual(gmm->weights()(0), rgmm.weights()(0), 1e-6)); double prec_m = 1e-3; double prec_v = 1e-3; for (int32 d = 0; d < dim; d++) { KALDI_ASSERT(std::abs(means.Row(0)(d) - ngmm.means_.Row(0)(d)) < prec_m); KALDI_ASSERT(std::abs(gmm->means_invcovars().Row(0)(d) - rgmm.means_invcovars().Row(0)(d)) < prec_v); for (int32 d2 = d; d2 < dim; ++d2) { KALDI_ASSERT(std::abs(covar(d, d2) - ngmm.vars_[0](d, d2)) < prec_v); KALDI_ASSERT(std::abs(gmm->inv_covars()[0](d, d2) - rgmm.inv_covars()[0](d, d2)) < prec_v); } } KALDI_LOG << "OK"; } MleFullGmmOptions config; GmmFlagsType flags_all = kGmmAll; AccumFullGmm est_gmm; est_gmm.Resize(gmm->NumGauss(), gmm->Dim(), flags_all); // iterate int32 iteration = 0; float lastloglike = 0.0; int32 lastloglike_nM = 0; while (iteration < maxiterations) { // First, resize accums for the case of component splitting est_gmm.Resize(gmm->NumGauss(), gmm->Dim(), flags_all); est_gmm.SetZero(flags_all); double loglike = 0.0; double loglike_test = 0.0; for (int32 i = 0; i < counter; i++) { loglike += static_cast( est_gmm.AccumulateFromFull(*gmm, feats.Row(i), 1.0F)); if (iteration < 4) { loglike_test += GetLogLikeTest(*gmm, feats.Row(i), (i == 0)); AssertEqual(loglike, loglike_test); } } std::cout << "Loglikelihood before iteration " << iteration << " : " << std::scientific << loglike << " number of components: " << gmm->NumGauss() << '\n'; // std::cout << "Model is: " << *gmm; // every 5th iteration check loglike change and update lastloglike if (iteration % 5 == 0) { // likelihood should be increasing on the long term if ((iteration > 0) && (gmm->NumGauss() >= lastloglike_nM)) { KALDI_ASSERT(loglike > lastloglike); } lastloglike = loglike; lastloglike_nM = gmm->NumGauss(); } BaseFloat obj, count; MleFullGmmUpdate(config, est_gmm, flags_all, gmm, &obj, &count); KALDI_LOG << "ML objective function change = " << (obj/count) << " per frame, over " << (count) << " frames."; // split components to double count at second iteration // and every next 3rd iteration // stop splitting when maxcomponents reached if ( (iteration < maxiterations - 3) && (iteration % 4 == 1) && (gmm->NumGauss() * 2 <= maxcomponents)) { gmm->Split(gmm->NumGauss() * 2, 0.01); } if (iteration == 5) { // run following tests with not too overfitted model std::cout << "Testing flags-driven updates kGmmAll" << '\n'; test_flags_driven_update(*gmm, feats, kGmmAll); std::cout << "Testing flags-driven updates kGmmWeights" << '\n'; test_flags_driven_update(*gmm, feats, kGmmWeights); std::cout << "Testing flags-driven kGmmMeans" << '\n'; test_flags_driven_update(*gmm, feats, kGmmMeans); std::cout << "Testing flags-driven kGmmVariances" << '\n'; test_flags_driven_update(*gmm, feats, kGmmVariances); std::cout << "Testing flags-driven kGmmWeights | kGmmMeans" << '\n'; test_flags_driven_update(*gmm, feats, kGmmWeights | kGmmMeans); std::cout << "Testing component-wise accumulation" << '\n'; TestComponentAcc(*gmm, feats); } iteration++; } { // I/O tests GmmFlagsType flags_all = kGmmAll; est_gmm.Resize(gmm->NumGauss(), gmm->Dim(), flags_all); est_gmm.SetZero(flags_all); float loglike = 0.0; for (int32 i = 0; i < counter; i++) { loglike += est_gmm.AccumulateFromFull(*gmm, feats.Row(i), 1.0F); } test_io(*gmm, est_gmm, false, feats); test_io(*gmm, est_gmm, true, feats); } delete gmm; gmm = NULL; } int main() { // repeat the test five times for (int i = 0; i < 2; i++) UnitTestEstimateFullGmm(); std::cout << "Test OK.\n"; }