diff --git a/src/mlpack/methods/hmm/hmm_generate_main.cpp b/src/mlpack/methods/hmm/hmm_generate_main.cpp index c9488d3466..0efc8dc840 100644 --- a/src/mlpack/methods/hmm/hmm_generate_main.cpp +++ b/src/mlpack/methods/hmm/hmm_generate_main.cpp @@ -68,6 +68,11 @@ struct Generate mat observations; Row sequence; + RequireParamValue("start_state", [](int x) { return x >= 0; }, true, + "Invalid start state"); + RequireParamValue("length", [](int x) { return x >= 0; }, true, + "Length must be >= 0"); + // Load the parameters. const size_t startState = (size_t) CLI::GetParam("start_state"); const size_t length = (size_t) CLI::GetParam("length"); @@ -104,7 +109,7 @@ static void mlpackMain() RandomSeed((size_t) time(NULL)); // Load model, and perform the generation. - HMMModel hmm; - hmm = std::move(CLI::GetParam("model")); - hmm.PerformAction(NULL); // No extra data required. + HMMModel* hmm; + hmm = std::move(CLI::GetParam("model")); + hmm->PerformAction(NULL); // No extra data required. } diff --git a/src/mlpack/tests/CMakeLists.txt b/src/mlpack/tests/CMakeLists.txt index 910d61c7ca..10b82a5c57 100644 --- a/src/mlpack/tests/CMakeLists.txt +++ b/src/mlpack/tests/CMakeLists.txt @@ -149,7 +149,11 @@ add_executable(mlpack_test main_tests/sparse_coding_test.cpp main_tests/kmeans_test.cpp main_tests/hoeffding_tree_test.cpp + main_tests/hmm_viterbi_test.cpp main_tests/hmm_train_test.cpp + main_tests/hmm_loglik_test.cpp + main_tests/hmm_generate_test.cpp + main_tests/hmm_test_utils.hpp ) # Link dependencies of test executable. diff --git a/src/mlpack/tests/main_tests/hmm_generate_test.cpp b/src/mlpack/tests/main_tests/hmm_generate_test.cpp new file mode 100644 index 0000000000..73a7e6cddd --- /dev/null +++ b/src/mlpack/tests/main_tests/hmm_generate_test.cpp @@ -0,0 +1,249 @@ +/** + * @file hmm_generate_test.cpp + * @author Daivik Nema + * + * Test mlpackMain() of hmm_generate_main.cpp + */ +#include + +#define BINDING_TYPE BINDING_TYPE_TEST +static const std::string testName = "HMMGenerate"; + +#include +#include +#include "test_helper.hpp" +#include +#include +#include + +#include +#include "../test_tools.hpp" + +#include "hmm_test_utils.hpp" + +using namespace mlpack; + +struct HMMGenerateTestFixture +{ + public: + HMMGenerateTestFixture() + { + // Cache in the options for this program. + CLI::RestoreSettings(testName); + } + + ~HMMGenerateTestFixture() + { + // Clear the settings. + bindings::tests::CleanMemory(); + CLI::ClearSettings(); + } +}; + +BOOST_FIXTURE_TEST_SUITE(HMMGenerateMainTest, HMMGenerateTestFixture); + +BOOST_AUTO_TEST_CASE(HMMGenerateDiscreteHMMCheckDimensionsTest) +{ + // Load data to train a discrete HMM model with. + arma::mat inp; + data::Load("obs1.csv", inp); + std::vector trainSeq = {inp}; + + // Initialize and train a discrete HMM model. + HMMModel* h = new HMMModel(DiscreteHMM); + h->PerformAction>(&trainSeq); + h->PerformAction>(&trainSeq); + + // Now that we have a trained HMM model, we can use it to generate a sequence + // of states and observations - using the hmm_generate utility. + // Load the input model to be used for inference and the length of sequence + // to be generated. + int length = 3; + SetInputParam("model", h); + SetInputParam("length", length); + + // Call to hmm_generate_main. + mlpackMain(); + + // Get the generated observation sequence. Ensure that the generated sequence + // has the correct length (as provided in the input). + arma::mat obsSeq = CLI::GetParam("output"); + BOOST_REQUIRE_EQUAL(obsSeq.n_cols, (size_t)length); + BOOST_REQUIRE_EQUAL(obsSeq.n_rows, (size_t)1); + BOOST_REQUIRE_EQUAL(obsSeq.n_elem, (size_t)length); + + // Get the generated state sequence. Ensure that the generated sequence + // has the correct length (as provided in the input). + arma::Mat stateSeq = CLI::GetParam>("state"); + BOOST_REQUIRE_EQUAL(stateSeq.n_cols, (size_t)length); + BOOST_REQUIRE_EQUAL(stateSeq.n_rows, (size_t)1); + BOOST_REQUIRE_EQUAL(stateSeq.n_elem, (size_t)length); +} + +BOOST_AUTO_TEST_CASE(HMMGenerateGaussianHMMCheckDimensionsTest) +{ + // Load data to train a gaussian HMM model with. + arma::mat inp; + data::Load("obs1.csv", inp); + std::vector trainSeq = {inp}; + + // Initialize and train a gaussian HMM model. + HMMModel* h = new HMMModel(GaussianHMM); + h->PerformAction>(&trainSeq); + h->PerformAction>(&trainSeq); + + // Now that we have a trained HMM model, we can use it to generate a sequence + // of states and observations - using the hmm_generate utility. + // Load the input model to be used for inference and the length of sequence + // to be generated. + int length = 3; + SetInputParam("model", h); + SetInputParam("length", length); + + // Call to hmm_generate_main. + mlpackMain(); + + // Get the generated observation sequence. Ensure that the generated sequence + // has the correct length (as provided in the input). + arma::mat obsSeq = CLI::GetParam("output"); + BOOST_REQUIRE_EQUAL(obsSeq.n_cols, (size_t)length); + BOOST_REQUIRE_EQUAL(obsSeq.n_rows, (size_t)1); + BOOST_REQUIRE_EQUAL(obsSeq.n_elem, (size_t)length); + + // Get the generated state sequence. Ensure that the generated sequence + // has the correct length (as provided in the input). + arma::Mat stateSeq = CLI::GetParam>("state"); + BOOST_REQUIRE_EQUAL(stateSeq.n_cols, (size_t)length); + BOOST_REQUIRE_EQUAL(stateSeq.n_rows, (size_t)1); + BOOST_REQUIRE_EQUAL(stateSeq.n_elem, (size_t)length); +} + +BOOST_AUTO_TEST_CASE(HMMGenerateGMMHMMCheckDimensionsTest) +{ + // Load data to train a Gaussian Mixture Model HMM model with. + std::vector gmms(2, GMM(2, 2)); + gmms[0].Weights() = arma::vec("0.3 0.7"); + + // N([2.25 3.10], [1.00 0.20; 0.20 0.89]) + gmms[0].Component(0) = GaussianDistribution("4.25 3.10", + "1.00 0.20; 0.20 0.89"); + + // N([4.10 1.01], [1.00 0.00; 0.00 1.01]) + gmms[0].Component(1) = GaussianDistribution("7.10 5.01", + "1.00 0.00; 0.00 1.01"); + + gmms[1].Weights() = arma::vec("0.20 0.80"); + + gmms[1].Component(0) = GaussianDistribution("-3.00 -6.12", + "1.00 0.00; 0.00 1.00"); + + gmms[1].Component(1) = GaussianDistribution("-4.25 -2.12", + "1.50 0.60; 0.60 1.20"); + + // Transition matrix. + arma::mat transMat("0.40 0.60;" + "0.60 0.40"); + + // Make a sequence of observations. + std::vector observations(5, arma::mat(2, 50)); + std::vector > states(5, arma::Row(50)); + for (size_t obs = 0; obs < 5; obs++) + { + states[obs][0] = 0; + observations[obs].col(0) = gmms[0].Random(); + + for (size_t i = 1; i < 50; i++) + { + double randValue = (double) rand() / (double) RAND_MAX; + + if (randValue <= transMat(0, states[obs][i - 1])) + states[obs][i] = 0; + else + states[obs][i] = 1; + + observations[obs].col(i) = gmms[states[obs][i]].Random(); + } + } + + // Initialize and train a GMM HMM model. + HMMModel* h = new HMMModel(GaussianMixtureModelHMM); + h->PerformAction>(&observations); + h->PerformAction>(&observations); + + // Now that we have a trained HMM model, we can use it to generate a sequence + // of states and observations - using the hmm_generate utility. + // Load the input model to be used for inference and the length of sequence + // to be generated. + int length = 3; + SetInputParam("model", h); + SetInputParam("length", length); + + // Call to hmm_generate_main + mlpackMain(); + + // Get the generated observation sequence. Ensure that the generated sequence + // has the correct length (as provided in the input). + arma::mat obsSeq = CLI::GetParam("output"); + BOOST_REQUIRE_EQUAL(obsSeq.n_cols, (size_t)length); + BOOST_REQUIRE_EQUAL(obsSeq.n_rows, (size_t)2); + BOOST_REQUIRE_EQUAL(obsSeq.n_elem, (size_t)(length*2)); + + // Get the generated state sequence. Ensure that the generated sequence + // has the correct length (as provided in the input). + arma::Mat stateSeq = CLI::GetParam>("state"); + BOOST_REQUIRE_EQUAL(stateSeq.n_cols, (size_t)length); + BOOST_REQUIRE_EQUAL(stateSeq.n_rows, (size_t)1); + BOOST_REQUIRE_EQUAL(stateSeq.n_elem, (size_t)length); +} + +BOOST_AUTO_TEST_CASE(HMMGenerateLengthPositiveTest) +{ + // Load data to train a Gaussian Mixture Model HMM model with. + arma::mat inp; + data::Load("obs1.csv", inp); + std::vector trainSeq = {inp}; + + // Initialize and train a HMM model. + HMMModel* h = new HMMModel(DiscreteHMM); + h->PerformAction>(&trainSeq); + h->PerformAction>(&trainSeq); + + // Set the params for the hmm_generate invocation + // Note that the length is negative - we expect that a runtime error will be + // raised in the call to hmm_generate_main + int length = -3; // Invalid + SetInputParam("model", h); + SetInputParam("length", length); + + Log::Fatal.ignoreInput = true; + BOOST_REQUIRE_THROW(mlpackMain(), std::runtime_error); + Log::Fatal.ignoreInput = false; +} + +BOOST_AUTO_TEST_CASE(HMMGenerateValidStartStateTest) +{ + // Load data to train a Gaussian Mixture Model HMM model with. + arma::mat inp; + data::Load("obs1.csv", inp); + std::vector trainSeq = {inp}; + + // Initialize and train a HMM model. + HMMModel* h = new HMMModel(DiscreteHMM); + h->PerformAction>(&trainSeq); + h->PerformAction>(&trainSeq); + + // Set the params for the hmm_generate invocation + // Note that the start state is invalid - we expect that a runtime error will + // be raised in the call to hmm_generate_main + int length = 3; + int startState = 2; // Invalid + SetInputParam("model", h); + SetInputParam("length", length); + SetInputParam("start_state", startState); + + Log::Fatal.ignoreInput = true; + BOOST_REQUIRE_THROW(mlpackMain(), std::runtime_error); + Log::Fatal.ignoreInput = false; +} + +BOOST_AUTO_TEST_SUITE_END(); diff --git a/src/mlpack/tests/main_tests/hmm_loglik_test.cpp b/src/mlpack/tests/main_tests/hmm_loglik_test.cpp new file mode 100644 index 0000000000..133e3c7b93 --- /dev/null +++ b/src/mlpack/tests/main_tests/hmm_loglik_test.cpp @@ -0,0 +1,70 @@ +/** + * @file hmm_loglik_test.cpp + * @author Daivik Nema + * + * Test mlpackMain() of hmm_loglik_main.cpp + */ +#include + +#define BINDING_TYPE BINDING_TYPE_TEST +static const std::string testName = "HMMLoglik"; + +#include +#include +#include "test_helper.hpp" +#include +#include +#include + +#include +#include "../test_tools.hpp" + +#include "hmm_test_utils.hpp" + +using namespace mlpack; + +struct HMMLoglikTestFixture +{ + public: + HMMLoglikTestFixture() + { + // Cache in the options for this program. + CLI::RestoreSettings(testName); + } + + ~HMMLoglikTestFixture() + { + // Clear the settings. + bindings::tests::CleanMemory(); + CLI::ClearSettings(); + } +}; + +BOOST_FIXTURE_TEST_SUITE(HMMLoglikMainTest, HMMLoglikTestFixture); + +BOOST_AUTO_TEST_CASE(HMMLoglikOutputNegativeTest) +{ + // Load data to train a discrete HMM model with. + arma::mat inp; + data::Load("obs1.csv", inp); + std::vector trainSeq = {inp}; + + // Initialize and train an HMM model. + HMMModel* h = new HMMModel(DiscreteHMM); + h->PerformAction>(&trainSeq); + h->PerformAction>(&trainSeq); + + + // Set the params for the hmm_loglik invocation + SetInputParam("input_model", h); + SetInputParam("input", inp); + + mlpackMain(); + + double loglik = CLI::GetParam("log_likelihood"); + + // Since the log of a probability <= 0 ... + BOOST_REQUIRE(loglik <= 0); +} + +BOOST_AUTO_TEST_SUITE_END(); diff --git a/src/mlpack/tests/main_tests/hmm_test_utils.hpp b/src/mlpack/tests/main_tests/hmm_test_utils.hpp new file mode 100644 index 0000000000..cf9d06fcc5 --- /dev/null +++ b/src/mlpack/tests/main_tests/hmm_test_utils.hpp @@ -0,0 +1,163 @@ +/** + * @file hmm_test_utils.hpp + * @author Daivik Nema + * + * Structs for initializing and training HMMs (either of Discrete, Gaussian or + * GMM HMMs). These structs are passed as template parameters to the + * PerformAction function of an HMMModel object. These structs have been adapted + * from the structs in mlpack/methods/hmm/hmm_train_main.cpp. + * + * mlpack is free software; you may redistribute it and/or modify it under the + * terms of the 3-clause BSD license. You should have received a copy of the + * 3-clause BSD license along with mlpack. If not, see + * http://www.opensource.org/licenses/BSD-3-Clause for more information. + */ +#ifndef MLPACK_TESTS_MAIN_TESTS_HMM_TEST_UTILS_HPP +#define MLPACK_TESTS_MAIN_TESTS_HMM_TEST_UTILS_HPP + +#include +#include + +struct InitHMMModel +{ + template + static void Apply(HMMType& hmm, vector* trainSeq) + { + const size_t states = 2; + + // Create the initialized-to-zero model. + Create(hmm, *trainSeq, states); + + // Initializing the emission distribution depends on the distribution. + // Therefore we have to use the helper functions. + RandomInitialize(hmm.Emission()); + } + + //! Helper function to create discrete HMM. + static void Create(HMM& hmm, + vector& trainSeq, + size_t states, + double tolerance = 1e-05) + { + // Maximum observation is necessary so we know how to train the discrete + // distribution. + arma::Col maxEmissions(trainSeq[0].n_rows); + maxEmissions.zeros(); + for (vector::iterator it = trainSeq.begin(); it != trainSeq.end(); + ++it) + { + arma::Col maxSeqs = + arma::conv_to>::from(arma::max(*it, 1)) + 1; + maxEmissions = arma::max(maxEmissions, maxSeqs); + } + + hmm = HMM(size_t(states), + DiscreteDistribution(maxEmissions), tolerance); + } + + static void Create(HMM& hmm, + vector& trainSeq, + size_t states, + double tolerance = 1e-05) + { + // Find dimension of the data. + const size_t dimensionality = trainSeq[0].n_rows; + + // Verify dimensionality of data. + for (size_t i = 0; i < trainSeq.size(); ++i) + { + if (trainSeq[i].n_rows != dimensionality) + { + Log::Fatal << "Observation sequence " << i << " dimensionality (" + << trainSeq[i].n_rows << " is incorrect (should be " + << dimensionality << ")!" << endl; + } + } + + // Get the model and initialize it. + hmm = HMM(size_t(states), + GaussianDistribution(dimensionality), tolerance); + } + + static void Create(HMM& hmm, + vector& trainSeq, + size_t states, + double tolerance = 1e-05) + { + // Find dimension of the data. + const size_t dimensionality = trainSeq[0].n_rows; + const int gaussians = 2; + + if (gaussians == 0) + { + Log::Fatal << "Number of gaussians for each GMM must be specified " + << "when type = 'gmm'!" << endl; + } + + if (gaussians < 0) + { + Log::Fatal << "Invalid number of gaussians (" << gaussians << "); must " + << "be greater than or equal to 1." << endl; + } + + // Create HMM object. + hmm = HMM(size_t(states), GMM(size_t(gaussians), dimensionality), + tolerance); + } + + //! Helper function for discrete emission distributions. + static void RandomInitialize(vector& e) + { + for (size_t i = 0; i < e.size(); ++i) + { + e[i].Probabilities().randu(); + e[i].Probabilities() /= arma::accu(e[i].Probabilities()); + } + } + + static void RandomInitialize(vector& e) + { + for (size_t i = 0; i < e.size(); ++i) + { + const size_t dimensionality = e[i].Mean().n_rows; + e[i].Mean().randu(); + // Generate random covariance. + arma::mat r = arma::randu(dimensionality, dimensionality); + e[i].Covariance(r * r.t()); + } + } + + static void RandomInitialize(vector& e) + { + for (size_t i = 0; i < e.size(); ++i) + { + // Random weights. + e[i].Weights().randu(); + e[i].Weights() /= arma::accu(e[i].Weights()); + + // Random means and covariances. + for (int g = 0; g < 2; ++g) + { + const size_t dimensionality = e[i].Component(g).Mean().n_rows; + e[i].Component(g).Mean().randu(); + + // Generate random covariance. + arma::mat r = arma::randu(dimensionality, + dimensionality); + e[i].Component(g).Covariance(r * r.t()); + } + } + } +}; + +struct TrainHMMModel +{ + template + static void Apply(HMMType& hmm, vector* trainSeq) + { + // For now, perform unsupervised (Baum-Welch) training. + hmm.Train(*trainSeq); + } +}; + +#endif diff --git a/src/mlpack/tests/main_tests/hmm_viterbi_test.cpp b/src/mlpack/tests/main_tests/hmm_viterbi_test.cpp new file mode 100644 index 0000000000..7b8dec4905 --- /dev/null +++ b/src/mlpack/tests/main_tests/hmm_viterbi_test.cpp @@ -0,0 +1,178 @@ +/** + * @file hmm_viterbi_test.cpp + * @author Daivik Nema + * + * Test mlpackMain() of hmm_viterbi_main.cpp + */ +#include + +#define BINDING_TYPE BINDING_TYPE_TEST +static const std::string testName = "HMMViterbi"; + +#include +#include +#include "test_helper.hpp" +#include +#include +#include + +#include +#include "../test_tools.hpp" + +#include "hmm_test_utils.hpp" + +using namespace mlpack; + +struct HMMViterbiTestFixture +{ + public: + HMMViterbiTestFixture() + { + // Cache in the options for this program. + CLI::RestoreSettings(testName); + } + + ~HMMViterbiTestFixture() + { + // Clear the settings. + bindings::tests::CleanMemory(); + CLI::ClearSettings(); + } +}; + +BOOST_FIXTURE_TEST_SUITE(HMMViterbiMainTest, HMMViterbiTestFixture); + +BOOST_AUTO_TEST_CASE(HMMViterbiDiscreteHMMCheckDimensionsTest) +{ + // Load data to train a discrete HMM model with. + arma::mat inp; + data::Load("obs1.csv", inp); + std::vector trainSeq = {inp}; + + // Initialize and train a discrete HMM model. + HMMModel* h = new HMMModel(DiscreteHMM); + h->PerformAction>(&trainSeq); + h->PerformAction>(&trainSeq); + + // Now that we have a trained HMM model, we can use it to predict the state + // sequence for a given observation sequence - using the Viterbi algorithm. + // Load the input model to be used for inference and the sequence over which + // inference is to be performed. + SetInputParam("input_model", h); + SetInputParam("input", inp); + + // Call to hmm_viterbi_main. + mlpackMain(); + + // Get the output of viterbi inference. + arma::Mat out = CLI::GetParam >("output"); + + // Output sequence length must be the same as input sequence length and + // there should only be one row (since states are single dimensional values). + BOOST_REQUIRE_EQUAL(out.n_rows, 1); + BOOST_REQUIRE_EQUAL(out.n_cols, inp.n_cols); +} + +BOOST_AUTO_TEST_CASE(HMMViterbiGaussianHMMCheckDimensionsTest) +{ + // Load data to train a gaussian HMM model with. + arma::mat inp; + data::Load("obs1.csv", inp); + std::vector trainSeq = {inp}; + + // Initialize and train a gaussian HMM model. + HMMModel* h = new HMMModel(GaussianHMM); + h->PerformAction>(&trainSeq); + h->PerformAction>(&trainSeq); + + // Now that we have a trained HMM model, we can use it to predict the state + // sequence for a given observation sequence - using the Viterbi algorithm. + // Load the input model to be used for inference and the sequence over which + // inference is to be performed. + SetInputParam("input_model", h); + SetInputParam("input", inp); + + // Call to hmm_viterbi_main. + mlpackMain(); + + // Get the output of viterbi inference. + arma::Mat out = CLI::GetParam >("output"); + + // Output sequence length must be the same as input sequence length and + // there should only be one row (since states are single dimensional values). + BOOST_REQUIRE_EQUAL(out.n_rows, 1); + BOOST_REQUIRE_EQUAL(out.n_cols, inp.n_cols); +} + +BOOST_AUTO_TEST_CASE(HMMViterbiGMMHMMCheckDimensionsTest) +{ + // Load data to train a Gaussian Mixture Model HMM model with. + std::vector gmms(2, GMM(2, 2)); + gmms[0].Weights() = arma::vec("0.3 0.7"); + + // N([2.25 3.10], [1.00 0.20; 0.20 0.89]) + gmms[0].Component(0) = GaussianDistribution("4.25 3.10", + "1.00 0.20; 0.20 0.89"); + + // N([4.10 1.01], [1.00 0.00; 0.00 1.01]) + gmms[0].Component(1) = GaussianDistribution("7.10 5.01", + "1.00 0.00; 0.00 1.01"); + + gmms[1].Weights() = arma::vec("0.20 0.80"); + + gmms[1].Component(0) = GaussianDistribution("-3.00 -6.12", + "1.00 0.00; 0.00 1.00"); + + gmms[1].Component(1) = GaussianDistribution("-4.25 -2.12", + "1.50 0.60; 0.60 1.20"); + + // Transition matrix. + arma::mat transMat("0.40 0.60;" + "0.60 0.40"); + + // Make a sequence of observations. + std::vector observations(5, arma::mat(2, 50)); + std::vector > states(5, arma::Row(50)); + for (size_t obs = 0; obs < 5; obs++) + { + states[obs][0] = 0; + observations[obs].col(0) = gmms[0].Random(); + + for (size_t i = 1; i < 50; i++) + { + double randValue = (double) rand() / (double) RAND_MAX; + + if (randValue <= transMat(0, states[obs][i - 1])) + states[obs][i] = 0; + else + states[obs][i] = 1; + + observations[obs].col(i) = gmms[states[obs][i]].Random(); + } + } + + // Initialize and train a GMM HMM model. + HMMModel* h = new HMMModel(GaussianMixtureModelHMM); + h->PerformAction>(&observations); + h->PerformAction>(&observations); + + // Now that we have a trained HMM model, we can use it to predict the state + // sequence for a given observation sequence - using the Viterbi algorithm. + // Load the input model to be used for inference and the sequence over which + // inference is to be performed. + SetInputParam("input_model", h); + SetInputParam("input", observations[0]); + + // Call to hmm_viterbi_main. + mlpackMain(); + + // Get the output of viterbi inference. + arma::Mat out = CLI::GetParam >("output"); + + // Output sequence length must be the same as input sequence length and + // there should only be one row (since states are single dimensional values). + BOOST_REQUIRE_EQUAL(out.n_rows, 1); + BOOST_REQUIRE_EQUAL(out.n_cols, observations[0].n_cols); +} + +BOOST_AUTO_TEST_SUITE_END();