diff --git a/HISTORY.md b/HISTORY.md index 1c53407e4b..fcb7568255 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,8 @@ * Add adaptive max pooling and adaptive mean pooling layers (#2195). + * Add parameter to avoid shuffling of data in preprocess_split (#2293). + * Add `MatType` parameter to `LSHSearch`, allowing sparse matrices to be used for search (#2395). @@ -105,7 +107,6 @@ * Add Soft Shrink Activation Function (#2174). * Add Hinge Embedding Loss Function (#2229). - * Add Cosine Embedding Loss Function (#2209). * Add Margin Ranking Loss Function (#2264). diff --git a/src/mlpack/core/data/split_data.hpp b/src/mlpack/core/data/split_data.hpp index 28b0bcf966..23c88c1c2b 100644 --- a/src/mlpack/core/data/split_data.hpp +++ b/src/mlpack/core/data/split_data.hpp @@ -44,6 +44,8 @@ namespace data { * @param trainLabel Vector to store training labels into. * @param testLabel Vector to store test labels into. * @param testRatio Percentage of dataset to use for test set (between 0 and 1). + * @param shuffleData If true, the sample order is shuffled; otherwise, each + * sample is visited in linear order. (Default true.) */ template void Split(const arma::Mat& input, @@ -52,7 +54,8 @@ void Split(const arma::Mat& input, arma::Mat& testData, arma::Row& trainLabel, arma::Row& testLabel, - const double testRatio) + const double testRatio, + const bool shuffleData = true) { const size_t testSize = static_cast(input.n_cols * testRatio); const size_t trainSize = input.n_cols - testSize; @@ -61,20 +64,33 @@ void Split(const arma::Mat& input, trainLabel.set_size(trainSize); testLabel.set_size(testSize); - const arma::Col order = - arma::shuffle(arma::linspace>(0, input.n_cols - 1, - input.n_cols)); - - for (size_t i = 0; i != trainSize; ++i) + if (shuffleData) { - trainData.col(i) = input.col(order[i]); - trainLabel(i) = inputLabel(order[i]); + arma::uvec order = arma::shuffle(arma::linspace( + 0, input.n_cols - 1, input.n_cols)); + if (trainSize > 0) + { + trainData = input.cols(order.subvec(0, trainSize - 1)); + trainLabel = inputLabel.cols(order.subvec(0, trainSize - 1)); + } + if (trainSize < input.n_cols) + { + testData = input.cols(order.subvec(trainSize, input.n_cols - 1)); + testLabel = inputLabel.cols(order.subvec(trainSize, input.n_cols - 1)); + } } - - for (size_t i = 0; i != testSize; ++i) + else { - testData.col(i) = input.col(order[i + trainSize]); - testLabel(i) = inputLabel(order[i + trainSize]); + if (trainSize > 0) + { + trainData = input.cols(0, trainSize - 1); + trainLabel = inputLabel.subvec(0, trainSize - 1); + } + if (trainSize < input.n_cols) + { + testData = input.cols(trainSize , input.n_cols - 1); + testLabel = inputLabel.subvec(trainSize , input.n_cols - 1); + } } } @@ -98,29 +114,39 @@ void Split(const arma::Mat& input, * @param trainData Matrix to store training data into. * @param testData Matrix to store test data into. * @param testRatio Percentage of dataset to use for test set (between 0 and 1). + * @param shuffleData If true, the sample order is shuffled; otherwise, each + * sample is visited in linear order. (Default true). */ template void Split(const arma::Mat& input, arma::Mat& trainData, arma::Mat& testData, - const double testRatio) + const double testRatio, + const bool shuffleData = true) { const size_t testSize = static_cast(input.n_cols * testRatio); const size_t trainSize = input.n_cols - testSize; trainData.set_size(input.n_rows, trainSize); testData.set_size(input.n_rows, testSize); - const arma::Col order = - arma::shuffle(arma::linspace>(0, input.n_cols -1, - input.n_cols)); + if (shuffleData) + { + arma::uvec order = arma::shuffle(arma::linspace( + 0, input.n_cols - 1, input.n_cols)); - for (size_t i = 0; i != trainSize; ++i) - { - trainData.col(i) = input.col(order[i]); + if (trainSize > 0) + trainData = input.cols(order.subvec(0, trainSize - 1)); + + if (trainSize < input.n_cols) + testData = input.cols(order.subvec(trainSize, input.n_cols - 1)); } - for (size_t i = 0; i != testSize; ++i) + else { - testData.col(i) = input.col(order[i + trainSize]); + if (trainSize > 0) + trainData = input.cols(0, trainSize - 1); + + if (trainSize < input.n_cols) + testData = input.cols(trainSize , input.n_cols - 1); } } @@ -140,6 +166,8 @@ void Split(const arma::Mat& input, * @param input Input dataset to split. * @param label Input labels to split. * @param testRatio Percentage of dataset to use for test set (between 0 and 1). + * @param shuffleData If true, the sample order is shuffled; otherwise, each + * sample is visited in linear order. (Default true). * @return std::tuple containing trainData (arma::Mat), testData * (arma::Mat), trainLabel (arma::Row), and testLabel (arma::Row). */ @@ -147,7 +175,8 @@ template std::tuple, arma::Mat, arma::Row, arma::Row> Split(const arma::Mat& input, const arma::Row& inputLabel, - const double testRatio) + const double testRatio, + const bool shuffleData = true) { arma::Mat trainData; arma::Mat testData; @@ -155,7 +184,7 @@ Split(const arma::Mat& input, arma::Row testLabel; Split(input, inputLabel, trainData, testData, trainLabel, testLabel, - testRatio); + testRatio, shuffleData); return std::make_tuple(std::move(trainData), std::move(testData), @@ -176,17 +205,20 @@ Split(const arma::Mat& input, * * @param input Input dataset to split. * @param testRatio Percentage of dataset to use for test set (between 0 and 1). + * @param shuffleData If true, the sample order is shuffled; otherwise, each + * sample is visited in linear order. (Default true). * @return std::tuple containing trainData (arma::Mat) * and testData (arma::Mat). */ template std::tuple, arma::Mat> Split(const arma::Mat& input, - const double testRatio) + const double testRatio, + const bool shuffleData = true) { arma::Mat trainData; arma::Mat testData; - Split(input, trainData, testData, testRatio); + Split(input, trainData, testData, testRatio, shuffleData); return std::make_tuple(std::move(trainData), std::move(testData)); diff --git a/src/mlpack/methods/preprocess/preprocess_split_main.cpp b/src/mlpack/methods/preprocess/preprocess_split_main.cpp index 3de7745d31..ccedb11b43 100644 --- a/src/mlpack/methods/preprocess/preprocess_split_main.cpp +++ b/src/mlpack/methods/preprocess/preprocess_split_main.cpp @@ -45,6 +45,13 @@ PROGRAM_INFO("Split Data", PRINT_CALL("preprocess_split", "input", "X", "training", "X_train", "test", "X_test", "test_ratio", 0.4) + "\n\n" + "Also by default the dataset is shuffled and split; you can provide the " + + PRINT_PARAM_STRING("no_shuffle") + " option to avoid shuffling the " + "data; an example to avoid shuffling of data is:" + "\n\n" + + PRINT_CALL("preprocess_split", "input", "X", "training", "X_train", "test", + "X_test", "test_ratio", 0.4, "no_shuffle", true) + + "\n\n" "If we had a dataset " + PRINT_DATASET("X") + " and associated labels " + PRINT_DATASET("y") + ", and we wanted to split these into " + PRINT_DATASET("X_train") + ", " + PRINT_DATASET("y_train") + ", " + @@ -73,6 +80,7 @@ PARAM_DOUBLE_IN("test_ratio", "Ratio of test set; if not set," "the ratio defaults to 0.2", "r", 0.2); PARAM_INT_IN("seed", "Random seed (0 for std::time(NULL)).", "s", 0); +PARAM_FLAG("no_shuffle", "Avoid shuffling and splitting the data.", "S"); using namespace mlpack; using namespace mlpack::util; @@ -83,6 +91,7 @@ static void mlpackMain() { // Parse command line options. const double testRatio = CLI::GetParam("test_ratio"); + const bool shuffleData = CLI::GetParam("no_shuffle"); if (CLI::GetParam("seed") == 0) mlpack::math::RandomSeed(std::time(NULL)); @@ -129,7 +138,7 @@ static void mlpackMain() CLI::GetParam>("input_labels"); arma::Row labelsRow = labels.row(0); - const auto value = data::Split(data, labelsRow, testRatio); + const auto value = data::Split(data, labelsRow, testRatio, !shuffleData); Log::Info << "Training data contains " << get<0>(value).n_cols << " points." << endl; Log::Info << "Test data contains " << get<1>(value).n_cols << " points." @@ -148,7 +157,7 @@ static void mlpackMain() } else // We have no labels, so just split the dataset. { - const auto value = data::Split(data, testRatio); + const auto value = data::Split(data, testRatio, !shuffleData); Log::Info << "Training data contains " << get<0>(value).n_cols << " points." << endl; Log::Info << "Test data contains " << get<1>(value).n_cols << " points." diff --git a/src/mlpack/tests/main_tests/preprocess_split_test.cpp b/src/mlpack/tests/main_tests/preprocess_split_test.cpp index 509a74a3d3..34d1515f0a 100644 --- a/src/mlpack/tests/main_tests/preprocess_split_test.cpp +++ b/src/mlpack/tests/main_tests/preprocess_split_test.cpp @@ -199,4 +199,35 @@ BOOST_AUTO_TEST_CASE(PreprocessSplitUnityTestRatioTest) labelSize); } +/** + * Check shuffle_data flag is working as expected. + */ +BOOST_AUTO_TEST_CASE(PreprocessSplitLabelShuffleDataTest) +{ + // Load custom dataset. + arma::mat inputData; + data::Load("vc2.csv", inputData); + + // Store size of input dataset. + int inputSize = inputData.n_cols; + + // Input custom data points and labels. + SetInputParam("input", inputData); + + // Input test_ratio. + SetInputParam("test_ratio", (double) 0.1); + SetInputParam("no_shuffle", true); + mlpackMain(); + + // Now check that the output has desired dimensions. + BOOST_REQUIRE_EQUAL(CLI::GetParam("training").n_cols, + std::ceil(0.9 * inputSize)); + BOOST_REQUIRE_EQUAL(CLI::GetParam("test").n_cols, + std::floor(0.1 * inputSize)); + + arma::mat concat = arma::join_rows(CLI::GetParam("training"), + CLI::GetParam("test")); + CheckMatrices(inputData, concat); +} + BOOST_AUTO_TEST_SUITE_END(); diff --git a/src/mlpack/tests/split_data_test.cpp b/src/mlpack/tests/split_data_test.cpp index 2846307b29..80464e2676 100644 --- a/src/mlpack/tests/split_data_test.cpp +++ b/src/mlpack/tests/split_data_test.cpp @@ -94,20 +94,65 @@ void CheckDuplication(const Row& trainLabels, BOOST_REQUIRE_EQUAL(counts[i], 1); } -BOOST_AUTO_TEST_CASE(SplitDataResultMat) +BOOST_AUTO_TEST_CASE(SplitShuffleDataResultMat) { mat input(2, 10); - size_t count = 0; // count for putting unique sequential values + size_t count = 0; // Counter for unique sequential values. input.imbue([&count] () { return ++count; }); const auto value = Split(input, 0.2); - BOOST_REQUIRE_EQUAL(std::get<0>(value).n_cols, 8); // train data - BOOST_REQUIRE_EQUAL(std::get<1>(value).n_cols, 2); // test data + BOOST_REQUIRE_EQUAL(std::get<0>(value).n_cols, 8); // Train data. + BOOST_REQUIRE_EQUAL(std::get<1>(value).n_cols, 2); // Test data. mat concat = arma::join_rows(std::get<0>(value), std::get<1>(value)); CheckMatEqual(input, concat); } +BOOST_AUTO_TEST_CASE(SplitDataResultMat) +{ + mat input(2, 10); + size_t count = 0; // Counter for unique sequential values. + input.imbue([&count] () { return ++count; }); + + const auto value = Split(input, 0.2, false); + BOOST_REQUIRE_EQUAL(std::get<0>(value).n_cols, 8); // Train data. + BOOST_REQUIRE_EQUAL(std::get<1>(value).n_cols, 2); // Test data. + + mat concat = arma::join_rows(std::get<0>(value), std::get<1>(value)); + // Order matters here. + CheckMatrices(input, concat); +} + +BOOST_AUTO_TEST_CASE(ZeroRatioSplitData) +{ + mat input(2, 10); + size_t count = 0; // Counter for unique sequential values. + input.imbue([&count] () { return ++count; }); + + const auto value = Split(input, 0, false); + BOOST_REQUIRE_EQUAL(std::get<0>(value).n_cols, 10); // Train data. + BOOST_REQUIRE_EQUAL(std::get<1>(value).n_cols, 0); // Test data. + + mat concat = arma::join_rows(std::get<0>(value), std::get<1>(value)); + // Order matters here. + CheckMatrices(input, concat); +} + +BOOST_AUTO_TEST_CASE(TotalRatioSplitData) +{ + mat input(2, 10); + size_t count = 0; // Counter for unique sequential values. + input.imbue([&count] () { return ++count; }); + + const auto value = Split(input, 1, false); + BOOST_REQUIRE_EQUAL(std::get<0>(value).n_cols, 0); // Train data. + BOOST_REQUIRE_EQUAL(std::get<1>(value).n_cols, 10); // Test data. + + mat concat = arma::join_rows(std::get<0>(value), std::get<1>(value)); + // Order matters here. + CheckMatrices(input, concat); +} + BOOST_AUTO_TEST_CASE(SplitLabeledDataResultMat) { mat input(2, 10);