Введение
Задавались ли вы когда-нибудь вопросом, что происходит под капотом обучения, например, линейной регрессии? Если вы до сих пор не нашли ответ на этот вопрос, то эта стат��я для вас. Сегодня простым языком разберём, что такое градиентный спуск — от интуиции до полноценного обучения линейной регрессии с нуля.
Интуиция
Приведу до боли заезженную, но очень наглядную интуицию. Представьте, что вы стоите на высокой горе, и ваша цель — спуститься с неё. У вас есть посох, который показывает направление, в котором нужно идти, чтобы быстрее оказаться внизу. Вы шаг за шагом начинаете движение: сначала быстро, так как склон крутой, а затем постепенно замедляетесь.
Математический аспект
В этой статье будет довольно много математики. Но не спешите пугаться: вам достаточно знать лишь то, что такое производная, и иметь базовое представление о том, как её брать. Если вы, например, можете найти производную функции:
то с пониманием материала проблем не возникнет.
Частные производные: что такое градиент
В основе алгоритма градиентного спуска лежит идея о векторе частных производных по всем аргументам функции. Сама фраза «вектор частных производных» может звучать непонятно, поэтому начнём издалека.
Как взять производную от функции одной переменной?
Несложно — это табличный случай. Но как быть, если у функции несколько переменных? Всё просто: дифференцируем по одной переменной, считая остальные константами. Затем делаем то же самое для каждой из оставшихся переменных.
Отлично, теперь составим вектор частных производных.
То есть итоговый результат:
Символ ∇ (набла) в данном случае обозначает градиент функции.
Градиент функции — это вектор, состоящий из всех её частных производных. Он указывает направление наискорейшего роста функции.

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

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

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

Что мы увидим? Градиент (стрелка) указывает направление наискорейшего роста функции. Но нам-то нужно противоположное — найти путь наискорейшего уменьшения функции. Именно здесь появляется понятие антиградиент
Антиградиент — это градиент со знаком минус, который указывает направление наибольшего уменьшения функции
Теперь давайте посмотрим, как использовать антиградиент шаг за шагом. Существует базовая формула обновления весов:
В этой формуле η обозначает скорость обучения (learning rate).
Если этот параметр очень маленький (например, 0.001), обучение будет идти медленно, но шаги будут аккуратными и ближе к минимуму.
Если скорость обучения слишком большая, то процесс будет быстрее, но может перепрыгивать через минимум и не сходиться.
Градиентный спуск на python
Пришло время практики, напишем градиентный спуск для функции:
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.0006Iter 9800 | Loss: 26.4123 | MAPE: 5.34% | w: 3.5797 | w0: 4.2575
Заключение
Это основные принципы градиентного спуска, которые стоит знать, чтобы не теряться в мире машинного обучения.
Помимо оригинального алгоритма, который на практике сейчас используют не так часто, существуют его модификации: SGD, Adam, градиентный спуск с импульсом и другие. Я не стал подробно останавливаться на них, так как хотел донести базовые концепции, чтобы с ними потом было легко понять модификации алгоритма.
Также я не стал показывать процесс дифференцирования MSE, так как не считаю это критически важным: опытный человек без труда продифференцирует функцию ошибки, а новички вряд ли поймут детали и могут испугаться.
Кроме того, существует алгоритм backpropagation, который использует идею градиентного спуска и применяется в нейронных сетях.
