Сверточный слой обратное распространение ошибки

Время на прочтение
5 мин

Количество просмотров 82K

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

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

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

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

Алгоритм обратного распространения ошибки

Для обучения описанной нейронной сети был использован алгоритм обратного распространения ошибки (backpropagation). Этот метод обучения многослойной нейронной сети называется обобщенным дельта-правилом. Метод был предложен в 1986 г. Румельхартом, Макклеландом и Вильямсом. Это ознаменовало возрождение интереса к нейронным сетям, который стал угасать в начале 70-х годов. Данный алгоритм является первым и основным практически применимым для обучения многослойных нейронных сетей.

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

Основные соотношения метода обратного распространения ошибки получены при следующих обозначениях:

Величина ошибки определяется по формуле 2.8 среднеквадратичная ошибка:

Неактивированное состояние каждого нейрона j для образа p записывается в виде взвешенной суммы по формуле 2.9:

Выход каждого нейрона j является значением активационной функции

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

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

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

Производную функции ошибки по конкретному образу можно записать по правилу цепочки, формула 2.12:

Ошибка нейрона обычно записывается в виде символа δ (дельта). Для выходного слоя ошибка определена в явном виде, если взять производную от формулы 2.8, то получим t минус y, то есть разницу между желаемым и полученным выходом. Но как рассчитать ошибку для скрытых слоев? Для решения этой задачи, как раз и был придуман алгоритм обратного распространения ошибки. Суть его заключается в последовательном вычислении ошибок скрытых слоев с помощью значений ошибки выходного слоя, т.е. значения ошибки распространяются по сети в обратном направлении от выхода к входу.

Ошибка δ для скрытого слоя рассчитывается по формуле 2.13:

Алгоритм распространения ошибки сводится к следующим этапам:

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

Алгоритм обратного распространения ошибки в многослойном персептроне продемонстрирован ниже:

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

Расчет ошибки на подвыборочном слое

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


Рисунок 2.8 — Интерпретация операции свертки в многослойный вид, где связи с одинаковым цветом имеют один и тот же вес. Синим цветом обозначена подвыборочная карта, разноцветным – синаптическое ядро, оранжевым – получившаяся свертка

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

Рисунок 2.9 — Вычисление δ подвыборочного слоя за счет δ сверточного слоя и ядра

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

Рисунок 2.10 — Результат операции обратной свертки


Рисунок 2.11 — Повернутое ядро на 180 градусов сканирует сверточную карту

Расчет ошибки на сверточном слое

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

Заключение

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

Источники

Алгоритм обратного распространения ошибки для сверточной нейронной сети

Обратное распространение ошибки в сверточных слоях
раз и два

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

Еще можно почитать в РГБ диссертацию Макаренко: АЛГОРИТМЫ И ПРОГРАММНАЯ СИСТЕМА КЛАССИФИКАЦИИ

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

Формат данных

Картинки в большинстве случаев представляют собой упорядоченный набор пикселей, где каждый пиксель – это вектор из трех «каналов» (интенсивность красного, интенсивность зелёного, интенсивность синего).

2_rgb_rooster_1.svg

Каждая интенсивность характеризуется числом от 0 до 1, но для привычных нам изображений этот интервал равномерно дискретизирован, чтобы уместиться в 8 бит (от 0 до 255), для экономии памяти. При этом (0, 0, 0), нулевая интенсивность, соответствует чёрному цвету, а (255, 255, 255), максимальная интенсивность, – белому.

Когда мы наблюдаем изображение на мониторе компьютера, мы видим эти пиксели «уложенными» в строки одинаковой длины (человек не сможет воспринять картинку, вытянутую в один вектор). Длину каждой такой строки называют шириной W картинки, а количество строк – высотой H. Резюмирую, мы можем рассматривать картинку, как тензор HxWx3, состоящий из чисел uint8.

1_rgb_split_rooster_1.svg

Существует множество разных форматов хранения картинок: вместо трех интенсивностей мы можем использовать триплет (оттенок,насыщенность,интенсивность), а сами картинки хранить, например, как тензор CxHxW.

MLP

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

Однако, такой подход имеет несколько недостатков:

  1. Количество параметров. В первом слое у нас получается HxWxCxCout параметров, где Cout – это количество нейронов в первом слое. Если поставить Cout слишком маленьким, мы рискуем потерять много важной информации, особенно, если рассматривать картинки размером, например, 1920×1080. Если же выставить Cout большим, рискуем получить слишком много параметров (а это только первый слой), а с этим и все вытекающие проблемы (переобучение, сложность оптимизации).

  2. Структура данных никак не учитывается. Что здесь имеется в виду под «структурой»? Попробуем объяснить на примере. Для этого рассмотрим картинку щеночка:

    puppy.jpg

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

    shifted_puppy.jpg

    Точно также мы останемся неизменны в своем мнении, если картинку отмасштабировать:

    scaled_puppy.jpg

    или повернуть/развернуть:

    rotated_puppy.jpg
    flipped_puppy.jpg

Получается, что нейронная сеть должна «сама» понять, что ее ответ должен быть инвариантен к описанным преобразованиям. Но, обычно, это достигается за счет увеличения количества нейронов в скрытых слоях (как мы можем помнить из universal approximation theorem), что и так для нас является головной болью из-за первого пункта.

С частью этих проблем нам поможет новый «строительный блок» – свёртка. О ней в следующем разделе.

Свёртки

Мотивация

Строгое определение свёртки мы дадим ниже, а вначале разберёмся в мотивации.

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

gif_shift_template.svg

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

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

padded_puppy.jpg

Возьмём окно размером с исходную картинку, и будем его сдвигать на все возможные смещения внутри нового изображения:

gif_conv_template0.svg

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

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

11_image_tensor_conv_cdaf6d395d.svg

Каждый «кубик» на картинке – это число. Большой черный тензор слева – это изображение щеночка $X$. Фиолетовым на нем выделено окно, из которого мы достаем все пиксели и разворачиваем в вектор (аналогично операции flatten в numpy) $v$. Далее этот вектор умножается на вектор весов класса «щенок» $w_1$, и получается число $k_1$ – логит интересующего класса. Добавив остальные классы, получим матрицу весов $W$ – прямо как в мультиномиальной логистической регрессии. Эту операцию мы повторяем для каждого возможного сдвига окна свёртки.

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

Вопрос на подумать. Какой геометрический смысл имеет свёртка с ядром

$$B_1 = frac19begin{pmatrix}1 & 1 & 1\ 1 & 1 & 1\ 1 & 1 & 1end{pmatrix}?$$

А с ядром

$$B_2 = begin{pmatrix}-1 & -1 & -1\ -1 & 8 & -1\ -1 & -1 & -1end{pmatrix}?$$

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

conv_examples.png

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

Вопрос на подумать. На краях картинок из ответа к предыдущему вопросу заметны тёмные рамки. Что это такое? Откуда они берутся?

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

  • Дополнить изображение по краям нулями. Когда мы будем рассматривать окна свёртки с центрами в крайних пикселях, они будут захватывать эти нули. Такая свёртка будет превращать изображение размером HxWx3 в изображение размером HxW, без уменьшения размера. Но так как нули соответствуют чёрному цвету, это будет вносить определённые изменения в крайние пиксели результата. Именно благодаря этому у картинок из предыдущего вопроса на подумать по краям появились тёмные рамки.
  • Разрешить только такие окна, которые целиком лежат внутри изображения. Это будет приводить к падению размера. Например, для окна размером 5x5 картинка размером HxWx3 превратится в картинку размером (H-2)x(W-2).

Решив проблему обеспечения устойчивости к сдвигу картинки и имея на руках наш огромный свёрточный фильтр, давайте попробуем теперь справиться с первой проблемой – количество параметров. Самое простое, что можно придумать, – это уменьшить размер окна с HxW до, допустим, kxk (обычно нечётное и $k in [3,11])$. В этом случае получается радикальное снижение количества параметров и сложности вычислений.

gif_small_conv_template.svg

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

very_small_conv.png

Область картинки, на которую «смотрит» наша нейронная сеть, назвается receptive field – и про него приходится часто думать в задачах компьютерного зрения. Давайте и мы подумаем, как его можно было бы увеличить, не увеличивая размер ядра. Вспомним, что в нашей нейронке сейчас есть только один слой, сразу предсказывающий класс. Выглядит так, что мы можем применить уже знакомую технику стекинга слоев: пусть на первой стадии мы делаем $C_1$ разных свёрток с фильтрам размером kxk. Результаты каждой свёртки можно упорядочить в виде новой «картинки», а из этих «картинок» сложить трёхмерный тензор. Получаем так называемую карту признаков размером HxWxC_1. Применим к ней поэлементно нелинейность и воспользуемся K новыми свёртками для получения предсказаний для каждого пикселя. На таком шаге получается, что наш receptive field для финальных нейронов вырос от kxk до (2k-1)x(2k-1) (пояснение на картинке). Повторяя такую операцию, мы можем добиться, чтобы наши финальные нейроны уже могли «видеть» почти всю нужную информацию для хорошего предикта. Более того, у нас возникает меньшее количество параметров и падает сложность вычислений в сравнении с использованием одной большой полносвязной сети.

Как это схематично выглядит:

15_conv_composition_e686591745.svg

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

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

16_receptive_field_fbd1c12519.svg

На картинке схематично изображен «плоский» двумерный тензор (количество каналов = 1), к которому последовательно применили три свёртки 3×3. В каждом случае рассматривается пиксель в центре. Каждый соответствующий тензор помечен, как $L_i$. Если рассматривать первую свёртку ($Xto L_1$), то размер receptive field равен размеру е окна = 3. Рассмотрим вторую свёртку $L_1 to L_2$. В ее вычислении участвуют пиксели из квадрата 3х3, причём каждый из них, в свою очередь, был получен при помощи предыдущей свёртки $X to L_1$. Получается, что receptive field композиции свёрток $Xto L_1to L_2$ – это объединение receptive fields свёртки $Xto L_1$ по всем пикселям из окна свёртки $L_1to L_2$, образуя новый, размером 5×5. Аналогичные рассуждения можно повторить и для всех последующих свёрток.

Ещё один способ увеличить receptive field – это использовать dilated convolution, в которых окно свёртки (то есть те пиксели картинки, на которые умножается ядро) не обязано быть цельным, а может идти с некоторым шагом (вообще говоря, даже разным по осям H и W). Проиллюстрируем, как будет выглядеть окно для обычной свёртки и для свёртки с шагом dilation=2:

dilated_convolution.svg

Если установить параметр dilation=(1,1), получится обычная свёртка.

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

Формальное определение свёртки

18_conv_def_142fec410a.svg

Вопрос на подумать. Пусть у нас есть тензор размером HxWxC_{in}, к которому одновременно применяется $C_{out}$ свёрток, размер окна каждой равен kxk. Посчитайте количество обучаемых параметров. Как изменится формула, если к свёртке добавить смещение (bias)? Во сколько раз изменится количество параметров, если увеличить размер окна в 2 раза? А если увеличить количество каналов $C_{in}$ и $C_{out}$ в два раза? А если увеличить размер входного тензора в 2 раза по высоте и ширине?

Вопрос на подумать. Оцените количество операций сложений-умножений для предыдущего упражнения. Как оно поменяется, если увеличить в два раза размер окна? Количество каналов? Размер входного тензора?

Вопрос на подумать. Пусть последовательно применяется $N$ свёрток $k times k$. Посчитайте размер receptive field для последнего оператора.

Свёртки не только для изображений

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

cnn word 1.svg

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

cnn_word.svg

Существуют свёртки и для тензоров более высокой размерности, например, для видео (где прибавляется ещё координата «время»).

Инвариантность не только к сдвигам?

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

В качестве потенциально интересного – но пока не проявившего себя на практике – направления исследований можно упомянуть капсульные нейросети. Кроме того, вам может быть интересно познакомиться с геометрическим глубинным обучением(в качестве короткого введения рекомендуем посмотреть вот этот keynote с ICLR 2021), которое ставит своей целью исследование общих принципов, связывающих устойчивость к различным преобразованием и современные нейросетевые архитектуры (авторы сравнивают свои идеи с эрлангенской программой Феликса Кляйна – отсюда название).

Свёрточный слой и обратное распространение ошибки

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

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

$$(x_1,ldots,x_d) ast (w_{-1},w_0,w_1) = $$

$$= (0,x_1,ldots,x_d,0) cdotbegin{pmatrix}
w_{-1} & & & & \
w_0 & w_{-1} & & & \
w_1 & w_0 & w_{-1} & & \
& w_1 & w_0 & ddots & \
& & w_1 & ddots & w_{-1} \
& & & ddots & w_0 \
& & & & w_1 \
end{pmatrix} = $$

$$= (x_1,ldots,x_d) cdotbegin{pmatrix}
w_0 & w_{-1} & & & & & \
w_1 & w_0 & w_{-1} & & & & \
& w_1 & w_0 & w_{-1} & && \
& & w_1 & w_0 & ddots & & \
& & & w_1 & ddots & w_{-1} & \
& & & & ddots & w_0 & w_{-1} \
& & & & & w_1 & w_0 \
end{pmatrix} = $$

Обозначим последнюю матрицу через $widehat{W}$, а ядро свёртки через $W$. Что происходит с градиентом при переходе через матричное умножение, мы уже отлично знаем. Градиент по весам равен

$$nabla_{X_0}mathcal{L} = nabla_{X_0ast W}mathcal{L}cdotwidehat{W}^T$$

Разберёмся, что из себя представляет умножение на $widehat{W}^T$ справа. Эта матрица имеет вид

$$begin{pmatrix}
w_0 & w_1 & & & & & \
w_{-1} & w_0 & w_1 & & & & \
& w_{-1} & w_0 & w_1 & && \
& & w_{-1} & w_0 & ddots & & \
& & & w_{-1} & ddots & w_1 & \
& & & & ddots & w_0 & w_1 \
& & & & & w_{-1} & w_0 \
end{pmatrix}$$

Она тоже соответствует свёртке, только:

  • с симметричным исходному ядром $(w_1, w_0, w_{-1})$;
  • с дополнением вектора $nabla_{X_0ast W}$ нулями (это как раз соответствует неполным столбцам: можно считать, что «выходящие» за границы матрицы и отсутствующие в ней элементы умножаются на нули).

Вопрос на подумать. Поменяется ли что-нибудь, если исходный вектор не дополнять нулями?

Общий случай

Рассмотрим теперь двумерную свёртку, для простоты нечётного размера и без свободного члена

$$(Xast W)_{ijc} = sum_{p=1}^{c_{text{in}}}sum_{k_1 = -k}^ksum_{k_2=-k}^kW^{c}_{k+1+k_1, k+1+k_2, p}X_{i + k_1, j + k_1, p}$$

  1. Продифференцируем по $X_{stl}$:

$$frac{partialmathcal{L}}{partial X_{stl}} = sum_{i, j, c}frac{partial (Xast W)_{ijc}}{partial X_{stl}}cdotfrac{partialmathcal{L}}{partial(Xast W)_{ijc}}$$

Разберёмся с производной $frac{partial (Xast W)_{ijc}}{partial X_{stl}}$. Во всей большой сумме из определения свёртки для $(Xast W)_{ijc}$ элемент $X_{stl}$ может встретиться в позициях $X_{i+k_1, j+k_2, l}$ при $i + k_1 = s$, $j + k_2 = t$ и всевозможных $c$, причём это возможно лишь если $k_1 = s — iin{-k,ldots,k}$, $k_2 = t — jin{-k,ldots,k}$ (для всех остальных $(Xast W)_{ijc}$ производная по $X_{stl}$ нулевая). Соответствующий коэффициент при $X_{stl}$ будет равен $W_{k + 1 + k_1, k + 1 + k_2, c}$. Таким образом, производная будет иметь вид:

$$frac{partialmathcal{L}}{partial X_{stl}} = sum_{c=1}^{c_{text{out}}}sum_{k_1=-k}^ksum_{k_2=-k}^kW_{k + 1 + k_1, k + 1 + k_2, c}cdotfrac{partialmathcal{L}}{partial(Xast W)_{s — k_1, t — k_2, c}}$$

Легко заметить, что это тоже свёртка, но поскольку индексы $k_1, k_2$ в $W$ и в $frac{partialmathcal{L}}{partial(Xast W)}$ стоят с разными знаками, получаем, что

$$color{blue}{nabla_{X}mathcal{L} = Wtext{[::-1,::-1,:]}astnabla_{Xast W}mathcal{L}}$$

  1. Продифференцируем по $W^q_{ab}$:

$$frac{partialmathcal{L}}{partial W^q_{ab}} = sum_{i, j, c}frac{partial (Xast W)_{ijc}}{partial W^q_{ab}}cdotfrac{partialmathcal{L}}{partial(Xast W)_{ijc}}$$

В формуле для $(Xast W)_{ijc}$ элемент $W^q_{ab}$ может встретиться в позициях $W^q_{k + 1 + k_1, k + 1 + k_2}$, для $k + 1 + k_1 = a$, $k + 1 + k_2 = b$, с коэффициентами $X_{i + k_1, j + k_2, p}$ (для любых $p$). Значит, производная будет иметь вид:

$$frac{partialmathcal{L}}{partial W^q_{ab}} = sum_{p=1}^{c_{text{in}}}sum_{i=1}^Hsum_{j=1}^WX_{a — k — 1, b — k — 1, p}cdotfrac{partialmathcal{L}}{partial(Xast W)_{a — k — 1, b — k — 1, q}}$$

В этой формуле тоже нетрудно узнать свёртку:

$$color{blue}{nabla_{W}mathcal{L} = Xastnabla_{Xast W}mathcal{L}}$$

Вопрос на подумать. Если всё-таки есть свободные члены, как будет выглядить градиент по $b_c$?

Остальные важные блоки свёрточных нейронных сетей

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

Max pool

Каждая из $C$ свёрток очередного свёрточного слоя – это новая карта признаков для нашего изображения, и нам, конечно, хотелось бы, чтобы таких карт было побольше: ведь это позволит нам выучивать больше новых закономерностей. Но для картинок в высоком разрешении это может быть затруднительно: слишком уж много будет параметров. Выходом оказалось использование следующей эвристики: сначала сделаем несколько свёрток с $C_1$ каналами, а затем как-нибудь уменьшим нашу карту признаков в 2 раза и одновременно увеличим количество свёрток во столько же. Посчитаем, как в таком случае изменится число параметров: было $H times W times K times K times C_1 times C_1$, стало $(H/2) times (W/2) times K times K times (C_1 times 2) times (C_1 times 2) = H times W times K times K times C_1 times C_1$, то есть, ничего не изменилось, а количество фильтров удвоилось, что приводит к выучиванию более сложных зависимостей.

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

Еще одно преимущество – увеличение receptive field. Получается, что он увеличивается в 2 раза:

21_maxpool_b19a0a26b2.svg

Операция понижения разрешения со взятием максимума в окне называется max pooling, а со взятием среднего – average pooling.

Вопрос на подумать. Как будет преобразовываться градиент во время error backpropagation для maxpool с окном и шагом 2×2? А для average pool?

Кстати, ещё одним способом уменьшать размер карт признаков по ходу применения свёрточной сети является использование strided convolution, в которых ядро свёртки сдвигается на каждом шаге на некоторое большее единицы число пикселей (возможно, разное для осей H и W; обычная свёртка получается, если установить параметр stride=(1,1)).

gif_conv_template1.svg

Global average pool

Как свёрточные слои, так и пулинг превращают картинку в «стопку» карт признаков. Но если мы решаем задачу классификации или регрессии, то в итоге нам надо получить число (или вектор логитов, если речь про многоклассовую классификацию). Один из способов добиться этого – воспользоваться тем, что свёртка без дополнения нулями и пулинг уменьшают размер карты признаков, и в итоге при должном терпении и верном расчёте мы можем получить тензор 1x1xC (финальные, общие признаки изображения), к которому уже можно применить один или несколько полносвязных слоёв. Или же можно, не дождавшись, пока пространственные измерения схлопнутся, «растянуть» всё в один вектор и после этого применить полносвязные слои (именно так, как мы не хотели делать, не правда ли?). Примерно так и происходило в старых архитектурах (alexnet, vgg).

Вопрос на подумать. Попробуйте соорудить конструкцию из свёточных слоёв и слоёв пулинга, превращающую изображение размера 128x128x3 в тензор размера 1x1xC.

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

Позднее было предложено следующее: после скольких-то свёрточных слоёв мы будем брать среднее вдоль пространственных осей нашего последнего тензора и усреднять их активации, а уже после этого строить MLP. Это и есть Global Average Pooling. У такого подхода есть несколько преимуществ:

  1. Радикально меньше параметров
  2. Теперь мы можем применять нейронку к картинку любого размера
  3. Мы сохраняем «магию» инвариантности предсказаний к сдвигам

23_globalpool_8b3ea2ca37.svg

Residual connection

Оказывается, что, если мы будем бесконтрольно стекать наши свёртки, то, несмотря на использование relu и batch normalization, градиенты все равно будут затухать, и на первых слоях будут почти нулевыми. Интересное решение предлагают авторы архитектуры resnet: давайте будем «прокидывать» признаки на предыдущем слое мимо свёрток на следующем:

24_residual_bb9168282a.svg

Таким образом получается, что градиент доплывет даже до самых первых слоев, что существенно ускоряет сходимость и качество полученной модели. Вопрос: почему именно сумма? Может, лучше конкатенировать? Авторы densenet именно такой подход и предлагают (с оговорками), получая результаты лучше, чем у resnet. Однако, такой подход получается вычислительно сложным и редко используется на практике.

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

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

Классические

Почти все регуляризаторы, которые использовались в классической машинке и полносвязных сетях, применимы и здесь: l1/l2, dropout и так далее.

Вопрос на подумать. Насколько разумно использовать dropout в свёрточных слоях? Как можно модифицировать метод, чтобы он стал «более подходящим»?

Аугментации

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

  1. сдвиги, повороты и отражения;
  2. добавление случайного гауссового шума;
  3. вырезание случайно части картинки (cutout);
  4. перспективные преобразования;
  5. случайное изменение оттенканасыщщеностияркости для всей картинки;
  6. и многое другое.

Пример хорошой библиотеки с аугментациями: Albumentations.

Label smoothing

Часто оказывается, что нейронная сеть делает «слишком уверенные предсказания»: 0.9999 или 0.00001. Это становится головной болью, если в нашей разметке есть шум – тогда градиенты на таких объектах могут сильно портить сходимость. Исследователи пришли к интересной идее: давайте предсказывать не one-hot метку, а ее сглаженный вариант. Итак, пусть у нас есть $K$ классов:

$$y_{ohot}=(0, 0, dots, 1, dots, 0)$$

$$y_{ls}=left(frac{varepsilon}{k-1},frac{varepsilon}{k-1}, dots, 1-varepsilon,frac{varepsilon}{k-1}, dots, frac{varepsilon}{k-1}right)$$

$$sum_i y^i_{ohot}=sum_i y^i_{ls}=1$$

Обычно берут $varepsilon=0.1$. Тем самым модель штрафуется за слишком уверенные предсказания, а шумные лейблы уже не вносят такого большого вклада в градиент.

Mixup

Самый интересный вариант. А что будет, если мы сделаем выпуклую комбинацию двух картинок и их лейблов:

мл 6_1.svg

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

Итого

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

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

Дисклеймер: это мнение одного автора. Приведённые в этом разделе вехи связаны преимущественно с архитектурами моделей, а не способом их оптимизации.

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

Зачем это полезно изучить (вместе с чтением статей)? Основных причин две:

  1. Общее развитие. Полезно понимать, откуда взялись и чем мотивированы те или иные компоненты.
  2. Этот вопрос задают на собеседовании, когда не знают, что еще спросить :)

lenet (1998)

Ссылка на статью

7 слоев

Первая свёрточная нейронная сеть, показавшая SOTA (State Of The Art) результаты на задаче классификации изображений цифр MNIST. В архитектуре впервые успешно использовались свёрточные слои с ядром 5x5. В качестве активации использовался tanh, а вместо max pool в тот момент использовался average.

alexnet (2012)

Ссылка на статью

11 слоев

Первая CNN (Convolutional Neural Network), взявшая победу на конкурсе imagenet. Автор предложил использовать ReLU вместо сигмоид (чтобы градиенты не затухали) и популяризовал max-pool вместо average. Что самое важное, обучение модели было перенесено на несколько GPU, что позволило обучать достаточно большую модель за относительное небольшое время (6 дней на двух видеокартах того времени). Также автор обратил внимание, что глубина нейросети важна, так как убирание хотя бы одного слоя стабильно ухудшало качество на несколько процентов.

network in network (2013)

Ссылка на статью

В статье не привели интересных SOTA результатов, но зато ввели два очень популярных впоследствии модуля. Первый – это GAP (Global Average Pooling), который стоит после последнего свёрточного слоя и усредняет все активации вдоль пространственных осей. Второй – стекинг 1x1 свёрток поверх 3x3, что эквивалентно тому, что вместо линейной свёртки используется полносвязный слой.

vgg (2014)

Ссылка на статью

19 слоев

Авторы предложили декомпозировать большие свёртки (5x5, 7x7 и выше) на последовательное выполнение свёрток 3x3 с нелинейностями между ними. Впоследствии, за нечастым исключением, свёртки 3x3 стали стандартом в индустрии (вместе со свёртками 1x1).

googleLeNet aka Inception (2014)

Ссылка на статью

22 слоя

Ввели inception слой, просуществовавший довольно продолжительное время. Сейчас сам слой уже не используется, но идея лежащая в его основе, эксплуатируется. Идея следующая: будем параллельно применять свёртки с разным пространственными размерами ядер, чтобы можно было одновременно обрабатывать как low, так и high level признаки. Еще полезной для сообщества оказалась идея с dimensionality reduction: перед тяжелой операцией поставим свёртку 1×1, чтобы уменьшить количество каналов и кратно ускорить вычисление.

batch normalization (2015)

Ссылка на статью

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

kaiming weight initialization (2015)

Ссылка на статью

В статье предложили использовать инициализацию весов, берущую во внимание особенность активации ReLU (в предыдущих работах предполагалось, что $Var[x] = mathbb{E}[x^2]$, что, очевидно, нарушается для $hat{x} = max(0, x)$). Применение этой и других «свистелок:: на VGG19 позволило существенно уменьшить ошибку на imagenet.

ResNet (2015)

Ссылка на статью

152 слоя

Архитектура, до сих пор (на момент написания – вторая половина 2021 года) являющаяся бейзлайном и отправной точкой во многих задачах. Основная идея – использование skip connections, что позволило градиенту протекать вплоть до первых слоев. Благодаря этому эффекту получилось успешно обучать очень глубокие нейронные сети, например, с 1202 слоями (впрочем, результаты на таких моделях менее впечатляющие, чем на 152 слойной). После этой статьи также стали повсеместно использоваться GAP и уменьшение размерности свёртками 1x1.

MobileNet (2017)

Ссылка на статью

Очень популярная модель для быстрого инференса (на мобильных устройствах или gpu). По качеству хоть и немного проигрывает «монстрам», но в индустрии, оказывается, зачастую этого достаточно (особенно если брать последние варианты модели). Основная деталь – это использование depthwise convolutions: параллельный стекинг свёрток 3x3x1x1 – то есть таких, в которых вычисление для каждого $с_{text{out}}$ канала просходит только на основе признаков одного $c_{text{in}}$ канала. Чтобы скомбинировать фичи между каналами, используется классическая 1x1 свёртка.

EfficientNet (2019)

Ссылка на статью

Одна из первых моделей, полученных при помощи NAS (Neural Architecture Search), которая взяла SOTA на imagenet. После этого, модели, где компоненты подбирались вручную, уже почти не показывали лучших результатов на классических задачах.

Бонус: не классификацией единой

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

  1. Сегментация. Если убрать в конце слои GlobalAveragePool или flatten, то можно делать предсказания для каждого пикселя в отдельности (подумайте, что делать, если в сети есть maxpool) – получаем сегментацию картинки. Проблема – долгая и дорогая разметка.
  2. Детекция. Часто намного дешевле получить разметку объектов обрамляющими прямоугольниками. Здесь уже можно для каждого пикселя предсказывать размеры прямоугольника, который обрамляет объект, к которому принадлежит пиксель. Проблемы – нужен этап агрегации прямоугольников + много неоднозначностей во время разметки + много эверистик на всех этапах + данных нужно больше.
  3. Понимание видео. Добавляем в тензор новый канал – временной, считаем четырехмерные свёртки – и получаем распознавание сцен на видео.
  4. Metric learning. Часто мы не можем собрать все интересующие нас классы, например, в задаче идентификации человека по лицу (или товара на полке). В этом случае используют такой трюк: научим модель в некотором смысле (обычно по косиносному расстоянию) разделять эмбеддинги существующих классов (уникальных людей). Если на руках была репрезентативная выборка, то модель, скорее всего (а обычно – всегда), выучит генерировать дискриминативные эмбеддинги, которые уже позволят различать между собой ранее невиданные лица.
  5. и многое другое

Превью к статье о свёрточной нейронной сети

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

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

Этап 0. Подготовительный

Прежде чем переходить к созданию свёрточного слоя, необходимо создать структуру для тензора. Тензор, как было сказано в прошлой статье, представляет из себя 3D массив. У тензора имеются три размерности, а значит нам потребуется структура для хранения размера тензора, мы назовём её TensorSize:

// размерность тензора
struct TensorSize {
    int depth; // глубина
    int height; // высота
    int width; // ширина
};

Создав размерность, можно создавать и сам тензор. В нём нам потребуется хранить depth·height·width вещественных чисел, которые мы будем хранить в одномерном векторе для уменьшения количества обращений к элементам по индексам. Для того, чтобы можно было работать с тензором, получать и изменять числа, необходима индексация. Индексация будет производиться по трём индексам в следующем порядке: глубина, высота, ширина. Поскольку в качестве хранения мы выбрали одномерный вектор, то потребуется применить несколько умножений и сложений.

// тензор
class Tensor {
    TensorSize size; // размерность тензора
    std::vector<double> values; // значения тензора

    int dw; // произведение глубины на ширину для индексации

    void Init(int width, int height, int depth);

public:
    Tensor(int width, int height, int depth); // создание из размеров
    Tensor(const TensorSize &size); // создание из размера

    double& operator()(int d, int i, int j); // индексация
    double operator()(int d, int i, int j) const; // индексация

    TensorSize GetSize() const; // получение размера

    friend std::ostream& operator<<(std::ostream& os, const Tensor &tensor); // вывод тензора
};

// инициализация по размерам
void Tensor::Init(int width, int height, int depth) {
    size.width = width; // запоминаем ширину
    size.height = height; // запоминаем высоту
    size.depth = depth; // запоминаем глубину

    dw = depth * width; // запоминаем произведение глубины на ширину для индексации

    values = std::vector<double>(width * height * depth, 0); // создаём вектор из width * height * depth нулей
}

// создание из размеров
Tensor::Tensor(int width, int height, int depth) {
    Init(width, height, depth);  
}

// создание из размера
Tensor::Tensor(const TensorSize &size) {
    Init(size.width, size.height, size.depth);
}

// индексация
double& Tensor::operator()(int d, int i, int j) {
    return values[i * dw + j * size.depth + d];
}

// индексация
double Tensor::operator()(int d, int i, int j) const {
    return values[i * dw + j * size.depth + d];
}

// получение размера
TensorSize Tensor::GetSize() const {
    return size;
}

// вывод тензора
std::ostream& operator<<(std::ostream& os, const Tensor &tensor) {
    for (int d = 0; d < tensor.size.depth; d++) {
        for (int i = 0; i < tensor.size.height; i++) {
            for (int j = 0; j < tensor.size.width; j++)
                os << tensor.values[i * tensor.dw + j * tensor.size.depth + d] << " ";
            
            os << std::endl;
        }

        os << std::endl;
    }

    return os;
}

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

Этап 1. Свёрточный слой

Из прошлой статьи мы узнали, что свёрточный слой содержит в себе набор фильтров и смещений. Также для создания слоя необохдимо задать несколько гиперпараметров, а именно: fs — размер фильтров, fc — количество фильтров, S — шаг свёртки и P — дополнение нулями. Поскольку фильтры являются тензорами, а смещения всего лишь числами, то добавим фильтры в виде вектора тензоров и назовём W, а смещения добавим в виде векторов вещественных чисел и назовём их b. Поскольку эти веса являются обучаемыми, нам потребуется аналогичные наборы векторов для хранения градиентов.

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

class ConvLayer {
    std::default_random_engine generator; // генератор случайных чисел
    std::normal_distribution<double> distribution; // с нормальным распределением

    TensorSize inputSize; // размер входа
    TensorSize outputSize; // размер выхода

    std::vector<Tensor> W; // фильтры
    std::vector<double> b; // смещения

    std::vector<Tensor> dW; // градиенты фильтров
    std::vector<double> db; // градиенты смещений
    
    int P; // дополнение нулями
    int S; // шаг свёртки

    int fc; // количество фильтров
    int fs; // размер фильтров
    int fd; // глубина фильтров

    void InitWeights(); // инициализация весовых коэффициентов

public:
    ConvLayer(TensorSize size, int fc, int fs, int P, int S); // создание слоя

    Tensor Forward(const Tensor &X); // прямое распространение сигнала
    Tensor Backward(const Tensor &dout, const Tensor &X); // обратное распространение ошибки
    void UpdateWeights(double learningRate); // обновление весовых коэффициентов
};

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

// создание свёрточного слоя
ConvLayer::ConvLayer(TensorSize size, int fc, int fs, int P, int S) : distribution(0.0, sqrt(2.0 / (fs*fs*size.depth))) {
    // запоминаем входной размер
    inputSize.width = size.width;
    inputSize.height = size.height;
    inputSize.depth = size.depth;

    // вычисляем выходной размер
    outputSize.width = (size.width - fs + 2 * P) / S + 1;
    outputSize.height = (size.height - fs + 2 * P) / S + 1;
    outputSize.depth = fc;

    this->P = P; // сохраняем дополнение нулями
    this->S = S; // сохраняем шаг свёртки

    this->fc = fc; // сохраняем число фильтров
    this->fs = fs; // сохраняем размер фильтров
    this->fd = size.depth; // сохраняем глубину фильтров

    // добавляем fc тензоров для весов фильтров и их градиентов
    W = std::vector<Tensor>(fc, Tensor(fs, fs, fd));
    dW = std::vector<Tensor>(fc, Tensor(fs, fs, fd));
        
    // добавляем fc нулей для весов смещения и их градиентов
    b = std::vector<double>(fc, 0);
    db = std::vector<double>(fc, 0);

    InitWeights(); // инициализируем весовые коэффициенты
}

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

// инициализация весовых коэффициентов
void ConvLayer::InitWeights() {
    // проходимся по каждому из фильтров
    for (int index = 0; index < fc; index++) {
        for (int i = 0; i < fs; i++)
            for (int j = 0; j < fs; j++)
                for (int k = 0; k < fd; k++)
                    W[index](k, i, j) = distribution(generator); // генерируем случайное число и записываем его в элемент фильтра

        b[index] = 0.01; // все смещения устанавливаем в 0.01
    }
}

Этап 2. Прямое распространение сигнала

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

for index = 0 to fc - 1 do
    output[index] = Convolve(X, W[index]) + b[index]

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

Дополнение нулями, padding

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

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

// прямое распространение
Tensor ConvLayer::Forward(const Tensor &X) {
    Tensor output(outputSize); // создаём выходной тензор

    // проходимся по каждому из фильтров
    for (int f = 0; f < fc; f++) {
        for (int y = 0; y < outputSize.height; y++) {
            for (int x = 0; x < outputSize.width; x++) {
                double sum = b[f]; // сразу прибавляем смещение

                // проходимся по фильтрам
                for (int i = 0; i < fs; i++) {
                    for (int j = 0; j < fs; j++) {
                        int i0 = S * y + i - P;
                        int j0 = S * x + j - P;

                        // поскольку вне границ входного тензора элементы нулевые, то просто игнорируем их
                        if (i0 < 0 || i0 >= inputSize.height || j0 < 0 || j0 >= inputSize.width)
                            continue;

                        // проходимся по всей глубине тензора и считаем сумму
                        for (int c = 0; c < fd; c++)
                            sum += X(c, i0, j0) * W[f](c, i, j);
                    }
                }

                output(f, y, x) = sum; // записываем результат свёртки в выходной тензор
            }
        }
    }

    return output; // возвращаем выходной тензор
}

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

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

) и градиенты весовых коэффициентов (

и

).

Это становится вполне логичным, если рассматривать нейросеть как обыкновенную функцию с огромным количеством параметров, которую мы хотим минимизировать. А чтобы найти её минимум, нам нужно найти производные по каждому из весовых коэффициентов, чтобы затем шагнуть в направлении антиградиента (направление, в котором функция уменьшается). Поскольку нейросеть состоит из большого числа блоков, сходу тяжело предрассчитать производные, а потому на помощь приходит расчёт производной сложной функции: пусть у нас есть некоторая функция y = f(x) и функция g(y), и нам требуется найти производную по x функции z = g(f(x)). Для этого сначала нужно найти производную функции g, а затем умножить её на производную функции f: z’ = g'(f(x)) * f'(x).

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

Вычисление градиента по весовым коэффициентам

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

for (int f = 0; f < fc; f++)
    for (int y = 0; y < size.height; y++)
        for (int x = 0; x < size.width; x++)
            db[f] += deltas(f, y, x);

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

for (int f = 0; f < fc; f++) {
    for (int y = 0; y < size.height; y++) {
        for (int x = 0; x < size.width; x++) {
            double delta = deltas(f, y, x); // запоминаем значение градиента

            for (int i = 0; i < fs; i++) {
                for (int j = 0; j < fs; j++) {
                    int i0 = i + y - P;
                    int j0 = j + x - P;

                    // игнорируем выходящие за границы элементы
                    if (i0 < 0 || i0 >= inputSize.height || j0 < 0 || j0 >= inputSize.width)
                        continue;

                    // наращиваем градиент фильтра
                    for (int c = 0; c < fd; c++)
                        dW[f](c, i, j) += delta * X(c, i0, j0);
                }
            }
        }
    }
}

Вычисление градиента по входному тензору

Градиент по входному тензору считается обыкновенной свёрткой тензора дельт и весов с небольшим отличием: для вычисления фильтры поворачиваются на 180 градусов, а дополнение нулями заменяется на величину fs — 1 — P. Данную операцию называют транспонированной свёрткой или кросскорелляцией.

Tensor dX(inputSize);
int pad = fs - 1 - P;

for (int y = 0; y < inputSize.height; y++) {
    for (int x = 0; x < inputSize.width; x++) {
        for (int c = 0; c < fd; c++) {
            double sum = 0; // сумма для градиента

            // идём по всем весовым коэффициентам фильтров
            for (int i = 0; i < fs; i++) {
                for (int j = 0; j < fs; j++) {
                    int i0 = y + i - pad;
                    int j0 = x + j - pad;

                    // игнорируем выходящие за границы элементы
                    if (i0 < 0 || i0 >= size.height || j0 < 0 || j0 >= size.width)
                        continue;

                    // суммируем по всем фильтрам
                    for (int f = 0; f < fc; f++)
                        sum += W[f](c, fs - 1 - i, fs - 1 - j) * deltas(f, i0, j0); // добавляем произведение повёрнутых фильтров на дельты
                }
            }

            dX(c, y, x) = sum; // записываем результат в тензор градиента
        }
    }
}

Внимательный читаель мог заметить, что в заголовке метода использовался параметр dout, а в приведённом выше коде используется параметр deltas, а также неизвестный размер size. Действительно, deltas и dout не совсем одно и то же. Если шаг свёртки равен 1, то они совпадают, при другом шаге size и deltas вычисляются следующим образом:

TensorSize size; // размер дельт

// расчитываем размер для дельт
size.height = S * (outputSize.height - 1) + 1;
size.width = S * (outputSize.width - 1) + 1;
size.depth = outputSize.depth;

Tensor deltas(size); // создаём тензор для дельт

// расчитываем значения дельт
for (int d = 0; d < size.depth; d++)
    for (int i = 0; i < outputSize.height; i++)
        for (int j = 0; j < outputSize.width; j++)
            deltas(d, i * S, j * S) = dout(d, i, j);

Итоговый код для расчёта градиентов будет таким:

// обратное распространение
Tensor ConvLayer::Backward(const Tensor &dout, const Tensor &X) {
    TensorSize size; // размер дельт

    // расчитываем размер для дельт
    size.height = S * (outputSize.height - 1) + 1;
    size.width = S * (outputSize.width - 1) + 1;
    size.depth = outputSize.depth;

    Tensor deltas(size); // создаём тензор для дельт

    // расчитываем значения дельт
    for (int d = 0; d < size.depth; d++)
        for (int i = 0; i < outputSize.height; i++)
            for (int j = 0; j < outputSize.width; j++)
                deltas(d, i * S, j * S) = dout(d, i, j);

    // расчитываем градиенты весов фильтров и смещений
    for (int f = 0; f < fc; f++) {
        for (int y = 0; y < size.height; y++) {
            for (int x = 0; x < size.width; x++) {
                double delta = deltas(f, y, x); // запоминаем значение градиента

                for (int i = 0; i < fs; i++) {
                    for (int j = 0; j < fs; j++) {
                        int i0 = i + y - P;
                        int j0 = j + x - P;
                        
                        // игнорируем выходящие за границы элементы
                        if (i0 < 0 || i0 >= inputSize.height || j0 < 0 || j0 >= inputSize.width)
                            continue;

                        // наращиваем градиент фильтра
                        for (int c = 0; c < fd; c++)
                            dW[f](c, i, j) += delta * X(c, i0, j0);
                    }
                }

                db[f] += delta; // наращиваем градиент смещения
            }
        }
    }

    int pad = fs - 1 - P; // заменяем величину дополнения
    Tensor dX(inputSize); // создаём тензор градиентов по входу

    // расчитываем значения градиента
    for (int y = 0; y < inputSize.height; y++) {
        for (int x = 0; x < inputSize.width; x++) {
            for (int c = 0; c < fd; c++) {
                double sum = 0; // сумма для градиента

                // идём по всем весовым коэффициентам фильтров
                for (int i = 0; i < fs; i++) {
                    for (int j = 0; j < fs; j++) {
                        int i0 = y + i - pad;
                        int j0 = x + j - pad;

                        // игнорируем выходящие за границы элементы
                        if (i0 < 0 || i0 >= size.height || j0 < 0 || j0 >= size.width)
                            continue;

                        // суммируем по всем фильтрам
                        for (int f = 0; f < fc; f++)
                            sum += W[f](c, fs - 1 - i, fs - 1 - j) * deltas(f, i0, j0); // добавляем произведение повёрнутых фильтров на дельты
                    }
                }

                dX(c, y, x) = sum; // записываем результат в тензор градиента
            }
        }
    }

    return dX; // возвращаем тензор градиентов
}

Этап 4. Обновление весовых коэффициентов

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

// обновление весовых коэффициентов
void ConvLayer::UpdateWeights(double learningRate) {
    for (int index = 0; index < fc; index++) {
        for (int i = 0; i < fs; i++) {
            for (int j = 0; j < fs; j++) {
                for (int d = 0; d < fd; d++) {
                    W[index](d, i, j) -= learningRate * dW[index](d, i, j); // вычитаем градиент, умноженный на скорость обучения
                    dW[index](d, i, j) = 0; // обнуляем градиент фильтра
                }
            }
        }

        b[index] -= learningRate * db[index]; // вычитаем градиент, умноженный на скорость обучения
        db[index] = 0; // обнуляем градиент веса смещения
    }
}

Этап 5. Проверка

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

// установка веса фильтра по индексу
void ConvLayer::SetWeight(int index, int i, int j, int k, double weight) {
    W[index](i, j, k) = weight;
}

// установка веса смещения по индексу
void ConvLayer::SetBias(int index, double bias) {
    b[index] = bias;
}

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

Для начала создадим свёрточный слой, который будет принимать на вход тензор размером 5x5x3 (три канала 5×5), содержать 2 фильтра размером 3×3, шаг смещения 2 и единичное дополнение нулями. На выходе мы ожидаем тензор размером (5-3+2*1)/2+1 x (5-3+2*1)/2+1 x 2 = 3x3x2. Возьмём следующие фильтры и входной тензор для проверки:

      |-1 1 1 |  |  0 -1  1 |  |  1 -1  0 |
W[0]: |-1 1 1 |  | -1  0 -1 |  | -1  0 -1 |
      | 0 0 1 |  |  1  0  0 |  |  0  1 -1 |

      | 0  0 -1 |  |  1 -1  0 |  |  1  0  1 |
W[1]: | 1  0  0 |  | -1  1  0 |  |  1 -1  1 |
      | 0 -1  0 |  | -1  1  1 |  | -1  0  0 |

b[0]: 1
b[1]: 0

   | 1 2 0 1 0 |  | 1 2 2 1 2 |  | 0 2 0 1 1 |
   | 2 0 0 0 1 |  | 0 2 2 0 2 |  | 2 0 2 1 1 |
X: | 1 2 2 0 2 |  | 1 2 2 1 1 |  | 2 0 1 2 1 |
   | 2 2 2 0 1 |  | 2 2 0 1 0 |  | 0 1 0 1 2 |
   | 2 0 1 0 1 |  | 2 2 1 0 0 |  | 1 0 1 2 1 |        

Если всё сделано правильно, то на выходе должен получиться следующий тензор:

    | 2 -3 1 |  | 3 5  2 |
Y:  | 5 -7 2 |  | 1 0 -3 |
    | 5  0 0 |  |-2 4  3 |

Пусть теперь слой принимает тензоры размером 4x4x1 и содержит 1 фильтр 3х3 с единичным шагом без дополнения нулями.

      | 1 4 1 |
W[0]: | 1 4 3 |
      | 3 3 1 |

b[0]: 0

   | 4 5 8 7 |
X: | 1 8 8 8 |
   | 3 6 6 4 |
   | 6 5 7 8 |

Результат прямого распространения должен быть таким:

Y: | 122 148 |
   | 126 134 |

Запустим же теперь обратное распространение с такими дельтами:

dout: | 2 1 |
      | 4 4 |

В результате должны получить такой тензор:

    |  2  9  6  1 |
dX: |  6 29 30  7 |
    | 10 29 33 13 |
    | 12 24 16  4 |

Интерактивный пример

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

Размер:
Глубина:
Число фильтров:
Размер фильтров:
Шаг свёртки:
Дополнение нулями:

Итоги

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

Следующая часть: Свёрточная нейронная сеть с нуля. Часть 2. Слой подвыборки

    • обратное распространение

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

      Обновление правила цепочки

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

      f(x)=A(B(C(x)))

      A, B, и C — функции активации на различных слоях. Пользуясь правилом цепочки, мы легко вычисляем производную f(x) по x:

      f′(x)=f′(A)⋅A′(B)⋅B′(C)⋅C′(x)

      Что насчёт производной относительно B? Чтобы найти производную по B, вы можете сделать вид, что B (C(x)) является константой, заменить ее переменной-заполнителем B, и продолжить поиск производной по B стандартно.

      f′(B)=f′(A)⋅A′(B)

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

      Применение правила цепочки

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

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

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

      Cost=C(R(Z(XW)))

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

      C′(W)=C′(R)⋅R′(Z)⋅Z′(W)=(y^−y)⋅R′(Z)⋅X

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

      обратное распространение ошибки нейронная сеть

      Какова производная от потери по Wo?

      C′(WO)=C′(y^)⋅y^′(ZO)⋅Z′O(WO)=(y^−y)⋅R′(ZO)⋅H

      А что насчет Wh? Чтобы узнать это, мы просто продолжаем возвращаться в нашу функцию, рекурсивно применяя правило цепочки, пока не доберемся до функции, которая имеет элемент Wh.

      C′(Wh)=C′(y^)⋅O′(Zo)⋅Z′o(H)⋅H′(Zh)⋅Z′h(Wh)=(y^−y)⋅R′(Zo)⋅Wo⋅R′(Zh)⋅X

      И просто забавы ради, что, если в нашей сети было бы 10 скрытых слоев. Что такое производная потери для первого веса w1?

      C(w1)=(dC/dy^)⋅(dy^/dZ11)⋅(dZ11/dH10)⋅(dH10/dZ10)⋅(dZ10/dH9)⋅(dH9/dZ9)⋅(dZ9/dH8)⋅(dH8/dZ8)⋅(dZ8/dH7)⋅(dH7/dZ7)⋅(dZ7/dH6)⋅(dH6/dZ6)⋅(dZ6/dH5)⋅(dH5/dZ5)⋅(dZ5/dH4)⋅(dH4/dZ4)⋅(dZ4/dH3)⋅(dH3/dZ3)⋅(dZ3/dH2)⋅(dH2/dZ2)⋅(dZ2/dH1)⋅(dH1/dZ1)⋅(dZ1/dW1)

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

      Сохранение работы с мемоизацией

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

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

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

      уравнение

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

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

      Ошибка выходного слоя

      Для расчета ошибки выходного слоя необходимо найти производную потерь по входу выходному слою, Zo. Это отвечает на вопрос: как веса последнего слоя влияют на общую ошибку в сети?  Тогда производная такова:

      C′(Zo)=(y^−y)⋅R′(Zo)

      Чтобы упростить запись, практикующие МО обычно заменяют последовательность (y^−y)∗R'(Zo) термином Eo. Итак, наша формула для ошибки выходного слоя равна:

      Eo=(y^−y)⋅R′(Zo)

      Ошибка скрытого слоя

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

      C′(Zh)=(y^−y)⋅R′(Zo)⋅Wo⋅R′(Zh)

      Далее мы можем поменять местами элемент Eo выше, чтобы избежать дублирования и создать новое упрощенное уравнение для ошибки скрытого слоя:

      Eh=Eo⋅Wo⋅R′(Zh)

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

      Производная потерь по любому весу

      Вернемся к нашей формуле для производной потерь по весу выходного слоя Wo.

      C′(WO)=(y^−y)⋅R′(ZO)⋅H

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

      C′(Wo)=Eo⋅H

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

      C′(w)=CurrentLayerError⋅CurrentLayerInput

      Примечание: вход относится к активации с предыдущего слоя, а не к взвешенному входу, Z.

      Подводя итог

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

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

      Вот процесс, визуализированный с использованием нашего примера нейронной сети выше:

      _images/backprop_visually.png

      Обратное распространение: пример кода

      def relu_prime(z):
      if z > 0:
      return 1
      return 0
      
      def cost(yHat, y):
      return 0.5 * (yHat - y)**2
      
      def cost_prime(yHat, y):
      return yHat - y
      
      def backprop(x, y, Wh, Wo, lr):
      yHat = feed_forward(x, Wh, Wo)
      
      # Layer Error
      Eo = (yHat - y) * relu_prime(Zo)
      Eh = Eo * Wo * relu_prime(Zh)
      
      # Cost derivative for weights
      dWo = Eo * H
      dWh = Eh * x
      
      # Update weights
      Wh -= lr * dWh
      Wo -= lr * dWo

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


        Перевод


        Ссылка на автора

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

      Введение

      мотивация

      Цель этого поста — подробно описать, как работает обратное распространение градиента в сверточном слое нейронной сети. Обычно выход этого слоя будет входом выбранной функции активации (reluнапример). Мы делаем предположение, что нам дан градиентdyобратно от этой функции активации. Поскольку я не смог найти в Интернете полное, подробное и «простое» объяснение того, как это работает. Я решил сделать математику, пытаясь понять шаг за шагом, как она работает на простых примерах, прежде чем обобщать. Перед дальнейшим чтением вы должны быть знакомы с нейронными сетями, и особенно с прямым проходом, обратным распространением градиента в вычислительном графе и базовой линейной алгеброй с тензорами.

      Сверточный слой — прямой проход и BP

      нотации

      *будет относиться к свертке 2 тензоров в случае нейронной сети (входxи фильтрw).

      • когдаxа такжеwэто матрицы:
      • еслиxа такжеwиметь одинаковую форму,x*wбудет скаляр, равный сумме по результатам поэлементного умножения между массивами.
      • еслиwменьшеx, мы получим карту активацииyгде каждое значение является предопределенной операцией свертки подобласти x с размерами w. Этот субрегион, активируемый фильтром, скользит по всему входному массивуx,
      • еслиxа такжеwимеют более двух измерений, мы рассматриваем последние 3 для свертки и последние 2 для выделенной области скольжения (мы просто добавляем одну глубину к нашей матрице)

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

      Обозначения сверточного слоя

      Цель

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

      Вход состоит из N точек данных, каждая из которых имеет каналы C, высоту H и ширину W. Мы сворачиваем каждый вход с F различными фильтрами, где каждый фильтр охватывает все каналы C и имеет высоту HH и ширину WW.

      Входные данные:

      • x: входные данные формы (N, C, H, W)
      • w: фильтр весов формы (F, C, HH, WW)
      • b: уклоны формы (F,)
      • conv_param: словарь со следующими ключами:
      • ‘Шага’: количество пикселей между смежными рецептивными полями в горизонтальном и вертикальном направлениях.
      • ‘Pad’: количество пикселей, которые будут использоваться для ввода нуля ввода.

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

      Возвращает кортеж из:

      • out: Выходные данные формы (N, F, H ’, W’), где H ’и W’, определяются как

      H ’= 1 + (H + 2 * pad — HH) / шаг

      W ’= 1 + (W + 2 * pad — WW) / шаг

      • кеш: (x, w, b, conv_param)

      Прямой проход

      Общий случай (упрощенно с N = 1, C = 1, F = 1)

      N = 1 один вход, C = 1 один канал, F = 1 один фильтр.

      Свертка 2D

      х: В × Ш
      х = х с дополнением
      w: HH × WW
      смещение b: скаляр
      y: H ′ × W ′
      шаг с

      Особый случай: шаг = 1, пад = 0 и без смещения.

      обратное распространение

      Мы знаем:

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

      Тривиальный случай: вход x является вектором (1 измерение)

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

      вход

      Выход

      Прямой проход — свертка с одним фильтром w, шагом = 1, заполнением = 0

      обратное распространение

      Мы знаем градиент нашей функции стоимости L относительно y:

      Это можно записать с помощью якобианской нотации:

      DY и Y имеют одинаковую форму:

      Мы ищем

      децибел

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

      с.в.

      Мы можем заметить, что dw является сверткой входа x с фильтром dy. Давайте посмотрим, действительно ли это все еще действует с дополнительным измерением.

      дх

      Еще раз, у нас есть свертка. На этот раз немного сложнее. Мы должны рассмотреть вход dy с 0-отступом размера 1, свернутый с «инвертированным» фильтром w, как (вес2,вес1)

      Следующим шагом будет посмотреть, как это работает на маленьких матрицах.

      Ввод x представляет собой матрицу (2 измерения)

      вход

      Выход

      Еще раз, мы выберем самый простой случай: шаг = 1 и без заполнения Форма у будет (3,3)

      Перевал

      Мы будем иметь:

      Написано с подписками:

      обратное распространение

      Мы знаем:

      децибел

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

      Суммирование по i и j. И у нас есть:

      с.в.

      Мы ищем

      Используя формулу (4) имеем:

      Все условия

      Кроме (К,Lзнак равном,N), где это 1, случай, встречающийся только один раз в двойной сумме. Следовательно:

      Используя формулу (3), мы теперь имеем:

      Если мы сравним это уравнение с формулой (1), дающей результат свертки, мы можем различить аналогичную схему, где dy — фильтр, применяемый к входу x.

      дх

      Используя правило цепочки, как мы это делали для (5), мы имеем:

      На этот раз мы ищем

      Используя уравнение (4):

      Теперь у нас есть:

      В нашем примере наборы диапазонов для индексов:

      Когда мы установимКзнак равномя+1 мы выйдем за установленные границы :( мя+ 1) ∈ [-1,4]

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

      Еще раз в двойной сумме, у нас только одна частная производная от х равна 1. Итак:

      гдевесэто наш 0-расширенный начальный фильтр, таким образом:

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

      Используя ∗ обозначение для свертки, имеем:

      КакдуОстанемся прежними, мы будем только смотреть на значения индексов w. Длядх22, диапазон для w: 3-я, 3-J

      Теперь у нас есть свертка между матрицей dy и w, определяемая как:

      Еще один случай, чтобы увидеть, что происходит.дх43, ш: 4-я, 3-J

      Последнийдх44

      Мы видим всплывающий «инвертированный фильтр» w ’. На этот раз у нас есть свертка между входомдус 0-отступной границей размера 1 и фильтром w, скользящим с шагом 1.

      Краткое изложение обратных уравнений

      Принимая во внимание глубины

      Все становится немного сложнее, когда мы пытаемся принять во внимание глубину (C каналов для входа x, и F различных фильтров для w)

      Входы:

      • х: форма (C, H, W)
      • w: форма весов фильтра (F, C, HH, WW)
      • б: форма (F,)

      Выходы:

      • y: форма (F, H ’, W’)

      Математические формулы видят появление многих индексов, что затрудняет их чтение. Формула прямого прохода в нашем примере будет:

      децибел

      Расчет дб остается простым, так как каждыйb_fсвязано с картой активацииy_f:

      с.в.

      Используя формулу прямого прохода, так как двойная сумма не использует индексы dy, мы можем написать:

      Алгоритм

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

      Градиентная числовая проверка

      Testing conv_backward_naive function
      dx error: 7.489787768926947e-09
      dw error: 1.381022780971562e-10
      db error: 1.1299800330640326e-10

      Почти 0 каждый раз, кажется, все в порядке! :)

      Ссылки

      • Этот пост в моем блоге с уравнениями mathjax:)
      • Стэнфордский курс по сверточным нейронным сетям для визуального распознавания
      • Стэнфордское задание CNN 2
      • Сверточная нейронная сеть, прямой проход
      • Слой свертки: Наивная реализация прямого прохода,
      • Обратное распространение в сверточных нейронных сетях
      • Cet Article En Français

      Комментарии могут улучшить этот пост, не стесняйтесь связаться со мной!

      Требования к символам

      z

      l

      z^l

      : Нет.

      l

      l

      После свертки слоя результат до активации

      a

      l

      a^l

      : Нет.

      l

      l

      Результат активации слоя при нормальных обстоятельствах также является первым

      l

      +

      1

      l+1

      Слой ввода
      Формула прямого распространения сверточного слоя:

      a

      l

      =

      σ

      (

      z

      l

      )

      =

      σ

      (

      a

      l

      1

      W

      l

      +

      b

      l

      )

      a^l= sigma(z^l) = sigma(a^{l-1}*W^l +b^l)


      δ

      l

      delta^l

      : При обратном распространении перейти к

      l

      l

      Ошибка слоя

      J

      (

      W

      ,

      b

      )

      {partial J(W,b)}

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

      Известный сверточный слой

      δ

      l

      delta^l

      , Получить предыдущий скрытый слой

      δ

      l

      1

      delta^{l-1}

      Первый

      l

      l

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

      l

      l

      Выход слоя — частная производная,

      l

      l

      Ошибка слоя и

      l

      1

      {l-1}

      Отношение повторяемости ошибки слоя:

      δ

      l

      =

      J

      (

      W

      ,

      b

      )

      z

      l

      =

      J

      (

      W

      ,

      b

      )

      z

      l

      +

      1

      z

      l

      +

      1

      z

      l

      =

      δ

      l

      +

      1

      z

      l

      +

      1

      z

      l

      delta^{l} = frac{partial J(W,b)}{partial z^l} = frac{partial J(W,b)}{partial z^{l+1}}frac{partial z^{l+1}}{partial z^{l}} = delta^{l+1}frac{partial z^{l+1}}{partial z^{l}}

      Итак, чтобы экспортировать

      δ

      l

      1

      delta^{l-1}

      с участием

      δ

      l

      delta^l

      Отношения должны быть рассчитаны

      z

      l

      z

      l

      1

      frac{partial z^{l}}{partial z^{l-1}}

      Выражение градиента.
      уведомление

      z

      l

      z^{l}

      с участием

      z

      l

      1

      z^{l-1}

      Отношения следующие:

      z

      l

      =

      a

      l

      1

      W

      l

      +

      b

      l

      =

      σ

      (

      z

      l

      1

      )

      W

      l

      +

      b

      l

      z^l = a^{l-1}*W^l +b^l =sigma(z^{l-1})*W^l +b^l


      Если прямое вычисление параметров слишком сложно, рассмотрим простой пример:
      Предполагая, что

      l

      1

      {l-1}

      Вывод слоя

      a

      l

      1

      a^{l-1}

      Матрица 3×3, первая

      l

      {l}

      Ядро свертки

      W

      l

      W^l

      Это матрица 2X2 с шагом 1 пиксель, на выходе получается матрица 2X2, мы упрощаем смещение

      b

      l

      b^l

      0, тогда:

      a

      l

      1

      W

      l

      =

      z

      l

      a^{l-1}*W^l = z^{l}


      Список матричных выражений a, W и z:

      (

      a

      11

      a

      12

      a

      13

      a

      21

      a

      22

      a

      23

      a

      31

      a

      32

      a

      33

      )

      (

      w

      11

      w

      12

      w

      21

      w

      22

      )

      =

      (

      z

      11

      z

      12

      z

      21

      z

      22

      )

      left( begin{array}{ccc} a_{11}&amp;a_{12}&amp;a_{13} \ a_{21}&amp;a_{22}&amp;a_{23}\ a_{31}&amp;a_{32}&amp;a_{33} end{array} right) * left( begin{array}{ccc} w_{11}&amp;w_{12}\ w_{21}&amp;w_{22} end{array} right) = left( begin{array}{ccc} z_{11}&amp;z_{12}\ z_{21}&amp;z_{22} end{array} right)


      Используя определение свертки, легко экспортировать:

      z

      11

      =

      a

      11

      w

      11

      +

      a

      12

      w

      12

      +

      a

      21

      w

      21

      +

      a

      22

      w

      22

      z_{11} = a_{11}w_{11} + a_{12}w_{12} + a_{21}w_{21} + a_{22}w_{22}


      z

      12

      =

      a

      12

      w

      11

      +

      a

      13

      w

      12

      +

      a

      22

      w

      21

      +

      a

      23

      w

      22

      z_{12} = a_{12}w_{11} + a_{13}w_{12} + a_{22}w_{21} + a_{23}w_{22}


      z

      21

      =

      a

      21

      w

      11

      +

      a

      22

      w

      12

      +

      a

      31

      w

      21

      +

      a

      32

      w

      22

      z_{21} = a_{21}w_{11} + a_{22}w_{12} + a_{31}w_{21} + a_{32}w_{22}


      z

      22

      =

      a

      22

      w

      11

      +

      a

      23

      w

      12

      +

      a

      32

      w

      21

      +

      a

      33

      w

      22

      z_{22} = a_{22}w_{11} + a_{23}w_{12} + a_{32}w_{21} + a_{33}w_{22}


      Затем смоделируйте обратную производную:

      a

      l

      1

      =

      J

      (

      W

      ,

      b

      )

      a

      l

      1

      =

      J

      (

      W

      ,

      b

      )

      z

      l

      z

      l

      a

      l

      1

      =

      δ

      l

      z

      l

      a

      l

      1

      nabla a^{l-1} = frac{partial J(W,b)}{partial a^{l-1}} = frac{partial J(W,b)}{partial z^{l}} frac{partial z^{l}}{partial a^{l-1}} = delta^{l} frac{partial z^{l}}{partial a^{l-1}}


      В приведенной выше формуле

      δ

      l

      delta^{l}

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

      z

      l

      a

      l

      1

      frac{partial z^{l}}{partial a^{l-1}}

      a

      l

      1

      nabla a^{l-1}

      Размеры и

      a

      l

      1

      a^{l-1}

      Сохраняйте постоянство, оно должно быть 3×3, что может быть выражено как: Например, для

      a

      11

      a_{11}

      Из-за 4 уравнений

      a

      11

      a_{11}

      Только и

      z

      11

      z_{11}

      Есть товарные отношения, поэтому у нас есть:

      a

      11

      =

      δ

      11

      w

      11

      nabla a_{11} = delta_{11}w_{11}


      a

      12

      =

      δ

      11

      w

      12

      +

      δ

      12

      w

      11

      nabla a_{12} = delta_{11}w_{12} + delta_{12}w_{11}


      a

      22

      =

      δ

      11

      w

      22

      +

      δ

      12

      w

      21

      +

      δ

      21

      w

      12

      +

      δ

      22

      w

      11

      nabla a_{22} = delta_{11}w_{22} + delta_{12}w_{21} + delta_{21}w_{12} + delta_{22}w_{11}


      Если он представлен матрицей, его можно записать как:

      (

      0

      0

      0

      0

      0

      δ

      11

      δ

      12

      0

      0

      δ

      21

      δ

      22

      0

      0

      0

      0

      0

      )

      (

      w

      22

      w

      21

      w

      12

      w

      11

      )

      =

      (

      a

      11

      a

      12

      a

      13

      a

      21

      a

      22

      a

      23

      a

      31

      a

      32

      a

      33

      )

      left( begin{array}{ccc} 0&amp;0&amp;0&amp;0 \ 0&amp;delta_{11}&amp; delta_{12}&amp;0 \ 0&amp;delta_{21}&amp;delta_{22}&amp;0 \ 0&amp;0&amp;0&amp;0 end{array} right) * left( begin{array}{ccc} w_{22}&amp;w_{21}\ w_{12}&amp;w_{11} end{array} right) = left( begin{array}{ccc} nabla a_{11}&amp;nabla a_{12}&amp;nabla a_{13} \ nabla a_{21}&amp;nabla a_{22}&amp;nabla a_{23}\ nabla a_{31}&amp;nabla a_{32}&amp;nabla a_{33} end{array} right)


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

      Известный сверточный слой

      δ

      l

      delta^l

      , Вывести градиент W и b этого слоя

      Обратите внимание, что соотношение между сверточным слоем z и W, b:

      z

      l

      =

      a

      l

      1

      W

      l

      +

      b

      z^l = a^{l-1}*W^l +b


      Итак, у нас есть:

      J

      (

      W

      ,

      b

      )

      W

      l

      =

      J

      (

      W

      ,

      b

      )

      z

      l

      z

      l

      W

      l

      =

      a

      l

      1

      δ

      l

      frac{partial J(W,b)}{partial W^{l}} = frac{partial J(W,b)}{partial z^{l}}frac{partial z^{l}}{partial W^{l}} =a^{l-1} *delta^l


      Обратите внимание, что ядро ​​свертки в это время не инвертируется, главным образом потому, что это производное внутри слоя в это время, а не производное, которое распространяется обратно на предыдущий уровень. Мы можем проанализировать конкретный процесс.
      — тот же упрощенный пример, что и в разделе 4. Здесь входными данными является матрица, а не тензор. Тогда для l-го слоя производная некоторой матрицы ядра свертки W может быть выражена следующим образом:

      J

      (

      W

      ,

      b

      )

      W

      p

      q

      l

      =

      i

      j

      (

      δ

      i

      j

      l

      x

      i

      +

      p

      1

      ,

      j

      +

      q

      1

      l

      1

      )

      frac{partial J(W,b)}{partial W_{pq}^{l}} = sumlimits_isumlimits_j(delta_{ij}^lx_{i+p-1,j+q-1}^{l-1})


      Если предположить, что наш вход a — это матрица 4×4, ядро ​​свертки W — это матрица 3×3, а выход z — матрица 2×2, тогда градиентная ошибка обратного распространения z

      δ

      delta

      Это тоже матрица 2×2.
      Тогда согласно приведенной выше формуле мы имеем:

      J

      (

      W

      ,

      b

      )

      W

      11

      l

      =

      a

      11

      δ

      11

      +

      a

      12

      δ

      12

      +

      a

      21

      δ

      21

      +

      a

      22

      δ

      22

      frac{partial J(W,b)}{partial W_{11}^{l}} = a_{11}delta_{11} + a_{12}delta_{12} + a_{21}delta_{21} + a_{22}delta_{22}


      J

      (

      W

      ,

      b

      )

      W

      12

      l

      =

      a

      12

      δ

      11

      +

      a

      13

      δ

      12

      +

      a

      22

      δ

      21

      +

      a

      23

      δ

      22

      frac{partial J(W,b)}{partial W_{12}^{l}} = a_{12}delta_{11} + a_{13}delta_{12} + a_{22}delta_{21} + a_{23}delta_{22}


      J

      (

      W

      ,

      b

      )

      W

      13

      l

      =

      a

      13

      δ

      11

      +

      a

      14

      δ

      12

      +

      a

      23

      δ

      21

      +

      a

      24

      δ

      22

      frac{partial J(W,b)}{partial W_{13}^{l}} = a_{13}delta_{11} + a_{14}delta_{12} + a_{23}delta_{21} + a_{24}delta_{22}


      J

      (

      W

      ,

      b

      )

      W

      21

      l

      =

      a

      21

      δ

      11

      +

      a

      22

      δ

      12

      +

      a

      31

      δ

      21

      +

      a

      32

      δ

      22

      frac{partial J(W,b)}{partial W_{21}^{l}} = a_{21}delta_{11} + a_{22}delta_{12} + a_{31}delta_{21} + a_{32}delta_{22}


      В итоге мы можем получить 9 формул. После сортировки в матричную форму можно получить:

      J

      (

      W

      ,

      b

      )

      W

      l

      =

      (

      a

      11

      a

      12

      a

      13

      a

      14

      a

      21

      a

      22

      a

      23

      a

      24

      a

      31

      a

      32

      a

      33

      a

      34

      a

      41

      a

      42

      a

      43

      a

      44

      )

      (

      δ

      11

      δ

      12

      δ

      21

      δ

      22

      )

      frac{partial J(W,b)}{partial W^{l}} =left( begin{array}{ccc} a_{11}&amp;a_{12}&amp;a_{13}&amp;a_{14} \ a_{21}&amp;a_{22}&amp;a_{23}&amp;a_{24} \ a_{31}&amp;a_{32}&amp;a_{33}&amp;a_{34} \ a_{41}&amp;a_{42}&amp;a_{43}&amp;a_{44} end{array} right) * left( begin{array}{ccc} delta_{11}&amp; delta_{12} \ delta_{21}&amp;delta_{22} end{array} right)


      Итак, мы ясно видим, почему в этот раз мы не сделали обратный ход.
      Для b он немного особенный, потому что δl — трехмерный тензор, а b — просто вектор, который не может быть напрямую равен δl, как DNN. Обычный подход состоит в суммировании элементов каждой подматрицы δl отдельно для получения вектора ошибок, который является градиентом b

      J

      (

      W

      ,

      b

      )

      b

      l

      =

      u

      ,

      v

      (

      δ

      l

      )

      u

      ,

      v

      frac{partial J(W,b)}{partial b^{l}} = sumlimits_{u,v}(delta^l)_{u,v}


      Конкретная реализация в python:

      	N, C, H, W = x.shape				#x вводится
      	F, C, HH, WW = w.shape			#w - ядро ​​свертки
      	x_padded = np.pad(x, ((0, 0), (0, 0), (pad, pad), (pad, pad)), 'constant')
          dx_padded = np.pad(dx, ((0, 0), (0, 0), (pad, pad), (pad, pad)), 'constant')
          for i in xrange(N):       # ith image
              for f in xrange(F):   # fth filter
                  for j in xrange(H_new):
                      for k in xrange(W_new):
                          window = x_padded[i, :, j*s:HH+j*s, k*s:WW+k*s]
                          db[f] += dout[i, f, j, k]
                          dw[f] += window * dout[i, f, j, k]
                          dx_padded[i, :, j*s:HH+j*s, k*s:WW+k*s] += w[f] * dout[i, f, j, k]
      

      Ссылка

      Алгоритм обратного распространения сверточной нейронной сети (CNN).

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

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

      Общее описание алгоритма обратного распространения ошибки

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

      4-20219-e537a8.png

      2-20219-7f9b72.png

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

      3-20219-2ac7f4.png

      Причём «альфа» здесь означает параметр наклона сигмоидальной функции. Меняя его, мы получаем возможность строить функции с разной крутизной.

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

      Цель обучения сети

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

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

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

      Пошаговая реализация метода обратного распространения ошибки

      Необходимо выполнить следующие действия:
      1. Инициализировать синаптические веса случайными маленькими значениями.
      2. Выбрать из обучающего множества очередную обучающую пару; подать на вход сети входной вектор.
      3. Выполнить вычисление выходных значений нейронной сети.
      4. Посчитать разность между выходом нейросети и требуемым выходом (речь идёт о целевом векторе обучающей пары).
      5. Скорректировать веса сети в целях минимизации ошибки.
      6. Повторять для каждого вектора обучающего множества шаги 2-5, пока ошибка обучения нейронной сети на всём множестве не достигнет уровня, который является приемлемым.

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

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

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

      Преимущества и недостатки метода

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

      Значение метода обратного распространения

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

      Источники:
      — «Алгоритм обратного распространения ошибки»;
      — «Back propagation algorithm».

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

      • Градиентный спуск
      • Функция ошибки
      • Метод обратного распространения ошибки
      • Пример расчета

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

      На своем сайте я всегда придерживаюсь концепции, при которой теоретические выкладки по максимуму сопровождаются практическими примерами для максимальной наглядности. Так мы поступим и сейчас 👍

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

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

      То есть на входы нейронов I1 и I2 мы подаем какие-либо числа, а на выходе сети получаем соответственно новое значение. При этом нам необходима некая выборка данных, включающая в себя значения входов и соответствующее им, правильное, значение на выходе:

      bold{I_1} bold{I_2} bold{O_{net}}
      x_{11} x_{12} y_{1}
      x_{21} x_{22} y_{2}
      x_{31} x_{32} y_{3}
      x_{N1} x_{N2} y_{N}

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

      bold{I_1} bold{I_2} bold{O_{net}}
      1 4 5
      2 7 9
      3 5 8
      1000 1500 2500

      Эти значения и используются для обучения сети. Как именно — рассмотрим чуть ниже, пока сконцентрируемся на идее процесса в целом. Для того, чтобы иметь возможность тестировать работу сети в процессе обучения, исходную выборку данных делят на две части — обучающую и тестовую. Пусть имеется 1000 образцов, тогда можно 900 использовать для обучения, а оставшиеся 100 — для тестирования. Эти величины взяты исключительно ради наглядности и демонстрации логики выполнения операций, на практике все зависит от задачи, размер обучающей выборки может спокойно достигать и сотен тысяч образцов.

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

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

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

      Анализируем нашу гипотетическую выборку:

      Обучающая выборка.

      Таким образом, для тестирования подаем на вход сети значения x_{(M+1)1}, x_{(M+1)2} и проверяем, чему равен выход, ожидаем очевидно значение y_{(M+1)}. Аналогично поступаем и для оставшихся тестовых образцов. После чего мы можем сделать вывод, успешно или нет работает сеть. Например, сеть дает правильный ответ для 90% тестовых данных, дальше уже встает вопрос — устраивает ли нас данная точность или процесс обучения необходимо повторить, либо провести заново, изменив какие-либо параметры сети.

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

      Обучение нейронных сетей. Градиентный спуск.

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

      Здесь Delta w_{ij} — величина, на которую необходимо изменить вес синапса, связывающего нейроны i и j нашей сети. Соответственно, зная это, необходимо на каждом этапе обучения производить корректировку весов связей между всеми элементами нейронной сети. Задача ясна, переходим к делу.

      Пусть функция ошибки от веса имеет следующий вид:

      Для удобства рассмотрим зависимость функции ошибки от одного конкретного веса:

      График ошибки.

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

      Минимизация ошибки при обучении нейронной сети.

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

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

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

      Алгоритм обратного распространения ошибки.

      Находясь в точке 1, целью является перейти в точку 2, поскольку в ней значение ошибки меньше (E_2 < E_1), а глобальная задача по-прежнему заключается в ее минимизации. Для этого необходимо изменить величину w на некое значение Delta w (Delta w = w_2 — w_1 > 0). При всем при этом в точке 1 градиент отрицательный. Фиксируем данные факты и переходим к точке 3, предположим, что мы находимся именно в ней.

      Тогда для уменьшения ошибки наш путь лежит в точку 4, а необходимое изменение значения: Delta w = w_4 — w_3 < 0. Градиент же в точке 3 положителен. Этот факт также фиксируем.

      А теперь соберем воедино эту информацию в виде следующей иллюстрации:

      Переход bold{Delta w} Знак bold{Delta w} Градиент
      1 rArr 2 w_2 — w_1 +
      3 rArr 4 w_4 — w_3 +

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

      Delta w = -alpha cdot frac{dE}{dw}

      Имеем в наличии:

      • Delta w — величина, на которую необходимо изменить значение w.
      • frac{dE}{dw} — градиент в этой точке.
      • alpha — скорость обучения.

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

      Delta w_{ij} = -alpha cdot frac{dE}{dw_{ij}}

      Более того, вспомним о важном свойстве, которое мы отдельно пометили. И заключается оно в том, что величина градиента будет уменьшаться по мере приближения к минимуму функции. Что это нам дает? А то, что в том случае, если наша текущая дислокация далека от места назначения, то величина, корректирующая вес связи, будет больше. А это обеспечит скорейшее приближение к цели. При приближении к целевому пункту, величина frac{dE}{dw_{ij}} будет уменьшаться, что поможет нам точнее попасть в нужную точку, а кроме того, не позволит нам ее проскочить. Визуализируем вышеописанное:

      Скорость обучения.

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

      Норма обучения.

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

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

      Локальные минимумы при обучении нейронных сетей.

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

      Delta w_{ij} = -alpha cdot frac{dE}{dw_{ij}} + gamma cdot Delta w_{ij}^{t - 1}

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

      Итого, резюмируем продвижение к цели:

      • Нашей задачей было найти закон, по которому необходимо изменять величину весов связей между нейронами.
      • Наш результат — Delta w_{ij} = -alpha cdot frac{dE}{dw_{ij}} + gamma cdot Delta w_{ij}^{t — 1} — именно то, что и требовалось 👍

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

      Обучение нейронных сетей. Функция ошибки.

      Начнем с того, что определимся с тем, что у нас в наличии, для этого вернемся к конкретной нейронной сети. Пусть вид ее таков:

      Пример нейронной сети.

      Интересует нас, в первую очередь, часть, относящаяся к нейронам выходного слоя. Подав на вход определенные значения, получаем значения на выходе сети: O_{net, 1} и O_{net, 2}. Кроме того, поскольку мы ведем речь о процессе обучения нейронной сети, то нам известны целевые значения: O_{correct, 1} и O_{correct, 2}. И именно этот набор данных на этом этапе является для нас исходным:

      • Известно: O_{net, 1}, O_{net, 2}, O_{correct, 1} и O_{correct, 2}.
      • Необходимо определить величины Delta w_{ij} для корректировки весов, для этого нужно вычислить градиенты (frac{dE}{dw_{ij}}) для каждого из синапсов.

      Полдела сделано — задача четко сформулирована, начинаем деятельность по поиску решения.

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

      E_k = O_{correct, k} - O_{net, k}

      Дополним пример числовыми значениями:

      Нейрон bold{O_{net}} bold{O_{correct}} bold{E}
      1 0.9 0.5 -0.4
      2 0.2 0.6 0.4

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

      E_{sum} = e_1 + e_2 = -0.4 + 0.4 = 0

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

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

      E_k = | O_{correct, k} - O_{net, k} |

      Тут в действие вступает уже проблема иного рода:

      График модуля.

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

      В итоге хороший результат дает зависимость (для выходного нейрона под номером k):

      E_k = (O_{correct, k} - O_{net, k})^2

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

      Краткий вывод промежуточного шага, на который мы вышли:

      • Имеющееся: frac{dE}{dw_{jk}} = frac{d}{d w_{jk}}(O_{correct, k} — O_{net, k})^2.
      • Искомое по-прежнему: Delta w_{jk}.

      Несложные диффернциально-математические изыскания выводят на следующий результат:

      frac{dE}{d w_{jk}} = -(O_{correct, k} - O_{net, k}) cdot f{Large{prime}}(sum_{j}w_{jk}O_j) cdot O_j

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

      Освежим в памяти структуру сети:

      Пример обучения нейронных сетей.

      Формулу можно упростить, сгруппировав отдельные ее части:

      • (O_{correct, k} — O_{net, k}) cdot f{Large{prime}}(sum_{j}w_{jk}O_j) — ошибка нейрона k.
      • O_j — тут все понятно, выходной сигнал нейрона j.

      f{Large{prime}}(sum_{j}w_{jk}O_j) — значение производной функции активации. Причем, обратите внимание, что sum_{j}w_{jk}O_j — это не что иное, как сигнал на входе нейрона k (I_{k}). Тогда для расчета ошибки выходного нейрона: delta_k = (O_{correct, k} — O_{net, k}) cdot f{Large{prime}}(I_k).

      Итог: frac{dE}{d w_{jk}} = -delta_k cdot O_j.

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

      f{'}(x) = f(x)medspace (1medspace-medspace f(x))

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

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

      frac{dE}{d w_{ij}} = -delta_j cdot O_i

      Который примет следующий вид:

      delta_j = (sum_{k}{}{delta_kmedspace w_{jk}}) cdot f{Large{prime}}(I_j)

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

      frac{dE}{d w_{ij}} = -(sum_{k}{}{delta_kmedspace w_{jk}}) cdot f{Large{prime}}(I_j) cdot O_i

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

      • Ошибка:
        • выходной слой: delta_k = (O_{correct, k} — O_{net, k}) cdot f{Large{prime}}(I_k)
        • скрытые слои: delta_j = (sum_{k}{}{delta_kmedspace w_{jk}}) cdot f{Large{prime}}(I_j)
      • Градиент: frac{dE}{d w_{ij}} = -delta_j cdot O_i
      • Корректировка весовых коэффициентов: Delta w_{ij} = -alpha cdot frac{dE}{dw_{ij}} + gamma cdot Delta w_{ij}^{t — 1}

      Преобразуем последнюю формулу:

      Delta w_{ij} = alpha cdot delta_j cdot O_i + gamma cdot Delta w_{ij}^{t - 1}

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

      Метод обратного распространения ошибки.

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

      Суть же метода подразумевает наличие двух этапов:

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

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

      Вернемся к конкретному примеру для явной демонстрации этих принципов:

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

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

      Процесс обучения нейронной сети для алгоритма обратного распространения ошибки будет таким:

      1. Прямой проход. Подаем на вход значения I_1, I_2, I_3 из обучающей выборки. В результате работы сети получаем выходные значения O_{net, 1}, O_{net, 2}. Этому целиком и полностью был посвящен предыдущий манускрипт.
      2. Рассчитываем величины ошибок для всех слоев:
        • для выходного: delta_k = (O_{correct, k} — O_{net, k}) cdot f{Large{prime}}(I_k)
        • для скрытых: delta_j = (sum_{k}{}{delta_kmedspace w_{jk}}) cdot f{Large{prime}}(I_j)
      3. Далее используем полученные значения для расчета Delta w_{ij} = alpha cdot delta_j cdot O_i + gamma cdot Delta w_{ij}^{t — 1}
      4. И финишируем, рассчитывая новые значения весов: w_{ij medspace new} = w_{ij} + Delta w_{ij}
      5. На этом один цикл обучения закончен, данные шаги 1 — 4 повторяются для других образцов из обучающей выборки.

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

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

      Пример расчетов для метода обратного распространения ошибки.

      Возьмем нейронную сеть и зададим начальные значения весов:

      Пример расчетов для метода обратного распространения ошибки.

      Здесь я задал значения не в соответствии с существующими на сегодняшний день методами, а просто случайным образом для наглядности примера.

      В качестве функции активации используем сигмоиду:

      f(x) = frac{1}{1 + e^{-x}}

      И ее производная:

      f{Large{prime}}(x) = f(x)medspace (1medspace-medspace f(x))

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

      • Входные: I_1 = 0.6, I_1 = 0.7.
      • Выходное: O_{correct} = 0.9.

      Скорость обучения alpha пусть будет равна 0.3, момент — gamma = 0.1. Все готово, теперь проведем полный цикл для метода обратного распространения ошибки, то есть прямой проход и обратный.

      Прямой проход.

      Начинаем с выходных значений нейронов 1 и 2, поскольку они являются входными, то:

      O_1 = I_1 = 0.6 \
      O_2 = I_2 = 0.7

      Значения на входе нейронов 3, 4 и 5:

      I_3 = O_1 cdot w_{13} + O_2 cdot w_{23} = 0.6 cdot (-1medspace) + 0.7 cdot 1 = 0.1 \
      I_4 = 0.6 cdot 2.5 + 0.7 cdot 0.4 = 1.78 \
      I_5 = 0.6 cdot 1 + 0.7 cdot (-1.5medspace) = -0.45

      На выходе этих же нейронов первого скрытого слоя:

      O_3 = f(I3medspace) = 0.52 \
      O_4 = 0.86\
      O_5 = 0.39

      Продолжаем аналогично для следующего скрытого слоя:

      I_6 = O_3 cdot w_{36} + O_4 cdot w_{46} + O_5 cdot w_{56} = 0.52 cdot 2.2 + 0.86 cdot (-1.4medspace) + 0.39 cdot 0.56 = 0.158 \
      I_7 = 0.52 cdot 0.34 + 0.86 cdot 1.05 + 0.39 cdot 3.1 = 2.288 \
      O_6 = f(I_6) = 0.54 \
      O_7 = 0.908

      Добрались до выходного нейрона:

      I_8 = O_6 cdot w_{68} + O_7 cdot w_{78} = 0.54 cdot 0.75 + 0.908 cdot (-0.22medspace) = 0.205 \
      O_8 = O_{net} = f(I_8) = 0.551

      Получили значение на выходе сети, кроме того, у нас есть целевое значение O_{correct} = 0.9. То есть все, что необходимо для обратного прохода, имеется.

      Обратный проход.

      Как мы и обсуждали, первым этапом будет вычисление ошибок всех нейронов, действуем:

      delta_8 = (O_{correct} - O_{net}) cdot f{Large{prime}}(I_8) = (O_{correct} - O_{net}) cdot f(I_8) cdot (1-f(I_8)) = (0.9 - 0.551medspace) cdot 0.551 cdot (1-0.551medspace) = 0.0863 \
      delta_7 = (sum_{k}{}{delta_kmedspace w_{jk}}) cdot f{Large{prime}}(I_7) = (delta_8 cdot w_{78}) cdot f{Large{prime}}(I_7) = 0.0863 cdot (-0.22medspace) cdot 0.908 cdot (1 - 0.908medspace) = -0.0016 \
      delta_6 = 0.086 cdot 0.75 cdot 0.54 cdot (1 - 0.54medspace) = 0.016 \
      delta_5 = (sum_{k}{}{delta_kmedspace w_{jk}}) cdot f{Large{prime}}(I_5) = (delta_7 cdot w_{57} + delta_6 cdot w_{56}) cdot f{Large{prime}}(I_7) = (-0.0016 cdot 3.1 + 0.016 cdot 0.56) cdot 0.39 cdot (1 - 0.39medspace) = 0.001 \
      delta_4 = (-0.0016 cdot 1.05 + 0.016 cdot (-1.4)) cdot 0.86 cdot (1 - 0.86medspace) = -0.003 \
      delta_3 = (-0.0016 cdot 0.34 + 0.016 cdot 2.2) cdot 0.52 cdot (1 - 0.52medspace) = -0.0087

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

      Delta w_{ij} = alpha cdot delta_j cdot O_i + gamma cdot Delta w_{ij}^{t - 1}

      Как вы помните, Delta w_{ij}^{t — 1} — это величина поправки для данного веса на предыдущей итерации. Но поскольку у нас это первый проход, то данное значение будет нулевым, соответственно, в данном случае второе слагаемое отпадает. Но забывать о нем нельзя. Продолжаем калькулировать:

      Delta w_{78} = alpha cdot delta_8 cdot O_7 = 0.3 cdot 0.0863 cdot 0.908 = 0.0235 \
      Delta w_{68} = 0.3 cdot 0.0863 cdot 0.54= 0.014 \
      Delta w_{57} = alpha cdot delta_7 cdot O_5 = 0.3 cdot (−0.0016medspace) cdot 0.39= -0.00019 \
      Delta w_{47} = 0.3 cdot (−0.0016medspace) cdot 0.86= -0.0004 \
      Delta w_{37} = 0.3 cdot (−0.0016medspace) cdot 0.52= -0.00025 \
      Delta w_{56} = alpha cdot delta_6 cdot O_5 = 0.3 cdot 0.016 cdot 0.39= 0.0019 \
      Delta w_{46} = 0.3 cdot 0.016 cdot 0.86= 0.0041 \
      Delta w_{36} = 0.3 cdot 0.016 cdot 0.52= 0.0025 \
      Delta w_{25} = alpha cdot delta_5 cdot O_2 = 0.3 cdot 0.001 cdot 0.7= 0.00021 \
      Delta w_{15} = 0.3 cdot 0.001 cdot 0.6= 0.00018 \
      Delta w_{24} = alpha cdot delta_4 cdot O_2 = 0.3 cdot (-0.003medspace) cdot 0.7= -0.00063 \
      Delta w_{14} = 0.3 cdot (-0.003medspace) cdot 0.6= -0.00054 \
      Delta w_{23} = alpha cdot delta_3 cdot O_2 = 0.3 cdot (−0.0087medspace) cdot 0.7= -0.00183 \
      Delta w_{13} = 0.3 cdot (−0.0087medspace) cdot 0.6= -0.00157

      И самый что ни на есть заключительный этап — непосредственно изменение значений весовых коэффициентов:

      w_{78 medspace new} = w_{78} + Delta w_{78} = -0.22 + 0.0235 = -0.1965 \
      w_{68 medspace new} = 0.75+ 0.014 = 0.764 \
      w_{57 medspace new} = 3.1 + (−0.00019medspace) = 3.0998\
      w_{47 medspace new} = 1.05 + (−0.0004medspace) = 1.0496\
      w_{37 medspace new} = 0.34 + (−0.00025medspace) = 0.3398\
      w_{56 medspace new} = 0.56 + 0.0019 = 0.5619 \
      w_{46 medspace new} = -1.4 + 0.0041 = -1.3959 \
      w_{36 medspace new} = 2.2 + 0.0025 = 2.2025 \
      w_{25 medspace new} = -1.5 + 0.00021 = -1.4998 \
      w_{15 medspace new} = 1 + 0.00018 = 1.00018 \
      w_{24 medspace new} = 0.4 + (−0.00063medspace) = 0.39937 \
      w_{14 medspace new} = 2.5 + (−0.00054medspace) = 2.49946 \
      w_{23 medspace new} = 1 + (−0.00183medspace) = 0.99817 \
      w_{13 medspace new} = -1 + (−0.00157medspace) = -1.00157\

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

      Понравилась статья? Поделить с друзьями:
    • Свч кертинг ошибка е3
    • Сверлильный станок ошибка
    • Свч борк ошибка е01
    • Сверка снилс ошибка нет дстп
    • Свч samsung коды ошибок