Вы когда-нибудь представляли себе, какую важную роль «оптимизация» играет в науке о данных? Что ж, взаимодействие между оптимизацией и машинным обучением — одно из самых полезных и мощных достижений в современной компьютерной науке и неотъемлемая часть этой области. Фактически, машинное обучение в наши дни может даже создавать новые методы оптимизации! Круто, правда?

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

Градиентный спуск на сегодняшний день является самым популярным и наиболее часто используемым алгоритмом для создания оптимизированных моделей в машинном и глубоком обучении. Самое приятное то, что его можно комбинировать практически с любым алгоритмом! Его можно расширить для решения как простых, так и сложных задач машинного обучения. Существуют расширения градиентного спуска, такие как ADAM, RMSPROP и т. д., которые используются для глубоких нейронных сетей, но их легко понять, если знать основы градиентного спуска.

Градиентный спуск работает с простой концепцией уклона. Наклон линии — это просто «число», которое определяет направление, а также крутизну линии. То же самое относится к наклону любой функции.

Если y = f(x), то наклон = скорость изменения y / скорость изменения x.

Также с математической точки зрения Δy/ Δx = tan θ, который представляет собой угол, образуемый линией с горизонтальной осью.

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

Мы просто используем «частные производные»! Градиент скалярной функции многих переменных f(x, y, ..) обозначается как ∇f, упаковывает всю информацию о частных производных в вектор:

f=[ ∂f/∂x, ∂f/∂y, …. ]

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

В линейной регрессии мы оптимизируем Intercept & Slope..

В логистической регрессии мы оптимизируем функцию сжатия под названием «сигмоид»…

В T-SNE мы оптимизируем кластеры… и т.д..

Gradient Descent гордится всеми этими оптимизациями!

Давайте посмотрим, как это можно комбинировать с логистической регрессией вместе с математикой, стоящей за ней…

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

Функция стоимости для логистической регрессии в бинарной классификации:

Теперь давайте погрузимся в код.

import numpy as np
import pandas as pd
import random
from sklearn.datasets import make_classification
import seaborn as sns
import matplotlib.pyplot as plt
from tqdm import tqdm
from sklearn.model_selection import train_test_split

Здесь я только что импортировал кучу библиотек.

X, y = make_classification(n_samples=50000,n_features=15, n_informative=10, n_redundant= 5,n_classes=2,weights=[0.7],class_sep=0.7,random_state=15)
#for proper np array multiplication in further calculations
y = y[:, np.newaxis]
#Split the data into train and test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=15)
X_train.shape, y_train.shape, X_test.shape, y_test.shape

Создайте случайный набор данных, используя sklearn формы (50000,15), и разделите его на наборы данных для обучения и тестирования.

Прогнозируемое значение рассчитывается с помощью сигмовидной функции:

Таким образом, это необходимо заменить в функции Loss, упомянутой ранее, вместо y_pred для каждого входа x. Следовательно, «потери» модели становятся функцией, зависящей от весового вектора переменных параметров и члена смещения, а также от констант, которые являются входными данными поезда и целевыми значениями.

#To calculate predictions
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
#To compute loss
def compute_log_loss(X, y, w_vec, inter):
   N = len(X)
   cost = 0
   for i in range(len(X)):
       y_pred = (sigmoid((X[i] @ w_vec) + inter))
       cost += ((y[i] * np.log(y_pred)) + ((1-y[i]) * (np.log(1-       y_pred))))     
   cost = (-1/N) * cost
   return cost

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

#SGD implementation
def gradient_descent(X, y, w_vec, learning_rate, epochs, intercept, hyper_param, batch_size):
    r = learning_rate
    e = epochs
    b = intercept
    lambda_ = hyper_param
    N = len(X)
    loss_history_train = []
    loss_history_test = []
    for i in tqdm(range(e)):
       for j in (np.arange(0, X.shape[0], batch_size)):
           X_i = X[j : j+batch_size]
           y_i = y[j : j+batch_size]
           y_pred = sigmoid(np.dot(X_i,w_vec) + b)
           loss = y_i - y_pred
           w_grad = (r * (X_i.T.dot(loss)))
           b_grad = (r * loss)
           w_vec = ((1 - ((r*lambda_)/N))* w_vec) + w_grad
           b = b + b_grad
       epochloss_train = 0
       epochloss_train= (compute_log_loss(X_train,y_train,w_vec,b))
       loss_history_train.append(epochloss_train)
       epochloss_test = 0
       epochloss_test = (compute_log_loss(X_test, y_test, w_vec, b))
       loss_history_test.append(epochloss_test)
   return (loss_history_train,loss_history_test, w_vec, b)

Это основной алгоритм градиентного спуска. Как именно это улучшает производительность нашей модели?

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

«Мы можем варьировать эти 2 переменные и найти оптимальный вектор веса и смещение, чтобы потери были минимальными»

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

Самое первое, что нужно иметь в виду при применении GD к любому алгоритму, это то, что функция, к которой он применяется, «должна быть дифференцируемой». Проще говоря, мы должны быть в состоянии найти производную функции, что, в свою очередь, означает, что она должна иметь конечный наклон! По этой причине функция потерь приближена к «Логистическим потерям» в Log Reg.

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

«Градиентный спуск помогает нам найти эти минимумы любой функции, чтобы мы могли получить оптимальные переменные, где потери минимальны!»

Переменные должны быть инициализированы некоторыми случайными значениями для начала.

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

Убыток = исходная стоимость — прогнозируемая стоимость

Затем я вычислил частные производные ∂L/ ∂w, обозначенные w_grad, и ∂L/ ∂b, обозначенные b_grad, поскольку здесь мы имеем дело с векторами.

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

Здесь r = размер шага (целое положительное число), а член производной df/dx в нашем случае будет частной производной.

В код я включил регуляризацию L2, поэтому в формуле есть небольшое изменение, поскольку ∂L/∂w будет содержать член λ(лямбда), и, следовательно, правило обновления также будет иметь его.

Правило обновления, используемое в приведенном выше коде:

Если вам нужно понять, как рассчитывается дифференциация потерь журнала, перейдите по ссылке ниже:

https://rpubs.com/dnuttle/ml-logistic-cost-func_derivative

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

Необходимо повторять это для нескольких эпох, чтобы добиться минимальных потерь.

import warnings
warnings.filterwarnings("ignore")
lambda_val = 0.0001
eta0 = 0.0001
batch_size = 1
iterations = 10
inter_val = 0
initial_loss_train = compute_log_loss(X_train,y_train,weight_vec,inter_val)
initial_loss_test = compute_log_loss(X_test, y_test, weight_vec, inter_val)
print("The initial log loss for train data is:\n", initial_loss_train)
print("The initial log loss for test data is :\n", initial_loss_test)
(loss_history_train, loss_history_test,weight_optimized, intercept) = gradient_descent(X_train, y_train, weight_vec, eta0, iterations, inter_val, lambda_val, batch_size)
print("Intercept's optimal value for train data is:\n",intercept)
print("Optimal weights for train data are: \n", weight_optimized)
train_history = []
for i in range(len(loss_history_train)):
    for j in range(len(loss_history_train[i])):
        for k in range(len(loss_history_train[i][j])):
            train_history.append((loss_history_train[i][j][k]))
test_history = []
for a in range(len(loss_history_test)):
    for b in range(len(loss_history_test[a])):
        for c in range(len(loss_history_test[a][b])):
            test_history.append((loss_history_test[a][b][c]))
plt.figure()
sns.set_style('white')
plt.plot((range(len(loss_history_train))), (train_history))
plt.plot((range(len(loss_history_test))), (test_history))
plt.title("Convergence Graph of loss Function")
plt.xlabel("Number of epochs")
plt.ylabel("Loss")
plt.show()

Вывод:

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

#Loss for test data
test_loss = compute_log_loss(X_test,y_test,weight_optimized, intercept)
print("Test loss is :\n", test_loss)

Вывод:

Test loss is :
 [[0.38029601]]

Благодаря этим оптимальным значениям потери тестовых данных значительно сократились!

y_pred_test = predict(X_test, weight_optimized, intercept)
y_pred_train = predict(X_train, weight_optimized, intercept)
#Scores of train and test predictions
score_train = float(sum(y_pred_test == y_test))/ float(len(y_test))
score_test = float(sum(y_pred_train == y_train))/float(len(y_train))
print("Train score", score_train)
print("Test score", score_test)

Вывод:

Train score 0.83384
Test score 0.8313333333333334

Здесь я предсказывал по одному входу за раз в функции градиента_спуска, это называется стохастическим GD». Он обновляет параметры для каждого обучающего примера один за другим.

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

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

Градиентный спуск для нейронных сетей

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

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

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

Правило обновления для нейронной сети:

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

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

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

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

Наиболее часто используемые вариации:

  • Пакетный сингапурский доллар + моментум
  • Нестеров Ускоренный градиент
  • Адаптивные градиенты (АДА-ГРАД)
  • Адаптивная оценка момента
  • Ада-дельта и RMSPROP

Ограничения градиентного спуска:

  1. Схождение к локальному минимуму может быть немного медленным в случае больших размерностей или большого набора данных.
  2. В случае невыпуклых функций, где есть несколько локальных минимумов, она может застрять в одном из них или в седловой точке (ни минимума, ни максимума) и может не достичь глобального минимума.
  3. Сигмовидная и тангенциальная функции активации могут вызвать проблему исчезающих градиентов, из-за которой SGD не будет сходиться, если не используются другие активации (RELU).