La descente en gradient ne trouve pas de solution aux moindres carrés ordinaires sur cet ensemble de données?

12

J'ai étudié la régression linéaire et je l'ai essayée sur l'ensemble ci-dessous {(x, y)}, où x spécifiait la superficie de la maison en pieds carrés et y spécifiait le prix en dollars. Ceci est le premier exemple dans Andrew Ng Notes .

2104,400
1600,330
2400,369
1416,232
3000,540

J'ai développé un exemple de code mais quand je l'exécute, le coût augmente à chaque étape alors qu'il devrait diminuer à chaque étape. Code et sortie donnés ci-dessous. biasest W 0 X 0 , où X 0 = 1. featureWeightsest un tableau de [X 1 , X 2 , ..., X N ]

J'ai également essayé une solution en ligne de python disponible ici et expliquée ici . Mais cet exemple donne également la même sortie.

Où est l'écart dans la compréhension du concept?

Code:

package com.practice.cnn;

import java.util.Arrays;

public class LinearRegressionExample {

    private float ALPHA = 0.0001f;
    private int featureCount = 0;
    private int rowCount = 0;

    private float bias = 1.0f;
    private float[] featureWeights = null;

    private float optimumCost = Float.MAX_VALUE;

    private boolean status = true;

    private float trainingInput[][] = null;
    private float trainingOutput[] = null;

    public void train(float[][] input, float[] output) {
        if (input == null || output == null) {
            return;
        }

        if (input.length != output.length) {
            return;
        }

        if (input.length == 0) {
            return;
        }

        rowCount = input.length;
        featureCount = input[0].length;

        for (int i = 1; i < rowCount; i++) {
            if (input[i] == null) {
                return;
            }

            if (featureCount != input[i].length) {
                return;
            }
        }

        featureWeights = new float[featureCount];
        Arrays.fill(featureWeights, 1.0f);

        bias = 0;   //temp-update-1
        featureWeights[0] = 0;  //temp-update-1

        this.trainingInput = input;
        this.trainingOutput = output;

        int count = 0;
        while (true) {
            float cost = getCost();

            System.out.print("Iteration[" + (count++) + "] ==> ");
            System.out.print("bias -> " + bias);
            for (int i = 0; i < featureCount; i++) {
                System.out.print(", featureWeights[" + i + "] -> " + featureWeights[i]);
            }
            System.out.print(", cost -> " + cost);
            System.out.println();

//          if (cost > optimumCost) {
//              status = false;
//              break;
//          } else {
//              optimumCost = cost;
//          }

            optimumCost = cost;

            float newBias = bias + (ALPHA * getGradientDescent(-1));

            float[] newFeaturesWeights = new float[featureCount];
            for (int i = 0; i < featureCount; i++) {
                newFeaturesWeights[i] = featureWeights[i] + (ALPHA * getGradientDescent(i));
            }

            bias = newBias;

            for (int i = 0; i < featureCount; i++) {
                featureWeights[i] = newFeaturesWeights[i];
            }
        }
    }

    private float getCost() {
        float sum = 0;
        for (int i = 0; i < rowCount; i++) {
            float temp = bias;
            for (int j = 0; j < featureCount; j++) {
                temp += featureWeights[j] * trainingInput[i][j];
            }

            float x = (temp - trainingOutput[i]) * (temp - trainingOutput[i]);
            sum += x;
        }
        return (sum / rowCount);
    }

    private float getGradientDescent(final int index) {
        float sum = 0;
        for (int i = 0; i < rowCount; i++) {
            float temp = bias;
            for (int j = 0; j < featureCount; j++) {
                temp += featureWeights[j] * trainingInput[i][j];
            }

            float x = trainingOutput[i] - (temp);
            sum += (index == -1) ? x : (x * trainingInput[i][index]);
        }
        return ((sum * 2) / rowCount);
    }

    public static void main(String[] args) {
        float[][] input = new float[][] { { 2104 }, { 1600 }, { 2400 }, { 1416 }, { 3000 } };

        float[] output = new float[] { 400, 330, 369, 232, 540 };

        LinearRegressionExample example = new LinearRegressionExample();
        example.train(input, output);
    }
}

Production:

Iteration[0] ==> bias -> 0.0, featureWeights[0] -> 0.0, cost -> 150097.0
Iteration[1] ==> bias -> 0.07484, featureWeights[0] -> 168.14847, cost -> 1.34029099E11
Iteration[2] ==> bias -> -70.60721, featureWeights[0] -> -159417.34, cost -> 1.20725801E17
Iteration[3] ==> bias -> 67012.305, featureWeights[0] -> 1.51299168E8, cost -> 1.0874295E23
Iteration[4] ==> bias -> -6.3599688E7, featureWeights[0] -> -1.43594258E11, cost -> 9.794949E28
Iteration[5] ==> bias -> 6.036088E10, featureWeights[0] -> 1.36281745E14, cost -> 8.822738E34
Iteration[6] ==> bias -> -5.7287012E13, featureWeights[0] -> -1.29341617E17, cost -> Infinity
Iteration[7] ==> bias -> 5.4369677E16, featureWeights[0] -> 1.2275491E20, cost -> Infinity
Iteration[8] ==> bias -> -5.1600908E19, featureWeights[0] -> -1.1650362E23, cost -> Infinity
Iteration[9] ==> bias -> 4.897313E22, featureWeights[0] -> 1.1057068E26, cost -> Infinity
Iteration[10] ==> bias -> -4.6479177E25, featureWeights[0] -> -1.0493987E29, cost -> Infinity
Iteration[11] ==> bias -> 4.411223E28, featureWeights[0] -> 9.959581E31, cost -> Infinity
Iteration[12] ==> bias -> -4.186581E31, featureWeights[0] -> -Infinity, cost -> Infinity
Iteration[13] ==> bias -> Infinity, featureWeights[0] -> NaN, cost -> NaN
Iteration[14] ==> bias -> NaN, featureWeights[0] -> NaN, cost -> NaN
Amber Beriwal
la source
C'est hors sujet ici.
Michael R. Chernick
3
Si les choses explosent à l'infini comme ici, vous oubliez probablement de diviser quelque part par l'échelle du vecteur.
StasK
5
La réponse acceptée par Matthew est évidemment statistique. Cela signifie que la question nécessitait une expertise statistique (et non de programmation) pour répondre; cela en fait un sujet par définition. Je vote pour rouvrir.
amibe dit Réintégrer Monica le

Réponses:

35

La réponse courte est que la taille de votre pas est trop grande. Au lieu de descendre la paroi du canyon, votre pas est si grand que vous sautez d'un côté à l'autre plus haut!

Fonction de coût ci-dessous:

entrez la description de l'image ici

La réponse longue est qu'il est difficile pour une descente de gradient naïf de résoudre ce problème car les ensembles de niveaux de votre fonction de coût sont des ellipses très allongées plutôt que des cercles. Pour résoudre ce problème de manière robuste, notez qu'il existe des moyens plus sophistiqués de choisir:

  • une taille de pas (que le codage en dur d'une constante).
  • une direction de pas (que la descente de gradient).

Problème sous-jacent

Le problème sous-jacent est que les ensembles de niveaux de votre fonction de coût sont des ellipses très allongées, ce qui pose des problèmes de descente de gradient. La figure ci-dessous montre les ensembles de niveaux pour la fonction de coût.

  • 026.789
  • Si la taille du pas est trop grande, vous sauterez littéralement par-dessus la région bleue inférieure et monterez au lieu de descendre.
  • θ0

Je suggère de lire cette réponse sur Quora.

entrez la description de l'image ici

Solution rapide 1:

Modifiez votre code private float ALPHA = 0.0000002f;et vous arrêterez le dépassement.

Solution rapide 2:

XX

Corrections plus avancées

Si l'objectif était de résoudre efficacement les moindres carrés ordinaires plutôt que d'apprendre simplement la descente de gradient pour une classe, observez que:

  • Il existe des moyens plus sophistiqués de calculer la taille des pas, tels que la recherche de ligne et la règle Armijo .
  • Près d'une réponse où les conditions locales prévalent, la méthode de Newton obtient une convergence quadratique et est un excellent moyen de choisir une direction et une taille de pas.
  • Résoudre les moindres carrés équivaut à résoudre un système linéaire. Les algorithmes modernes n'utilisent pas de descente de gradient naïve. Au lieu:
    • k
    • Pour les grands systèmes, ils formulent un problème d'optimisation et utilisent des méthodes itératives telles que les méthodes du sous-espace de Krylov .

(XX)b=Xyb

La solution réelle est

  26.789880528523071
   0.165118878075797

Vous constaterez que ceux-ci atteignent la valeur minimale pour la fonction de coût.

Matthew Gunn
la source
5
+1 c'est du luxe de laisser d'autres personnes déboguer le code!
Haitao Du
4
@ hxd1011 Je pensais que c'était une erreur de codage stupide au début, mais à la place, cela se transforme (à mon humble avis) en un exemple assez instructif sur ce qui peut mal tourner avec une descente de gradient naïve.
Matthew Gunn
@MatthewGunn J'ai obtenu la solution b = 0,99970686, m = 0,17655967 (y = mx + b). Et qu'entendiez-vous par "une taille de pas plutôt que de coder en dur une constante"? Cela signifie-t-il que nous devrions le changer à chaque itération? ou nous devons le calculer en fonction des valeurs d'entrée?
Amber Beriwal
αiiααif
@AmberBeriwal Vous constaterez que (26.789, .1651) aura un coût légèrement inférieur. Il est légèrement en descente par rapport à (.9997, .1766) dans une direction où la fonction de coût a une toute petite pente.
Matthew Gunn
2

Comme Matthew (Gunn) l'a déjà indiqué, les contours de la fonction de coût ou de performance tridimensionnelle sont très elliptiques dans ce cas. Étant donné que votre code Java utilise une seule valeur de pas pour les calculs de descente de gradient, les mises à jour des poids (c'est-à-dire l'ordonnée à l'origine et la pente de la fonction linéaire) sont toutes deux régies par cette seule taille de pas.

En conséquence, la très petite taille de pas qui est nécessaire pour contrôler la mise à jour du poids associé au plus grand gradient (dans ce cas, la pente de la fonction linéaire) limite considérablement la vitesse à laquelle l'autre poids avec le plus petit gradient (le l'ordonnée à l'origine de la fonction linéaire) est mise à jour. Dans les conditions actuelles, ce dernier poids ne converge pas vers sa vraie valeur d'environ 26,7.

Compte tenu du temps et des efforts que vous avez investis dans l'écriture de votre code Java, je vous suggère de le modifier pour utiliser deux valeurs de taille de pas discrètes, une taille de pas appropriée pour chaque poids. Andrew Ng suggère dans ses notes qu'il est préférable d'utiliser la mise à l'échelle des fonctionnalités pour s'assurer que les contours de la fonction de coût sont de forme plus régulière (c'est-à-dire circulaires). Cependant, la modification de votre code Java pour utiliser une taille de pas différente pour chaque poids peut être un bon exercice en plus d'étudier la mise à l'échelle des fonctionnalités.

Une autre idée à considérer est de savoir comment les valeurs de poids initiales sont sélectionnées. Dans votre code Java, vous avez initialisé les deux valeurs à zéro. Il est également assez courant d'initialiser les poids à de petites valeurs fractionnaires. Dans ce cas particulier, cependant, ces deux approches ne fonctionneraient pas à la lumière des contours très elliptiques (c'est-à-dire non circulaires) de la fonction de coût tridimensionnelle. Étant donné que les poids pour ce problème peuvent être trouvés en utilisant d'autres méthodes, comme la solution pour le système linéaire suggérée par Matthew à la fin de son article, vous pouvez essayer d'initialiser les poids à des valeurs plus proches des poids corrects et voir comment votre code d'origine en utilisant une seule taille de pas converge.

Le code Python que vous avez trouvé aborde la solution de la même manière que votre code Java - les deux utilisent un seul paramètre de taille d'étape. J'ai modifié ce code Python pour utiliser une taille de pas différente pour chaque poids. Je l'ai inclus ci-dessous.

from numpy import *

def compute_error_for_line_given_points(b, m, points):
    totalError = 0
    for i in range(0, len(points)):
        x = points[i, 0]
        y = points[i, 1]
        totalError += (y - (m * x + b)) ** 2
    return totalError / float(len(points))

def step_gradient(b_current, m_current, points, learningRate_1, learningRate_2):
    b_gradient = 0
    m_gradient = 0
    N = float(len(points))
    for i in range(0, len(points)):
        x = points[i, 0]
        y = points[i, 1]
        b_gradient += -(2/N) * (y - ((m_current * x) + b_current))
        m_gradient += -(2/N) * x * (y - ((m_current * x) + b_current))
    new_b = b_current - (learningRate_1 * b_gradient)
    new_m = m_current - (learningRate_2 * m_gradient)
    return [new_b, new_m]

def gradient_descent_runner(points, starting_b, starting_m, learning_rate_1, learning_rate_2, num_iterations):
    b = starting_b
    m = starting_m
    for i in range(num_iterations):
        b, m = step_gradient(b, m, array(points), learning_rate_1, learning_rate_2)
    return [b, m]

def run():
    #points = genfromtxt("data.csv", delimiter=",")
    #learning_rate = 0.0001
    #num_iterations = 200

    points = genfromtxt("test_set.csv", delimiter=",")
    learning_rate_1 = 0.5
    learning_rate_2 = 0.0000001
    num_iterations = 1000

    initial_b = 0 # initial y-intercept guess
    initial_m = 0 # initial slope guess


    print("Starting gradient descent at b = {0}, m = {1}, error = {2}".format(initial_b, initial_m, compute_error_for_line_given_points(initial_b, initial_m, points)))
    print("Running...")

    [b, m] = gradient_descent_runner(points, initial_b, initial_m, learning_rate_1, learning_rate_2, num_iterations)

    print("After {0} iterations b = {1}, m = {2}, error = {3}".format(num_iterations, b, m, compute_error_for_line_given_points(b, m, points)))

if __name__ == '__main__':
    run()

Il s'exécute sous Python 3, ce qui nécessite les parenthèses autour de l'argument des instructions "print". Sinon, il s'exécutera sous Python 2 en supprimant les parenthèses. Vous devrez créer un fichier CSV avec les données de l'exemple d'Andrew Ng.

L'utilisation peut renvoyer le code Python pour vérifier votre code Java.

Michael_RW
la source