Градиентный спуск не находит решения для обычных наименьших квадратов в этом наборе данных?

12

Я изучал линейную регрессию и попробовал ее на приведенном ниже множестве {(x, y)}, где x указал площадь дома в квадратных футах, а y - цену в долларах. Это первый пример в Andrew Ng Notes .

2104.400
1600.330
2400.369
1416.232
3000.540

Я разработал пример кода, но когда я его запускаю, стоимость увеличивается с каждым шагом, тогда как она должна уменьшаться с каждым шагом. Код и вывод приведены ниже. biasявляется W 0 X 0 , где X 0 = 1. featureWeightsявляется массивом [X 1 , X 2 , ..., X N ]

Я также попробовал онлайн-решение Python, доступное здесь , и объяснил здесь . Но этот пример также дает тот же результат.

Где пробел в понимании концепции?

Код:

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

Выход:

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
Янтарь беривал
источник
Это не по теме здесь.
Майкл Р. Черник
3
Если вещи взрываются до бесконечности, как здесь, вы, вероятно, забываете делить на масштаб вектора где-нибудь.
StasK
5
Принятый ответ от Матфея, очевидно, статистический. Это означает, что для ответа на вопрос требовалась статистическая (а не программная) экспертиза; это делает это по теме по определению. Я голосую, чтобы открыть.
говорит амеба: восстанови Монику

Ответы:

35

Короткий ответ: размер шага слишком велик. Вместо спуска стены каньона, ваш шаг настолько велик , что вы прыжки через дорогу от одной стороны к более на другом!

Функция стоимости ниже:

введите описание изображения здесь

Длинный ответ состоит в том, что наивному градиентному спуску трудно решить эту проблему, потому что наборы уровней вашей функции стоимости представляют собой сильно вытянутые эллипсы, а не круги. Чтобы надежно решить эту проблему, обратите внимание, что есть более сложные способы выбора:

  • размер шага (чем жесткое кодирование константы).
  • направление шага (чем градиентный спуск).

Основная проблема

Основная проблема заключается в том, что наборы уровней вашей функции стоимости представляют собой сильно вытянутые эллипсы, и это вызывает проблемы для градиентного спуска. На приведенном ниже рисунке показаны наборы уровней для функции стоимости.

  • С высоко наборами эллиптическим уровня, направление градиентного спуска может едва совпадать с направлением раствора. Например, в этой задаче термин «перехват» (то, что вы называете «смещением») должен пройти большое расстояние (от до вдоль дна каньона), но это для другой особенности, где частная производная имеет гораздо больший склон.026.789
  • Если размер шага слишком велик, вы будете буквально перепрыгивать через нижнюю синюю область и подниматься вместо спуска.
  • НО, если вы уменьшите размер шага, ваш прогресс в получении до правильного значения станет мучительно медленным.θ0

Я предлагаю прочитать этот ответ на Quora.

введите описание изображения здесь

Быстрое исправление 1:

Измените свой код на, private float ALPHA = 0.0000002f;и вы перестанете выходить за пределы.

Быстрое исправление 2:

Если вы масштабируете свои данные X до 2.104, 1.600 и т. Д. ... ваши уровни становятся сферическими, и градиентное снижение быстро сходится с более высокой скоростью обучения. Это снижает число условий вашей матрицы дизайна .XX

Более сложные исправления

Если целью было эффективное решение обычных наименьших квадратов, а не просто изучение градиентного спуска для класса, обратите внимание на то, что:

  • Существуют более сложные способы вычисления размера шага, такие как поиск строки и правило Армихо .
  • Рядом с ответом, где преобладают местные условия, метод Ньютона получает квадратичную сходимость и является отличным способом выбора направления и размера шага.
  • Решение наименьших квадратов эквивалентно решению линейной системы. Современные алгоритмы не используют наивный градиентный спуск. Вместо:
    • Для небольших систем (k порядка нескольких тысяч или менее) они используют что-то вроде QR-разложения с частичным поворотом.
    • Для больших систем они формулируют это как проблему оптимизации и используют итерационные методы, такие как методы подпространства Крылова .

(XX)b=Xyb

Фактическое решение

  26.789880528523071
   0.165118878075797

Вы обнаружите, что они достигают минимального значения для функции стоимости.

Мэтью Ганн
источник
5
+1 это роскошь позволять другим людям отлаживать код!
Haitao Du
4
@ hxd1011 Сначала я думал, что это глупая ошибка кодирования, но вместо этого она (imho) становится весьма поучительным примером того, что может пойти не так с наивным градиентным спуском.
Мэтью Ганн
@ MatthewGunn Я получил решение b = 0,99970686, m = 0,17655967 (y = mx + b). И что вы имели в виду под «размером шага, чем жесткое кодирование константы»? Значит ли это, что мы должны менять его для каждой итерации? или нам нужно рассчитать его на основе входных значений?
Янтарь Беривал
αiiααif
@AmberBeriwal Вы обнаружите, что (26.789, .1651) будет стоить немного дешевле. Это немного вниз от (.9997, .1766) в направлении, где функция стоимости имеет небольшой уклон.
Мэтью Ганн
2

Как уже указывал Мэтью (Ганн), в этом случае контуры трехмерной функции стоимости или производительности являются очень эллиптическими. Так как ваш код Java использует одно значение шага размера для градиентных расчетов по родовым, обновлениям для весов (то есть ось у перехватывать и наклон линейной функции) являются как регулируются этим одностадийного размером.

В результате, очень маленький размер шага, необходимый для управления обновлением веса, связанного с большим градиентом (в данном случае наклон линейной функции), резко ограничивает, насколько быстро другой вес с меньшим градиентом ( пересечение оси Y линейной функции). В текущих условиях последний вес не сходится к своему истинному значению приблизительно 26,7.

Учитывая время и усилия, которые вы вложили в написание своего Java-кода, я бы предложил изменить его, чтобы использовать два дискретных значения размера шага, соответствующий размер шага для каждого веса. Эндрю Нг в своих заметках предполагает, что лучше использовать масштабирование объектов, чтобы гарантировать, что контуры функции стоимости имеют более регулярную (т.е. круговую) форму. Тем не менее, изменение вашего Java-кода для использования разного размера шага для каждого веса может быть хорошим упражнением в дополнение к рассмотрению масштабирования функций.

Другая идея, которую стоит рассмотреть, - это как выбрать начальные значения веса. В вашем коде Java вы инициализировали оба значения нулем. Также довольно часто инициализировать веса для небольших дробных значений. Однако в этом конкретном случае оба эти подхода не будут работать в свете сильно эллиптических (то есть некруглых) контуров трехмерной функции стоимости. Учитывая, что веса для этой проблемы можно найти с помощью других методов, таких как решение для линейной системы, предложенное Мэтью в конце его поста, вы можете попытаться инициализировать веса для значений, близких к правильным весам, и посмотреть, как ваш исходный код с помощью одного шага размер сходится.

Код Python, который вы нашли, подходит к решению так же, как ваш код Java - оба используют один параметр размера шага. Я изменил этот код Python, чтобы использовать разные размеры шага для каждого веса. Я включил это ниже.

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

Он работает под Python 3, который требует скобки вокруг аргумента для операторов "print". В противном случае он будет работать под Python 2, удалив скобки. Вам нужно будет создать CSV-файл с данными из примера Эндрю Нга.

Использование может дать перекрестную ссылку на код Python, чтобы проверить ваш код Java.

michael_rw
источник