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

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

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

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

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

  1. Краткий обзор того, что делает каждый алгоритм.
  2. Код для алгоритма
  3. Дополнительные пояснения к непонятным частям кода

Мы будем использовать знаменитый набор данных о жилье в Бостоне, который предварительно встроен в scikit-learn. Мы также будем строить линейную модель с нуля, так что будьте готовы, потому что вы собираетесь войти в совершенно новый мир!

Итак, сначала давайте сделаем базовый импорт (обычные вещи). Я не буду здесь заниматься EDA, потому что это не совсем цель нашей статьи. Тем не менее, я покажу несколько визуализаций, чтобы прояснить некоторые моменты.

import numpy as np
import pandas as pd 
import plotly.express as px
from sklearn.datasets import load_boston
from sklearn.metrics import mean_squared_error

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

data = load_boston()
df = pd.DataFrame(data['data'],columns=data['feature_names'])
df.insert(13,'target',data['target'])
df.head(5)

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

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

X,y = df.drop('target',axis=1),df['target']
thetas = np.zeros(X.shape[1])

Функция стоимости

Напомним, что функция стоимости — это то, что измеряет производительность вашей модели, и это то, что Gradient Descent стремится улучшить. Функция стоимости, которую мы будем использовать, известна как MSE или среднеквадратическая ошибка. Формула примерно такая:

Хорошо, давайте закодируем это:

def cost_function(X,Y,B):
    predictions = np.dot(X,B.T)
    
    cost = (1/len(Y)) * np.sum((predictions - Y) ** 2)
    return cost

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

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

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

mean_squared_error(np.dot(X,thetas.T),y)
OUT: 592.14691169960474
cost_function(X,y,thetas)
OUT: 592.14691169960474

Фантастика, наша функция стоимости работает!

Масштабирование функций

Масштабирование признаков — это метод предварительной обработки, необходимый для линейной модели (линейная регрессия, KNN, SVM). По сути, функции уменьшены до меньшего масштаба, и функции также находятся в определенном диапазоне. Думайте о масштабировании функций так:

  1. У вас очень большое здание
  2. Вы хотите сохранить форму здания, но хотите уменьшить его масштаб.

Масштабирование функций обычно используется в следующих сценариях:

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

Хотя существует множество различных методов масштабирования функций, мы создадим собственную реализацию MinMaxScaler, используя формулу:

Мы будем использовать масштабирование по причинам, изложенным выше.

Теперь для реализации Python:

X_norm = (X - X.min()) / (X.max() - X.min())
X = X_norm

Здесь нет ничего особенного, мы просто переводим формулу в код. Теперь шоу действительно начинается: Градиентный спуск!

Градиентный спуск

Конкретно, градиентный спуск — это алгоритм оптимизации, который стремится найти минимум функции (в нашем случае MSE), итеративно просматривая данные и получая частную производную.

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

Теперь Gradient Descent поставляется в разных версиях, но чаще всего вы сталкиваетесь со следующими:

  1. Пакетный градиентный спуск
  2. Стохастический градиентный спуск
  3. Мини-пакетный градиентный спуск

Теперь мы обсудим, реализуем и проанализируем каждый из них в указанном порядке, так что давайте начнем!

Пакетный градиентный спуск

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

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

Наш набор данных довольно мал, поэтому мы можем реализовать пакетный градиентный спуск следующим образом:

def batch_gradient_descent(X,Y,theta,alpha,iters):
    cost_history = [0] * iters  # initalize our cost history list
    for i in range(iters):         
        prediction = np.dot(X,theta.T)                  
        theta = theta - (alpha/len(Y)) * np.dot(prediction - Y,X)   
        cost_history[i] = cost_function(X,Y,theta)               
    return theta,cost_history

Чтобы прояснить некоторую терминологию:

альфа: относится к скорости обучения.

iters: количество выполняемых итераций.

Отлично, а теперь давайте посмотрим на результаты!

batch_theta,batch_history=batch_gradient_descent(X,y,theta,0.05,500)

Ладно, не супербыстро, но и не так уж медленно. Давайте визуализируем и получим наши затраты с нашими новыми и улучшенными параметрами:

cost_function(X,y,batch_theta)
OUT: 27.537447130784262

Ничего себе, с 592 до 27! Это всего лишь проблеск силы градиентного спуска! Давайте визуализируем функцию стоимости по количеству итераций:

fig = px.line(batch_history,x=range(5000),y=batch_history,labels={'x':'no. of iterations','y':'cost function'})
fig.show()

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

Итак, чтобы завершить пакетный градиент:

Плюсы

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

Минусы

  1. Может быть медленным на больших наборах данных
  2. Вычислительно дорого

Стохастический градиентный спуск

Здесь вместо вычисления частной производной для всей обучающей выборки вычисление частной производной выполняется только для одной случайной выборки (стохастической означает случайной).

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

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

Расписание обучения

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

По сути, это постепенно снижает скорость обучения. Таким образом, скорость обучения изначально велика (это помогает избежать локального минимума) и постепенно уменьшается по мере приближения к глобальному минимуму. Однако вы должны быть осторожны:

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

Теперь мы реализуем стохастический градиентный спуск с базовым графиком обучения:

t0,t1 = 5,50 # learning schedule hyperparameters
def learning_schedule(t):
    return t0/(t+t1)
def stochastic_gradient_descent(X,y,thetas,n_epochs=30):
    c_hist = [0] * n_epochs # Cost history list
    for epoch in range(n_epochs):
        for i in range(len(y)):
            random_index = np.random.randint(len(Y))
            xi = X[random_index:random_index+1]
            yi = y[random_index:random_index+1]
            
            prediction = xi.dot(thetas)
            
            gradient = 2 * xi.T.dot(prediction-yi)
            eta = learning_schedule(epoch * len(Y) + i)
            thetas = thetas - eta * gradient
            c_hist[epoch] = cost_function(xi,yi,thetas)
    return thetas,c_hist

Теперь запустим нашу функцию:

sdg_thetas,sgd_cost_hist = stochastic_gradient_descent(X,Y,theta)

Хорошо, отлично, значит работает! Теперь давайте посмотрим на результаты:

cost_function(X,y,sdg_thetas)
OUT:
29.833230764634493

Вау! Мы перешли от 592 к 29, но обратите внимание: мы сделали только 30 итераций. С пакетным градиентным спуском мы получили 27 после 500 итераций! Это всего лишь проблеск необычайной силы стохастического градиентного спуска.

Давайте снова визуализируем это с помощью линейного графика:

Поскольку это небольшой набор данных, пакетного градиентного спуска будет достаточно, однако это просто показывает силу стохастического градиентного спуска.

Итак, завершая стохастический градиентный спуск:

Плюсы:

  1. Быстрее по сравнению с пакетным градиентным спуском
  2. Лучше работать с большими наборами данных

Минусы:

  1. Могут возникнуть трудности с установлением определенного минимума
  2. Не всегда имеет четкий путь и может прыгать вокруг минимума, но никогда не достигает оптимального минимума.

Мини-пакетный градиентный спуск

Хорошо, почти готово, осталось пройти еще один! Теперь в мини-пакетном градиентном спуске вместо вычисления частных производных для всей обучающей выборки или случайного примера мы вычисляем их для небольших подмножеств полной обучающей выборки.

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

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

np.random.seed(42) # so we get equal results
t0, t1 = 200, 1000
def learning_schedule(t):
    return t0 / (t + t1)
def mini_batch_gradient_descent(X,y,thetas,n_iters=100,batch_size=20):
    t = 0
    c_hist = [0] * n_iters
    for epoch in range(n_iters):
        shuffled_indices = np.random.permutation(len(y))
        X_shuffled = X_scaled[shuffled_indices]
        y_shuffled = y[shuffled_indices]
        
        for i in range(0,len(Y),batch_size):
            t+=1
            xi = X_shuffled[i:i+batch_size]
            yi = y_shuffled[i:i+batch_size]
            
            gradient = 2/batch_size * xi.T.dot(xi.dot(thetas) - yi)
            eta = learning_schedule(t)
            thetas = thetas - eta * gradient
            c_hist[epoch] = cost_function(xi,yi,thetas)
    return thetas,c_hist

Запускаем и получаем результаты:

mini_batch_gd_thetas,mini_batch_gd_cost = mini_batch_gradient_descent(X,y,theta)

И функция стоимости с нашими новыми параметрами:

cost_function(X,Y,mini_batch_gd_thetas)
OUT: 27.509689139167012

Опять же действительно удивительно. Мы выполнили 1/5 итераций, которые мы сделали для пакетного градиентного спуска, и получили лучший результат! К слову об эффективности!

Давайте снова нарисуем функцию:

Итак, на этом я завершаю серию статей о градиентном спуске! Я действительно надеюсь, что вам понравилось, и не стесняйтесь задавать любые вопросы или просить любые разъяснения!

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