Что такое градиентный спуск (Gradient Descent) и как он работает?

Ответ

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

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

Формула обновления (для параметра θ_i): θ_i := θ_i - α * ∂J(θ) / ∂θ_i где α — скорость обучения (learning rate), гиперпараметр, контролирующий размер шага.

Основные варианты алгоритма:

Вариант Описание Плюсы Минусы
Batch Gradient Descent Вычисляет градиент по всему обучающему набору за одну итерацию. Стабильное, детерминированное направление к минимуму. Очень медленно на больших датасетах; требует всей памяти данных.
Stochastic Gradient Descent (SGD) Вычисляет градиент и обновляет параметры для одного случайного примера за итерацию. Быстрый; может выпрыгивать из локальных минимумов. Сильно флуктуирует; сходимость может быть нестабильной.
Mini-batch Gradient Descent Компромисс: вычисляет градиент по небольшой случайной подвыборке (mini-batch). Более стабилен, чем SGD; использует аппаратное ускорение (векторизацию). Требует настройки размера батча.

Практический пример реализации Mini-batch SGD для линейной регрессии на NumPy:

import numpy as np

def mini_batch_gradient_descent(X, y, learning_rate=0.01, epochs=100, batch_size=32):
    """
    X: матрица признаков (m samples, n features)
    y: вектор целевых значений (m, )
    """
    m, n = X.shape
    theta = np.random.randn(n)  # Инициализация параметров

    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):
            X_batch = X_shuffled[i:i+batch_size]
            y_batch = y_shuffled[i:i+batch_size]

            # Вычисление градиента для батча
            predictions = X_batch.dot(theta)
            errors = predictions - y_batch
            gradient = (2 / batch_size) * X_batch.T.dot(errors)

            # Обновление параметров
            theta -= learning_rate * gradient

        # (Опционально) Мониторинг потерь
        if epoch % 10 == 0:
            loss = np.mean((X.dot(theta) - y) ** 2)
            print(f"Epoch {epoch}, Loss: {loss:.4f}")

    return theta

Критические аспекты для успеха:

  1. Скорость обучения (α): Слишком большая — алгоритм расходится; слишком малая — обучение происходит крайне медленно. Часто используют затухание (decay) или адаптивные методы (Adam, RMSprop).
  2. Инициализация параметров: Важна для сложных моделей (например, нейросетей). Используют методы Xavier/Glorot или He.
  3. Момент (Momentum): Техника, которая помогает ускорить сходимость и проходить мелкие локальные минимумы, добавляя инерцию от предыдущих обновлений градиента.

Ответ 18+ 🔞

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

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

θ_i := θ_i - α * (производная J по θ_i)

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

А теперь про варианты, их три основных:

  1. Batch (Пакетный). Это когда ты, прежде чем шагнуть, опрашиваешь ВСЕХ жителей деревни, куда идти. Точный, стабильный, но овердохуища медленный, если деревня — это весь интернет. Памяти жрёт как не в себя.
  2. Stochastic (Стохастический, SGD). Полная противоположность. Ты останавливаешь первого встречного, спрашиваешь у него и сразу шагаешь. Быстро? Быстро. Но этот встречный может быть пьян, и ты шагнёшь в сторону обрыва. Сильно дёргается, флуктуирует, но зато может выпрыгнуть из мелких локальных ямок.
  3. Mini-batch (Мини-пакетный). Золотая середина. Ты собираешь небольшую толпу из 16, 32, 128 случайных человек, усредняешь их мнение и шагаешь. И стабильнее, чем у одного, и быстрее, чем у всех. Именно это все и используют, потому что это ещё и векторизуется на GPU — красота.

Вот тебе живой код, как это может выглядеть на Python:

import numpy as np

def mini_batch_gradient_descent(X, y, learning_rate=0.01, epochs=100, batch_size=32):
    """
    X: матрица признаков (m samples, n features)
    y: вектор целевых значений (m, )
    """
    m, n = X.shape
    theta = np.random.randn(n)  # Инициализация параметров

    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):
            X_batch = X_shuffled[i:i+batch_size]
            y_batch = y_shuffled[i:i+batch_size]

            # Вычисление градиента для батча
            predictions = X_batch.dot(theta)
            errors = predictions - y_batch
            gradient = (2 / batch_size) * X_batch.T.dot(errors)

            # Обновление параметров
            theta -= learning_rate * gradient

        # (Опционально) Мониторинг потерь
        if epoch % 10 == 0:
            loss = np.mean((X.dot(theta) - y) ** 2)
            print(f"Epoch {epoch}, Loss: {loss:.4f}")

    return theta

На что смотреть, чтобы не облажаться:

  • Скорость обучения (α). Это царица полей. Её часто делают затухающей со временем или используют хитрожопые адаптивные алгоритмы вроде Adam, которые сами подбирают шаг для каждого параметра. Adam — это как градиентный спуск с мозгом, он запоминает, куда ты шагал раньше.
  • Инициализация. Нельзя начинать со всех нулей, особенно в нейросетях. Это как собрать всех в одной точке — градиенты будут одинаковые, и ничего не сдвинется с мёртвой точки. Используют Xavier или He инициализацию — это чтобы разброс значений на старте был нормальный.
  • Момент (Momentum). Просто гениальная приблуда. Представь, что ты катишься с горы не просто шариком, а тяжёлым шаром для боулинга. У него есть инерция. Если последние несколько шагов были в одном направлении, он будет катиться туда же сильнее. Это помогает разогнаться на пологих участках и проехать мелкие кочки (локальные минимумы), не застряв. Без него сейчас вообще редко кто работает.

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