La discesa gradiente non trova soluzione ai minimi quadrati ordinari in questo set di dati?


12

Ho studiato la regressione lineare e l'ho provato sul set sottostante ((x, y)}, dove x ha specificato l'area della casa in piedi quadrati e y ha specificato il prezzo in dollari. Questo è il primo esempio in Andrew Ng Notes .

2104.400
1600.330
2400.369
1416.232
3000.540

Ho sviluppato un codice di esempio, ma quando lo eseguo, il costo aumenta con ogni passaggio mentre dovrebbe diminuire con ogni passaggio. Codice e output forniti di seguito. biasè W 0 X 0 , dove X 0 = 1. featureWeightsè un array di [X 1 , X 2 , ..., X N ]

Ho anche provato una soluzione Python online disponibile qui e spiegata qui . Ma questo esempio fornisce anche lo stesso output.

Dov'è il divario nella comprensione del concetto?

Codice:

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);
    }
}

Produzione:

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

Questo è fuori tema qui.
Michael R. Chernick,

3
Se le cose esplodono all'infinito come fanno qui, probabilmente stai dimenticando di dividere da qualche parte la scala del vettore.
StasK,

5
La risposta accettata da Matthew è ovviamente statistica. Ciò significa che la domanda richiede competenze statistiche (e non di programmazione) per rispondere; lo rende in argomento per definizione. Voto per riaprire.
ameba dice di reintegrare Monica il

Risposte:


35

La risposta breve è che la dimensione del tuo passo è troppo grande. Invece di scendere dalla parete del canyon, il tuo passo è così grande che stai saltando da una parte all'altra più in alto!

Funzione di costo di seguito:

inserisci qui la descrizione dell'immagine

La lunga risposta è che è difficile per una discesa con gradiente ingenuo risolvere questo problema perché i set di livelli della funzione di costo sono ellissi altamente allungate piuttosto che cerchi. Per risolvere in modo efficace questo problema, tenere presente che esistono modi più sofisticati per scegliere:

  • una dimensione del passo (rispetto all'hardcoding di una costante).
  • una direzione del passo (rispetto alla discesa del gradiente).

Problema di fondo

Il problema di fondo è che gli insiemi di livelli della funzione di costo sono ellissi altamente allungate e ciò causa problemi di discesa del gradiente. La figura seguente mostra i set di livelli per la funzione di costo.

  • 026.789
  • Se la dimensione del gradino è troppo grande, salterai letteralmente sopra la regione blu inferiore e salirai invece di scendere.
  • θ0

Suggerisco di leggere questa risposta su Quora.

inserisci qui la descrizione dell'immagine

Correzione rapida 1:

Cambia il tuo codice in private float ALPHA = 0.0000002f;e smetterai di eccedere.

Correzione rapida 2:

XX

Correzioni più avanzate

Se l'obiettivo era risolvere efficacemente i minimi quadrati ordinari anziché semplicemente imparare la discesa gradiente per una classe, osservare che:

  • Esistono modi più sofisticati per calcolare la dimensione del passo, come la ricerca di linee e la regola di Armijo .
  • Vicino a una risposta in cui prevalgono le condizioni locali, il metodo di Newton ottiene la convergenza quadratica ed è un ottimo modo per scegliere una direzione e una dimensione del passo.
  • Risolvere i minimi quadrati equivale a risolvere un sistema lineare. Gli algoritmi moderni non usano l'ingenua discesa del gradiente. Anziché:
    • k
    • Per i sistemi di grandi dimensioni, formulano un problema di ottimizzazione e utilizzano metodi iterativi come i metodi del sottospazio di Krylov .

(XX)b=Xyb

La vera soluzione è

  26.789880528523071
   0.165118878075797

Scoprirai che raggiungono il valore minimo per la funzione di costo.


5
+1 è un lusso consentire ad altre persone di eseguire il debug del codice!
Haitao Du,

4
@ hxd1011 All'inizio ho pensato che fosse un errore di codifica stupido, ma invece (imho) si trasforma in un esempio abbastanza istruttivo su cosa può andare storto con una discesa gradiente ingenua.
Matthew Gunn,

@MatthewGunn Ho ottenuto la soluzione b = 0.99970686, m = 0.17655967 (y = mx + b). E cosa intendevi con "una dimensione del passo rispetto alla codifica costante di una costante"? Significa che dovremmo cambiarlo per ogni iterazione? o dobbiamo calcolarlo in base ai valori di input?
Amber Beriwal,

αiiααif

@AmberBeriwal Scoprirai che (26.789, .1651) avrà un costo leggermente inferiore. È leggermente in discesa da (.9997, .1766) in una direzione in cui la funzione di costo ha una leggera pendenza.
Matthew Gunn,

2

Come già indicato da Matthew (Gunn), in questo caso i contorni del costo tridimensionale o della funzione di prestazione sono estremamente ellittici. Poiché il codice Java utilizza un singolo valore di dimensione del passo per i calcoli della discesa del gradiente, gli aggiornamenti ai pesi (ovvero l'intercettazione dell'asse y e la pendenza della funzione lineare) sono entrambi regolati da questo singolo passo.

Di conseguenza, la dimensione del gradino molto piccola necessaria per controllare l'aggiornamento del peso associato al gradiente più grande (in questo caso, la pendenza della funzione lineare) limita drasticamente la velocità dell'altro peso con il gradiente più piccolo (il l'intercetta sull'asse y della funzione lineare) viene aggiornata. Nelle condizioni attuali, quest'ultimo peso non converge al suo valore reale di circa 26,7.

Dato il tempo e lo sforzo che hai investito nella scrittura del tuo codice Java, suggerirei di modificarlo per utilizzare due valori discreti di dimensione del passo, una dimensione del passo appropriata per ogni peso. Andrew Ng suggerisce nelle sue note che è meglio usare il ridimensionamento delle caratteristiche per garantire che i contorni della funzione di costo abbiano una forma più regolare (cioè circolare). Tuttavia, la modifica del codice Java in modo da utilizzare una dimensione del passo diversa per ciascun peso potrebbe essere un buon esercizio oltre a considerare il ridimensionamento delle funzionalità.

Un'altra idea da considerare è come vengono scelti i valori di peso iniziali. Nel tuo codice Java hai inizializzato entrambi i valori a zero. È anche abbastanza comune inizializzare i pesi su valori piccoli e frazionari. In questo caso particolare, tuttavia, entrambi questi approcci non funzionerebbero alla luce dei contorni altamente ellittici (cioè non circolari) della funzione di costo tridimensionale. Dati i pesi per questo problema possono essere trovati usando altri metodi, come la soluzione per il sistema lineare suggerita da Matthew alla fine del suo post, potresti provare a inizializzare i pesi a valori più vicini ai pesi corretti e vedere come il tuo codice originale utilizzando una convergenza di un singolo passo.

Il codice Python che hai trovato si avvicina alla soluzione allo stesso modo del tuo codice Java: entrambi utilizzano un singolo parametro di dimensione del passo. Ho modificato questo codice Python per utilizzare una dimensione del gradino diversa per ciascun peso. L'ho incluso di seguito.

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()

Funziona con Python 3, che richiede le parentesi attorno all'argomento per le istruzioni "print". Altrimenti verrà eseguito in Python 2 rimuovendo le parentesi. Dovrai creare un file CSV con i dati dell'esempio di Andrew Ng.

Usa può fare un riferimento incrociato al codice Python per controllare il tuo codice Java.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.