package de.jungblut.online.regression; import java.util.List; import java.util.Random; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.commons.math3.random.RandomDataImpl; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import de.jungblut.math.DoubleVector; import de.jungblut.math.MathUtils; import de.jungblut.math.activation.ActivationFunction; import de.jungblut.math.activation.LinearActivationFunction; import de.jungblut.math.activation.SigmoidActivationFunction; import de.jungblut.math.activation.StepActivationFunction; import de.jungblut.math.dense.DenseDoubleVector; import de.jungblut.math.dense.SingleEntryDoubleVector; import de.jungblut.math.loss.HingeLoss; import de.jungblut.math.loss.LogLoss; import de.jungblut.math.loss.LossFunction; import de.jungblut.math.loss.StepLoss; import de.jungblut.math.minimize.CostGradientTuple; import de.jungblut.online.minimizer.StochasticGradientDescent; import de.jungblut.online.minimizer.StochasticGradientDescent.StochasticGradientDescentBuilder; import de.jungblut.online.ml.FeatureOutcomePair; import de.jungblut.online.regularization.GradientDescentUpdater; import de.jungblut.online.regularization.L1Regularizer; import de.jungblut.online.regularization.L2Regularizer; import de.jungblut.online.regularization.WeightUpdater; public class TestRegressionLearner { private RandomDataImpl rnd; @Before public void setup() { rnd = new RandomDataImpl(); rnd.reSeed(0); } @Test public void gradCheck() { RegressionLearner learner = new RegressionLearner( StochasticGradientDescentBuilder.create(0.1).build(), new SigmoidActivationFunction(), new LogLoss()); learner.setRandom(new Random(0)); // for both classes for (int i = 0; i <= 1; i++) { gridGradCheck(learner, i, new GradientDescentUpdater()); } } @Test public void ridgeGradCheck() { RegressionLearner learner = new RegressionLearner( StochasticGradientDescentBuilder.create(0.1).build(), new SigmoidActivationFunction(), new LogLoss()); learner.setRandom(new Random(0)); // for both classes for (int i = 0; i <= 1; i++) { gridGradCheck(learner, i, new L2Regularizer(1d)); } } public void gridGradCheck(RegressionLearner learner, int clz, WeightUpdater updater) { // 0.1 steps between zero and 1 for (double d = 0.0; d <= 1.0; d += 0.1) { DoubleVector weights = new DenseDoubleVector(new double[] { d, d }); DoubleVector nextFeature = new DenseDoubleVector(new double[] { 1, 10 }); DoubleVector nextOutcome = new DenseDoubleVector(new double[] { clz }); DoubleVector numGrad = MathUtils.numericalGradient( weights, (x) -> { CostGradientTuple tmpGrad = learner.observeExample( new FeatureOutcomePair(nextFeature, nextOutcome), x); CostGradientTuple tmpUpdatedGradient = updater.updateGradient(x, tmpGrad.getGradient(), 1d, 0, tmpGrad.getCost()); return new CostGradientTuple(tmpUpdatedGradient.getCost(), null); }); CostGradientTuple realGrad = learner.observeExample( new FeatureOutcomePair(nextFeature, nextOutcome), weights); // we compute the new weights (to test regularization gradients) CostGradientTuple updatedGradient = updater.updateGradient(weights, realGrad.getGradient(), 1d, 0, realGrad.getCost()); Assert.assertArrayEquals(numGrad.toArray(), updatedGradient.getGradient() .toArray(), 1e-4); } } @Test public void testSimpleLogisticRegression() { List<FeatureOutcomePair> data = generateData(); RegressionLearner learner = newLearner(); RegressionModel model = learner.train(() -> data.stream()); double acc = computeClassificationAccuracy(generateData(), model); Assert.assertEquals(1d, acc, 0.1); } @Test public void testPerceptron() { List<FeatureOutcomePair> data = generateData(); RegressionLearner learner = newRegularizedLearner( new GradientDescentUpdater(), new StepActivationFunction(0.5), new StepLoss()); RegressionModel model = learner.train(() -> data.stream()); double acc = computeClassificationAccuracy(generateData(), model); Assert.assertEquals(1d, acc, 0.1); } @Test public void testLinearSVM() { // hinge loss needs -1 vs. 1 as outcome double negativeOutputClass = -1; List<FeatureOutcomePair> data = generateData(negativeOutputClass); RegressionLearner learner = newSVMLearner(); RegressionModel model = learner.train(() -> data.stream()); // since SVM's output the distance from the hyperplane to the given feature, // we clip the values between -1 and 1. // a common technique used to get probabilities is using Platt scaling, // which trains a logistic regression on top of the SVM output. Function<DoubleVector, DoubleVector> clipping = (v) -> new SingleEntryDoubleVector( v.get(0) > 0 ? 1 : negativeOutputClass); double acc = computeClassificationAccuracy( generateData(negativeOutputClass), model, clipping); Assert.assertEquals(1d, acc, 0.1); } @Test public void testRidgeLogisticRegression() { List<FeatureOutcomePair> data = generateData(); RegressionLearner learner = newRegularizedLearner(new L2Regularizer(1d)); RegressionModel model = learner.train(() -> data.stream()); double acc = computeClassificationAccuracy(generateData(), model); Assert.assertEquals(1d, acc, 0.1); } @Test public void testLassoLogisticRegression() { List<FeatureOutcomePair> data = generateDataAddedNoise(2); RegressionLearner learner = newRegularizedLearner(new L1Regularizer(1d)); RegressionModel model = learner.train(() -> data.stream()); // basically the l1 norm should set the noise to zero Assert.assertArrayEquals(new double[] { 0.0, 0.0 }, model.getWeights() .sliceByLength(3, 2).toArray(), 1e-2); double acc = computeClassificationAccuracy(generateDataAddedNoise(2), model); Assert.assertEquals(1d, acc, 0.1); } @Test public void testParallelLogisticRegression() { List<FeatureOutcomePair> data = generateData(); RegressionLearner learner = newLearner(); // the parallel one converges usually to a different version, because there // is no defined order of updates, thus we only assert the accuracy. RegressionModel model = learner.train(() -> data.stream().parallel()); double acc = computeClassificationAccuracy(generateData(), model); Assert.assertEquals(1d, acc, 0.1); } public RegressionLearner newLearner() { return newRegularizedLearner(new GradientDescentUpdater()); } public RegressionLearner newRegularizedLearner(WeightUpdater updater) { return newRegularizedLearner(updater, new SigmoidActivationFunction(), new LogLoss()); } public RegressionLearner newRegularizedLearner(WeightUpdater updater, ActivationFunction activationFunction, LossFunction loss) { StochasticGradientDescentBuilder builder = StochasticGradientDescentBuilder .create(0.1); if (updater != null) { builder = builder.weightUpdater(updater); } StochasticGradientDescent min = builder.build(); RegressionLearner learner = new RegressionLearner(min, activationFunction, loss); learner.setRandom(new Random(1337)); learner.setNumPasses(25); return learner; } public RegressionLearner newSVMLearner() { StochasticGradientDescentBuilder builder = StochasticGradientDescentBuilder .create(0.1); StochasticGradientDescent min = builder.build(); RegressionLearner learner = new RegressionLearner(min, new LinearActivationFunction(), new HingeLoss()); learner.setRandom(new Random(1337)); learner.setNumPasses(5); return learner; } public double computeClassificationAccuracy(List<FeatureOutcomePair> data, RegressionModel model) { return computeClassificationAccuracy(data, model, (v) -> v); } public double computeClassificationAccuracy(List<FeatureOutcomePair> data, RegressionModel model, Function<DoubleVector, DoubleVector> clippingFunction) { double correct = 0; RegressionClassifier clf = new RegressionClassifier(model); for (FeatureOutcomePair pair : data) { DoubleVector prediction = clippingFunction.apply(clf.predict(pair .getFeature())); if (prediction.subtract(pair.getOutcome()).abs().sum() < 0.1d) { correct++; } } return correct / data.size(); } public List<FeatureOutcomePair> generateDataAddedNoise(int noiseFeatures) { return generateData() .stream() .map( (featOut) -> { DoubleVector oldFeature = featOut.getFeature(); DoubleVector feat = new DenseDoubleVector(oldFeature .getDimension() + noiseFeatures); for (int i = 0; i < oldFeature.getDimension(); i++) { feat.set(i, oldFeature.get(i)); } Random random = new Random(1337); for (int i = 0; i < noiseFeatures; i++) { feat.set(i + oldFeature.getDimension(), random.nextDouble()); } return new FeatureOutcomePair(feat, featOut.getOutcome()); }).collect(Collectors.toList()); } public List<FeatureOutcomePair> generateData() { return generateData(0); } public List<FeatureOutcomePair> generateData(double negativeClasValue) { return IntStream .range(1, 2000) .mapToObj( (i) -> { double mean = i % 2 == 0 ? 25d : 75d; double stddev = 10d; double clzVal = i % 2 == 0 ? negativeClasValue : 1d; double[] feat = new double[] { 1, rnd.nextGaussian(mean, stddev), rnd.nextGaussian(mean, stddev) }; return new FeatureOutcomePair(new DenseDoubleVector(feat), new SingleEntryDoubleVector(clzVal)); }).collect(Collectors.toList()); } }