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