Free cookie consent management tool by TermsFeed Policy Generator

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

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

#745:

  • using a line chart (IndexedDataTable) instead of a scatter plot
  • added number of variables (non-zero coefficients) to the line chart
File size: 17.6 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 DataTable("Coefficient Paths", "The paths of standarized coefficient values over different lambda values");
138      var nLambdas = lambda.Length;
139      var nCoeff = coeff.GetLength(1);
140      var dataRows = new DataRow[nCoeff];
141      var allowedVars = Problem.ProblemData.AllowedInputVariables.ToArray();
142      var numNonZeroCoeffs = new int[nLambdas];
143      for (int i = 0; i < nCoeff; i++) {
144        var coeffId = allowedVars[i];
145        double sigma = Problem.ProblemData.Dataset.GetDoubleValues(coeffId).StandardDeviation();
146        var path = Enumerable.Range(0, nLambdas).Select(r => coeff[r, i] * sigma).ToArray();
147        dataRows[i] = new DataRow(coeffId, coeffId, path);
148        coeffTable.Rows.Add(dataRows[i]);
149      }
150      for (int i = 0; i < coeff.GetLength(0); i++) {
151        for (int j = 0; j < coeff.GetLength(1); j++) {
152          if (!coeff[i, j].IsAlmost(0.0)) {
153            numNonZeroCoeffs[i]++;
154          }
155        }
156      }
157
158      Results.Add(new Result(coeffTable.Name, coeffTable.Description, coeffTable));
159
160      var nmsePlot = new IndexedDataTable<double>("NMSE", "Path of NMSE values over different lambda values");
161      nmsePlot.VisualProperties.YAxisMaximumAuto = false;
162      nmsePlot.VisualProperties.YAxisMinimumAuto = false;
163      nmsePlot.VisualProperties.XAxisMaximumAuto = false;
164      nmsePlot.VisualProperties.XAxisMinimumAuto = false;
165
166      nmsePlot.VisualProperties.YAxisMinimumFixedValue = 0;
167      nmsePlot.VisualProperties.YAxisMaximumFixedValue = 1.0;
168      nmsePlot.VisualProperties.XAxisLogScale = true;
169      nmsePlot.VisualProperties.XAxisTitle = "Log10(Lambda)";
170      nmsePlot.VisualProperties.YAxisTitle = "Normalized mean of squared errors (NMSE)";
171      nmsePlot.VisualProperties.SecondYAxisTitle= "Number of variables";
172      nmsePlot.Rows.Add(new IndexedDataRow<double>("NMSE (train)", "Path of NMSE values over different lambda values", lambda.Zip(trainNMSE, (l, v) => Tuple.Create(l, v))));
173      nmsePlot.Rows.Add(new IndexedDataRow<double>("NMSE (test)", "Path of NMSE values over different lambda values", lambda.Zip(testNMSE, (l, v) => Tuple.Create(l, v))));
174      nmsePlot.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))));
175      if (lambda.Length > 2) {
176        nmsePlot.VisualProperties.XAxisMinimumFixedValue = Math.Pow(10, Math.Floor(Math.Log10(lambda.Last())));
177        nmsePlot.VisualProperties.XAxisMaximumFixedValue = Math.Pow(10, Math.Ceiling(Math.Log10(lambda.Skip(1).First())));
178      }
179      nmsePlot.Rows["NMSE (train)"].VisualProperties.ChartType = DataRowVisualProperties.DataRowChartType.Points;
180      nmsePlot.Rows["NMSE (test)"].VisualProperties.ChartType = DataRowVisualProperties.DataRowChartType.Points;
181      nmsePlot.Rows["Number of variables"].VisualProperties.ChartType = DataRowVisualProperties.DataRowChartType.Points;
182      nmsePlot.Rows["Number of variables"].VisualProperties.SecondYAxis = true;
183
184
185      Results.Add(new Result(nmsePlot.Name, nmsePlot.Description, nmsePlot));
186    }
187
188    public static double[] CreateElasticNetLinearRegressionSolution(IRegressionProblemData problemData, double penalty, double lambda,
189            out double trainNMSE, out double testNMSE,
190            double coeffLowerBound = double.NegativeInfinity, double coeffUpperBound = double.PositiveInfinity) {
191      double[] trainNMSEs;
192      double[] testNMSEs;
193      // run for exactly one lambda
194      var coeffs = CreateElasticNetLinearRegressionSolution(problemData, penalty, new double[] { lambda }, out trainNMSEs, out testNMSEs, coeffLowerBound, coeffUpperBound);
195      trainNMSE = trainNMSEs[0];
196      testNMSE = testNMSEs[0];
197      return coeffs[0];
198    }
199    public static double[][] CreateElasticNetLinearRegressionSolution(IRegressionProblemData problemData, double penalty, double[] lambda,
200            out double[] trainNMSEs, out double[] testNMSEs,
201            double coeffLowerBound = double.NegativeInfinity, double coeffUpperBound = double.PositiveInfinity,
202            int maxVars = -1) {
203      // run for multiple user-supplied lambdas
204      double[,] coeff;
205      double[] intercept;
206      RunElasticNetLinearRegression(problemData, penalty, lambda.Length, 1.0, lambda, out lambda, out trainNMSEs, out testNMSEs, out coeff, out intercept, coeffLowerBound, coeffUpperBound, maxVars);
207
208      int nRows = intercept.Length;
209      int nCols = coeff.GetLength(1) + 1;
210      double[][] sols = new double[nRows][];
211      for (int solIdx = 0; solIdx < nRows; solIdx++) {
212        sols[solIdx] = new double[nCols];
213        for (int cIdx = 0; cIdx < nCols - 1; cIdx++) {
214          sols[solIdx][cIdx] = coeff[solIdx, cIdx];
215        }
216        sols[solIdx][nCols - 1] = intercept[solIdx];
217      }
218      return sols;
219    }
220
221    public static void RunElasticNetLinearRegression(IRegressionProblemData problemData, double penalty,
222      out double[] lambda, out double[] trainNMSE, out double[] testNMSE, out double[,] coeff, out double[] intercept,
223      double coeffLowerBound = double.NegativeInfinity, double coeffUpperBound = double.PositiveInfinity,
224      int maxVars = -1
225      ) {
226      double[] userLambda = new double[0];
227      // automatically determine lambda values (maximum 100 different lambda values)
228      RunElasticNetLinearRegression(problemData, penalty, 100, 0.0, userLambda, out lambda, out trainNMSE, out testNMSE, out coeff, out intercept, coeffLowerBound, coeffUpperBound, maxVars);
229    }
230
231    /// <summary>
232    /// Elastic net with squared-error-loss for dense predictor matrix, runs the full path of all lambdas
233    /// </summary>
234    /// <param name="problemData">Predictor target matrix x and target vector y</param>
235    /// <param name="penalty">Penalty for balance between ridge (0.0) and lasso (1.0) regression</param>
236    /// <param name="nlam">Maximum number of lambda values (default 100)</param>
237    /// <param name="flmin">User control of lambda values (&lt;1.0 => minimum lambda = flmin * (largest lambda value), >= 1.0 => use supplied lambda values</param>
238    /// <param name="ulam">User supplied lambda values</param>
239    /// <param name="lambda">Output lambda values</param>
240    /// <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>
241    /// <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>
242    /// <param name="coeff">Vector of coefficient vectors for each solution along the path</param>
243    /// <param name="intercept">Vector of intercepts for each solution along the path</param>
244    /// <param name="coeffLowerBound">Optional lower bound for all coefficients</param>
245    /// <param name="coeffUpperBound">Optional upper bound for all coefficients</param>
246    /// <param name="maxVars">Maximum allowed number of variables in each solution along the path (-1 => all variables are allowed)</param>
247    private static void RunElasticNetLinearRegression(IRegressionProblemData problemData, double penalty,
248  int nlam, double flmin, double[] ulam, out double[] lambda, out double[] trainNMSE, out double[] testNMSE, out double[,] coeff, out double[] intercept,
249  double coeffLowerBound = double.NegativeInfinity, double coeffUpperBound = double.PositiveInfinity,
250  int maxVars = -1
251  ) {
252      if (penalty < 0.0 || penalty > 1.0) throw new ArgumentException("0 <= penalty <= 1", "penalty");
253
254      double[,] trainX;
255      double[,] testX;
256
257      double[] trainY;
258      double[] testY;
259      int numTrainObs, numTestObs;
260      int numVars;
261      PrepareData(problemData, out trainX, out trainY, out numTrainObs, out testX, out testY, out numTestObs, out numVars);
262
263      int ka = 1; // => covariance updating algorithm
264      double parm = penalty;
265      double[] w = Enumerable.Repeat(1.0, numTrainObs).ToArray(); // all observations have the same weight
266      int[] jd = new int[1]; // do not force to use any of the variables
267      double[] vp = Enumerable.Repeat(1.0, numVars).ToArray(); // all predictor variables are unpenalized
268      double[,] cl = new double[numVars, 2]; // use the same bounds for all coefficients
269      for (int i = 0; i < numVars; i++) {
270        cl[i, 0] = coeffLowerBound;
271        cl[i, 1] = coeffUpperBound;
272      }
273
274      int ne = maxVars > 0 ? maxVars : numVars;
275      int nx = numVars;
276      double thr = 1.0e-5; // default value as recommended in glmnet
277      int isd = 1; //  => regression on standardized predictor variables
278      int intr = 1;  // => do include intercept in model
279      int maxit = 100000; // default value as recommended in glmnet
280      // outputs
281      int lmu = -1;
282      double[,] ca;
283      int[] ia;
284      int[] nin;
285      int nlp = -99;
286      int jerr = -99;
287      double[] trainR2;
288      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);
289
290      trainNMSE = new double[lmu]; // elnet returns R**2 as 1 - NMSE
291      testNMSE = new double[lmu];
292      coeff = new double[lmu, numVars];
293      for (int solIdx = 0; solIdx < lmu; solIdx++) {
294        trainNMSE[solIdx] = 1.0 - trainR2[solIdx];
295
296        // uncompress coefficients of solution
297        int selectedNin = nin[solIdx];
298        double[] coefficients;
299        double[] selectedCa = new double[nx];
300        for (int i = 0; i < nx; i++) {
301          selectedCa[i] = ca[solIdx, i];
302        }
303
304        // apply to test set to calculate test NMSE values for each lambda step
305        double[] fn;
306        Glmnet.modval(intercept[solIdx], selectedCa, ia, selectedNin, numTestObs, testX, out fn);
307        OnlineCalculatorError error;
308        var nmse = OnlineNormalizedMeanSquaredErrorCalculator.Calculate(testY, fn, out error);
309        if (error != OnlineCalculatorError.None) nmse = double.MaxValue;
310        testNMSE[solIdx] = nmse;
311
312        // uncompress coefficients
313        Glmnet.uncomp(numVars, selectedCa, ia, selectedNin, out coefficients);
314        for (int i = 0; i < coefficients.Length; i++) {
315          coeff[solIdx, i] = coefficients[i];
316        }
317      }
318    }
319
320    private static void PrepareData(IRegressionProblemData problemData, out double[,] trainX, out double[] trainY, out int numTrainObs,
321      out double[,] testX, out double[] testY, out int numTestObs, out int numVars) {
322      numVars = problemData.AllowedInputVariables.Count();
323      numTrainObs = problemData.TrainingIndices.Count();
324      numTestObs = problemData.TestIndices.Count();
325
326      trainX = new double[numVars, numTrainObs];
327      trainY = new double[numTrainObs];
328      testX = new double[numVars, numTestObs];
329      testY = new double[numTestObs];
330      var ds = problemData.Dataset;
331      var targetVar = problemData.TargetVariable;
332      // train
333      int rIdx = 0;
334      foreach (var row in problemData.TrainingIndices) {
335        int cIdx = 0;
336        foreach (var var in problemData.AllowedInputVariables) {
337          trainX[cIdx, rIdx] = ds.GetDoubleValue(var, row);
338          cIdx++;
339        }
340        trainY[rIdx] = ds.GetDoubleValue(targetVar, row);
341        rIdx++;
342      }
343      // test
344      rIdx = 0;
345      foreach (var row in problemData.TestIndices) {
346        int cIdx = 0;
347        foreach (var var in problemData.AllowedInputVariables) {
348          testX[cIdx, rIdx] = ds.GetDoubleValue(var, row);
349          cIdx++;
350        }
351        testY[rIdx] = ds.GetDoubleValue(targetVar, row);
352        rIdx++;
353      }
354    }
355
356  }
357}
Note: See TracBrowser for help on using the repository browser.