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