Введение

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

Интуиция

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

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

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

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, который использует идею градиентного спуска и применяется в нейронных сетях.