Введение

Задавались ли вы когда-нибудь вопросом, что происходит под капотом обучения, например, линейной регрессии? Если вы до сих пор не нашли ответ на этот вопрос, то эта статья для вас. Сегодня простым языком разберём, что такое градиентный спуск — от интуиции до полноценного обучения линейной регрессии с нуля.

Интуиция

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

Математический аспект

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

f(x) = x^2

то с пониманием материала проблем не возникнет.

Частные производные: что такое градиент

В основе алгоритма градиентного спуска лежит идея о векторе частных производных по всем аргументам функции. Сама фраза «вектор частных производных» может звучать непонятно, поэтому начнём издалека.

Как взять производную от функции одной переменной?

f(x) = x^2f(x) = 2x

Несложно — это табличный случай. Но как быть, если у функции несколько переменных? Всё просто: дифференцируем по одной переменной, считая остальные константами. Затем делаем то же самое для каждой из оставшихся переменных.

f(x,y) = x^2+y^2\frac{df}{dx} = 2x+0 = 2x\frac{df}{dy} = 0+2y

Отлично, теперь составим вектор частных производных.

\nabla f(x,y) = ( \frac{df}{dx}, \frac{df}{dy})

То есть итоговый результат:

\nabla f(x,y) = (2x,2y)

Символ ∇ (набла) в данном случае обозначает градиент функции.

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

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

Роль градиента в оптимизации функции: антиградиент

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

Для простоты возьмём случай, когда у модели есть только один параметр w. Тогда график ошибки будет двумерным. Наша цель — с помощью градиентного спуска найти минимум этой функции.

Предположим, мы случайным образом выбрали значение веса w_4​, и при нём ошибка модели равна примерно 22. Как уменьшить эту ошибку? Нужно посчитать градиент в этой точке.

Что мы увидим? Градиент (стрелка) указывает направление наискорейшего роста функции. Но нам-то нужно противоположное — найти путь наискорейшего уменьшения функции. Именно здесь появляется понятие антиградиент

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

Теперь давайте посмотрим, как использовать антиградиент шаг за шагом. Существует базовая формула обновления весов:

w_{k+1} =w_k -\eta *\nabla f(w_k)

В этой формуле η обозначает скорость обучения (learning rate).

  • Если этот параметр очень маленький (например, 0.001), обучение будет идти медленно, но шаги будут аккуратными и ближе к минимуму.

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

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

Пришло время практики, напишем градиентный спуск для функции:

f(x,y) = x^2+y^2
def gradient(x, y, learning_rate, iteration):
    for i in range(iteration):
        x = x - learning_rate * 2*x
        y = y - learning_rate * 2*y
        z = x**2 + y**2
    return x, y

x0, y0 = 10, 23
learning_rate = 0.01
iteration = 200
x, y = gradient(x0, y0, learning_rate, iteration)
print(x,y)

Анимацию данного процесса можно увидеть ниже:

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

import pandas as pd 
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np 

df = pd.read_csv('/kaggle/input/dataset-for-linear-reg/linear_regression_friendly_dataset.csv')
x = df['x'].values
y = df['y'].values

np.random.seed(12354)

w = np.random.randint(12)
w0 = np.random.randint(12)

def linear_regression(x,w,w0):
    return w0 + w*x

def mse(y_true, y_pred):
    return np.mean((y_true - y_pred)**2)


def mape(y_true, y_pred):
    epsilon = 1e-10
    y_true_safe = np.clip(y_true, epsilon, None)
    return np.mean(np.abs((y_true - y_pred) / y_true_safe)) * 100

def grad_w(x,y_true, y_pred):
    n = len(y_true)
    return (2/n) * np.sum(x * (y_pred - y_true))


def grad_w0(y_true, y_pred):
    n = len(y_true)
    return (2/n) * np.sum(y_pred - y_true)



def gradient_descent(x, y, w, w0):
    iterations = 10000
    learning_rate = 0.00001
    
    for i in range(iterations):
        y_pred = linear_regression(x=x, w=w, w0=w0)
        loss = mse(y, y_pred)
        
        w = w - learning_rate * grad_w(x=x, y_true=y, y_pred=y_pred)
        w0 = w0 - learning_rate * grad_w0(y_true=y, y_pred=y_pred)
        

        
        if i % 200 == 0 :
            mape_value = mape(y_true=y, y_pred=y_pred)
            print(f"Iter {i:4d} | Loss: {loss:.4f} | MAPE: {mape_value:.2f}% | w: {w:.4f} | w0: {w0:.4f}")
            
    return w,w0
            
new_w, new_w0 = gradient_descent(x=x, y=y, w=w, w0=w0)

pred = linear_regression(w=new_w, w0=new_w0, x=x)

Для этого примера данные были искусственно сгенерированы, а код взят с моего Kaggle. Посмотрим на показатели, начиная с нулевой итерации и заканчивая последними.

Iter    0 | Loss: 1167.8956 | MAPE: 18.81% | w: 3.0391 | w0: 4.0006
Iter 9800 | Loss: 26.4123 | MAPE: 5.34% | w: 3.5797 | w0: 4.2575
Итог как обучилась линейная регрессия
Итог как обучилась линейная регрессия

Заключение

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

Помимо оригинального алгоритма, который на практике сейчас используют не так часто, существуют его модификации: SGD, Adam, градиентный спуск с импульсом и другие. Я не стал подробно останавливаться на них, так как хотел донести базовые концепции, чтобы с ними потом было легко понять модификации алгоритма.

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

Кроме того, существует алгоритм backpropagation, который использует идею градиентного спуска и применяется в нейронных сетях.

Комментарии (0)