1103 lines
32 KiB
C++
1103 lines
32 KiB
C++
/**
|
|
* @file tests/decision_tree_regressor_test.cpp
|
|
* @author Rishabh Garg
|
|
*
|
|
* Tests for the DecisionTreeRegressor class and related classes.
|
|
*
|
|
* 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.
|
|
*/
|
|
#include <mlpack/core.hpp>
|
|
#include <mlpack/methods/decision_tree/decision_tree_regressor.hpp>
|
|
#include <mlpack/methods/decision_tree/mad_gain.hpp>
|
|
#include <mlpack/methods/decision_tree/mse_gain.hpp>
|
|
#include <mlpack/methods/decision_tree/random_dimension_select.hpp>
|
|
#include <mlpack/methods/decision_tree/multiple_random_dimension_select.hpp>
|
|
#include <mlpack/core/math/random.hpp>
|
|
|
|
#include "catch.hpp"
|
|
#include "serialization.hpp"
|
|
#include "mock_categorical_data.hpp"
|
|
#include "test_function_tools.hpp"
|
|
|
|
using namespace mlpack;
|
|
using namespace mlpack::tree;
|
|
using namespace mlpack::distribution;
|
|
|
|
/**
|
|
* Creates dataset with 5 groups with all the points in same group have exactly
|
|
* same responses.
|
|
*/
|
|
void CreateMultiSplitData(arma::mat& d, arma::rowvec& r, const size_t count,
|
|
arma::rowvec& values)
|
|
{
|
|
d = arma::mat(10, count, arma::fill::randu);
|
|
r = arma::rowvec(count);
|
|
|
|
// Group 1.
|
|
for (size_t i = 0; i < count / 5; i++)
|
|
{
|
|
d(3, i) = i;
|
|
r(i) = values[0];
|
|
}
|
|
// Group 2.
|
|
for (size_t i = count / 5; i < (count / 5) * 2; i++)
|
|
{
|
|
d(3, i) = i;
|
|
r(i) = values[1];
|
|
}
|
|
// Group 3.
|
|
for (size_t i = (count / 5) * 2; i < (count / 5) * 3; i++)
|
|
{
|
|
d(3, i) = i;
|
|
r(i) = values[2];
|
|
}
|
|
// Group 4.
|
|
for (size_t i = (count / 5) * 3; i < (count / 5) * 4; i++)
|
|
{
|
|
d(3, i) = i;
|
|
r(i) = values[3];
|
|
}
|
|
// Group 5.
|
|
for (size_t i = (count / 5) * 4; i < count; i++)
|
|
{
|
|
d(3, i) = i;
|
|
r(i) = values[4];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make sure the MSE gain is zero when the responses are perfect.
|
|
*/
|
|
TEST_CASE("MSEGainPerfectTest", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec weights(10, arma::fill::ones);
|
|
arma::rowvec responses;
|
|
responses.ones(10);
|
|
|
|
REQUIRE(MSEGain::Evaluate<false>(responses, weights) ==
|
|
Approx(0.0).margin(1e-5));
|
|
}
|
|
|
|
/**
|
|
* The MSE gain of an empty vector is 0.
|
|
*/
|
|
TEST_CASE("MSEGainEmptyTest", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec weights = arma::ones<arma::rowvec>(10);
|
|
arma::rowvec responses;
|
|
|
|
REQUIRE(MSEGain::Evaluate<false>(responses, weights) ==
|
|
Approx(0.0).margin(1e-5));
|
|
REQUIRE(MSEGain::Evaluate<true>(responses, weights) ==
|
|
Approx(0.0).margin(1e-5));
|
|
}
|
|
|
|
/**
|
|
* Making sure that MSE gain is evaluated correctly by doing calculation by
|
|
* hand.
|
|
*/
|
|
TEST_CASE("MSEGainHandCalculation", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec responses = {4., 2., 3., 4., 13., 6., 20., 8., 9., 10.};
|
|
arma::rowvec weights = {0.3, 0.3, 0.3, 0.3, 0.3, 0.7, 0.7, 0.7, 0.7, 0.7};
|
|
|
|
// Hand calculated gain values.
|
|
const double gain = -27.08999;
|
|
const double weightedGain = -27.53960;
|
|
|
|
REQUIRE(MSEGain::Evaluate<false>(responses, weights) ==
|
|
Approx(gain).margin(1e-5));
|
|
REQUIRE(MSEGain::Evaluate<true>(responses, weights) ==
|
|
Approx(weightedGain).margin(1e-5));
|
|
}
|
|
|
|
/**
|
|
* Make sure the MAD gain is zero when the responses are perfect.
|
|
*/
|
|
TEST_CASE("MADGainPerfectTest", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec weights(10, arma::fill::ones);
|
|
arma::rowvec responses;
|
|
responses.ones(10);
|
|
|
|
REQUIRE(MADGain::Evaluate<false>(responses, weights) ==
|
|
Approx(0.0).margin(1e-5));
|
|
}
|
|
|
|
/**
|
|
* Make sure that when mean of responses is zero, MAD_gain = mean of
|
|
* absolute values of the distribution.
|
|
*/
|
|
TEST_CASE("MADGainNormalTest", "[DecisionTreeRegressorTest")
|
|
{
|
|
arma::rowvec weights(10, arma::fill::ones);
|
|
arma::rowvec responses = { 1, 2, 3, 4, 5, -1, -2, -3, -4, -5 }; // Mean = 0.
|
|
|
|
// Theoretical gain.
|
|
double theoreticalGain = 0.0;
|
|
for (size_t i = 0; i < responses.n_elem; ++i)
|
|
theoreticalGain -= std::abs(responses[i]);
|
|
theoreticalGain /= (double) responses.n_elem;
|
|
|
|
// Calculated gain.
|
|
const double calculatedGain = MADGain::Evaluate<false>(responses, weights);
|
|
|
|
REQUIRE(calculatedGain == Approx(theoreticalGain).margin(1e-5));
|
|
}
|
|
|
|
/**
|
|
* The MAD gain of an empty vector is 0.
|
|
*/
|
|
TEST_CASE("MADGainEmptyTest", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec weights = arma::ones<arma::rowvec>(10);
|
|
arma::rowvec responses;
|
|
|
|
REQUIRE(MADGain::Evaluate<false>(responses, weights) ==
|
|
Approx(0.0).margin(1e-5));
|
|
REQUIRE(MADGain::Evaluate<true>(responses, weights) ==
|
|
Approx(0.0).margin(1e-5));
|
|
}
|
|
|
|
/**
|
|
* Making sure that MAD gain is evaluated correctly by doing calculation by
|
|
* hand.
|
|
*/
|
|
TEST_CASE("MADGainHandCalculation", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec responses = {4., 2., 3., 4., 13., 6., 20., 8., 9., 10.};
|
|
arma::rowvec weights = {0.3, 0.3, 0.3, 0.3, 0.3, 0.7, 0.7, 0.7, 0.7, 0.7};
|
|
|
|
// Hand calculated gain values.
|
|
const double gain = -4.1;
|
|
const double weightedGain = -3.8592;
|
|
|
|
REQUIRE(MADGain::Evaluate<false>(responses, weights) ==
|
|
Approx(gain).margin(1e-5));
|
|
REQUIRE(MADGain::Evaluate<true>(responses, weights) ==
|
|
Approx(weightedGain).margin(1e-5));
|
|
}
|
|
|
|
/**
|
|
* Check that AllCategoricalSplit will split when the split is obviously
|
|
* better.
|
|
*/
|
|
TEST_CASE("AllCategoricalSplitSimpleSplitTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::vec predictor(100);
|
|
arma::rowvec responses(100);
|
|
arma::rowvec weights(responses.n_elem);
|
|
weights.ones();
|
|
|
|
for (size_t i = 0; i < 100; i+=2)
|
|
{
|
|
predictor[i] = 0;
|
|
responses[i] = 5.0;
|
|
predictor[i + 1] = 1;
|
|
responses[i + 1] = 100;
|
|
}
|
|
|
|
double splitInfo;
|
|
AllCategoricalSplit<MSEGain>::AuxiliarySplitInfo aux;
|
|
|
|
// Call the method to do the splitting.
|
|
MSEGain f;
|
|
const double bestGain = f.Evaluate<false>(responses, weights);
|
|
const double gain = AllCategoricalSplit<MSEGain>::SplitIfBetter<false>(
|
|
bestGain, predictor, 2, responses, weights, 3, 1e-7, splitInfo, aux, f);
|
|
const double weightedGain =
|
|
AllCategoricalSplit<MSEGain>::SplitIfBetter<true>(bestGain, predictor, 2,
|
|
responses, weights, 3, 1e-7, splitInfo, aux, f);
|
|
|
|
// Make sure that a split was made.
|
|
REQUIRE(gain > bestGain);
|
|
|
|
REQUIRE(gain == weightedGain);
|
|
|
|
// Make sure that splitInfo now hold the number of children.
|
|
REQUIRE((size_t) splitInfo == 2);
|
|
}
|
|
|
|
/**
|
|
* Make sure that AllCategoricalSplit respects the minimum number of samples
|
|
* required to split.
|
|
*/
|
|
TEST_CASE("AllCategoricalSplitMinSamplesTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec predictors = {0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3};
|
|
arma::rowvec responses = {0, 0, 0, 2, 2, 2, 1, 1, 1, 2, 2, 2};
|
|
arma::rowvec weights(responses.n_elem);
|
|
weights.ones();
|
|
|
|
double splitInfo;
|
|
AllCategoricalSplit<MSEGain>::AuxiliarySplitInfo aux;
|
|
|
|
// Call the method to do the splitting.
|
|
MSEGain f;
|
|
const double bestGain = f.Evaluate<false>(responses, weights);
|
|
const double gain = AllCategoricalSplit<MSEGain>::SplitIfBetter<false>(
|
|
bestGain, predictors, 4, responses, weights, 4, 1e-7, splitInfo, aux, f);
|
|
|
|
// Make sure it's not split.
|
|
REQUIRE(gain == DBL_MAX);
|
|
}
|
|
|
|
/**
|
|
* Check that no split is made when it doesn't get us anything.
|
|
*/
|
|
TEST_CASE("AllCategoricalSplitNoGainTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec predictors(300);
|
|
arma::rowvec responses(300);
|
|
arma::rowvec weights = arma::ones<arma::rowvec>(300);
|
|
|
|
for (size_t i = 0; i < 300; i += 3)
|
|
{
|
|
predictors[i] = int(i / 3) % 10;
|
|
responses[i] = -0.5;
|
|
predictors[i + 1] = int(i / 3) % 10;
|
|
responses[i + 1] = 0;
|
|
predictors[i + 2] = int(i / 3) % 10;
|
|
responses[i + 2] = 0.5;
|
|
}
|
|
|
|
double splitInfo;
|
|
AllCategoricalSplit<MSEGain>::AuxiliarySplitInfo aux;
|
|
|
|
// Call the method to do the splitting.
|
|
MSEGain f;
|
|
const double bestGain = f.Evaluate<false>(responses, weights);
|
|
const double gain = AllCategoricalSplit<MSEGain>::SplitIfBetter<false>(
|
|
bestGain, predictors, 10, responses, weights, 10, 1e-7,
|
|
splitInfo, aux, f);
|
|
const double weightedGain =
|
|
AllCategoricalSplit<MSEGain>::SplitIfBetter<true>(bestGain, predictors,
|
|
10, responses, weights, 10, 1e-7, splitInfo, aux, f);
|
|
|
|
// Make sure that there was no split.
|
|
REQUIRE(gain == DBL_MAX);
|
|
REQUIRE(gain == weightedGain);
|
|
}
|
|
|
|
/**
|
|
* Check that the BestBinaryNumericSplit will split on an obviously splittable
|
|
* dimension.
|
|
*/
|
|
TEST_CASE("BestBinaryNumericSplitSimpleSplitTest_",
|
|
"[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec predictors =
|
|
{ 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 };
|
|
arma::rowvec responses =
|
|
{ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 };
|
|
arma::rowvec weights(responses.n_elem);
|
|
weights.ones();
|
|
|
|
double splitInfo;
|
|
BestBinaryNumericSplit<MADGain>::AuxiliarySplitInfo aux;
|
|
|
|
// Call the method to do the splitting.
|
|
MADGain f;
|
|
const double bestGain = f.Evaluate<false>(responses, weights);
|
|
const double gain = BestBinaryNumericSplit<MADGain>::SplitIfBetter<false>(
|
|
bestGain, predictors, responses, weights, 3, 1e-7, splitInfo, aux, f);
|
|
const double weightedGain =
|
|
BestBinaryNumericSplit<MADGain>::SplitIfBetter<true>(bestGain, predictors,
|
|
responses, weights, 3, 1e-7, splitInfo, aux, f);
|
|
|
|
// Make sure that a split was made.
|
|
REQUIRE(gain > bestGain);
|
|
|
|
// Make sure weight works and is not different than the unweighted one.
|
|
REQUIRE(gain == weightedGain);
|
|
|
|
// The class probabilities, for this split, hold the splitting point, which
|
|
// should be between 4 and 5.
|
|
REQUIRE(splitInfo > 0.4);
|
|
REQUIRE(splitInfo < 0.5);
|
|
}
|
|
|
|
/**
|
|
* Check that the BestBinaryNumericSplit won't split if not enough points are
|
|
* given.
|
|
*/
|
|
TEST_CASE("BestBinaryNumericSplitMinSamplesTest_",
|
|
"[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec predictors =
|
|
{ 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 };
|
|
arma::rowvec responses =
|
|
{ 0.5, 0.5, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 };
|
|
arma::rowvec weights(responses.n_elem);
|
|
|
|
double splitInfo;
|
|
BestBinaryNumericSplit<MSEGain>::AuxiliarySplitInfo aux;
|
|
|
|
// Call the method to do the splitting.
|
|
MSEGain f;
|
|
const double bestGain = f.Evaluate<false>(responses, weights);
|
|
const double gain = BestBinaryNumericSplit<MSEGain>::SplitIfBetter<false>(
|
|
bestGain, predictors, responses, weights, 8, 1e-7, splitInfo, aux, f);
|
|
// This should make no difference because it won't split at all.
|
|
const double weightedGain =
|
|
BestBinaryNumericSplit<MSEGain>::SplitIfBetter<true>(bestGain,
|
|
predictors, responses, weights, 8, 1e-7, splitInfo, aux, f);
|
|
|
|
// Make sure that no split was made.
|
|
REQUIRE(gain == DBL_MAX);
|
|
REQUIRE(gain == weightedGain);
|
|
}
|
|
|
|
/**
|
|
* Check that the BestBinaryNumericSplit doesn't split a dimension that gives
|
|
* no gain.
|
|
*/
|
|
TEST_CASE("BestBinaryNumericSplitNoGainTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::rowvec predictors(100);
|
|
arma::rowvec responses(100);
|
|
arma::rowvec weights;
|
|
for (size_t i = 0; i < 100; i += 2)
|
|
{
|
|
predictors[i] = i;
|
|
responses[i] = 0.0;
|
|
predictors[i + 1] = i;
|
|
responses[i + 1] = 1.0;
|
|
}
|
|
|
|
double splitInfo;
|
|
BestBinaryNumericSplit<MSEGain>::AuxiliarySplitInfo aux;
|
|
|
|
// Call the method to do the splitting.
|
|
MSEGain f;
|
|
const double bestGain = f.Evaluate<false>(responses, weights);
|
|
const double gain = BestBinaryNumericSplit<MSEGain>::SplitIfBetter<false>(
|
|
bestGain, predictors, responses, weights, 10, 1e-7, splitInfo, aux, f);
|
|
|
|
// Make sure there was no split.
|
|
REQUIRE(gain == DBL_MAX);
|
|
}
|
|
|
|
/**
|
|
* Check that the RandomBinaryNumericSplit always splits when splitIfBetterGain
|
|
* is false.
|
|
*/
|
|
TEST_CASE("RandomBinaryNumericSplitAlwaysSplit_",
|
|
"[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::vec values("0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0");
|
|
arma::rowvec responses("0 0 0 0 0 1 1 1 1 1 1");
|
|
arma::rowvec weights;
|
|
weights.ones(responses.n_elem);
|
|
|
|
double splitInfo;
|
|
RandomBinaryNumericSplit<MSEGain>::AuxiliarySplitInfo aux;
|
|
|
|
// Call the method to do the splitting.
|
|
MSEGain f;
|
|
const double bestGain = f.Evaluate<false>(responses, weights);
|
|
const double gain = RandomBinaryNumericSplit<MSEGain>::SplitIfBetter<false>(
|
|
bestGain, values, responses, weights, 1, 1e-7, splitInfo, aux, f);
|
|
const double weightedGain =
|
|
RandomBinaryNumericSplit<MSEGain>::SplitIfBetter<true>(bestGain, values,
|
|
responses, weights, 1, 1e-7, splitInfo, aux, f);
|
|
|
|
// Make sure that split was made.
|
|
REQUIRE(gain != DBL_MAX);
|
|
REQUIRE(weightedGain != DBL_MAX);
|
|
}
|
|
|
|
/**
|
|
* Check that the RandomBinaryNumericSplit won't split if not enough points are
|
|
* given.
|
|
*/
|
|
TEST_CASE("RandomBinaryNumericSplitMinSamplesTest_",
|
|
"[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::vec values("0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0");
|
|
arma::rowvec responses("0 0 0 0 0 1 1 1 1 1 1");
|
|
arma::rowvec weights(responses.n_elem);
|
|
|
|
double splitInfo;
|
|
RandomBinaryNumericSplit<MSEGain>::AuxiliarySplitInfo aux;
|
|
|
|
// Call the method to do the splitting.
|
|
MSEGain f;
|
|
const double bestGain = f.Evaluate<false>(responses, weights);
|
|
const double gain = RandomBinaryNumericSplit<MSEGain>::SplitIfBetter<false>(
|
|
bestGain, values, responses, weights, 8, 1e-7, splitInfo, aux, f);
|
|
// This should make no difference because it won't split at all.
|
|
const double weightedGain =
|
|
RandomBinaryNumericSplit<MSEGain>::SplitIfBetter<true>(bestGain, values,
|
|
responses, weights, 8, 1e-7, splitInfo, aux, f);
|
|
|
|
// Make sure that no split was made.
|
|
REQUIRE(gain == DBL_MAX);
|
|
REQUIRE(gain == weightedGain);
|
|
}
|
|
|
|
/**
|
|
* Check that the RandomBinaryNumericSplit doesn't split a dimension that gives
|
|
* no gain when splitIfBetterGain is true.
|
|
*/
|
|
TEST_CASE("RandomBinaryNumericSplitNoGainTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::vec values(100);
|
|
arma::rowvec responses(100);
|
|
arma::rowvec weights;
|
|
for (size_t i = 0; i < 100; i += 2)
|
|
{
|
|
values[i] = i;
|
|
responses[i] = 0.0;
|
|
values[i + 1] = i;
|
|
responses[i + 1] = 1.0;
|
|
}
|
|
|
|
double splitInfo;
|
|
RandomBinaryNumericSplit<MSEGain>::AuxiliarySplitInfo aux;
|
|
|
|
// Call the method to do the splitting.
|
|
MSEGain f;
|
|
const double bestGain = f.Evaluate<false>(responses, weights);
|
|
const double gain = RandomBinaryNumericSplit<MSEGain>::SplitIfBetter<false>(
|
|
bestGain, values, responses, weights, 10, 1e-7, splitInfo, aux, f, true);
|
|
|
|
// Make sure there was no split.
|
|
REQUIRE(gain == DBL_MAX);
|
|
}
|
|
|
|
/**
|
|
* A basic construction of the decision tree---ensure that we can create the
|
|
* tree and that it split at least once.
|
|
*/
|
|
TEST_CASE("BasicConstructionTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat dataset(10, 100, arma::fill::randu);
|
|
arma::rowvec responses(100);
|
|
for (size_t i = 0; i < 50; ++i)
|
|
{
|
|
dataset(3, i) = i;
|
|
responses[i] = 0.0;
|
|
}
|
|
for (size_t i = 50; i < 100; ++i)
|
|
{
|
|
dataset(3, i) = i;
|
|
responses[i] = 1.0;
|
|
}
|
|
|
|
// Use default parameters.
|
|
DecisionTreeRegressor<> d(dataset, responses);
|
|
|
|
// Now require that we have some children.
|
|
REQUIRE(d.NumChildren() > 0);
|
|
}
|
|
|
|
/**
|
|
* Construct a tree with weighted responses.
|
|
*/
|
|
TEST_CASE("BasicConstructionTestWithWeight_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat dataset(10, 100, arma::fill::randu);
|
|
arma::rowvec responses(100);
|
|
for (size_t i = 0; i < 50; ++i)
|
|
{
|
|
dataset(3, i) = i;
|
|
responses[i] = 0.0;
|
|
}
|
|
for (size_t i = 50; i < 100; ++i)
|
|
{
|
|
dataset(3, i) = i;
|
|
responses[i] = 1.0;
|
|
}
|
|
arma::rowvec weights(responses.n_elem);
|
|
weights.ones();
|
|
|
|
// Use default parameters.
|
|
DecisionTreeRegressor<> wd(dataset, responses, weights);
|
|
DecisionTreeRegressor<> d(dataset, responses);
|
|
|
|
// Now require that we have some children.
|
|
REQUIRE(wd.NumChildren() > 0);
|
|
REQUIRE(wd.NumChildren() == d.NumChildren());
|
|
}
|
|
|
|
/**
|
|
* Construct the decision tree on numeric data only and see that we can fit it
|
|
* exactly and achieve perfect performance on the training set.
|
|
*/
|
|
TEST_CASE("PerfectTrainingSet_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat dataset(10, 100, arma::fill::randu);
|
|
arma::rowvec responses(100);
|
|
for (size_t i = 0; i < 50; ++i)
|
|
{
|
|
dataset(3, i) = i;
|
|
responses[i] = 0.0;
|
|
}
|
|
for (size_t i = 50; i < 100; ++i)
|
|
{
|
|
dataset(3, i) = i;
|
|
responses[i] = 1.0;
|
|
}
|
|
|
|
// Minimum leaf size of 1.
|
|
DecisionTreeRegressor<> d(dataset, responses, 1, 0.0);
|
|
|
|
// Make sure that we can get perfect fit on the training set.
|
|
for (size_t i = 0; i < 100; ++i)
|
|
{
|
|
double prediction;
|
|
prediction = d.Predict(dataset.col(i));
|
|
|
|
REQUIRE(prediction == Approx(responses[i]).epsilon(1e-7));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Construct the decision tree with weighted responses.
|
|
*/
|
|
TEST_CASE("PerfectTrainingSetWithWeight_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
// Completely random dataset with no structure.
|
|
arma::mat dataset(10, 100, arma::fill::randu);
|
|
arma::rowvec responses(100);
|
|
for (size_t i = 0; i < 50; ++i)
|
|
{
|
|
dataset(3, i) = i;
|
|
responses[i] = 0.0;
|
|
}
|
|
for (size_t i = 50; i < 100; ++i)
|
|
{
|
|
dataset(3, i) = i;
|
|
responses[i] = 1.0;
|
|
}
|
|
arma::rowvec weights = arma::ones<arma::rowvec>(responses.n_elem);
|
|
|
|
// Minimum leaf size of 1.
|
|
DecisionTreeRegressor<> d(dataset, responses, weights, 1, 0.0);
|
|
|
|
// This part of code is dupliacte with no weighted one.
|
|
for (size_t i = 0; i < 100; ++i)
|
|
{
|
|
size_t prediction;
|
|
prediction = d.Predict(dataset.col(i));
|
|
|
|
REQUIRE(prediction == Approx(responses[i]).epsilon(1e-7));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test that the tree is able to perfectly fit all the obvious splits present
|
|
* in the data.
|
|
*
|
|
* |
|
|
* |
|
|
* 2 | xxxxxx
|
|
* |
|
|
* |
|
|
* 1 | xxxxxx xxxxxx
|
|
* |
|
|
* |
|
|
* 0 |xxxxxx xxxxxx
|
|
* |___________________________________
|
|
*/
|
|
TEST_CASE("MultiSplitTest1", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat dataset;
|
|
arma::rowvec responses;
|
|
arma::rowvec values = {0.0, 1.0, 2.0, 1.0, 0.0};
|
|
|
|
CreateMultiSplitData(dataset, responses, 1000, values);
|
|
|
|
arma::rowvec weights(responses.n_elem);
|
|
weights.ones();
|
|
|
|
// Minimum leaf size of 1.
|
|
DecisionTreeRegressor<> d(dataset, responses, weights, 2, 0.0);
|
|
arma::rowvec preds;
|
|
d.Predict(dataset, preds);
|
|
|
|
// Ensure that the predictions are perfect.
|
|
for (size_t i = 0; i < responses.n_elem; ++i)
|
|
REQUIRE(preds[i] == responses[i]);
|
|
|
|
// Ensure that a split is made only when required and no redundant splits are
|
|
// made.
|
|
REQUIRE(d.NumLeaves() == 5);
|
|
}
|
|
|
|
/**
|
|
* Test that the tree is able to perfectly fit all the obvious splits present
|
|
* in the data. Same test as above, but with less data.
|
|
*/
|
|
TEST_CASE("MultiSplitTest2", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat dataset;
|
|
arma::rowvec responses;
|
|
arma::rowvec values = {0.0, 1.0, 2.0, 1.0, 0.0};
|
|
|
|
CreateMultiSplitData(dataset, responses, 200, values);
|
|
|
|
arma::rowvec weights(responses.n_elem);
|
|
weights.ones();
|
|
|
|
// Minimum leaf size of 1.
|
|
DecisionTreeRegressor<> d(dataset, responses, weights, 2, 0.0);
|
|
arma::rowvec preds;
|
|
d.Predict(dataset, preds);
|
|
|
|
// Ensure that the predictions are perfect.
|
|
for (size_t i = 0; i < responses.n_elem; ++i)
|
|
REQUIRE(preds[i] == responses[i]);
|
|
|
|
// Ensure that a split is made only when required and no redundant splits are
|
|
// made.
|
|
REQUIRE(d.NumLeaves() == 5);
|
|
}
|
|
|
|
/**
|
|
* Test that the tree is able to perfectly fit all the obvious splits present
|
|
* in the data.
|
|
*
|
|
* |
|
|
* 20 | xxxxxx
|
|
* |
|
|
* |
|
|
* 15 | xxxxxx
|
|
* |
|
|
* |
|
|
* 10 | xxxxxx
|
|
* |
|
|
* |
|
|
* 5 | xxxxxx
|
|
* |
|
|
* |
|
|
* 0 |xxxxxx
|
|
* |________________________________________
|
|
*/
|
|
TEST_CASE("MultiSplitTest3", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat dataset;
|
|
arma::Row<double> responses;
|
|
arma::rowvec values = {0.0, 5.0, 10.0, 15.0, 20.0};
|
|
|
|
CreateMultiSplitData(dataset, responses, 500, values);
|
|
|
|
arma::rowvec weights(responses.n_elem);
|
|
weights.ones();
|
|
|
|
// Minimum leaf size of 1.
|
|
DecisionTreeRegressor<> d(dataset, responses, weights, 2, 0.0);
|
|
arma::rowvec preds;
|
|
d.Predict(dataset, preds);
|
|
|
|
// Ensure that the predictions are perfect.
|
|
for (size_t i = 0; i < responses.n_elem; ++i)
|
|
REQUIRE(preds[i] == responses[i]);
|
|
|
|
// Ensure that a split is made only when required and no redundant splits are
|
|
// made.
|
|
REQUIRE(d.NumLeaves() == 5);
|
|
}
|
|
|
|
/**
|
|
* Test that the tree builds correctly on unweighted numerical dataset.
|
|
*/
|
|
TEST_CASE("NumericalBuildTest", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat X;
|
|
arma::rowvec Y;
|
|
|
|
if (!data::Load("lars_dependent_x.csv", X))
|
|
FAIL("Cannot load dataset lars_dependent_x.csv");
|
|
if (!data::Load("lars_dependent_y.csv", Y))
|
|
FAIL("Cannot load dataset lars_dependent_y.csv");
|
|
|
|
arma::mat XTrain, XTest;
|
|
arma::rowvec YTrain, YTest;
|
|
data::Split(X, Y, XTrain, XTest, YTrain, YTest, 0.3);
|
|
|
|
DecisionTreeRegressor<> tree(XTrain, YTrain, 5);
|
|
|
|
arma::rowvec predictions;
|
|
tree.Predict(XTest, predictions);
|
|
|
|
// Ensuring a decent performance.
|
|
const double rmse = RMSE(predictions, YTest);
|
|
REQUIRE(rmse < 1.0);
|
|
}
|
|
|
|
/**
|
|
* Test that the tree builds correctly on weighted numerical dataset.
|
|
*/
|
|
TEST_CASE("NumericalBuildTestWithWeights", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat X;
|
|
arma::rowvec Y;
|
|
|
|
if (!data::Load("lars_dependent_x.csv", X))
|
|
FAIL("Cannot load dataset lars_dependent_x.csv");
|
|
if (!data::Load("lars_dependent_y.csv", Y))
|
|
FAIL("Cannot load dataset lars_dependent_y.csv");
|
|
|
|
arma::mat XTrain, XTest;
|
|
arma::rowvec YTrain, YTest;
|
|
data::Split(X, Y, XTrain, XTest, YTrain, YTest, 0.3);
|
|
|
|
arma::rowvec weights = arma::ones<arma::rowvec>(XTrain.n_elem);
|
|
|
|
DecisionTreeRegressor<> tree(XTrain, YTrain, weights, 5);
|
|
|
|
arma::rowvec predictions;
|
|
tree.Predict(XTest, predictions);
|
|
|
|
// Ensuring a decent performance.
|
|
const double rmse = RMSE(predictions, YTest);
|
|
REQUIRE(rmse < 1.0);
|
|
}
|
|
|
|
/**
|
|
* Test that we can build a decision tree on a simple categorical dataset.
|
|
*/
|
|
TEST_CASE("CategoricalBuildTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat d;
|
|
arma::rowvec r;
|
|
data::DatasetInfo di;
|
|
MockCategoricalData(d, r, di);
|
|
|
|
// Split into a training set and a test set.
|
|
arma::mat trainingData = d.cols(0, 1999);
|
|
arma::mat testData = d.cols(2000, 3999);
|
|
arma::rowvec trainingResponses = r.subvec(0, 1999);
|
|
arma::rowvec testResponses = r.subvec(2000, 3999);
|
|
|
|
// Build the tree.
|
|
DecisionTreeRegressor<> tree(trainingData, di, trainingResponses, 10);
|
|
|
|
// Now evaluate the quality of predictions.
|
|
arma::rowvec predictions;
|
|
tree.Predict(testData, predictions);
|
|
|
|
REQUIRE(predictions.n_elem == testData.n_cols);
|
|
|
|
// Make sure we get reasonable rmse.
|
|
const double rmse = RMSE(predictions, testResponses);
|
|
REQUIRE(rmse < 1.0);
|
|
}
|
|
|
|
/**
|
|
* Test that we can build a decision tree with weights on a simple categorical
|
|
* dataset.
|
|
*/
|
|
TEST_CASE("CategoricalBuildTestWithWeight_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat d;
|
|
arma::rowvec r;
|
|
data::DatasetInfo di;
|
|
MockCategoricalData(d, r, di);
|
|
|
|
// Split into a training set and a test set.
|
|
arma::mat trainingData = d.cols(0, 1999);
|
|
arma::mat testData = d.cols(2000, 3999);
|
|
arma::rowvec trainingResponses = r.subvec(0, 1999);
|
|
arma::rowvec testResponses = r.subvec(2000, 3999);
|
|
|
|
arma::rowvec weights = arma::ones<arma::rowvec>(trainingResponses.n_elem);
|
|
|
|
// Build the tree.
|
|
DecisionTreeRegressor<> tree(trainingData, di, trainingResponses, weights,
|
|
10);
|
|
|
|
// Now evaluate the quality of predictions.
|
|
arma::rowvec predictions;
|
|
tree.Predict(testData, predictions);
|
|
|
|
REQUIRE(predictions.n_elem == testData.n_cols);
|
|
|
|
// Make sure we get reasonable rmse.
|
|
const double rmse = RMSE(predictions, testResponses);
|
|
REQUIRE(rmse < 1.0);
|
|
}
|
|
|
|
/**
|
|
* Test that we can build a decision tree on a simple categorical dataset using
|
|
* weights, with low-weight noise added.
|
|
*/
|
|
TEST_CASE("CategoricalWeightedBuildTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat d;
|
|
arma::rowvec r;
|
|
data::DatasetInfo di;
|
|
MockCategoricalData(d, r, di);
|
|
|
|
// Split into a training set and a test set.
|
|
arma::mat trainingData = d.cols(0, 1999);
|
|
arma::mat testData = d.cols(2000, 3999);
|
|
arma::rowvec trainingResponses = r.subvec(0, 1999);
|
|
arma::rowvec testResponses = r.subvec(2000, 3999);
|
|
|
|
// Now create random points.
|
|
arma::mat randomNoise(5, 2000);
|
|
arma::rowvec randomResponses(2000);
|
|
for (size_t i = 0; i < 2000; ++i)
|
|
{
|
|
randomNoise(0, i) = math::Random();
|
|
randomNoise(1, i) = math::Random(-1, 1);
|
|
randomNoise(2, i) = math::Random();
|
|
randomNoise(3, i) = math::RandInt(0, 2);
|
|
randomNoise(4, i) = math::RandInt(0, 5);
|
|
randomResponses[i] = math::Random(-10, 18);
|
|
}
|
|
|
|
// Generate weights.
|
|
arma::rowvec weights(4000);
|
|
for (size_t i = 0; i < 2000; ++i)
|
|
weights[i] = math::Random(0.9, 1.0);
|
|
for (size_t i = 2000; i < 4000; ++i)
|
|
weights[i] = math::Random(0.0, 0.001);
|
|
|
|
arma::mat fullData = arma::join_rows(trainingData, randomNoise);
|
|
arma::rowvec fullResponses = arma::join_rows(trainingResponses,
|
|
randomResponses);
|
|
|
|
// Build the tree.
|
|
DecisionTreeRegressor<> tree(fullData, di, fullResponses, weights, 10);
|
|
|
|
// Now evaluate the quality of predictions.
|
|
arma::rowvec predictions;
|
|
tree.Predict(testData, predictions);
|
|
|
|
REQUIRE(predictions.n_elem == testData.n_cols);
|
|
|
|
// Make sure we get reasonable rmse.
|
|
const double rmse = RMSE(predictions, testResponses);
|
|
REQUIRE(rmse < 1.5);
|
|
}
|
|
|
|
/**
|
|
* Test that we can build a decision tree using MAD gain on a simple
|
|
* categorical dataset using weights, with low-weight noise added.
|
|
*/
|
|
TEST_CASE("CategoricalMADGainWeightedBuildTest", "[DecisionTreeRegressorTest]")
|
|
{
|
|
arma::mat d;
|
|
arma::rowvec r;
|
|
data::DatasetInfo di;
|
|
MockCategoricalData(d, r, di);
|
|
|
|
// Split into a training set and a test set.
|
|
arma::mat trainingData = d.cols(0, 1999);
|
|
arma::mat testData = d.cols(2000, 3999);
|
|
arma::rowvec trainingResponses = r.subvec(0, 1999);
|
|
arma::rowvec testResponses = r.subvec(2000, 3999);
|
|
|
|
// Now create random points.
|
|
arma::mat randomNoise(5, 2000);
|
|
arma::rowvec randomResponses(2000);
|
|
for (size_t i = 0; i < 2000; ++i)
|
|
{
|
|
randomNoise(0, i) = math::Random();
|
|
randomNoise(1, i) = math::Random(-1, 1);
|
|
randomNoise(2, i) = math::Random();
|
|
randomNoise(3, i) = math::RandInt(0, 2);
|
|
randomNoise(4, i) = math::RandInt(0, 5);
|
|
randomResponses[i] = math::Random(-10, 18);
|
|
}
|
|
|
|
// Generate weights.
|
|
arma::rowvec weights(4000);
|
|
for (size_t i = 0; i < 2000; ++i)
|
|
weights[i] = math::Random(0.9, 1.0);
|
|
for (size_t i = 2000; i < 4000; ++i)
|
|
weights[i] = math::Random(0.0, 0.001);
|
|
|
|
arma::mat fullData = arma::join_rows(trainingData, randomNoise);
|
|
arma::rowvec fullResponses = arma::join_rows(trainingResponses,
|
|
randomResponses);
|
|
|
|
// Build the tree using MAD gain.
|
|
DecisionTreeRegressor<MADGain> tree(fullData, di, fullResponses, weights,
|
|
10);
|
|
|
|
// Now evaluate the quality of predictions.
|
|
arma::rowvec predictions;
|
|
tree.Predict(testData, predictions);
|
|
|
|
REQUIRE(predictions.n_elem == testData.n_cols);
|
|
|
|
// Make sure we get reasonable rmse.
|
|
const double rmse = RMSE(predictions, testResponses);
|
|
REQUIRE(rmse < 1.05);
|
|
}
|
|
|
|
/**
|
|
* Test that the decision tree generalizes reasonably.
|
|
*/
|
|
TEST_CASE("SimpleGeneralizationTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
// Loading data.
|
|
data::DatasetInfo info;
|
|
arma::mat trainData, testData;
|
|
arma::rowvec trainResponses, testResponses;
|
|
LoadBostonHousingDataset(trainData, testData, trainResponses, testResponses,
|
|
info);
|
|
arma::rowvec weights = arma::ones<arma::rowvec>(trainResponses.n_elem);
|
|
|
|
// Build decision tree.
|
|
DecisionTreeRegressor<> d(trainData, info, trainResponses);
|
|
DecisionTreeRegressor<> wd(trainData, info, trainResponses, weights);
|
|
|
|
// Get the predicted test responses.
|
|
arma::rowvec predictions;
|
|
d.Predict(testData, predictions);
|
|
|
|
REQUIRE(predictions.n_elem == testData.n_cols);
|
|
|
|
// Figure out rmse.
|
|
double rmse = RMSE(predictions, testResponses);
|
|
REQUIRE(rmse < 6.1);
|
|
|
|
// Reset the predictions.
|
|
predictions.zeros();
|
|
wd.Predict(testData, predictions);
|
|
|
|
REQUIRE(predictions.n_elem == testData.n_cols);
|
|
|
|
// Figure out rmse.
|
|
rmse = RMSE(predictions, testResponses);
|
|
REQUIRE(rmse < 6.1);
|
|
}
|
|
|
|
/**
|
|
* Test that the decision tree generalizes reasonably when built on float data.
|
|
*/
|
|
TEST_CASE("SimpleGeneralizationFMatTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
// Loading data.
|
|
data::DatasetInfo info;
|
|
arma::fmat trainData, testData;
|
|
arma::rowvec trainLabels, testLabels;
|
|
LoadBostonHousingDataset(trainData, testData, trainLabels, testLabels, info);
|
|
|
|
// Initialize an all-ones weight matrix.
|
|
arma::rowvec weights(trainLabels.n_cols, arma::fill::ones);
|
|
|
|
// Build decision tree.
|
|
DecisionTreeRegressor<> d(trainData, trainLabels);
|
|
DecisionTreeRegressor<> wd(trainData, trainLabels, weights);
|
|
|
|
// Get the predicted test labels.
|
|
arma::rowvec predictions;
|
|
d.Predict(testData, predictions);
|
|
|
|
REQUIRE(predictions.n_elem == testData.n_cols);
|
|
|
|
// Figure out the rmse.
|
|
double rmse = RMSE(predictions, testLabels);
|
|
REQUIRE(rmse < 6.0);
|
|
|
|
// Reset the prediction.
|
|
predictions.zeros();
|
|
wd.Predict(testData, predictions);
|
|
|
|
REQUIRE(predictions.n_elem == testData.n_cols);
|
|
|
|
// Figure out the rmse.
|
|
double wdrmse = RMSE(predictions, testLabels);
|
|
REQUIRE(wdrmse < 6.0);
|
|
}
|
|
|
|
/**
|
|
* Test that we can build a decision tree using weighted data (where the
|
|
* low-weighted data is random noise), and that the tree still builds correctly
|
|
* enough to get good results.
|
|
*/
|
|
TEST_CASE("WeightedDecisionTreeTest_", "[DecisionTreeRegressorTest]")
|
|
{
|
|
// Loading data.
|
|
data::DatasetInfo info;
|
|
arma::mat trainData, testData;
|
|
arma::rowvec trainResponses, testResponses;
|
|
LoadBostonHousingDataset(trainData, testData, trainResponses, testResponses,
|
|
info);
|
|
|
|
// Add some noise.
|
|
arma::mat noise(trainData.n_rows, 100, arma::fill::randu);
|
|
arma::rowvec noiseResponses(100);
|
|
for (size_t i = 0; i < noiseResponses.n_elem; ++i)
|
|
noiseResponses[i] = 15 + math::Random(0, 10); // Random response.
|
|
|
|
// Concatenate data matrices.
|
|
arma::mat data = arma::join_rows(trainData, noise);
|
|
arma::rowvec fullResponses = arma::join_rows(trainResponses, noiseResponses);
|
|
|
|
// Now set weights.
|
|
arma::rowvec weights(trainData.n_cols + 100);
|
|
for (size_t i = 0; i < trainData.n_cols; ++i)
|
|
weights[i] = math::Random(0.9, 1.0);
|
|
for (size_t i = trainData.n_cols; i < trainData.n_cols + 100; ++i)
|
|
weights[i] = math::Random(0.0, 0.01); // Low weights for false points.
|
|
|
|
// Now build the decision tree.
|
|
DecisionTreeRegressor<> d(data, fullResponses, weights);
|
|
|
|
// Now we can check that we get good performance on the test set.
|
|
arma::rowvec predictions;
|
|
d.Predict(testData, predictions);
|
|
|
|
REQUIRE(predictions.n_elem == testData.n_cols);
|
|
|
|
// Figure out the rmse.
|
|
double rmse = RMSE(predictions, testResponses);
|
|
REQUIRE(rmse < 6.0);
|
|
}
|
|
|
|
/**
|
|
* Test that we can build a decision tree using weighted data (where the
|
|
* low-weighted data is random noise) with MAD gain, and that the tree
|
|
* still builds correctly enough to get good results.
|
|
*/
|
|
TEST_CASE("WeightedDecisionTreeMADGainTest", "[DecisionTreeRegressorTest]")
|
|
{
|
|
// Loading data.
|
|
data::DatasetInfo info;
|
|
arma::mat trainData, testData;
|
|
arma::rowvec trainResponses, testResponses;
|
|
LoadBostonHousingDataset(trainData, testData, trainResponses, testResponses,
|
|
info);
|
|
|
|
// Add some noise.
|
|
arma::mat noise(trainData.n_rows, 100, arma::fill::randu);
|
|
arma::rowvec noiseResponses(100);
|
|
for (size_t i = 0; i < noiseResponses.n_elem; ++i)
|
|
noiseResponses[i] = 15 + math::Random(0, 10); // Random response.
|
|
|
|
// Concatenate data matrices.
|
|
arma::mat data = arma::join_rows(trainData, noise);
|
|
arma::rowvec fullResponses = arma::join_rows(trainResponses, noiseResponses);
|
|
|
|
// Now set weights.
|
|
arma::rowvec weights(trainData.n_cols + 100);
|
|
for (size_t i = 0; i < trainData.n_cols; ++i)
|
|
weights[i] = math::Random(0.9, 1.0);
|
|
for (size_t i = trainData.n_cols; i < trainData.n_cols + 100; ++i)
|
|
weights[i] = math::Random(0.0, 0.01); // Low weights for false points.
|
|
|
|
// Now build the decision tree using MADGain.
|
|
DecisionTreeRegressor<MADGain> d(data, fullResponses, weights);
|
|
|
|
// Now we can check that we get good performance on the test set.
|
|
arma::rowvec predictions;
|
|
d.Predict(testData, predictions);
|
|
|
|
REQUIRE(predictions.n_elem == testData.n_cols);
|
|
|
|
// Figure out the rmse.
|
|
double rmse = RMSE(predictions, testResponses);
|
|
REQUIRE(rmse < 6.5);
|
|
}
|