Java实现多元梯度下降

时间:2016-12-14 13:46:13

标签: java machine-learning gradient-descent

我正在尝试用Java实现多变量梯度下降算法(来自AI课程),我无法弄清楚代码中的错误位置。

这是以下程序的输出:

Before train: parameters := [0.0, 0.0, 0.0] -> cost function := 2.5021875E9
After first iteration: parameters := [378.5833333333333, 2.214166666666667, 50043.75000000001] -> cost function := 5.404438291015627E9

正如您所看到的,在第一次迭代之后,值就会消失。我做错了什么?

这是我想要实现的算法:

enter image description here

代码:

    import java.util.*;

    public class GradientDescent {

        private double[][] trainingData;
        private double[]   means;
        private double[]   scale;

        private double[]   parameters;
        private double     learningRate;

        GradientDescent() {
            this.learningRate = 0D;
        }

        public double predict(double[] inp){
            double[] features = new double[inp.length + 1];
            features[0] = 1;
            for(int i = 0; i < inp.length; i++) {
                features[i+1] = inp[i];
            }

            double prediction = 0;
            for(int i = 0; i < parameters.length; i++) {
                prediction = parameters[i] * features[i];
            }

            return prediction;
        }

        public void train(){
            double[] tempParameters = new double[parameters.length];
            for(int i = 0; i < parameters.length; i++) {
                tempParameters[i] = parameters[i] - learningRate * partialDerivative(i);
                //System.out.println(tempParameters[i] + " = " + parameters[i] + " - " + learningRate + " * " + partialDerivative(i));
            }

            System.out.println("Before train: parameters := " + Arrays.toString(parameters) + " -> cost function := " + costFunction());
            parameters = tempParameters;
            System.out.println("After first iteration: parameters := " + Arrays.toString(parameters) + " -> cost function := " + costFunction());
        }

        private double partialDerivative(int index) {
            double sum = 0;
            for(int i = 0; i < trainingData.length; i++) {
                double[] input = new double[trainingData[i].length - 1];
                int j = 0;
                for(; j < trainingData[i].length - 1; j++) {
                    input[j] = trainingData[i][j];
                }
                sum += ((predict(input) - trainingData[i][j]) * trainingData[i][index]);
            }

            return (1D/trainingData.length) * sum;
        }

        public double[][] getTrainingData() {
            return trainingData;
        }
        public void setTrainingData(double[][] data) {
            this.trainingData = data;
            this.means = new double[this.trainingData[0].length-1];
            this.scale = new double[this.trainingData[0].length-1];

            for(int j = 0; j < data[0].length-1; j++) {
                double min = data[0][j], max = data[0][j];
                double sum = 0;
                for(int i = 0; i < data.length; i++) {
                    if(data[i][j] < min) min = data[i][j];
                    if(data[i][j] > max) max = data[i][j];
                    sum += data[i][j];
                }
                scale[j] = max - min;
                means[j] = sum / data.length;
            }
        }   

        public double[] getParameters() {
            return parameters;
        }
        public void setParameters(double[] parameters) {
            this.parameters = parameters;
        }

        public double getLearningRate() {
            return learningRate;
        }
        public void setLearningRate(double learningRate) {
            this.learningRate = learningRate;
        }

        /**              1      m           i     i  2
        *   J(theta) = ----- * SUM( h     (x ) - y  )
        *               2*m    i=1   theta
        */  
        public double costFunction() {
            double sum = 0;

            for(int i = 0; i < trainingData.length; i++) {
                double[] input = new double[trainingData[i].length - 1];
                int j = 0;
                for(; j < trainingData[i].length - 1; j++) {
                    input[j] = trainingData[i][j];
                }
                sum += Math.pow(predict(input) - trainingData[i][j], 2);
            }

            double factor = 1D/(2*trainingData.length);
            return factor * sum;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("hypothesis: ");
            int i = 0;
            sb.append(parameters[i++] + " + ");
            for(; i < parameters.length-1; i++) {
                sb.append(parameters[i] + "*x" + i + " + ");
            }
            sb.append(parameters[i] + "*x" + i);

            sb.append("\n Feature scale: ");
            for(i = 0; i < scale.length-1; i++) {
                sb.append(scale[i] + " ");
            }
            sb.append(scale[i]);

            sb.append("\n Feature means: ");
            for(i = 0; i < means.length-1; i++) {
                sb.append(means[i] + " ");
            }
            sb.append(means[i]);

            sb.append("\n Cost fuction: " + costFunction());

            return sb.toString();
        }

        public static void main(String[] args) {

            final double[][] TDATA = {
                {200, 2, 20000},
                {300, 2, 41000},
                {400, 3, 51000},
                {500, 3, 61500},
                {800, 4, 41000},
                {900, 5, 141000}
            };

            GradientDescent gd = new GradientDescent();
            gd.setTrainingData(TDATA);
            gd.setParameters(new double[]{0D,0D,0D});
            gd.setLearningRate(0.00001);
            gd.train();
            //System.out.println(gd);
            //System.out.println("PREDICTION: " + gd.predict(new double[]{300, 2}));
        }
    }

编辑:

我已更新代码以使其更具可读性,并尝试将其映射到道格拉斯使用的符号。我认为它现在工作得更好,但仍然有一些我不太了解的阴暗区域。

似乎如果我有多个参数(如下面的示例,房间数和面积),预测与第二个参数(在这种情况下是区域)密切相关,并且它没有太大的影响第一个参数(房间数)。

以下是{2, 200}的预测:

PREDICTION: 200000.00686158828

以下是{5, 200}的预测:

PREDICTION: 200003.0068315415

正如您所看到的,两个值之间几乎没有任何区别。

我尝试将数学转换为代码时仍然存在错误吗?

以下是更新后的代码:

import java.util.*;

public class GradientDescent {

    private double[][] trainingData;
    private double[]   means;
    private double[]   scale;

    private double[]   parameters;
    private double     learningRate;

    GradientDescent() {
        this.learningRate = 0D;
    }

    public double predict(double[] inp) {
        return predict(inp, this.parameters);
    }
    private double predict(double[] inp, double[] parameters){
        double[] features = concatenate(new double[]{1}, inp);

        double prediction = 0;
        for(int j = 0; j < features.length; j++) {
            prediction += parameters[j] * features[j];
        }

        return prediction;
    }

    public void train(){
        readjustLearningRate();

        double costFunctionDelta = Math.abs(costFunction() - costFunction(iterateGradient()));

        while(costFunctionDelta > 0.0000000001) {
            System.out.println("Old cost function : " + costFunction());
            System.out.println("New cost function : " + costFunction(iterateGradient()));
            System.out.println("Delta: " + costFunctionDelta);

            parameters = iterateGradient();
            costFunctionDelta = Math.abs(costFunction() - costFunction(iterateGradient()));
            readjustLearningRate();
        }
    }

    private double[] iterateGradient() {
        double[] nextParameters = new double[parameters.length];
        // Calculate parameters for the next iteration
        for(int r = 0; r < parameters.length; r++) {
            nextParameters[r] = parameters[r] - learningRate * partialDerivative(r);
        }

        return nextParameters;
    }
    private double partialDerivative(int index) {
        double sum = 0;
        for(int i = 0; i < trainingData.length; i++) {
            int indexOfResult = trainingData[i].length - 1;
            double[] input = Arrays.copyOfRange(trainingData[i], 0, indexOfResult);
            sum += ((predict(input) - trainingData[i][indexOfResult]) * trainingData[i][index]);
        }

        return sum/trainingData.length ;
    }
    private void readjustLearningRate() {

        while(costFunction(iterateGradient()) > costFunction()) {           
            // If the cost function of the new parameters is higher that the current cost function
            // it means the gradient is diverging and we have to adjust the learning rate
            // and recalculate new parameters
            System.out.print("Learning rate: " + learningRate + " is too big, readjusted to: ");
            learningRate = learningRate/2;
            System.out.println(learningRate);
        }
        // otherwise we are taking small enough steps, we have the right learning rate
    }

    public double[][] getTrainingData() {
        return trainingData;
    }
    public void setTrainingData(double[][] data) {
        this.trainingData = data;
        this.means = new double[this.trainingData[0].length-1];
        this.scale = new double[this.trainingData[0].length-1];

        for(int j = 0; j < data[0].length-1; j++) {
            double min = data[0][j], max = data[0][j];
            double sum = 0;
            for(int i = 0; i < data.length; i++) {
                if(data[i][j] < min) min = data[i][j];
                if(data[i][j] > max) max = data[i][j];
                sum += data[i][j];
            }
            scale[j] = max - min;
            means[j] = sum / data.length;
        }
    }   

    public double[] getParameters() {
        return parameters;
    }
    public void setParameters(double[] parameters) {
        this.parameters = parameters;
    }

    public double getLearningRate() {
        return learningRate;
    }
    public void setLearningRate(double learningRate) {
        this.learningRate = learningRate;
    }

    /**              1      m           i     i  2
    *   J(theta) = ----- * SUM( h     (x ) - y  )
    *               2*m    i=1   theta
    */  
    public double costFunction() {
        return costFunction(this.parameters);
    }
    private double costFunction(double[] parameters) {
        int m = trainingData.length;
        double sum = 0;

        for(int i = 0; i < m; i++) {
            int indexOfResult = trainingData[i].length - 1;
            double[] input = Arrays.copyOfRange(trainingData[i], 0, indexOfResult);
            sum += Math.pow(predict(input, parameters) - trainingData[i][indexOfResult], 2);
        }

        double factor = 1D/(2*m);
        return factor * sum;
    }

    private double[] normalize(double[] input) {
        double[] normalized = new double[input.length];
        for(int i = 0; i < input.length; i++) {
            normalized[i] = (input[i] - means[i]) / scale[i];
        }

        return normalized;
    }

    private double[] concatenate(double[] a, double[] b) {
        int size = a.length + b.length;

        double[] concatArray = new double[size];
        int index = 0;

        for(double d : a) {
            concatArray[index++] = d;
        }
        for(double d : b) {
            concatArray[index++] = d;
        }

        return concatArray;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("hypothesis: ");
        int i = 0;
        sb.append(parameters[i++] + " + ");
        for(; i < parameters.length-1; i++) {
            sb.append(parameters[i] + "*x" + i + " + ");
        }
        sb.append(parameters[i] + "*x" + i);

        sb.append("\n Feature scale: ");
        for(i = 0; i < scale.length-1; i++) {
            sb.append(scale[i] + " ");
        }
        sb.append(scale[i]);

        sb.append("\n Feature means: ");
        for(i = 0; i < means.length-1; i++) {
            sb.append(means[i] + " ");
        }
        sb.append(means[i]);

        sb.append("\n Cost fuction: " + costFunction());

        return sb.toString();
    }

    public static void main(String[] args) {

        final double[][] TDATA = {
            //number of rooms, area, price
            {2, 200, 200000},
            {3, 300, 300000},
            {4, 400, 400000},
            {5, 500, 500000},
            {8, 800, 800000},
            {9, 900, 900000}
        };

        GradientDescent gd = new GradientDescent();
        gd.setTrainingData(TDATA);
        gd.setParameters(new double[]{0D, 0D, 0D});
        gd.setLearningRate(0.1);
        gd.train();
        System.out.println(gd);
        System.out.println("PREDICTION: " + gd.predict(new double[]{3, 600}));
    }
}

2 个答案:

答案 0 :(得分:3)

似乎你有一个合理的开始,但在将数学转换为代码时存在一些问题。请参阅以下数学。

The math

我采取了一些步骤来澄清数学和算法的收敛机制。

  1. 为了提高易读性,而不是使用括号上标来表示行,在符号中使用了更标准的逗号分隔下标。
  2. 尝试使用零基数来求和控制变量以匹配Java / C索引约定,而不会在数学中引入错误。 (希望正确完成。)
  3. 进行了课程材料所暗示的各种替换。
  4. 确定已过帐代码中的变量名称与数学表示之间的映射。
  5. 之后,在求和循环中出现了比缺失加号更多的错误。偏导数似乎需要重写或重大修改以匹配课程概念。

    注意k = 0-> n的内部循环在所有特征上产生点积,然后在i = 0-> m-1循环内应用以解释每个训练案例。

    所有这些必须包含在每个迭代r中。该外循环的循环标准不应该是某个最大r值。一旦收敛足够完成,您将需要满足一些标准。

    回复评论的附加说明:

    由于马丁福勒称之为语义差距,因此很难发现代码的不一致性。在这种情况下,它介于三件事之间。

    1. 数学表示
    2. 讲座术语
    3. 代码中的算法
    4. 重构成员变量并从x矩阵中断开y向量(如下所示)可能有助于发现不协调。

      private int countMExamples;  
      private int countNFeatures;  
      private double[][] aX;  
      private double[] aY;  
      private double[] aMeans;  
      private double[] aScales;  
      private double[] aParamsTheta;  
      private double learnRate;
      

答案 1 :(得分:0)

在计算预测时,您错过了+。您应该使用

将权重*输入值相加

prediction += parameters[i] * features[i];

作为产生你正在使用的偏导数的激活函数是h θ i x i θ i

此外,您似乎需要降低学习率才能使培训功能收敛。

-

我不知道课程的内容,所以我不确定你是否期望具体的结果,但我认为现在的问题在于你的训练数据。

您的训练数据无法区分输入对结果的影响程度,因为两种输入相互成比例增加,从而产生按比例增加的结果。我建议提供一个变量更大,两个输入之间依赖性更小的数据集。

如果您正在努力获取要使用的数据,可以尝试使用UCI housing data set.(实际上,您可以更好地使用下面提供的数据)。

-

我运行了您的新代码并解决了以下问题,效果很好。您的更新引入了两个关键缺陷,即动态学习率和培训终止案例。

动态学习率是一种有效的方法,但是创建适当降低学习率的功能可能是困难的。您当前的方法会过快地降低学习速度,从而使其减小到一个值,使得算法过早停止时,您的权重会发生微不足道的变化。目前,保持不变的学习率并手动调整它。

对于您的训练终止案例,要求在成本函数中进行如此小的更改,因为您的条件将导致算法过度训练您的训练数据。结果将是该算法在训练数据上表现良好,但在基于训练数据特有的细节预测的任何新事物上表现不佳。

我建议大大降低要求,或者更好地实现一个训练验证循环。每次迭代都会在不同的验证集上测试参数,并根据该性能终止。

另外,对于简单的梯度下降算法,我对输入数据的上述建议很差。住房数据集不是线性可分的,因此梯度下降算法(优化线性函数h θ)只会使预测通常接近平均值。

相反,您应该使用可线性分离的数据,例如UCI Iris data set。解决了上述两个问题后,您的算法可以准确地处理这些数据。

最后,我同意道格拉斯的观点,你应该考虑重写你的算法,以便更清楚。虽然当前的实现有效,但如果您的代码简洁有序,它将有助于您的学习过程。您正在使用不具有描述性的变量名称,混合过程和OOP方法,以及随意封装方法。