Классификация изображений

Классификация изображений

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

Содержание:
1) Введение в классификацию изображений
2) Классификатор ближайшего соседа
3) Классификатор k - ближайших соседей
4) Наборы данных для настройки гиперпараметров
5) Применение kNN на практике
6) Дополнительные материалы

Введение в классификацию изображений

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

Пример. Например, на изображении ниже модель классификации изображений принимает одно изображение и присваивает вероятности четырём меткам: {«кошка», «собака», «шляпа», «кружка»}. Как показано на изображении, для компьютера изображение представляет собой один большой трёхмерный массив чисел. В этом примере изображение кошки имеет ширину 248 пикселей, высоту 400 пикселей и три цветовых канала: красный, зелёный, синий (или сокращённо RGB). Таким образом, изображение состоит из 248 x 400 x 3 чисел, или в общей сложности 297 600 чисел. Каждое число представляет собой целое число от 0 (чёрный) до 255 (белый). Наша задача — превратить эти четверть миллиона чисел в одну метку, например «кошка».

Задача классификации изображений состоит в том, чтобы предсказать одну метку для заданного изображения. Так же мы можем предсказать распределение вероятностей для всех меток, что отражает степень нашей уверенности в результате классификации. Изображения представляют собой трёхмерные массивы целых чисел от 0 до 255 размером «ширина x высота x 3». Число 3 обозначает три цветовых канала: красный, зелёный и синий.

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

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

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

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

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

Классификатор ближайшего соседа

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

Пример набора данных для классификации изображений: CIFAR-10.
Одним из популярных наборов данных для классификации изображений является набор данных CIFAR-10. Этот набор данных состоит из 60 000 крошечных изображений высотой и шириной 32 пикселя. Каждое изображение относится к одному из 10 классов: самолет, автомобиль, птица и т. д. Эти 60 000 изображений разделены на обучающую выборку из 50 000 изображений и тестовую выборку из 10 000 изображений. На изображении ниже вы можете увидеть 10 случайных примеров изображений для каждого класса.

Слева: примеры изображений из набора данных CIFAR-10.
Справа: в первом столбце показаны несколько тестовых изображений.

Рядом с каждым изображением изображены 10 наиболее похожих изображений из обучающей выборки.

Изначально, у нас есть обучающая выборка CIFAR-10 из 50 000 изображений (по 5000 изображений для каждой из 10 категорий). Мы хотим классифицировать оставшиеся 10 000 изображений. Классификатор ближайших соседей работает следующим образом. Берется тестовое изображение и сравается с каждым изображением из обучающей выборки. Будем считать, что метка тестового изображения будет такой же, как и у самого похожего на него изображения.

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

Мы не уточнили, как именно мы сравниваем два изображения. Технически изображения представляют собой просто два блока (тензора) размером 32 x 32 x 3. Один из самых простых способов — сравнивать изображения попиксельно и суммировать все разности. Другими словами, если у вас есть два изображения, представленные в виде векторов $I_1$, $I_2$, разумным выбором для их сравнения может быть расстояние L1:

$$ d_1 (I_1, I_2) = \sum_{p} \left| I^p_1 - I^p_2 \right| $$

Сумма берется по всем пикселям. Вот как выглядит эта процедура:

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

Давайте также посмотрим, как можно реализовать классификатор в коде. Сначала загрузим данные CIFAR-10 в память в виде четырех массивов: обучающие данные/метки и тестовые данные/метки. В приведенном ниже коде Xtr хранятся все изображения из обучающей выборки (объем 50 000 x 32 x 32 x 3), а соответствующий одномерный массив Ytr (длиной 50 000) содержит обучающие метки (от 0 до 9):

Xtr, Ytr, Xte, Yte = load_CIFAR10('data/cifar10/') # a magic function we provide
# flatten out all images to be one-dimensional 
Xtr_rows = Xtr.reshape(Xtr.shape[0], 32 * 32 * 3) # Xtr_rows becomes 50000 x 3072
Xte_rows = Xte.reshape(Xte.shape[0], 32 * 32 * 3) # Xte_rows becomes 10000 x 3072 

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

nn = NearestNeighbor() # create a Nearest Neighbor classifier class
nn.train(Xtr_rows, Ytr) # train the classifier on the training images and labels
Yte_predict = nn.predict(Xte_rows) # predict labels on the test images
# and now print the classification accuracy, which is the average number
# of examples that are correctly predicted (i.e. label matches)
print 'accuracy: %f' % ( np.mean(Yte_predict == Yte) )

Обратите внимание, что в качестве критерия оценки обычно используется метрика accuracy, Эта метрика измеряет долю правильных прогнозов в тестовой выборке. Обратите внимание, что все классификаторы, которые мы создадим, имеют общий интерфейс (API). У них есть метод train(X,y), который принимает на вход данные и метки для обучения. Внутри класса должна быть построена своего рода модель, которая предсказывает метки на основе данных. Метод predict(X) принимает новые данные и предсказывает метки.

Пример реализации простого классификатора ближайшего соседа с расстоянием $L_1$, который реализует интерфейс классификатора:

import numpy as np

class NearestNeighbor(object):
    def __init__(self):
        pass

    def train(self, X, y):
    """ X is N x D where each row is an example. Y is 1-dimension of size N
        The nearest neighbor classifier simply remembers all the training data """
        self.Xtr = X
        self.ytr = y

    def predict(self, X):
    """ X is N x D where each row is an example we wish to predict label for """
        num_test = X.shape[0]
        # lets make sure that the output type matches the input type
        Ypred = np.zeros(num_test, dtype = self.ytr.dtype)

        # loop over all test rows
        for i in range(num_test):
        # find the nearest training image to the i'th test image
        # using the L1 distance (sum of absolute value differences)
        distances = np.sum(np.abs(self.Xtr - X[i,:]), axis = 1)
        min_index = np.argmin(distances) # get the index with smallest distance
        Ypred[i] = self.ytr[min_index] # predict the label of the nearest example

        return Ypred

Если вы запустите этот код, то увидите, что классификатор достигает точности 38,6% на тестовой выборке CIFAR-10. Это более впечатляющий результат, чем случайное угадывание (которое дало бы 10% точности для 10 классов). Но он далёк от результатов человека, которые оцениваются примерно в 94%. Еще лучший результат можно получить с помощью свёрточных нейронных сетей, которые достигают примерно 95% (см. таблицу соревнования Kaggle по CIFAR-10).

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

$$ d_2 (I_1, I_2) = \sqrt{\sum_{p} \left( I^p_1 - I^p_2 \right)^2} $$

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

distances = np.sqrt(np.sum(np.square(self.Xtr - X[i,:]), axis = 1))

Обратите внимание на вычисление корня в функции np.sqrt. В практической реализации метода ближайшего соседа мы могли бы не использовать операцию извлечения квадратного корня, потому что он является монотонной функцией. То есть он масштабирует абсолютные значения расстояний, но сохраняет порядок. Поэтому с ним или без него, ближайшие соседи будут идентичны. Однако если применить классификатор ближайшего соседа к CIFAR-10 с L2 расстоянием, получится всего 35,4% точности. Это немного ниже, чем результат с расстоянием $L_1$.

Расстояние $L_1$ против $L_2$ .
Различие между этими двумя метриками в том, что расстояние $L_2$ более строгое, чем расстояние $L_1$. Это значит, что если имеется множество небольших расхождений и всего одно большое, расстояние $L_2$ будет больше, чем $L_1$ . Понятия расстояний $L_1$ и $L_2$ эквивалентно нормам, которые являются частными случаями p-нормы.

Классификатор k - ближайших соседей

когда мы хотим сделать более точный прогноз, нам не обязательно использовать только одну метку ближайшего изображения. Действительно, почти всегда можно добиться лучшего результата, используя так называемый классификатор k-ближайших соседей. Идея очень проста: вместо того, чтобы искать одно ближайшее изображение в обучающем наборе, мы найдём k ближайших изображений Дальше мы сравним их и устроим "голосование" за метку тестового изображения. В частности, когда k = 1, мы получаем классификатор ближайшего соседа. Интуитивно понятно, что чем больше значений k мы возьмем, тем больше будет сглаживающий эффект. Это первый пример гиперпараметра, который делает классификатор более устойчивым к ошибкам:

Пример разницы между классификатором «один ближайший сосед» и классификатором «ближайшие 5 соседей» с использованием двумерных точек и 3 классов (красный, синий, зелёный). Цветные области показывают границы решений, создаваемые классификатором с использованием расстояния $L_2$. Белые области показывают точки, которые классифицируются неоднозначно - голоса за классы равны как минимум для двух классов. Обратите внимание, что в случае классификатора 1-соседа ошибки создают небольшие островки вероятных неверных прогнозов. Например, зелёная точка в середине облака синих точек. В это же время классификатор 5-соседей сглаживает эти неровности, что, приводит к лучшему обобщению на тестовых данных. Также обратите внимание, что серые области на изображении 5-соседей вызваны равенством голосов ближайших соседей. Например, 2 соседа красные, следующие два соседа синие, последний сосед зелёный.

На практике почти всегда используется метод k-ближайших соседей. Но какое значение k следует использовать? Давайте рассмотрим этот вопрос поподробнее.

Наборы данных и настройки гиперпараметров

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

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

Итого: Оценивайте модель на тестовой выборке только один раз, в самом конце!

Существует правильный способ настройки гиперпараметров, который никак не затрагивает тестовый набор данных. Идея состоит в том, чтобы разделить обучающую выборку на две части: немного меньший обучающий набор и то, что называется выборкой для валидации. Используя в качестве примера CIFAR-10, мы могли бы использовать 49 000 обучающих изображений для обучения и оставить 1000 для валидации. Этот набор данных по сути используется для настройки гиперпараметров.

Вот как это может выглядеть в случае CIFAR-10:

# assume we have Xtr_rows, Ytr, Xte_rows, Yte as before
# recall Xtr_rows is 50,000 x 3072 matrix
Xval_rows = Xtr_rows[:1000, :] # take first 1000 for validation
Yval = Ytr[:1000]
Xtr_rows = Xtr_rows[1000:, :] # keep last 49,000 for train
Ytr = Ytr[1000:]

# find hyperparameters that work best on the validation set
validation_accuracies = []
for k in [1, 3, 5, 10, 20, 50, 100]:

# use a particular value of k and evaluation on validation data
nn = NearestNeighbor()
nn.train(Xtr_rows, Ytr)
# here we assume a modified NearestNeighbor class that can take a k as input
Yval_predict = nn.predict(Xval_rows, k = k)
acc = np.mean(Yval_predict == Yval)
print 'accuracy: %f' % (acc,)

# keep track of what works on the validation set
validation_accuracies.append((k, acc))

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

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

Кросс-валидация
В случаях, когда размер обучающих данных (и, следовательно, проверочных данных) может быть небольшим, люди иногда используют более сложный метод настройки гиперпараметров, называемый Кросс-валидацией. Если вернуться к нашему предыдущему примеру, то идея заключается в том, что вместо произвольного выбора первых 1000 точек данных в качестве проверочного набора, а остальных — в качестве обучающего, можно получить более точную и менее зашумлённую оценку того, насколько хорошо работает определённое значение k, перебирая различные проверочные наборы и усредняя результаты по ним. Например, при 5-кратной перекрёстной проверке мы разделили бы обучающие данные на 5 равных частей, использовали 4 из них для обучения, а 1 — для проверки. Затем мы бы определили, какая из выборок является контрольной, оценили бы производительность и, наконец, усреднили бы производительность по разным выборкам.


Пример 5-кратного выполнения перекрестной проверки для параметра k. Для каждого значения k мы тренируемся на 4 сгибах и оцениваем на 5-м. Следовательно, для каждого k мы получаем 5 значений точности для проверочного сгиба (точность отражается на оси y, и каждый результат равен точке). Линия тренда проводится через среднее значение результатов для каждого k, а столбики ошибок указывают на стандартное отклонение. Обратите внимание, что в данном конкретном случае перекрёстная проверка показывает, что значение около k = 7 лучше всего подходит для этого конкретного набора данных (соответствует пику на графике). Если бы мы использовали более 5 циклов, то могли бы ожидать более плавную (то есть менее шумную) кривую.


На практике люди предпочитают избегать перекрёстной проверки в пользу одного проверочного набора данных, поскольку перекрёстная проверка может быть ресурсозатратной. Обычно люди используют от 50% до 90% обучающих данных для обучения и остальную часть для проверки. Однако это зависит от множества факторов: например, если количество гиперпараметров велико, вы можете предпочесть использовать более крупные проверочные наборы данных. Если количество примеров в проверочном наборе невелико (возможно, всего несколько сотен или около того), безопаснее использовать перекрёстную проверку. На практике обычно используется 3-кратная, 5-кратная или 10-кратная перекрёстная проверка.


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


Плюсы и минусы классификатора ближайших соседей.

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

Кроме того, вычислительная сложность классификатора «ближайший сосед» является активной областью исследований, и существует несколько алгоритмов и библиотек приблизительного поиска ближайшего соседа (ANN), которые могут ускорить поиск ближайшего соседа в наборе данных (например, FLANN ). Эти алгоритмы позволяют найти компромисс между точностью поиска ближайшего соседа и его пространственной/временной сложностью во время поиска и обычно полагаются на этап предварительной обработки/индексирования, который включает в себя построение KD-дерева или запуск алгоритма k-средних.

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


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


Вот ещё одна визуализация, которая убедит вас в том, что использование разницы в пикселях для сравнения изображений недостаточно. Мы можем использовать метод визуализации под названием t-SNE, чтобы взять изображения CIFAR-10 и разместить их в двух измерениях так, чтобы их парные (локальные) расстояния сохранялись наилучшим образом. В этой визуализации изображения, которые показаны рядом, считаются очень близкими в соответствии с расстоянием $L_2$ по пикселям, которое мы разработали выше:


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


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

Применение kNN на практике

Подводя итог: - Мы рассмотрели задачу классификации изображений, в которой нам даётся набор изображений, каждое из которых помечено одной категорией. Затем нас просят предсказать эти категории для нового набора тестовых изображений и оценить точность прогнозов. - Мы представили простой классификатор под названием «классификатор ближайших соседей». Мы увидели, что существует множество гиперпараметров (например, значение k или тип расстояния, используемого для сравнения примеров), связанных с этим классификатором, и что не существует очевидного способа их выбора. - Мы увидели, что правильный способ задать эти гиперпараметры — разделить обучающие данные на две части: обучающий набор и поддельный тестовый набор, который мы называем набором для проверки. Мы пробуем разные значения гиперпараметров и оставляем те, которые обеспечивают наилучшую производительность на наборе для проверки. - Если вас беспокоит нехватка обучающих данных, мы обсудили процедуру под названием перекрёстная проверка, которая может помочь уменьшить погрешность при оценке наиболее эффективных гиперпараметров. - Как только мы находим оптимальные гиперпараметры, мы фиксируем их и проводим одну оценку на реальном тестовом наборе данных. - Мы увидели, что метод ближайшего соседа может обеспечить нам точность около 40% на CIFAR-10. Он прост в реализации, но требует хранения всего обучающего набора данных, и его сложно оценивать на тестовых изображениях. - В итоге мы увидели, что использование расстояний $L_1$ или $L_2\) по необработанным значениям пикселей нецелесообразно, поскольку эти расстояния сильнее коррелируют с фоном и цветовыми распределениями изображений, чем с их семантическим содержанием.

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

Если вы хотите применить kNN на практике (не на изображениях), действуйте следующим образом:

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

  2. Рассмотрите возможность снижения размерности данных. На практике для снижения размерности используются следующие методы:

  3. метод главных компонент ссылка на вики-страницу, ссылка на CS229, ссылка на блог,
  4. метод независимых компонент ссылка на вики-страницу, ссылка на блог
  5. случайные проекции.

  6. Разделите обучающие данные случайным образом на обучающую и проверочную (валидационную) выборки. Как правило, в обучающую выборку попадает от 70 до 90% данных. Этот параметр зависит от того, сколько у вас гиперпараметров и насколько сильно они влияют на результат. Если нужно оценить множество гиперпараметров, лучше использовать более крупную проверочную выборку для их эффективной оценки. Если вас беспокоит размер проверочной выборки, лучше разделить обучающие данные на части и выполнить кросс-валидацию.

  7. Обучите и оцените классификатор kNN на кросс-валидации. По возможности выполняйте кросс-валидацию для множества вариантов k и для разных типов расстояний ($L_1$ и $L_2$ — хорошие кандидаты).
    Если вы можете позволить себе потратить больше времени на вычисления, всегда безопаснее использовать кросс-валидацию. Чем больше циклов обучения пройдет, тем лучше, но тем дороже с точки зрения вычислений.

  8. Оцените задержку классификатора. Если ваш классификатор kNN работает слишком долгo, рассмотрите возможность использования библиотеки приближённых ближайших соседей. Например, библиотека FLANN позволяет ускорить поиск за счёт некоторой потери точности.

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

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

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

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

Понимание и визуализация

Понимание и визуализация

(эта страница в настоящее время находится в черновом варианте)

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

Визуализация активаций и веса первого слоя

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




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


Фильтры Conv/FC. Вторая распространенная стратегия заключается в визуализации весов. Обычно они наиболее интерпретируемы на первом слое CONV, который смотрит непосредственно на необработанные пиксельные данные, но также можно показать веса фильтров в более глубоких слоях сети. Весовые коэффициенты полезны для визуализации, потому что хорошо обученные сети обычно отображают красивые и плавные фильтры без каких-либо зашумленных узоров. Зашумленные паттерны могут быть индикатором сети, которая не обучалась достаточно долго, или, возможно, очень низкой интенсивности регуляризации, которая могла привести к переобучению.




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


Получение изображений, которые максимально активируют нейрон

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

Максимально активизирующие изображения для некоторых нейронов POOL5 (5-й слой пула) AlexNet. Значения активации и рецептивное поле конкретного нейрона показаны белым цветом. (В частности, обратите внимание, что нейроны POOL5 являются функцией относительно большой части входного изображения!) Можно видеть, что некоторые нейроны реагируют на верхнюю часть тела, текст или зеркальные блики.


Одна из проблем с этим подходом заключается в том, что нейроны ReLU не обязательно имеют какое-либо семантическое значение сами по себе. Скорее, более уместно думать о множественных нейронах ReLU как о базисных векторах некоторого пространства, представленного в виде участков изображения. Другими словами, визуализация показывает участки на краю облака представлений, вдоль (произвольных) осей, которые соответствуют весам фильтра. Это также можно увидеть по тому факту, что нейроны в ConvNet работают линейно над входным пространством, поэтому любое произвольное вращение этого пространства является запретным. Этот момент был далее аргументирован в книге Сегеди и др. «Интригующие свойства нейронных сетей», где они выполняют аналогичную визуализацию вдоль произвольных направлений в пространстве представления.

Встраивание кодов с помощью t-SNE

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

Чтобы произвести встраивание, мы можем взять набор изображений и использовать ConvNet для извлечения кодов CNN (например, в AlexNet 4096-мерный вектор прямо перед классификатором, и, что особенно важно, включая нелинейность ReLU). Затем мы можем подключить их к t-SNE и получить двумерный вектор для каждого изображения. Соответствующие изображения могут быть визуализированы в виде сетки:



Встраивание набора изображений в t-SNE на основе их кодов CNN. Изображения, которые находятся рядом друг с другом, также близки в пространстве репрезентации CNN, что подразумевает, что CNN «видит» их как очень похожие. Обратите внимание, что сходства чаще всего основаны на классах и семантике, а не на пикселях и цветах. Для получения более подробной информации о том, как была создана эта визуализация, связанный код, а также другие связанные визуализации в разных масштабах см. Визуализация кодов CNN в t-SNE.


Окклюзия частей изображения

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



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


Визуализация градиента данных и его друзей

Градиент данных.|

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

DeconvNet.

Визуализация и понимание сверточных сетей

Управляемое обратное распространение.

Стремление к простоте: Всесвёрточная сеть

Восстановление оригинальных изображений на основе кодов CNN

Понимание глубоких представлений изображений путем их инвертирования

Какой объем пространственной информации сохраняется?

Учатся ли ConvNet переписываться? (Вкратце: да)

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

ImageNet Wide Scale Visual Recognition Challenge

Обман ConvNet

Объяснение и использование состязательных примеров

Сравнение ConvNet с людьми-маркировщиками

Что я узнал, соревнуясь с ConvNet на ImageNet

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

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

Содержание: - Обзор архитектуры - Слои ConvNet - Сверточный слой - Слой пула - Слой нормализации - Полностью подключенный слой - Преобразование полносвязных слоев в сверточные слои - Архитектуры ConvNet - Узоры слоев - Шаблоны для определения размеров слоев - Тематические исследования (LeNet / AlexNet / ZFNet / GoogLeNet / VGGNet) - Вычислительные соображения - Дополнительные материалы

Сверточные нейронные сети (CNNs / ConvNets)

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

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

Обзор архитектуры

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

Обычные нейронные сети плохо масштабируются до полных изображений. В CIFAR-10 изображения имеют размер всего 32x32x3 (32 в ширину, 32 в высоту, 3 цветных канала), поэтому один полностью связанный нейрон в первом скрытом слое обычной нейронной сети будет иметь 32 * 32 * 3 = 3072 веса. Это количество все еще кажется управляемым, но очевидно, что эта полностью связанная структура не масштабируется до более крупных изображений. Например, изображение более приличного размера, например, 200x200x3, приведет к нейронам с весом 2002003 = 120 000. Более того, мы почти наверняка хотели бы иметь несколько таких нейронов, чтобы параметры быстро складывались! Очевидно, что такая полная связность является расточительной, а огромное количество параметров быстро приведет к переобучению.

3D объемы нейронов. Сверточные нейронные сети используют тот факт, что входные данные состоят из изображений, и они ограничивают архитектуру более разумным образом. В частности, в отличие от обычной нейронной сети, слои ConvNet имеют нейроны, расположенные в трех измерениях: ширина, высота, глубина. (Обратите внимание, что слово «глубина» здесь относится к третьему измерению объема активации, а не к глубине полной нейронной сети, которая может относиться к общему количеству слоев в сети.) Например, входные изображения в CIFAR-10 представляют собой входной объем активаций, а объем имеет размеры 32х32х3 (ширина, высота, глубина соответственно). Как мы вскоре увидим, нейроны в слое будут соединены только с небольшой областью слоя перед ним, а не со всеми нейронами в полном объеме. Более того, итоговый выходной слой для CIFAR-10 будет иметь размеры 1x1x10, так как к концу архитектуры ConvNet мы сведем полное изображение к единому вектору оценок классов, расположенных по размерности глубины. Вот визуализация:


Сверху: обычная 3-слойная нейронная сеть.
Снизу: ConvNet располагает свои нейроны в трех измерениях (ширина, высота, глубина), как это визуализировано в одном из слоев. Каждый слой ConvNet преобразует входной объем 3D в объем активации нейронов на выходе 3D. В этом примере красный входной слой содержит изображение, поэтому его ширина и высота будут соответствовать размерам изображения, а глубина будет равна 3 (красный, зеленый, синий каналы).


ConvNet состоит из слоев. У каждого слоя есть простой API: он преобразует входной 3D-объем в выходной 3D-объем с помощью некоторой дифференцируемой функции, которая может иметь или не иметь параметры.

Слои, используемые для построения ConvNet

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

Пример архитектуры: обзор. Мы рассмотрим это более подробно ниже, но простой ConvNet для классификации CIFAR-10 может иметь архитектуру [INPUT - CONV - RELU - POOL - FC]. Более подробно:

  • INPUT [32x32x3] будет содержать исходные значения пикселей изображения, в данном случае изображение шириной 32, высотой 32 и с тремя цветовыми каналами R,G,B.
  • Слой CONV будет вычислять выходные данные нейронов, которые соединены с локальными областями на входных данных, каждый из которых вычисляет скалярное произведение между их весами и небольшой областью, к которой они подключены во входном объеме. Это может привести к объему [32x32x12], если мы решили использовать 12 фильтров.
  • Слой RELU будет применять функцию поэлементной активации, такую как max(0,х) с пороговым значением на нуле. При этом размер тома остается неизменным ([32x32x12]).
  • Слой POOL выполнит операцию понижения дискретизации вдоль пространственных измерений (ширина, высота), в результате чего будет получен объем, такой как [16x16x12].
  • Уровень FC (т.е. полностью подключенный) будет вычислять баллы класса, в результате чего будет получен объем размера [1x1x10], где каждое из 10 чисел соответствует баллу класса, например, среди 10 категорий CIFAR-10. Как и в случае с обычными нейронными сетями и как следует из названия, каждый нейрон в этом слое будет связан со всеми числами в предыдущем объеме.

Таким образом, ConvNet слой за слоем преобразуют исходное изображение от исходных значений пикселей до итоговых оценок класса. Обратите внимание, что некоторые слои содержат параметры, а другие нет. В частности, слои CONV/FC выполняют преобразования, которые являются функцией не только активации входного объема, но и параметров (весов и смещений нейронов). С другой стороны, слои RELU/POOL будут реализовывать фиксированную функцию. Параметры в слоях CONV/FC будут обучаться с помощью градиентного спуска, чтобы оценки классов, вычисляемые ConvNet, соответствовали меткам в обучающем наборе для каждого изображения.

Вкратце:

  • Архитектура ConvNet в простейшем случае представляет собой список слоев, которые преобразуют объем изображения в выходной объем (например, содержат оценки классов)
  • Существует несколько различных типов слоев (например, CONV/FC/RELU/POOL на сегодняшний день являются наиболее популярными)
  • Каждый слой принимает входной 3D-объем и преобразует его в выходной 3D-объем с помощью дифференцируемой функции
  • Каждый слой может иметь или не иметь параметры (например, у CONV/FC есть, у RELU/POOL нет)
  • Каждый слой может иметь или не иметь дополнительные гиперпараметры (например, у CONV/FC/POOL есть, у RELU нет)


Активация примера архитектуры ConvNet. Начальный том хранит необработанные пиксели изображения (слева), а последний том хранит оценки класса (справа). Каждый объем активаций на пути обработки отображается в виде столбца. Так как визуализировать 3D-объемы сложно, мы выкладываем срезы каждого тома в ряды. Последний объем слоя содержит баллы для каждого класса, но здесь мы визуализируем только отсортированные 5 лучших баллов и печатаем этикетки каждого из них. Полный прототип веб-версии приведен в шапке нашего веб-сайта. Архитектура, показанная здесь, представляет собой крошечную сеть VGG, о которой мы поговорим позже.


Теперь мы опишем отдельные слои и детали их гиперпараметров и связуемости.

Сверточный слой

Уровень Conv является основным строительным блоком сверточной сети, который выполняет большую часть тяжелой вычислительной работы.

Обзор и интуиция без мозгов. Давайте сначала обсудим, что вычисляет слой CONV без аналогий между мозгом и нейронами. Параметры слоя CONV состоят из набора обучаемых фильтров. Каждый фильтр имеет небольшие пространственные размеры (по ширине и высоте), но простирается на всю глубину входного объема. Например, типичный фильтр на первом слое ConvNet может иметь размер 5x5x3 (т. е. 5 пикселей в ширину и высоту, и 3, поскольку изображения имеют глубину 3, цветовые каналы). Во время прямого прохода мы скользим (точнее, свертываем) каждый фильтр по ширине и высоте входного объема и вычисляем точечные произведения между входами фильтра и входом в любом положении. Когда мы перемещаем фильтр по ширине и высоте входного объема, мы создадим двумерную карту активации, которая дает ответы этого фильтра в каждом пространственном положении. Интуитивно сеть будет изучать фильтры, которые активируются, когда они видят какой-либо визуальный признак, такой как край определенной ориентации или пятно определенного цвета на первом слое, или, в конечном итоге, целые соты или узоры, похожие на колеса, на более высоких слоях сети. Теперь у нас будет целый набор фильтров в каждом слое CONV (например, 12 фильтров), и каждый из них создаст отдельную двухмерную карту активации. Мы наложим эти карты активации вдоль измерения глубины и получим выходной объем.

Взгляд на мозг. Если вы являетесь поклонником аналогий между мозгом и нейронами, то каждая запись в 3D-объеме вывода также может быть интерпретирована как выход нейрона, который смотрит только на небольшую область на входе и разделяет параметры со всеми нейронами слева и справа в пространстве (поскольку все эти числа являются результатом применения одного и того же фильтра).

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

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

Пример 1. Например, предположим, что входной объем имеет размер [32x32x3] (например, изображение RGB CIFAR-10). Если рецептивное поле (или размер фильтра) равно 5x5, то каждый нейрон в слое Conv будет иметь веса в области [5x5x3] во входном объеме, что в сумме составляет 5x5x3 = 75 весов (и параметр смещения +1). Обратите внимание, что степень связности вдоль оси глубины должна быть равна 3, так как это глубина входного объема.

Пример 2. Предположим, что входной объем имеет размер [16x16x20]. Затем, используя пример с размером рецептивного поля 3x3, каждый нейрон в слое Conv теперь будет иметь в общей сложности 3x3x20 = 180 соединений с входным объемом. Обратите внимание, что, опять же, связность является локальной в 2D-пространстве (например, 3x3), но полной по глубине ввода (20).




Сверху: Пример входного объема красным цветом (например, изображение CIFAR-10 размером 32x32x3) и пример объема нейронов в первом сверточном слое. Каждый нейрон в сверточном слое пространственно связан только с локальной областью во входном объеме, но на всю глубину (т.е. со всеми цветовыми каналами). Обратите внимание, что в глубине есть несколько нейронов (5 в этом примере), все они смотрят на одну и ту же область на входе: линии, которые соединяют этот столбец из 5 нейронов, не представляют веса (т.е. эти 5 нейронов не имеют одинаковых весов, но они связаны с 5 разными фильтрами), они просто указывают на то, что эти нейроны связаны или смотрят на одно и то же рецептивное поле или область входного объема. т.е. они имеют одно и то же рецептивное поле, но не одинаковые веса. Снизу: Нейроны из главы «Нейронные сети» остаются неизменными: они по-прежнему вычисляют скалярное произведение своих весов с последующим нелинейным значением, но их связность теперь ограничена локальными пространственными данными.


Пространственное расположение. Мы объяснили связь каждого нейрона в слое Conv с входным объемом, но мы еще не обсуждали, сколько нейронов находится в выходном объеме или как они организованы. Три гиперпараметра контролируют размер выходного объема: глубина, шаг и нулевое отступление. Мы обсудим их далее:
- Во-первых, глубина выходного объема — это гиперпараметр: он соответствует количеству фильтров, которые мы хотели бы использовать, каждый из которых учится искать что-то свое во входных данных. Например, если первый сверточный слой принимает в качестве входных данных исходное изображение, то различные нейроны в измерении глубины могут активироваться в присутствии различных ориентированных краев или цветовых пятен. Мы будем называть набор нейронов, которые смотрят на одну и ту же область входных данных, столбцом глубины (некоторые люди также предпочитают термин «волокно»). - Во-вторых, мы должны указать шаг, с которым мы перемещаем фильтр. Когда шаг равен 1, мы перемещаем фильтры по одному пикселю за раз. Когда шаг равен 2 (или редко 3 или более, хотя на практике это редкость), фильтры прыгают на 2 пикселя за раз, когда мы их перемещаем. Это позволит производить меньшие объемы выпуска в пространственном отношении. - Как мы скоро увидим, иногда будет удобно заполнять входной объем нулями по границе. Размер этого нулевого отступа является гиперпараметром. Приятная особенность нулевого заполнения заключается в том, что он позволяет нам контролировать пространственный размер выходных объемов (чаще всего, как мы скоро увидим, мы будем использовать его для точного сохранения пространственного размера входного объема, чтобы ширина и высота входного и выходного объема были одинаковыми).

Мы можем вычислить пространственный размер выходного объема как функцию от размера входного объема (W), размер рецептивного поля нейронов Conv слоя (F), шаг, с которым они наносятся (S) и количество использованного нулевого заполнения (P) на границе. Вы можете убедиться в том, что правильная формула для расчета количества нейронов «поместится» по формуле (W−F+2P)/S+1. Например, для входа 7x7 и фильтра 3x3 со stride 1 и pad 0 мы получим выход 5x5. С помощью шага 2 мы получим выход 3x3. Давайте также посмотрим еще на один графический пример:


Иллюстрация пространственного расположения. В этом примере есть только одно пространственное измерение (ось x), один нейрон с размером рецептивного поля F = 3, входной размер W = 5 и нулевое заполнение P = 1. Сверху: Нейрон шагал по входу с шагом S = 1, давая на выходе размер (5 - 3 + 2)/1 + 1 = 5. Снизу: Нейрон использует шаг S = 2, давая выход размера (5 - 3 + 2)/2 + 1 = 3. Обратите внимание, что шаг S = 3 не может быть использован, так как он не будет аккуратно помещаться по объему. С точки зрения уравнения, это можно определить, так как (5 - 3 + 2) = 4 не делится на 3. Веса нейронов в этом примере [1,0,-1] (показаны справа), и их смещение равно нулю. Эти веса являются общими для всех желтых нейронов (см. общие параметры ниже).


Использование нулевой набивки. В приведенном выше примере слева обратите внимание, что входной размер был равен 5, а выходной размер был равен: также 5. Это сработало так, потому что наши рецептивные поля были равны 3, и мы использовали нулевую набивку 1. Если бы не использовалось заполнение нуля, то выходной объем имел бы пространственную размерность только 3, потому что именно столько нейронов «поместилось» бы на исходном входе. Как правило, установка нулевого заполнения равным P=(F−1)/2. Когда шаг S=1 гарантирует, что входной и выходной объем будут иметь одинаковый пространственный размер. Очень часто используется нулевое заполнение таким образом, и мы обсудим все причины, когда будем говорить больше об архитектурах ConvNet.

Ограничения на шаг. Обратите внимание, что гиперпараметры пространственного расположения имеют взаимные ограничения. Например, когда входные данные имеют размер W=10, нулевой отступ не используется P=0, а размер фильтра равен F=3, то использовать stride было бы невозможно S=2__с (W−F+2P)/S+1=(10−3+0)/2+1=4.5__, т.е. не целое число, указывающее на то, что нейроны не «помещаются» аккуратно и симметрично на входе. Таким образом, эта настройка гиперпараметров считается недопустимой, и библиотека ConvNet может выдать исключение или обнулить заполнение оставшейся части, чтобы она поместилась, или обрезать входные данные, чтобы она поместилась, или что-то еще. Как мы увидим в разделе Архитектуры ConvNet, правильный выбор размеров ConvNet, чтобы все размеры «отрабатывались», может стать настоящей головной болью, которую использование нулевого заполнения и некоторые рекомендации по проектированию значительно облегчат.

Пример из жизни. Архитектура Крижевского и др., которая выиграла конкурс ImageNet в 2012 году, принимала изображения размером [227x227x3]. На первом сверточном слое использовались нейроны с размером рецептивного поля F=11__шаг __S=4 и без нулевой набивки P=0. Так как (227 - 11)/4 + 1 = 55, и так как слой Conv имел глубину K=96 , выходной объем слоя Conv имел размер [55x55x96]. Каждый из 55x55x96 нейронов в этом объеме был соединен с областью размера [11x11x3] во входном объеме. Более того, все 96 нейронов в каждой глубинной колонке подключены к одной и той же области входного канала [11x11x3], но, конечно, с разными весами. В качестве забавного отступления, если вы прочитаете реальную статью, она утверждает, что входные изображения были 224x224, что, безусловно, неверно, потому что (224 - 11)/4 + 1 совершенно очевидно не является целым числом. Это сбило с толку многих людей в истории ConvNets, и мало что известно о том, что произошло. Мое собственное предположение заключается в том, что Алекс использовал нулевое заполнение из 3 дополнительных пикселей, о которых он не упоминает в статье.

Совместное использование параметров. Схема совместного использования параметров используется в сверточных слоях для управления количеством параметров. Используя приведенный выше пример из реальной жизни, мы видим, что в первом слое Conv 55 * 55 * 96 = 290 400 нейронов, и каждый из них имеет 11 * 11 * 3 = 363 веса и 1 смещение. В совокупности это дает 290400 * 364 = 105 705 600 параметров только на первом уровне ConvNet. Понятно, что это очень большое число.

Оказывается, что мы можем значительно сократить число параметров, если сделать одно разумное допущение: если один признак полезен для вычисления в некотором пространственном положении (x,y), то он также должен быть полезен для вычисления в другом положении (\(x_2, y_2\)). Другими словами, обозначив один двумерный срез глубины как срез глубины (например, объем размером [55x55x96] имеет 96 срезов глубины, каждый размером [55x55]), мы собираемся ограничить нейроны в каждом срезе глубины, чтобы они использовали одни и те же веса и смещение. При такой схеме распределения параметров первый слой Conv в нашем примере теперь будет иметь только 96 уникальных наборов весов (по одному для каждого среза глубины), что в сумме составит 96 * 11 * 11 * 3 = 34 848 уникальных весов, или 34 944 параметра (+96 смещений). В качестве альтернативы, все 55*55 нейронов в каждом срезе глубины теперь будут использовать одни и те же параметры. На практике во время обратного распространения каждый нейрон в объеме будет вычислять градиент для своих весов, но эти градиенты будут суммироваться для каждого среза глубины и обновлять только один набор весов для каждого среза.

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



Примеры фильтров, изученных Крижским и др. Каждый из 96 показанных здесь фильтров имеет размер [11x11x3], и каждый из них является общим для нейронов 55 * 55 в одном глубинном срезе. Обратите внимание, что предположение о совместном использовании параметров относительно разумно: если обнаружение горизонтального края важно в каком-то месте изображения, оно должно быть интуитивно полезным и в каком-то другом месте из-за трансляционно-инвариантной структуры изображений. Таким образом, нет необходимости заново учиться обнаруживать горизонтальный ребро в каждом из 55 * 55 различных мест в выходном объеме слоя Conv.


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

Нумерные примеры. Чтобы сделать обсуждение выше более конкретным, давайте выразим те же идеи, но в коде и на конкретном примере. Предположим, что входной объем представляет собой массив numpy. Тогда:X - Колонка глубины (или волокно) в позиции будет активацией.(x,y) X[x,y,:] - Глубинным срезом или, что эквивалентноd, картой активации на глубине были бы активации X[:,:,d] .

Пример слоя conv. Предположим, что входной объем имеет форму . Предположим далее, что мы не используем нулевое заполнение (X X.shape: (11,11,4)P=0), что размер фильтра равен F=5, и что шаг является S=2. Таким образом, выходной объем будет иметь пространственный размер (11-5)/2+1 = 4, что дает объем с шириной и высотой 4. Карта активации в выходном объеме (назовем его ) будет выглядеть следующим образом (в этом примере вычисляются только некоторые элементы):V - V[0,0,0] = np.sum(X[:5,:5,:] * W0) + b0 - V[1,0,0] = np.sum(X[2:7,:5,:] * W0) + b0 - V[2,0,0] = np.sum(X[4:9,:5,:] * W0) + b0 - V[3,0,0] = np.sum(X[6:11,:5,:] * W0) + b0

Помните, что в numpy приведенная выше операция обозначает поэлементное умножение между массивами. Заметьте также, что вектор веса — это вектор веса этого нейрона и смещение. Здесь предполагается, что он имеет форму , так как размер фильтра равен 5, а глубина входного объема равна 4. Обратите внимание, что в каждой точке мы вычисляем скалярное произведение, как это было показано ранее в обычных нейронных сетях. Кроме того, мы видим, что мы используем тот же вес и смещение (из-за совместного использования параметров), и где размеры по ширине увеличиваются с шагом 2 (т.е. шаг). Чтобы построить вторую карту активации в выходном объеме, у нас есть:* W0 b0 W0 W0.shape: (5,5,4)
- V[0,0,1] = np.sum(X[:5,:5,:] * W1) + b1 - V[1,0,1] = np.sum(X[2:7,:5,:] * W1) + b1 - V[2,0,1] = np.sum(X[4:9,:5,:] * W1) + b1 - V[3,0,1] = np.sum(X[6:11,:5,:] * W1) + b1 - V[0,1,1] = np.sum(X[:5,2:7,:] * W1) + b1 (пример перехода по y) - V[2,3,1] = np.sum(X[4:9,6:11,:] * W1) + b1 (или по обоим)

где мы видим, что мы индексируем второе измерение глубины в V (по индексу 1), потому что мы вычисляем вторую карту активации, и что теперь используется другой набор параметров (W1). В приведенном выше примере мы для краткости опускаем некоторые другие операции, которые Conv Layer выполнил бы для заполнения других частей выходного массива V. Кроме того, вспомните, что эти карты активации часто отслеживаются по элементам с помощью функции активации, такой как ReLU, но здесь это не показано.

Резюме. Подводя итог, можно сказать, что слой Conv:

  • Принимает объем любого размера \(W_1 \times H_1 \times D_1\)
  • Требуется четыре гиперпараметра:
    • Количество фильтров K,
    • их пространственная протяженность F,
    • Шаг вперед S,
    • Величина нулевого отступа P.
  • Производит объем большого размера \(W_2 \times H_2 \times D_2\) где:
    • W2=(W1−F+2P)/S+1
    • H2=(H1−F+2P)/S+1 (т.е. ширина и высота вычисляются поровну по симметрии)
    • D2=K
  • Благодаря совместному использованию параметров он вводит \(F \cdot F \cdot D_1\) веса на фильтр, итого \((F \cdot F \cdot D_1) \cdot K\) веса и K cмещений.
  • В выходном объеме метод d-я глубина среза (размера \(W_2 \times H_2\) ) является результатом выполнения валидной свертки d-й фильтр по входной громкости с шагом S, а затем сместить на d-ое смещение.

Общая настройка гиперпараметров выглядит следующим образом: F=3,S=1,P=1. Тем не менее, существуют общие условности и эмпирические правила, которые мотивируют эти гиперпараметры. Смотрите раздел Архитектуры ConvNet ниже.

Демо свертки. Ниже приведена бегущая демонстрация слоя CONV. Поскольку 3D-объемы трудно визуализировать, все объемы (входной объем (синий), весовой объем (красный), выходной объем (зеленый)) визуализируются с каждым срезом глубины, уложенным в ряды. Входной объем имеет размер \(W_1 = 5, H_1 = 5, D_1 = 3\), а параметры слоя CONV равны \(K = 2, F = 3, S = 2, P = 1\). То есть у нас есть два фильтра размера 3×3, и наносятся они с шагом 2. Следовательно, размер выходного объема имеет пространственный размер (5 - 3 + 2)/2 + 1 = 3. Кроме того, обратите внимание, что отступ P=1 применяется к входному объему, при этом внешняя граница входного объема обнуляется. На приведенной ниже визуализации перебираются выходные активации (зеленый) и показано, что каждый элемент вычисляется путем поэлементного умножения выделенных входных данных (синий) на фильтр (красный), суммирования его и последующего смещения результата.


- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -

  • !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Реализация в виде умножения матриц. Обратите внимание, что операция свертки по сути выполняет скалярное произведение между фильтрами и локальными областями входных данных. Общий шаблон реализации слоя CONV заключается в том, чтобы воспользоваться этим фактом и сформулировать прямой проход сверточного слоя в виде умножения одной большой матрицы следующим образом: _ Локальные регионы на входном изображении растягиваются в столбцы с помощью операции, обычно называемой im2col. Например, если входной параметр имеет размер [227x227x3] и он должен быть свернут с помощью фильтров 11x11x3 на шаге 4, то мы возьмем [11x11x3] блоков пикселей на входе и растянем каждый блок в вектор-столбец размером 11113 = 363. Повторение этого процесса на входе с шагом 4 дает (227-11)/4+1 = 55 позиций как по ширине, так и по высоте, что приводит к выходной матрице im2col размера [363 x 3025], где каждый столбец представляет собой растянутое восприимчивое поле, и всего их 55 x 55 = 3025. Обратите внимание, что поскольку рецептивные поля перекрываются, каждое число во входном объеме может дублироваться в нескольких отдельных столбцах.X_col - Грузы слоя CONV аналогичным образом растягиваются в ряды. Например, если имеется 96 фильтров размера [11x11x3], то получится матрица размера [96 x 363]. W_row - Результат свертки теперь эквивалентен выполнению одного умножения большой матрицы , которое вычисляет скалярное произведение между каждым фильтром и каждым местоположением восприимчивого поля. В нашем примере результатом этой операции будет [96 x 3025], что дает выходные данные скалярного произведения каждого фильтра в каждом месте.np.dot(W_row, X_col) - В конечном итоге результат должен быть возвращен к его надлежащему выходному размеру [55x55x96].

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

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

Свертка 1х1. В качестве отступления, в нескольких работах используются свертки 1x1, впервые исследованные Network in Network. Некоторые люди поначалу путаются, видя свертки 1x1, особенно когда они исходят из фона обработки сигналов. Обычно сигналы двумерны, поэтому свертки 1x1 не имеют смысла (это просто поточечное масштабирование). Однако в ConvNet это не так, потому что необходимо помнить, что мы работаем с трехмерными объемами и что фильтры всегда распространяются на всю глубину входного объема. Например, если входные данные равны [32x32x3], то выполнение сверток 1x1 фактически будет выполнением трехмерных скалярных произведений (поскольку глубина входных данных равна 3 каналам).

Расширенные извилины. Недавняя разработка (см., например, статью Фишера Ю. и Владлена Колтуна) заключается в введении еще одного гиперпараметра в слой CONV, называемого дилатацией. До сих пор мы обсуждали только непрерывные фильтры CONV. Тем не менее, можно иметь фильтры, которые имеют промежутки между каждой клеткой, называемые расширением. Например, в одном измерении фильтр размера 3 будет вычислять на входных данных следующее: . Это расширение до 0. Для расширения 1 фильтр вместо этого будет вычислять ; Другими словами, между заявками есть разрыв в 1. Это может быть очень полезно в некоторых настройках для использования в сочетании с фильтрами 0-расширения, поскольку это позволяет объединять пространственную информацию по входным данным гораздо более агрессивно с меньшим количеством слоев. Например, если вы наложите два слоя CONV 3x3 друг на друга, то вы можете убедить себя, что нейроны на втором слое являются функцией участка входного сигнала 5x5 (мы бы сказали, что эффективное рецептивное поле этих нейронов равно 5x5). Если мы будем использовать расширенные извилины, то это эффективное рецептивное поле будет расти гораздо быстрее.w x w[0]*x[0] + w[1]*x[1] + w[2]*x[2]w[0]*x[0] + w[1]*x[2] + w[2]*x[4]

Слой пула

В архитектуре ConvNet обычно периодически вставляется слой Pooling между последовательными слоями Conv. Его функция состоит в том, чтобы постепенно уменьшать пространственный размер представления для уменьшения количества параметров и вычислений в сети и, следовательно, также контролировать переобучение. Слой пулинга работает независимо на каждом срезе глубины входных данных и изменяет его пространственный размер с помощью операции MAX. Наиболее распространенной формой является пулинговый слой с фильтрами размером 2x2, применяемыми с шагом 2 вниздискретизации каждого глубинного среза на входе на 2 по ширине и высоте, отбрасывая 75% активаций. Каждая операция MAX в этом случае будет принимать максимум более 4 чисел (маленькая область 2x2 в некотором глубинном срезе). Размер глубины остается неизменным. В более общем смысле, пуловый слой: - Принимает объем любого размера W1×H1×D1 - Требуется два гиперпараметра: - их пространственная протяженность F, - Шаг вперед S, - Производит объем большого размера \(W_2 \times H_2 \times D_2\) где: - \(W_2 = (W_1 - F)/S + 1\) - \(H_2 = (H_1 - F)/S + 1\) - \(D_2 = D_1\) - Вводит нулевые параметры, так как вычисляет фиксированную функцию входных данных - Для слоев Pooling заполнение входных данных не является обычным способом с использованием нулевого отступа.

Стоит отметить, что на практике встречаются только два распространенных варианта максимального слоя пула: Слой пулинга с F=3,S=2 (также называемое перекрывающимся пулом) и чаще F=2,S=2. Объединение размеров с большими рецептивными полями слишком разрушительно.

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




Пулинг слоя понижает дискретизацию объема пространственно, независимо в каждом глубинном срезе входного объема. Сверху: В этом примере входной объем размера [224x224x64] объединяется с фильтром размера 2, шаг 2 в выходной объем размера [112x112x64]. Обратите внимание, что глубина объема сохраняется. Снизу: Наиболее распространенной операцией понижения дискретизации является max, что приводит к максимальному объединению, показанному здесь с шагом 2. То есть, каждый макс берется над 4 числами (маленький квадратик 2х2).


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

Избавление от пулов. Многим людям не нравится операция по объединению, и они думают, что мы можем обойтись без нее. Например, Pursuit for Simplicity: The All Convolutional Net предлагает отказаться от пулового слоя в пользу архитектуры, состоящей только из повторяющихся слоев CONV. Чтобы уменьшить размер представления, они предлагают время от времени использовать больший шаг в слое CONV. Также было обнаружено, что отказ от слоев пула важен для обучения хороших генеративных моделей, таких как вариационные автоэнкодеры (VAE) или генеративно-состязательные сети (GAN). Вполне вероятно, что в будущих архитектурах будет очень мало или вообще не будет слоев пула.

Слой нормализации

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

Полносвязный слой

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

Преобразование слоев FC в слои CONV

Стоит отметить, что единственное различие между слоями FC и CONV заключается в том, что нейроны в слое CONV связаны только с локальной областью на входе, и что многие нейроны в объеме CONV имеют общие параметры. Тем не менее, нейроны в обоих слоях по-прежнему вычисляют точечные произведения, поэтому их функциональная форма идентична. Таким образом, оказывается, что можно преобразовывать между слоями FC и CONV: - Для любого слоя CONV существует слой FC, реализующий ту же прямую функцию. Матрица весов будет большой матрицей, которая в основном равна нулю, за исключением некоторых блоков (из-за локальной связности), где веса во многих блоках равны (из-за совместного использования параметров). - И наоборот, любой слой FC может быть преобразован в слой CONV. Например, слой FC с K=4096, то есть с учетом некоторого входного объема размера 7×7×512 может быть эквивалентно выражен в виде слоя CONV с помощью F=7,P=0,S=1,K=4096. Другими словами, мы устанавливаем размер фильтра точно равным размеру входного объема, и, следовательно, на выходе будет просто 1×1×4096 так как только один столбец глубины «помещается» поперек входного объема, давая тот же результат, что и исходный слой FC.

Конверсия FC->CONV. Из этих двух преобразований возможность преобразования слоя FC в слой CONV особенно полезна на практике. Рассмотрим архитектуру ConvNet, которая берет изображение размером 224x224x3, а затем использует ряд слоев CONV и слоев POOL для уменьшения объема активации до размера 7x7x512 (в архитектуре AlexNet, которую мы увидим позже, это делается с помощью 5 слоев пула, которые каждый раз уменьшают пространственную дискретизацию входных данных в два раза). Получаем итоговый пространственный размер 224/2/2/2/2/2 = 7). После этого AlexNet использует два слоя FC размера 4096 и, наконец, последний слой FC с 1000 нейронами, которые вычисляют баллы класса. Мы можем преобразовать каждый из этих трех слоев FC в слои CONV, как описано выше: - Замените первый слой FC, который смотрит на объем [7x7x512], на слой CONV, использующий размер фильтра F=7, дающий выходной объем [1x1x4096]. - Замените второй слой FC на слой CONV, использующий размер фильтра F=1, дающий выходной объем [1x1x4096] - Замените последний слой FC аналогичным образом, на F=1, выдающий итоговый вывод [1x1x1000]

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

Например, если образ 224x224 дает объем размера [7x7x512] - т.е. уменьшение на 32, то пересылка образа размера 384x384 через преобразованную архитектуру даст эквивалентный объем в размере [12x12x512], так как 384/32 = 12. Последующие 3 слоя CONV, которые мы только что преобразовали из слоев FC, теперь дадут окончательный объем размера [6x6x1000], поскольку (12 - 7)/1 + 1 = 6. Обратите внимание, что вместо одного вектора оценок классов размера [1x1x1000] мы теперь получаем целый массив оценок классов 6x6 на изображении 384x384.

Независимая оценка исходной ConvNet (со слоями FC) по кадрам 224x224 изображения 384x384 с шагом 32 пикселя дает результат, идентичный однократной пересылке преобразованной ConvNet.

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

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

  • Блокнот IPython по сетевой хирургии показывает, как выполнить преобразование на практике, в коде (с использованием Caffe)

Архитектуры ConvNet

Мы видели, что сверточные сети обычно состоят только из трех типов слоев: CONV, POOL (мы предполагаем Max pool, если не указано иное) и FC (сокращение от fully connected). Мы также явно напишем функцию активации RELU в виде слоя, который применяет элементную нелинейность. В этом разделе мы обсудим, как они обычно складываются в целые ConvNet.

Узоры слоев

Наиболее распространенная форма архитектуры ConvNet состоит из нескольких слоев CONV-RELU, затем за ними следуют слои POOL и повторяет этот шаблон до тех пор, пока изображение не будет объединено в пространстве до небольшого размера. В какой-то момент часто происходит переход к полносвязным слоям. Последний полносвязный слой содержит выходные данные, такие как баллы класса. Другими словами, наиболее распространенная архитектура ConvNet следует шаблону:

INPUT -> [[CONV -> RELU]*N -> POOL?]*M -> [FC -> RELU]*K -> FC

где * указывает на повторение, а POOL? указывает на необязательный слой пула. Более того, N >= 0 (и обычно N <= 3 M >= 0 K >= 0), , (и обычно K < 3 ). Например, вот некоторые распространенные архитектуры ConvNet, которые следуют этому шаблону:
- INPUT -> FC реализует линейный классификатор. Здесь N = M = K = 0 - INPUT -> CONV -> RELU -> FC - INPUT -> [CONV -> RELU -> POOL]*2 -> FC -> RELU -> FC. Здесь мы видим, что между каждым слоем POOL есть один слой CONV. -INPUT -> [CONV -> RELU -> CONV -> RELU -> POOL]*3 -> [FC -> RELU]*2 -> FC. Здесь мы видим два слоя CONV, расположенных перед каждым слоем POOL. Как правило, это хорошая идея для более крупных и глубоких сетей, поскольку несколько слоев CONV могут привести к более сложным характеристикам входного объема перед деструктивной операцией объединения.

Отдайте предпочтение стеку небольших фильтров CONV одному большому слою рецептивного поля CONV. Предположим, что вы накладываете три слоя CONV 3x3 друг на друга (конечно, с нелинейностями между ними). При таком расположении каждый нейрон на первом слое CONV имеет представление входного объема 3x3. Нейрон на втором слое CONV имеет представление 3x3 первого слоя CONV и, следовательно, представление входного объема 5x5. Аналогично, нейрон на третьем слое CONV имеет представление 3x3 на 2-й слой CONV и, следовательно, на входной объем 7x7. Предположим, что вместо этих трех слоев 3x3 CONV мы хотим использовать только один слой CONV с рецептивными полями 7x7. Эти нейроны будут иметь размер рецептивного поля входного объема, идентичный в пространственном масштабе (7x7), но с некоторыми недостатками. - Во-первых, нейроны будут вычислять линейную функцию над входными данными, в то время как три стека слоев CONV содержат нелинейности, которые делают их особенности более выразительными. - Во-вторых, если мы предположим, что все тома имеют C каналов, то можно видеть, что один слой 7x7 CONV будет содержать \(C \times (7 \times 7 \times C) = 49 C^2\) параметры, в то время как три слоя 3x3 CONV будут содержать только \(3 \times (C \times (3 \times 3 \times C)) = 27 C^2\) параметры. Интуитивно понятно, что наложение слоев CONV с маленькими фильтрами в отличие от одного слоя CONV с большими фильтрами позволяет нам выразить более мощные функции входных данных с меньшим количеством параметров. В качестве практического недостатка нам может потребоваться больше памяти для хранения всех результатов промежуточного слоя CONV, если мы планируем использовать обратное распространение.

Недавние уходы. Следует отметить, что традиционная парадигма линейного списка слоев в последнее время была поставлена под сомнение в архитектурах Google Inception, а также в современных (современных) Residual Networks от Microsoft Research Asia. Оба они (см. подробности ниже в разделе тематических исследований) имеют более сложные и разные структуры подключения.

__На практике: используйте то, что лучше всего работает на ImageNet__. Если вы чувствуете некоторую усталость, думая об архитектурных решениях, вам будет приятно узнать, что в 90% или более приложений вам не нужно беспокоиться об этом. Я предпочитаю резюмировать этот момент как «не будьте героем» : вместо того, чтобы создавать свою собственную архитектуру для решения проблемы, вы должны посмотреть, какая архитектура в настоящее время лучше всего работает на ImageNet, загрузить предварительно обученную модель и настроить ее на основе ваших данных. В редких случаях приходится обучать ConvNet с нуля или проектировать его с нуля. Я также говорил об этом в школе Deep Learning.

Шаблоны для определения размеров слоев

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

Входной слой (содержащий изображение) должен быть кратен 2 много раз. К распространенным номерам относятся 32 (например, CIFAR-10), 64, 96 (например, STL-10) или 224 (например, общие ImageNet ConvNet), 384 и 512.

Для конвальных слоев следует использовать небольшие фильтры (например, 3x3 или максимум 5x5), с шагом S=1 и, что особенно важно, заполнение входного объема нулями таким образом, чтобы слой conv не изменял пространственные размеры входных данных. То есть, когда F=3, то с помощью P=1 сохранит исходный размер входных данных. Когда F=5, P=2. Для генерала F, видно, что P=(F−1)/2 cохраняет размер ввода. Если вам нужно использовать фильтры большего размера (например, 7x7 или около того), это обычно можно увидеть только на самом первом выпуклом слое, который смотрит на входное изображение.

Слои пула отвечают за понижение дискретизации пространственных размеров входных данных. Наиболее распространенной настройкой является использование max-pooling с рецептивными полями 2x2 (т.е. F=2), и с шагом 2 (т.е. S=2). Обратите внимание, что при этом отбрасывается ровно 75% активаций входного объема (из-за уменьшения дискретизации на 2 раза как по ширине, так и по высоте). Другой, чуть менее распространенный вариант — использование рецептивных полей 3x3 с шагом 2, но это делает «подгонку» более сложной (например, слой 32x32x3 потребует нулевого заполнения для использования с максимальным объединением полей с рецептивным полем 3x3 и шагом 2). Очень редко можно увидеть, что размеры рецептивных полей для максимального пула больше 3, потому что в этом случае пулинг слишком потерян и агрессивен. Обычно это приводит к ухудшению производительности.

Уменьшение головной боли при уменьшении размера. Представленная выше схема радует тем, что все слои CONV сохраняют пространственный размер входных данных, в то время как только слои POOL отвечают за пространственное понижение объемов. В альтернативной схеме, где мы используем шаги больше 1 или не обнуляем входные данные в слоях CONV, нам пришлось бы очень тщательно отслеживать входные объемы по всей архитектуре CNN и убедиться, что все шаги и фильтры «работают» , и что архитектура ConvNet хорошо и симметрично связана.

Зачем использовать strace of 1 в CONV? Меньшие шаги лучше работают на практике. Кроме того, как уже упоминалось, шаг 1 позволяет нам оставить всю пространственную понижение дискретизации слоям POOL, при этом слои CONV преобразуют только входной объем по глубине.

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

Компрометация из-за ограничений памяти. В некоторых случаях (особенно на ранних этапах архитектуры ConvNet) объем памяти может очень быстро увеличиваться с помощью эмпирических правил, представленных выше. Например, фильтрация изображения размером 224x224x3 с тремя слоями CONV 3x3 с 64 фильтрами каждый и отступом 1 создаст три объема активации размером [224x224x64]. Это составляет в общей сложности около 10 миллионов активаций, или 72 МБ памяти (на изображение, как для активаций, так и для градиентов). Поскольку графические процессоры часто имеют узкие места из-за памяти, может потребоваться пойти на компромисс. На практике люди предпочитают идти на компромисс только на первом уровне CONV сети. Например, одним из компромиссов может быть использование первого слоя CONV с размерами фильтра 7x7 и шагом 2 (как в сети ZF). В качестве другого примера, AlexNet использует размеры фильтров 11x11 и stride 4.

Тематические исследования

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

  • LeNet. Первые успешные приложения сверточных сетей были разработаны Яном Лекуном в 1990-х годах. Из них наиболее известной является архитектура LeNet, которая использовалась для чтения почтовых индексов, цифр и т. д.
  • AlexNet. Первой работой, которая популяризировала сверточные сети в компьютерном зрении, стала сеть AlexNet, разработанная Алексом Крижевским, Ильей Суцкевером и Джеффом Хинтоном. В 2012 году AlexNet был представлен на конкурс ImageNet ILSVRC и значительно превзошел занявшего второе место (ошибка в топ-5 16% по сравнению с ошибкой в 26%, занявшей второе место). Сеть имела очень похожую архитектуру на LeNet, но была глубже, больше и включала сверточные слои, наложенные друг на друга (ранее было обычным делом иметь только один слой CONV, за которым всегда следовал слой POOL).
  • ZF Net. Победителем ILSVRC 2013 стала сверточная сеть от Мэтью Зейлера и Роба Фергуса. Она стала известна как ZFNet (сокращение от Zeiler & Fergus Net). Это было усовершенствование AlexNet за счет настройки гиперпараметров архитектуры, в частности, за счет увеличения размера средних сверточных слоев и уменьшения размера шага и фильтра на первом слое.
  • GoogLeNet. Победителем ILSVRC 2014 стала сверточная сеть от Сегеди и др. от Google. Ее основным вкладом стала разработка модуля Inception, который значительно сократил количество параметров в сети (4M, по сравнению с AlexNet с 60M). Кроме того, в этой статье используется Average Pooling вместо Fully Connected layers в верхней части ConvNet, что устраняет большое количество параметров, которые не имеют большого значения. Существует также несколько последующих версий GoogLeNet, последняя из которых Inception-v4.
  • VGGNet. Второе место на ILSVRC 2014 заняла сеть Карена Симоняна и Эндрю Зиссермана, которая стала известна как VGGNet. Его основной вклад заключался в том, что он показал, что глубина сети является критически важным компонентом для хорошей производительности. Их окончательная лучшая сеть содержит 16 слоев CONV/FC и, что привлекательно, отличается чрезвычайно однородной архитектурой, которая выполняет только свертки 3x3 и пул 2x2 от начала до конца. Их предварительно обученная модель доступна для использования в Caffe по принципу «подключи и работай». Недостатком VGGNet является то, что он дороже в оценке и использует гораздо больше памяти и параметров (140M). Большинство этих параметров находятся в первом полностью связанном слое, и с тех пор было обнаружено, что эти слои FC могут быть удалены без снижения производительности, что значительно сокращает количество необходимых параметров.
  • ResNet.. Остаточная сеть, разработанная Каим Хе и др., стала победителем ILSVRC 2015. Она оснащена специальными соединениями для пропуска и интенсивным использованием пакетной нормализации. В архитектуре также отсутствуют полностью связанные слои в конце сети. Читателю также предлагается ознакомиться с презентацией Кайминга (видео, слайды) и некоторыми недавними экспериментами, которые воспроизводят эти сети в Torch. В настоящее время ResNet являются самыми современными моделями сверточных нейронных сетей и являются выбором по умолчанию для использования ConvNet на практике (по состоянию на 10 мая 2016 года). В частности, см. более поздние разработки, которые корректируют исходную архитектуру, из книги Каим Хе и др. IСопоставления идентификационных данных в глубоких остаточных сетях (опубликована в марте 2016 г.).

VGGNet в деталях. Давайте разберем VGGNet более подробно в качестве тематического исследования. Вся сеть VGGNet состоит из слоев CONV, которые выполняют свертки 3x3 со stride 1 и pad 1, и из слоев POOL, которые выполняют 2x2 max pooling с stride 2 (и без отступа). Мы можем записывать размер представления на каждом шаге обработки и отслеживать как размер представления, так и общее количество весов:

INPUT: [224x224x3]        memory:  224*224*3=150K   weights: 0
CONV3-64: [224x224x64]  memory:  224*224*64=3.2M   weights: (3*3*3)*64 = 1,728
CONV3-64: [224x224x64]  memory:  224*224*64=3.2M   weights: (3*3*64)*64 = 36,864
POOL2: [112x112x64]  memory:  112*112*64=800K   weights: 0
CONV3-128: [112x112x128]  memory:  112*112*128=1.6M   weights: (3*3*64)*128 = 73,728
CONV3-128: [112x112x128]  memory:  112*112*128=1.6M   weights: (3*3*128)*128 = 147,456
POOL2: [56x56x128]  memory:  56*56*128=400K   weights: 0
CONV3-256: [56x56x256]  memory:  56*56*256=800K   weights: (3*3*128)*256 = 294,912
CONV3-256: [56x56x256]  memory:  56*56*256=800K   weights: (3*3*256)*256 = 589,824
CONV3-256: [56x56x256]  memory:  56*56*256=800K   weights: (3*3*256)*256 = 589,824
POOL2: [28x28x256]  memory:  28*28*256=200K   weights: 0
CONV3-512: [28x28x512]  memory:  28*28*512=400K   weights: (3*3*256)*512 = 1,179,648
CONV3-512: [28x28x512]  memory:  28*28*512=400K   weights: (3*3*512)*512 = 2,359,296
CONV3-512: [28x28x512]  memory:  28*28*512=400K   weights: (3*3*512)*512 = 2,359,296
POOL2: [14x14x512]  memory:  14*14*512=100K   weights: 0
CONV3-512: [14x14x512]  memory:  14*14*512=100K   weights: (3*3*512)*512 = 2,359,296
CONV3-512: [14x14x512]  memory:  14*14*512=100K   weights: (3*3*512)*512 = 2,359,296
CONV3-512: [14x14x512]  memory:  14*14*512=100K   weights: (3*3*512)*512 = 2,359,296
POOL2: [7x7x512]  memory:  7*7*512=25K  weights: 0
FC: [1x1x4096]  memory:  4096  weights: 7*7*512*4096 = 102,760,448
FC: [1x1x4096]  memory:  4096  weights: 4096*4096 = 16,777,216
FC: [1x1x1000]  memory:  1000 weights: 4096*1000 = 4,096,000

TOTAL memory: 24M * 4 bytes ~= 93MB / image (only forward! ~*2 for bwd)
TOTAL params: 138M parameters  

Как и в случае со сверточными сетями, обратите внимание, что большая часть памяти (а также вычислительного времени) используется в ранних слоях CONV, а большинство параметров — в последних слоях FC. В данном конкретном случае первый слой FC содержит 100 м грузов из общего числа 140 м.

Вычислительные соображения

Самым большим узким местом, о котором следует знать при создании архитектур ConvNet, является узкое место памяти. Многие современные графические процессоры имеют ограничение в 3/4/6 ГБ памяти, а лучшие графические процессоры имеют около 12 ГБ памяти. Существует три основных источника памяти, которые необходимо отслеживать:

  • Из промежуточных размеров объемов: Это исходное количество активаций на каждом уровне ConvNet, а также их градиенты (одинакового размера). Как правило, большая часть активаций происходит на более ранних уровнях ConvNet (т.е. на первых уровнях Conv). Они сохраняются, потому что они нужны для обратного распространения, но умная реализация, которая запускает ConvNet только во время тестирования, в принципе могла бы значительно сократить это, сохраняя только текущие активации на любом уровне и отбрасывая предыдущие активации на уровнях ниже.
  • Из размеров параметров: Это числа, которые содержат параметры сети, их градиенты во время обратного распространения и, как правило, также кэш шагов, если оптимизация выполняется с использованием momentum, Adagrad или RMSProp. Следовательно, память для хранения только вектора параметров обычно должна быть умножена по крайней мере на 3 или около того.
  • Каждая реализация ConvNet должна поддерживать различную память, такую как пакеты данных изображений, возможно, их дополненные версии и т.д.

После того, как вы получили приблизительную оценку общего количества значений (для активаций, градиентов и разного), это число следует преобразовать в размер в ГБ. Возьмите количество значений, умножьте на 4, чтобы получить исходное количество байтов (поскольку каждая плавающая точка равна 4 байтам, или, возможно, на 8 для двойной точности), а затем разделите на 1024 несколько раз, чтобы получить объем памяти в КБ, МБ и, наконец, в ГБ. Если ваша сеть не подходит, обычной эвристикой для «подгонки» является уменьшение размера пакета, поскольку большая часть памяти обычно потребляется активациями.

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

Дополнительные ресурсы, связанные с реализацией:

Обучение нейронных сетей 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.