1 /**
2  * @file tests/hmm_test.cpp
3  *
4  * Test file for HMMs.
5  *
6  * mlpack is free software; you may redistribute it and/or modify it under the
7  * terms of the 3-clause BSD license.  You should have received a copy of the
8  * 3-clause BSD license along with mlpack.  If not, see
9  * http://www.opensource.org/licenses/BSD-3-Clause for more information.
10  */
11 #include <mlpack/core.hpp>
12 #include <mlpack/methods/hmm/hmm.hpp>
13 #include <mlpack/methods/gmm/gmm.hpp>
14 #include <mlpack/methods/gmm/diagonal_gmm.hpp>
15 
16 #include "catch.hpp"
17 #include "test_catch_tools.hpp"
18 
19 using namespace mlpack;
20 using namespace mlpack::hmm;
21 using namespace mlpack::distribution;
22 using namespace mlpack::gmm;
23 
24 /**
25  * We will use the simple case proposed by Russell and Norvig in Artificial
26  * Intelligence: A Modern Approach, 2nd Edition, around p.549.
27  */
28 TEST_CASE("SimpleDiscreteHMMTestViterbi", "[HMMTest]")
29 {
30   // We have two hidden states: rain/dry.  Two emission states: umbrella/no
31   // umbrella.
32   // In this example, the transition matrix is
33   //  rain  dry
34   // [[0.7 0.3]  rain
35   //  [0.3 0.7]] dry
36   // and the emission probability is
37   //  rain dry
38   // [[0.9 0.2]  umbrella
39   //  [0.1 0.8]] no umbrella
40   arma::vec initial("1 0"); // Default MATLAB initial states.
41   arma::mat transition("0.7 0.3; 0.3 0.7");
42   std::vector<DiscreteDistribution> emission(2);
43   emission[0] = DiscreteDistribution(std::vector<arma::vec>{"0.9 0.1"});
44   emission[1] = DiscreteDistribution(std::vector<arma::vec>{"0.2 0.8"});
45 
46   HMM<DiscreteDistribution> hmm(initial, transition, emission);
47 
48   // Now let's take a sequence and find what the most likely state is.
49   // We'll use the sequence [U U N U U] (U = umbrella, N = no umbrella) like on
50   // p. 547.
51   arma::mat observation = "0 0 1 0 0";
52   arma::Row<size_t> states;
53   hmm.Predict(observation, states);
54 
55   // Check each state.
56   REQUIRE(states[0] == 0); // Rain.
57   REQUIRE(states[1] == 0); // Rain.
58   REQUIRE(states[2] == 1); // No rain.
59   REQUIRE(states[3] == 0); // Rain.
60   REQUIRE(states[4] == 0); // Rain.
61 }
62 
63 /**
64  * This example is from Borodovsky & Ekisheva, p. 80-81.  It is just slightly
65  * more complex.
66  */
67 TEST_CASE("BorodovskyHMMTestViterbi", "[HMMTest]")
68 {
69   // Equally probable initial states.
70   arma::vec initial(3);
71   initial.fill(1.0 / 3.0);
72 
73   // Two hidden states: H (high GC content) and L (low GC content), as well as a
74   // start state.
75   arma::mat transition("0.0 0.0 0.0;"
76                        "0.5 0.5 0.4;"
77                        "0.5 0.5 0.6");
78   // Four emission states: A, C, G, T.  Start state doesn't emit...
79   std::vector<DiscreteDistribution> emission(3);
80   emission[0] = DiscreteDistribution(
81       std::vector<arma::vec>{"0.25 0.25 0.25 0.25"});
82   emission[1] = DiscreteDistribution(
83       std::vector<arma::vec>{"0.20 0.30 0.30 0.20"});
84   emission[2] = DiscreteDistribution(
85       std::vector<arma::vec>{"0.30 0.20 0.20 0.30"});
86 
87   HMM<DiscreteDistribution> hmm(initial, transition, emission);
88 
89   // GGCACTGAA.
90   arma::mat observation("2 2 1 0 1 3 2 0 0");
91   arma::Row<size_t> states;
92   hmm.Predict(observation, states);
93 
94   // Most probable path is HHHLLLLLL.
95   REQUIRE(states[0] == 1);
96   REQUIRE(states[1] == 1);
97   REQUIRE(states[2] == 1);
98   REQUIRE(states[3] == 2);
99   // This could actually be one of two states (equal probability).
100   REQUIRE(((states[4] == 1) || (states[4] == 2)) == true);
101   REQUIRE(states[5] == 2);
102   // This could also be one of two states.
103   REQUIRE(((states[6] == 1) || (states[6] == 2)) == true);
104   REQUIRE(states[7] == 2);
105   REQUIRE(states[8] == 2);
106 }
107 
108 /**
109  * Ensure that the forward-backward algorithm is correct.
110  */
111 TEST_CASE("ForwardBackwardTwoState", "[HMMTest]")
112 {
113   arma::mat obs("3 3 2 1 1 1 1 3 3 1");
114 
115   // The values used for the initial distribution here don't entirely make
116   // sense.  I am not sure how the output came from hmmdecode(), and the
117   // documentation below doesn't completely say.  It seems like maybe the
118   // transition matrix needs to be transposed and the results recalculated, but
119   // I am not certain.
120   arma::vec initial("0.1 0.4");
121   arma::mat transition("0.1 0.9; 0.4 0.6");
122   std::vector<DiscreteDistribution> emis(2);
123   emis[0] = DiscreteDistribution(std::vector<arma::vec>{"0.85 0.15 0.00 0.00"});
124   emis[1] = DiscreteDistribution(std::vector<arma::vec>{"0.00 0.00 0.50 0.50"});
125 
126   HMM<DiscreteDistribution> hmm(initial, transition, emis);
127 
128   // Now check we are getting the same results as MATLAB for this sequence.
129   arma::mat stateProb;
130   arma::mat forwardProb;
131   arma::mat backwardProb;
132   arma::vec scales;
133 
134   const double log = hmm.Estimate(obs, stateProb, forwardProb, backwardProb,
135       scales);
136 
137   // All values obtained from MATLAB hmmdecode().
138   REQUIRE(log == Approx(-23.4349).epsilon(1e-5));
139 
140   REQUIRE(stateProb(0, 0) == Approx(0.0).margin(1e-7));
141   REQUIRE(stateProb(1, 0) == Approx(1.0).epsilon(1e-7));
142   REQUIRE(stateProb(0, 1) == Approx(0.0).margin(1e-7));
143   REQUIRE(stateProb(1, 1) == Approx(1.0).epsilon(1e-7));
144   REQUIRE(stateProb(0, 2) == Approx(0.0).margin(1e-7));
145   REQUIRE(stateProb(1, 2) == Approx(1.0).epsilon(1e-7));
146   REQUIRE(stateProb(0, 3) == Approx(1.0).epsilon(1e-7));
147   REQUIRE(stateProb(1, 3) == Approx(0.0).margin(1e-7));
148   REQUIRE(stateProb(0, 4) == Approx(1.0).epsilon(1e-7));
149   REQUIRE(stateProb(1, 4) == Approx(0.0).margin(1e-7));
150   REQUIRE(stateProb(0, 5) == Approx(1.0).epsilon(1e-7));
151   REQUIRE(stateProb(1, 5) == Approx(0.0).margin(1e-7));
152   REQUIRE(stateProb(0, 6) == Approx(1.0).epsilon(1e-7));
153   REQUIRE(stateProb(1, 6) == Approx(0.0).margin(1e-7));
154   REQUIRE(stateProb(0, 7) == Approx(0.0).margin(1e-7));
155   REQUIRE(stateProb(1, 7) == Approx(1.0).epsilon(1e-7));
156   REQUIRE(stateProb(0, 8) == Approx(0.0).margin(1e-7));
157   REQUIRE(stateProb(1, 8) == Approx(1.0).epsilon(1e-7));
158   REQUIRE(stateProb(0, 9) == Approx(1.0).epsilon(1e-7));
159   REQUIRE(stateProb(1, 9) == Approx(0.0).margin(1e-7));
160 }
161 
162 /**
163  * In this example we try to estimate the transmission and emission matrices
164  * based on some observations.  We use the simplest possible model.
165  */
166 TEST_CASE("SimplestBaumWelchDiscreteHMM", "[HMMTest]")
167 {
168   // Don't yet require a useful distribution.  1 state, 1 emission.
169   HMM<DiscreteDistribution> hmm(1, DiscreteDistribution(1));
170 
171   std::vector<arma::mat> observations;
172   // Different lengths for each observation sequence.
173   observations.push_back("0 0 0 0 0 0 0 0"); // 8 zeros.
174   observations.push_back("0 0 0 0 0 0 0"); // 7 zeros.
175   observations.push_back("0 0 0 0 0 0 0 0 0 0 0 0"); // 12 zeros.
176   observations.push_back("0 0 0 0 0 0 0 0 0 0"); // 10 zeros.
177 
178   hmm.Train(observations);
179 
180   REQUIRE(hmm.Initial()[0] == Approx(1.0).epsilon(1e-7));
181   REQUIRE(hmm.Emission()[0].Probability("0") == Approx(1.0).epsilon(1e-7));
182   REQUIRE(hmm.Transition()(0, 0) == Approx(1.0).epsilon(1e-7));
183 }
184 
185 /**
186  * A slightly more complex model to estimate.
187  */
188 TEST_CASE("SimpleBaumWelchDiscreteHMM", "[HMMTest]")
189 {
190   HMM<DiscreteDistribution> hmm(1, 2); // 1 state, 2 emissions.
191   // Randomize the emission matrix.
192   hmm.Emission()[0].Probabilities() = arma::randu<arma::vec>(2);
193   hmm.Emission()[0].Probabilities() /= accu(hmm.Emission()[0].Probabilities());
194 
195   // P(each emission) = 0.5.
196   // I've been careful to make P(first emission = 0) = P(first emission = 1).
197   std::vector<arma::mat> observations;
198   observations.push_back("0 1 0 1 0 1 0 1 0 1 0 1");
199   observations.push_back("0 0 0 0 0 0 1 1 1 1 1 1");
200   observations.push_back("1 1 1 1 1 1 0 0 0 0 0 0");
201   observations.push_back("1 1 1 0 0 0 1 1 1 0 0 0");
202   observations.push_back("0 0 1 1 0 0 0 0 1 1 1 1");
203   observations.push_back("1 1 1 0 0 0 1 1 1 0 0 0");
204   observations.push_back("0 1 0 1 0 1 0 1 0 1 0 1");
205   observations.push_back("0 0 0 0 0 0 1 1 1 1 1 1");
206   observations.push_back("1 1 1 1 1 0 1 0 0 0 0 0");
207   observations.push_back("1 1 1 0 0 1 0 1 1 0 0 0");
208   observations.push_back("0 0 1 1 0 0 0 1 0 1 1 1");
209   observations.push_back("1 1 1 0 0 1 0 1 1 0 0 0");
210 
211   hmm.Train(observations);
212 
213   REQUIRE(hmm.Emission()[0].Probability("0") == Approx(0.5).epsilon(1e-7));
214   REQUIRE(hmm.Emission()[0].Probability("1") == Approx(0.5).epsilon(1e-7));
215   REQUIRE(hmm.Transition()(0, 0) == Approx(1.0).epsilon(1e-7));
216   REQUIRE(hmm.Initial()[0] == Approx(1.0).epsilon(1e-7));
217 }
218 
219 /**
220  * Increasing complexity, but still simple; 4 emissions, 2 states; the state can
221  * be determined directly by the emission.
222  */
223 TEST_CASE("SimpleBaumWelchDiscreteHMM_2", "[HMMTest]")
224 {
225   HMM<DiscreteDistribution> hmm(2, DiscreteDistribution(4));
226 
227   // A little bit of obfuscation to the solution.
228   hmm.Transition() = arma::mat("0.1 0.4; 0.9 0.6");
229   hmm.Emission()[0].Probabilities() = "0.85 0.15 0.00 0.00";
230   hmm.Emission()[1].Probabilities() = "0.00 0.00 0.50 0.50";
231 
232   // True emission matrix:
233   //  [[0.4 0  ]
234   //   [0.6 0  ]
235   //   [0   0.2]
236   //   [0   0.8]]
237 
238   // True transmission matrix:
239   //  [[0.5 0.5]
240   //   [0.5 0.5]]
241 
242   // Generate observations randomly by hand.  This is kinda ugly, but it works.
243   std::vector<arma::mat> observations;
244   size_t obsNum = 250; // Number of observations.
245   size_t obsLen = 500; // Number of elements in each observation.
246   size_t stateZeroStarts = 0; // Number of times we start in state 0.
247   for (size_t i = 0; i < obsNum; ++i)
248   {
249     arma::mat observation(1, obsLen);
250 
251     size_t state = 0;
252     size_t emission = 0;
253 
254     for (size_t obs = 0; obs < obsLen; obs++)
255     {
256       // See if state changed.
257       double r = math::Random();
258 
259       if (r <= 0.5)
260       {
261         if (obs == 0)
262           ++stateZeroStarts;
263         state = 0;
264       }
265       else
266       {
267         state = 1;
268       }
269 
270       // Now set the observation.
271       r = math::Random();
272 
273       switch (state)
274       {
275         // case 0 is not possible.
276         case 0:
277           if (r <= 0.4)
278             emission = 0;
279           else
280             emission = 1;
281           break;
282         case 1:
283           if (r <= 0.2)
284             emission = 2;
285           else
286             emission = 3;
287           break;
288       }
289 
290       observation(0, obs) = emission;
291     }
292 
293     observations.push_back(observation);
294   }
295 
296   hmm.Train(observations);
297 
298   // Calculate true probability of class 0 at the start.
299   double prob = double(stateZeroStarts) / observations.size();
300 
301   // Only require 2.5% tolerance, because this is a little fuzzier.
302   REQUIRE(hmm.Initial()[0] == Approx(prob).epsilon(0.025));
303   REQUIRE(hmm.Initial()[1] == Approx(1.0 - prob).epsilon(0.025));
304 
305   REQUIRE(hmm.Transition()(0, 0) == Approx(0.5).epsilon(0.025));
306   REQUIRE(hmm.Transition()(1, 0) == Approx(0.5).epsilon(0.025));
307   REQUIRE(hmm.Transition()(0, 1) == Approx(0.5).epsilon(0.025));
308   REQUIRE(hmm.Transition()(1, 1) == Approx(0.5).epsilon(0.025));
309 
310   REQUIRE(hmm.Emission()[0].Probability("0") == Approx(0.4).epsilon(0.04));
311   REQUIRE(hmm.Emission()[0].Probability("1") == Approx(0.6).epsilon(0.04));
312   REQUIRE(hmm.Emission()[0].Probability("2") == Approx(0.0).margin(2.5));
313   REQUIRE(hmm.Emission()[0].Probability("3") == Approx(0.0).margin(2.5));
314   REQUIRE(hmm.Emission()[1].Probability("0") == Approx(0.0).margin(2.5));
315   REQUIRE(hmm.Emission()[1].Probability("1") == Approx(0.0).margin(2.5));
316   REQUIRE(hmm.Emission()[1].Probability("2") == Approx(0.2).epsilon(0.04));
317   REQUIRE(hmm.Emission()[1].Probability("3") == Approx(0.8).epsilon(0.04));
318 }
319 
320 TEST_CASE("DiscreteHMMLabeledTrainTest", "[HMMTest]")
321 {
322   // Generate a random Markov model with 3 hidden states and 6 observations.
323   arma::mat transition;
324   std::vector<DiscreteDistribution> emission(3);
325 
326   transition.randu(3, 3);
327   emission[0].Probabilities() = arma::randu<arma::vec>(6);
328   emission[0].Probabilities() /= accu(emission[0].Probabilities());
329   emission[1].Probabilities() = arma::randu<arma::vec>(6);
330   emission[1].Probabilities() /= accu(emission[1].Probabilities());
331   emission[2].Probabilities() = arma::randu<arma::vec>(6);
332   emission[2].Probabilities() /= accu(emission[2].Probabilities());
333 
334   // Normalize so they we have a correct transition matrix.
335   for (size_t col = 0; col < 3; col++)
336     transition.col(col) /= accu(transition.col(col));
337 
338   // Now generate sequences.
339   size_t obsNum = 250;
340   size_t obsLen = 800;
341 
342   std::vector<arma::mat> observations(obsNum);
343   std::vector<arma::Row<size_t> > states(obsNum);
344 
345   for (size_t n = 0; n < obsNum; n++)
346   {
347     observations[n].set_size(1, obsLen);
348     states[n].set_size(obsLen);
349 
350     // Random starting state.
351     states[n][0] = math::RandInt(3);
352 
353     // Random starting observation.
354     observations[n].col(0) = emission[states[n][0]].Random();
355 
356     // Now the rest of the observations.
357     for (size_t t = 1; t < obsLen; t++)
358     {
359       // Choose random number for state transition.
360       double state = math::Random();
361 
362       // Decide next state.
363       double sumProb = 0;
364       for (size_t st = 0; st < 3; st++)
365       {
366         sumProb += transition(st, states[n][t - 1]);
367         if (sumProb >= state)
368         {
369           states[n][t] = st;
370           break;
371         }
372       }
373 
374       // Decide observation.
375       observations[n].col(t) = emission[states[n][t]].Random();
376     }
377   }
378 
379   // Now that our data is generated, we give the HMM the labeled data to train
380   // on.
381   HMM<DiscreteDistribution> hmm(3, DiscreteDistribution(6));
382 
383   hmm.Train(observations, states);
384 
385   // Make sure the initial weights are fine.  They should be equal (or close).
386   arma::vec initial(3);
387   initial.fill(1.0 / 3.0);
388   REQUIRE(arma::norm(hmm.Initial() - initial) <  0.2);
389 
390   // Check that the transition matrix is close.
391   REQUIRE(arma::norm(hmm.Transition() - transition) < 0.1);
392 
393   for (size_t col = 0; col < hmm.Emission().size(); col++)
394   {
395     for (size_t row = 0; row < hmm.Emission()[col].Probabilities().n_elem;
396         row++)
397     {
398       arma::vec obs(1);
399       obs[0] = row;
400       REQUIRE(hmm.Emission()[col].Probability(obs) -
401           emission[col].Probability(obs) == Approx(0.0).margin(0.07));
402     }
403   }
404 }
405 
406 /**
407  * Make sure the Generate() function works for a uniformly distributed HMM;
408  * we'll take many samples just to make sure.
409  */
410 TEST_CASE("DiscreteHMMSimpleGenerateTest", "[HMMTest]")
411 {
412   // Very simple HMM.  4 emissions with equal probability and 2 states with
413   // equal probability.
414   HMM<DiscreteDistribution> hmm(2, DiscreteDistribution(4));
415   hmm.Initial() = arma::ones<arma::vec>(2) / 2.0;
416   hmm.Transition() = arma::ones<arma::mat>(2, 2) / 2.0;
417 
418   // Now generate a really, really long sequence.
419   arma::mat dataSeq;
420   arma::Row<size_t> stateSeq;
421 
422   hmm.Generate(100000, dataSeq, stateSeq);
423 
424   // Now find the empirical probabilities of each state.
425   arma::vec emissionProb(4);
426   arma::vec stateProb(2);
427   emissionProb.zeros();
428   stateProb.zeros();
429   for (size_t i = 0; i < 100000; ++i)
430   {
431     emissionProb[(size_t) dataSeq.col(i)[0] + 0.5]++;
432     stateProb[stateSeq[i]]++;
433   }
434 
435   // Normalize so these are probabilities.
436   emissionProb /= accu(emissionProb);
437   stateProb /= accu(stateProb);
438 
439   // Now check that the probabilities are right.  3% tolerance.
440   REQUIRE(emissionProb[0] == Approx(0.25).epsilon(0.03));
441   REQUIRE(emissionProb[1] == Approx(0.25).epsilon(0.03));
442   REQUIRE(emissionProb[2] == Approx(0.25).epsilon(0.03));
443   REQUIRE(emissionProb[3] == Approx(0.25).epsilon(0.03));
444 
445   REQUIRE(stateProb[0] == Approx(0.50).epsilon(0.03));
446   REQUIRE(stateProb[1] == Approx(0.50).epsilon(0.03));
447 }
448 
449 /**
450  * More complex test for Generate().
451  */
452 TEST_CASE("DiscreteHMMGenerateTest", "[HMMTest]")
453 {
454   // 6 emissions, 4 states.  Random transition and emission probability.
455   arma::vec initial("1 0 0 0");
456   arma::mat transition(4, 4);
457   std::vector<DiscreteDistribution> emission(4);
458   emission[0].Probabilities() = arma::randu<arma::vec>(6);
459   emission[0].Probabilities() /= accu(emission[0].Probabilities());
460   emission[1].Probabilities() = arma::randu<arma::vec>(6);
461   emission[1].Probabilities() /= accu(emission[1].Probabilities());
462   emission[2].Probabilities() = arma::randu<arma::vec>(6);
463   emission[2].Probabilities() /= accu(emission[2].Probabilities());
464   emission[3].Probabilities() = arma::randu<arma::vec>(6);
465   emission[3].Probabilities() /= accu(emission[3].Probabilities());
466 
467   transition.randu();
468 
469   // Normalize matrix.
470   for (size_t col = 0; col < 4; col++)
471     transition.col(col) /= accu(transition.col(col));
472 
473   // Create HMM object.
474   HMM<DiscreteDistribution> hmm(initial, transition, emission);
475 
476   // We'll create a bunch of sequences.
477   int numSeq = 400;
478   int numObs = 3000;
479   std::vector<arma::mat> sequences(numSeq);
480   std::vector<arma::Row<size_t> > states(numSeq);
481   for (int i = 0; i < numSeq; ++i)
482   {
483     // Random starting state.
484     size_t startState = math::RandInt(4);
485 
486     hmm.Generate(numObs, sequences[i], states[i], startState);
487   }
488 
489   // Now we will calculate the full probabilities.
490   HMM<DiscreteDistribution> hmm2(4, 6);
491   hmm2.Train(sequences, states);
492 
493   // Check that training gives the same result.
494   REQUIRE(arma::norm(hmm.Transition() - hmm2.Transition()) <  0.02);
495 
496   for (size_t row = 0; row < 6; row++)
497   {
498     arma::vec obs(1);
499     obs[0] = row;
500     for (size_t col = 0; col < 4; col++)
501     {
502       REQUIRE(hmm.Emission()[col].Probability(obs) -
503           hmm2.Emission()[col].Probability(obs) == Approx(0.0).margin(0.02));
504     }
505   }
506 }
507 
508 TEST_CASE("DiscreteHMMLogLikelihoodTest", "[HMMTest]")
509 {
510   // Create a simple HMM with three states and four emissions.
511   arma::vec initial("0.5 0.2 0.3"); // Default MATLAB initial states.
512   arma::mat transition("0.5 0.0 0.1;"
513                        "0.2 0.6 0.2;"
514                        "0.3 0.4 0.7");
515   std::vector<DiscreteDistribution> emission(3);
516   emission[0].Probabilities() = "0.75 0.25 0.00 0.00";
517   emission[1].Probabilities() = "0.00 0.25 0.25 0.50";
518   emission[2].Probabilities() = "0.10 0.40 0.40 0.10";
519 
520   HMM<DiscreteDistribution> hmm(initial, transition, emission);
521 
522   // Now generate some sequences and check that the log-likelihood is the same
523   // as MATLAB gives for this HMM.
524   REQUIRE(hmm.LogLikelihood("0 1 2 3") == Approx(-4.9887223949).epsilon(1e-7));
525   REQUIRE(hmm.LogLikelihood("1 2 0 0") == Approx(-6.0288487077).epsilon(1e-7));
526   REQUIRE(hmm.LogLikelihood("3 3 3 3") == Approx(-5.5544000018).epsilon(1e-7));
527   REQUIRE(hmm.LogLikelihood("0 2 2 1 2 3 0 0 1 3 1 0 0 3 1 2 2") ==
528       Approx(-24.51556128368).epsilon(1e-7));
529 }
530 
531 /**
532  * A simple test to make sure HMMs with Gaussian output distributions work.
533  */
534 TEST_CASE("GaussianHMMSimpleTest", "[HMMTest]")
535 {
536   // We'll have two Gaussians, far away from each other, one corresponding to
537   // each state.
538   //  E(0) ~ N([ 5.0  5.0], eye(2)).
539   //  E(1) ~ N([-5.0 -5.0], eye(2)).
540   // The transition matrix is simple:
541   //  T = [[0.75 0.25]
542   //       [0.25 0.75]]
543   GaussianDistribution g1("5.0 5.0", "1.0 0.0; 0.0 1.0");
544   GaussianDistribution g2("-5.0 -5.0", "1.0 0.0; 0.0 1.0");
545 
546   arma::vec initial("1 0"); // Default MATLAB initial states.
547   arma::mat transition("0.75 0.25; 0.25 0.75");
548 
549   std::vector<GaussianDistribution> emission;
550   emission.push_back(g1);
551   emission.push_back(g2);
552 
553   HMM<GaussianDistribution> hmm(initial, transition, emission);
554 
555   // Now, generate some sequences.
556   arma::mat observations(2, 1000);
557   arma::Row<size_t> classes(1000);
558 
559   // 1000-observations sequence.
560   classes[0] = 0;
561   observations.col(0) = g1.Random();
562   for (size_t i = 1; i < 1000; ++i)
563   {
564     double randValue = math::Random();
565 
566     if (randValue > 0.75) // Then we change state.
567       classes[i] = (classes[i - 1] + 1) % 2;
568     else
569       classes[i] = classes[i - 1];
570 
571     if (classes[i] == 0)
572       observations.col(i) = g1.Random();
573     else
574       observations.col(i) = g2.Random();
575   }
576 
577   // Now predict the sequence.
578   arma::Row<size_t> predictedClasses;
579   arma::mat stateProb;
580 
581   hmm.Predict(observations, predictedClasses);
582   hmm.Estimate(observations, stateProb);
583 
584   // Check that each prediction is right.
585   for (size_t i = 0; i < 1000; ++i)
586   {
587     REQUIRE(predictedClasses[i] == classes[i]);
588 
589     // The probability of the wrong class should be infinitesimal.
590     REQUIRE(stateProb((classes[i] + 1) % 2, i) == Approx(0.0).margin(0.001));
591   }
592 }
593 
594 /**
595  * Ensure that Gaussian HMMs can be trained properly, for the labeled training
596  * case and also for the unlabeled training case.
597  */
598 TEST_CASE("GaussianHMMTrainTest", "[HMMTest]")
599 {
600   // Four emission Gaussians and three internal states.  The goal is to estimate
601   // the transition matrix correctly, and each distribution correctly.
602   std::vector<GaussianDistribution> emission;
603   emission.push_back(GaussianDistribution("0.0 0.0 0.0", "1.0 0.2 0.2;"
604                                                          "0.2 1.5 0.0;"
605                                                          "0.2 0.0 1.1"));
606   emission.push_back(GaussianDistribution("2.0 1.0 5.0", "0.7 0.3 0.0;"
607                                                          "0.3 2.6 0.0;"
608                                                          "0.0 0.0 1.0"));
609   emission.push_back(GaussianDistribution("5.0 0.0 0.5", "1.0 0.0 0.0;"
610                                                          "0.0 1.0 0.0;"
611                                                          "0.0 0.0 1.0"));
612 
613   arma::mat transition("0.3 0.5 0.7;"
614                        "0.3 0.4 0.1;"
615                        "0.4 0.1 0.2");
616 
617   // Now generate observations.
618   std::vector<arma::mat> observations(100);
619   std::vector<arma::Row<size_t> > states(100);
620 
621   for (size_t obs = 0; obs < 100; obs++)
622   {
623     observations[obs].set_size(3, 1000);
624     states[obs].set_size(1000);
625 
626     // Always start in state zero.
627     states[obs][0] = 0;
628     observations[obs].col(0) = emission[0].Random();
629 
630     for (size_t t = 1; t < 1000; t++)
631     {
632       // Choose the state.
633       double randValue = math::Random();
634       double probSum = 0;
635       for (size_t state = 0; state < 3; state++)
636       {
637         probSum += transition(state, states[obs][t - 1]);
638         if (probSum >= randValue)
639         {
640           states[obs][t] = state;
641           break;
642         }
643       }
644 
645       // Now choose the emission.
646       observations[obs].col(t) = emission[states[obs][t]].Random();
647     }
648   }
649 
650   // Now that the data is generated, train the HMM.
651   HMM<GaussianDistribution> hmm(3, GaussianDistribution(3));
652 
653   hmm.Train(observations, states);
654 
655   // Check initial weights.
656   REQUIRE(hmm.Initial()[0] == Approx(1.0).epsilon(1e-7));
657   REQUIRE(hmm.Initial()[1] == Approx(0.0).margin(1e-3));
658   REQUIRE(hmm.Initial()[2] == Approx(0.0).margin(1e-3));
659 
660   // We use a tolerance of 0.05 for the transition matrices.
661   // Check that the transition matrix is correct.
662   REQUIRE(arma::norm(hmm.Transition() - transition) < 0.05);
663 
664   // Check that each distribution is correct.
665   for (size_t dist = 0; dist < 3; dist++)
666   {
667     REQUIRE(arma::norm(hmm.Emission()[dist].Mean() -
668         emission[dist].Mean()) < 0.05);
669     REQUIRE(arma::norm(hmm.Emission()[dist].Covariance() -
670         emission[dist].Covariance()) < 0.1);
671   }
672 
673   // Now let's try it all again, but this time, unlabeled.  Everything will fail
674   // if we don't have a decent guess at the Gaussians, so we'll take a "poor"
675   // guess at it ourselves.  I won't use K-Means because we can't afford to add
676   // the instability of that to our test.  We'll leave the covariances as the
677   // identity.
678   HMM<GaussianDistribution> hmm2(3, GaussianDistribution(3));
679   hmm2.Emission()[0].Mean() = "0.3 -0.2 0.1"; // Actual: [0 0 0].
680   hmm2.Emission()[1].Mean() = "1.0 1.4 3.2";  // Actual: [2 1 5].
681   hmm2.Emission()[2].Mean() = "3.1 -0.2 6.1"; // Actual: [5 0 5].
682 
683   // We'll only use 20 observation sequences to try and keep training time
684   // shorter.
685   observations.resize(20);
686 
687   hmm.Train(observations);
688 
689   REQUIRE(hmm.Initial()[0] == Approx(1.0).epsilon(0.001));
690   REQUIRE(hmm.Initial()[1] == Approx(0.0).margin(0.05));
691   REQUIRE(hmm.Initial()[2] == Approx(0.0).margin(0.05));
692 
693   // The tolerances are increased because there is more error in unlabeled
694   // training; we use an absolute tolerance of 0.03 for the transition matrices.
695   // Check that the transition matrix is correct.
696   for (size_t row = 0; row < 3; row++)
697     for (size_t col = 0; col < 3; col++)
698       REQUIRE(transition(row, col) - hmm.Transition()(row, col) ==
699           Approx(.0).margin(0.03));
700 
701   // Check that each distribution is correct.
702   for (size_t dist = 0; dist < 3; dist++)
703   {
704     REQUIRE(arma::norm(hmm.Emission()[dist].Mean() -
705         emission[dist].Mean()) < 0.1);
706     REQUIRE(arma::norm(hmm.Emission()[dist].Covariance() -
707         emission[dist].Covariance()) < 0.25);
708   }
709 }
710 
711 /**
712  * Make sure that a random sequence generated by a Gaussian HMM fits the
713  * distribution correctly.
714  */
715 TEST_CASE("GaussianHMMGenerateTest", "[HMMTest]")
716 {
717   // Our distribution will have three two-dimensional output Gaussians.
718   HMM<GaussianDistribution> hmm(3, GaussianDistribution(2));
719   hmm.Transition() = arma::mat("0.4 0.6 0.8; 0.2 0.2 0.1; 0.4 0.2 0.1");
720   hmm.Emission()[0] = GaussianDistribution("0.0 0.0", "1.0 0.0; 0.0 1.0");
721   hmm.Emission()[1] = GaussianDistribution("2.0 2.0", "1.0 0.5; 0.5 1.2");
722   hmm.Emission()[2] = GaussianDistribution("-2.0 1.0", "2.0 0.1; 0.1 1.0");
723 
724   // Now we will generate a long sequence.
725   std::vector<arma::mat> observations(1);
726   std::vector<arma::Row<size_t> > states(1);
727 
728   // Start in state 1 (no reason).
729   hmm.Generate(10000, observations[0], states[0], 1);
730 
731   HMM<GaussianDistribution> hmm2(3, GaussianDistribution(2));
732 
733   // Now estimate the HMM from the generated sequence.
734   hmm2.Train(observations, states);
735 
736   // Check that the estimated matrices are the same.
737   REQUIRE(arma::norm(hmm.Transition() - hmm2.Transition()) < 0.1);
738 
739   // Check that each Gaussian is the same.
740   for (size_t dist = 0; dist < 3; dist++)
741   {
742     REQUIRE(arma::norm(hmm.Emission()[dist].Mean() -
743         hmm2.Emission()[dist].Mean()) < 0.2);
744     REQUIRE(arma::norm(hmm.Emission()[dist].Covariance() -
745         hmm2.Emission()[dist].Covariance()) < 0.3);
746   }
747 }
748 
749 /**
750  * Make sure that Predict() is numerically stable.
751  */
752 TEST_CASE("GaussianHMMPredictTest", "[HMMTest]")
753 {
754   size_t numState = 10;
755   size_t obsDimension = 2;
756   HMM<GaussianDistribution> hmm(numState, GaussianDistribution(obsDimension));
757 
758   arma::vec initial = {1.0000, 0, 0, 0, 0, 0, 0, 0, 0, 0};
759   arma::mat transition = {{0.9149, 0, 0, 0, 0, 0, 0, 0, 0, 0},
760                           {0.0851, 0.8814, 0, 0, 0, 0, 0, 0, 0, 0},
761                           {0, 0.1186, 0.9031, 0, 0, 0, 0, 0, 0, 0},
762                           {0, 0, 0.0969, 0.903, 0, 0, 0, 0, 0, 0},
763                           {0, 0, 0, 0.097, 0.8941, 0, 0, 0, 0, 0},
764                           {0, 0, 0, 0, 0.1059, 0.9024, 0, 0, 0, 0},
765                           {0, 0, 0, 0, 0, 0.0976, 0.8902, 0, 0, 0},
766                           {0, 0, 0, 0, 0, 0, 0.1098, 0.9107, 0, 0},
767                           {0, 0, 0, 0, 0, 0, 0, 0.0893, 0.8964, 0},
768                           {0, 0, 0, 0, 0, 0, 0, 0, 0.1036, 1}};
769 
770   std::vector<arma::vec> mean = {{0, 0.259},
771                                  {0.0372, 0.2063},
772                                  {0.1496, -0.3075},
773                                  {-0.0366, -0.3255},
774                                  {-0.2866, -0.0202},
775                                  {0.1804, 0.1385},
776                                  {0.1922, -0.0616},
777                                  {-0.378, -0.1751},
778                                  {-0.1346, 0.1357},
779                                  {0.338, 0.183}};
780 
781   std::vector<arma::mat> cov = {
782       {{3.2837e-07, 0}, {0, 0.032837}},
783       {{0.0154, -0.0093}, {-0.0093, 0.0358}},
784       {{0.1087, -0.0032}, {-0.0032, 0.0587}},
785       {{0.3185, -0.0069}, {-0.0069, 0.0396}},
786       {{0.3472, 0.0484}, {0.0484, 0.0706}},
787       {{0.39, 0.0406}, {0.0406, 0.0653}},
788       {{0.4502, 0.0718}, {0.0718, 0.0705}},
789       {{0.3253, 0.0312}, {0.0312, 0.0783}},
790       {{0.2355, 0.0195}, {0.0195, 0.0276}},
791       {{0.0818, 0.022}, {0.022, 0.0282}}};
792 
793   hmm.Initial() = initial;
794   hmm.Transition() = transition;
795 
796   for (size_t i = 0; i < numState; ++i)
797   {
798     GaussianDistribution& emission = hmm.Emission().at(i);
799     emission.Mean() = mean.at(i);
800     emission.Covariance(cov.at(i));
801   }
802 
803   arma::mat obs = {
804       {
805           -0.0424, -0.0395, -0.0336, -0.0294, -0.0299, -0.032, -0.0289, -0.0148,
806           0.0095, 0.0416, 0.0795, 0.1173, 0.1491, 0.1751, 0.1999, 0.2277,
807           0.2586, 0.2858, 0.3019, 0.303, 0.289, 0.2632, 0.2301, 0.1923, 0.1498,
808           0.1021, 0.0471, -0.0191, -0.0969, -0.1795, -0.2559, -0.323, -0.3882,
809           -0.4582, -0.5334, -0.609, -0.6778999999999999, -0.7278, -0.7481,
810           -0.7356, -0.6953, -0.635, -0.5617, -0.478, -0.3833, -0.2721, -0.1365,
811           0.0283, 0.217, 0.4148, 0.6028, 0.7664, 0.8937, 0.9737, 1, 0.972,
812           0.8972, 0.7891, 0.6613, 0.524, 0.3847, 0.2489, 0.1187, -0.0045,
813           -0.1214, -0.2316, -0.3328, -0.4211, -0.4963, -0.5607, -0.6136,
814           -0.6532, -0.6777, -0.6867, -0.6807, -0.6612, -0.6345, -0.6075,
815           -0.5748, -0.5278, -0.4747, -0.4176, -0.33, -0.2036, -0.0597,
816           0.07240000000000001, 0.1754, 0.2471, 0.295, 0.3356, 0.3809, 0.4299,
817           0.4737, 0.4987, 0.4958, 0.4676, 0.4253, 0.3802, 0.342, 0.3183
818       },
819       {
820           0.2355, 0.2639, 0.2971, 0.3301, 0.3598, 0.3842, 0.3995, 0.4019, 0.39,
821           0.3624, 0.3201, 0.2658, 0.203, 0.1341, 0.06, -0.0179, -0.1006,
822           -0.1869, -0.2719, -0.35, -0.4176, -0.4739, -0.52, -0.5584, -0.5913,
823           -0.6196, -0.642, -0.6554, -0.6567, -0.6459, -0.6271, -0.6029, -0.5722,
824           -0.5318000000000001, -0.4802, -0.4174, -0.3449, -0.2685, -0.1927,
825           -0.1201, -0.0532, 0.008699999999999999, 0.0673, 0.1204, 0.1647,
826           0.2008, 0.2284, 0.2447, 0.2504, 0.2479, 0.2373, 0.2148, 0.1781,
827           0.1283, 0.06710000000000001, -0.0022, -0.0743, -0.1463, -0.2149,
828           -0.2784, -0.3362, -0.3867, -0.4297, -0.4651, -0.4924, -0.5101,
829           -0.5168, -0.5117, -0.496, -0.4706, -0.4358, -0.3923, -0.3419, -0.2868,
830           -0.2289, -0.1702, -0.1094, -0.0421, 0.0311, 0.1047, 0.1732, 0.2257,
831           0.254, 0.2532, 0.2308, 0.2017, 0.1724, 0.1425, 0.1195, 0.099, 0.0759,
832           0.0521, 0.0313, 0.0188, 0.0113, 0.0068, 0.0042, 0.0026, 0.0018, 0.0014
833       }
834   };
835 
836   arma::Row<size_t> stateSeq;
837   auto likelihood = hmm.LogLikelihood(obs);
838   hmm.Predict(obs, stateSeq);
839 
840   arma::Row<size_t> stateSeqRef = { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
841       1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4,
842       4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7,
843       7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9,
844       9, 9, 9, 9, 9, 9, 9, 9, 9, 9 };
845 
846   REQUIRE(likelihood == Approx(-2734.43).epsilon(1e-5));
847 
848   for (size_t i = 0; i < stateSeqRef.n_cols; ++i)
849   {
850     REQUIRE(stateSeqRef.at(i) == stateSeq.at(i));
851   }
852 }
853 
854 /**
855  * Test that HMMs work with Gaussian mixture models.  We'll try putting in a
856  * simple model by hand and making sure that prediction of observation sequences
857  * works correctly.
858  */
859 TEST_CASE("GMMHMMPredictTest", "[HMMTest]")
860 {
861   // It's possible, but extremely unlikely, that this test can fail.  So we are
862   // willing to do three trials in case the first two fail.
863   bool success = false;
864   for (size_t trial = 0; trial < 3; ++trial)
865   {
866     // We will use two GMMs; one with two components and one with three.
867     std::vector<GMM> gmms(2);
868     gmms[0] = GMM(2, 2);
869     gmms[0].Weights() = arma::vec("0.75 0.25");
870 
871     // N([2.25 3.10], [1.00 0.20; 0.20 0.89])
872     gmms[0].Component(0) = GaussianDistribution("4.25 3.10",
873                                                 "1.00 0.20; 0.20 0.89");
874 
875     // N([4.10 1.01], [1.00 0.00; 0.00 1.01])
876     gmms[0].Component(1) = GaussianDistribution("7.10 5.01",
877                                                 "1.00 0.00; 0.00 1.01");
878 
879     gmms[1] = GMM(3, 2);
880     gmms[1].Weights() = arma::vec("0.4 0.2 0.4");
881 
882     gmms[1].Component(0) = GaussianDistribution("-3.00 -6.12",
883                                                 "1.00 0.00; 0.00 1.00");
884 
885     gmms[1].Component(1) = GaussianDistribution("-4.25 -7.12",
886                                                 "1.50 0.60; 0.60 1.20");
887 
888     gmms[1].Component(2) = GaussianDistribution("-6.15 -2.00",
889                                                 "1.00 0.80; 0.80 1.00");
890 
891     // Default MATLAB initial probabilities.
892     arma::vec initial("1 0");
893 
894     // Transition matrix.
895     arma::mat trans("0.30 0.50;"
896                     "0.70 0.50");
897 
898     // Now build the model.
899     HMM<GMM> hmm(initial, trans, gmms);
900 
901     // Make a sequence of observations.
902     arma::mat observations(2, 1000);
903     arma::Row<size_t> states(1000);
904     states[0] = 0;
905     observations.col(0) = gmms[0].Random();
906 
907     for (size_t i = 1; i < 1000; ++i)
908     {
909       double randValue = math::Random();
910 
911       if (randValue <= trans(0, states[i - 1]))
912         states[i] = 0;
913       else
914         states[i] = 1;
915 
916       observations.col(i) = gmms[states[i]].Random();
917     }
918 
919     // Run the prediction.
920     arma::Row<size_t> predictions;
921     hmm.Predict(observations, predictions);
922 
923     // Check that the predictions were correct.
924     success = true;
925     for (size_t i = 0; i < 1000; ++i)
926     {
927       if (predictions[i] != states[i])
928       {
929         success = false;
930         break;
931       }
932     }
933 
934     if (success)
935       break;
936   }
937 
938   REQUIRE(success == true);
939 }
940 
941 /**
942  * Test that GMM-based HMMs can train on models correctly using labeled training
943  * data.
944  */
945 TEST_CASE("GMMHMMLabeledTrainingTest", "[HMMTest]")
946 {
947   // We will use two GMMs; one with two components and one with three.
948   std::vector<GMM> gmms(2, GMM(2, 2));
949   gmms[0].Weights() = arma::vec("0.3 0.7");
950 
951   // N([2.25 3.10], [1.00 0.20; 0.20 0.89])
952   gmms[0].Component(0) = GaussianDistribution("4.25 3.10",
953                                               "1.00 0.20; 0.20 0.89");
954 
955   // N([4.10 1.01], [1.00 0.00; 0.00 1.01])
956   gmms[0].Component(1) = GaussianDistribution("7.10 5.01",
957                                               "1.00 0.00; 0.00 1.01");
958 
959   gmms[1].Weights() = arma::vec("0.20 0.80");
960 
961   gmms[1].Component(0) = GaussianDistribution("-3.00 -6.12",
962                                               "1.00 0.00; 0.00 1.00");
963 
964   gmms[1].Component(1) = GaussianDistribution("-4.25 -2.12",
965                                               "1.50 0.60; 0.60 1.20");
966 
967   // Transition matrix.
968   arma::mat transMat("0.40 0.60;"
969                      "0.60 0.40");
970 
971   // Make a sequence of observations.
972   std::vector<arma::mat> observations(5, arma::mat(2, 2500));
973   std::vector<arma::Row<size_t> > states(5, arma::Row<size_t>(2500));
974   for (size_t obs = 0; obs < 5; obs++)
975   {
976     states[obs][0] = 0;
977     observations[obs].col(0) = gmms[0].Random();
978 
979     for (size_t i = 1; i < 2500; ++i)
980     {
981       double randValue = (double) rand() / (double) RAND_MAX;
982 
983       if (randValue <= transMat(0, states[obs][i - 1]))
984         states[obs][i] = 0;
985       else
986         states[obs][i] = 1;
987 
988       observations[obs].col(i) = gmms[states[obs][i]].Random();
989     }
990   }
991 
992   // Set up the GMM for training.
993   HMM<GMM> hmm(2, GMM(2, 2));
994 
995   // Train the HMM.
996   hmm.Train(observations, states);
997 
998   // Check the initial weights.  The dataset was generated with 100% probability
999   // of a sequence starting in state 0.
1000   REQUIRE(hmm.Initial()[0] == Approx(1.0).epsilon(0.0001));
1001   REQUIRE(hmm.Initial()[1] == Approx(.0).margin(0.01));
1002 
1003   // Check the results.  Use absolute tolerances instead of percentages.
1004   REQUIRE(hmm.Transition()(0, 0) - transMat(0, 0) == Approx(.0).margin(0.03));
1005   REQUIRE(hmm.Transition()(0, 1) - transMat(0, 1) == Approx(.0).margin(0.03));
1006   REQUIRE(hmm.Transition()(1, 0) - transMat(1, 0) == Approx(.0).margin(0.03));
1007   REQUIRE(hmm.Transition()(1, 1) - transMat(1, 1) == Approx(.0).margin(0.03));
1008 
1009   // Now the emission probabilities (the GMMs).
1010   // We have to sort each GMM for comparison.
1011   arma::uvec sortedIndices = sort_index(hmm.Emission()[0].Weights());
1012 
1013   REQUIRE(hmm.Emission()[0].Weights()[sortedIndices[0]] -
1014       gmms[0].Weights()[0] == Approx(.0).margin(0.08));
1015   REQUIRE(hmm.Emission()[0].Weights()[sortedIndices[1]] -
1016       gmms[0].Weights()[1] == Approx(.0).margin(0.08));
1017 
1018   REQUIRE(arma::norm(
1019       hmm.Emission()[0].Component(sortedIndices[0]).Mean() -
1020       gmms[0].Component(0).Mean()) < 0.2);
1021   REQUIRE(arma::norm(
1022       hmm.Emission()[0].Component(sortedIndices[1]).Mean() -
1023       gmms[0].Component(1).Mean()) < 0.2);
1024 
1025   REQUIRE(arma::norm(
1026       hmm.Emission()[0].Component(sortedIndices[0]).Covariance() -
1027       gmms[0].Component(0).Covariance()) < 0.5);
1028   REQUIRE(arma::norm(
1029       hmm.Emission()[0].Component(sortedIndices[1]).Covariance() -
1030       gmms[0].Component(0).Covariance()) < 0.5);
1031 
1032   // Sort the GMM.
1033   sortedIndices = sort_index(hmm.Emission()[1].Weights());
1034 
1035   REQUIRE(hmm.Emission()[1].Weights()[sortedIndices[0]] -
1036       gmms[1].Weights()[0] == Approx(.0).margin(0.08));
1037   REQUIRE(hmm.Emission()[1].Weights()[sortedIndices[1]] -
1038       gmms[1].Weights()[1] == Approx(.0).margin(0.08));
1039 
1040   REQUIRE(arma::norm(
1041       hmm.Emission()[1].Component(sortedIndices[0]).Mean() -
1042       gmms[1].Component(0).Mean()) < 0.2);
1043   REQUIRE(arma::norm(
1044       hmm.Emission()[1].Component(sortedIndices[1]).Mean() -
1045       gmms[1].Component(1).Mean()) < 0.2);
1046 
1047   REQUIRE(arma::norm(
1048       hmm.Emission()[1].Component(sortedIndices[0]).Covariance() -
1049       gmms[1].Component(0).Covariance()) < 0.5);
1050   REQUIRE(arma::norm(
1051       hmm.Emission()[1].Component(sortedIndices[1]).Covariance() -
1052       gmms[1].Component(1).Covariance()) < 0.5);
1053 }
1054 
1055 /**
1056  * Test saving and loading of GMM HMMs
1057  */
1058 TEST_CASE("GMMHMMLoadSaveTest", "[HMMTest]")
1059 {
1060   // Create a GMM HMM, save it, and load it.
1061   HMM<GMM> hmm(3, GMM(4, 3));
1062 
1063   for (size_t j = 0; j < hmm.Emission().size(); ++j)
1064   {
1065     hmm.Emission()[j].Weights().randu();
1066     for (size_t i = 0; i < hmm.Emission()[j].Gaussians(); ++i)
1067     {
1068       hmm.Emission()[j].Component(i).Mean().randu();
1069       arma::mat covariance = arma::randu<arma::mat>(
1070           hmm.Emission()[j].Component(i).Covariance().n_rows,
1071           hmm.Emission()[j].Component(i).Covariance().n_cols);
1072       covariance *= covariance.t();
1073       covariance += arma::eye<arma::mat>(covariance.n_rows, covariance.n_cols);
1074       hmm.Emission()[j].Component(i).Covariance(std::move(covariance));
1075     }
1076   }
1077 
1078   // Save the HMM.
1079   {
1080     std::ofstream ofs("test-hmm-save.xml");
1081     boost::archive::xml_oarchive ar(ofs);
1082     ar << BOOST_SERIALIZATION_NVP(hmm);
1083   }
1084 
1085   // Load the HMM.
1086   HMM<GMM> hmm2(3, GMM(4, 3));
1087   {
1088     std::ifstream ifs("test-hmm-save.xml");
1089     boost::archive::xml_iarchive ar(ifs);
1090     ar >> BOOST_SERIALIZATION_NVP(hmm2);
1091   }
1092 
1093   // Remove clutter.
1094   remove("test-hmm-save.xml");
1095 
1096   for (size_t j = 0; j < hmm.Emission().size(); ++j)
1097   {
1098     REQUIRE(hmm.Emission()[j].Gaussians() ==
1099                         hmm2.Emission()[j].Gaussians());
1100     REQUIRE(hmm.Emission()[j].Dimensionality() ==
1101                         hmm2.Emission()[j].Dimensionality());
1102 
1103     for (size_t i = 0; i < hmm.Emission()[j].Dimensionality(); ++i)
1104       REQUIRE(hmm.Emission()[j].Weights()[i] ==
1105           Approx(hmm2.Emission()[j].Weights()[i]).epsilon(1e-5));
1106 
1107     for (size_t i = 0; i < hmm.Emission()[j].Gaussians(); ++i)
1108     {
1109       for (size_t l = 0; l < hmm.Emission()[j].Dimensionality(); ++l)
1110       {
1111         REQUIRE(hmm.Emission()[j].Component(i).Mean()[l] ==
1112             Approx(hmm2.Emission()[j].Component(i).Mean()[l]).epsilon(1e-5));
1113 
1114         for (size_t k = 0; k < hmm.Emission()[j].Dimensionality(); ++k)
1115         {
1116           REQUIRE(hmm.Emission()[j].Component(i).Covariance()(l, k) ==
1117               Approx(hmm2.Emission()[j].Component(i).Covariance()(l,
1118               k)).epsilon(1e-5));
1119         }
1120       }
1121     }
1122   }
1123 }
1124 
1125 /**
1126  * Test saving and loading of Gaussian HMMs
1127  */
1128 TEST_CASE("GaussianHMMLoadSaveTest", "[HMMTest]")
1129 {
1130   // Create a Gaussian HMM, save it, and load it.
1131   HMM<GaussianDistribution> hmm(3, GaussianDistribution(2));
1132 
1133   for (size_t j = 0; j < hmm.Emission().size(); ++j)
1134   {
1135     hmm.Emission()[j].Mean().randu();
1136     arma::mat covariance = arma::randu<arma::mat>(
1137         hmm.Emission()[j].Covariance().n_rows,
1138         hmm.Emission()[j].Covariance().n_cols);
1139     covariance *= covariance.t();
1140     covariance += arma::eye<arma::mat>(covariance.n_rows, covariance.n_cols);
1141     hmm.Emission()[j].Covariance(std::move(covariance));
1142   }
1143 
1144   // Save the HMM.
1145   {
1146     std::ofstream ofs("test-hmm-save.xml");
1147     boost::archive::xml_oarchive ar(ofs);
1148     ar << BOOST_SERIALIZATION_NVP(hmm);
1149   }
1150 
1151   // Load the HMM.
1152   HMM<GaussianDistribution> hmm2(3, GaussianDistribution(2));
1153   {
1154     std::ifstream ifs("test-hmm-save.xml");
1155     boost::archive::xml_iarchive ar(ifs);
1156     ar >> BOOST_SERIALIZATION_NVP(hmm2);
1157   }
1158 
1159   // Remove clutter.
1160   remove("test-hmm-save.xml");
1161 
1162   for (size_t j = 0; j < hmm.Emission().size(); ++j)
1163   {
1164     REQUIRE(hmm.Emission()[j].Dimensionality() ==
1165                         hmm2.Emission()[j].Dimensionality());
1166 
1167     for (size_t i = 0; i < hmm.Emission()[j].Dimensionality(); ++i)
1168     {
1169         REQUIRE(hmm.Emission()[j].Mean()[i] ==
1170             Approx(hmm2.Emission()[j].Mean()[i]).epsilon(1e-5));
1171 
1172       for (size_t k = 0; k < hmm.Emission()[j].Dimensionality(); ++k)
1173       {
1174         REQUIRE(hmm.Emission()[j].Covariance()(i, k) ==
1175             Approx(hmm2.Emission()[j].Covariance()(i, k)).epsilon(1e-5));
1176       }
1177     }
1178   }
1179 }
1180 
1181 /**
1182  * Test saving and loading of Discrete HMMs
1183  */
1184 TEST_CASE("DiscreteHMMLoadSaveTest", "[HMMTest]")
1185 {
1186   // Create a Discrete HMM, save it, and load it.
1187   std::vector<DiscreteDistribution> emission(4);
1188   emission[0].Probabilities() = arma::randu<arma::vec>(6);
1189   emission[0].Probabilities() /= accu(emission[0].Probabilities());
1190   emission[1].Probabilities() = arma::randu<arma::vec>(6);
1191   emission[1].Probabilities() /= accu(emission[1].Probabilities());
1192   emission[2].Probabilities() = arma::randu<arma::vec>(6);
1193   emission[2].Probabilities() /= accu(emission[2].Probabilities());
1194   emission[3].Probabilities() = arma::randu<arma::vec>(6);
1195   emission[3].Probabilities() /= accu(emission[3].Probabilities());
1196 
1197 
1198   // Create HMM object.
1199   HMM<DiscreteDistribution> hmm(3, DiscreteDistribution(3));
1200 
1201 
1202   for (size_t j = 0; j < hmm.Emission().size(); ++j)
1203   {
1204     hmm.Emission()[j].Probabilities() = arma::randu<arma::vec>(3);
1205     hmm.Emission()[j].Probabilities() /= accu(emission[j].Probabilities());
1206   }
1207 
1208   // Save the HMM.
1209   {
1210     std::ofstream ofs("test-hmm-save.xml");
1211     boost::archive::xml_oarchive ar(ofs);
1212     ar << BOOST_SERIALIZATION_NVP(hmm);
1213   }
1214 
1215   // Load the HMM.
1216   HMM<DiscreteDistribution> hmm2(3, DiscreteDistribution(3));
1217   {
1218     std::ifstream ifs("test-hmm-save.xml");
1219     boost::archive::xml_iarchive ar(ifs);
1220     ar >> BOOST_SERIALIZATION_NVP(hmm2);
1221   }
1222 
1223   // Remove clutter.
1224   remove("test-hmm-save.xml");
1225 
1226   for (size_t j = 0; j < hmm.Emission().size(); ++j)
1227     for (size_t i = 0; i < hmm.Emission()[j].Probabilities().n_elem; ++i)
1228       REQUIRE(hmm.Emission()[j].Probabilities()[i] ==
1229           Approx(hmm2.Emission()[j].Probabilities()[i]).epsilon(1e-5));
1230 }
1231 
1232 /**
1233  * Test that HMM::Train() returns finite log-likelihood.
1234  */
1235 TEST_CASE("HMMTrainReturnLogLikelihood", "[HMMTest]")
1236 {
1237   HMM<DiscreteDistribution> hmm(1, 2); // 1 state, 2 emissions.
1238   // Randomize the emission matrix.
1239   hmm.Emission()[0].Probabilities() = arma::randu<arma::vec>(2);
1240   hmm.Emission()[0].Probabilities() /= accu(hmm.Emission()[0].Probabilities());
1241 
1242   std::vector<arma::mat> observations;
1243   observations.push_back("0 1 0 1 0 1 0 1 0 1 0 1");
1244   observations.push_back("0 0 0 0 0 0 1 1 1 1 1 1");
1245   observations.push_back("1 1 1 1 1 1 0 0 0 0 0 0");
1246   observations.push_back("1 1 1 0 0 0 1 1 1 0 0 0");
1247   observations.push_back("0 0 1 1 0 0 0 0 1 1 1 1");
1248   observations.push_back("1 1 1 0 0 0 1 1 1 0 0 0");
1249   observations.push_back("0 1 0 1 0 1 0 1 0 1 0 1");
1250   observations.push_back("0 0 0 0 0 0 1 1 1 1 1 1");
1251   observations.push_back("1 1 1 1 1 0 1 0 0 0 0 0");
1252   observations.push_back("1 1 1 0 0 1 0 1 1 0 0 0");
1253   observations.push_back("0 0 1 1 0 0 0 1 0 1 1 1");
1254   observations.push_back("1 1 1 0 0 1 0 1 1 0 0 0");
1255 
1256   double loglik = hmm.Train(observations);
1257 
1258   REQUIRE(std::isfinite(loglik) == true);
1259 }
1260 
1261 /********************************************/
1262 /** DiagonalGMM Hidden Markov Models Tests **/
1263 /********************************************/
1264 
1265 //! Make sure the prediction of DiagonalGMM HMMs is reasonable.
1266 TEST_CASE("DiagonalGMMHMMPredictTest", "[HMMTest]")
1267 {
1268   // This test is probabilistic, so we perform it three times to make it robust.
1269   bool success = false;
1270   for (size_t trial = 0; trial < 3; trial++)
1271   {
1272     std::vector<DiagonalGMM> gmms(2);
1273     gmms[0] = DiagonalGMM(2, 2);
1274 
1275     gmms[0].Component(0) = DiagonalGaussianDistribution("3.25 2.10",
1276         "0.97 1.00");
1277     gmms[0].Component(1) = DiagonalGaussianDistribution("5.03 7.28",
1278         "1.20 0.89");
1279 
1280     gmms[1] = DiagonalGMM(3, 2);
1281     gmms[1].Weights() = arma::vec("0.3 0.2 0.5");
1282     gmms[1].Component(0) = DiagonalGaussianDistribution("-2.48 -3.02",
1283         "1.02 0.80");
1284     gmms[1].Component(1) = DiagonalGaussianDistribution("-1.24 -2.40",
1285         "0.85 0.78");
1286     gmms[1].Component(2) = DiagonalGaussianDistribution("-5.68 -4.83",
1287         "1.42 0.96");
1288 
1289     // Initial probabilities.
1290     arma::vec initial("1 0");
1291 
1292     // Transition matrix.
1293     arma::mat transProb("0.40 0.70;"
1294                         "0.60 0.30");
1295 
1296     // Build the model.
1297     HMM<DiagonalGMM> hmm(initial, transProb, gmms);
1298 
1299     // Make a sequence of observations according to transition probabilities.
1300     arma::mat observations(2, 1000);
1301     arma::Row<size_t> states(1000);
1302 
1303     // Set initial state to zero.
1304     states[0] = 0;
1305     observations.col(0) = gmms[0].Random();
1306 
1307     for (size_t i = 1; i < 1000; ++i)
1308     {
1309       double randValue = math::Random();
1310 
1311       if (randValue <= transProb(0, states[i - 1]))
1312         states[i] = 0;
1313       else
1314         states[i] = 1;
1315 
1316       observations.col(i) = gmms[states[i]].Random();
1317     }
1318 
1319     // Predict the most probable hidden state sequence.
1320     arma::Row<size_t> predictions;
1321     hmm.Predict(observations, predictions);
1322 
1323     // Check them.
1324     success = true;
1325     for (size_t i = 0; i < 1000; ++i)
1326     {
1327       if (predictions[i] != states[i])
1328       {
1329         success = false;
1330         break;
1331       }
1332     }
1333 
1334     if (success)
1335       break;
1336   }
1337 
1338   REQUIRE(success == true);
1339 }
1340 
1341 /**
1342  * Make sure a random data sequence generation is correct when the emission
1343  * distribution is DiagonalGMM.
1344  */
1345 TEST_CASE("DiagonalGMMHMMGenerateTest", "[HMMTest]")
1346 {
1347   // Build the model.
1348   HMM<DiagonalGaussianDistribution> hmm(3, DiagonalGaussianDistribution(2));
1349   hmm.Transition() = arma::mat("0.2 0.3 0.8;"
1350                                "0.4 0.5 0.1;"
1351                                "0.4 0.2 0.1");
1352 
1353   hmm.Emission()[0] = DiagonalGaussianDistribution("0.0 0.0", "1.0 0.7");
1354   hmm.Emission()[1] = DiagonalGaussianDistribution("1.0 1.0", "0.7 0.5");
1355   hmm.Emission()[2] = DiagonalGaussianDistribution("-3.0 2.0", "2.0 0.3");
1356 
1357   // Now we will generate a long sequence.
1358   std::vector<arma::mat> observations(1);
1359   std::vector<arma::Row<size_t> > states(1);
1360 
1361   // Generate a random data sequence.
1362   hmm.Generate(10000, observations[0], states[0], 1);
1363 
1364   // Build the hmm2.
1365   HMM<DiagonalGaussianDistribution> hmm2(3, DiagonalGaussianDistribution(2));
1366 
1367   // Now estimate the HMM from the generated sequence.
1368   hmm2.Train(observations, states);
1369 
1370   // Check that the estimated matrices are the same.
1371   REQUIRE(arma::norm(hmm.Transition() - hmm2.Transition()) < 0.05);
1372 
1373   // Check that each Gaussian is the same.
1374   for (size_t dist = 0; dist < 3; dist++)
1375   {
1376     REQUIRE(arma::norm(hmm.Emission()[dist].Mean() -
1377         hmm2.Emission()[dist].Mean()) < 0.1);
1378     REQUIRE(arma::norm(hmm.Emission()[dist].Covariance() -
1379         hmm2.Emission()[dist].Covariance()) < 0.2);
1380   }
1381 }
1382 
1383 /**
1384  * Make sure the unlabeled 1-state training works reasonably given a single
1385  * distribution with diagonal covariance.
1386  */
1387 TEST_CASE("DiagonalGMMHMMOneGaussianOneStateTrainingTest", "[HMMTest]")
1388 {
1389   // Create a Gaussian distribution with diagonal covariance.
1390   DiagonalGaussianDistribution d("2.05 3.45", "0.89 1.05");
1391 
1392   // Make a sequence of observations.
1393   std::vector<arma::mat> observations(1, arma::mat(2, 5000));
1394   for (size_t obs = 0; obs < 1; obs++)
1395   {
1396     observations[obs].col(0) = d.Random();
1397 
1398     for (size_t i = 1; i < 5000; ++i)
1399     {
1400       observations[obs].col(i) = d.Random();
1401     }
1402   }
1403 
1404   // Build the model.
1405   HMM<DiagonalGMM> hmm(1, DiagonalGMM(1, 2));
1406 
1407   // Train with observations.
1408   hmm.Train(observations);
1409 
1410   // Generate the ground truth values.
1411   arma::vec actualMean = arma::mean(observations[0], 1);
1412   arma::vec actualCovar = arma::diagvec(
1413       mlpack::math::ColumnCovariance(observations[0],
1414       1 /* biased estimator */));
1415 
1416   // Check the model to see that it is correct.
1417   CheckMatrices(hmm.Emission()[0].Component(0).Mean(), actualMean);
1418   CheckMatrices(hmm.Emission()[0].Component(0).Covariance(), actualCovar);
1419 }
1420 
1421 /**
1422  * Make sure the unlabeled training works reasonably given a single
1423  * distribution with diagonal covariance.
1424  */
1425 TEST_CASE("DiagonalGMMHMMOneGaussianUnlabeledTrainingTest", "[HMMTest]")
1426 {
1427   // Create a sequence of DiagonalGMMs. Each GMM has one gaussian distribution.
1428   std::vector<DiagonalGMM> gmms(2, DiagonalGMM(1, 2));
1429   gmms[0].Component(0) = DiagonalGaussianDistribution("1.25 2.10",
1430       "0.97 1.00");
1431 
1432   gmms[1].Component(0) = DiagonalGaussianDistribution("-2.48 -3.02",
1433       "1.02 0.80");
1434 
1435   // Transition matrix.
1436   arma::mat transProbs("0.30 0.80;"
1437                        "0.70 0.20");
1438 
1439   arma::vec initialProb("1 0");
1440 
1441   // Make a sequence of observations.
1442   std::vector<arma::mat> observations(2, arma::mat(2, 500));
1443   std::vector<arma::Row<size_t>> states(2, arma::Row<size_t>(500));
1444   for (size_t obs = 0; obs < 2; obs++)
1445   {
1446     states[obs][0] = 0;
1447     observations[obs].col(0) = gmms[0].Random();
1448 
1449     for (size_t i = 1; i < 500; ++i)
1450     {
1451       double randValue = math::Random();
1452 
1453       if (randValue <= transProbs(0, states[obs][i - 1]))
1454         states[obs][i] = 0;
1455       else
1456         states[obs][i] = 1;
1457 
1458       observations[obs].col(i) = gmms[states[obs][i]].Random();
1459     }
1460   }
1461 
1462   // Build the model.
1463   HMM<DiagonalGMM> hmm(initialProb, transProbs, gmms);
1464 
1465   // Train the model. If labels are not given, when training GMM, the estimated
1466   // probabilities based on the forward and backward probabilities is used.
1467   hmm.Train(observations);
1468 
1469   // Check the initial weights.
1470   REQUIRE(hmm.Initial()[0] == Approx(1.0).epsilon(0.0001));
1471   REQUIRE(hmm.Initial()[1] == Approx(0.0).margin(0.01));
1472 
1473   // Check the transition probability matrix.
1474   for (size_t i = 0; i < 2; ++i)
1475     for (size_t j = 0; j < 2; ++j)
1476       REQUIRE(hmm.Transition()(i, j) - transProbs(i, j) ==
1477           Approx(0.0).margin(0.08));
1478 
1479   // Check the estimated weights of the each emission distribution.
1480   for (size_t i = 0; i < 2; ++i)
1481     REQUIRE(hmm.Emission()[i].Weights()[0] - gmms[i].Weights()[0] ==
1482         Approx(0.0).margin(0.08));
1483 
1484   // Check the estimated means of the each emission distribution.
1485   for (size_t i = 0; i < 2; ++i)
1486     REQUIRE(arma::norm(hmm.Emission()[i].Component(0).Mean() -
1487         gmms[i].Component(0).Mean()) < 0.2);
1488 
1489   // Check the estimated covariances of the each emission distribution.
1490   for (size_t i = 0; i < 2; ++i)
1491     REQUIRE(arma::norm(hmm.Emission()[i].Component(0).Covariance() -
1492         gmms[i].Component(0).Covariance()) < 0.5);
1493 }
1494 
1495 /**
1496  * Make sure the labeled training works reasonably given a single distribution
1497  * with diagonal covariance.
1498  */
1499 TEST_CASE("DiagonalGMMHMMOneGaussianLabeledTrainingTest", "[HMMTest]")
1500 {
1501   // Create a sequence of DiagonalGMMs.
1502   std::vector<DiagonalGMM> gmms(3, DiagonalGMM(1, 2));
1503   gmms[0].Component(0) = DiagonalGaussianDistribution("5.25 7.10",
1504       "0.97 1.00");
1505 
1506   gmms[1].Component(0) = DiagonalGaussianDistribution("4.48 6.02",
1507       "1.02 0.80");
1508 
1509   gmms[2].Component(0) = DiagonalGaussianDistribution("-3.28 -5.30",
1510       "0.87 1.05");
1511 
1512   // Transition matrix.
1513   arma::mat transProbs("0.2 0.4 0.4;"
1514                        "0.3 0.4 0.3;"
1515                        "0.5 0.2 0.3");
1516 
1517   arma::vec initialProb("1 0 0");
1518 
1519   // Make a sequence of observations.
1520   std::vector<arma::mat> observations(3, arma::mat(2, 5000));
1521   std::vector<arma::Row<size_t>> states(3, arma::Row<size_t>(5000));
1522   for (size_t obs = 0; obs < 3; obs++)
1523   {
1524     states[obs][0] = 0;
1525     observations[obs].col(0) = gmms[0].Random();
1526 
1527     for (size_t i = 1; i < 5000; ++i)
1528     {
1529       double randValue = math::Random();
1530       double probSum = 0;
1531       for (size_t state = 0; state < 3; state++)
1532       {
1533         probSum += transProbs(state, states[obs][i - 1]);
1534         if (randValue <= probSum)
1535         {
1536           states[obs][i] = state;
1537           break;
1538         }
1539       }
1540 
1541       observations[obs].col(i) = gmms[states[obs][i]].Random();
1542     }
1543   }
1544 
1545   // Build the model.
1546   HMM<DiagonalGMM> hmm(3, DiagonalGMM(1, 2));
1547 
1548   // Train the model.
1549   hmm.Train(observations, states);
1550 
1551   // Check the initial weights.
1552   REQUIRE(hmm.Initial()[0] == Approx(1.0).epsilon(0.0001));
1553   REQUIRE(hmm.Initial()[1] == Approx(0.0).margin(0.01));
1554   REQUIRE(hmm.Initial()[2] == Approx(0.0).margin(0.01));
1555 
1556   // Check the transition probability matrix.
1557   for (size_t i = 0; i < 3; ++i)
1558     for (size_t j = 0; j < 3; ++j)
1559       REQUIRE(hmm.Transition()(i, j) - transProbs(i, j) ==
1560           Approx(0.0).margin(0.03));
1561 
1562   // Check the estimated weights of the each emission distribution.
1563   for (size_t i = 0; i < 3; ++i)
1564     REQUIRE(hmm.Emission()[i].Weights()[0] - gmms[i].Weights()[0] ==
1565         Approx(0.0).margin(0.08));
1566 
1567   // Check the estimated means of the each emission distribution.
1568   for (size_t i = 0; i < 3; ++i)
1569     REQUIRE(arma::norm(hmm.Emission()[i].Component(0).Mean() -
1570         gmms[i].Component(0).Mean()) < 0.2);
1571 
1572   // Check the estimated covariances of the each emission distribution.
1573   for (size_t i = 0; i < 3; ++i)
1574     REQUIRE(arma::norm(hmm.Emission()[i].Component(0).Covariance() -
1575         gmms[i].Component(0).Covariance()) < 0.5);
1576 }
1577 
1578 /**
1579  * Make sure the unlabeled training works reasonably given multiple
1580  * distributions with diagonal covariance.
1581  */
1582 TEST_CASE("DiagonalGMMHMMMultipleGaussiansUnlabeledTrainingTest", "[HMMTest]")
1583 {
1584   // Create a sequence of DiagonalGMMs.
1585   std::vector<DiagonalGMM> gmms(2, DiagonalGMM(2, 2));
1586   gmms[0].Weights() = arma::vec("0.3 0.7");
1587   gmms[0].Component(0) = DiagonalGaussianDistribution("8.25 7.10",
1588       "0.97 1.00");
1589   gmms[0].Component(1) = DiagonalGaussianDistribution("-3.03 -2.28",
1590       "1.20 0.89");
1591 
1592   gmms[1].Weights() = arma::vec("0.4 0.6");
1593   gmms[1].Component(0) = DiagonalGaussianDistribution("4.48 6.02",
1594         "1.02 0.80");
1595   gmms[1].Component(1) = DiagonalGaussianDistribution("-9.24 -8.40",
1596         "0.85 1.58");
1597 
1598   // Transition matrix.
1599   arma::mat transProbs("0.30 0.40;"
1600                        "0.70 0.60");
1601 
1602   arma::vec initialProb("1 0");
1603 
1604   // Make a sequence of observations.
1605   std::vector<arma::mat> observations(2, arma::mat(2, 1000));
1606   std::vector<arma::Row<size_t>> states(2, arma::Row<size_t>(1000));
1607   for (size_t obs = 0; obs < 2; obs++)
1608   {
1609     states[obs][0] = 0;
1610     observations[obs].col(0) = gmms[0].Random();
1611 
1612     for (size_t i = 1; i < 1000; ++i)
1613     {
1614       double randValue = math::Random();
1615 
1616       if (randValue <= transProbs(0, states[obs][i - 1]))
1617         states[obs][i] = 0;
1618       else
1619         states[obs][i] = 1;
1620 
1621       observations[obs].col(i) = gmms[states[obs][i]].Random();
1622     }
1623   }
1624 
1625   // Build the model.
1626   HMM<DiagonalGMM> hmm(initialProb, transProbs, gmms);
1627 
1628   // Train the model. If labels are not given, when training GMM, the estimated
1629   // probabilities based on the forward and backward probabilities is used.
1630   hmm.Train(observations);
1631 
1632   // Check the initial weights.
1633   REQUIRE(hmm.Initial()[0] == Approx(1.0).epsilon(0.0001));
1634   REQUIRE(hmm.Initial()[1] == Approx(0.0).margin(0.01));
1635 
1636   // Check the transition probability matrix.
1637   for (size_t i = 0; i < 2; ++i)
1638     for (size_t j = 0; j < 2; ++j)
1639       REQUIRE(hmm.Transition()(i, j) - transProbs(i, j) ==
1640           Approx(0.0).margin(0.08));
1641 
1642   // Sort by the estimated weights of the first emission distribution.
1643   arma::uvec sortedIndices = sort_index(hmm.Emission()[0].Weights());
1644 
1645   // Check the first emission distribution.
1646   for (size_t i = 0; i < 2; ++i)
1647   {
1648     // Check the estimated weights using the first DiagonalGMM.
1649     REQUIRE(hmm.Emission()[0].Weights()[sortedIndices[i]] -
1650         gmms[0].Weights()[i] == Approx(0.0).margin(0.08));
1651 
1652     // Check the estimated means using the first DiagonalGMM.
1653     REQUIRE(arma::norm(
1654       hmm.Emission()[0].Component(sortedIndices[i]).Mean() -
1655       gmms[0].Component(i).Mean()) < 0.35);
1656 
1657     // Check the estimated covariances using the first DiagonalGMM.
1658     REQUIRE(arma::norm(
1659       hmm.Emission()[0].Component(sortedIndices[i]).Covariance() -
1660       gmms[0].Component(i).Covariance()) < 0.6);
1661   }
1662 
1663   // Sort by the estimated weights of the second emission distribution.
1664   sortedIndices = sort_index(hmm.Emission()[1].Weights());
1665 
1666   // Check the second emission distribution.
1667   for (size_t i = 0; i < 2; ++i)
1668   {
1669     // Check the estimated weights using the second DiagonalGMM.
1670     REQUIRE(hmm.Emission()[1].Weights()[sortedIndices[i]] -
1671         gmms[1].Weights()[i] == Approx(0.0).margin(0.08));
1672 
1673     // Check the estimated means using the second DiagonalGMM.
1674     REQUIRE(arma::norm(
1675       hmm.Emission()[1].Component(sortedIndices[i]).Mean() -
1676       gmms[1].Component(i).Mean()) < 0.35);
1677 
1678     // Check the estimated covariances using the second DiagonalGMM.
1679     REQUIRE(arma::norm(
1680       hmm.Emission()[1].Component(sortedIndices[i]).Covariance() -
1681       gmms[1].Component(i).Covariance()) < 0.6);
1682   }
1683 }
1684 
1685 /**
1686  * Make sure the labeled training works reasonably given multiple distributions
1687  * with diagonal covariance.
1688  */
1689 TEST_CASE("DiagonalGMMHMMMultipleGaussiansLabeledTrainingTest", "[HMMTest]")
1690 {
1691   // Create a sequence of DiagonalGMMs.
1692   std::vector<DiagonalGMM> gmms(2, DiagonalGMM(2, 2));
1693   gmms[0].Weights() = arma::vec("0.3 0.7");
1694   gmms[0].Component(0) = DiagonalGaussianDistribution("2.25 5.30",
1695       "0.97 1.00");
1696   gmms[0].Component(1) = DiagonalGaussianDistribution("-3.15 -2.50",
1697       "1.20 0.89");
1698 
1699   gmms[1].Weights() = arma::vec("0.4 0.6");
1700   gmms[1].Component(0) = DiagonalGaussianDistribution("-4.48 -6.30",
1701         "1.02 0.80");
1702   gmms[1].Component(1) = DiagonalGaussianDistribution("5.24 2.40",
1703         "0.85 1.58");
1704 
1705   // Transition matrix.
1706   arma::mat transProbs("0.30 0.80;"
1707                        "0.70 0.20");
1708 
1709   // Make a sequence of observations.
1710   std::vector<arma::mat> observations(5, arma::mat(2, 2500));
1711   std::vector<arma::Row<size_t>> states(5, arma::Row<size_t>(2500));
1712   for (size_t obs = 0; obs < 5; obs++)
1713   {
1714     states[obs][0] = 0;
1715     observations[obs].col(0) = gmms[0].Random();
1716 
1717     for (size_t i = 1; i < 2500; ++i)
1718     {
1719       double randValue = math::Random();
1720 
1721       if (randValue <= transProbs(0, states[obs][i - 1]))
1722         states[obs][i] = 0;
1723       else
1724         states[obs][i] = 1;
1725 
1726       observations[obs].col(i) = gmms[states[obs][i]].Random();
1727     }
1728   }
1729 
1730   // Build the model.
1731   HMM<DiagonalGMM> hmm(2, DiagonalGMM(2, 2));
1732 
1733   // Train the model.
1734   hmm.Train(observations, states);
1735 
1736   // Check the initial weights.
1737   REQUIRE(hmm.Initial()[0] == Approx(1.0).epsilon(0.0001));
1738   REQUIRE(hmm.Initial()[1] == Approx(0.0).margin(0.01));
1739 
1740   // Check the transition probability matrix.
1741   for (size_t i = 0; i < 2; ++i)
1742     for (size_t j = 0; j < 2; ++j)
1743       REQUIRE(hmm.Transition()(i, j) - transProbs(i, j) ==
1744           Approx(0.0).margin(0.03));
1745 
1746   // Sort by the estimated weights of the first emission distribution.
1747   arma::uvec sortedIndices = sort_index(hmm.Emission()[0].Weights());
1748 
1749   // Check the first emission distribution.
1750   for (size_t i = 0; i < 2; ++i)
1751   {
1752     // Check the estimated weights using the first DiagonalGMM.
1753     REQUIRE(hmm.Emission()[0].Weights()[sortedIndices[i]] -
1754         gmms[0].Weights()[i] == Approx(0.0).margin(0.08));
1755 
1756     // Check the estimated means using the first DiagonalGMM.
1757     REQUIRE(arma::norm(
1758       hmm.Emission()[0].Component(sortedIndices[i]).Mean() -
1759       gmms[0].Component(i).Mean()) < 0.2);
1760 
1761     // Check the estimated covariances using the first DiagonalGMM.
1762     REQUIRE(arma::norm(
1763       hmm.Emission()[0].Component(sortedIndices[i]).Covariance() -
1764       gmms[0].Component(i).Covariance()) < 0.5);
1765   }
1766 
1767   // Sort by the estimated weights of the second emission distribution.
1768   sortedIndices = sort_index(hmm.Emission()[1].Weights());
1769 
1770   // Check the second emission distribution.
1771   for (size_t i = 0; i < 2; ++i)
1772   {
1773     // Check the estimated weights using the second DiagonalGMM.
1774     REQUIRE(hmm.Emission()[1].Weights()[sortedIndices[i]] -
1775         gmms[1].Weights()[i] == Approx(0.0).margin(0.08));
1776 
1777     // Check the estimated means using the second DiagonalGMM.
1778     REQUIRE(arma::norm(
1779       hmm.Emission()[1].Component(sortedIndices[i]).Mean() -
1780       gmms[1].Component(i).Mean()) < 0.2);
1781 
1782     // Check the estimated covariances using the second DiagonalGMM.
1783     REQUIRE(arma::norm(
1784       hmm.Emission()[1].Component(sortedIndices[i]).Covariance() -
1785       gmms[1].Component(i).Covariance()) < 0.5);
1786   }
1787 }
1788 
1789 /**
1790  * Make sure loading and saving the model is correct.
1791  */
1792 TEST_CASE("DiagonalGMMHMMLoadSaveTest", "[HMMTest]")
1793 {
1794   // Create a GMM HMM, save and load it.
1795   HMM<DiagonalGMM> hmm(3, DiagonalGMM(4, 3));
1796 
1797   // Generate intial random values.
1798   for (size_t j = 0; j < hmm.Emission().size(); ++j)
1799   {
1800     hmm.Emission()[j].Weights().randu();
1801     for (size_t i = 0; i < hmm.Emission()[j].Gaussians(); ++i)
1802     {
1803       hmm.Emission()[j].Component(i).Mean().randu();
1804       arma::vec covariance = arma::randu<arma::vec>(
1805           hmm.Emission()[j].Component(i).Covariance().n_elem);
1806 
1807       covariance += arma::ones<arma::vec>(covariance.n_elem);
1808       hmm.Emission()[j].Component(i).Covariance(std::move(covariance));
1809     }
1810   }
1811 
1812   // Save the HMM.
1813   {
1814     std::ofstream ofs("test-hmm-save.xml");
1815     boost::archive::xml_oarchive ar(ofs);
1816     ar << BOOST_SERIALIZATION_NVP(hmm);
1817   }
1818 
1819   // Load the HMM.
1820   HMM<DiagonalGMM> hmm2(3, DiagonalGMM(4, 3));
1821   {
1822     std::ifstream ifs("test-hmm-save.xml");
1823     boost::archive::xml_iarchive ar(ifs);
1824     ar >> BOOST_SERIALIZATION_NVP(hmm2);
1825   }
1826 
1827   // Remove clutter.
1828   remove("test-hmm-save.xml");
1829 
1830   for (size_t j = 0; j < hmm.Emission().size(); ++j)
1831   {
1832     // Check the number of Gaussians.
1833     REQUIRE(hmm.Emission()[j].Gaussians() == hmm2.Emission()[j].Gaussians());
1834 
1835     // Check the dimensionality.
1836     REQUIRE(hmm.Emission()[j].Dimensionality() ==
1837             hmm2.Emission()[j].Dimensionality());
1838 
1839     for (size_t i = 0; i < hmm.Emission()[j].Dimensionality(); ++i)
1840       // Check the weights.
1841       REQUIRE(hmm.Emission()[j].Weights()[i] ==
1842           Approx(hmm2.Emission()[j].Weights()[i]).epsilon(1e-5));
1843 
1844     for (size_t i = 0; i < hmm.Emission()[j].Gaussians(); ++i)
1845     {
1846       for (size_t l = 0; l < hmm.Emission()[j].Dimensionality(); l++)
1847       {
1848         // Check the means.
1849       REQUIRE(hmm.Emission()[j].Component(i).Mean()[l] ==
1850           Approx(hmm2.Emission()[j].Component(i).Mean()[l]).epsilon(1e-5));
1851 
1852         // Check the covariances.
1853       REQUIRE(hmm.Emission()[j].Component(i).Covariance()[l] ==
1854           Approx(hmm2.Emission()[j].Component(i).Covariance()[l]).epsilon(
1855           1e-5));
1856       }
1857     }
1858   }
1859 }
1860