Free cookie consent management tool by TermsFeed Policy Generator

source: branches/HeuristicLab.Algorithms.DataAnalysis.Glmnet/3.4/ElasticNetLinearRegression.cs @ 14375

Last change on this file since 14375 was 14375, checked in by gkronber, 8 years ago

#745: 'Coefficient values' -> 'Coefficients'

File size: 18.9 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2016 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Linq;
24using HeuristicLab.Analysis;
25using HeuristicLab.Common;
26using HeuristicLab.Core;
27using HeuristicLab.Data;
28using HeuristicLab.Encodings.SymbolicExpressionTreeEncoding;
29using HeuristicLab.Optimization;
30using HeuristicLab.Parameters;
31using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
32using HeuristicLab.Problems.DataAnalysis;
33using HeuristicLab.Problems.DataAnalysis.Symbolic;
34using HeuristicLab.Problems.DataAnalysis.Symbolic.Regression;
35
36namespace HeuristicLab.Algorithms.DataAnalysis.Glmnet {
37  [Item("Elastic-net Linear Regression (LR)", "Linear regression with elastic-net regularization (wrapper for glmnet)")]
38  [Creatable(CreatableAttribute.Categories.DataAnalysisRegression, Priority = 110)]
39  [StorableClass]
40  public sealed class ElasticNetLinearRegression : FixedDataAnalysisAlgorithm<IRegressionProblem> {
41    private const string PenalityParameterName = "Penality";
42    private const string LogLambdaParameterName = "Log10(Lambda)";
43    #region parameters
44    public IFixedValueParameter<DoubleValue> PenalityParameter {
45      get { return (IFixedValueParameter<DoubleValue>)Parameters[PenalityParameterName]; }
46    }
47    public IValueParameter<DoubleValue> LogLambdaParameter {
48      get { return (IValueParameter<DoubleValue>)Parameters[LogLambdaParameterName]; }
49    }
50    #endregion
51    #region properties
52    public double Penality {
53      get { return PenalityParameter.Value.Value; }
54      set { PenalityParameter.Value.Value = value; }
55    }
56    public DoubleValue LogLambda {
57      get { return LogLambdaParameter.Value; }
58      set { LogLambdaParameter.Value = value; }
59    }
60    #endregion
61
62    [StorableConstructor]
63    private ElasticNetLinearRegression(bool deserializing) : base(deserializing) { }
64    private ElasticNetLinearRegression(ElasticNetLinearRegression original, Cloner cloner)
65      : base(original, cloner) {
66    }
67    public ElasticNetLinearRegression()
68      : base() {
69      Problem = new RegressionProblem();
70      Parameters.Add(new FixedValueParameter<DoubleValue>(PenalityParameterName, "Penalty factor for balancing between ridge (0.0) and lasso (1.0) regression", new DoubleValue(0.5)));
71      Parameters.Add(new OptionalValueParameter<DoubleValue>(LogLambdaParameterName, "Optional: the value of lambda for which to calculate an elastic-net solution. lambda == null => calculate the whole path of all lambdas"));
72    }
73
74    [StorableHook(HookType.AfterDeserialization)]
75    private void AfterDeserialization() { }
76
77    public override IDeepCloneable Clone(Cloner cloner) {
78      return new ElasticNetLinearRegression(this, cloner);
79    }
80
81    protected override void Run() {
82      if (LogLambda == null) {
83        CreateSolutionPath();
84      } else {
85        CreateSolution(LogLambda.Value);
86      }
87    }
88
89    private void CreateSolution(double logLambda) {
90      double trainNMSE;
91      double testNMSE;
92      var coeff = CreateElasticNetLinearRegressionSolution(Problem.ProblemData, Penality, Math.Pow(10, logLambda), out trainNMSE, out testNMSE);
93      Results.Add(new Result("NMSE (train)", new DoubleValue(trainNMSE)));
94      Results.Add(new Result("NMSE (test)", new DoubleValue(testNMSE)));
95
96      // copied from LR => TODO: reuse code (but skip coefficients = 0.0)
97      ISymbolicExpressionTree tree = new SymbolicExpressionTree(new ProgramRootSymbol().CreateTreeNode());
98      ISymbolicExpressionTreeNode startNode = new StartSymbol().CreateTreeNode();
99      tree.Root.AddSubtree(startNode);
100      ISymbolicExpressionTreeNode addition = new Addition().CreateTreeNode();
101      startNode.AddSubtree(addition);
102
103      int col = 0;
104      foreach (string column in Problem.ProblemData.AllowedInputVariables) {
105        if (!coeff[col].IsAlmost(0.0)) {
106          VariableTreeNode vNode = (VariableTreeNode)new HeuristicLab.Problems.DataAnalysis.Symbolic.Variable().CreateTreeNode();
107          vNode.VariableName = column;
108          vNode.Weight = coeff[col];
109          addition.AddSubtree(vNode);
110        }
111        col++;
112      }
113
114      if (!coeff[coeff.Length - 1].IsAlmost(0.0)) {
115        ConstantTreeNode cNode = (ConstantTreeNode)new Constant().CreateTreeNode();
116        cNode.Value = coeff[coeff.Length - 1];
117        addition.AddSubtree(cNode);
118      }
119
120      SymbolicRegressionSolution solution = new SymbolicRegressionSolution(
121        new SymbolicRegressionModel(Problem.ProblemData.TargetVariable, tree, new SymbolicDataAnalysisExpressionTreeInterpreter()),
122        (IRegressionProblemData)Problem.ProblemData.Clone());
123      solution.Model.Name = "Elastic-net Linear Regression Model";
124      solution.Name = "Elastic-net Linear Regression Solution";
125
126      Results.Add(new Result(solution.Name, solution.Description, solution));
127    }
128
129    private void CreateSolutionPath() {
130      double[] lambda;
131      double[] trainNMSE;
132      double[] testNMSE;
133      double[,] coeff;
134      double[] intercept;
135      RunElasticNetLinearRegression(Problem.ProblemData, Penality, out lambda, out trainNMSE, out testNMSE, out coeff, out intercept);
136
137      var coeffTable = new IndexedDataTable<double>("Coefficients", "The paths of standarized coefficient values over different lambda values");
138      coeffTable.VisualProperties.YAxisMaximumAuto = false;
139      coeffTable.VisualProperties.YAxisMinimumAuto = false;
140      coeffTable.VisualProperties.XAxisMaximumAuto = false;
141      coeffTable.VisualProperties.XAxisMinimumAuto = false;
142
143      coeffTable.VisualProperties.XAxisLogScale = true;
144      coeffTable.VisualProperties.XAxisTitle = "Log10(Lambda)";
145      coeffTable.VisualProperties.YAxisTitle = "Coefficients";
146      coeffTable.VisualProperties.SecondYAxisTitle = "Number of variables";
147
148      var nLambdas = lambda.Length;
149      var nCoeff = coeff.GetLength(1);
150      var dataRows = new IndexedDataRow<double>[nCoeff];
151      var allowedVars = Problem.ProblemData.AllowedInputVariables.ToArray();
152      var numNonZeroCoeffs = new int[nLambdas];
153      for (int i = 0; i < nCoeff; i++) {
154        var coeffId = allowedVars[i];
155        double sigma = Problem.ProblemData.Dataset.GetDoubleValues(coeffId).StandardDeviation();
156        var path = Enumerable.Range(0, nLambdas).Select(r => Tuple.Create(lambda[r], coeff[r, i] * sigma)).ToArray();
157        dataRows[i] = new IndexedDataRow<double>(coeffId, coeffId, path);
158        coeffTable.Rows.Add(dataRows[i]);
159      }
160      for (int i = 0; i < coeff.GetLength(0); i++) {
161        for (int j = 0; j < coeff.GetLength(1); j++) {
162          if (!coeff[i, j].IsAlmost(0.0)) {
163            numNonZeroCoeffs[i]++;
164          }
165        }
166      }
167      if (lambda.Length > 2) {
168        coeffTable.VisualProperties.XAxisMinimumFixedValue = Math.Pow(10, Math.Floor(Math.Log10(lambda.Last())));
169        coeffTable.VisualProperties.XAxisMaximumFixedValue = Math.Pow(10, Math.Ceiling(Math.Log10(lambda.Skip(1).First())));
170      }
171      coeffTable.Rows.Add(new IndexedDataRow<double>("Number of variables", "The number of non-zero coefficients for each step in the path", lambda.Zip(numNonZeroCoeffs, (l, v) => Tuple.Create(l, (double)v))));
172      coeffTable.Rows["Number of variables"].VisualProperties.ChartType = DataRowVisualProperties.DataRowChartType.Points;
173      coeffTable.Rows["Number of variables"].VisualProperties.SecondYAxis = true;
174
175      Results.Add(new Result(coeffTable.Name, coeffTable.Description, coeffTable));
176
177      var errorTable = new IndexedDataTable<double>("NMSE", "Path of NMSE values over different lambda values");
178      errorTable.VisualProperties.YAxisMaximumAuto = false;
179      errorTable.VisualProperties.YAxisMinimumAuto = false;
180      errorTable.VisualProperties.XAxisMaximumAuto = false;
181      errorTable.VisualProperties.XAxisMinimumAuto = false;
182
183      errorTable.VisualProperties.YAxisMinimumFixedValue = 0;
184      errorTable.VisualProperties.YAxisMaximumFixedValue = 1.0;
185      errorTable.VisualProperties.XAxisLogScale = true;
186      errorTable.VisualProperties.XAxisTitle = "Log10(Lambda)";
187      errorTable.VisualProperties.YAxisTitle = "Normalized mean of squared errors (NMSE)";
188      errorTable.VisualProperties.SecondYAxisTitle= "Number of variables";
189      errorTable.Rows.Add(new IndexedDataRow<double>("NMSE (train)", "Path of NMSE values over different lambda values", lambda.Zip(trainNMSE, (l, v) => Tuple.Create(l, v))));
190      errorTable.Rows.Add(new IndexedDataRow<double>("NMSE (test)", "Path of NMSE values over different lambda values", lambda.Zip(testNMSE, (l, v) => Tuple.Create(l, v))));
191      errorTable.Rows.Add(new IndexedDataRow<double>("Number of variables", "The number of non-zero coefficients for each step in the path", lambda.Zip(numNonZeroCoeffs, (l, v) => Tuple.Create(l, (double)v))));
192      if (lambda.Length > 2) {
193        errorTable.VisualProperties.XAxisMinimumFixedValue = Math.Pow(10, Math.Floor(Math.Log10(lambda.Last())));
194        errorTable.VisualProperties.XAxisMaximumFixedValue = Math.Pow(10, Math.Ceiling(Math.Log10(lambda.Skip(1).First())));
195      }
196      errorTable.Rows["NMSE (train)"].VisualProperties.ChartType = DataRowVisualProperties.DataRowChartType.Points;
197      errorTable.Rows["NMSE (test)"].VisualProperties.ChartType = DataRowVisualProperties.DataRowChartType.Points;
198      errorTable.Rows["Number of variables"].VisualProperties.ChartType = DataRowVisualProperties.DataRowChartType.Points;
199      errorTable.Rows["Number of variables"].VisualProperties.SecondYAxis = true;
200                                                                                       
201      Results.Add(new Result(errorTable.Name, errorTable.Description, errorTable));
202    }
203
204    public static double[] CreateElasticNetLinearRegressionSolution(IRegressionProblemData problemData, double penalty, double lambda,
205            out double trainNMSE, out double testNMSE,
206            double coeffLowerBound = double.NegativeInfinity, double coeffUpperBound = double.PositiveInfinity) {
207      double[] trainNMSEs;
208      double[] testNMSEs;
209      // run for exactly one lambda
210      var coeffs = CreateElasticNetLinearRegressionSolution(problemData, penalty, new double[] { lambda }, out trainNMSEs, out testNMSEs, coeffLowerBound, coeffUpperBound);
211      trainNMSE = trainNMSEs[0];
212      testNMSE = testNMSEs[0];
213      return coeffs[0];
214    }
215    public static double[][] CreateElasticNetLinearRegressionSolution(IRegressionProblemData problemData, double penalty, double[] lambda,
216            out double[] trainNMSEs, out double[] testNMSEs,
217            double coeffLowerBound = double.NegativeInfinity, double coeffUpperBound = double.PositiveInfinity,
218            int maxVars = -1) {
219      // run for multiple user-supplied lambdas
220      double[,] coeff;
221      double[] intercept;
222      RunElasticNetLinearRegression(problemData, penalty, lambda.Length, 1.0, lambda, out lambda, out trainNMSEs, out testNMSEs, out coeff, out intercept, coeffLowerBound, coeffUpperBound, maxVars);
223
224      int nRows = intercept.Length;
225      int nCols = coeff.GetLength(1) + 1;
226      double[][] sols = new double[nRows][];
227      for (int solIdx = 0; solIdx < nRows; solIdx++) {
228        sols[solIdx] = new double[nCols];
229        for (int cIdx = 0; cIdx < nCols - 1; cIdx++) {
230          sols[solIdx][cIdx] = coeff[solIdx, cIdx];
231        }
232        sols[solIdx][nCols - 1] = intercept[solIdx];
233      }
234      return sols;
235    }
236
237    public static void RunElasticNetLinearRegression(IRegressionProblemData problemData, double penalty,
238      out double[] lambda, out double[] trainNMSE, out double[] testNMSE, out double[,] coeff, out double[] intercept,
239      double coeffLowerBound = double.NegativeInfinity, double coeffUpperBound = double.PositiveInfinity,
240      int maxVars = -1
241      ) {
242      double[] userLambda = new double[0];
243      // automatically determine lambda values (maximum 100 different lambda values)
244      RunElasticNetLinearRegression(problemData, penalty, 100, 0.0, userLambda, out lambda, out trainNMSE, out testNMSE, out coeff, out intercept, coeffLowerBound, coeffUpperBound, maxVars);
245    }
246
247    /// <summary>
248    /// Elastic net with squared-error-loss for dense predictor matrix, runs the full path of all lambdas
249    /// </summary>
250    /// <param name="problemData">Predictor target matrix x and target vector y</param>
251    /// <param name="penalty">Penalty for balance between ridge (0.0) and lasso (1.0) regression</param>
252    /// <param name="nlam">Maximum number of lambda values (default 100)</param>
253    /// <param name="flmin">User control of lambda values (&lt;1.0 => minimum lambda = flmin * (largest lambda value), >= 1.0 => use supplied lambda values</param>
254    /// <param name="ulam">User supplied lambda values</param>
255    /// <param name="lambda">Output lambda values</param>
256    /// <param name="trainNMSE">Vector of normalized mean of squared error (NMSE = Variance(res) / Variance(y)) values on the training set for each set of coefficients along the path</param>
257    /// <param name="testNMSE">Vector of normalized mean of squared error (NMSE = Variance(res) / Variance(y)) values on the test set for each set of coefficients along the path</param>
258    /// <param name="coeff">Vector of coefficient vectors for each solution along the path</param>
259    /// <param name="intercept">Vector of intercepts for each solution along the path</param>
260    /// <param name="coeffLowerBound">Optional lower bound for all coefficients</param>
261    /// <param name="coeffUpperBound">Optional upper bound for all coefficients</param>
262    /// <param name="maxVars">Maximum allowed number of variables in each solution along the path (-1 => all variables are allowed)</param>
263    private static void RunElasticNetLinearRegression(IRegressionProblemData problemData, double penalty,
264  int nlam, double flmin, double[] ulam, out double[] lambda, out double[] trainNMSE, out double[] testNMSE, out double[,] coeff, out double[] intercept,
265  double coeffLowerBound = double.NegativeInfinity, double coeffUpperBound = double.PositiveInfinity,
266  int maxVars = -1
267  ) {
268      if (penalty < 0.0 || penalty > 1.0) throw new ArgumentException("0 <= penalty <= 1", "penalty");
269
270      double[,] trainX;
271      double[,] testX;
272
273      double[] trainY;
274      double[] testY;
275      int numTrainObs, numTestObs;
276      int numVars;
277      PrepareData(problemData, out trainX, out trainY, out numTrainObs, out testX, out testY, out numTestObs, out numVars);
278
279      int ka = 1; // => covariance updating algorithm
280      double parm = penalty;
281      double[] w = Enumerable.Repeat(1.0, numTrainObs).ToArray(); // all observations have the same weight
282      int[] jd = new int[1]; // do not force to use any of the variables
283      double[] vp = Enumerable.Repeat(1.0, numVars).ToArray(); // all predictor variables are unpenalized
284      double[,] cl = new double[numVars, 2]; // use the same bounds for all coefficients
285      for (int i = 0; i < numVars; i++) {
286        cl[i, 0] = coeffLowerBound;
287        cl[i, 1] = coeffUpperBound;
288      }
289
290      int ne = maxVars > 0 ? maxVars : numVars;
291      int nx = numVars;
292      double thr = 1.0e-5; // default value as recommended in glmnet
293      int isd = 1; //  => regression on standardized predictor variables
294      int intr = 1;  // => do include intercept in model
295      int maxit = 100000; // default value as recommended in glmnet
296      // outputs
297      int lmu = -1;
298      double[,] ca;
299      int[] ia;
300      int[] nin;
301      int nlp = -99;
302      int jerr = -99;
303      double[] trainR2;
304      Glmnet.elnet(ka, parm, numTrainObs, numVars, trainX, trainY, w, jd, vp, cl, ne, nx, nlam, flmin, ulam, thr, isd, intr, maxit, out lmu, out intercept, out ca, out ia, out nin, out trainR2, out lambda, out nlp, out jerr);
305
306      trainNMSE = new double[lmu]; // elnet returns R**2 as 1 - NMSE
307      testNMSE = new double[lmu];
308      coeff = new double[lmu, numVars];
309      for (int solIdx = 0; solIdx < lmu; solIdx++) {
310        trainNMSE[solIdx] = 1.0 - trainR2[solIdx];
311
312        // uncompress coefficients of solution
313        int selectedNin = nin[solIdx];
314        double[] coefficients;
315        double[] selectedCa = new double[nx];
316        for (int i = 0; i < nx; i++) {
317          selectedCa[i] = ca[solIdx, i];
318        }
319
320        // apply to test set to calculate test NMSE values for each lambda step
321        double[] fn;
322        Glmnet.modval(intercept[solIdx], selectedCa, ia, selectedNin, numTestObs, testX, out fn);
323        OnlineCalculatorError error;
324        var nmse = OnlineNormalizedMeanSquaredErrorCalculator.Calculate(testY, fn, out error);
325        if (error != OnlineCalculatorError.None) nmse = double.MaxValue;
326        testNMSE[solIdx] = nmse;
327
328        // uncompress coefficients
329        Glmnet.uncomp(numVars, selectedCa, ia, selectedNin, out coefficients);
330        for (int i = 0; i < coefficients.Length; i++) {
331          coeff[solIdx, i] = coefficients[i];
332        }
333      }
334    }
335
336    private static void PrepareData(IRegressionProblemData problemData, out double[,] trainX, out double[] trainY, out int numTrainObs,
337      out double[,] testX, out double[] testY, out int numTestObs, out int numVars) {
338      numVars = problemData.AllowedInputVariables.Count();
339      numTrainObs = problemData.TrainingIndices.Count();
340      numTestObs = problemData.TestIndices.Count();
341
342      trainX = new double[numVars, numTrainObs];
343      trainY = new double[numTrainObs];
344      testX = new double[numVars, numTestObs];
345      testY = new double[numTestObs];
346      var ds = problemData.Dataset;
347      var targetVar = problemData.TargetVariable;
348      // train
349      int rIdx = 0;
350      foreach (var row in problemData.TrainingIndices) {
351        int cIdx = 0;
352        foreach (var var in problemData.AllowedInputVariables) {
353          trainX[cIdx, rIdx] = ds.GetDoubleValue(var, row);
354          cIdx++;
355        }
356        trainY[rIdx] = ds.GetDoubleValue(targetVar, row);
357        rIdx++;
358      }
359      // test
360      rIdx = 0;
361      foreach (var row in problemData.TestIndices) {
362        int cIdx = 0;
363        foreach (var var in problemData.AllowedInputVariables) {
364          testX[cIdx, rIdx] = ds.GetDoubleValue(var, row);
365          cIdx++;
366        }
367        testY[rIdx] = ds.GetDoubleValue(targetVar, row);
368        rIdx++;
369      }
370    }
371
372  }
373}
Note: See TracBrowser for help on using the repository browser.