Градиентный спуск: самый полный гайд, что нужно знать для ML

Разбираемся, как работает алгоритм, который заставляет нейросеть учиться на своих ошибках. И пишем его с нуля на Python.

Градиентный спуск: самый полный гайд, что нужно знать для ML

Когда мы говорим, что нейросеть «учится», то немного лукавим. На самом деле она просто решает огромную математическую задачу по поиску минимальной ошибки. Изначально модель ничего не знает и выдает случайные ответы. Чтобы стать умнее, ей нужен механизм, который подскажет, в какую сторону нужно поменять свои внутренние настройки, чтобы в следующий раз ошибиться чуть меньше.

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

Сегодня разберемся в механизме градиентного спуска и напишем свой собственный алгоритм оптимизации на Python.

Что такое градиентный спуск и зачем он нужен

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

Функция потерь и задача оптимизации

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

Что такое градиентный спуск и зачем он нужен
Смотрим на нижнюю точку функции потерь, определяем оптимальные веса

Интуиция алгоритма

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

Разберем процесс по шагам:

Шаг 1. Как модель делает предсказание

У модели есть параметры — веса, которые она должна настроить. Допустим, мы предсказываем цену квартиры y по её площади x. Базовая формула предсказания выглядит как уравнение прямой:

Градиентный спуск: самый полный гайд, что нужно знать для ML

x — входные данные (площадь).

w — вес признака: насколько сильно x влияет на цену.

b — смещение: базовая стоимость.

ŷ— то, что предсказала модель.

Наша задача — найти такие идеальные w и b, при которых ŷ будет максимально близко к реальной цене y.

Шаг 2. Как мы оцениваем ошибку

Чтобы понять, насколько мы ошиблись, нужна функция потерь (Loss Function), обозначаемая J. Самая популярная для задач регрессии — MSE (Mean Squared Error, среднеквадратичная ошибка):

Градиентный спуск: самый полный гайд, что нужно знать для ML

N — количество примеров в датасете.

yi— реальная цена i-й квартиры.

ŷi— предсказанная цена i-й квартиры.

(…)2 — возведение в квадрат избавляет от отрицательных значений и сильнее штрафует модель за крупные промахи.

Эта функция рисует ту самую «гору» в пространстве параметров — параболу, на дно которой нам нужно спуститься. Чем меньше J, тем точнее модель.

Шаг 3. Куда шагать — градиент

Градиент — это вектор из частных производных функции потерь по каждому из параметров. Производная показывает крутизну склона в текущей точке. Если взять производную J по весу w, получится:

Градиентный спуск: самый полный гайд, что нужно знать для ML

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

Градиентный спуск: самый полный гайд, что нужно знать для ML

— склон идёт вверх,

если

Градиентный спуск: самый полный гайд, что нужно знать для ML

— вниз.

Шаг 4. Делаем шаг — правило обновления весов

Вычислив градиент, алгоритм корректирует текущие веса так, чтобы на следующей итерации ошибка стала чуть меньше:

Градиентный спуск: самый полный гайд, что нужно знать для ML

Wold— текущий вес (наша точка на горе).

∇J(w)— градиент, указывающий в сторону самого крутого подъёма.

Знак – означает, что мы идём в противоположном направлении — то есть спускаемся.

α — скорость обучения (Learning Rate): гиперпараметр, определяющий длину шага.

Подробнее мы поговорим о скорости обучения позже, а пока запомним: при слишком малом α алгоритм будет сходиться бесконечно долго; при слишком большом α — «перепрыгивать» через минимум J, не в состоянии на нём остановиться.

Повторяя эти четыре шага снова и снова, алгоритм постепенно движется к минимуму J — туда, где модель ошибается меньше всего.

Градиентный спуск: самый полный гайд, что нужно знать для ML
Цикл работы алгоритма градиентного спуска

Успейте: скидка 15% до 29 мая

Градиентный спуск — это фундамент. Следующий шаг — научиться строить на нём реальные модели, понимать архитектуру и зарабатывать больше, — держите промокод Практикума на любой платный курс: по ссылке (можно просто нажать). Даёт скидку 15% при покупке, плюс дополнительно 5 мини-курсов и 5 книг стоимостью ~75 000 ₽. Но только до 29 мая — потом всё, промокод сгорит.

Три варианта градиентного спуска

В зависимости от объема данных алгоритм может использовать разные стратегии для расчета уклона. От этого зависит скорость и плавность обучения.

Batch Gradient Descent

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

Формула:

Градиентный спуск: самый полный гайд, что нужно знать для ML

Где:

θ — веса модели,

α — скорость обучения (learning rate),

m — количество примеров в датасете,

X — матрица признаков,

y — вектор истинных ответов.

Stochastic Gradient Descent

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

Mini-batch Gradient Descent

Золотая середина. Весь набор данных разбивается на небольшие пакеты — обычно по 32, 64 или 128 примеров. Этот подход сочетает высокую скорость вычислений SGD и стабильность классического метода. Видеокарты отлично оптимизированы под матричные вычисления таких размеров. Кроме того, легкий шум от мини-батчей помогает модели не зазубривать данные и снижает риск переобучения нейросети.

Градиентный спуск: самый полный гайд, что нужно знать для ML
Три траектории градиентного спуска

Каждый вариант легко реализуется на Python, понадобится только numpy.

Batch Gradient Descent:

def batch_gradient_descent(X, y, theta, alpha, iterations):
    m = len(y)
    for i in range(iterations):
        # 1. Вычисляем предсказания для всего датасета
        predictions = X.dot(theta)
        # 2. Находим разницу между предсказаниями и реальными значениями
        error = predictions - y
        # 3. Вычисляем средний градиент по всем примерам
        gradient = (1/m) * X.T.dot(error)
        # 4. Обновляем веса
        theta = theta - alpha * gradient
    return theta

Stochastic Gradient Descent:

def stochastic_gradient_descent(X, y, theta, alpha, iterations):
    m = len(y)
    for i in range(iterations):
        # Выбираем индекс одного случайного примера
        random_index = np.random.randint(m)
        xi = X[random_index:random_index+1]
        yi = y[random_index:random_index+1]
        
        # Считаем ошибку и градиент только для этого примера
        prediction = xi.dot(theta)
        error = prediction - yi
        gradient = xi.T.dot(error)
        
        # Сразу обновляем веса
        theta = theta - alpha * gradient
    return theta

Mini-batch Gradient Descent:

def mini_batch_gradient_descent(X, y, theta, alpha, epochs, batch_size):
    m = len(y)
    for epoch in range(epochs):
        # Перемешиваем индексы датасета перед каждой эпохой
        indices = np.random.permutation(m)
        X_shuffled = X[indices]
        y_shuffled = y[indices]
        
        # Нарезаем перемешанный массив на батчи (через срезы)
        for i in range(0, m, batch_size):
            xi = X_shuffled[i:i+batch_size]
            yi = y_shuffled[i:i+batch_size]
            
            # Обновляем веса для текущего мини-батча
            predictions = xi.dot(theta)
            error = predictions - yi
            gradient = (1/len(xi)) * xi.T.dot(error)
            theta = theta - alpha * gradient
    return theta

Learning Rate — ключевой гиперпараметр

Learning rate, или скорость обучения (обычно обозначается буквой α или ƞ), — это множитель, который определяет, насколько большим будет шаг алгоритма в сторону минимума.

Эффекты слишком большого и слишком малого α

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

Градиентный спуск: самый полный гайд, что нужно знать для ML
Влияние Learning Rate на градиентный спуск

Learning Rate Scheduling

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

Градиентный спуск: самый полный гайд, что нужно знать для ML
Уменьшение размера шага по мере обучения

Чтобы алгоритм быстро двигался к цели в начале, но не «перепрыгивал» через минимум в конце, скорость обучения можно менять прямо в процессе работы. Существуют три основные стратегии:

  • Константный LR: скорость обучения остается неизменной на протяжении всего процесса (просто, но не всегда эффективно).
  • Step decay (ступенчатое затухание): LR искусственно уменьшается в определенное количество раз каждые N пройденных эпох.
  • Cosine annealing (косинусное затухание): LR плавно убывает по кривой косинуса, постепенно снижаясь к нулю к концу обучения.

Проблемы сходимости и как их решать

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

Локальные минимумы и сёдловые точки

Раньше считалось, что главная проблема — это локальные минимумы (ямки на склоне горы, которые алгоритм может принять за глобальное дно). Но в сетях с миллионами параметров шанс застрять в локальной яме математически мал. Реальный враг —  сёдловые точки. Это места, где функция имеет минимум по одной оси и максимум по другой. Градиент там равен нулю, и алгоритм может надолго застрять на абсолютно ровном плато, перестав обучаться.

Градиентный спуск: самый полный гайд, что нужно знать для ML
Сёдловая точка в трёхмерном представлении

Приглядитесь к рельефу — в сёдловой точке он ведет себя коварно:

  • Если посмотреть вдоль оси X (Вес 1), поверхность образует U-образную впадину-параболу. То есть, если двигаться только по этому направлению, наша красная точка — это минимум.
  • Но если посмотреть вдоль оси Y (Вес 2), поверхность образует перевернутую параболу. По этому направлению красная точка — это максимум.

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

Исчезающий и взрывной градиент

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

Функции активации — это математические фильтры, которые решают, передавать сигнал дальше по сети или нет. Чаще всего используют функцию ReLU (Rectified Linear Unit). Она все отрицательные числа превращает в ноль, а положительные пропускает без изменений. Это позволяет градиенту проходить сквозь десятки слоев сети, не теряя своей силы.

Нормализация батчей принудительно выравнивает масштаб данных перед каждым слоем. Представьте, что на каждом слое нейросети масштаб чисел начинает скакать: на одном слое числа от -1 до 1, а на следующем — от -1000 до 1000. Из-за этого градиенту очень сложно найти правильное направление. Нормализация собирает мини-батч, находит в нём среднее значение и делает так, чтобы данные распределялись вокруг нуля. Это делает рельеф функции потерь более гладким, убирает резкие скачки и позволяет смело использовать большой Learning Rate.

Оптимизаторы: расширения базового SGD

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

Momentum

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

Градиентный спуск: самый полный гайд, что нужно знать для ML
SGD с инерцией и без

Формула:

Градиентный спуск: самый полный гайд, что нужно знать для ML
Градиентный спуск: самый полный гайд, что нужно знать для ML

Инерция помогает градиентному спуску накапливать скорость в том направлении, где градиент ∇L стабилен, и гасить колебания там, где градиент постоянно меняет знак.

В формуле vₜ — это экспоненциальное скользящее среднее прошлых градиентов, а параметр β (обычно 0,9) определяет, насколько сильно мы учитываем прошлые шаги.

RMSprop

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

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

Допустим, у нас есть пять признаков разной степени редкости:

  1. Площадь. Этот параметр есть у абсолютно каждой квартиры в базе. Модель натыкается на него постоянно, и градиент для этого веса обновляется на каждом шаге. Если оптимизатор будет делать большие шаги, модель начнет «штормить», и она проскочит идеальный вес. Поэтому RMSprop автоматически «тормозит» обучение для этого признака, делая шаги очень мелкими и аккуратными.
  2. Наличие балкона. Встречается часто, но не везде. Шаг чуть больше.
  3. Наличие консьержа. Встречается в каждой пятой новостройке. Средний шаг.
  4. Умный дом. Редкая фича, есть в премиум-сегменте. Шаг большой.
  5. Вертолетная площадка на крыше. Встречается в одной из тысячи записей. Если модель будет менять вес этого признака такими же микро-шажками, как у «площади», она никогда не научится адекватно оценивать влияние вертолетной площадки на цену. Поэтому RMSprop видит, что этот параметр обновляется крайне редко, и выдает ему огромный размер шага, чтобы модель усвоила информацию с первого же раза.

Градиентный спуск: самый полный гайд, что нужно знать для ML
Адаптивный спуск для признаков разной частотности

Формула:

Градиентный спуск: самый полный гайд, что нужно знать для ML
Градиентный спуск: самый полный гайд, что нужно знать для ML

Мы накапливаем квадрат градиента sₜ (чтобы понимать масштабы изменений). При обновлении весов мы делим градиент на корень из этого накопленного значения √(sₜ + ∈) (где — маленькое число, чтобы избежать деления на ноль). Таким образом, параметры, которые редко обновлялись, получают больший шаг, а те, что скачут слишком сильно — сдерживаются. Это решает проблему затухающих и взрывающихся градиентов.

Adam (Adaptive Moment Estimation)

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

  • Накопление скорости. Adam вычисляет первый момент — экспоненциально скользящее среднее прошлых градиентов. Благодаря этому алгоритм помнит, куда он двигался на предыдущих шагах. Если он попадает на ровное плато или в небольшую ямку, инерция помогает ему проскочить сложные участки и не топтаться на месте.
  • Индивидуальный размер шага. Adam вычисляет второй момент — скользящее среднее квадратов прошлых градиентов. Алгоритм анализирует каждый параметр отдельно: если по какой-то оси склон слишком крутой, он тормозит шаг, а если пологий — ускоряет.

Если просто сложить два этих метода, в самом начале обучения алгоритм будет работать плохо. Поскольку истории шагов еще нет (переменные стартуют с нулей), метод будет делать микроскопические, неуверенные шаги. Чтобы этого избежать, Adam использует математический трюк — коррекцию смещения. Она искусственно «разгоняет» алгоритм на первых итерациях, пока он не накопит достаточно статистики.

В результате получается невероятно быстрый и стабильный алгоритм, который отлично работает «из коробки» и прощает неидеальный выбор начального learning rate. За детальным математическим обоснованием можно обратиться к оригинальной статье по Adam от его создателей.

Градиентный спуск: самый полный гайд, что нужно знать для ML
Градиентный спуск с разными оптимизаторами

Сравнение оптимизаторов:

ОптимизаторСкорость сходимостиТочность поиска минимумаКогда применяютРаспространенность на практике
SGDНизкаяВысокая (при хорошей настройке LR)Простые линейные модели, базовая аналитика.Средняя (в чистом виде используется редко).
SGD + MomentumСредняяВысокаяКомпьютерное зрение, например, если Adam переобучается.Высокая
RMSpropВысокаяСредняя/ВысокаяРекуррентные нейросети, задачи NLP.Средняя.
AdamОчень высокаяВысокаяСложные многослойные нейросети по умолчанию.Повсеместная

Реализация с нуля на Python

Вместо использования готовых библиотек напишем чистую математическую реализацию градиентного спуска с помощью NumPy. В реальных проектах для этого обычно используют готовые библиотеки машинного обучения для Python.

import numpy as np

# Генерируем случайные данные для простой регрессии
np.random.seed(42)
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)
# Добавляем единичный столбец для свободного члена (bias)
X_b = np.c_[np.ones((100, 1)), X]

def train_gradient_descent(lr, iterations=50):
    # Случайная инициализация весов модели
    theta = np.random.randn(2, 1) 
    m = len(X_b)
    loss_history = []
    
    for iteration in range(iterations):
        # 1. Вычисляем градиент по всем данным
        gradients = 2/m * X_b.T.dot(X_b.dot(theta) - y)
        
        # 2. Делаем шаг в сторону антиградиента
        theta = theta - lr * gradients
        
        # 3. Считаем текущую ошибку (MSE) для мониторинга
        loss = np.mean((X_b.dot(theta) - y)**2)
        loss_history.append(loss)
        
    return theta, loss_history

# Обучаем модель
final_weights, history = train_gradient_descent(lr=0.1)
print("Найденные веса:", final_weights.ravel())

Вывод консоли:

Найденные веса: [3.91125197 3.03839129]

Вы не спрашивали, но мы ответим

Всегда ли алгоритм находит глобальный минимум?

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

Чем эпоха отличается от итерации?

Эпоха — это когда алгоритм просмотрел абсолютно весь доступный тренировочный датасет. Итерация — это один шаг обновления весов. При BGD эпоха равна одной итерации, а при Mini-batch одна эпоха может состоять из сотен итераций.

Заключение

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

Советуем дополнительно почитать по теме: 

Бонус для читателей

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

Вам может быть интересно
medium