Обучение нейронных сетей 2

Обучение нейронных сетей

Содержание: - Генерация некоторых данных - Обучение линейного классификатора Softmax - Инициализируйте параметры - Подсчитайте баллы за класс - Вычислите потери - Вычисление аналитического градиента с обратным распространением - Выполнение обновления параметров - Сведение всего этого воедино: обучение классификатора Softmax - Обучение нейронной сети - Краткая сводка - Дополнительные материалы

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

Генерация некоторых данных

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

N = 100 # number of points per class
D = 2 # dimensionality
K = 3 # number of classes
X = np.zeros((N*K,D)) # data matrix (each row = single example)
y = np.zeros(N*K, dtype='uint8') # class labels
for j in range(K):
  ix = range(N*j,N*(j+1))
  r = np.linspace(0.0,1,N) # radius
  t = np.linspace(j*4,(j+1)*4,N) + np.random.randn(N)*0.2 # theta
  X[ix] = np.c_[r*np.sin(t), r*np.cos(t)]
  y[ix] = j
# lets visualize the data:
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)
plt.show()

Данные игрушечной спирали состоят из трёх классов (синий, красный, жёлтый), которые нельзя разделить линейно.


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

Обучение линейного классификатора Softmax

Инициализируйте параметры

Давайте сначала обучим классификатор Softmax на этом наборе данных для классификации. Как мы видели в предыдущих разделах, классификатор Softmax имеет линейную функцию оценки и использует функцию потерь кросс-энтропии. Параметры линейного классификатора состоят из весовой матрицы W и вектора смещения b для каждого класса. Давайте сначала инициализируем эти параметры случайными числами:

# initialize parameters randomly
W = 0.01 * np.random.randn(D,K)
b = np.zeros((1,K))

Напомним, что D = 2 — это размерность, а K = 3 — количество классов.

Подсчитайте баллы за класс

Поскольку это линейный классификатор, мы можем очень просто вычислить оценки для всех классов параллельно с помощью одного умножения матриц:

# compute class scores for a linear classifier
scores = np.dot(X, W) + b

В этом примере у нас есть 300 двумерных точек, поэтому после этого умножения массив scores будет иметь размер [300 x 3], где каждая строка содержит баллы за классы, соответствующие трём классам (синий, красный, жёлтый).

Вычислите потери

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

$$ L_i = -\log\left(\frac{e^{f_{y_i}}}{ \sum_j e^{f_j} }\right) $$

Мы можем видеть, что классификатор Softmax интерпретирует каждый элемент f. В качестве входных данных используются (ненормализованные) логарифмические вероятности трёх классов. Мы возводим их в степень, чтобы получить (ненормализованные) вероятности, а затем нормализуем их, чтобы получить вероятности. Таким образом, выражение внутри логарифма — это нормализованная вероятность правильного класса. Обратите внимание на то, как работает это выражение: эта величина всегда находится в диапазоне от 0 до 1. Когда вероятность правильного класса очень мала (близка к 0), потери будут стремиться к (положительной) бесконечности. И наоборот, когда вероятность правильного класса приближается к 1, потери приближаются к нулю, потому что log(1)=0. Следовательно, выражение для \(L_i\). Вероятность правильного класса низкая, когда она высока, и очень высокая, когда она низка.

Напомним также, что полная потеря классификатора Softmax определяется как средняя потеря кросс-энтропии по обучающим примерам и регуляризация:

$$ L = \underbrace{ \frac{1}{N} \sum_i L_i }\text{потеря данных} + \underbrace{ \frac{1}{2} \lambda \sum_k\sum_l W \\ $$ }^2 }_\text{потеря регуляризации

Учитывая массив scores значений, которые мы вычислили выше, мы можем вычислить потери. Во-первых, способ получения вероятностей прост:

num_examples = X.shape[0]
# get unnormalized probabilities
exp_scores = np.exp(scores)
# normalize them for each example
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

Теперь у нас есть массив probs размером [300 x 3], где каждая строка содержит вероятности классов. В частности, поскольку мы их нормализовали, сумма значений в каждой строке равна единице. Теперь мы можем запросить логарифмические вероятности, присвоенные правильным классам в каждом примере:

orrect_logprobs = -np.log(probs[range(num_examples),y])

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

# compute the loss: average cross-entropy loss and regularization
data_loss = np.sum(correct_logprobs)/num_examples
reg_loss = 0.5*reg*np.sum(W*W)
loss = data_loss + reg_loss

В этом коде сила регуляризации λ хранится внутри reg. Коэффициент удобства 0.5 умножения регуляризации станет ясен через секунду. Оценка этого вначале (со случайными параметрами) может дать нам loss = 1.1, что и есть -np.log(1.0/3), поскольку при небольших начальных случайных весах все вероятности, присвоенные всем классам, составляют около одной трети. Теперь мы хотим сделать потери как можно более низкими, используя loss = 0 в качестве абсолютной нижней границы. Но чем меньше потери, тем выше вероятности, присвоенные правильным классам для всех примеров.

Вычисление аналитического градиента с обратным распространением

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

$$ p_k = \frac{e^{f_k}}{ \sum_j e^{f_j} } \hspace{1in} L_i =-\log\left(p_{y_i}\right) $$

Теперь мы хотим понять, как вычисляются баллы внутри f следует изменить, чтобы уменьшить потери \(L_i\), что этот пример соответствует общей цели. Другими словами, мы хотим вычислить градиент \( \partial L_i / \partial f_k \). Потеря __\(L_i\)__вычисляется из p, что , в свою очередь , зависит от f. Читателю будет интересно использовать правило дифференцирования сложной функции для вычисления градиента, но в итоге всё оказывается очень простым и понятным, после того как многое сокращается:

$$ \frac{\partial L_i }{ \partial f_k } = p_k - \mathbb{1}(y_i = k) $$

Обратите внимание, насколько элегантно и просто выглядит это выражение. Предположим, что вычисленные нами вероятности были p = [0.2, 0.3, 0.5] и что правильным классом был средний (с вероятностью 0,3). Согласно этому выводу, градиент оценок будет равен df = [0.2, -0.7, 0.5]. Вспомнив, что означает интерпретация градиента, мы видим, что этот результат вполне интуитивен: увеличение первого или последнего элемента вектора оценок f (оценок неверных классов) приводит к увеличению потерь (из-за положительных значений +0,2 и +0,5) — а увеличение потерь плохо, как и ожидалось. Однако увеличение оценки правильного класса отрицательно влияет на потери. Градиент -0,7 говорит нам о том, что увеличение оценки правильного класса приведёт к уменьшению потерь \(L_i\), что имеет смысл.

Всё это сводится к следующему коду. Напомним, что probs хранит вероятности всех классов (в виде строк) для каждого примера. Чтобы получить градиент оценок, который мы называем dscores, мы поступаем следующим образом:

dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples

Наконец, у нас есть scores = np.dot(X, W) + b и, вооружившись градиентом scores (хранящимся в dscores), мы можем выполнить обратное распространение ошибки в W и b:

dW = np.dot(X.T, dscores)
db = np.sum(dscores, axis=0, keepdims=True)
dW += reg*W # don't forget the regularization gradient

Здесь мы видим, что мы выполнили обратное преобразование с помощью операции умножения матриц, а также добавили вклад от регуляризации. Обратите внимание, что градиент регуляризации имеет очень простую форму reg*W, поскольку мы использовали константу 0.5 для вклада в потери (т. е. \(\frac{d}{dw} ( \frac{1}{2} \lambda w^2) = \lambda w\) ). Это распространенный удобный прием, который упрощает выражение градиента.

Выполнение обновления параметров

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

# perform a parameter update
W += -step_size * dW
b += -step_size * db

Сведение всего этого воедино: обучение классификатора Softmax

Если собрать всё это воедино, получится полный код для обучения классификатора Softmax с помощью градиентного спуска:

#Train a Linear Classifier

# initialize parameters randomly
W = 0.01 * np.random.randn(D,K)
b = np.zeros((1,K))

# some hyperparameters
step_size = 1e-0
reg = 1e-3 # regularization strength

# gradient descent loop
num_examples = X.shape[0]
for i in range(200):

  # evaluate class scores, [N x K]
  scores = np.dot(X, W) + b

  # compute the class probabilities
  exp_scores = np.exp(scores)
  probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # [N x K]

  # compute the loss: average cross-entropy loss and regularization
  correct_logprobs = -np.log(probs[range(num_examples),y])
  data_loss = np.sum(correct_logprobs)/num_examples
  reg_loss = 0.5*reg*np.sum(W*W)
  loss = data_loss + reg_loss
  if i % 10 == 0:
    print "iteration %d: loss %f" % (i, loss)

  # compute the gradient on scores
  dscores = probs
  dscores[range(num_examples),y] -= 1
  dscores /= num_examples

  # backpropate the gradient to the parameters (W,b)
  dW = np.dot(X.T, dscores)
  db = np.sum(dscores, axis=0, keepdims=True)

  dW += reg*W # regularization gradient

  # perform a parameter update
  W += -step_size * dW
  b += -step_size * db
  ```


 При выполнении этой операции выводятся выходные данные:  

 ```
 iteration 0: loss 1.096956
iteration 10: loss 0.917265
iteration 20: loss 0.851503
iteration 30: loss 0.822336
iteration 40: loss 0.807586
iteration 50: loss 0.799448
iteration 60: loss 0.794681
iteration 70: loss 0.791764
iteration 80: loss 0.789920
iteration 90: loss 0.788726
iteration 100: loss 0.787938
iteration 110: loss 0.787409
iteration 120: loss 0.787049
iteration 130: loss 0.786803
iteration 140: loss 0.786633
iteration 150: loss 0.786514
iteration 160: loss 0.786431
iteration 170: loss 0.786373
iteration 180: loss 0.786331
iteration 190: loss 0.786302

Мы видим, что после примерно 190 итераций мы приблизились к чему-то. Мы можем оценить точность обучающего набора данных:

# evaluate training set accuracy
scores = np.dot(X, W) + b
predicted_class = np.argmax(scores, axis=1)
print 'training accuracy: %.2f' % (np.mean(predicted_class == y))

Это выводит 49%. Не очень хорошо, но и неудивительно, учитывая, что набор данных составлен таким образом, что он не является линейно разделимым. Мы также можем построить границы принятых решений:



Линейный классификатор не может изучить набор данных toy spiral.


Обучение нейронной сети

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

# initialize parameters randomly
h = 100 # size of hidden layer
W = 0.01 * np.random.randn(D,h)
b = np.zeros((1,h))
W2 = 0.01 * np.random.randn(h,K)
b2 = np.zeros((1,K))

Прямой проход для подсчета очков теперь меняет форму:

# evaluate class scores with a 2-layer Neural Network
hidden_layer = np.maximum(0, np.dot(X, W) + b) # note, ReLU activation
scores = np.dot(hidden_layer, W2) + b2

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

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

```# backpropate the gradient to the parameters

first backprop into parameters W2 and b2

dW2 = np.dot(hidden_layer.T, dscores) db2 = np.sum(dscores, axis=0, keepdims=True)

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

dhidden = np.dot(dscores, W2.T)

Теперь у нас есть градиент на выходе скрытого слоя. Далее нам нужно выполнить обратное распространение ошибки для нелинейности **ReLU**. Это оказывается простым, потому что **ReLU** при обратном распространении ошибки фактически является переключателем. Поскольку **r=max(0,x)**, у нас есть это **dr/dx=1(x>0)**. В сочетании с правилом дифференцирования по частям мы видим, что блок **ReLU** пропускает градиент без изменений, если его входные данные больше 0, но _отменяет_ его, если входные данные меньше нуля во время прямого прохода. Следовательно, мы можем выполнить обратное распространение ошибки для **ReLU** следующим образом:  

backprop the ReLU non-linearity

dhidden[hidden_layer <= 0] = 0

И теперь мы, наконец, переходим к первому слою весов и смещений:  

finally into W,b

dW = np.dot(X.T, dhidden) db = np.sum(dhidden, axis=0, keepdims=True)

**Готово!** У нас есть градиенты `dW,db,dW2,db2` и мы можем выполнить обновление параметров. Всё остальное остаётся без изменений. Полный код выглядит очень похоже:  

initialize parameters randomly

h = 100 # size of hidden layer W = 0.01 * np.random.randn(D,h) b = np.zeros((1,h)) W2 = 0.01 * np.random.randn(h,K) b2 = np.zeros((1,K))

some hyperparameters

step_size = 1e-0 reg = 1e-3 # regularization strength

gradient descent loop

num_examples = X.shape[0] for i in range(10000):

# evaluate class scores, [N x K] hidden_layer = np.maximum(0, np.dot(X, W) + b) # note, ReLU activation scores = np.dot(hidden_layer, W2) + b2

# compute the class probabilities exp_scores = np.exp(scores) probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # [N x K]

# compute the loss: average cross-entropy loss and regularization correct_logprobs = -np.log(probs[range(num_examples),y]) data_loss = np.sum(correct_logprobs)/num_examples reg_loss = 0.5regnp.sum(WW) + 0.5regnp.sum(W2W2) loss = data_loss + reg_loss if i % 1000 == 0: print "iteration %d: loss %f" % (i, loss)

# compute the gradient on scores dscores = probs dscores[range(num_examples),y] -= 1 dscores /= num_examples

# backpropate the gradient to the parameters # first backprop into parameters W2 and b2 dW2 = np.dot(hidden_layer.T, dscores) db2 = np.sum(dscores, axis=0, keepdims=True) # next backprop into hidden layer dhidden = np.dot(dscores, W2.T) # backprop the ReLU non-linearity dhidden[hidden_layer <= 0] = 0 # finally into W,b dW = np.dot(X.T, dhidden) db = np.sum(dhidden, axis=0, keepdims=True)

# add regularization gradient contribution dW2 += reg * W2 dW += reg * W

# perform a parameter update W += -step_size * dW b += -step_size * db W2 += -step_size * dW2 b2 += -step_size * db2 ```

Это печатает:

iteration 0: loss 1.098744
iteration 1000: loss 0.294946
iteration 2000: loss 0.259301
iteration 3000: loss 0.248310
iteration 4000: loss 0.246170
iteration 5000: loss 0.245649
iteration 6000: loss 0.245491
iteration 7000: loss 0.245400
iteration 8000: loss 0.245335
iteration 9000: loss 0.245292

Точность обучения теперь равна:

# evaluate training set accuracy
hidden_layer = np.maximum(0, np.dot(X, W) + b)
scores = np.dot(hidden_layer, W2) + b2
predicted_class = np.argmax(scores, axis=1)
print 'training accuracy: %.2f' % (np.mean(predicted_class == y))

Что выводит 98%!. Мы также можем визуализировать границы решений:



Классификатор нейронной сети сжимает набор данных spiral.


Краткие сведения

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

Дополнительные материалы

Обучение нейронных сетей

Обучение нейронных сетей

Содержание: - Проверка градиента - Проверки здравомыслия - Присмотр за процессом обучения - Функция потерь - Точность поезда/вала - Соотношение весов:обновлений - Распределение активации/градиента на слой - Визуализация - Обновление параметров - Первый порядок (SGD), импульс, импульс Нестерова - Отжиг темпов обучения - Методы второго порядка - Дополнительные материалы - Методы адаптивной скорости обучения для каждого параметра (Adagrad, RMSProp) - Оптимизация гиперпараметров - Оценка - Модельные ансамбли - Краткая сводка - Дополнительные материалы

Обучение

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

Проверка градиента

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

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

$$ \frac{df(x)}{dx} = \frac{f(x + h) - f(x)}{h} \hspace{0.1in} \text{(bad, do not use)} $$

где h это очень небольшое число, на практике примерно 1e-5 или около того. На практике оказывается, что гораздо лучше использовать формулу центрированной разности вида:

$$ \frac{df(x)}{dx} = \frac{f(x + h) - f(x - h)}{2h} \hspace{0.1in} \text{(use instead)} $$

Для этого вам придется дважды оценить функцию потерь, чтобы проверить каждое измерение градиента (так что это примерно в 2 раза дороже), но аппроксимация градиента оказывается гораздо более точной. Чтобы убедиться в этом, можно использовать разложение Тейлора \(f(x+h)\) и \(f(x-h)\) и убедитесь, что первая формула содержит ошибку порядка O(h), в то время как вторая формула содержит только члены ошибки порядка \(O(h^2)\) (т.е. это приближение второго порядка).

Используйте относительную погрешность для сравнения. В чем особенности сравнения численного градиента \(f'_n\) и аналитический градиент \(f'_a\)? То есть, как мы узнаем, что они несовместимы? У вас может возникнуть соблазн отслеживать разницу \(\mid f'_a - f'_n \mid \) или его квадрат и определите проверку градиента как неудачную, если эта разница превышает пороговое значение. Однако это проблематично. Для примера рассмотрим случай, когда их разница равна 1e-4. Это кажется очень подходящей разницей, если два градиента близки к 1.0, поэтому мы считаем, что два градиента совпадают. Но если бы оба градиента были порядка 1e-5 или ниже, то мы бы считали 1e-4 огромной разницей и, скорее всего, неудачей. Следовательно, всегда более уместно учитывать относительную ошибку:

$$ \frac{\mid f'_a - f'_n \mid}{\max(\mid f'_a \mid, \mid f'_n \mid)} $$

которая рассматривает отношение их разностей к отношению абсолютных значений обоих градиентов. Обратите внимание, что обычно формула относительной ошибки включает только один из двух членов (любой из них), но я предпочитаю увеличивать (или добавлять) оба, чтобы сделать его симметричным и предотвратить деление на ноль в случае, когда одно из двух равно нулю (что часто случается, особенно с ReLU). Тем не менее, необходимо явно отслеживать случай, когда оба равны нулю, и пройти проверку градиента в этом крайнем случае. На практике:
- Относительная погрешность > 1e-2 обычно означает, что градиент, вероятно, неправильный - 1e-2 > относительная погрешность > 1e-4 должна заставить вас чувствовать себя некомфортно - 1e-4 > относительная погрешность обычно приемлема для целей с изломами. Но если нет перегибов (например, использование нелинейностей tanh и softmax), то 1e-4 слишком велико. - 1e-7 и меньше вы должны быть счастливы.

Также имейте в виду, что чем глубже сеть, тем выше будут относительные ошибки. Таким образом, если вы проверяете входные данные для 10-слойной сети, относительная ошибка 1e-2 может быть нормальной, потому что ошибки накапливаются по мере прохождения. И наоборот, ошибка 1e-2 для одной дифференцируемой функции, скорее всего, указывает на неправильный градиент.

Используйте двойную точность. Распространенной ошибкой является использование плавающей точки одинарной точности для вычисления проверки градиента. Часто бывает так, что вы можете получить высокие относительные ошибки (до 1e-2) даже при правильной реализации градиента. По моему опыту, я иногда видел, как мои относительные ошибки резко уменьшались с 1e-2 до 1e-8 при переходе на двойную точность.

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

Однако, если градиенты для каждой точки данных очень малы, то дополнительное деление их на количество точек данных начинает давать очень маленькие числа, что, в свою очередь, приведет к большему количеству числовых проблем. Вот почему я предпочитаю всегда печатать исходный числовой/аналитический градиент и следить за тем, чтобы числа, которые вы сравниваете, не были слишком маленькими (например, примерно 1e-10 и меньше по абсолютному значению вызывает беспокойство). Если это так, вы можете временно масштабировать функцию потерь на константу, чтобы привести их к "более хорошему" диапазону, где числа с плавающей запятой более плотные - в идеале порядка 1,0, где экспонента с плавающей запятой равна 0.

Изгибы в достижении цели. Одним из источников неточностей, о которых следует знать при проверке градиента, является проблема изгибов. Изгибы относятся к недифференцируемым частям целевой функции, вводимым такими функциями, как ReLU (\(max(0,x)\)) ) или потеря SVM, нейроны Maxout и т.д. Рассмотрим градиентную проверку функции ReLU по адресу \(x = -1e6\). С \(x < 0\), аналитический градиент в этой точке равен нулю. Однако числовой градиент внезапно вычислит ненулевой градиент, потому что \(f(x+h)\) может пересечь излом (например, если \(h > 1e-6\) ) и ввести ненулевой взнос. Вы можете подумать, что это патологический случай, но на самом деле этот случай может быть очень распространенным. Например, СВМ для CIFAR-10 содержит до 450 000 (\(max(0,x)\)) термины, потому что существует 50 000 примеров, и каждый пример дает 9 терминов для цели. Более того, нейронная сеть с классификатором SVM будет содержать гораздо больше изломов из-за ReLU.

Обратите внимание, что можно узнать, был ли пересечен излом при оценке убытка. Это можно сделать, отслеживая личности всех «победителей» в функции формы (\(max(0,x)\)); То есть был x или y выше во время паса вперед. Если при оценке изменилась личность хотя бы одного победителя \(f(x+h)\), а в последствии \(f(x-h)\), то был пересечен излом и числовой градиент не будет точным.

Используйте только несколько точек данных. Одним из решений вышеупомянутой проблемы перегибов является использование меньшего количества точек данных, поскольку функции потерь, которые содержат перегибы (например, из-за использования ReLU или маржинальных потерь и т. д.), будут иметь меньше перегибов с меньшим количеством точек данных, поэтому вероятность того, что вы пересечете одну из них, при выполнении конечного другого приближения, снижается. Более того, если ваш gradcheck всего на ~2 или 3 точки данных, то вы почти наверняка проверите весь пакет. Использование очень небольшого количества точек данных также делает проверку градиента быстрее и эффективнее.

Будьте осторожны с размером шага h. Не обязательно минимальный размер h- это хорошо, так как в таком случае есть шанс напороться на проблему численной точности. Иногда, при проверке корректности градиента h, возможно, что значение 1е-4 и 1е-6 будет изменяться от абсолютно неверному при увеличении этого показателя. Эта статья в Википедии содержит диаграмму, которая отображает значение h по оси x и числовую ошибку градиента по оси y.

Градусная проверка во время «характерного» режима работы. Важно понимать, что проверка градиента выполняется в определенной (и обычно случайной), единственной точке в пространстве параметров. Даже если проверка градиента на этом этапе выполнена успешно, не сразу можно быть уверенным в том, что градиент правильно реализован глобально. Кроме того, случайная инициализация может быть не самой «характерной» точкой в пространстве параметров и фактически может привести к патологическим ситуациям, когда градиент кажется правильно реализованным, но на самом деле это не так. Например, SVM с очень малой инициализацией веса присвоит почти ровно нулевые оценки всем точкам данных, а градиенты будут демонстрировать определенную закономерность во всех точках данных. Неправильная реализация градиента все равно может привести к появлению этого шаблона и не привести к более характерному режиму работы, в котором одни баллы больше других. Поэтому, чтобы быть в безопасности, лучше всего использовать короткое время прогорания, в течение которого сеть может обучиться и выполнить градиентную проверку после того, как потери начнут снижаться. Опасность его выполнения на первой итерации заключается в том, что это может привести к патологическим пограничным случаям и замаскировать неправильную реализацию градиента.

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

Не забудьте отключить выпадение/аугментации. Выполняя градиентную проверку, не забывайте отключать любые недетерминированные эффекты в сети, такие как выпадение, случайные аугментации данных и т. д. В противном случае это может привести к огромным ошибкам при оценке численного градиента. Недостатком отключения этих эффектов является то, что вы не будете проверять их градиент (например, может случиться так, что выпадение не будет правильно распространено). Следовательно, лучшим решением может быть принудительное использование определенного случайного начального значения перед оценкой обоих \(f(x+h)\) и \(f(x-h)\), а также при оценке аналитического градиента.

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

Перед изучением: советы и рекомендации по проверке здравомыслия

Вот несколько проверок здравого смысла, которые вы могли бы провести, прежде чем погрузиться в дорогостоящую оптимизацию: - Ищите правильный проигрыш при случайном исполнении. Убедитесь, что вы получаете ожидаемые потери при инициализации с небольшими параметрами. Лучше всего сначала проверить только потерю данных (поэтому установите интенсивность регуляризации равной нулю). Например, для CIFAR-10 с классификатором Softmax мы ожидаем, что начальный убыток составит 2,302, потому что мы ожидаем диффузную вероятность 0,1 для каждого класса (поскольку классов 10), а Softmax убыток — это отрицательная логарифмическая вероятность правильного класса, таким образом: -ln(0,1) = 2,302. Для The Weston Watkins SVM мы ожидаем, что все желаемые маржи будут нарушены (поскольку все баллы примерно равны нулю), и, следовательно, ожидаем потери 9 (поскольку маржа равна 1 для каждого неправильного класса). Если вы не видите этих потерь, возможно, возникла проблема с инициализацией.

  • В качестве второй проверки здравомыслия, увеличение силы регуляризации должно привести к увеличению потерь
  • Переобучение крошечного подмножества данных. И последнее, и самое важное, прежде чем обучаться на полном наборе данных, попытайтесь обучиться на крошечной части (например, на 20 примерах) ваших данных и убедитесь, что вы можете достичь нулевой стоимости. Для этого эксперимента также лучше всего установить регуляризацию равной нулю, иначе это может помешать получению нулевой стоимости. Если вы не пройдете эту проверку на здравомыслие с небольшим набором данных, не стоит переходить к полному набору данных. Обратите внимание, что может случиться так, что вы можете переобучать очень маленький набор данных, но все равно иметь неправильную реализацию. Например, если признаки точек данных являются случайными из-за какой-либо ошибки, то можно перенаучить небольшой обучающий набор, но вы никогда не заметите обобщения при свертывании всего набора данных.

Присмотр за процессом обучения

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

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

Функция потерь

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



Сверху: Мультфильм, изображающий эффекты различных скоростей обучения. При низких темпах обучения улучшения будут линейными. С высокими темпами обучения они начнут выглядеть более экспоненциально. Более высокие темпы обучения будут уменьшать потери быстрее, но они застревают на худших значениях потерь (зеленая линия). Это связано с тем, что в оптимизации слишком много «энергии», а параметры хаотично колеблются, не в силах занять хорошее место в ландшафте оптимизации. Снизу: Пример типичной функции потерь во времени при обучении небольшой сети на наборе данных CIFAR-10. Эта функция потерь выглядит разумной (она может указывать на слишком маленькую скорость обучения, основанную на скорости распада, но трудно сказать), а также указывает на то, что размер партии может быть слишком низким (поскольку стоимость слишком зашумлена).


Величина «покачивания» в потерях связана с размером партии. Когда размер партии равен 1, покачивание будет относительно большим. Если размер пакета равен полному набору данных, покачивание будет минимальным, так как каждое обновление градиента должно монотонно улучшать функцию потерь (если только скорость обучения не установлена слишком высокой).

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

Иногда функции проигрыша могут выглядеть забавно lossfunctions.tumblr.com.

Точность поезда/вала

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


Разрыв между точностью обучения и валидации указывает на степень переобучения. Два возможных случая показаны на схеме слева. Синяя кривая ошибок валидации показывает очень низкую точность валидации по сравнению с точностью обучения, что указывает на сильное переобучение (обратите внимание, что точность валидации может даже начать снижаться после какого-то момента). Когда вы видите это на практике, вы, вероятно, захотите увеличить регуляризацию (сильнее штраф в весе \(L_2\), больше отсева и т.д.) или собрать больше данных. Другой возможный случай — когда точность валидации достаточно хорошо отслеживает точность обучения. Этот случай указывает на то, что емкость вашей модели недостаточно высока: увеличьте модель, увеличив количество параметров.


Соотношение весов:обновления

Последняя величина, которую вы, возможно, захотите отслеживать, — это отношение величин обновления к величине значений. Примечание: обновления, а не исходные градиенты (например, в ванильном sgd это будет градиент, умноженный на скорость обучения). Возможно, вы захотите оценить и отследить это соотношение для каждого набора параметров независимо. Грубая эвристика заключается в том, что это соотношение должно быть где-то в районе 1e-3. Если он ниже, то скорость обучения может быть слишком низкой. Если он выше, то, скорее всего, уровень обучения слишком высок. Вот конкретный пример:

# assume parameter vector W and its gradient vector dW
param_scale = np.linalg.norm(W.ravel())
update = -learning_rate*dW # simple SGD update
update_scale = np.linalg.norm(update.ravel())
W += update # the actual update
print update_scale / param_scale # want ~1e-3

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

Распределение активации/градиента на слой

Неправильная инициализация может замедлить или даже полностью затормозить процесс обучения. К счастью, эту проблему можно диагностировать относительно легко. Одним из способов сделать это является построение гистограмм активации/градиента для всех слоев сети. Интуитивно понятно, что не очень хорошо видеть какие-либо странные распределения - например, с нейронами tanh мы хотели бы видеть распределение активаций нейронов между полным диапазоном [-1,1], вместо того, чтобы видеть, как все нейроны выдают ноль, или все нейроны полностью насыщаются либо при -1, либо при 1.

Визуализации первого уровня

Наконец, при работе с пикселями изображения может быть полезно и приятно визуально отобразить объекты первого слоя:




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


Обновление параметров

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

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

Первый порядок (SGD), импульс, импульс Нестерова

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

# Vanilla update
x += - learning_rate * dx

где гиперпараметр - фиксированная константа. При оценке на полном наборе данных и при достаточно низкой скорости обучения это гарантированно приведет к неотрицательному прогрессу в функции потерь.learning_rate

Обновление импульса (Momentum update) — еще один подход, который почти всегда имеет более высокую скорость сходимости в глубоких сетях. Это обновление может быть мотивировано с физической точки зрения задачи оптимизации. В частности, потери можно интерпретировать как высоту холмистой местности (и, следовательно, также как потенциальную энергию, так как U=mgh. И поэтому \( U \propto h \) ). Инициализация параметров случайными числами эквивалентна установке частицы с нулевой начальной скоростью в каком-либо месте. В этом случае процесс оптимизации можно рассматривать как эквивалент процесса моделирования вектора параметров (т.е. частицы) как движущейся по ландшафту.

Поскольку сила, действующая на частицу, связана с градиентом потенциальной энергии (т.е. F=−∇U ), сила, ощущаемая частицей, в точности является (отрицательным) градиентом функции потерь. Сверх того F=ma. Таким образом, (отрицательный) градиент с этой точки зрения пропорционален ускорению частицы. Обратите внимание, что это отличается от показанного выше обновления SGD, где градиент напрямую интегрирует положение. Вместо этого физический взгляд предлагает обновление, в котором градиент только напрямую влияет на скорость, что, в свою очередь, влияет на положение:

```

Momentum update

v = mu * v - learning_rate * dx # integrate velocity x += v # integrate position

Здесь мы видим введение переменной `v`, которая инициализируется нулем, и дополнительный гиперпараметр (`mu`). К сожалению, эта переменная в оптимизации называется _импульсом_ (ее типичное значение составляет около **0,9**), но ее физическое значение больше соответствует коэффициенту трения. По сути, эта переменная гасит скорость и снижает кинетическую энергию системы, иначе частица никогда бы не остановилась у подножия холма. При перекрестной проверке этому параметру обычно присваиваются такие значения, как **[0.5, 0.9, 0.95, 0.99]**. Подобно графикам отжига для темпов обучения (*обсуждается ниже*), оптимизация иногда может немного выиграть от графиков импульса, где импульс увеличивается на более поздних этапах обучения. Типичная настройка заключается в том, чтобы начать с импульса около **0,5** и отжечь его до **0,99** или около того в течение нескольких эпох.`v` `mu`  

>При обновлении Momentum вектор параметра будет наращивать скорость в любом направлении, которое имеет постоянный градиент.  

__Импульс Нестерова__ (*Nesterov Momentum*)  это немного другая версия обновления *Momentum*, которое в последнее время набирает популярность. Он обладает более сильными теоретическими гарантиями сходимости для выпуклых функций и на практике также стабильно работает немного лучше стандартного импульса.  

Основная идея метода Нестерова заключается в том, что когда текущий вектор параметров находится в некотором положении `x`, то, глядя на приведённое выше обновление импульса, мы знаем, что только импульс (то есть без учёта второго слагаемого с градиентом) должен сдвинуть вектор параметров на `mu * v`. Поэтому, если мы собираемся вычислить градиент, мы можем рассматривать будущее приблизительное положение `x + mu * v` как «забежание вперёд»  это точка в окрестности того места, где мы вскоре окажемся. Следовательно, имеет смысл вычислять градиент в `x + mu * v` вместо «старой/устаревшей» позиции `x`.  
___  

  ![](https://cs231n.github.io/assets/nn3/nesterov.jpeg)  

Нестеровский импульс. Вместо того, чтобы оценивать градиент в текущем положении (красный круг), мы знаем, что наш импульс вот-вот приведет нас к кончику зеленой стрелки. Таким образом, с помощью импульса Нестерова мы оцениваем градиент в этой «просматриваемой» позиции.  
  ___  

То есть, в немного неудобной нотации, мы хотели бы сделать следующее:  

  ```
  x_ahead = x + mu * v
# evaluate dx_ahead (the gradient at x_ahead instead of at x)
v = mu * v - learning_rate * dx_ahead
x += v

Однако на практике люди предпочитают выражать обновление так, чтобы оно было максимально похоже на ванильный SGD или на предыдущее импульсное обновление. Этого можно достичь, манипулируя приведенным выше обновлением с помощью переменной transform , а затем выражая обновление в терминах вместо . То есть, вектор параметров, который мы на самом деле сохраняем, всегда является опережающей версией. Уравнения в терминах (но переименовывая его обратно в ) становятся такими:x_ahead = x + mu * v x_ahead x x_ahead x

v_prev = v # back this up
v = mu * v - learning_rate * dx # velocity update stays the same
x += -mu * v_prev + (1 + mu) * v # position update changes form

Мы рекомендуем эту дополнительную литературу, чтобы понять источник этих уравнений и математическую формулировку ускоренного импульса Нестерова (NAG):

Отжиг темпов обучения

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

  • Шаг затухания: Уменьшайте скорость обучения в несколько раз каждые несколько эпох. Типичными значениями могут быть снижение скорости обучения вдвое каждые 5 эпох или на 0,1 каждые 20 эпох. Эти цифры в значительной степени зависят от типа задачи и модели. Одна из эвристик, которую вы можете увидеть на практике, заключается в том, чтобы наблюдать за ошибкой валидации во время обучения с фиксированной скоростью обучения и уменьшать скорость обучения на константу (например, 0,5) всякий раз, когда ошибка валидации перестает улучшаться.

  • Экспоненциальный затухание. имеет математическую форму \(\alpha = \alpha_0 e^{-k t}\), где \(\alpha_0, k\) являются гиперпараметрами и t — номер итерации (но можно использовать и единицы измерения эпох).

  • Распад на 1/т имеет математический вид \(\alpha = \alpha_0 / (1 + k t )\), где \(a_0, k\) являются гиперпараметрами и t — номер итерации.

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

Методы второго порядка

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

$$ x \leftarrow x - [H f(x)]^{-1} \nabla f(x) $$

Здесь Hf(x)матрица Гессена, представляющая собой квадратную матрицу частных производных функции второго порядка. Термин ∇f(x)— вектор градиента, как показано в Gradient Descent. Интуитивно гессенский метод описывает локальную кривизну функции потерь, что позволяет нам выполнить более эффективное обновление. В частности, умножение на обратное гессенское значение приводит к тому, что оптимизация делает более агрессивные шаги в направлениях малой кривизны и более короткие шаги в направлениях крутой кривизны. Обратите внимание, что особенно важно, на отсутствие каких-либо гиперпараметров скорости обучения в формуле обновления, что сторонники этих методов называют большим преимуществом по сравнению с методами первого порядка.

Тем не менее, приведенное выше обновление непрактично для большинства приложений глубокого обучения, потому что вычисление (и инвертирование) гессена в его явной форме является очень дорогостоящим процессом как в пространстве, так и во времени. Например, нейронная сеть с одним миллионом параметров будет иметь гессенову матрицу размером [1 000 000 x 1 000 000], занимающую примерно 3725 гигабайт оперативной памяти. Следовательно, было разработано большое разнообразие квазиньютоновских методов, которые стремятся аппроксимировать обратный гессенский метод. Среди них наиболее популярным является L-BFGS, который использует информацию в градиентах с течением времени для неявного формирования аппроксимации (т.е. полная матрица никогда не вычисляется).

Тем не менее, даже после того, как мы устраним проблемы с памятью, большим недостатком наивного применения L-BFGS является то, что его приходится вычислять по всему обучающему набору, который может содержать миллионы примеров. В отличие от мини-партий SGD, заставить L-BFGS работать с мини-партиями сложнее и является активной областью исследований.

На практике в настоящее время не часто можно увидеть, чтобы L-BFGS или аналогичные методы второго порядка применялись к крупномасштабному глубокому обучению и сверточным нейронным сетям. Вместо этого варианты SGD, основанные на импульсе (Нестерова), более стандартны, потому что они проще и легче масштабируются.

Дополнительные материалы:

  • Large Scale Distributed Deep Networks — это статья от команды Google Brain, в которой сравниваются варианты L-BFGS и SGD в крупномасштабной распределенной оптимизации.
  • Алгоритм SFO стремится объединить преимущества SGD с преимуществами L-BFGS.

Методы адаптивной скорости обучения для каждого параметра

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

Adagrad — это метод адаптивной скорости обучения, первоначально предложенный Дучи и др.

# Assume the gradient dx and parameter vector x
cache += dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

Обратите внимание, что переменная имеет размер, равный размеру градиента, и отслеживает сумму квадратов градиентов по каждому параметру. Затем это используется для нормализации шага обновления параметров по элементам. Обратите внимание, что для весов, получающих высокие градиенты, эффективная скорость обучения будет снижена, в то время как для весов, получающих небольшие или нечастые обновления, эффективная скорость обучения будет увеличена. Забавно, но операция извлечения квадратного корня оказывается очень важной, и без нее алгоритм работает гораздо хуже. Сглаживание (обычно задается в диапазоне от 1e-4 до 1e-8) позволяет избежать деления на ноль. Недостатком Adagrad является то, что в случае глубокого обучения монотонный темп обучения обычно оказывается слишком агрессивным и прекращает обучение слишком рано.cache eps

RMSprop — это очень эффективный, но в настоящее время неопубликованный метод адаптивной скорости обучения. Забавно, что все, кто использует этот метод в своей работе, в настоящее время цитируют слайд 29 лекции 6 курса Джеффа Хинтона на Coursera. Обновление RMSProp очень просто корректирует метод Adagrad в попытке снизить его агрессивную, монотонно снижающуюся скорость обучения. В частности, вместо этого он использует скользящее среднее квадратов градиентов, дающее:

cache = decay_rate * cache + (1 - decay_rate) * dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

Здесь находится гиперпараметр decay_rate, типичные значения которого равны [0.9, 0.99, 0.999]. Обратите внимание, что обновление x+= идентично Adagrad, но переменнаяcache "учетка". Следовательно, RMSProp по-прежнему модулирует скорость обучения каждого веса на основе величин его градиентов, что имеет положительный уравнительный эффект, но в отличие от Adagrad обновления не становятся монотонно меньше.

Адам. Adam — это недавно предложенное обновление, которое немного похоже на RMSProp с импульсом. (Упрощённое) обновление выглядит следующим образом:

m = beta1*m + (1-beta1)*dx
v = beta2*v + (1-beta2)*(dx**2)
x += - learning_rate * m / (np.sqrt(v) + eps)

Обратите внимание, что обновление выглядит точно так же, как обновление RMSProp, за исключением того, что вместо необработанного (и, возможно, зашумленного) вектора градиента dx используется “сглаженная” версия градиента m. Рекомендуемые значения в документе - eps = 1e-8, beta1 = 0,9, beta2 = 0,999. На практике Adam в настоящее время рекомендуется использовать в качестве алгоритма по умолчанию и часто работает немного лучше, чем RMSProp. Однако часто также стоит попробовать SGD+Nesterov Momentum в качестве альтернативы. Полное обновление Adam также включает механизм коррекции смещения, который компенсирует тот факт, что на первых нескольких временных шагах векторы m,v инициализируются и, следовательно, смещаются на ноль, прежде чем они полностью “разогреются”. С механизмом коррекции смещения обновление выглядит следующим образом:

# t is your iteration counter going from 1 to infinity
m = beta1*m + (1-beta1)*dx
mt = m / (1-beta1**t)
v = beta2*v + (1-beta2)*(dx**2)
vt = v / (1-beta2**t)
x += - learning_rate * mt / (np.sqrt(vt) + eps)

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

Дополнительные ссылки:

  • Модульные тесты для стохастической оптимизации предлагают серию тестов в качестве стандартизированного эталона для стохастической оптимизации.

Анимация, которая может помочь вашей интуиции о динамике процесса обучения. Сверху: Контуры поверхности потерь и временная эволюция различных алгоритмов оптимизации. Обратите внимание на «чрезмерное» поведение методов, основанных на импульсе, из-за чего оптимизация выглядит как мяч, катящийся с горки.
Снизу: Визуализация седловой точки в ландшафте оптимизации, где кривизна по разным размерностям имеет разные знаки (одно измерение искривляется вверх, а другое вниз). Обратите внимание, что SGD с трудом нарушает симметрию и застревает на вершине. И наоборот, такие алгоритмы, как RMSprop, будут видеть очень низкие градиенты в направлении седла. Из-за знаменателя в обновлении RMSprop это увеличит эффективную скорость обучения в этом направлении, что поможет RMSProp двигаться дальше. Изображения предоставлены: Алек Рэдфорд.


Оптимизация гиперпараметров

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

  • начальная скорость обучения
  • График снижения скорости обучения (например, постоянная затухания)
  • регуляризация силы (штраф \(L_2\), сила отсева)

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

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

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

Диапазоны гиперпараметров. Поиск гиперпараметров в логарифмической шкале. Например, типичная выборка коэффициента обучения будет выглядеть следующим образом: learning_rate = 10 ** uniform(-6, 1). То есть мы генерируем случайное число из равномерного распределения, но затем возводим его в степень 10. Та же стратегия должна быть использована и для силы регуляризации. Интуитивно это объясняется тем, что скорость обучения и сила регуляризации оказывают мультипликативное влияние на динамику тренировки. Например, фиксированное изменение при добавлении 0,01 к коэффициенту обучения оказывает огромное влияние на динамику, если коэффициент обучения равен 0,001, но почти не оказывает никакого влияния, если коэффициент обучения равен 10. Это связано с тем, что скорость обучения умножает вычисленный градиент в обновлении. Следовательно, гораздо естественнее рассматривать диапазон скорости обучения, умноженный или деленный на некоторую величину, чем диапазон скорости обучения, прибавленный или вычтенный на некоторую величину. Некоторые параметры (например, отсеивание) обычно ищутся в исходной шкале (например, dropout = uniform(0,1)).

Отдайте предпочтение случайному поиску, а не поиску по сетке. Как утверждают Бергстра и Бенджио в книге «Случайный поиск для оптимизации гиперпараметров», «случайно выбранные испытания более эффективны для оптимизации гиперпараметров, чем испытания на сетке» . Как оказалось, это тоже обычно проще реализовать.


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


Осторожнее с лучшими значениями на границе. Иногда может случиться так, что вы ищете гиперпараметр (например, скорость обучения) в плохом диапазоне. Например, предположим, что мы используем learning_rate = 10 ** uniform(-6, 1) . Как только мы получим результаты, важно еще раз проверить, что итоговая скорость обучения не находится на краю этого интервала, иначе вы можете пропустить более оптимальную настройку гиперпараметров за пределами интервала.

Этапируйте свой поиск от грубого к хорошему. На практике может быть полезно сначала искать в грубых диапазонах (например, 10 ** [-6, 1]), а затем, в зависимости от того, где появляются наилучшие результаты, сужать диапазон. Кроме того, может быть полезно выполнить первоначальный грубый поиск во время обучения только за 1 эпоху или даже меньше, потому что многие настройки гиперпараметров могут привести к тому, что модель вообще не будет обучаться или сразу же взорвется с бесконечными затратами. Второй этап может выполнять более узкий поиск с 5 эпохами, а последний этап может выполнять детальный поиск в конечном диапазоне для гораздо большего количества эпох (например).

Байесовская оптимизация гиперпараметров — это целая область исследований, посвященная созданию алгоритмов, которые пытаются более эффективно ориентироваться в пространстве гиперпараметров. Основная идея заключается в том, чтобы правильно сбалансировать компромисс между исследованием и эксплуатацией при запросе производительности при различных гиперпараметрах. На основе этих моделей также было разработано несколько библиотек, среди наиболее известных — Spearmint, SMAC и Hyperopt. Тем не менее, в практических условиях с ConvNet все еще относительно сложно превзойти случайный поиск в тщательно выбранных интервалах. Смотрите дополнительную дискуссию из окопов здесь.

Оценка

Модельные ансамбли

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

  • Одна и та же модель, разные инициализации. Используйте перекрестную проверку для определения наилучших гиперпараметров, а затем обучите несколько моделей с лучшим набором гиперпараметров, но с разной случайной инициализацией. Опасность при таком подходе заключается в том, что сорт получается только за счет инициализации.
  • Лучшие модели, обнаруженные во время перекрестной проверки. Используйте перекрестную проверку для определения наилучших гиперпараметров, а затем выберите несколько лучших (например, 10) моделей для формирования ансамбля. Это повышает разнообразие ансамбля, но чревато опасностью включения неоптимальных моделей. На практике это может быть проще выполнить, так как не требует дополнительного переобучения моделей после перекрестной проверки
  • Разные контрольные точки одной модели. Если обучение стоит очень дорого, то некоторые люди имеют ограниченный успех в прохождении различных контрольных точек одной сети с течением времени (например, после каждой эпохи) и использовании их для формирования ансамбля. Очевидно, что это страдает от некоторого недостатка разнообразия, но все же может работать достаточно хорошо на практике. Преимущество такого подхода в том, что он очень дешевый.
  • Бегущее среднее по параметрам во время тренировки. Что касается последнего пункта, то дешевый способ почти всегда получить дополнительный процент или два производительности — это хранить в памяти вторую копию весовых коэффициентов сети, которая поддерживает экспоненциально уменьшающуюся сумму предыдущих весов во время обучения. Таким образом, вы усредняете состояние сети за последние несколько итераций. Вы обнаружите, что эта «сглаженная» версия весов за последние несколько шагов почти всегда приводит к лучшей ошибке проверки. Грубая интуиция, которую следует иметь в виду, заключается в том, что цель имеет форму чаши, и ваша сеть прыгает вокруг режима, поэтому среднее значение имеет больше шансов оказаться где-то ближе к режиму.

Одним из недостатков ансамблей моделей является то, что их оценка на тестовом примере занимает больше времени. Заинтересованного читателя может вдохновить недавняя работа Джеффа Хинтона «Темное знание», в которой идея состоит в том, чтобы «дистиллировать» хороший ансамбль обратно к одной модели, включив логарифмические правдоподобия ансамбля в модифицированную цель.

Краткая сводка

Чтобы обучить нейронную сеть:

  • Градиент: проверьте свою реализацию с помощью небольшого пакета данных и помните о подводных камнях.
  • В качестве проверки здравого смысла убедитесь, что ваши первоначальные потери разумны, и что вы можете достичь 100% точности обучения на очень небольшой части данных
  • Во время обучения отслеживайте потери, точность обучения/проверки, а если вы чувствуете себя более склонным, величину обновлений по отношению к значениям параметров (она должна быть ~1e-3), а при работе с ConvNet — веса первого слоя.
  • Рекомендуется использовать два обновления: SGD+Nesterov Momentum или Adam.
  • Снижайте скорость обучения в течение периода обучения. Например, уменьшите вдвое скорость обучения после фиксированного количества эпох или всякий раз, когда точность проверки достигает максимума.
  • Поиск хороших гиперпараметров с помощью случайного поиска (не поиска по сетке). Дифференцируйте поиск от грубого (широкие диапазоны гиперпараметров, обучение только для 1-5 эпох) до тонкого (более узкие рейнджеры, обучение для гораздо большего количества эпох)
  • Формируйте модельные ансамбли для дополнительной производительности

Дополнительные материалы

Предобработка, инициализация весов, функции потерь

Предобработка, инициализация весов, функции потерь

Содержание: - Настройка данных и модели + Предварительная обработка данных + Инициализация веса + Нормализация партии + Регуляризация (L2/L1/Maxnorm/Dropout) - Функции потерь - Краткая сводка

Настройка данных и модели

В предыдущем разделе мы представили модель нейрона, который вычисляет скалярное произведение с помощью нелинейности, и нейронные сети, которые объединяют нейроны в слои. Вместе эти решения определяют новую форму функции оценки, которую мы расширили по сравнению с простым линейным отображением, которое мы рассматривали в разделе «Линейная классификация». В частности, нейронная сеть выполняет последовательность линейных отображений с вложенными нелинейностями. В этом разделе мы обсудим дополнительные решения, касающиеся предварительной обработки данных, инициализации весов и функций потерь.

Предварительная обработка данных

Существует три распространённые формы предварительной обработки данных в матрице данных X, где мы будем предполагать, что X имеет размер [N x D] (N — количество данных, D — их размерность).

Вычитание среднего значения — наиболее распространённая форма предварительной обработки. Она заключается в вычитании среднего значения для каждого отдельного признака в данных и имеет геометрическую интерпретацию центрирования облака данных вокруг начала координат по каждому измерению. В numpy эта операция будет реализована следующим образом: (X -= np.mean(X, axis = 0). В случае с изображениями для удобства можно вычесть одно значение из всех пикселей (например, X -= np.mean(X)), либо сделать это отдельно для трёх цветовых каналов.

Нормализация — это приведение размерностей данных к примерно одинаковому масштабу. Существует два распространённых способа достижения такой нормализации: - Один из них — разделить каждую размерность на её стандартное отклонение после центрирования по нулю: (X /= np.std(X, axis = 0)). - Другой способ предварительной обработки — нормализовать каждую размерность так, чтобы минимальное и максимальное значения по каждой размерности составляли -1 и 1 соответственно. Имеет смысл применять эту предварительную обработку только в том случае, если у вас есть основания полагать, что разные входные параметры имеют разные масштабы (или единицы измерения), но они должны быть примерно одинаково важны для алгоритма обучения. В случае изображений относительные масштабы пикселей уже примерно одинаковы (и находятся в диапазоне от 0 до 255), поэтому нет необходимости выполнять этот дополнительный этап предварительной обработки.



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


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

# Assume input data matrix X of size [N x D]
X -= np.mean(X, axis = 0) # zero-center the data (important)
cov = np.dot(X.T, X) / X.shape[0] # get the data covariance matrix

Элемент (i,j) ковариационной матрицы данных содержит ковариацию между i-м и j-м измерениями данных. В частности, диагональ этой матрицы содержит дисперсии. Кроме того, ковариационная матрица является симметричной и положительно определённой. Мы можем вычислить разложение ковариационной матрицы данных по методу сингулярного разложения:

U,S,V = np.linalg.svd(cov)

где столбцы U являются собственными векторами, а S — одномерным массивом сингулярных значений. Чтобы устранить корреляцию в данных, мы проецируем исходные (но центрированные по нулю) данные на собственный базис:

Xrot = np.dot(X, U) # decorrelate the data

Обратите внимание, что столбцы U представляют собой набор ортогональных векторов (норма которых равна 1 и которые ортогональны друг другу), поэтому их можно рассматривать как базисные векторы. Таким образом, проекция соответствует повороту данных в X таким образом, чтобы новые оси были собственными векторами. Если бы мы вычислили ковариационную матрицу Xrot, то увидели бы, что теперь она диагональная. Преимущество np.linalg.svd в том, что в возвращаемом значении U столбцы собственных векторов отсортированы по собственным значениям. Мы можем использовать это для уменьшения размерности данных, используя только несколько главных собственных векторов и отбрасывая измерения, в которых данные не имеют дисперсии. Это также иногда называют анализом главных компонент (PCA) для уменьшения размерности:

Xrot_reduced = np.dot(X, U[:,:100]) # Xrot_reduced becomes [N x 100]

После этой операции мы уменьшили исходный набор данных размером [N x D] до размера [N x 100], сохранив 100 измерений данных, которые содержат наибольшую дисперсию. Очень часто можно добиться очень хорошей производительности, обучая линейные классификаторы или нейронные сети на наборах данных, уменьшенных с помощью метода главных компонент, что позволяет сэкономить место и время.

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

# whiten the data:
# divide by the eigenvalues (which are square roots of the singular values)
Xwhite = Xrot / np.sqrt(S + 1e-5)  

Предупреждение: усиливается шум. Обратите внимание, что мы добавляем 1e-5 (или небольшую константу), чтобы предотвратить деление на ноль. Одним из недостатков этого преобразования является то, что оно может сильно усиливать шум в данных, поскольку растягивает все измерения (включая несущественные измерения с небольшой дисперсией, которые в основном являются шумом) до одинакового размера на входе. На практике это можно смягчить более сильным сглаживанием (т. е. увеличив 1e-5 до большего числа).


PCA / Отбеливание. Слева: оригинальная игрушка, 2-мерные входные данные. Посередине: после выполнения PCA. Данные центрируются на нуле, а затем поворачиваются в собственный базис ковариационной матрицы данных. Это декоррелирует данные (ковариационная матрица становится диагональной). Справа: каждое измерение дополнительно масштабируется по собственным значениям, преобразуя матрицу ковариации данных в единичную матрицу. Геометрически это соответствует растяжению и сжатию данных в изотропный гауссовский большой объект.


Мы также можем попытаться визуализировать эти преобразования с помощью изображений CIFAR-10. Обучающий набор CIFAR-10 имеет размер 50 000 x 3072, где каждое изображение растягивается в вектор-строку размером 3072. Затем мы можем вычислить ковариационную матрицу [3072 x 3072] и вычислить её разложение по методу сингулярного значения (что может быть относительно затратным). Как выглядят вычисленные собственные векторы визуально? Возможно, вам поможет изображение:



Слева: пример набора из 49 изображений. 2-е слева: 144 главных собственных вектора из 3072. Главные собственные векторы объясняют большую часть дисперсии данных, и мы видим, что они соответствуют более низким частотам на изображениях. 2-е справа: 49 изображений, уменьшенных с помощью метода главных компонент с использованием 144 показанных здесь собственных векторов. То есть вместо того, чтобы представлять каждое изображение в виде 3072-мерного вектора, где каждый элемент — это яркость конкретного пикселя в определённом месте и на определённом канале, каждое изображение выше представлено только 144-мерным вектором, где каждый элемент показывает, какая часть каждого собственного вектора составляет изображение. Чтобы увидеть, какая информация об изображении содержится в этих 144 числах, мы должны вернуться к «пиксельному» базису из 3072 чисел. Поскольку U — это поворот, этого можно добиться, умножив на U.transpose()[:144,:], а затем, визуализировав полученные 3072 числа в виде изображения, вы можете заметить, что изображения немного размыты, что отражает тот факт, что верхние собственные векторы захватывают более низкие частоты. Однако большая часть информации всё равно сохраняется. Справа: визуализация «белого» представления, в котором дисперсия по каждому из 144 измерений сжата до одинаковой длины. Здесь 144 «белых» числа возвращаются к пикселям изображения путём умножения на U.transpose()[:144,:]. Более низкие частоты (на которые изначально приходилось больше всего дисперсии) теперь незначительны, а более высокие частоты (на которые изначально приходилось относительно мало дисперсии) становятся более выраженными.


На практике. Мы упоминаем метод главных компонент/отбеливание в этих заметках для полноты картины, но эти преобразования не используются в свёрточных сетях. Однако очень важно центрировать данные по нулевому значению, и часто также выполняется нормализация каждого пикселя.

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

Инициализация веса

Мы рассмотрели, как построить архитектуру нейронной сети и как предварительно обработать данные. Прежде чем приступить к обучению сети, необходимо инициализировать её параметры.

Ловушка: инициализация всех весов нулями. Давайте начнём с того, чего делать не следует. Обратите внимание, что мы не знаем, каким должно быть конечное значение каждого веса в обученной сети, но при правильной нормализации данных разумно предположить, что примерно половина весов будет положительной, а половина — отрицательной. Тогда разумной идеей может быть установка всех начальных весов в нулевое значение, что, как мы ожидаем, будет «наилучшим предположением». Это оказалось ошибкой, потому что если каждый нейрон в сети вычисляет один и тот же результат, значит все они также будут вычислять одни и те же градиенты во время обратного распространения ошибки и подвергаться одним и тем же обновлениям параметров. Другими словами, если веса нейронов инициализированы одинаково, то между ними не будет асимметрии.

Небольшие случайные числа. Поэтому мы по-прежнему хотим, чтобы весовые коэффициенты были очень близки к нулю, но, как мы уже говорили выше, не равнялись нулю. В качестве решения принято инициализировать весовые коэффициенты нейронов небольшими числами и называть это нарушением симметрии. Идея заключается в том, что изначально все нейроны случайны и уникальны, поэтому они будут вычислять разные обновления и интегрироваться в сеть как её различные части. Реализация для одной весовой матрицы может выглядеть так: W = 0.01* np.random.randn(D,H), где randn — выборки из гауссианы с нулевым средним и единичным стандартным отклонением. При такой формулировке вектор весов каждого нейрона инициализируется как случайный вектор, выбранный из многомерной гауссианы, поэтому нейроны ориентированы в случайном направлении во входном пространстве. Также можно использовать небольшие числа, выбранные из равномерного распределения, но на практике это, по-видимому, относительно мало влияет на конечную производительность.

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

Калибровка дисперсии с помощью 1/sqrt(n). Одна из проблем, связанных с вышеописанным предложением, заключается в том, что дисперсия выходных данных случайно инициализированного нейрона растёт с увеличением количества входных данных. Оказывается, мы можем нормализовать дисперсию выходных данных каждого нейрона до 1, масштабируя его вектор весов на квадратный корень из количества входов (т. е. количества входных данных). То есть рекомендуемая эвристика заключается в инициализации вектора весов каждого нейрона следующим образом: w = np.random.randn(n) / sqrt(n), где n — количество входных данных. Это гарантирует, что все нейроны в сети изначально имеют примерно одинаковое распределение выходных данных, и эмпирически улучшает скорость сходимости.

Схема вывода выглядит следующим образом:Рассмотрим внутренний продукт \(s = \sum_i^n w_i x_i\) между весами w и входные данные x, что даёт исходную активацию нейрона до нелинейности. Мы можем изучить дисперсию s:

$$ \begin{align} \text{Var}(s) &= \text{Var}(\sum_i^n w_ix_i) \\ &= \sum_i^n \text{Var}(w_ix_i) \\ &= \sum_i^n [E(w_i)]^2\text{Var}(x_i) + [E(x_i)]^2\text{Var}(w_i) + \text{Var}(x_i)\text{Var}(w_i) \\ &= \sum_i^n \text{Var}(x_i)\text{Var}(w_i) \\ &= \left( n \text{Var}(w) \right) \text{Var}(x) \end{align} $$

где на первых двух этапах мы использовали свойства дисперсии . На третьем этапе мы предположили, что входные данные и веса имеют нулевое среднее значение, поэтому \(E[x_i] = E[w_i] = 0\). Обратите внимание, что в общем случае это не так: например, блоки ReLU будут иметь положительное среднее значение. На последнем этапе мы предположили, что все \(w_i, x_i\) являются одинаково распределёнными. Из этого вывода следует, что если мы хотим s иметь ту же дисперсию, что и все его входные данные x, тогда во время инициализации мы должны убедиться, что дисперсия каждого веса w является 1/n. Следовательно при учете \(\text{Var}(aX) = a^2\text{Var}(X)\)) для случайной величины X и скаляра a говорит о необходимости взять единичный гауссовское распределение, а затем масштабировать его \(a = \sqrt{1/n}\), чтобы внести свой вклад в его дисперсию 1/n. Это дает инициализацию w = np.random.randn(n) / sqrt(n).

Аналогичный анализ проводится в статье «Понимание сложности обучения глубоких нейронных сетей прямого распространения» Глоро и др. В этой статье авторы в итоге рекомендуют инициализацию в виде Var(w)=2/(nin+nout), где \(n_in\), \(n_out)\ - это количество единиц в предыдущем слое и в следующем слое. Это основано на компромиссе и эквивалентном анализе градиентов обратного распространения. В более поздней статье на эту тему «Глубокое изучение выпрямителей: превосходные результаты на уровне человека при классификации ImageNet» Хе и др. выводится инициализация специально для нейронов ReLU, и делается вывод, что дисперсия нейронов в сети должна быть 2.0/n. Это даёт инициализацию w = np.random.randn(n) * sqrt(2.0/n) и является текущей рекомендацией для использования на практике в конкретном случае нейронных сетей с нейронами ReLU.

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

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

На практике в настоящее время рекомендуется использовать блоки ReLU и w = np.random.randn(n) * sqrt(2.0/n) в соответствии с Хе и др..

Нормализация партии

Нормализация с помощью пакетов. Недавно разработанная Иоффе и Сегеди техника под названием «Нормализация с помощью пакетов» избавляет от многих проблем, связанных с правильной инициализацией нейронных сетей, поскольку в начале обучения активации во всей сети принудительно распределяются по единичному гауссовскому распределению. Основное наблюдение заключается в том, что это возможно, потому что нормализация — это простая дифференцируемая операция. При реализации этой техники обычно вставляется слой BatchNorm сразу после полносвязных слоёв (или свёрточных слоёв, как мы вскоре увидим) и перед нелинейностями. Мы не будем подробно останавливаться на этом методе, поскольку он хорошо описан в статье по ссылке, но отметим, что использование пакетной нормализации в нейронных сетях стало очень распространённой практикой. На практике сети, использующие пакетную нормализацию, значительно более устойчивы к неправильной инициализации. Кроме того, пакетную нормализацию можно интерпретировать как предварительную обработку на каждом слое сети, но интегрированную в саму сеть дифференцируемым образом. Здорово!

Регуляризация

Существует несколько способов контроля возможностей нейронных сетей для предотвращения переобучения:

Регуляризация L2 — это, пожалуй, самая распространённая форма регуляризации. Её можно реализовать, штрафуя за квадраты значений всех параметров непосредственно в целевой функции. То есть для каждого веса w в сети мы добавляем термин \(\frac{1}{2} \lambda w^2\) к цели, где λ является силой регуляризации. Обычно наблюдается фактор \(\frac{1}{2}\) впереди, потому что тогда градиент этого члена по отношению к параметру w это просто λw вместо 2λw.Регуляризация \(L_2\) интуитивно понятна: она сильно штрафует векторы с пиковыми значениями и отдаёт предпочтение векторам с размытыми значениями. Как мы обсуждали в разделе «Линейная классификация», из-за мультипликативных взаимодействий между весами и входными данными это позволяет сети использовать все входные данные понемногу, а не некоторые из них — по максимуму. Наконец, обратите внимание, что при обновлении параметров методом градиентного спуска использование регуляризации \(L_2\) в конечном итоге означает, что каждый вес уменьшается линейно: W += -lambda * W по направлению к нулю.

Регуляризация L1 — ещё одна относительно распространённая форма регуляризации, при которой для каждого веса w мы добавляем термин λ∣w∣ к цели. Можно комбинировать регуляризацию \(L_1\) с регуляризацией \(L_2\): \(\lambda_1 \mid w \mid + \lambda_2 w^2\) (это называется регуляризацией эластичной сети). Регуляризация \(L_1\) обладает интригующим свойством: она приводит к тому, что весовые векторы во время оптимизации становятся разреженными (то есть очень близкими к нулю). Другими словами, нейроны с регуляризацией \(L_1\) в конечном итоге используют только разреженное подмножество наиболее важных входных данных и становятся почти невосприимчивыми к «зашумлённым» входным данным. Для сравнения, конечные весовые векторы при регуляризации \(L_2\) обычно представляют собой размытые, небольшие числа. На практике, если вас не интересует явный выбор признаков, можно ожидать, что регуляризация \(L_2\) будет работать лучше, чем регуляризация \(L_1\).

Ограничения по максимальной норме. Другой формой регуляризации является установление абсолютной верхней границы для величины вектора весов каждого нейрона и использование проецируемого градиентного спуска для обеспечения соблюдения ограничения. На практике это соответствует обычному обновлению параметров, а затем обеспечению соблюдения ограничения путём ограничения вектора весов \(\vec{w}\) каждого нейрона, чтобы удовлетворить \(\Vert \vec{w} \Vert_2 < c\). Типичные значения c. Они составляют порядка 3 или 4. Некоторые пользователи сообщают об улучшениях при использовании этой формы регуляризации. Одно из её привлекательных свойств заключается в том, что сеть не может «взрывообразно» расти, даже если скорость обучения установлена слишком высокой, потому что обновления всегда ограничены.

Выпадение — чрезвычайно эффективный, простой и недавно представленный метод регуляризации, описанный Шриваставой и др. в «Выпадении: простом способе предотвращения переобучения нейронных сетей» (pdf), который дополняет другие методы (\(L_1\), \(L_2\), maxnorm). Во время обучения выпадение реализуется путём активации нейрона только с некоторой вероятностью p (гиперпараметр), или в противном случае установив его равным нулю.


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


Выпадение в примере трёхслойной нейронной сети будет реализовано следующим образом:

``` """ Vanilla Dropout: Not recommended implementation (see notes below) """

p = 0.5 # probability of keeping a unit active. higher = less dropout

def train_step(X): """ X contains the data """

# forward pass for example 3-layer neural network H1 = np.maximum(0, np.dot(W1, X) + b1) U1 = np.random.rand(H1.shape) < p # first dropout mask H1 = U1 # drop! H2 = np.maximum(0, np.dot(W2, H1) + b2) U2 = np.random.rand(H2.shape) < p # second dropout mask H2 = U2 # drop! out = np.dot(W3, H2) + b3

# backward pass: compute gradients... (not shown) # perform parameter update... (not shown)

def predict(X): # ensembled forward pass H1 = np.maximum(0, np.dot(W1, X) + b1) * p # NOTE: scale the activations H2 = np.maximum(0, np.dot(W2, H1) + b2) * p # NOTE: scale the activations out = np.dot(W3, H2) + b3 ```

В приведённом выше коде внутри функции train_step мы дважды применили отсев: на первом скрытом слое и на втором скрытом слое. Отсев также можно применить непосредственно на входном слое, в этом случае мы также создадим бинарную маску для входных данных X. Обратный проход остаётся неизменным, но, конечно, должен учитывать созданные маски U1,U2.

Важно отметить, что в функции predict мы больше не отбрасываем значения, а выполняем масштабирование выходных значений скрытого слоя с помощью p.Это важно, потому что во время тестирования все нейроны видят все свои входные данные, поэтому мы хотим, чтобы выходные данные нейронов во время тестирования были идентичны ожидаемым выходным данным во время обучения. Например, в случае p=0.5. Нейроны должны вдвое уменьшить свои выходные данные во время тестирования, чтобы получить те же выходные данные, что и во время обучения (в среднем). Чтобы понять это, рассмотрим выходные данные нейрона x (до отбрасывания). При отбрасывании ожидаемый результат от этого нейрона станет px+(1−p)0, потому что выходной сигнал нейрона с вероятностью будет равен нулю 1−p. Во время тестирования, когда мы поддерживаем постоянную активность нейрона, мы должны корректировать x→px, чтобы сохранить ожидаемый результат. Также можно показать, что выполнение этого ослабления во время тестирования может быть связано с процессом перебора всех возможных бинарных масок (и, следовательно, всех экспоненциально большого количества подсетей) и вычисления их совокупного прогноза.

Нежелательным свойством представленной выше схемы является то, что мы должны масштабировать активации по p во время тестирования. Поскольку производительность во время тестирования очень важна, всегда предпочтительнее использовать инвертированное отбрасывание, при котором масштабирование выполняется во время обучения, а прямой проход во время тестирования остаётся нетронутым. Кроме того, это удобно тем, что код прогнозирования может оставаться нетронутым, если вы решите изменить место применения отбрасывания или отказаться от него. Инвертированное отбрасывание выглядит следующим образом:

``` """ Inverted Dropout: Recommended implementation example. We drop and scale at train time and don't do anything at test time. """

p = 0.5 # probability of keeping a unit active. higher = less dropout

def train_step(X): # forward pass for example 3-layer neural network H1 = np.maximum(0, np.dot(W1, X) + b1) U1 = (np.random.rand(H1.shape) < p) / p # first dropout mask. Notice /p! H1 = U1 # drop! H2 = np.maximum(0, np.dot(W2, H1) + b2) U2 = (np.random.rand(H2.shape) < p) / p # second dropout mask. Notice /p! H2 = U2 # drop! out = np.dot(W3, H2) + b3

# backward pass: compute gradients... (not shown) # perform parameter update... (not shown)

def predict(X): # ensembled forward pass H1 = np.maximum(0, np.dot(W1, X) + b1) # no scaling necessary H2 = np.maximum(0, np.dot(W2, H1) + b2) out = np.dot(W3, H2) + b3 ```

После первого появления метода отсева было проведено множество исследований, направленных на то, чтобы понять, в чём заключается его эффективность на практике и как он соотносится с другими методами регуляризации. Рекомендуем ознакомиться с дополнительной литературой для заинтересованных читателей:
- Статья о выпадении из курса Шриваставы и др., 2014. - Выборочное обучение как адаптивная регуляризация: «мы показываем, что регуляризация с помощью вычеркивания эквивалентна регуляризации \(L_2\) первого порядка, применяемой после масштабирования признаков с помощью оценки обратной диагональной информационной матрицы Фишера».

Тема шума при прямом проходе. Выпадение относится к более общей категории методов, которые вводят стохастическое поведение при прямом проходе сети. Во время тестирования шум усредняется аналитически (как в случае с выпадением при умножении на p) или численно (например, с помощью выборки, выполняя несколько прямых проходов с разными случайными решениями, а затем усредняя их). Примером других исследований в этом направлении является DropConnect, где во время прямого прохода случайный набор весовых коэффициентов устанавливается в ноль. В качестве предвосхищения отметим, что свёрточные нейронные сети также используют эту тему с помощью таких методов, как стохастическое объединение, дробное объединение и увеличение данных. Мы подробно рассмотрим эти методы позже.

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

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

На практике: чаще всего используется единая глобальная сила регуляризации \(L_2\), которая проходит перекрестную проверку. Также часто применяется комбинация с отбрасыванием данных после всех слоев. Значение p=0.5 это разумное значение по умолчанию, но его можно настроить на основе данных проверки.

Функции потерь

Мы обсудили часть целевой функции, отвечающую за регуляризацию, которую можно рассматривать как штраф за определённую меру сложности модели. Вторая часть целевой функции — это потеря данных, которая в задаче обучения с учителем измеряет соответствие между прогнозом (например, оценками классов при классификации) и истинным значением. Потеря данных представляет собой среднее значение потерь данных для каждого отдельного примера. То есть, \(L = \frac{1}{N} \sum_i L_i\) where \(N\), где N - количество обучающих данных. Давайте сократим \(f = f(x_i; W)\) активация выходного слоя в нейронной сети. Существует несколько типов задач, которые вы можете решить на практике: - Классификация — это случай, который мы подробно обсуждали. Здесь мы предполагаем наличие набора примеров и одной правильной метки (из фиксированного набора) для каждого примера. Одной из двух наиболее часто встречающихся функций стоимости в этой задаче является SVM (например, формулировка Уэстона Уоткинса):
$$
L_i = \sum_{j\neq y_i} \max(0, f_j - f_{y_i} + 1) $$
Как мы вкратце упомянули, некоторые люди сообщают о более высокой производительности при использовании квадратичной функции потерь (т. е. вместо \(\max(0, f_j - f_{y_i} + 1)^2\)). Вторым распространенным выбором является классификатор Softmax, который использует кросс-энтропийные потери:
$$
L_i = -\log\left(\frac{e^{f_{y_i}}}{ \sum_j e^{f_j} }\right) $$

Проблема: большое количество классов. Когда набор меток очень велик (например, слова в английском словаре или ImageNet, содержащий 22 000 категорий), вычисление полной вероятности по методу Softmax становится дорогостоящим. Для некоторых приложений популярны приближённые версии. Например, в задачах обработки естественного языка может быть полезно использовать иерархический Softmax (см. одно из объяснений здесь (pdf)).

Иерархический Softmax раскладывает слова на метки в виде дерева. Затем каждая метка представляется в виде пути по дереву, и в каждом узле дерева обучается классификатор Softmax, чтобы различать левую и правую ветви. Структура дерева сильно влияет на производительность и, как правило, зависит от задачи.

  • Классификация атрибутов. Оба приведённых выше примера предполагают, что существует единственный правильный ответ \(y_i\). Но что , если \(y_i\)- это бинарный вектор, в котором каждый пример может иметь или не иметь определённый атрибут, и где атрибуты не исключают друг друга? Например, изображения Вконтакте можно рассматривать как помеченные определённым подмножеством хэштегов из большого набора всех хэштегов, и изображение может содержать несколько хэштегов. Разумным подходом в этом случае будет создание бинарного классификатора для каждого отдельного атрибута. Например, бинарный классификатор для каждой категории отдельно будет иметь вид:
    $$ L_i = \sum_j \max(0, 1 - y_{ij} f_j) $$
    где сумма по всем категориям \(j\) и \(y_{ij}\) равно +1 или -1 в зависимости от того, помечен ли i-й пример j-м атрибутом, а вектор оценки \(f_j\) будет положительным, если класс прогнозируется как присутствующий, и отрицательным в противном случае. Обратите внимание, что потери накапливаются, если положительный пример имеет оценку меньше +1 или если отрицательный пример имеет оценку больше -1.
    Альтернативой этому подходу было бы обучение классификатора логистической регрессии для каждого атрибута по отдельности. Бинарный классификатор логистической регрессии имеет только два класса (0, 1) и вычисляет вероятность класса 1 следующим образом:
    $$ P(y = 1 \mid x; w, b) = \frac{1}{1 + e^{-(w^Tx +b)}} = \sigma (w^Tx + b) $$
    Поскольку сумма вероятностей классов 1 и 0 равна единице, вероятность класса 0 равна \(P(y = 0 \mid x; w, b) = 1 - P(y = 1 \mid x; w,b)\). Таким образом, пример классифицируется как положительный (y = 1), если \(\sigma (w^Tx + b) > 0.5\), или что эквивалентно , если оценка \(w^Tx +b > 0\). Затем функция потерь максимизирует эту вероятность. Вы можете убедиться, что это сводится к минимизации отрицательного логарифма правдоподобия:
    $$ L_i = -\sum_j y_{ij} \log(\sigma(f_j)) + (1 - y_{ij}) \log(1 - \sigma(f_j)) $$
    где этикетки \(y_{ij}\) считаются равными либо 1 (положительному), либо 0 (отрицательному), и \(\sigma(\cdot)\). Это сигмоидальная функция. Приведённое выше выражение может показаться пугающим, но градиент на f на самом деле он чрезвычайно прост и интуитивно понятен: \(\partial{L_i} / \partial{f_j} = \sigma(f_j) - y_{ij}\) (поскольку вы можете перепроверить себя, взяв производные).
  • Регрессия — это задача прогнозирования величин с действительными значениями, таких как цена дома или длина чего-либо на изображении. Для решения этой задачи обычно вычисляют потери между прогнозируемой величиной и истинным ответом, а затем измеряют норму \(L_2\) в квадрате или норму \(L_1\) разности. Норма \(L_2\) в квадрате вычисляет потери для одного примера в виде:
    $$ L_i = \Vert f - y_i \Vert_2^2 $$
    Причина, по которой норма \(L_2\) возводится в квадрат в целевой функции, заключается в том, что градиент становится намного проще, не меняя оптимальные параметры, поскольку возведение в квадрат — это монотонная операция. Норма \(L_1\) вычисляется путём суммирования абсолютных значений по каждому измерению:
    $$ L_i = \Vert f - y_i \Vert_1 = \sum_j \mid f_j - (y_i)j \mid $$
    _где сумма__ \(\sum_j\) это сумма по всем параметрам желаемого прогноза, если прогнозируется более одной величины. Рассмотрим только j-й параметр i-го примера и обозначим разницу между истинным и прогнозируемым значением как \(\delta_{ij}\), градиент для этого измерения (т.е. \(\partial{L_i} / \partial{f_j}\))) легко выводится как либо \(\delta_{ij}\) с нормой \(L_2\), или \(sign(\delta_{ij})\).То есть градиент оценки будет либо прямо пропорционален разнице в ошибках, либо будет фиксированным и унаследует только знак разницы.
    _Предупреждение
    : важно отметить, что функцию потерь \(L_2\) гораздо сложнее оптимизировать, чем более стабильную функцию потерь, такую как Softmax.

Интуитивно понятно, что для этого требуется очень хрупкое и специфическое свойство сети, чтобы она выдавала ровно одно правильное значение для каждого входного сигнала (и его расширений). Обратите внимание, что это не относится к Softmax, где точное значение каждого балла менее важно: важно только, чтобы их величины были соответствующими. Кроме того, функция потерь \(L_2\) менее устойчива, поскольку выбросы могут приводить к огромным градиентам. Столкнувшись с проблемой регрессии, сначала подумайте, абсолютно ли недостаточно квантовать выходные данные по ячейкам. Например, если вы прогнозируете звездный рейтинг продукта, возможно, гораздо лучше использовать 5 независимых классификаторов для оценок в 1-5 звезд вместо потери регрессии. Классификация имеет дополнительное преимущество в том, что она может дать вам распределение по результатам регрессии, а не только по одному результату без указания его достоверности. Если вы уверены, что классификация не подходит, используйте \(L_2\), но будьте осторожны: например, \(L_2\) более нестабилен, и применять отсев в сети (особенно на слое непосредственно перед потерей \(L_2\)) — не лучшая идея.

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

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

Краткая сводка

Подводя итог: - Рекомендуется предварительно обработать данные, чтобы их среднее значение было равно нулю, и нормализовать их масштаб до [-1, 1] по каждому признаку - Инициализируйте веса, извлекая их из гауссова распределения со стандартным отклонением \(\sqrt{2/n}\), where \(n\), где n - это количество входов в нейрон. Например, в NumPy: w = np.random.randn(n) * sqrt(2.0/n). - Используйте регуляризацию \(L_2\) и отсев (перевернутая версия) - Используйте пакетную нормализацию - Мы обсудили различные задачи, которые вы можете выполнять на практике, и наиболее распространённые функции потерь для каждой задачи

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

Сверточные сети. Введение

Сверточные сети. Введение

Сожержание: - Краткое вступление без мозговых аналогий - Моделирование одного нейрона + Биологическая мотивация и связи + Одиночный нейрон как линейный классификатор + Часто используемые функции активации - Архитектуры нейронных сетей + Многоуровневая организация + Пример вычисления с прямой связью + Представительская власть + Настройка количества слоев и их размеров - Краткие сведения - Дополнительные ссылки

Краткое вступление

Можно представить нейронные сети, не прибегая к аналогам с мозгом. В разделе о линейной классификации мы вычисляли баллы для различных визуальных категорий по изображению с помощью формулы s=Wx, где W была матрицей и x был вектор входных данных, содержащий все пиксельные данные изображения. В случае CIFAR-10 x является вектором-столбцом [3072x1], и W. Это матрица [10x3072], так что выходные данные представляют собой вектор из 10 оценок по классам.

Примерная нейронная сеть вместо этого вычисляла бы \( s = \( W_2 \max(0, W_1 x) \). Здесь, \(W_1\) может быть, например, матрицей [100x3072], преобразующей изображение в 100-мерный промежуточный вектор. Функция \(max(0,-) \) это нелинейность, котрая применяется поэлементно. Существует несколько вариантов нелинейности (которые мы рассмотрим ниже), но этот вариант является распространённым и просто приравнивает все значения ниже нуля к нулю. Наконец, матрица \(W_2\) тогда будет иметь размер [10x100], так что мы снова получим 10 чисел, которые мы интерпретируем как оценки классов. Обратите внимание, что нелинейность имеет решающее значение с точки зрения вычислений — если бы мы её не использовали, то две матрицы можно было бы объединить в одну, и, следовательно, прогнозируемые оценки классов снова были бы линейной функцией входных данных. Нелинейность — это то, что даёт нам колебания. Параметры W2,W1. Они обучаются с помощью стохастического градиентного спуска, а их градиенты вычисляются с помощью правила дифференцирования (и обратного распространения ошибки).

Аналогично трехслойная нейронная сеть могла бы выглядеть следующим образом \( s = W_3 \max(0, W_2 \max(0, W_1 x)) \), где все \(W_3, W_2, W_1\)- это параметры, которые необходимо изучить. Размеры промежуточных скрытых векторов являются гиперпараметрами сети, и мы рассмотрим, как их можно задать позже. Теперь давайте посмотрим, как можно интерпретировать эти вычисления с точки зрения нейронов/сети.

Моделирование одного нейрона

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

Биологическая мотивация и связи

Основной вычислительной единицей мозга является нейрон. В нервной системе человека насчитывается около 86 миллиардов нейронов, и они соединены примерно с 10^1410^15 синапсами. На схеме ниже показан схематичный рисунок биологического нейрона (сверху) и распространённая математическая модель (снизу). Каждый нейрон получает входные сигналы от своих дендритов и выдаёт выходные сигналы по своему (единственному) аксону. В конечном итоге аксон разветвляется и соединяется через синапсы с дендритами других нейронов. В вычислительной модели нейрона сигналы, которые проходят по аксонам (например, \(x_0\) взаимодействуют мультипликативно (например, \(w_0 x_0\) с дендритами другого нейрона в зависимости от силы синапса (например, \(w_0\). Идея заключается в том, что синаптические силы (веса w) являются обучаемыми и контролируют силу влияния (и его направление: возбуждающее (положительный вес) или тормозящее (отрицательный вес) одного нейрона на другой. В базовой модели дендриты передают сигнал в тело клетки, где он суммируется. Если итоговая сумма превышает определённый порог, нейрон может сработать, отправив импульс по своему аксону. В вычислительной модели мы предполагаем, что точное время срабатывания импульсов не имеет значения и что информация передаётся только частотой срабатывания. Основываясь на этой интерпретации, это частотного кода, мы моделируем частоту срабатывания нейрона с помощью функции активации f, которая представляет собой частоту импульсов вдоль аксона. Исторически сложилось так, что в качестве функции активации часто используется сигмоидальная функция σ, поскольку она принимает вещественные входные данные (силу сигнала после суммирования) и преобразует их в диапазон от 0 до 1. Подробнее об этих функциях активации мы поговорим далее в этом разделе.




Карикатурное изображение биологического нейрона (сверху) и его математическая модель (снизу).


Пример кода для прямого распространения сигнала по одному нейрону может выглядеть следующим образом:

class Neuron(object):
  # ... 
  def forward(self, inputs):
    """ assume inputs and weights are 1-D numpy arrays and bias is a number """
    cell_body_sum = np.sum(inputs * self.weights) + self.bias
    firing_rate = 1.0 / (1.0 + math.exp(-cell_body_sum)) # sigmoid activation function
    return firing_rate

Другими словами, каждый нейрон выполняет скалярное произведение входных данных и своих весов, добавляет смещение и применяет нелинейность (или функцию активации), в данном случае сигмоидальную ** sigmoid \(\sigma(x) = 1/(1+e^{-x})\)**. Более подробно о различных функциях активации мы расскажем в конце этого раздела.

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

Одиночный нейрон как линейный классификатор

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

Бинарный классификатор Softmax. Например, мы можем интерпретировать \(\sigma(\sum_i * w_i * x_i + b)\ ),как вероятность_ одного из классов \(P(y_i = 1 \mid x_i; w) \). Вероятность появления другого класса была бы равна \(P(y_i = 0 \mid x_i; w) = 1 - P(y_i = 1 \mid x_i; w) \), так как их сумма должна быть равна единице. С помощью этой интерпретации мы можем сформулировать функцию потерь перекрёстной энтропии, как мы видели в разделе «Линейная классификация», и оптимизация этой функции приведёт к созданию бинарного классификатора Softmax (также известного как логистическая регрессия). Поскольку сигмоидальная функция принимает значения от 0 до 1, прогнозы этого классификатора основаны на том, превышает ли выходное значение нейрона 0,5.

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

Интерпретация регуляризации. В этом биологическом контексте потеря регуляризации в обоих случаях SVM/Softmax может быть интерпретирована как постепенное забывание, поскольку она приводит к уменьшению всех синаптических весов w приближается к нулю после каждого обновления параметра.

Один нейрон можно использовать для реализации бинарного классификатора (например, бинарного классификатора Softmax или бинарного классификатора SVM)

Часто используемые функции активации

Каждая функция активации (или нелинейность) принимает одно число и выполняет над ним определённую фиксированную математическую операцию. На практике вы можете столкнуться с несколькими функциями активации:




Сверху: сигмоидальная нелинейность сжимает действительные числа до диапазона [0,1].
Справа: нелинейность tanh сжимает действительные числа до диапазона [-1,1].


Сигмоида. Сигмоидальная нелинейность имеет математическую форму \(\sigma(x) = 1 / (1 + e^{-x})\). Она показана на изображении выше слева. Как упоминалось в предыдущем разделе, она принимает вещественное число и «сжимает» его до диапазона от 0 до 1. В частности, большие отрицательные числа становятся равными 0, а большие положительные числа становятся равными 1. Сигмоидальная функция часто использовалась в прошлом, так как её можно интерпретировать как частоту срабатывания нейрона: от полного отсутствия срабатывания (0) до полного срабатывания с предполагаемой максимальной частотой (1). На практике сигмоидальная нелинейность в последнее время вышла из моды и используется редко. У неё есть два основных недостатка:
- Сигмоиды насыщаются и уничтожают градиенты. Очень нежелательное свойство сигмоидального нейрона заключается в том, что, когда активация нейрона насыщается на одном из концов 0 или 1, градиент в этих областях почти равен нулю. Напомним, что во время обратного распространения ошибки этот (локальный) градиент будет умножен на градиент выхода этого нейрона для всей задачи. Поэтому, если локальный градиент очень мал, он фактически «уничтожит» градиент, и почти никакой сигнал не пройдёт через нейрон к его весам и рекурсивно к его данным. Кроме того, необходимо соблюдать особую осторожность при инициализации весов сигмоидальных нейронов, чтобы предотвратить перегрузку. Например, если начальные веса слишком велики, то большинство нейронов будут перегружены, и сеть едва ли будет обучаться.
- Сигмоидальные выходные данные не центрированы по нулю. Это нежелательно, так как нейроны на более поздних уровнях обработки в нейронной сети (подробнее об этом позже) будут получать данные, не центрированные по нулю. Это влияет на динамику во время градиентного спуска, потому что если данные, поступающие в нейрон, всегда положительные (например, x>0 поэлементно в \(f = w^Tx + b\))), тогда градиент по весам w во время обратного распространения ошибки все значения станут либо положительными, либо отрицательными (в зависимости от градиента всего выражения f). Это может привести к нежелательной зигзагообразной динамике в обновлении градиентов весовых коэффициентов. Однако обратите внимание, что после суммирования этих градиентов по пакету данных окончательное обновление весовых коэффициентов может иметь разные знаки, что несколько смягчает эту проблему. Таким образом, это неудобство, но его последствия менее серьёзны по сравнению с проблемой насыщенной активации, описанной выше.

Tanh. Нелинейность tanh показана на изображении выше снизу. Она сжимает вещественное число до диапазона [-1, 1]. Как и в случае с сигмоидальным нейроном, его активация насыщается, но, в отличие от сигмоидального нейрона, его выходная величина смещена относительно нуля. Поэтому на практике нелинейность tanh всегда предпочтительнее сигмоидальной нелинейности. Также обратите внимание, что нейрон tanh — это просто масштабированный сигмоидальный нейрон, из-за чего, в частности, верно следующее: \( \tanh(x) = 2 \sigma(2x) -1 \).



Сверху: функция активации выпрямленной линейной единицы (ReLU), которая равна нулю, когда x < 0, а затем линейна с наклоном 1, когда x > 0.
Снизу: график из статьи Крижевски и др. (pdf), показывающий 6-кратное улучшение сходимости с модулем ReLU по сравнению с модулем tanh.


ReLU. Выпрямленный линейный блок стал очень популярным в последние несколько лет. Он вычисляет функцию \(f(x) = \max(0, x)\). Другими словами, активация просто ограничивается нулём (см. изображение выше сверху). У использования ReLU есть несколько плюсов и минусов:

  • (+) Было обнаружено, что она значительно ускоряет (например, в 6 раз в Крижевски и др.) сходимость стохастического градиентного спуска по сравнению с сигмоидальными/тангенциальными функциями. Утверждается, что это связано с её линейной, ненасыщаемой формой.
  • (+) По сравнению с нейронами tanh/сигмоидными нейронами, которые требуют дорогостоящих операций (экспоненциальных и т. д.), ReLU можно реализовать, просто установив пороговое значение для матрицы активации равным нулю.
  • (-) К сожалению, блоки ReLU могут быть нестабильными во время обучения и могут «умирать» . Например, большой градиент, проходящий через нейрон ReLU, может привести к обновлению весов таким образом, что нейрон больше никогда не активируется ни для одной точки данных. Если это произойдёт, то градиент, проходящий через блок, с этого момента будет равен нулю. То есть блоки ReLU могут необратимо «умирать» во время обучения, поскольку они могут быть отброшены от множества данных. Например, если скорость обучения установлена слишком высокой, вы можете обнаружить, что до 40% вашей сети могут быть «мёртвыми» (то есть нейроны, которые никогда не активируются на протяжении всего набора обучающих данных). При правильной настройке скорости обучения эта проблема возникает реже.

Протекающий ReLU. Протекающий ReLU — это одна из попыток решить проблему «умирающего ReLU». Вместо того чтобы быть равной нулю при x < 0, функция просачивающегося ReLU будет иметь небольшой положительный наклон (около 0,01). То есть функция вычисляет \(f(x) = \mathbb{1}(x < 0) (\alpha x) + \mathbb{1}(x>=0) (x) \) where \(\alpha\), где α- это небольшая константа. Некоторые люди сообщают об успехах с использованием этой формы функции активации, но результаты не всегда стабильны. Наклон в отрицательной области также может быть параметром каждого нейрона, как в случае с нейронами PReLU, представленными в работе «Глубокое погружение в выпрямители» Кайминга Хэ и др., 2015. Однако в настоящее время неясно, насколько стабильны преимущества при выполнении разных задач.

Maxout. Были предложены другие типы устройств, которые не имеют функциональной формы \(f(w^Tx + b)\), где нелинейность применяется к скалярному произведению весов и данных. Одним из относительно популярных вариантов является нейрон Maxout (введённый недавно Goodfellowи др.), который обобщает ReLU и его неидеальную версию. Нейрон Maxout вычисляет функцию \(\max(w_1^Tx+b_1, w_2^Tx + b_2)\). Обратите внимание, что и ReLU, и Leaky ReLU являются частным случаем этой формы (например, для ReLU мы имеем \(w_1, b_1 = 0\)). Таким образом, нейрон Maxout обладает всеми преимуществами блока ReLU (линейный режим работы, отсутствие насыщения) и не имеет его недостатков (умирающий ReLU). Однако, в отличие от нейронов ReLU, он удваивает количество параметров для каждого отдельного нейрона, что приводит к большому общему количеству параметров.

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

TLDR: «Какой тип нейронов мне следует использовать?» Используйте нелинейность ReLU, будьте осторожны с темпами обучения и, возможно, отслеживайте долю «мёртвых» нейронов в сети. Если вас это беспокоит, попробуйте Leaky ReLU или Maxout. Никогда не используйте сигмоид. Попробуйте tanh, но будьте готовы к тому, что он будет работать хуже, чем ReLU/Maxout.

Архитектуры нейронных сетей

Многоуровневая организация

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




Сверху: двухслойная нейронная сеть (один скрытый слой из 4 нейронов (или единиц) и один выходной слой из 2 нейронов) с тремя входами. Снизу: трёхслойная нейронная сеть с тремя входами, двумя скрытыми слоями по 4 нейрона в каждом и одним выходным слоем. Обратите внимание, что в обоих случаях между нейронами разных слоёв есть связи (синапсы), но не внутри слоя.


Соглашения об именовании. Обратите внимание, что, когда мы говорим о N-слойной нейронной сети, мы не учитываем входной слой. Таким образом, однослойная нейронная сеть — это сеть без скрытых слоёв (входные данные напрямую преобразуются в выходные). В этом смысле иногда можно услышать, что логистическая регрессия или метод опорных векторов — это просто частный случай однослойных нейронных сетей. Вы также можете услышать, что эти сети называют «искусственными нейронными сетями» (ИНС) или «многослойными перцептронами» (МПП). Многим не нравятся аналогии между нейронными сетями и реальным мозгом, и они предпочитают называть нейроны единицами.

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

Размер нейронных сетей. Два показателя, которые обычно используются для измерения размера нейронных сетей, — это количество нейронов или, чаще, количество параметров. Рассмотрим две сети на рисунке выше: - В первой сети (слева) 4 + 2 = 6 нейронов (не считая входных данных), (3 x 4) + (4 x 2) = 20 весовых коэффициентов и 4 + 2 = 6 смещений, всего 26 обучаемых параметров. - Во второй сети (справа) 4 + 4 + 1 = 9 нейронов, (3 x 4) + (4 x 4) + (4 x 1) = 12 + 16 + 4 = 32 весовых коэффициента и 4 + 4 + 1 = 9 смещений, всего 41 обучаемый параметр.

Для сравнения: современные свёрточные нейронные сети содержат порядка 100 миллионов параметров и обычно состоят примерно из 10–20 слоёв (отсюда глубокое обучение). Однако, как мы увидим, количество эффективных связей значительно больше из-за совместного использования параметров. Подробнее об этом в модуле «Свёрточные нейронные сети».

Пример вычисления с прямой связью

Повторное матричное умножение в сочетании с функцией активации. Одна из основных причин, по которой нейронные сети организованы в виде слоёв, заключается в том, что такая структура позволяет очень просто и эффективно оценивать нейронные сети с помощью матричных векторных операций. Если рассматривать трёхслойную нейронную сеть на приведённой выше схеме, то входными данными будет вектор [3x1]. Все весовые коэффициенты для слоя можно хранить в одной матрице. Например, веса первого скрытого слоя W1 будут иметь размер [4x3], а смещения для всех нейронов будут находиться в векторе b1 размером [4x1]. Здесь каждый нейрон имеет свои веса в строке W1, поэтому умножение матрицы на вектор np.dot(W1,x) вычисляет активации всех нейронов в этом слое. Аналогично, W2 будет матрицей [4x4], которая хранит связи второго скрытого слоя, а W3 — матрицей [1x4] для последнего (выходного) слоя. Полный прямой проход этой трёхслойной нейронной сети — это просто три матричных умножения, объединённых с применением функции активации:

# forward-pass of a 3-layer neural network:
f = lambda x: 1.0/(1.0 + np.exp(-x)) # activation function (use sigmoid)
x = np.random.randn(3, 1) # random input vector of three numbers (3x1)
h1 = f(np.dot(W1, x) + b1) # calculate first hidden layer activations (4x1)
h2 = f(np.dot(W2, h1) + b2) # calculate second hidden layer activations (4x1)
out = np.dot(W3, h2) + b3 # output neuron (1x1)

В приведённом выше коде W1,W2,W3,b1,b2,b3 — это обучаемые параметры сети. Обратите внимание, что вместо одного входного вектора-столбца переменная x может содержать целую выборку обучающих данных (где каждый входной пример будет столбцом x), и тогда все примеры будут эффективно обрабатываться параллельно. Обратите внимание, что последний слой нейронной сети обычно не имеет функции активации (например, он представляет собой (числовое) значение класса в задаче классификации).

Прямой проход полносвязного слоя соответствует одному умножению матриц, за которым следует смещение и функция активации.

Представительская власть

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

Оказывается, что нейронные сети, содержащие хотя бы один скрытый слой, являются универсальными аппроксиматорами. То есть можно показать (например, см. «Аппроксимацию суперпозициями сигмоидальных функций» 1989 года (pdf) или это интуитивное объяснение Майкла Нильсена), что для любой непрерывной функции f(x) и некоторые ϵ>0, существует Нейронная сеть g(x)_ с одним скрытым слоем (с разумным выбором нелинейности, например, сигмоидальной) таким образом, что ∀x,∣f(x)−g(x)∣<ϵ__. Другими словами, нейронная сеть может аппроксимировать любую непрерывную функцию.

Если для аппроксимации любой функции достаточно одного скрытого слоя, зачем использовать больше слоёв и углубляться в детали? Ответ заключается в том, что тот факт, что двухслойная нейронная сеть является универсальным аппроксиматором, хоть и выглядит красиво с математической точки зрения, на практике является относительно слабым и бесполезным утверждением. В одномерном пространстве функция «сумма пиков индикаторов» \(g(x) = \sum_i c_i \mathbb{1}(a_i < x < b_i)\), где \(a,b,c\). Векторы параметров также являются универсальным аппроксиматором, но никто не предлагает использовать эту функциональную форму в машинном обучении. Нейронные сети хорошо работают на практике, потому что они компактно выражают красивые, плавные функции, которые хорошо согласуются со статистическими свойствами данных, с которыми мы сталкиваемся на практике, а также легко обучаются с помощью наших алгоритмов оптимизации (например, градиентного спуска). Точно так же тот факт, что более глубокие сети (с несколькими скрытыми слоями) могут работать лучше, чем сети с одним скрытым слоем, является эмпирическим наблюдением, несмотря на то, что их репрезентативная мощность одинакова.

Кстати, на практике часто бывает так, что 3-слойные нейронные сети превосходят 2-слойные, но ещё большее количество слоёв (4, 5, 6) редко приносит большую пользу. Это резко контрастирует с свёрточными сетями, где глубина оказалась чрезвычайно важным компонентом для хорошей системы распознавания (например, порядка 10 обучаемых слоёв). Один из аргументов в пользу этого наблюдения заключается в том, что изображения имеют иерархическую структуру (например, лица состоят из глаз, которые состоят из контуров и т. д.), поэтому несколько уровней обработки интуитивно понятны для этой области данных.

Полная история, конечно, гораздо сложнее и является предметом многочисленных недавних исследований. Если вас интересуют эти темы, мы рекомендуем вам прочитать: - Книга «Глубокое обучение» Бенджио, Гудфеллоу, Курвиля, в частности глава 6.4. - Действительно ли Глубокие сети должны быть глубокими? - ФитНеты: Советы для тонких глубоких Сеток

Настройка количества слоев и их размеров

Как мы решаем, какую архитектуру использовать, когда сталкиваемся с практической задачей? Следует ли нам использовать несколько скрытых слоёв? Один скрытый слой? Два скрытых слоя? Насколько большим должен быть каждый слой? Во-первых, обратите внимание, что по мере увеличения размера и количества слоёв в нейронной сети ёмкость сети увеличивается. То есть пространство представимых функций растёт, поскольку нейроны могут взаимодействовать для выражения множества различных функций. Например, предположим, что у нас есть задача бинарной классификации в двух измерениях. Мы могли бы обучить три отдельные нейронные сети, каждая из которых имеет один скрытый слой определённого размера, и получить следующие классификаторы:



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


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

Исходя из нашего обсуждения выше, можно сделать вывод, что нейронные сети меньшего размера предпочтительнее, если данные недостаточно сложны, чтобы предотвратить переобучение. Однако это неверно — существует множество других предпочтительных способов предотвращения переобучения в нейронных сетях, которые мы обсудим позже (например, регуляризация \(L_2\), отсев, входной шум). На практике всегда лучше использовать эти методы для контроля переобучения, а не количество нейронов.

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

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



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


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

Краткие сведения

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

Дополнительные ссылки

Обратное распространение ошибки

Обратное распространение ошибки

Содержание: - Введение - Простые выражения, интерпретирующие градиент - Составные выражения, цепное правило, обратное распространение - Интуитивное понимание обратного распространения - Модульность: Пример сигмовидной активации - Бэкпроп на практике: Поэтапное вычисление - Закономерности в обратном потоке - Градиенты для векторизованных операций - Краткая сводка

Введение

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

Постановка задачи. Основная задача, рассматриваемая в этом разделе, заключается в следующем: нам дана некоторая функция f(x),где x является вектором входных данных, и мы заинтересованы в вычислении градиента f в x (т.е. ∇f(x)).

Мотивация. Напомним, что основная причина, по которой мы интересуемся этой проблемой, заключается в том, что в конкретном случае нейронных сетей f будет соответствовать функции потерь ( L ) и входные данные x будет состоять из обучающих данных и весовых коэффициентов нейронной сети. Например, в качестве функции потерь может использоваться функция потерь SVM, а в качестве входных данных — обучающие данные \((x_i,y_i), i=1 \ldots N\), а также веса и предубеждения W,b. Обратите внимание, что (как это обычно бывает в машинном обучении) мы рассматриваем обучающие данные как заданные и фиксированные, а весовые коэффициенты — как переменные, которыми мы можем управлять. Следовательно, даже если мы можем легко использовать обратное распространение ошибки для вычисления градиента по входным примерам \(x_i\). На практике мы обычно вычисляем градиент только для параметров (например, W,b), чтобы мы могли использовать его для обновления параметров. Однако, как мы увидим позже, градиент по \(x_i\), например, может быть полезен для визуализации и интерпретации того, что может делать нейронная сеть.

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

Простые выражения и интерпретация градиента

Давайте начнём с простого, чтобы разработать обозначения и соглашения для более сложных выражений. Рассмотрим простую функцию умножения двух чисел f(x,y)=xy. Чтобы вычислить частную производную для любого из входных параметров, достаточно воспользоваться простым математическим расчётом:

$$ f(x,y) = x y \hspace{0.5in} \rightarrow \hspace{0.5in} \frac{\partial f}{\partial x} = y \hspace{0.5in} \frac{\partial f}{\partial y} = x $$

Интерпретация. Помните, что показывают производные: они указывают на скорость изменения функции по отношению к переменной, окружающей бесконечно малую область вблизи определённой точки:

$$ \frac{df(x)}{dx} = \lim_{h\ \to 0} \frac{f(x + h) - f(x)}{h} $$

Технически примечательно, что знак деления в левой части, в отличие от знака деления в правой части, не является делением. Вместо этого эта запись указывает на то, что оператор \( \frac{d}{dx} \) применяется к функции f и возвращает другую функцию (производную). Можно представить, что приведённое выше выражение означает, что h очень мало, а значит функция хорошо аппроксимируется прямой линией, а производная — это её наклон. Другими словами, производная от каждой переменной показывает чувствительность всего выражения к её значению. Например, если x=4,y=−3 тогда f(x,y)=−12 и производная от \(x\) \(\frac{\partial f}{\partial x} = -3\)3. Это говорит нам о том, что если мы увеличим значение этой переменной на небольшую величину, то всё выражение уменьшится (из-за отрицательного знака) в три раза. Это можно увидеть, если переставить слагаемые в приведённом выше уравнении ( \( f(x + h) = f(x) + h \frac{df(x)}{dx} \) ). Аналогично, поскольку \(\frac{\partial f}{\partial y} = 4\), мы ожидаем, что увеличение стоимости y на какую-то очень небольшую сумму h также увеличит результат функции (из-за положительного знака) и 4h.

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

Как уже упоминалось, градиент ∇f является вектором частных производных, поэтому мы имеем, что \(\nabla f = [\frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}] = [y, x]\). Несмотря на то, что градиент технически является вектором, для простоты мы часто используем такие термины, как «градиент по x», вместо технически корректного выражения «частная производная по x».

Мы также можем вывести производные для операции сложения:

$$ f(x,y) = x + y \hspace{0.5in} \rightarrow \hspace{0.5in} \frac{\partial f}{\partial x} = 1 \hspace{0.5in} \frac{\partial f}{\partial y} = 1 $$

то есть производная по обоим x,y является единым, независимо от того, какими значения x,y являются. Это имеет смысл, поскольку увеличение x или y увеличило бы выпуск продукции f. И скорость этого увеличения будет зависеть от фактических значений x,y (в отличие от случая с умножением выше). Последняя функция, которую мы будем часто использовать в этом классе, — это операция max:

$$ f(x,y) = \max(x, y) \hspace{0.5in} \rightarrow \hspace{0.5in} \frac{\partial f}{\partial x} = \mathbb{1}(x >= y) \hspace{0.5in} \frac{\partial f}{\partial y} = \mathbb{1}(y >= x) $$

То есть (суб)градиент равен 1 для большего входного значения и 0 для другого входного значения. Интуитивно понятно, что если входные значения x=4,y=2тогда максимальное значение равно 4, и функция не чувствительна к настройке y. То есть, если бы мы увеличили его на крошечную величину h функция бы продолжила выводить 4, и поэтому градиент равен нулю: эффекта нет. Конечно, если бы мы изменили y на большую величину (например, больше 2), то значение f изменилось бы, но производные ничего не говорят нам о влиянии таких больших изменений на входные данные функции. Они информативны только для крошечных, бесконечно малых изменений входных данных, как показано на \(\lim_{h \rightarrow 0}\) в его определении.

Составные выражения с правилом цепочки

Теперь давайте рассмотрим более сложные выражения, включающие несколько составных функций, например f(x,y,z)=(x+y)z. Это выражение по-прежнему достаточно простое, чтобы дифференцировать его напрямую, но мы подойдём к нему с особой стороны, которая поможет понять принцип обратного распространения ошибки. В частности, обратите внимание, что это выражение можно разбить на два: q=x+y и f=qz. Более того, мы знаем, как вычислить производные обоих выражений по отдельности, как показано в предыдущем разделе. f это просто умножение q и z, так что \(\frac{\partial f}{\partial q} = z, \frac{\partial f}{\partial z} = q\), и q является добавлением x и y, итак \( \frac{\partial q}{\partial x} = 1, \frac{\partial q}{\partial y} = 1 \). Однако нам необязательно знать градиент для промежуточного значения q - ценность \(\frac{\partial f}{\partial q}\) нивелируется. Вместо этого нас, в конечном счете, интересует градиент f в отношении его вклада x,y,z. Правило цепочки говорит нам о том, что правильный способ «объединить» эти выражения градиента в цепочку — это умножение. Например, \(\frac{\partial f}{\partial x} = \frac{\partial f}{\partial q} \frac{\partial q}{\partial x} \). На практике это просто умножение двух чисел, обозначающих два градиента. Давайте рассмотрим это на примере:

# set some inputs
x = -2; y = 5; z = -4

# perform the forward pass
q = x + y # q becomes 3
f = q * z # f becomes -12

# perform the backward pass (backpropagation) in reverse order:
# first backprop through f = q * z
dfdz = q # df/dz = q, so gradient on z becomes 3
dfdq = z # df/dq = z, so gradient on q becomes -4
dqdx = 1.0
dqdy = 1.0
# now backprop through q = x + y
dfdx = dfdq * dqdx  # The multiplication here is the chain rule!
dfdy = dfdq * dqdy  

У нас остаётся градиент в переменных [dfdx,dfdy,dfdz], который показывает чувствительность переменных x,y,z к f!. Это самый простой пример обратного распространения ошибки. Далее мы будем использовать более лаконичную запись, в которой отсутствует префикс df! Например, мы будем просто писать dq вместо dfdq и всегда предполагать, что градиент вычисляется для конечного результата.

Это вычисление также можно хорошо визуализировать с помощью принципиальной схемы:


-2-4x5-4y-43z3-4q+-121f*

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


Интуитивное понимание обратного распространения

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

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

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

Давайте разберёмся, как это работает, на примере. Сложение получило на вход [-2, 5] и выдало на выходе 3. Поскольку сложение вычисляет операцию сложения, его локальный градиент для обоих входных значений равен +1. Остальная часть схемы вычислила итоговое значение, равное -12. Во время обратного прохода, при котором правило цепочки рекурсивно применяется в обратном направлении, сложение (которое является входом для умножения) узнаёт, что градиент его выходного значения равен -4. Если мы представим, что схема «хочет» вывести более высокое значение (что может помочь с интуитивным пониманием), то мы можем представить, что схема «хочет», чтобы выходное значение логического элемента «и» было ниже (из-за отрицательного знака) и с силой 4. Чтобы продолжить рекурсию и вычислить градиент, логический элемент «и» берёт этот градиент и умножает его на все локальные градиенты для своих входов (делая градиент для x и y равным 1 * -4 = -4). Обратите внимание, что это даёт желаемый эффект: если x, y уменьшатся (в соответствии с их отрицательным градиентом), то выходное значение сумматора уменьшится, что, в свою очередь, приведёт к увеличению выходного значения умножителя.

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

Модульность: Пример сигмовидной активации

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

$$ f(w,x) = \frac{1}{1+e^{-(w_0x_0 + w_1x_1 + w_2)}} $$

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

$$ f(x) = \frac{1}{x} \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = -1/x^2 \\ f_c(x) = c + x \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = 1 \\ f(x) = e^x \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = e^x \\ f_a(x) = ax \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = a $$

Где функции \(f_c, f_a\) преобразуйте входные данные в константу, равную c и масштабируйте входные данные на константу, равную a, соответственно. Технически это частные случаи сложения и умножения, но мы вводим их как (новые) унарные операции, поскольку нам не нужны градиенты для констант c,a. Тогда полная схема выглядит следующим образом:

2.00-0.20w0-1.000.39x0-3.00-0.39w1-2.00-0.59x1-3.000.20w2-2.000.20*6.000.20*4.000.20+1.000.20+-1.00-0.20*-10.37-0.53exp1.37-0.53+10.731.001/x
Пример схемы для двумерного нейрона с сигмоидальной функцией активации. Входные данные — [x0, x1], а (обучаемые) весовые коэффициенты нейрона — [w0, w1, w2]. Как мы увидим позже, нейрон вычисляет скалярное произведение входных данных, а затем его активация мягко сжимается сигмоидальной функцией до диапазона от 0 до 1.

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

$$ \sigma(x) = \frac{1}{1+e^{-x}} \\ \rightarrow \hspace{0.3in} \frac{d\sigma(x)}{dx} = \frac{e^{-x}}{(1+e^{-x})^2} = \left( \frac{1 + e^{-x} - 1}{1 + e^{-x}} \right) \left( \frac{1}{1+e^{-x}} \right) = \left( 1 - \sigma(x) \right) \sigma(x) $$

Как мы видим, градиент упрощается и становится на удивление простым. Например, сигмоидальное выражение получает на вход 1,0 и вычисляет на выходе 0,73 во время прямого прохода. Приведённый выше вывод показывает, что локальный градиент будет равен (1 — 0,73) * 0,73 ~= 0,2, как и в случае с предыдущей схемой (см. изображение выше), за исключением того, что в этом случае это будет сделано с помощью одного простого и эффективного выражения (и с меньшим количеством численных проблем). Таким образом, в любом реальном практическом применении было бы очень полезно объединить эти операции в один элемент управления. Давайте рассмотрим обратное распространение ошибки для этого нейрона в коде:

w = [2,-3,-3] # assume some random weights and data
x = [-1, -2]

# forward pass
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid function

# backward pass through the neuron (backpropagation)
ddot = (1 - f) * f # gradient on dot variable, using the sigmoid gradient derivation
dx = [w[0] * ddot, w[1] * ddot] # backprop into x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # backprop into w
# we're done! we have the gradients on the inputs to the circuit

Совет по реализации: поэтапное обратное распространение ошибки. Как показано в приведенном выше коде, на практике всегда полезно разбивать прямой проход на этапы, которые легко поддаются обратному распространению ошибки. Например, здесь мы создали промежуточную переменную dot, которая содержит результат скалярного произведения w и x. Затем во время обратного прохода мы последовательно вычисляем (в обратном порядке) соответствующие переменные (например, ddot и, в конечном итоге, dw, dx), которые содержат градиенты этих переменных.

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

Бэкпроп на практике: Поэтапное вычисление

Давайте рассмотрим это на другом примере. Предположим, что у нас есть функция следующего вида:

$$ f(x,y) = \frac{x + \sigma(y)}{\sigma(x) + (x+y)^2} $$

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

x = 3 # example values
y = -4

# forward pass
sigy = 1.0 / (1 + math.exp(-y)) # sigmoid in numerator   #(1)
num = x + sigy # numerator                               #(2)
sigx = 1.0 / (1 + math.exp(-x)) # sigmoid in denominator #(3)
xpy = x + y                                              #(4)
xpysqr = xpy**2                                          #(5)
den = sigx + xpysqr # denominator                        #(6)
invden = 1.0 / den                                       #(7)
f = num * invden # done!                                 #(8)

Фух, к концу выражения мы вычислили прямой проход. Обратите внимание, что мы структурировали код таким образом, что он содержит несколько промежуточных переменных, каждая из которых представляет собой простое выражение, для которого мы уже знаем локальные градиенты. Поэтому вычислить обратный путь легко: мы пойдём в обратном направлении, и для каждой переменной на пути прямого прохода (sigy, num, sigx, xpy, xpysqr, den, invden) у нас будет та же переменная, но начинающаяся с d, которая будет содержать градиент выходного сигнала схемы по отношению к этой переменной. Кроме того, обратите внимание, что каждый элемент в нашем обратном распространении ошибки будет включать вычисление локального градиента этого выражения и объединение его с градиентом этого выражения путём умножения. Для каждой строки мы также указываем, к какой части прямого прохода она относится:

# backprop f = num * invden
dnum = invden # gradient on numerator                             #(8)
dinvden = num                                                     #(8)
# backprop invden = 1.0 / den 
dden = (-1.0 / (den**2)) * dinvden                                #(7)
# backprop den = sigx + xpysqr
dsigx = (1) * dden                                                #(6)
dxpysqr = (1) * dden                                              #(6)
# backprop xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr                                        #(5)
# backprop xpy = x + y
dx = (1) * dxpy                                                   #(4)
dy = (1) * dxpy                                                   #(4)
# backprop sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below  #(3)
# backprop num = x + sigy
dx += (1) * dnum                                                  #(2)
dsigy = (1) * dnum                                                #(2)
# backprop sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy                                 #(1)
# done! phew

Обратите внимание на несколько вещей:

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

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

Закономерности в обратном потоке

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


3.00-8.00x-4.006.00y2.002.00z-1.000.00w-12.002.00*2.002.00max-10.002.00+-20.001.00*2
Пример схемы, демонстрирующей интуитивное понимание операций, которые выполняет обратное распространение ошибки во время обратного прохода для вычисления градиентов по входным данным. Операция суммирования равномерно распределяет градиенты по всем своим входам. Операция максимального значения направляет градиент на вход с наибольшим значением. Операция умножения принимает входные значения, меняет их местами и умножает на градиент.

Рассматривая приведенную выше диаграмму в качестве примера, мы можем видеть, что:

Сложение всегда берёт градиент на выходе и распределяет его поровну между всеми входами, независимо от того, какими были их значения во время прямого прохода. Это следует из того, что локальный градиент для операции сложения равен +1,0, поэтому градиенты на всех входах будут в точности равны градиентам на выходе, потому что они будут умножены на x1,0 (и останутся неизменными). В приведённом выше примере обратите внимание, что сумматор направил градиент 2,00 на оба входа, разделив его поровну и оставив без изменений.

Макс-сглаживатель направляет градиент. В отличие от сумматора, который распределяет градиент без изменений по всем своим входам, макс-сглаживатель распределяет градиент (без изменений) только по одному из своих входов (по входу, который имел наибольшее значение во время прямого прохода). Это связано с тем, что локальный градиент для макс-сглаживателя равен 1,0 для наибольшего значения и 0,0 для всех остальных значений. В приведённом выше примере функция max направила градиент 2,00 на переменную z, которая имела более высокое значение, чем w, а градиент w остался равным нулю.

Умножитель немного сложнее в интерпретации. Его локальные градиенты — это входные значения (кроме переключаемых), которые умножаются на градиент выходного значения в соответствии с правилом цепочки. В приведённом выше примере градиент x равен -8,00, что составляет -4,00 x 2,00.

Неинтуитивные эффекты и их последствия. Обратите внимание, что если один из входов умножителя очень мал, а другой очень велик, то умножитель сделает что-то немного неинтуитивное: он присвоит относительно большой градиент малому входу и крошечный градиент большому входу. Обратите внимание, что в линейных классификаторах, где веса умножаются на скалярное произведение, \(w^Tx_i\) (умноженные) на входные данные, это означает, что масштаб данных влияет на величину градиента весовых коэффициентов. Например, если вы умножите все примеры входных данных \(x_i\). Если во время предварительной обработки умножить на 1000, то градиент по весам будет в 1000 раз больше, и вам придётся уменьшить скорость обучения на этот коэффициент, чтобы компенсировать разницу. Вот почему предварительная обработка так важна, иногда даже в мелочах! Интуитивное понимание того, как распределяются градиенты, может помочь вам отладить некоторые из этих случаев.

Градиенты для векторизованных операций

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

Градиент при умножении матриц. Возможно, самая сложная операция — это умножение матриц (которое обобщает все операции умножения матриц на векторы и векторов на векторы):

# forward pass
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)

# now suppose we had the gradient on D from above in the circuit
dD = np.random.randn(*D.shape) # same shape as D
dW = dD.dot(X.T) #.T gives the transpose of the matrix
dX = W.T.dot(dD)

Совет: используйте анализ измерений! Обратите внимание, что вам не нужно запоминать выражения для dW и dX, поскольку их легко повторно вывести на основе измерений. Например, мы знаем, что градиент весов dW должен быть того же размера, что и W после его вычисления, и что он должен зависеть от матричного умножения X и dD (как в случае, когда оба X,W являются одиночными числами, а не матрицами). Всегда есть только один способ достичь этого, чтобы размеры соответствовали друг другу. Например, X имеет размер [10 x 3], а dD — размер [5 x 3], поэтому, если мы хотим, чтобы dW и W имели форму [5 x 10], то единственный способ добиться этого — использовать dD.dot(X.T), как показано выше.

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

Эрик Леарнед-Миллер также написал более подробный документ о вычислении матричных/векторных производных, который может оказаться вам полезным. Найдите его здесь.

Краткая сводка

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

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

Ссылки

Оптимизация

Оптимизация

Содержание: - Введение - Визуализация функции потерь - Оптимизация - Стратегия #1: Случайный поиск - Стратегия #2: Случайный локальный поиск - Стратегия #3: Следование градиенту - Вычисление градиента - Численно с конечными разностями - Аналитически с помощью исчисления - Градиентный спуск - Краткая сводка

Введение #

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

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

В частности, вспомним, что линейная функция имела вид ( f(x_i, W) = W x_i \ и разработанная нами SVM была сформулирована следующим образом:

$$ L = \frac{1}{N} \sum_i \sum_{j\neq y_i} \left[ \max(0, f(x_i; W)j - f(x_i; W) + 1) \right] + \alpha R(W) $$

Мы увидели, что настройка параметров \(W\), которые выдавали прогнозы для примера \(x_i\). В соответствии с их основными истинными метками \(y_i\) также будет иметь очень низкий убыток L. Теперь мы представим третий и последний ключевой компонент: оптимизацию. Оптимизация — это процесс нахождения набора параметров (W), которые минимизируют функцию потерь.

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

Визуализация функции потерь #

Функции потерь, которые мы рассмотрим в этом классе, обычно определяются в очень больших пространствах (например, в CIFAR-10 матрица весов линейного классификатора имеет размер [10 x 3073] для всего 30 730 параметров), что затрудняет их визуализацию. Тем не менее, мы все еще можем получить некоторые интуитивные представления об единице, разрезая пространство высокой размерности вдоль лучей (1 измерение) или вдоль плоскостей (2 измерения). Например, мы можем сгенерировать случайную матрицу весов \(W\), (которая соответствует одной точке в пространстве), затем маршировать по лучу и записывать значение функции потерь по пути. То есть мы можем сгенерировать случайное направление \(W\) и рассчитать потери в этом направлении, оценив \( L(W + a W_1 + b W_2) \) для различных значений \(a\). В результате этого процесса создается простой график со значением \(a\) в качестве оси (X) и значение функции потерь по оси \(Y\). Мы также можем провести ту же процедуру с двумя измерениями, оценив потери \( L(W + a W_1 + b W_2) \) по мере того, как меняются значения \(a, b\). На графике \(a, b\) могут соответствовать осям x и y, а значение функции потерь может быть отображено цветом:





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


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

$$ L_i = \sum_{j\neq y_i} \left[ \max(0, w_j^Tx_i - w_{y_i}^Tx_i + 1) \right] $$

Из уравнения ясно, что потеря данных для каждого примера равна сумме (нулевой порог из-за \(\max(0,-)\ функции) линейных функций \(W\). Более того, каждый ряд \(W\) (т.е. \(w_j\)) иногда имеет перед собой положительный знак (когда он соответствует неправильному классу для примера), а иногда отрицательный знак (когда он соответствует правильному классу для этого примера). Чтобы сделать это более явным, рассмотрим простой набор данных, содержащий три одномерные точки и три класса. Полная потеря SVM (без регуляризации) становится следующей:

$$ \begin{align} L_0 = & \max(0, w_1^Tx_0 - w_0^Tx_0 + 1) + \max(0, w_2^Tx_0 - w_0^Tx_0 + 1) \\ L_1 = & \max(0, w_0^Tx_1 - w_1^Tx_1 + 1) + \max(0, w_2^Tx_1 - w_1^Tx_1 + 1) \\ L_2 = & \max(0, w_0^Tx_2 - w_2^Tx_2 + 1) + \max(0, w_1^Tx_2 - w_2^Tx_2 + 1) \\ L = & (L_0 + L_1 + L_2)/3 \end{align} $$

Поскольку эти примеры являются одномерными, данные \(x_i\) и веса \(w_j\) - это цифры. Глядя, например, на \(w_0\), некоторые из приведенных выше членов являются линейными функциями \(w_0\). И каждая из них зажата в точке ноль. Мы можем визуализировать это следующим образом:



1-мерная иллюстрация потери данных:
Ось x - это один груз
ось y — потери.


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

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

# Оптимизация

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

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

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

# assume X_train is the data where each column is an example (e.g. 3073 x 50,000)
# assume Y_train are the labels (e.g. 1D array of 50,000)
# assume the function L evaluates the loss function

bestloss = float("inf") # Python assigns the highest possible float value
for num in range(1000):
  W = np.random.randn(10, 3073) * 0.0001 # generate random parameters
  loss = L(X_train, Y_train, W) # get the loss over the entire training set
  if loss < bestloss: # keep track of the best solution
    bestloss = loss
    bestW = W
  print 'in attempt %d the loss was %f, best %f' % (num, loss, bestloss)

# prints:
# 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
# ... (trunctated: continues for 1000 lines)

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

# Assume X_test is [3073 x 10000], Y_test [10000 x 1]
scores = Wbest.dot(Xte_cols) # 10 x 10000, the class scores for all test examples
# find the index with max score in each column (the predicted class)
Yte_predict = np.argmax(scores, axis = 0)
# and calculate accuracy (fraction of predictions that are correct)
np.mean(Yte_predict == Yte)
# returns 0.1555

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

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

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

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

Стратегия №2: Случайный локальный поиск

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

W = np.random.randn(10, 3073) * 0.001 # generate random starting 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%. Это лучше, но всё равно неэффективно и требует больших вычислительных мощностей.

Стратегия №3: Следование градиенту

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

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

$$ \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):
  """
  a naive implementation of numerical gradient of f at x
  - f should be a function that takes a single argument
  - x is the point (numpy array) to evaluate the gradient at
  """

  fx = f(x) # evaluate function value at original point
  grad = np.zeros(x.shape)
  h = 0.00001

  # iterate over all indexes in x
  it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
  while not it.finished:

    # evaluate function at x+h
    ix = it.multi_index
    old_value = x[ix]
    x[ix] = old_value + h # increment by h
    fxh = f(x) # evalute f(x + h)
    x[ix] = old_value # restore to previous value (very important!)

    # compute the partial derivative
    grad[ix] = (fxh - fx) / h # the slope
    it.iternext() # step to next dimension

  return grad

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

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

Мы можем использовать приведённую выше функцию для вычисления градиента в любой точке и для любой функции. Давайте вычислим градиент функции потерь CIFAR-10 в некоторой случайной точке в пространстве весов:

# to use the generic code above we want a function that takes a single argument
# (the weights in our case) so we close over X_train and Y_train
def CIFAR10_loss_fun(W):
  return L(X_train, Y_train, W)

W = np.random.rand(10, 3073) * 0.001 # random weight vector
df = eval_numerical_gradient(CIFAR10_loss_fun, W) # get the gradient

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

loss_original = CIFAR10_loss_fun(W) # the original loss
print 'original loss: %f' % (loss_original, )

# lets see the effect of multiple step sizes
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 # new position in the weight space
  loss_new = CIFAR10_loss_fun(W_new)
  print 'for step size %f new loss: %f' % (step_size, loss_new)

# prints:
# 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, поскольку хотим, чтобы наша функция потерь уменьшалась, а не увеличивалась.

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



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


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

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

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

Давайте рассмотрим пример функции потерь SVM для одной точки данных:

$$ L_i = \sum_{j\neq y_i} \left[ \max(0, w_j^Tx_i - w_{y_i}^Tx_i + \Delta) \right] $$

Мы можем дифференцировать функцию по весовым коэффициентам. Например, взяв градиент по \(w_{y_i}\) мы получаем:

$$ \nabla_{w_{y_i}} L_i = - \left( \sum_{j\neq y_i} \mathbb{1}(w_j^Tx_i - w_{y_i}^Tx_i + \Delta > 0) \right) x_i $$

где \(\mathbb{1}\)- это индикаторная функция, которая принимает значение 1, если условие внутри истинно, и 0 в противном случае. Хотя это выражение может показаться пугающим, когда вы записываете его, при реализации в коде вы просто подсчитываете количество классов, которые не соответствуют желаемой погрешности (и, следовательно, влияют на функцию потерь), а затем вектор данных \(x_i\), умноженное на это число — это градиент. Обратите внимание, что это градиент только по отношению к строке W, а это соответствует правильному классу. Для других строк, где j≠\(y_i\) градиент равен:

$$ \nabla_{w_j} L_i = \mathbb{1}(w_j^Tx_i - w_{y_i}^Tx_i + \Delta > 0) x_i $$

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

Градиентный спуск

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

# Vanilla Gradient Descent

while True:
  weights_grad = evaluate_gradient(loss_fun, data, weights)
  weights += - step_size * weights_grad # perform parameter update

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

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

# Vanilla Minibatch Gradient Descent

while True:
  data_batch = sample_training_data(data, 256) # sample 256 examples
  weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
  weights += - step_size * weights_grad # perform parameter update

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

Крайним случаем этого является ситуация, когда мини-пакет содержит только один пример. Этот процесс называется стохастическим градиентным спуском (SGD) (или иногда онлайн-градиентным спуском). Это относительно редкое явление, потому что на практике из-за оптимизации кода с помощью векторизации гораздо эффективнее вычислять градиент для 100 примеров, чем градиент для одного примера 100 раз. Несмотря на то, что SGD технически подразумевает использование одного примера для оценки градиента, вы услышите, как люди используют термин SGD даже при упоминании градиентного спуска с мини-пакетами (т. е. редко можно встретить упоминания MGD для «градиентного спуска с мини-пакетами» или BGD для «пакетного градиентного спуска»), где обычно предполагается использование мини-пакетов. Размер мини-пакета является гиперпараметром, но его нечасто проверяют на перекрёстной проверке. Обычно это зависит от ограничений памяти (если они есть) или устанавливается на какое-то значение, например 32, 64 или 128. На практике мы используем степени двойки, потому что многие реализации векторизованных операций работают быстрее, если размер входных данных равен степени двойки.

Краткая сводка



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


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

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

Линейный классификатор

Линейный классификатор

Содержание: - Линейная классификация - Параметризованное сопоставление изображений с оценками меток - Интерпретация линейного классификатора - Функция потерь - Потеря машины мультиклассового опорного вектора - Практические соображения - Классификатор Softmax - SVM против Softmax - Интерактивная веб-демонстрация - Краткая сводка - Дополнительные материалы

Линейная классификация

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

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

Параметризованное сопоставление изображений с оценками меток

Первым компонентом этого подхода является определение функции оценки, которая сопоставляет значения пикселей изображения со значениями уверенности для каждого класса. Мы рассмотрим этот подход на конкретном примере. Как и прежде, предположим, что у нас есть набор обучающих изображений $x_i \ in R^D $ каждый из которых связан с меткой $ y_i $. Здесь $i=1 ... N $ и $y_i \in { 1 ... K } $.

То есть у нас есть N примеров (каждый из которых имеет размерность D) и K различных категорий. Например, в CIFAR-10 у нас есть обучающий набор из N = 50 000 изображений, каждое из которых имеет D = 32 x 32 x 3 = 3072 пикселя, и K = 10, так как существует 10 различных классов (собака, кошка, автомобиль и т. д.). Теперь мы определим функцию оценки $f: R^D \mapsto R^K$, которое сопоставляет пиксели необработанного изображения с оценками класса.

Линейный классификатор. В этом модуле мы начнём с, пожалуй, самой простой из возможных функций — линейного отображения:

$$ f(x_i, W, b) = W x_i + b $$

В приведенном выше уравнении мы предполагаем, что изображение $x_i$ все его пиксели сглаживаются до одного вектора-столбца размером [D x 1] . Матрица W (размером [K x D]) и вектор b (размером [K x 1]) являются параметрами функции. В CIFAR-10 $x_i$ содержит все пиксели в i-м изображении, объединённые в один столбец [3072 x 1], W имеет размер [10 x 3072], а b имеет размер [10 x 1], то есть в функцию поступает 3072 числа (исходные значения пикселей), а выходит 10 чисел (оценки классов). Параметры в W часто называют весами, а b называют вектором смещения, потому что он влияет на выходные оценки, но не взаимодействует с фактическими данными $x_i$. Однако вы часто будете слышать, как люди используют термины веса и параметры как взаимозаменяемые. Есть несколько вещей, на которые следует обратить внимание: - Во-первых, обратите внимание, что умножение одной матрицы $W x_1$. Фактически выполняется параллельная оценка 10 отдельных классификаторов (по одному для каждого класса), где каждый классификатор представляет собой строку W. - Обратите также внимание, что мы думаем о входных данных $ (x_i, y_i) $ Мы считаем, что они заданы и неизменны, но мы можем управлять параметрами W, b. Наша цель — настроить их таким образом, чтобы вычисленные оценки соответствовали истинным меткам во всём обучающем наборе данных. Мы подробно рассмотрим, как это сделать, но интуитивно понятно, что мы хотим, чтобы оценка правильного класса была выше, чем оценка неправильных классов. - Преимущество этого подхода заключается в том, что обучающие данные используются для определения параметров W, b, но после завершения обучения мы можем отбросить весь обучающий набор данных и оставить только полученные параметры. Это связано с тем, что новое тестовое изображение можно просто передать в функцию и классифицировать на основе вычисленных показателей. - Наконец, обратите внимание, что классификация тестового изображения включает в себя одно матричное умножение и сложение, что значительно быстрее, чем сравнение тестового изображения со всеми обучающими изображениями.

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

Интерпретация линейного классификатора

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



Пример сопоставления изображения с баллами по классам. Для наглядности предположим, что изображение состоит всего из 4 пикселей (4 монохромных пикселя, в этом примере мы не рассматриваем цветовые каналы для краткости) и что у нас есть 3 класса (красный (кошка), зелёный (собака), синий (корабль)). (Уточнение: в частности, цвета здесь просто обозначают 3 класса и не связаны с каналами RGB.) Мы растягиваем пиксели изображения в столбец и выполняем умножение матриц, чтобы получить баллы по каждому классу. Обратите внимание, что этот конкретный набор весовых коэффициентов W совсем не хорош: весовые коэффициенты присваивают нашему изображению кошки очень низкий балл. В частности, этот набор весовых коэффициентов, похоже, убеждён, что видит собаку.


Аналогия изображений с многомерными точками. Поскольку изображения растягиваются в многомерные векторы-столбцы, мы можем интерпретировать каждое изображение как отдельную точку в этом пространстве (например, каждое изображение в CIFAR-10 — это точка в 3072-мерном пространстве размером 32x32x3 пикселя). Аналогично, весь набор данных — это (помеченный) набор точек.

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



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


Как мы видели выше, каждый ряд W является классификатором для одного из классов. Геометрическая интерпретация этих чисел заключается в том, что при изменении одного из столбцов W соответствующая линия в пиксельном пространстве будет поворачиваться в разных направлениях. Смещение b. С другой стороны, наши классификаторы позволяют переводить строки. В частности, обратите внимание, что без коэффициентов смещения подстановка $ x_i = 0 $ всегда будет давать нулевой результат независимо от весов, поэтому все линии будут вынуждены пересекать начало координат.

Интерпретация линейных классификаторов как сопоставление шаблонов. Другая интерпретация весовых коэффициентов W заключается в том, что каждая строка W соответствует шаблону (или, как его иногда называют, прототипу) для одного из классов. Оценка каждого класса для изображения затем получается путём сравнения каждого шаблона с изображением с помощью скалярного произведения (или точечного произведения) по очереди, чтобы найти наиболее подходящий. В этой терминологии линейный классификатор выполняет сопоставление шаблонов, которые он изучает. Другой способ взглянуть на это — представить, что мы по-прежнему используем метод ближайшего соседа, но вместо тысяч обучающих изображений мы используем только одно изображение для каждого класса (хотя мы его изучим, и оно не обязательно должно быть одним из изображений в обучающем наборе), и в качестве расстояния мы используем (отрицательное) скалярное произведение вместо расстояния L1 или L2.



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


Кроме того, обратите внимание, что шаблон лошади, по-видимому, содержит двухголовую лошадь, что связано с тем, что в наборе данных есть лошади, смотрящие как влево, так и вправо. Линейный классификатор объединяет эти два вида лошадей в данных в один шаблон. Аналогичным образом, классификатор автомобилей, по-видимому, объединил несколько видов в один шаблон, который должен распознавать автомобили со всех сторон и всех цветов. В частности, этот шаблон оказался красным, что указывает на то, что в наборе данных CIFAR-10 больше красных автомобилей, чем автомобилей любого другого цвета. Линейный классификатор слишком слаб, чтобы правильно распознавать автомобили разных цветов, но, как мы увидим позже, нейронные сети позволят нам выполнить эту задачу. Забегая немного вперёд, скажу, что нейронная сеть сможет создавать промежуточные нейроны в своих скрытых слоях, которые смогут распознавать определённые типы автомобилей (например, зелёный автомобиль, поворачивающий налево, синий автомобиль, поворачивающий вперёд, и т. д.), а нейроны на следующем слое смогут объединять их в более точную оценку автомобиля с помощью взвешенной суммы отдельных детекторов автомобилей.

Уловка с предвзятостью. Прежде чем двигаться дальше, мы хотим упомянуть распространённую упрощающую уловку для представления двух параметров W,b как один. Напомним, что мы определили функцию оценки как:

$$ f(x_i, W, b) = W x_i + b $$

По мере изучения материала становится немного сложнее отслеживать два набора параметров (смещения b и веса W) по отдельности. Часто используемый приём заключается в объединении двух наборов параметров в одну матрицу, которая содержит их оба, путём расширения вектора $ x_i $ с одним дополнительным измерением, которое всегда сохраняет константу 1 - измерение смещения по умолчанию. С дополнительным измерением новая функция оценки упростится до простого умножения матриц: $$ f(x_i, W) = W x_i $$ С нашим примером CIFAR-10, $ x_i $ теперь [3073 x 1] вместо [3072 x 1] — (с дополнительным измерением, содержащим константу 1), и W теперь имеет значение [10 x 3073] вместо [10 x 3072]. Дополнительный столбец, который W теперь соответствует смещению b. Иллюстрация могла бы помочь прояснить ситуацию:



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


Предварительная обработка данных изображения. В качестве примечания: в приведённых выше примерах мы использовали необработанные значения пикселей (которые находятся в диапазоне [0…255]). В машинном обучении очень распространена практика нормализации входных признаков (в случае изображений каждый пиксель считается признаком). В частности, важно центрировать данные, вычитая среднее значение из каждого признака. В случае с изображениями это соответствует вычислению среднего значения изображения по обучающим изображениям и вычитанию его из каждого изображения, чтобы получить изображения, в которых пиксели находятся в диапазоне примерно от [-127 до 127]. Далее обычно выполняется предварительная обработка, при которой каждый входной признак масштабируется так, чтобы его значения находились в диапазоне от [-1 до 1]. Из них, пожалуй, более важным является центрирование относительно нуля, но нам придётся подождать с его обоснованием, пока мы не поймём динамику градиентного спуска.

Функция потерь

В предыдущем разделе мы определили функцию преобразования значений пикселей в оценки классов, которая параметризуется набором весовых коэффициентов W. Более того, мы увидели, что у нас нет контроля над данными $(x_i, y_i)$ (это фиксировано и задано), но мы можем управлять этими весами и хотим установить их так, чтобы прогнозируемые оценки классов соответствовали исходным меткам в обучающих данных. Например, если вернуться к примеру с изображением кошки и её оценками для классов «кошка», «собака» и «корабль», то мы увидим, что конкретный набор весов в этом примере был не очень хорошим: мы ввели пиксели, изображающие кошку, но оценка кошки получилась очень низкой (-96,8) по сравнению с другими классами (оценка собаки 437,9, а оценка корабля 61,95). Мы будем измерять степень нашего недовольства такими результатами, как этот, с помощью функции потерь (иногда также называемой функцией затрат или целевой функцией). Интуитивно понятно, что потери будут высокими, если мы плохо справляемся с классификацией обучающих данных, и низкими, если мы хорошо справляемся.

Потеря машины мультиклассового опорного вектора

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

Теперь давайте уточним. Напомним, что для i-го примера нам даны пиксели изображения $ x_i $ и этикетка $ y_i $ которая определяет индекс правильного класса. Функция оценки принимает пиксели и вычисляет вектор $ f(x_i, W) $ оценок за класс, которые мы будем сокращать до s (сокращение от «баллы»). Например, балл за j-й класс — это j-й элемент: $ s_j = f(x_i, W)_j $. Потери многоклассового SVM для i-го примера формализуются следующим образом:

$$ L_i = Σ_{j\neq y_i} max(0, s_j - s_{y_i} + \Delta) $$

Пример. Давайте разберём это на примере, чтобы понять, как это работает. Предположим, что у нас есть три класса, которые получают оценки s=[13,−7,11], и что первый класс является истинным классом (т.е. $ y_i = 0 $ ). Также предположим, что Δ (гиперпараметр, о котором мы вскоре поговорим подробнее) равен 10. Приведенное выше выражение суммирует все неправильные классы ($ j \neq y_i $), таким образом, мы получаем два термина:

$$ L_i = max(0, -7 - 13 + 10) + \max(0, 11 - 13 + 10) $$

Вы видите, что первый член даёт ноль, так как [-7 - 13 + 10] даёт отрицательное число, которое затем округляется до ннля с помощью max(0,−) функция. Мы получаем нулевые потери для этой пары, потому что оценка правильного класса (13) была больше, чем оценка неправильного класса (-7), как минимум на 10. На самом деле разница составляла 20, что намного больше 10, но SVM интересует только то, что разница составляет не менее 10; любая дополнительная разница, превышающая 10, ограничивается нулём с помощью операции max. Второй член вычисляет [11 - 13 + 10], что даёт 8. То есть, даже если правильный класс имел более высокий балл, чем неправильный (13 > 11), он не был выше желаемого значения в 10 баллов. Разница составила всего 2, поэтому проигрыш равен 8 (т. Е. Насколько выше должна быть разница, чтобы соответствовать марже). Таким образом, функция SVM loss запрашивает оценку правильного класса $ y_i = 0 $ быть больше, чем неправильные оценки класса, по крайней мере, на Δ (дельта). Если это не так, мы понесём убытки.

Обратите внимание, что в этом конкретном модуле мы работаем с линейными функциями оценки ( $ f(x_i; W) = W x_i $ ), поэтому мы также можем переписать функцию потерь в этой эквивалентной форме:

$$ L_i = Σ_{j\neq y_i} max(0, w_j^T x_i - w_{y_i}^T x_i + \Delta) $$

где $ w_j$ является j-й строкой W преобразован в столбец. Однако это не обязательно будет так, если мы начнём рассматривать более сложные формы функции оценки f.

Последний термин, который мы упомянем, прежде чем закончить этот раздел, — это нулевой порог max(0,−). Эта функция часто называется потерей от перегиба. Иногда можно услышать, что вместо этого люди используют SVM с квадратичной потерей от перегиба (или L2-SVM), которая имеет вид $max(0,−) ^ 2$ это сильнее6 сказывается на нарушении границ (квадратично, а не линейно). Неквадратичная версия является более стандартной, но в некоторых наборах данных квадратичная функция потерь может работать лучше. Это можно определить во время перекрестной проверки.

Функция потерь количественно определяет наше недовольство прогнозами на обучающем наборе


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


Регуляризация. Есть одна ошибка с функцией потерь, которую мы представили выше. Предположим, что у нас есть набор данных и набор параметров W, которые правильно классифицируют каждый пример (т.е. все оценки таковы, что все поля соблюдены, и $L_i = 0)\ для всех i). Проблема в том, что этот набор W не обязательно уникален: может быть много похожих W, которые правильно классифицируют примеры. Один из простых способов увидеть это заключается в том, что если некоторые параметры W правильно классифицируют все примеры (так что потери равны нулю для каждого примера), то любое кратное этим параметрам λW, где λ>1 также даст нулевой убыток, потому что это преобразование равномерно растягивает все величины счета и, следовательно, их абсолютные разности. Например, если разница в оценках между правильным классом и ближайшим неправильным классом равна 15, то умножение всех элементов W на 2 даст новую разницу 30.

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

$$ R(W) = \sum_k\sum_l W_{k,l}^2 $$

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

$$ L = \underbrace{ \frac{1}{N} \sum_i L_i }\text{потеря данных} + \underbrace{ \lambda R(W) }\text{потеря регуляризации} \\ $$

Или расширить это в его полной форме:

$$ L = \frac{1}{N} \sum_i \sum_{j\neq y_i} \left[ \max(0, f(x_i; W)j - f(x_i; W)^2 $$ } + \Delta) \right] + \lambda \sum_k\sum_l W_{k,l

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

Помимо мотивации, которую мы привели выше, существует множество желательных свойств, связанных с включением штрафа за регуляризацию, к которым мы вернёмся в следующих разделах. Например, оказывается, что включение штрафа L2 приводит к привлекательному свойству максимального запаса прочности в SVM (если вам интересно, см. CS229 для получения полной информации).

Наиболее привлекательным свойством является то, что штрафные санкции за большие веса, как правило, улучшают обобщение, поскольку это означает, что ни один входной параметр сам по себе не может оказывать очень сильное влияние на оценки. Например, предположим, что у нас есть некоторый входной вектор $x=[1,1,1,1] )) и два весовых вектора__$w1=[1,0,0,0] $, $w2=[0,25,0,25,0,25,0,25] $__. Затем $w_1^Tx = w_2^Tx = 1$. Таким образом, оба вектора весов приводят к одному и тому же скалярному произведению, но штраф L2 $w_1$ равно 1.0, в то время как штраф L2 равен $w_2$ составляет всего 0,5. Следовательно, согласно штрафу L2, вектор весов $w_2$ это предпочтительнее, так как достигается меньшая потеря при регуляризации. Интуитивно понятно, что это происходит потому, что веса в $w_2$ являются более компактными и менее размытыми. Поскольку штраф L2 предпочитает более компактные и менее размытые векторы весов, итоговому классификатору рекомендуется учитывать все входные параметры в небольших количествах, а не несколько входных параметров в очень больших количествах. Как мы увидим далее в этом курсе, этот эффект может улучшить обобщающую способность классификаторов на тестовых изображениях и привести к меньшему переобучению.

Обратите внимание, что смещения не оказывают такого же эффекта, поскольку, в отличие от весовых коэффициентов, они не контролируют силу влияния входного параметра. Поэтому обычно нормализуют только весовые коэффициенты W но не из - за предубеждений b. Однако на практике это часто оказывается несущественным. Наконец, обратите внимание, что из-за штрафа за регуляризацию мы никогда не сможем добиться потери точности, равной 0,0, во всех примерах, потому что это возможно только в патологических условиях W=0.

Код. Вот функция потерь (без регуляризации), реализованная на Python как в не векторизованной, так и в полувекторной форме:

def L_i(x, y, W):
  """
  unvectorized version. Compute the multiclass svm loss for a single example (x,y)
  - x is a column vector representing an image (e.g. 3073 x 1 in CIFAR-10)
    with an appended bias dimension in the 3073-rd position (i.e. bias trick)
  - y is an integer giving index of correct class (e.g. between 0 and 9 in CIFAR-10)
  - W is the weight matrix (e.g. 10 x 3073 in CIFAR-10)
  """
  delta = 1.0 # see notes about delta later in this section
  scores = W.dot(x) # scores becomes of size 10 x 1, the scores for each class
  correct_class_score = scores[y]
  D = W.shape[0] # number of classes, e.g. 10
  loss_i = 0.0
  for j in range(D): # iterate over all wrong classes
    if j == y:
      # skip for the true class to only loop over incorrect classes
      continue
    # accumulate loss for the i-th example
    loss_i += max(0, scores[j] - correct_class_score + delta)
  return loss_i

def L_i_vectorized(x, y, W):
  """
  A faster half-vectorized implementation. half-vectorized
  refers to the fact that for a single example the implementation contains
  no for loops, but there is still one loop over the examples (outside this function)
  """
  delta = 1.0
  scores = W.dot(x)
  # compute the margins for all classes in one vector operation
  margins = np.maximum(0, scores - scores[y] + delta)
  # on y-th position scores[y] - scores[y] canceled and gave delta. We want
  # to ignore the y-th position and only consider margin on max wrong class
  margins[y] = 0
  loss_i = np.sum(margins)
  return loss_i

def L(X, y, W):
  """
  fully-vectorized implementation :
  - X holds all the training examples as columns (e.g. 3073 x 50,000 in CIFAR-10)
  - y is array of integers specifying correct class (e.g. 50,000-D array)
  - W are weights (e.g. 10 x 3073)
  """
  # evaluate loss over all examples in X without using any for loops
  # left as exercise to reader in the assignment
  ```

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

>Теперь нам нужно придумать способ найти веса, которые минимизируют потери.  

## Практические соображения  

__Устанавливаем дельту__. Обратите внимание, что мы затронули гиперпараметр __Δ__ и его настройка. Какое значение следует установить и нужно ли проводить перекрестную проверку? Оказывается, этот гиперпараметр можно смело устанавливать на __Δ=1.0_ во всех случаях. Гиперпараметры __Δ__ и __λ__: кажется, что это два разных гиперпараметра, но на самом деле они оба управляют одним и тем же компромиссом: компромиссом между потерей данных и потерей от регуляризации в целевой функции. Ключ к пониманию этого заключается в том, что величина весовых коэффициентов __W__ оказывает непосредственное влияние на баллы (и, следовательно, на их разницу): по мере уменьшения всех значений внутри __W__ разница в баллах будет уменьшаться, а по мере увеличения весов разница в баллах будет увеличиваться. Таким образом, точное значение разницы между баллами (например, __Δ=1__ , или __Δ=100__) в некотором смысле бессмысленно, потому что веса могут произвольно уменьшать или увеличивать разницу. Следовательно, единственный реальный компромисс заключается в том, насколько сильно мы позволяем весам увеличиваться (с помощью силы регуляризации __λ__).  

__Связь с бинарной машиной опорных векторов__. Возможно, вы пришли на этот курс, уже имея опыт работы с бинарными машинами опорных векторов, где потери для i-го примера можно записать как:  

$$
L_i = C \max(0, 1 - y_i w^Tx_i) + R(W)
$$  

где __C__ является гиперпараметром, и $y_i \in \\{ -1,1 \\} $. Вы можете убедиться, что представленная в этом разделе формулировка содержит бинарный SVM в качестве частного случая, когда есть только два класса. То есть, если бы у нас было только два класса, то потери сводились бы к бинарному SVM, показанному выше. Кроме того, __C__ в этой формулировке и __λ__ в нашей формулировке контролируется один и тот же компромисс и они связаны через взаимное отношение $C \propto \frac{1}{\lambda}$.  

__Примечание: оптимизация в прямой форме__. Если вы пришли на этот курс, уже имея представление о SVM, то, возможно, слышали о ядрах, двойственных задачах, алгоритме SMO и т. д. В этом курсе (как и в случае с нейронными сетями в целом) мы всегда будем работать с задачами оптимизации в их прямой форме без ограничений. Многие из этих задач технически не являются дифференцируемыми (например, функция max(x,y) не является дифференцируемой, потому что имеет излом при x=y), но на практике это не является проблемой, и обычно используется субградиент.  

Примечание: другие многоклассовые формулировки SVM. Стоит отметить, что многоклассовая формулировка SVM, представленная в этом разделе, является одним из немногих способов формулировки SVM для нескольких классов. Другой часто используемой формой является SVM _«один против всех»_ (OVA), которая обучает независимый бинарный SVM для каждого класса по сравнению со всеми остальными классами. С ней связана, но реже встречается на практике стратегия _«все против всех»_ (AVA). Наша формулировка основана на версии [Weston and Watkins 1999](https://www.elen.ucl.ac.be/Proceedings/esann/esannpdf/es1999-461.pdf) (pdf), которая является более мощной версией, чем OVA (в том смысле, что вы можете создавать многоклассовые наборы данных, в которых эта версия может обеспечить нулевую потерю данных, а OVA  нет. Если интересно, подробности можно найти в статье). Последняя формулировка, которую вы можете увидеть,  это _структурированный_ SVM, который максимизирует разницу между оценкой правильного класса и оценкой наиболее близкого к нему неправильного класса. Понимание различий между этими формулировками выходит за рамки данного курса. Версия, представленная в этих заметках, безопасна для использования на практике, но, возможно, самая простая стратегия OVA тоже будет работать (как утверждают Рикин и др. в 2004 году в [«В защиту классификации «один против всех»](http://www.jmlr.org/papers/volume5/rifkin04a/rifkin04a.pdf) (pdf)).  

## Классификатор Softmax  

Оказывается, что SVM  один из двух наиболее распространённых классификаторов. Другой популярный вариант  __классификатор Softmax__, у которого другая функция потерь. Если вы раньше слышали о классификаторе бинарной логистической регрессии, то классификатор Softmax  это его обобщение для нескольких классов. В отличие от SVM, который обрабатывает выходные данные $f(x_i,W)$. В качестве (некалиброванных и, возможно, трудно интерпретируемых) оценок для каждого класса классификатор Softmax даёт чуть более понятный результат (нормализованные вероятности классов), а также имеет вероятностную интерпретацию, которую мы вскоре опишем. В классификаторе Softmax функция, отображающая $f(x_i; W) =  W x_i$ остаётся неизменным, но теперь мы интерпретируем эти оценки как ненормированные логарифмические вероятности для каждого класса и заменяем  _потерю от перегиба_ __потерю от перекрёстной энтропии__, которая имеет вид:  

$$
L_i = -\log\left(\frac{e^{f_{y_i}}}{ \sum_j e^{f_j} }\right) \hspace{0.5in} \text{or equivalently} \hspace{0.5in} L_i = -f_{y_i} + \log\sum_j e^{f_j}
$$  

где мы используем обозначение __$f_j$__ для обозначения j-го элемента вектора оценок класса __f__. Как и прежде, полная потеря набора данных является средним значением __$L_i$__
 по всем обучающим примерам вместе с термином регуляризации __$R(W)$__. Функция __$f_j(z) = \frac{e^{z_j}}{\sum_k e^{z_k}} $__ называется __функцией softmax__: она принимает вектор произвольных числовых значений  (в $z$) и преобразует его в вектор значений от нуля до единицы, сумма которых равна единице. Полная функция потерь перекрёстной энтропии, включающая функцию softmax, может показаться пугающей, если вы видите её впервые, но её относительно легко объяснить.  

 __С точки зрения теории информации__. _Перекрёстная энтропия_ между «истинным» распределением p
 и предполагаемое распределение __q__ определяется как:  

$$
H(p,q) = - \sum_x p(x) \log q(x)
$$  

Таким образом, классификатор Softmax минимизирует перекрестную энтропию между оцененными вероятностями классов ( $q = e^{f_{y_i}}  / \sum_j e^{f_j} $ как показано выше) и _«истинное»_ распределение, которое в этой интерпретации представляет собой распределение, при котором вся масса вероятностей приходится на правильный класс 
то есть $p = [0, \ldots 1, \ldots, 0]\\ содержит единственную единицу в __$y_i$__-й позиции.). Более того, поскольку кросс-энтропию можно записать в терминах энтропии, а дивергенцию Кульбака-Лейблера - как $H(p,q) = H(p) + D_{KL}(p\|\|q)$, и энтропия дельта - функции __p__ равно нулю, что также эквивалентно минимизации расхождения Кульбака  Лейблера между двумя распределениями (мера расстояния). Другими словами, цель кросс-энтропии _заключается в том_, чтобы прогнозируемое распределение было сосредоточено на правильном ответе.  

__Вероятностная интерпретация__. Глядя на выражение, мы видим, что:  

$$
P(y_i \mid x_i; W) = \frac{e^{f_{y_i}}}{\sum_j e^{f_j} }
$$

может быть интерпретирована как (нормализованная) вероятность, присвоенная правильной метке __$y_i$__ учитывая изображение __$x_i$__ и параметризуется с помощью __W__.Чтобы понять это, вспомните, что классификатор Softmax интерпретирует оценки в выходном векторе __f__, как ненормированные логарифмические вероятности. Возведение этих величин в степень даёт (ненормированные) вероятности, а деление выполняет нормализацию, чтобы сумма вероятностей равнялась единице. Таким образом, в вероятностной интерпретации мы минимизируем отрицательную логарифмическую вероятность правильного класса, что можно интерпретировать как выполнение __оценки максимального правдоподобия__ (MLE). Преимущество такого подхода в том, что теперь мы можем интерпретировать и член регуляризации __R(W)__ в полной функции потерь, исходящей из гауссовского априора по весовой матрице __W__, где вместо MLE мы выполняем оценку _максимального апостериорного значения_ (MAP). Мы приводим эти интерпретации, чтобы помочь вам разобраться, но подробное описание этого вывода выходит за рамки данного курса.  

__Практические вопросы: числовая стабильность__. Когда вы пишете код для вычисления функции Softmax на практике, промежуточные значения$e^{f_{y_i}}$ и $\sum_j e^{f_j}$ может быть очень большим из-за экспоненциальных функций. Деление больших чисел может быть неустойчивым с точки зрения вычислений, поэтому важно использовать приём нормализации. Обратите внимание, что если мы умножим числитель и знаменатель дроби на константу __C__ и подставим его в сумму, получим следующее (математически эквивалентное) выражение:  

$$
\frac{e^{f_{y_i}}}{\sum_j e^{f_j}}
= \frac{Ce^{f_{y_i}}}{C\sum_j e^{f_j}}
= \frac{e^{f_{y_i} + \log C}}{\sum_j e^{f_j + \log C}}
$$  

Мы вольны выбирать стоимость __C__. Это не повлияет ни на один из результатов, но мы можем использовать это значение для повышения численной стабильности вычислений. Обычно выбирают __C__ заключается в том, чтобы установить __$\log C = -\max_j f_j $__. Это просто указывает на то, что мы должны сместить значения внутри вектора __f__ так что наибольшее значение равно нулю. В коде:  


```py
f = np.array([123, 456, 789]) # example with 3 classes and each having large scores
p = np.exp(f) / np.sum(np.exp(f)) # Bad: Numeric problem, potential blowup

# instead: first shift the values of f so that the highest number is 0:
f -= np.max(f) # f becomes [-666, -333, 0]
p = np.exp(f) / np.sum(np.exp(f)) # safe to do, gives the correct answer

Возможно, сбивающие с толку соглашения об именовании. Чтобы быть точным, в классификаторе SVM используется потеря шарнира, или также иногда называемая потерей максимальной маржи. Классификатор Softmax использует кросс-энтропийные потери. Классификатор Softmax получил свое название от функции softmax, которая используется для преобразования необработанных оценок класса в нормализованные положительные значения, которые в сумме равны единице, чтобы можно было применить потери от перекрестной энтропии. В частности, обратите внимание, что технически не имеет смысла говорить о «потере при softmax», поскольку softmax — это просто функция сжатия, но это относительно часто используемое сокращение.

SVM против Softmax

Изображение может помочь прояснить разницу между классификаторами Softmax и SVM:


Пример разницы между классификаторами SVM и Softmax для одной точки данных. В обоих случаях мы вычисляем один и тот же вектор оценок f (например, путём умножения матриц в этом разделе). Разница заключается в интерпретации оценок в f: SVM интерпретирует их как оценки классов, и его функция потерь поощряет правильный класс (класс 2, выделен синим цветом) к получению более высокой оценки, чем у других классов. Вместо этого классификатор Softmax интерпретирует баллы как (ненормализованные) логарифмические вероятности для каждого класса, а затем стремится к тому, чтобы (нормализованная) логарифмическая вероятность правильного класса была высокой (эквивалентно, чтобы её отрицательная величина была низкой). Окончательное значение потерь для этого примера составляет 1,58 для SVM и 1,04 (обратите внимание, что это 1,04 с использованием натурального логарифма, а не логарифма по основанию 2 или 10) для классификатора Softmax, но обратите внимание, что эти числа несопоставимы. Они имеют смысл только в сравнении с потерями, рассчитанными для того же классификатора и с теми же данными.


Классификатор Softmax предоставляет «вероятности» для каждого класса. В отличие от SVM, который вычисляет некалиброванные и трудно интерпретируемые оценки для всех классов, классификатор Softmax позволяет вычислять «вероятности» для всех меток. Например, для изображения классификатор SVM может выдать оценки [12,5, 0,6, -23,0] для классов «кошка», «собака» и «корабль». Вместо этого классификатор softmax может вычислить вероятности трёх меток как [0,9, 0,09, 0,01], что позволяет интерпретировать его уверенность в каждом классе. Однако мы взяли слово «вероятности» в кавычки, потому что то, насколько выраженными или размытыми будут эти вероятности, напрямую зависит от силы регуляризации λ, которые вы вводите в систему в качестве входных данных. Например, предположим, что ненормированные логарифмические вероятности для трёх классов равны [1, -2, 0]. Тогда функция softmax вычислит:

$$ [1, -2, 0] \rightarrow [e^1, e^{-2}, e^0] = [2.71, 0.14, 1] \rightarrow [0.7, 0.04, 0.26] $$

Где шаги, предпринятые для возведения в степень и нормализации, суммируются до единицы. Теперь, если сила регуляризации λ был выше, вес W будет больше штрафоваться, и это приведёт к уменьшению весов. Например, предположим, что веса стали в два раза меньше ([0,5, -1, 0]). Теперь softmax будет вычислять:

$$ [0.5, -1, 0] \rightarrow [e^{0.5}, e^{-1}, e^0] = [1.65, 0.37, 1] \rightarrow [0.55, 0.12, 0.33] $$

где вероятности теперь более размыты. Более того, в пределе, когда веса стремятся к малым значениям из-за очень сильной регуляризации __λ__Выходные вероятности были бы почти равномерными. Следовательно, вероятности, вычисляемые классификатором Softmax, лучше рассматривать как степени уверенности, где, как и в случае с SVM, порядок значений интерпретируется, но абсолютные значения (или их разница) технически не интерпретируются.

На практике SVM и Softmax обычно сопоставимы по эффективности. Разница в производительности между SVM и Softmax обычно очень мала, и разные люди по-разному оценивают, какой классификатор работает лучше. По сравнению с классификатором Softmax, SVM является более локальной целью, что можно рассматривать как недостаток или преимущество. Рассмотрим пример, в котором достигаются баллы [10, -2, 3] и где первый класс является правильным. SVM (например, с желаемым запасом прочности Δ=1) увидит, что правильный класс уже имеет оценку выше, чем разница между классами, и вычислит нулевую потерю. SVM не обращает внимания на детали отдельных оценок: если бы они были [10, -100, -100] или [10, 9, 9], SVM было бы всё равно, так как разница в 1 соблюдена и, следовательно, потеря равна нулю. Однако эти сценарии не эквивалентны классификатору Softmax, который привёл бы к гораздо более высоким потерям для оценок [10, 9, 9], чем для [10, -100, -100]. Другими словами, классификатор Softmax никогда не будет полностью удовлетворён полученными оценками: правильный класс всегда может иметь более высокую вероятность, а неправильные классы — более низкую, и потери всегда будут уменьшаться. Однако SVM доволен, если соблюдены границы, и не контролирует точные оценки за пределами этого ограничения. Это можно интуитивно воспринимать как особенность: например, классификатор автомобилей, который, скорее всего, тратит большую часть своих «усилий» на решение сложной задачи по отделению автомобилей от грузовиков, не должен подвергаться влиянию примеров с лягушками, которым он уже присваивает очень низкие баллы и которые, скорее всего, группируются в совершенно другой части облака данных.

Интерактивная веб-демонстрация


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


Краткая сводка

Подводя итог: - Мы определили функцию оценки от пикселей изображения к оценкам классов (в этом разделе — линейную функцию, которая зависит от весовых коэффициентов W и смещений b). - В отличие от классификатора kNN, преимущество этого параметрического подхода заключается в том, что после определения параметров мы можем отказаться от обучающих данных. Кроме того, прогнозирование для нового тестового изображения выполняется быстро, поскольку требует лишь одного умножения матрицы на W, а не исчерпывающего сравнения с каждым отдельным обучающим примером. - Мы ввели уловку со смещением, которая позволяет нам сложить вектор смещения в весовую матрицу для удобства, чтобы отслеживать только одну матрицу параметров. - Мы определили функцию потерь (мы ввели две часто используемые функции потерь для линейных классификаторов: SVM и Softmax), которая измеряет, насколько заданный набор параметров соответствует истинным меткам в обучающем наборе данных. Мы также увидели, что функция потерь определена таким образом, что хорошие прогнозы на обучающих данных эквивалентны небольшим потерям.

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

Дополнительные материалы

Эти показания являются необязательными и содержат указания, представляющие интерес: - «Глубокое обучение с использованием линейных машин опорных векторов» от Чарли Танга, 2013 г., представляет некоторые результаты, согласно которым L2SVM превосходит Softmax.

Трансфер обучения

Трансфер обучения

(Эти заметки в настоящее время находятся в черновой форме и находятся в разработке)

Содержание: - Трансферное обучение - Дополнительные примечания

Трансферное обучение

На практике очень немногие обучают всю сверточную сеть с нуля (со случайной инициализацией), потому что относительно редко удается иметь набор данных достаточного размера. Вместо этого обычно предварительно обучают ConvNet на очень большом наборе данных (например, ImageNet, который содержит 1,2 миллиона изображений с 1000 категориями), а затем используют ConvNet либо в качестве инициализации, либо в качестве фиксированного экстрактора признаков для интересующей задачи. Три основных сценария трансферного обучения выглядят следующим образом: - ConvNet в качестве фиксированного экстрактора признаков. Возьмите предварительно обученный ConvNet на ImageNet, удалите последний полностью подключенный слой (выходные данные этого слоя представляют собой 1000 баллов класса для другой задачи, такой как ImageNet), а затем обрабатывайте остальную часть ConvNet как фиксированный экстрактор признаков для нового набора данных. В AlexNet это позволило бы вычислить вектор 4096-D для каждого изображения, содержащего активации скрытого слоя непосредственно перед классификатором. Мы называем эти функции кодами CNN. Для производительности важно, чтобы эти коды были ReLUd (т.е. пороговыми на нуле), если они также были пороговыми во время обучения ConvNet на ImageNet (как это обычно бывает). После извлечения кодов 4096-D для всех изображений обучите линейный классификатор (например, Linear SVM или классификатор Softmax) для нового набора данных. - Тонкая настройка ConvNet. Вторая стратегия заключается не только в замене и переобучении классификатора поверх ConvNet на новом наборе данных, но и в тонкой настройке весов предварительно обученной сети путем продолжения обратного распространения. Можно тонко настроить все уровни ConvNet, или можно оставить некоторые из более ранних уровней фиксированными (из-за опасений переобучения) и выполнить тонкую настройку только некоторой части сети более высокого уровня. Это мотивировано наблюдением, что более ранние функции ConvNet содержат более общие функции (например, детекторы краев или детекторы цветных пятен), которые должны быть полезны для многих задач, но более поздние уровни ConvNet становятся все более специфичными для деталей классов, содержащихся в исходном наборе данных. Например, в случае ImageNet, который содержит множество пород собак, значительная часть репрезентативной мощности ConvNet может быть направлена на функции, специфичные для дифференциации между породами собак. - Предварительно обученные модели. Поскольку обучение современных ConvNet на нескольких графических процессорах ImageNet занимает 2–3 недели, часто можно увидеть, как люди выпускают свои окончательные контрольные точки ConvNet в пользу других пользователей, которые могут использовать сети для тонкой настройки. Например, в библиотеке Caffe есть Model Zoo, где люди делятся своими сетевыми весами.

Когда и как проводить тонкую настройку? Как вы решаете, какой тип переносного обучения вы должны выполнять на новом наборе данных? Это зависит от нескольких факторов, но два наиболее важных из них — это размер нового набора данных (маленький или большой) и его сходство с исходным набором данных (например, похожий на ImageNet с точки зрения содержимого изображений и классов, или сильно отличающийся, например, изображения микроскопа). Помня о том, что объекты ConvNet более универсальны в ранних слоях и более специфичны для исходного набора данных в более поздних слоях, вот некоторые общие эмпирические правила для навигации по 4 основным сценариям:
1. Новый набор данных имеет небольшой размер и похож на исходный набор данных. Поскольку объем данных невелик, тонкая настройка ConvNet не является хорошей идеей из-за проблем с переобучением. Поскольку данные аналогичны исходным данным, мы ожидаем, что функции более высокого уровня в ConvNet также будут иметь отношение к этому набору данных. Следовательно, лучшей идеей может быть обучение линейного классификатора на кодах CNN. 2. Новый набор данных имеет большой размер и похож на исходный набор данных. Поскольку у нас больше данных, мы можем быть уверены в том, что не переучимся, если попытаемся выполнить тонкую настройку через всю сеть. 3. Новый набор данных небольшой, но сильно отличается от исходного. Поскольку данные невелики, вероятно, лучше всего обучить только линейный классификатор. Поскольку набор данных сильно отличается, возможно, не стоит обучать классификатор с вершины сети, которая содержит больше функций, специфичных для набора данных. Вместо этого, возможно, лучше обучить классификатор SVM от активаций где-то раньше в сети. 4. Новый набор данных имеет большой размер и сильно отличается от исходного набора данных. Поскольку набор данных очень большой, можно ожидать, что мы сможем позволить себе обучить ConvNet с нуля. Однако на практике очень часто все же полезно инициализировать весами из предварительно обученной модели. В этом случае у нас было бы достаточно данных и уверенности для тонкой настройки по всей сети.

Практические советы. Есть несколько дополнительных моментов, о которых следует помнить при выполнении Transfer Learning:

  • Ограничения из предварительно обученных моделей. Обратите внимание, что если вы хотите использовать предварительно обученную сеть, вы можете быть немного ограничены с точки зрения архитектуры, которую вы можете использовать для вашего нового набора данных. Например, вы не можете произвольно удалять слои Conv из предварительно обученной сети. Тем не менее, некоторые изменения просты: благодаря совместному использованию параметров вы можете легко запустить предварительно обученную сеть на изображениях разного пространственного размера. Это ясно видно в случае слоев Conv/Pool, потому что их прямая функция не зависит от пространственного размера входного объема (до тех пор, пока шаги «подходят»). В случае слоев FC это по-прежнему верно, потому что слои FC могут быть преобразованы в сверточный слой: например, в AlexNet окончательный объем пула перед первым слоем FC имеет размер [6x6x512]. Следовательно, слой FC, рассматривающий этот объем, эквивалентен наличию сверточного слоя, который имеет размер рецептивного поля 6x6 и применяется с отступом 0.
  • Скорость обучения. Обычно для тонко настраиваемых весов ConvNet используется меньшая скорость обучения по сравнению с весами (случайно инициализированными) для нового линейного классификатора, который вычисляет баллы классов нового набора данных. Это связано с тем, что мы ожидаем, что веса ConvNet относительно хороши, поэтому мы не хотим искажать их слишком быстро и слишком сильно (особенно когда новый линейный классификатор над ними обучается на основе случайной инициализации).

Дополнительные примечания

NLP за 90 минут

Основы обработки естественного языка

Вопросы:

  1. Краткая история машинной обработки текстов (5 мин)
  2. Основные определения (5 мин)
  3. Методы предварительной обработки текста (10 мин)
  4. Моделирование языка (языковые модели) (10 мин)
  5. Нейронные сети в NLP (30 мин)
  6. GPT модели (30 мин)

1. Краткая история машинной обработки текстов

NLP (Natural Language Processing) - это область науки, которая изучает методы обработки текстов на естественных языках с помощью вычислительных машин. Основной акцент в NLP делается на прикладные методы, которые можно реализовать на языке программирования. Для вычислительно сложных методов используют языки низкого уровня (С, С++), потому что важна эффективность вычислений. Для проведение экспериментов используют языки высокого уровня (python), потому что для проверки гипотез важна скорость написания кода. Сегодня исследователям доступно множество библиотек на python, которые служат оберткой для оптимизированного машинного кода.

В 1913 году русский математик Андрей Андреевич Марков провел эксперимент по оценке частоты появления разных букв в тексте. Он выписал первые 20 000 букв поэмы А. С. Пушкина «Евгений Онегин» в одну длинную строчку из букв, опустив все пробелы и знаки пунктуации. Затем он переставил эти буквы в 200 решёток (по 10х10 символов в каждой), и начал подсчитывать гласные звуки в каждой строке и столбце, записывая результаты. Марков считал, что большинство явлений происходит по цепочке причинно-следственной связи и зависит от предыдущих результатов. Он хотел найти способ моделировать эти события посредством вероятностного анализа. Он обнаружил, что для любой буквы текста Пушкина выполнялось правило: если это была гласная, то скорее всего за ней будет стоять согласная, и наоборот.

В 1950 году в работе "Вычислительные машины и разум" ученый Алан Тьюринг предложил тест разумности для искуственного интеллекта.
Если компьютер сможет провести убедительную беседу с человеком в текстовом режиме, можно будет предположить, что он разумен.

В 1954 году в штаб-квартире корпорации IBM состоялся Джорджтаунский эксперимент — демонстрация возможностей машинного перевода. В ходе эксперимента был продемонстрирован полностью автоматический перевод более 60 предложений с русского языка на английский. Программа выполнялась на мейнфрейме IBM 701. В том же году первый эксперимент по машинному переводу был произведён в Институте точной механики и вычислительной техники АН СССР, на компьютере БЭСМ.

В 1966 году Джозеф Вейценбаум, работавший в лаборатории ИИ при MIT, разработал первый в мире чатбот "Элиза". Пользователь мог ввести некое утверждение или набор утверждений на обычном языке, нажать «ввод», и получить от машины ответ.

В 1986 Давид Румельхарт разработал базовую концепцию рекуррентной нейросети (recurrent neural network, RNN). Этот метод позволял решать такие задачи как распознавание речи и текста.

В 2017 году группа исследователей Google представила архитектуру трансформера (Transformer), которая позволяет обрабатывать тексты, в которых слова расположены в произвольном порядке. В настоящее время трансформеры используются в сервисах многих компаний, включая Яндекс и Google, являются основой для самых современных моделей GPT, Bert и т.д.

В 2023 году OpenAI опубликовала языковую модель GPT-4, которая легко проходит тест Тьюринга и порождает споры об опасности искусственного интеллекта.

2. Основные определения

Символ - это условный знак каких-либо понятий, идей, явлений. Первые наскальные символы обозначали зверей, охоту, солнце и другие предметы, которые отражались в сознании первобытных людей. Мы и сегодня часто используем символы эмоджи для отображения своих эмоций и скрытых смыслов. У разных народов сформировались свои уникальные алфавиты, которы представляют собой множества допустимых символов для письма.

У компьютера тоже есть уникальный набор символов, определяемый кодировкой. Например, кодировка ASCII (American standard code for information interchange) была стандартизована в 1963 году и определяет символы:
- десятичных цифр;
- латинского алфавита;
- национального алфавита;
- знаков препинания;
- управляющих символов.

С математической точки зрения алфавит как множество символов обозначается символом $V$.
Всевозможные комбинации символов, образующих конечные слова, в т.ч. состоящие из одного символа и бессмысленные комбинации, образуют множество $V^*$.

Слово - наименьшая единица языка, служащая для именования предметов, качеств, характеристик, взаимодействий, а также для служебных целей. Например слово "Я" состоит всего из одного символа и в русском языке обозначает меня как субъект.
Большинство слов состоят из нескольких символов, связанных в последовательность по определенным правилам.

Язык - это множество слов, которые несут в себе хоть какой-то смысл. Например, слово "тывщштс" не имеет смысла в русском языке, хоть и состоит из символов кириллицы. А слово "дом", наоборот, является вполне известным и входит в множество слов русского языка.
Формально язык обозначается символом $L$. В алфавите $V$ язык является подмножеством всех конечных слов $$L \in V^*$$

Текст - это зафиксированая на материальном носителе человеческая мысль в виде последовательности символов. Текст может состоять из одного или нескольких слов.

Представление текста в компьютере

Для человека символы, слова и тексты несут в себе определенный смысл. Компьютер же работает только с байтами. Рассмотрим пример, как реализовано посимвольное кодирование алфавита в кодировке ASCII.

bin dec hex symbol
110 0001 97 61 a
110 0010 98 62 b
110 0011 99 63 c
110 1010 106 6A j
110 1011 107 6B k

bin - двоичное представление символа,
dec - представление в десятичной системе счисления,
hex - представление в шестнадцатеричной системе счисления.
С полной таблицей кодировки ASCII можно ознакомиться по ссылке.
Для хранения одного символа латинского алфавита достаточно 7 бит. Для хранения одного символа небольшого национального алфавита (например, кириллицы) достаточно 8 бит. Для кодировки символов более объемных алфавитов (например, китайского) используют 16 бит или два байта.
Слова, как и тексты, хранятся в компьютерной памяти в виде последовательности символов.