¿El descenso de gradiente no encuentra solución a los mínimos cuadrados ordinarios en este conjunto de datos?

12

He estado estudiando la regresión lineal y la probé en el siguiente conjunto {(x, y)}, donde x especificó el área de la casa en pies cuadrados e y especificó el precio en dólares. Este es el primer ejemplo en Andrew Ng Notes .

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

Desarrollé un código de muestra, pero cuando lo ejecuto, el costo aumenta con cada paso, mientras que debería disminuir con cada paso. Código y salida a continuación. biases W 0 X 0 , donde X 0 = 1. featureWeightses una matriz de [X 1 , X 2 , ..., X N ]

También probé una solución de Python en línea disponible aquí , y lo expliqué aquí . Pero este ejemplo también está dando el mismo resultado.

¿Dónde está la brecha en la comprensión del concepto?

Código:

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

Salida:

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
fuente
Esto está fuera de tema aquí.
Michael R. Chernick
3
Si las cosas explotan hasta el infinito como lo hacen aquí, probablemente estés olvidando dividir por la escala del vector en alguna parte.
StasK
55
La respuesta aceptada por Matthew es obviamente estadística. Esto significa que la pregunta requería experiencia estadística (y no de programación) para responder; lo hace sobre el tema por definición. Yo voto para reabrir.
ameba dice Reinstate Monica

Respuestas:

35

La respuesta corta es que el tamaño de tu paso es demasiado grande. En lugar de descender por la pared del cañón, su paso es tan grande que usted está saltando a través de un lado a mayor en el otro!

Función de costo a continuación:

ingrese la descripción de la imagen aquí

La respuesta larga es que es difícil para un descenso de gradiente ingenuo resolver este problema porque los conjuntos de niveles de su función de costo son elipses altamente alargadas en lugar de círculos. Para resolver este problema de manera sólida, tenga en cuenta que hay formas más sofisticadas de elegir:

  • un tamaño de paso (que codificar una constante).
  • una dirección de paso (que el descenso en gradiente).

Problema subyacente

El problema subyacente es que los conjuntos de niveles de su función de costo son elipses altamente alargadas, y esto causa problemas para el descenso del gradiente. La siguiente figura muestra conjuntos de niveles para la función de costo.

  • 026.789
  • Si el tamaño del escalón es demasiado grande, literalmente saltará sobre la región azul inferior y ascenderá en lugar de descender.
  • θ0

Sugiero leer esta respuesta en Quora.

ingrese la descripción de la imagen aquí

Solución rápida 1:

Cambie su código a private float ALPHA = 0.0000002f;y dejará de sobrepasar.

Solución rápida 2:

XX

Soluciones más avanzadas

Si el objetivo fuera resolver eficientemente los mínimos cuadrados ordinarios en lugar de simplemente aprender el descenso de gradiente para una clase, observe que:

  • Hay formas más sofisticadas de calcular el tamaño del paso, como la búsqueda de línea y la regla Armijo .
  • Cerca de una respuesta donde prevalecen las condiciones locales, el método de Newton obtiene convergencia cuadrática y es una excelente manera de elegir la dirección y el tamaño del paso.
  • Resolver mínimos cuadrados es equivalente a resolver un sistema lineal. Los algoritmos modernos no utilizan el descenso de gradiente ingenuo. En lugar:
    • k
    • Para sistemas grandes, formulan que es un problema de optimización y usan métodos iterativos como los métodos de subespacio de Krylov .

(XX)b=Xyb

La solución real es

  26.789880528523071
   0.165118878075797

Encontrará que esos alcanzan el valor mínimo para la función de costo.

Matthew Gunn
fuente
55
¡+1 es un lujo dejar que otras personas depuren el código!
Haitao Du
44
@ hxd1011 Al principio pensé que era un error de codificación tonto, pero en cambio se convierte (imho) en un ejemplo bastante instructivo sobre lo que puede salir mal con un descenso de gradiente ingenuo.
Matthew Gunn el
@MatthewGunn Obtuve la solución b = 0.99970686, m = 0.17655967 (y = mx + b). ¿Y qué querías decir con "un tamaño de paso que codificar una constante"? ¿Eso significa que deberíamos cambiarlo para cada iteración? o tenemos que calcularlo en función de los valores de entrada?
Amber Beriwal
αiiααif
@AmberBeriwal Encontrará que (26.789, .1651) tendrá un costo ligeramente menor. Está ligeramente cuesta abajo desde (.9997, .1766) en una dirección donde la función de costo tiene una pequeña pendiente.
Matthew Gunn el
2

Como Matthew (Gunn) ya ha indicado, los contornos de la función de costo o rendimiento tridimensional son altamente elípticos en este caso. Desde su código Java utiliza un valor único paso de tamaño para los cálculos de descenso de gradiente, los cambios a los pesos (es decir, la intersección del eje Y y la pendiente de la función lineal) están ambos gobernados por esta única etapa de tamaño.

Como resultado, el tamaño de paso muy pequeño que se requiere para controlar la actualización del peso asociado con el gradiente más grande (en este caso, la pendiente de la función lineal) limita drásticamente la velocidad del otro peso con el gradiente más pequeño (el la intersección del eje y de la función lineal) se actualiza. En las condiciones actuales, el último peso no converge a su verdadero valor de aproximadamente 26.7.

Dado el tiempo y el esfuerzo que ha invertido en escribir su código Java, sugeriría modificarlo para usar dos valores discretos de tamaño de paso, un tamaño de paso apropiado para cada peso. Andrew Ng sugiere en sus notas que es mejor usar el escalado de características para garantizar que los contornos de la función de costo sean más regulares (es decir, circulares) en forma. Sin embargo, modificar su código Java para usar un tamaño de paso diferente para cada peso podría ser un buen ejercicio además de considerar el escalado de características.

Otra idea a considerar es cómo se seleccionan los valores de peso iniciales. En su código Java inicializó ambos valores a cero. También es bastante común inicializar los pesos en valores pequeños y fraccionarios. En este caso particular, sin embargo, ambos enfoques no funcionarían a la luz de los contornos altamente elípticos (es decir, no circulares) de la función de costo tridimensional. Dado que los pesos para este problema se pueden encontrar utilizando otros métodos, como la solución para el sistema lineal sugerido por Matthew al final de su publicación, podría intentar inicializar los pesos a valores más cercanos a los pesos correctos y ver cómo su código original utilizando un solo paso de tamaño converge.

El código Python que encontró se acerca a la solución de la misma manera que su código Java: ambos usan un único parámetro de tamaño de paso. Modifiqué este código de Python para usar un tamaño de paso diferente para cada peso. Lo he incluido a continuación.

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

Se ejecuta bajo Python 3, que requiere los paréntesis alrededor del argumento para las declaraciones "print". De lo contrario, se ejecutará en Python 2 eliminando los paréntesis. Deberá crear un archivo CSV con los datos del ejemplo de Andrew Ng.

El uso puede hacer una referencia cruzada del código Python para verificar su código Java.

Michael_RW
fuente