Спуск градієнта не знаходить рішення для цього найменшого простору на цьому наборі даних?


12

Я вивчав лінійну регресію і спробував її на нижньому наборі {(x, y)}, де x вказав площу будинку в квадратних футах, а y вказав ціну в доларах. Це перший приклад у Ендрю Нґ Нотатки .

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
Якщо речі вибухають до нескінченності, як це робиться тут, ви, мабуть, забули десь поділитися за шкалою вектора.
Стаск

5
Прийнята відповідь Метью очевидно є статистичною. Це означає, що на запитання потрібні статистичні (а не програмні) знання для відповіді; це робить це на тему за визначенням. Я голосую за повторне відкриття.
амеба каже: Відновити Моніку

Відповіді:


35

Коротка відповідь - ваш розмір кроку занадто великий. Замість спуску стіни каньйону, ваш крок настільки великий , що ви стрибки через дорогу від однієї сторони до більш на іншому!

Функція витрат нижче:

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

Довга відповідь полягає в тому, що для наївного градієнтного спуску важко вирішити цю проблему, оскільки набори рівнів вашої функції витрат - це сильно витягнуті еліпси, а не кола. Щоб надійно вирішити цю проблему, зауважте, що є більш складні способи вибору:

  • розмір кроку (ніж жорстке кодування константа).
  • напрямок кроку (ніж спуск градієнта).

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

Основна проблема полягає в тому, що набори рівнів вашої функції витрат - це сильно витягнуті еліпси, і це спричиняє проблеми для градієнтного спуску. На рисунку нижче представлені набори рівня для функції витрат.

  • При високоеліптичних наборах рівня напрямок найкрутішого спуску ледь може відповідати напрямку розчину. Наприклад, у цій проблемі термін перехоплення (те, що ви називаєте, "ухил") повинен пройти велику відстань (від до вздовж підлоги каньйону), але це стосується іншої функції, коли часткова похідна має набагато більше схил.026.789
  • Якщо розмір кроку занадто великий, ви буквально перестрибнете через нижню синю область і підніметесь замість спуску.
  • Але якщо ви зменшите розмір кроку, ваш прогрес у до потрібного значення стає болісно повільним.θ0

Я пропоную прочитати цю відповідь на Quora.

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

Швидке виправлення 1:

Змініть свій код на, private float ALPHA = 0.0000002f;і ви припините зайве усунення.

Швидке виправлення 2:

Якщо ви масштабуєте свої дані X до 2.104, 1.600 тощо, ваші набори рівня стають сферичними, а градієнтний спуск швидко зближується з більш високою швидкістю навчання. Це знижує номер умови дизайнерської матриці .XX

Більш вдосконалені виправлення

Якщо метою було ефективно вирішити звичайні найменші квадрати, а не просто вивчити спуск градієнта для класу, зауважте, що:

  • Існують більш складні способи обчислення розміру кроку, такі як пошук рядків та правило Armijo .
  • Біля відповіді, де переважають місцеві умови, метод Ньютона отримує квадратичну конвергенцію і є прекрасним способом вибору напрямку та розміру кроку.
  • Розв’язування найменших квадратів рівносильно розв’язуванню лінійної системи. Сучасні алгоритми не використовують наївні градієнтні спуски. Замість цього:
    • Для невеликих систем ( порядку декількох тисяч або менше) вони використовують щось на зразок QR-розкладання з частковим поворотом.k
    • Для великих систем вони формулюють це проблема оптимізації та використовують ітеративні методи, такі як методи підпростору Крилова .

Зауважте, що існує багато пакетів, які вирішать лінійну систему(XX)b=Xy для і ви можете перевірити результати алгоритму спуску градієнта проти цього.b

Актуальне рішення є

  26.789880528523071
   0.165118878075797

Ви побачите, що ті досягають мінімального значення для функції витрат.


5
+1 - це розкіш, щоб дозволити іншим налагоджувати код!
Haitao Du

4
@ hxd1011 Я подумав, що спочатку це була німа помилка кодування, але натомість це виявляється (imho) досить повчальним прикладом того, що може піти не так з наївним градієнтом.
Меттью Ганн

@MatthewGunn Я отримав рішення b = 0,99970686, m = 0,17655967 (y = mx + b). І що ти мав на увазі під "розміром кроку, ніж жорстким кодуванням константи"? Чи означає це, що ми повинні змінювати її для кожної ітерації? чи нам потрібно обчислити його на основі вхідних значень?
Бурштиновий перенос

@ Amber Beriwal Так, у вас буде специфічна для ітерації . Питання в тому, як далеко пройти в напрямку негативного градієнта? Проста стратегія (як те, що ви робите) - мати твердо кодоване значення для (у вас було .0001). Дещо складнішим є пошук рядків та / або правило Armijo . Ідея пошуку рядків полягає у виборі для мінімізації . Виберіть напрямок (наприклад, градієнт), а потім зробіть пошук лінії, щоб знайти найнижчу точку вздовж лінії. i α α i fα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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.