diff --git a/src/mlpack/methods/cf/cf_main.cpp b/src/mlpack/methods/cf/cf_main.cpp index 726535fb96..49faf450fc 100644 --- a/src/mlpack/methods/cf/cf_main.cpp +++ b/src/mlpack/methods/cf/cf_main.cpp @@ -100,6 +100,15 @@ PROGRAM_INFO("Collaborative Filtering", " - 'average' -- Average Interpolation Algorithm\n" " - 'regression' -- Regression Interpolation Algorithm\n" " - 'similarity' -- Similarity Interpolation Algorithm\n" + "\n\n" + "The following ranking normalization algorithms can be specified via" + + " the " + PRINT_PARAM_STRING("normalization") + " parameter:" + "\n" + " - 'none' -- No Normalization\n" + " - 'item_mean' -- Item Mean Normalization\n" + " - 'overall_mean' -- Overall Mean Normalization\n" + " - 'user_mean' -- User Mean Normalization\n" + " - 'z_score' -- Z-Score Normalization\n" "\n" "A trained model may be saved to with the " + PRINT_PARAM_STRING("output_model") + " output parameter." @@ -136,6 +145,8 @@ PROGRAM_INFO("Collaborative Filtering", PARAM_MATRIX_IN("training", "Input dataset to perform CF on.", "t"); PARAM_STRING_IN("algorithm", "Algorithm used for matrix factorization.", "a", "NMF"); +PARAM_STRING_IN("normalization", "Normalization performed on the ratings.", "z", + "none"); PARAM_INT_IN("neighborhood", "Size of the neighborhood of similar users to " "consider for each query user.", "n", 5); PARAM_INT_IN("rank", "Rank of decomposed matrices (if 0, a heuristic is used to" @@ -372,8 +383,12 @@ void PerformAction(arma::mat& dataset, { const size_t neighborhood = (size_t) CLI::GetParam("neighborhood"); CFModel* c = new CFModel(); + + const string normalizationType = CLI::GetParam("normalization"); + c->template Train(dataset, neighborhood, rank, - maxIterations, minResidue, CLI::HasParam("iteration_only_termination")); + maxIterations, minResidue, CLI::HasParam("iteration_only_termination"), + normalizationType); PerformAction(c); } diff --git a/src/mlpack/methods/cf/cf_model.hpp b/src/mlpack/methods/cf/cf_model.hpp index ef1ebac214..112e093345 100644 --- a/src/mlpack/methods/cf/cf_model.hpp +++ b/src/mlpack/methods/cf/cf_model.hpp @@ -1,6 +1,7 @@ /** * @file cf_model.hpp * @author Wenhao Huang + * @author Khizir Siddiqui * * A serializable CF model, used by the main program. * @@ -24,6 +25,12 @@ #include #include +#include +#include +#include +#include +#include + namespace mlpack { namespace cf { @@ -35,8 +42,9 @@ class DeleteVisitor : public boost::static_visitor { public: //! Delete CFType object. - template - void operator()(CFType* c) const; + template + void operator()(CFType* c) const; }; /** @@ -46,8 +54,9 @@ class GetValueVisitor : public boost::static_visitor { public: //! Return stored pointer as void* type. - template - void* operator()(CFType* c) const; + template + void* operator()(CFType* c) const; }; /** @@ -66,8 +75,9 @@ class PredictVisitor : public boost::static_visitor public: //! Predict ratings for each user-item combination. - template - void operator()(CFType* c) const; + template + void operator()(CFType* c) const; //! Visitor constructor. PredictVisitor(const arma::Mat& combinations, @@ -100,8 +110,9 @@ class RecommendationVisitor : public boost::static_visitor const bool usersGiven); //! Generates the given number of recommendations. - template - void operator()(CFType* c) const; + template + void operator()(CFType* c) const; }; /** @@ -112,17 +123,54 @@ class CFModel private: /** * cf holds an instance of the CFType class for the current - * decompositionPolicy. It is initialized every time Train() is executed. - * We access to the contained value through the visitor classes defined above. + * decompositionPolicy and normalizationType. It is initialized every time + * Train() is executed. We access to the contained value through the visitor + * classes defined above. */ - boost::variant*, - CFType*, - CFType*, - CFType*, - CFType*, - CFType*, - CFType*, - CFType*> cf; + boost::variant*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*, + CFType*> cf; public: //! Create an empty CF model. @@ -132,8 +180,9 @@ class CFModel ~CFModel(); //! Get the pointer to CFType<> object. - template - const CFType* CFPtr() const; + template + const CFType* CFPtr() const; //! Train the model. template +#include +#include +#include +#include +#include + using namespace mlpack::cf; -template -void DeleteVisitor::operator()(CFType* c) const +template +void DeleteVisitor:: +operator()(CFType* c) const { if (c) delete c; } -template -void* GetValueVisitor::operator()(CFType* c) const +template +void* GetValueVisitor:: +operator()(CFType* c) const { if (!c) throw std::runtime_error("no cf model initialized"); @@ -45,9 +55,10 @@ PredictVisitor::PredictVisitor( template -template +template void PredictVisitor - ::operator()(CFType* c) const + ::operator()(CFType* c) const { if (!c) { @@ -75,9 +86,10 @@ RecommendationVisitor template -template +template void RecommendationVisitor - ::operator()(CFType* c) const + ::operator()(CFType* c) const { if (!c) { @@ -105,15 +117,50 @@ void CFModel::Train(const MatType& data, const size_t rank, const size_t maxIterations, const double minResidue, - const bool mit) + const bool mit, + const std::string& normalization) { // Delete the current CFType object, if there is one. boost::apply_visitor(DeleteVisitor(), cf); // Instantiate a new CFType object. DecompositionPolicy decomposition; - cf = new CFType(data, decomposition, - numUsersForSimilarity, rank, maxIterations, minResidue, mit); + if (normalization == "overall_mean") + { + cf = new CFType(data, + decomposition, numUsersForSimilarity, rank, maxIterations, minResidue, + mit); + } + else if (normalization == "item_mean") + { + cf = new CFType(data, + decomposition, numUsersForSimilarity, rank, maxIterations, minResidue, + mit); + } + else if (normalization == "user_mean") + { + cf = new CFType(data, + decomposition, numUsersForSimilarity, rank, maxIterations, minResidue, + mit); + } + else if (normalization == "z_score") + { + cf = new CFType(data, + decomposition, numUsersForSimilarity, rank, maxIterations, minResidue, + mit); + } + else if (normalization == "none") + { + cf = new CFType(data, + decomposition, numUsersForSimilarity, rank, maxIterations, minResidue, + mit); + } + else + { + throw std::runtime_error("Unsupported normalization algorithm." + " It should be one of none, overall_mean, " + "item_mean, user_mean or z_score"); + } } //! Make predictions. @@ -151,11 +198,12 @@ void CFModel::GetRecommendations(const size_t numRecs, boost::apply_visitor(recommendation, cf); } -template -const CFType* CFModel::CFPtr() const +template +const CFType* CFModel::CFPtr() const { void* pointer = boost::apply_visitor(GetValueVisitor(), cf); - return (CFType*) pointer; + return (CFType*) pointer; } template diff --git a/src/mlpack/tests/main_tests/cf_test.cpp b/src/mlpack/tests/main_tests/cf_test.cpp index 8a9837800d..936f39cbef 100644 --- a/src/mlpack/tests/main_tests/cf_test.cpp +++ b/src/mlpack/tests/main_tests/cf_test.cpp @@ -616,4 +616,102 @@ BOOST_AUTO_TEST_CASE(CFNeighborSearchTest) BOOST_REQUIRE(arma::any(arma::vectorise(output1 != output3))); } +/** + * Ensure normalization algorithm is one of { "none", "z_score", + * "item_mean", "user_mean" }. + */ +BOOST_AUTO_TEST_CASE(CFNormalizationBoundTest) +{ + mat dataset; + data::Load("GroupLensSmall.csv", dataset); + + const int querySize = 7; + Mat query = arma::linspace>(0, querySize - 1, querySize); + + SetInputParam("neighbor_search", std::string("cosine")); + SetInputParam("algorithm", std::string("NMF")); + + // Normalization algorithm should be valid. + SetInputParam("normalization", std::string("invalid_normalization")); + SetInputParam("training", std::move(dataset)); + SetInputParam("query", query); + + Log::Fatal.ignoreInput = true; + BOOST_REQUIRE_THROW(mlpackMain(), std::runtime_error); + Log::Fatal.ignoreInput = false; +} + +/** + * Ensure that using normalization techniques make difference. + */ +BOOST_AUTO_TEST_CASE(CFNormalizationTest) +{ + mat dataset; + data::Load("GroupLensSmall.csv", dataset); + + const int querySize = 7; + Mat query = arma::linspace>(0, querySize - 1, querySize); + + // Query with different normalization techniques. + ResetSettings(); + + SetInputParam("training", dataset); + SetInputParam("max_iterations", int(10)); + SetInputParam("query", query); + SetInputParam("algorithm", std::string("NMF")); + + // Using without Normalization. + SetInputParam("normalization", std::string("none")); + SetInputParam("recommendations", 5); + + mlpackMain(); + + const arma::Mat output1 = CLI::GetParam>("output"); + + BOOST_REQUIRE_EQUAL(output1.n_rows, 5); + BOOST_REQUIRE_EQUAL(output1.n_cols, 7); + + // Query with different normalization techniques. + ResetSettings(); + + SetInputParam("training", dataset); + SetInputParam("max_iterations", int(10)); + SetInputParam("query", query); + SetInputParam("algorithm", std::string("NMF")); + + // Using Item Mean normalization. + SetInputParam("normalization", std::string("item_mean")); + SetInputParam("recommendations", 5); + + mlpackMain(); + + const arma::Mat output2 = CLI::GetParam>("output"); + + BOOST_REQUIRE_EQUAL(output2.n_rows, 5); + BOOST_REQUIRE_EQUAL(output2.n_cols, 7); + + // Query with different normalization techniques. + ResetSettings(); + + SetInputParam("training", dataset); + SetInputParam("max_iterations", int(10)); + SetInputParam("query", query); + SetInputParam("algorithm", std::string("NMF")); + + // Using Z-Score normalization. + SetInputParam("normalization", std::string("z_score")); + SetInputParam("recommendations", 5); + + mlpackMain(); + + const arma::Mat output3 = CLI::GetParam>("output"); + + BOOST_REQUIRE_EQUAL(output3.n_rows, 5); + BOOST_REQUIRE_EQUAL(output3.n_cols, 7); + + // The resulting matrices should be different. + BOOST_REQUIRE(arma::any(arma::vectorise(output1 != output2))); + BOOST_REQUIRE(arma::any(arma::vectorise(output1 != output3))); +} + BOOST_AUTO_TEST_SUITE_END();