Перейти к содержимому
ПереводыStanford

CS231n: оптимизация — ландшафт потерь, градиент и стохастический градиентный спуск (перевод конспекта)

Полный перевод конспекта Stanford CS231n «Optimization: Stochastic Gradient Descent»: визуализация функции потерь SVM, случайный поиск и случайный локальный поиск, численный и аналитический градиент, проверка градиента, выбор шага, градиентный спуск по мини-батчам.

В предыдущем разделе мы ввели два ключевых компонента задачи классификации изображений:

  1. (Параметрическую) функцию оценок, отображающую сырые пиксели в оценки классов (например, линейную функцию).
  2. Функцию потерь, измеряющую качество конкретного набора параметров по тому, насколько полученные оценки согласуются с истинными метками обучающих данных. Мы видели, что вариантов тут много (например, Softmax или SVM).

Напомним: линейная функция имела вид f(xi,W)=Wxif(x_i, W) = W x_i, а SVM мы сформулировали так:

L=1N∑i∑j≠yi[max⁡(0,f(xi;W)j−f(xi;W)yi+1)]+αR(W)L = \frac{1}{N} \sum_i \sum_{j \neq y_i} \left[ \max(0, f(x_i; W)_j - f(x_i; W)_{y_i} + 1) \right] + \alpha R(W)

Мы видели, что параметры WW, дающие для примеров xix_i предсказания, согласованные с их истинными метками yiy_i, дают и очень маленькие потери LL. Теперь введём третий и последний ключевой компонент — оптимизацию. Оптимизация — это процесс поиска параметров WW, минимизирующих функцию потерь.

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

Функции потерь в этом курсе обычно определены на пространствах очень высокой размерности (например, в CIFAR-10 матрица весов линейного классификатора имеет размер [10 × 3073], то есть всего 30 730 параметров), и визуализировать их трудно. Но интуицию можно получить, рассекая многомерное пространство лучами (одно измерение) или плоскостями (два измерения). Например, можно сгенерировать случайную матрицу весов WW (одна точка пространства), а затем идти вдоль луча и записывать значения функции потерь. То есть сгенерировать случайное направление W1W_1 и вычислять потери вдоль него, L(W+aW1)L(W + a W_1), для разных aa. Получится простой график: по оси x — значение aa, по оси y — значение функции потерь. То же можно сделать в двух измерениях, вычисляя L(W+aW1+bW2)L(W + a W_1 + b W_2) при разных a,ba, b. Тогда a,ba, b — оси x и y, а значение функции потерь показывается цветом.

Рисунок (в оригинале). Ландшафт функции потерь многоклассового SVM (без регуляризации) для одного примера (слева и в центре) и для ста примеров (справа) из CIFAR-10. Слева — одномерные потери при изменении только a. В центре и справа — двумерные срезы потерь: синий — низкие потери, красный — высокие. Обратите внимание на кусочно-линейную структуру функции потерь. Потери многих примеров усредняются, поэтому «чаша» справа — среднее многих кусочно-линейных чаш (вроде той, что в центре).

Кусочно-линейную структуру можно объяснить, посмотрев на формулу. Для одного примера:

Li=∑j≠yi[max⁡(0,wjTxi−wyiTxi+1)]L_i = \sum_{j \neq y_i} \left[ \max(0, w_j^T x_i - w_{y_i}^T x_i + 1) \right]

Из формулы видно, что потери на данных для каждого примера — сумма линейных функций от WW (обрезанных в нуле функцией max⁡(0,−)\max(0, -)). Причём перед каждой строкой WW (то есть wjw_j) иногда стоит плюс (когда она соответствует неправильному классу примера), а иногда минус (когда правильному). Чтобы было нагляднее, возьмём простой набор из трёх одномерных точек и трёх классов. Полные потери SVM (без регуляризации):

L0=max⁡(0,w1Tx0−w0Tx0+1)+max⁡(0,w2Tx0−w0Tx0+1)L1=max⁡(0,w0Tx1−w1Tx1+1)+max⁡(0,w2Tx1−w1Tx1+1)L2=max⁡(0,w0Tx2−w2Tx2+1)+max⁡(0,w1Tx2−w2Tx2+1)L=(L0+L1+L2)/3\begin{aligned} L_0 = {} & \max(0, w_1^T x_0 - w_0^T x_0 + 1) + \max(0, w_2^T x_0 - w_0^T x_0 + 1) \\ L_1 = {} & \max(0, w_0^T x_1 - w_1^T x_1 + 1) + \max(0, w_2^T x_1 - w_1^T x_1 + 1) \\ L_2 = {} & \max(0, w_0^T x_2 - w_2^T x_2 + 1) + \max(0, w_1^T x_2 - w_2^T x_2 + 1) \\ L = {} & (L_0 + L_1 + L_2) / 3 \end{aligned}

Поскольку примеры одномерные, данные xix_i и веса wjw_j — числа. Возьмём, например, w0w_0: некоторые слагаемые выше — линейные функции от w0w_0, и каждое обрезано в нуле. Это можно изобразить так:

Рисунок. Одномерная иллюстрация потерь на данных. По оси x — один вес, по оси y — потери. Потери на данных — сумма нескольких слагаемых, каждое из которых либо не зависит от этого веса, либо является его линейной функцией, обрезанной в нуле. Полные потери SVM на данных — 30 730-мерная версия этой формы.

Кстати, по форме чаши вы могли догадаться, что функция стоимости SVM — пример выпуклой функции. Эффективной минимизации таких функций посвящено много литературы, а в Стэнфорде есть и отдельный курс (выпуклая оптимизация). Когда мы расширим функции оценок ff до нейронных сетей, целевые функции станут невыпуклыми, и вместо чаш визуализации покажут сложный «ухабистый» рельеф.

Недифференцируемые функции потерь. Техническое замечание: изломы функции потерь (из-за операции max) формально делают её недифференцируемой — в точках излома градиент не определён. Однако субградиент всё равно существует, и обычно используют его. В этом курсе мы будем употреблять слова субградиент и градиент как синонимы.

Повторим: функция потерь позволяет количественно оценить качество любого набора весов W. Цель оптимизации — найти W, минимизирующий функцию потерь. Сейчас мы постепенно мотивируем и построим подход к оптимизации функции потерь. Тем, кто пришёл с опытом, этот раздел может показаться странным: наш рабочий пример (потери SVM) — выпуклая задача. Но помните, что в итоге мы хотим оптимизировать нейронные сети, где инструменты из литературы по выпуклой оптимизации просто так применить не получится.

Стратегия № 1: первая, очень плохая идея — случайный поиск

Заголовок раздела «Стратегия № 1: первая, очень плохая идея — случайный поиск»

Раз проверить, насколько хорош данный набор параметров W, так просто, первая (очень плохая) идея — перепробовать много случайных весов и запоминать лучший. Это может выглядеть так:

# пусть X_train — данные, каждый столбец — пример (например, 3073 x 50 000)
# пусть Y_train — метки (например, одномерный массив из 50 000)
# пусть функция L вычисляет функцию потерь
bestloss = float("inf") # Python присваивает наибольшее возможное значение float
for num in range(1000):
W = np.random.randn(10, 3073) * 0.0001 # генерируем случайные параметры
loss = L(X_train, Y_train, W) # потери на всей обучающей выборке
if loss < bestloss: # запоминаем лучшее решение
bestloss = loss
bestW = W
print('in attempt %d the loss was %f, best %f' % (num, loss, bestloss))
# печатает:
# in attempt 0 the loss was 9.401632, best 9.401632
# in attempt 1 the loss was 8.959668, best 8.959668
# in attempt 2 the loss was 9.044034, best 8.959668
# in attempt 3 the loss was 9.278948, best 8.959668
# in attempt 4 the loss was 8.857370, best 8.857370
# in attempt 5 the loss was 8.943151, best 8.857370
# in attempt 6 the loss was 8.605604, best 8.605604
# ... (обрезано: продолжается 1000 строк)

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

# Пусть X_test имеет размер [3073 x 10000], Y_test — [10000 x 1]
scores = bestW.dot(X_test) # 10 x 10000, оценки классов для всех тестовых примеров
# находим индекс максимальной оценки в каждом столбце (предсказанный класс)
Yte_predict = np.argmax(scores, axis = 0)
# и считаем точность (долю правильных предсказаний)
np.mean(Yte_predict == Y_test)
# возвращает 0.1555

С лучшим W точность получается около 15,5%. Учитывая, что угадывание классов наугад даёт лишь 10%, для такого «безмозглого» случайного поиска это не так уж плохо!

Главная идея: итеративное улучшение. Конечно, можно сделать гораздо лучше. Главная идея в том, что найти наилучший набор весов W — очень трудная или даже невозможная задача (особенно когда W — веса целой сложной нейронной сети), а вот немного улучшить конкретный набор весов W — задача куда проще. Иначе говоря, наш подход: начать со случайного W и итеративно улучшать его, каждый раз чуть-чуть.

Наша стратегия — начать со случайных весов и итеративно улучшать их, получая всё меньшие потери.

Аналогия с путником с завязанными глазами. Дальше вам может помочь такая аналогия: вы идёте по холмистой местности с завязанными глазами и пытаетесь спуститься в самую низкую точку. В примере с CIFAR-10 холмы 30 730-мерные, поскольку W имеет размер 10 × 3073. В каждой точке холма достигается определённое значение потерь (высота местности).

Первое, что приходит в голову, — вытянуть ногу в случайном направлении и сделать шаг, только если он ведёт вниз. Конкретно: начинаем со случайного WW, генерируем к нему случайные возмущения δW\delta W, и если потери в точке W+δWW + \delta W меньше, выполняем обновление. Код:

W = np.random.randn(10, 3073) * 0.001 # случайная начальная W
bestloss = float("inf")
for i in range(1000):
step_size = 0.0001
Wtry = W + np.random.randn(10, 3073) * step_size
loss = L(Xtr_cols, Ytr, Wtry)
if loss < bestloss:
W = Wtry
bestloss = loss
print('iter %d loss is %f' % (i, bestloss))

При том же числе вычислений функции потерь (1000) этот подход даёт точность на тестовой выборке 21,4%. Лучше, но всё ещё расточительно и дорого.

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

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

df(x)dx=lim⁡h→0f(x+h)−f(x)h\frac{df(x)}{dx} = \lim_{h \to 0} \frac{f(x + h) - f(x)}{h}

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

Градиент можно вычислить двумя способами: медленным, приближённым, но простым (численный градиент), и быстрым, точным, но более подверженным ошибкам, требующим математического анализа (аналитический градиент). Разберём оба.

Численный градиент через конечные разности

Заголовок раздела «Численный градиент через конечные разности»

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

def eval_numerical_gradient(f, x):
"""
наивная реализация численного градиента f в точке x
- f — функция одного аргумента
- x — точка (массив numpy), в которой вычисляется градиент
"""
fx = f(x) # значение функции в исходной точке
grad = np.zeros(x.shape)
h = 0.00001
# перебираем все индексы x
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
# значение функции в точке x+h
ix = it.multi_index
old_value = x[ix]
x[ix] = old_value + h # увеличиваем на h
fxh = f(x) # вычисляем f(x + h)
x[ix] = old_value # восстанавливаем прежнее значение (очень важно!)
# частная производная
grad[ix] = (fxh - fx) / h # наклон
it.iternext() # к следующему измерению
return grad

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

Практические соображения. В математической формулировке градиент определяется в пределе при h, стремящемся к нулю, но на практике обычно достаточно очень маленького значения (например, 1e-5, как в примере). В идеале берут наименьший шаг, который ещё не приводит к численным проблемам. Кроме того, на практике численный градиент часто лучше считать по формуле центральной разности: [f(x+h)−f(x−h)]/2h[f(x + h) - f(x - h)] / 2h. Подробности — в Википедии.

Этой функцией можно вычислить градиент любой функции в любой точке. Вычислим градиент функции потерь CIFAR-10 в случайной точке пространства весов:

# чтобы использовать общий код выше, нужна функция одного аргумента
# (в нашем случае — весов), поэтому «замыкаем» X_train и Y_train
def CIFAR10_loss_fun(W):
return L(X_train, Y_train, W)
W = np.random.rand(10, 3073) * 0.001 # случайный вектор весов
df = eval_numerical_gradient(CIFAR10_loss_fun, W) # получаем градиент

Градиент сообщает наклон функции потерь по каждому измерению, и с его помощью можно сделать обновление:

loss_original = CIFAR10_loss_fun(W) # исходные потери
print('original loss: %f' % (loss_original, ))
# посмотрим на действие разных размеров шага
for step_size_log in [-10, -9, -8, -7, -6, -5,-4,-3,-2,-1]:
step_size = 10 ** step_size_log
W_new = W - step_size * df # новая позиция в пространстве весов
loss_new = CIFAR10_loss_fun(W_new)
print('for step size %f new loss: %f' % (step_size, loss_new))
# печатает:
# original loss: 2.200718
# for step size 1.000000e-10 new loss: 2.200652
# for step size 1.000000e-09 new loss: 2.200057
# for step size 1.000000e-08 new loss: 2.194116
# for step size 1.000000e-07 new loss: 2.135493
# for step size 1.000000e-06 new loss: 1.647802
# for step size 1.000000e-05 new loss: 2.844355
# for step size 1.000000e-04 new loss: 25.558142
# for step size 1.000000e-03 new loss: 254.086573
# for step size 1.000000e-02 new loss: 2539.370888
# for step size 1.000000e-01 new loss: 25392.214036

Обновление в направлении минус градиента. Обратите внимание: вычисляя W_new, мы делаем шаг в направлении, противоположном градиенту df, потому что хотим, чтобы функция потерь уменьшалась, а не росла.

Влияние размера шага. Градиент указывает направление, в котором функция растёт быстрее всего, но не говорит, насколько далеко в этом направлении шагать. Как мы увидим позже, выбор размера шага (его также называют скоростью обучения, learning rate) станет одним из самых важных (и самых мучительных) гиперпараметров при обучении нейронной сети. В аналогии со спуском вслепую мы чувствуем, что склон под ногами идёт в какую-то сторону, но длина шага неизвестна. Если осторожно переставлять ноги, можно рассчитывать на стабильное, но очень медленное продвижение (это маленький шаг). Можно, наоборот, сделать большой уверенный шаг, чтобы спуститься быстрее, но это может не окупиться. Как видно в примере выше, с какого-то момента больший шаг даёт бо́льшие потери: мы «перешагиваем».

Рисунок. Визуализация влияния размера шага. Мы стоим в некоторой точке W и вычисляем градиент (точнее, минус градиент — белая стрелка), который указывает направление наискорейшего убывания функции потерь. Маленькие шаги, скорее всего, дадут стабильное, но медленное продвижение. Большие шаги могут дать больше, но они рискованнее. В конце концов при слишком большом шаге мы перескочим и ухудшим потери. Размер шага (или, как мы будем называть его позже, скорость обучения) станет одним из важнейших гиперпараметров, требующих аккуратной настройки.

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

Аналитический градиент с помощью математического анализа

Заголовок раздела «Аналитический градиент с помощью математического анализа»

Численный градиент очень просто вычислить приближением конечными разностями, но у него два минуса: он приближённый (приходится выбирать малое h, а истинный градиент определён как предел при h, стремящемся к нулю) и очень дорогой. Второй способ — аналитический, с помощью матанализа: он даёт прямую формулу для градиента (без приближений), которая к тому же очень быстро вычисляется. Однако, в отличие от численного градиента, в реализации легче ошибиться. Поэтому на практике очень часто вычисляют аналитический градиент и сравнивают его с численным, чтобы проверить правильность реализации. Это называется проверкой градиента (gradient check).

Возьмём для примера функцию потерь SVM для одной точки данных:

Li=∑j≠yi[max⁡(0,wjTxi−wyiTxi+Δ)]L_i = \sum_{j \neq y_i} \left[ \max(0, w_j^T x_i - w_{y_i}^T x_i + \Delta) \right]

Продифференцируем её по весам. Например, градиент по wyiw_{y_i}:

∇wyiLi=−(∑j≠yi1(wjTxi−wyiTxi+Δ>0))xi\nabla_{w_{y_i}} L_i = - \left( \sum_{j \neq y_i} \mathbb{1}(w_j^T x_i - w_{y_i}^T x_i + \Delta > 0) \right) x_i

где 1\mathbb{1} — индикаторная функция: единица, если условие внутри истинно, и ноль иначе. Записанное выражение выглядит страшно, но в коде вы просто посчитаете число классов, не выдержавших нужный зазор (и, значит, давших вклад в потери), — и градиент равен вектору данных xix_i, умноженному на это число. Заметьте, что это градиент только по строке WW, соответствующей правильному классу. Для остальных строк, где j≠yij \neq y_i, градиент равен:

∇wjLi=1(wjTxi−wyiTxi+Δ>0) xi\nabla_{w_j} L_i = \mathbb{1}(w_j^T x_i - w_{y_i}^T x_i + \Delta > 0)\, x_i

Выведя выражение для градиента, реализовать его и использовать для обновления параметров уже несложно.

Теперь, когда мы умеем вычислять градиент функции потерь, процедура «многократно вычислять градиент и обновлять параметры» называется градиентным спуском (Gradient Descent). Его базовая (vanilla) версия выглядит так:

# Базовый градиентный спуск
while True:
weights_grad = evaluate_gradient(loss_fun, data, weights)
weights += - step_size * weights_grad # обновление параметров

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

Градиентный спуск по мини-батчам. В крупномасштабных задачах (вроде соревнования ILSVRC) обучающие данные могут насчитывать миллионы примеров. Поэтому кажется расточительным вычислять полную функцию потерь по всей выборке ради одного обновления параметров. Очень распространённый выход — вычислять градиент по батчам обучающих данных. Например, в современных свёрточных сетях типичный батч содержит 256 примеров из всей выборки в 1,2 млн. По этому батчу и делается обновление:

# Базовый градиентный спуск по мини-батчам
while True:
data_batch = sample_training_data(data, 256) # выбираем 256 примеров
weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
weights += - step_size * weights_grad # обновление параметров

Это хорошо работает, потому что примеры в обучающих данных коррелированы. Рассмотрим крайний случай: все 1,2 млн изображений ILSVRC на самом деле — точные копии всего 1000 уникальных изображений (по одному на класс, то есть по 1200 одинаковых копий каждого). Ясно, что градиенты для всех 1200 копий одинаковы, и если усреднить потери на данных по всем 1,2 млн изображений, получится ровно то же, что и на маленьком подмножестве из 1000. На практике, конечно, дубликатов в наборе нет, но градиент по мини-батчу — хорошее приближение градиента полной цели. Поэтому, вычисляя градиенты по мини-батчам и чаще обновляя параметры, на практике можно сходиться гораздо быстрее.

Крайний случай — мини-батч из одного примера. Такой процесс называют стохастическим градиентным спуском (Stochastic Gradient Descent, SGD), иногда — онлайн-градиентным спуском. Встречается он сравнительно редко: благодаря векторизации вычислить градиент для 100 примеров разом гораздо эффективнее, чем 100 раз для одного примера. Хотя формально SGD означает вычисление градиента по одному примеру, термин SGD употребляют и для градиентного спуска по мини-батчам (сокращения MGD — Minibatch Gradient Descent — или BGD — Batch Gradient Descent — встречаются редко); обычно подразумевается, что используются мини-батчи. Размер мини-батча — гиперпараметр, но подбирать его кросс-валидацией не принято. Обычно его выбирают по ограничениям памяти (если они есть) или ставят какое-то значение, например 32, 64 или 128. На практике берут степени двойки, потому что многие векторизованные операции работают быстрее, когда размеры входов — степени двойки.

Рисунок. Схема потока информации. Набор пар (x, y) задан и фиксирован. Веса стартуют со случайных чисел и могут меняться. При прямом проходе функция оценок вычисляет оценки классов, хранящиеся в векторе f. Функция потерь состоит из двух компонентов: потери на данных измеряют согласованность оценок f и меток y, а потери регуляризации зависят только от весов. При градиентном спуске мы вычисляем градиент по весам (и, при желании, по данным) и используем его для обновления параметров.

В этом разделе:

  • Мы развили интуицию о функции потерь как о многомерном ландшафте оптимизации, на дне которого хотим оказаться. Рабочая аналогия — путник с завязанными глазами, желающий спуститься вниз. В частности, мы увидели, что функция стоимости SVM кусочно-линейна и имеет форму чаши.
  • Мы мотивировали оптимизацию функции потерь итеративным улучшением: начинаем со случайных весов и улучшаем их шаг за шагом, пока потери не станут минимальными.
  • Мы увидели, что градиент функции задаёт направление наискорейшего подъёма, и обсудили простой, но неэффективный способ вычислить его численно — приближением конечными разностями (конечная разность — это значение h, используемое при вычислении численного градиента).
  • Мы увидели, что обновление параметров требует тонкой настройки размера шага (или скорости обучения): если он слишком мал, продвижение стабильное, но медленное; если слишком велик — быстрее, но рискованнее. Этот компромисс мы гораздо подробнее разберём в следующих разделах.
  • Мы обсудили компромисс между численным и аналитическим градиентом. Численный прост, но приближён и дорог. Аналитический точен и быстр, но в нём легче ошибиться, так как требует математического вывода. Поэтому на практике всегда используют аналитический градиент, а затем выполняют проверку градиента, сравнивая реализацию с численным.
  • Мы ввели алгоритм градиентного спуска, который в цикле вычисляет градиент и обновляет параметры.

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

ENRU
loss landscapeландшафт функции потерь
random search, random local searchслучайный поиск, случайный локальный поиск
iterative refinementитеративное улучшение
numerical gradient, finite differencesчисленный градиент, конечные разности
centered differenceцентральная разность
analytic gradient, gradient checkаналитический градиент, проверка градиента
step size, learning rateразмер шага, скорость обучения
mini-batch, SGDмини-батч, стохастический градиентный спуск
subgradientсубградиент