1 /**
2  * @file tests/lars_test.cpp
3  * @author Nishant Mehta
4  *
5  * Test for LARS.
6  *
7  * mlpack is free software; you may redistribute it and/or modify it under the
8  * terms of the 3-clause BSD license.  You should have received a copy of the
9  * 3-clause BSD license along with mlpack.  If not, see
10  * http://www.opensource.org/licenses/BSD-3-Clause for more information.
11  */
12 
13 #include <mlpack/methods/lars/lars.hpp>
14 #include <mlpack/core/data/load.hpp>
15 
16 #include "catch.hpp"
17 #include "test_catch_tools.hpp"
18 
19 using namespace mlpack;
20 using namespace mlpack::regression;
21 
GenerateProblem(arma::mat & X,arma::rowvec & y,size_t nPoints,size_t nDims)22 void GenerateProblem(
23     arma::mat& X, arma::rowvec& y, size_t nPoints, size_t nDims)
24 {
25   X = arma::randn(nDims, nPoints);
26   arma::vec beta = arma::randn(nDims, 1);
27   y = beta.t() * X;
28 }
29 
LARSVerifyCorrectness(arma::vec beta,arma::vec errCorr,double lambda)30 void LARSVerifyCorrectness(arma::vec beta, arma::vec errCorr, double lambda)
31 {
32   size_t nDims = beta.n_elem;
33   const double tol = 1e-10;
34   for (size_t j = 0; j < nDims; ++j)
35   {
36     if (beta(j) == 0)
37     {
38       // Make sure that |errCorr(j)| <= lambda.
39       REQUIRE(std::max(fabs(errCorr(j)) - lambda, 0.0) ==
40           Approx(0.0).margin(tol));
41     }
42     else if (beta(j) < 0)
43     {
44       // Make sure that errCorr(j) == lambda.
45       REQUIRE(errCorr(j) - lambda == Approx(0.0).margin(tol));
46     }
47     else // beta(j) > 0
48     {
49       // Make sure that errCorr(j) == -lambda.
50       REQUIRE(errCorr(j) + lambda == Approx(0.0).margin(tol));
51     }
52   }
53 }
54 
LassoTest(size_t nPoints,size_t nDims,bool elasticNet,bool useCholesky)55 void LassoTest(size_t nPoints, size_t nDims, bool elasticNet, bool useCholesky)
56 {
57   arma::mat X;
58   arma::rowvec y;
59 
60   for (size_t i = 0; i < 100; ++i)
61   {
62     GenerateProblem(X, y, nPoints, nDims);
63 
64     // Armadillo's median is broken, so...
65     arma::vec sortedAbsCorr = sort(abs(X * y.t()));
66     double lambda1 = sortedAbsCorr(nDims / 2);
67     double lambda2;
68     if (elasticNet)
69       lambda2 = lambda1 / 2;
70     else
71       lambda2 = 0;
72 
73 
74     LARS lars(useCholesky, lambda1, lambda2);
75     arma::vec betaOpt;
76     lars.Train(X, y, betaOpt);
77 
78     arma::vec errCorr = (X * trans(X) + lambda2 *
79         arma::eye(nDims, nDims)) * betaOpt - X * y.t();
80 
81     LARSVerifyCorrectness(betaOpt, errCorr, lambda1);
82   }
83 }
84 
85 TEST_CASE("LARSTestLassoCholesky", "[LARSTest]")
86 {
87   LassoTest(100, 10, false, true);
88 }
89 
90 
91 TEST_CASE("LARSTestLassoGram", "[LARSTest]")
92 {
93   LassoTest(100, 10, false, false);
94 }
95 
96 TEST_CASE("LARSTestElasticNetCholesky", "[LARSTest]")
97 {
98   LassoTest(100, 10, true, true);
99 }
100 
101 TEST_CASE("LARSTestElasticNetGram", "[LARSTest]")
102 {
103   LassoTest(100, 10, true, false);
104 }
105 
106 // Ensure that LARS doesn't crash when the data has linearly dependent features
107 // (meaning that there is a singularity).  This test uses the Cholesky
108 // factorization.
109 TEST_CASE("CholeskySingularityTest", "[LARSTest]")
110 {
111   arma::mat X;
112   arma::mat Y;
113 
114   data::Load("lars_dependent_x.csv", X);
115   data::Load("lars_dependent_y.csv", Y);
116 
117   arma::rowvec y = Y.row(0);
118 
119   // Test for a couple values of lambda1.
120   for (double lambda1 = 0.0; lambda1 < 1.0; lambda1 += 0.1)
121   {
122     LARS lars(true, lambda1, 0.0);
123     arma::vec betaOpt;
124     lars.Train(X, y, betaOpt);
125 
126     arma::vec errCorr = (X * X.t()) * betaOpt - X * y.t();
127 
128     LARSVerifyCorrectness(betaOpt, errCorr, lambda1);
129   }
130 }
131 
132 // Same as the above test but with no cholesky factorization.
133 TEST_CASE("NoCholeskySingularityTest", "[LARSTest]")
134 {
135   arma::mat X;
136   arma::mat Y;
137 
138   data::Load("lars_dependent_x.csv", X);
139   data::Load("lars_dependent_y.csv", Y);
140 
141   arma::rowvec y = Y.row(0);
142 
143   // Test for a couple values of lambda1.
144   for (double lambda1 = 0.0; lambda1 < 1.0; lambda1 += 0.1)
145   {
146     LARS lars(false, lambda1, 0.0);
147     arma::vec betaOpt;
148     lars.Train(X, y, betaOpt);
149 
150     arma::vec errCorr = (X * X.t()) * betaOpt - X * y.t();
151 
152     // #373: this test fails on i386 only sometimes.
153 //    LARSVerifyCorrectness(betaOpt, errCorr, lambda1);
154   }
155 }
156 
157 // Make sure that Predict() provides reasonable enough solutions.
158 TEST_CASE("PredictTest", "[LARSTest]")
159 {
160   for (size_t i = 0; i < 2; ++i)
161   {
162     // Run with both true and false.
163     bool useCholesky = bool(i);
164 
165     arma::mat X;
166     arma::rowvec y;
167 
168     GenerateProblem(X, y, 1000, 100);
169 
170     for (double lambda1 = 0.0; lambda1 < 1.0; lambda1 += 0.2)
171     {
172       for (double lambda2 = 0.0; lambda2 < 1.0; lambda2 += 0.2)
173       {
174         LARS lars(useCholesky, lambda1, lambda2);
175         arma::vec betaOpt;
176         lars.Train(X, y, betaOpt);
177 
178         // Calculate what the actual error should be with these regression
179         // parameters.
180         arma::vec betaOptPred = (X * X.t()) * betaOpt;
181         arma::rowvec predictions;
182         lars.Predict(X, predictions);
183         arma::vec adjPred = X * predictions.t();
184 
185         REQUIRE(predictions.n_elem == 1000);
186         for (size_t i = 0; i < betaOptPred.n_elem; ++i)
187         {
188           if (std::abs(betaOptPred[i]) < 1e-5)
189             REQUIRE(adjPred[i] == Approx(0.0).margin(1e-5));
190           else
191             REQUIRE(adjPred[i] == Approx(betaOptPred[i]).epsilon(1e-7));
192         }
193       }
194     }
195   }
196 }
197 
198 TEST_CASE("PredictRowMajorTest", "[LARSTest]")
199 {
200   arma::mat X;
201   arma::rowvec y;
202   GenerateProblem(X, y, 1000, 100);
203 
204   // Set lambdas to 0.
205 
206   LARS lars(false, 0, 0);
207   arma::vec betaOpt;
208   lars.Train(X, y, betaOpt);
209 
210   // Get both row-major and column-major predictions.  Make sure they are the
211   // same.
212   arma::rowvec rowMajorPred, colMajorPred;
213 
214   lars.Predict(X, colMajorPred);
215   lars.Predict(X.t(), rowMajorPred, true);
216 
217   REQUIRE(colMajorPred.n_elem == rowMajorPred.n_elem);
218   for (size_t i = 0; i < colMajorPred.n_elem; ++i)
219   {
220     if (std::abs(colMajorPred[i]) < 1e-5)
221       REQUIRE(rowMajorPred[i] == Approx(0.0).margin(1e-5));
222     else
223       REQUIRE(colMajorPred[i] == Approx(rowMajorPred[i]).epsilon(1e-7));
224   }
225 }
226 
227 /**
228  * Make sure that if we train twice, there is no issue.
229  */
230 TEST_CASE("RetrainTest", "[LARSTest]")
231 {
232   arma::mat origX;
233   arma::rowvec origY;
234   GenerateProblem(origX, origY, 1000, 50);
235 
236   arma::mat newX;
237   arma::rowvec newY;
238   GenerateProblem(newX, newY, 750, 75);
239 
240   LARS lars(false, 0.1, 0.1);
241   arma::vec betaOpt;
242   lars.Train(origX, origY, betaOpt);
243 
244   // Now train on new data.
245   lars.Train(newX, newY, betaOpt);
246 
247   arma::vec errCorr = (newX * trans(newX) + 0.1 *
248         arma::eye(75, 75)) * betaOpt - newX * newY.t();
249 
250   LARSVerifyCorrectness(betaOpt, errCorr, 0.1);
251 }
252 
253 /**
254  * Make sure if we train twice using the Cholesky decomposition, there is no
255  * issue.
256  */
257 TEST_CASE("RetrainCholeskyTest", "[LARSTest]")
258 {
259   arma::mat origX;
260   arma::rowvec origY;
261   GenerateProblem(origX, origY, 1000, 50);
262 
263   arma::mat newX;
264   arma::rowvec newY;
265   GenerateProblem(newX, newY, 750, 75);
266 
267   LARS lars(true, 0.1, 0.1);
268   arma::vec betaOpt;
269   lars.Train(origX, origY, betaOpt);
270 
271   // Now train on new data.
272   lars.Train(newX, newY, betaOpt);
273 
274   arma::vec errCorr = (newX * trans(newX) + 0.1 *
275         arma::eye(75, 75)) * betaOpt - newX * newY.t();
276 
277   LARSVerifyCorrectness(betaOpt, errCorr, 0.1);
278 }
279 
280 /**
281  * Make sure that we get correct solution coefficients when running training
282  * and accessing solution coefficients separately.
283  */
284 TEST_CASE("TrainingAndAccessingBetaTest", "[LARSTest]")
285 {
286   arma::mat X;
287   arma::rowvec y;
288 
289   GenerateProblem(X, y, 1000, 100);
290 
291   LARS lars1;
292   arma::vec beta;
293   lars1.Train(X, y, beta);
294 
295   LARS lars2;
296   lars2.Train(X, y);
297 
298   REQUIRE(beta.n_elem == lars2.Beta().n_elem);
299   for (size_t i = 0; i < beta.n_elem; ++i)
300     REQUIRE(beta[i] == Approx(lars2.Beta()[i]).epsilon(1e-7));
301 }
302 
303 /**
304  * Make sure that we learn the same when running training separately and through
305  * constructor. Test it with default parameters.
306  */
307 TEST_CASE("TrainingConstructorWithDefaultsTest", "[LARSTest]")
308 {
309   arma::mat X;
310   arma::rowvec y;
311 
312   GenerateProblem(X, y, 1000, 100);
313 
314   LARS lars1;
315   arma::vec beta;
316   lars1.Train(X, y, beta);
317 
318   LARS lars2(X, y);
319 
320   REQUIRE(beta.n_elem == lars2.Beta().n_elem);
321   for (size_t i = 0; i < beta.n_elem; ++i)
322     REQUIRE(beta[i] == Approx(lars2.Beta()[i]).epsilon(1e-7));
323 }
324 
325 /**
326  * Make sure that we learn the same when running training separately and through
327  * constructor. Test it with non default parameters.
328  */
329 TEST_CASE("TrainingConstructorWithNonDefaultsTest", "[LARSTest]")
330 {
331   arma::mat X;
332   arma::rowvec y;
333 
334   GenerateProblem(X, y, 1000, 100);
335 
336   bool transposeData = true;
337   bool useCholesky = true;
338   double lambda1 = 0.2;
339   double lambda2 = 0.4;
340 
341   LARS lars1(useCholesky, lambda1, lambda2);
342   arma::vec beta;
343   lars1.Train(X, y, beta);
344 
345   LARS lars2(X, y, transposeData, useCholesky, lambda1, lambda2);
346 
347   REQUIRE(beta.n_elem == lars2.Beta().n_elem);
348   for (size_t i = 0; i < beta.n_elem; ++i)
349     REQUIRE(beta[i] == Approx(lars2.Beta()[i]).epsilon(1e-7));
350 }
351 
352 /**
353  * Test that LARS::Train() returns finite error value.
354  */
355 TEST_CASE("LARSTrainReturnCorrelation", "[LARSTest]")
356 {
357   arma::mat X;
358   arma::mat Y;
359 
360   data::Load("lars_dependent_x.csv", X);
361   data::Load("lars_dependent_y.csv", Y);
362 
363   arma::rowvec y = Y.row(0);
364 
365   double lambda1 = 0.1;
366   double lambda2 = 0.1;
367 
368   // Test with Cholesky decomposition and with lasso.
369   LARS lars1(true, lambda1, 0.0);
370   arma::vec betaOpt1;
371   double error = lars1.Train(X, y, betaOpt1);
372 
373   REQUIRE(std::isfinite(error) == true);
374 
375   // Test without Cholesky decomposition and with lasso.
376   LARS lars2(false, lambda1, 0.0);
377   arma::vec betaOpt2;
378   error = lars2.Train(X, y, betaOpt2);
379 
380   REQUIRE(std::isfinite(error) == true);
381 
382   // Test with Cholesky decomposition and with elasticnet.
383   LARS lars3(true, lambda1, lambda2);
384   arma::vec betaOpt3;
385   error = lars3.Train(X, y, betaOpt3);
386 
387   REQUIRE(std::isfinite(error) == true);
388 
389   // Test without Cholesky decomposition and with elasticnet.
390   LARS lars4(false, lambda1, lambda2);
391   arma::vec betaOpt4;
392   error = lars4.Train(X, y, betaOpt4);
393 
394   REQUIRE(std::isfinite(error) == true);
395 }
396 
397 /**
398  * Test that LARS::ComputeError() returns error value less than 1
399  * and greater than 0.
400  */
401 TEST_CASE("LARSTestComputeError", "[LARSTest]")
402 {
403   arma::mat X;
404   arma::mat Y;
405 
406   data::Load("lars_dependent_x.csv", X);
407   data::Load("lars_dependent_y.csv", Y);
408 
409   arma::rowvec y = Y.row(0);
410 
411   LARS lars1(true, 0.1, 0.0);
412   arma::vec betaOpt1;
413   double train1 = lars1.Train(X, y, betaOpt1);
414   double cost = lars1.ComputeError(X, y);
415 
416   REQUIRE(cost <= 1);
417   REQUIRE(cost >= 0);
418   REQUIRE(cost == train1);
419 }
420 
421 /**
422  * Simple test for LARS copy constructor.
423  */
424 TEST_CASE("LARSCopyConstructorTest", "[LARSTest]")
425 {
426   arma::mat features, Y;
427   arma::rowvec targets;
428 
429   // Load training input and predictions for testing.
430   data::Load("lars_dependent_x.csv", features);
431   data::Load("lars_dependent_y.csv", Y);
432   targets = Y.row(0);
433 
434   // Check if the copy is accessible even after deleting the pointer to the
435   // object.
436   mlpack::regression::LARS* glm1 = new mlpack::regression::LARS(false, .1, .1);
437   arma::rowvec predictions, predictionsFromCopiedModel;
438   std::vector<mlpack::regression::LARS> models;
439   glm1->Train(features, targets);
440   glm1->Predict(features, predictions);
441   models.emplace_back(*glm1); // Call the copy constructor.
442   delete glm1; // Free LARS internal memory.
443   models[0].Predict(features, predictionsFromCopiedModel);
444   // The output of both models should be the same.
445   CheckMatrices(predictions, predictionsFromCopiedModel);
446   // Check if we can train the model again.
447   REQUIRE_NOTHROW(models[0].Train(features, targets));
448 
449   // Check if we can train the copied model.
450   mlpack::regression::LARS glm2(false, 0.1, 0.1);
451   models.emplace_back(glm2); // Call the copy constructor.
452   REQUIRE_NOTHROW(glm2.Train(features, targets));
453   REQUIRE_NOTHROW(models[1].Train(features, targets));
454 
455   // Create a copy using assignment operator.
456   mlpack::regression::LARS glm3 = glm2;
457   models[1].Predict(features, predictions);
458   glm3.Predict(features, predictionsFromCopiedModel);
459   // The output of both models should be the same.
460   CheckMatrices(predictions, predictionsFromCopiedModel);
461 }
462