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

В этот момент, как и многие из вас, я тоже потерялся! Что, черт возьми это? Но потерпите меня. Начнем с более простых слов и примера. Представьте, что вы хотите спуститься с холма, чтобы достичь дна. Вы не знаете, как легче всего спуститься, поэтому делаете небольшие шаги и смотрите на свое окружение. После каждого шага вы проверяете, приближаетесь ли вы к дну или отдаляетесь от него. Если вы подходите ближе, вы продолжаете двигаться в том же направлении. Но если вы уходите, вы поворачиваетесь и делаете шаг в противоположном направлении.

Этот процесс небольших шагов и корректировки вашего направления — это то, чего хочет достичь градиентный спуск. В градиентном спуске вместо физического холма у нас есть математическая функция, которую мы хотим минимизировать. Функция может представлять что угодно, например стоимость производства чего-либо или ошибку в модели прогнозирования.

Цель градиентного спуска — найти входные значения функции, которые дают нам наименьшее возможное значение. Это как найти подножие математического холма. Для этого мы начинаем с некоторых начальных значений для входных данных и вычисляем градиент функции в этой точке.

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

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

Рабочий пример градиентного спуска

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

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

Скучная математика градиентного спуска

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

Что такое конвергенция?

Ситуация, при которой проигрыш (или ваше положение по отношению к нижней части холма) существенно не меняется. Другими словами, вы застряли в позиции вблизи дна.

Давайте пройдемся по градиентному спуску шаг за шагом.

  1. Получить определение функции
def func(x):
    return 2*x**2

2. Получите градиент для функции

def grad(x):
    return 4*x

3. Обновите параметр, используя скорость обучения (lr)

x_new = x_old - lr*grad(x_old)

4. Повторяйте шаги со 2 по 3, пока не будет достигнута конвергенция. Это помогает нам остановить цикл, когда либо градиент становится очень маленьким, либо количество итераций превышает 100. Их можно варьировать в зависимости от приложения.

while abs(grad(x_old)) > 0.001 and no_of_iter < 100

Теперь давайте определим полную функцию градиентного спуска:

def grad_desc(x_old, lr=0.1):
    
    #variable to count number of iterations
    no_of_iter = 0

    #variables to keep track of all values
    history_x = []
    history_func = []

    #saving first value
    history_x.append(x_old)
    history_func.append(func(x_old))

    #loop to stop at convergence
while abs(grad(x_old)) > 0.001 and no_of_iter < 100:
        
        #Use gradient to update parameters
        x_new = x_old - lr*grad(x_old)

        #save current value in history variables
        history_x.append(x_new)
        history_func.append(func(x_new))
        
        #update old variables
        x_old = x_new
    
        #increment no. of iterations
        no_of_iter += 1

    return history_x, history_func

Примечание: history_x и history_func сохраняются для отслеживания изменения параметров.

Давайте попробуем построить кривую и посмотреть, как движется градиентный спуск для достижения минимума.

#running gradient descent with initial position (4.7) 
#with learning rate of 0.1
hist_x, hist_func = grad_desc(4.7, lr=0.1)

#Plotting 2D graph
import numpy as np
import matplotlib.pyplot as plt

#getting all values of x to plot
x = np.linspace(-5, 5, 100)

#Calculating function value using x
y = [func(i) for i in x]

#plotting function
plt.plot(x, y,color='blue')

#plotting history of movement
plt.plot(hist_x, hist_func,color='red',marker='o')

plt.show()

Выход:

На что влияет скорость обучения?

Это значение, которое управляет размером шага или изменениями, которые необходимо выполнить в параметре.

Если ваш x = 2, grad(x) становится равным 8.

(i) При скорости обучения 0,1 ваше изменение становится -0,1 * 8 = -0,8.

Используя это, вы достигаете своего минимума за 20 шагов, как показано на графике.

Количество итераций: 20

Локальные минимумы происходят по адресу: 0,00017183944668295985

Значение функции в локальных минимумах: 5.90575908726116e-08

(ii) При скорости обучения 0,2 наше изменение становится -0,2 * 8 = -1,6.

Количество итераций: 7

Локальные минимумы происходят по адресу: 6.0159999999999885e-05

Значение функции в локальных минимумах: 7,238451199999973e-09

На этот раз размер вашего шага был больше; следовательно, мы достигли минимума за 7 шагов.

(iii) При скорости обучения 0,49 ваше изменение становится -0,49 * 8 = -3,92.

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

Количество итераций: 100

Локальные минимумы встречаются на: 0,0792905009865931

Значение функции в локальных минимумах: 0,012573967093409842

В этом случае мы исчерпали наши 100 итераций, но еще не достигли минимума. В двух других случаях мы достигли минимума всего за 7 или 20 шагов.

Пример с двумя переменными:

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

#define function
def func(x1,x2):
    return 2*x1**2 + (x2-1)**2

#define gradient of function with respect to first variable
def grad_x1(x1):
    return 4*x1

#define gradient of function with respect to second variable
def grad_x2(x2):
    return 2*(x2-1)

def grad_desc(x1_old,x2_old, lr=0.1):

    #variable to count number of iterations
    no_of_iter = 0

    #variables to keep track of all values 
    history_x = []
    history_func = []

    #saving initial value
    history_x.append([x1_old,x2_old])
    history_func.append(func(x1_old,x2_old))

    #loop to stop at convergence
while (abs(grad_x1(x1_old)) > 0.001 and abs(grad_x2(x2_old)) > 0.001) and no_of_iter < 100:
        
        #Use gradient to update parameters
        x1_new = x1_old - lr*grad_x1(x1_old)
        x2_new = x2_old - lr*grad_x2(x2_old)

        #save current value in history variables
        history_x.append([x1_new,x2_new])
        history_func.append(func(x1_new,x2_new))
        
        #update old variables
        x1_old = x1_new
        x2_old = x2_new
    
        #increment no. of iterations
        no_of_iter += 1

    return history_x, history_func


#running gradient descent with initial position (1.3,4.7) 
#with learning rate of 0.3
hist_x, hist_func = grad_desc(1.3,4.7, lr=0.3)


#Plotting 3D graph
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

#getting all x1 and x2 values
feature_x1 = np.arange(-5,5,0.005)
feature_x2 = np.arange(-5,5,0.005)
  
#creating a mesh using x1, x2 values
[X1, X2] = np.meshgrid(feature_x1, feature_x2)

#Calculating function value using X1,X2
Z = np.square(X1)*2 + np.square(X2-1)

#plotting history of movement
ax.plot(np.array(hist_x)[:,0],np.array(hist_x)[:,1],hist_func,marker='o',color='r',markersize=5,linewidth=3)

#plotting function
ax.plot_surface(X1, X2, Z, cmap=Pastel1, alpha=0.6)

plt.show()

Вывод:

Скорость обучения: 0,3

Количество итераций: 6

Локальные минимумы встречаются по адресу: 8.319999999999996e-05, 1.0151552000000001

Значение функции в локальных минимумах: 0,00022969393152000444

Бонус: контурный сюжет

Вы можете попробовать построить контурный график, используя следующий код Python:

Код:

import matplotlib.pyplot as plt
import numpy as np

#getting all x1 and x2 values
feature_x1 = np.arange(-8,8,0.01)
feature_x2 = np.arange(-8,8,0.01)

#creating a mesh using x1, x2 values
[X1, X2] = np.meshgrid(feature_x1, feature_x2)

#Calculating function value using X1,X2
Z = np.square(X1)*2 + np.square(X2-1)

fig, ax = plt.subplots(1, 1)
ax.contour (X1, X2, Z)
ax.plot(np.array(hist_x)[:,0],np.array(hist_x)[:,1],marker='o',color='r',markersize=5,linewidth=3)
ax.set_title('Contour Plot')
ax.set_xlabel('feature_x1')
ax.set_ylabel('feature_x2')
  
plt.show()

Выход:

Примечание. Вы можете запустить следующую команду в терминале Python, чтобы получить интерактивный график.

%matplotlib qt

Запустите следующую команду в терминале Python, чтобы вернуться к обычному (или встроенному графику).

%matplotlib inline

Вот еще один анимированный пример от Google, который вы можете попробовать.

Уменьшение потерь: оптимизация скорости обучения | Машинное обучение | Google для разработчиков

В следующем посте мы увидим различные варианты градиентного спуска.