
Несмотря на то, что можно найти не одну статью, объясняющую принцип метода обратного распространения ошибки в сверточных сетях (раз, два, три и даже дающих “интуитивное” понимание — четыре), мне, тем не менее, никак не удавалось полностью понять эту тему. Кажется, что авторы недостаточно внимания уделяют обычным примерам либо же опускают какие-то хорошо понятные им, но не очевидные другим особенности, и весь материал по этой причине становится неподъемным. Мне хотелось разложить все по полочкам для самого себя и в итоге конспекты вылились в статью. Я постарался исключить все недостатки существующих объяснений и надеюсь, что эта статья ни у кого не вызовет вопросов или недопониманий. И, может, следующий новичок, который, также как и я, захочет во всем разобраться, потратит уже меньше времени.
В этой, первой статье, мы рассмотрим архитектуру будущей сети, и все формулы для прямого прохождения через эту сеть. Во второй статье мы подробно остановимся на обратном распространении ошибки, выведем и разберем формулы — ради этой части все и затевалось, именно формулы для обучения модели и в особенности сверточного слоя показались мне самыми тяжелыми. Последняя статья представит примерный вид реализации сети на python, а также попробуем обучить сеть на настоящем датасете и сравним результаты с аналогичной реализацией, но уже с помощью библиотеки pytorch. В течение всего материала я буду по частям выкладывать код на python, чтобы сразу можно было видеть реализации формул. При написании кода акцентировал внимание на том, чтобы формулы легко “читались” в строках, меньше времени уделяя оптимизации и красоте. Вообще, конечная цель — чтобы читатель разобрался во всех тонкостях обновления параметров сверточной и полносвязной сетей и смог представить, как может выглядеть работающий код этой сети.
Чего не будет в этих статьях? Объяснений основ математики и частных производных, подробностей “интуитивного” понимания сути backpropagation (для начала можете прочесть эту отличную статью) или того, как работают сети вообще, в том числе сверточные. Для лучшего понимания материала желательно знать эти вещи и особенно — основы работы нейросетей.
Итак, первая статья.
Конволюция

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

Но, возвращаясь к формуле конволюции, что означает — сумма от минус бесконечности до плюс бесконечности? Ведь само ядро имеет вполне определенные размеры и у него нет бесконечного числа элементов. Я находил разные варианты написания формулы, например,
Минус в формуле для ядра свертки — это следствие расположения центрального элемента. Нам следует “перебрать” все возможные существующие элементы, и начать можно от минус бесконечности. Или от минус
На словах может звучать тяжело, и, наверное, лучше всего посмотреть, как это можно реализовать на python. Ниже ссылка на jupyter-ноутбук для расчета индексов “новой” оси ядра в зависимости от выбранного центрального элемента:
recalculating_kernel_indexes.ipynb
На картинке ниже мы объявили центром ядра тот элемент, который находится в позиции (1,1).

Но “старые” координаты говорят нам, что позиция центрального элемента должна находиться по индексам (0,0), а значит, необходимо переопределить координатные оси для нового положения центрального элемента.
Если мы подставим в код выше наши значения, то получим заполненный питоновский лист значениями из range(-1, 2), то есть лист будет содержать [-1,0,1]. Еще раз, почему range(-1, 2)? “Минус один” потому, что операция начинается от минус индекса нашего центрального элемента, а “два” получается как длина оси (равная трем) минус индекс центрального элемента в старых координатах (то есть один). Последний элемент range не включается.
Кросс-корреляция
Приведу еще раз формулу конволюции:

Здесь можно видеть позицию ядра, его расположение во время свертки относительно матрицы
demo_of_conv_feed.ipynb
И сразу хотелось бы отметить, что выбор центрального элемента или значений шага свертки, размеров матрицы ядра, формулы корреляции или конволюции — все эти нюансы непосредственно отражаются в формулах обратного распространения ошибки и поэтому обучение будет проходить корректно вне зависимости от выбранных параметров. В коде я постарался реализовать все эти вещи, их можно будет настраивать и попробовать запустить все самостоятельно.
В зависимости от способа свертки — конволюции или кросс-корреляции, различной величины шага и выбора центрального элемента ядра — размерность выходной матрицы
Формулы не учитывают положение центрального элемента, но зато так будет легче сравнивать результаты вычислений нашего кода с pytorch.
Функции активации
Ниже формулы функций активации, которые можно будет использовать в будущей модели. Фактически, это просто “превращение”
Итак, ReLU:

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

demo_of_maxpooling.ipynb
Код очень похож на свертку, причем даже сохранились те же параметры: выбор страйда, флаг операции конволюции или кросс-корреляции (так как по логике данной функции окно макспулинга тождественно ядру свертки) и выбора центрального элемента. Но, конечно, здесь не происходит поэлементного перемножения матриц, а только, собственно, выбор максимального значения из заданного окна. “Классические” значения параметров макспулинга в параметрах свертки — это кросс-корреляция и позиция центрального элемента в левом верхнем углу.
Функция из демонстрационного кода возвращает две матрицы — выходная матрица меньшей размерности и еще одна матрица с координатами элементов, которые были выбраны максимальными из исходной матрицы во время операции макспулинга. Вторая матрица пригодится во время обратного распространения ошибки.
Слой полносвязной сети
После слоев конволюции мы получим множество карт признаков. Их соединим в один вектор и этот вектор подадим на вход fully connected сети.

Формула для fc-слоя (fully connected) выглядит так:
Функция потерь
Завершающий этап сети — функция, оценивающая качество работы всей модели. Функция потерь находится в самом конце, после всех слоев сети. Выглядеть она может так:
После прочтения этой статьи решил использовать cross-entropy: последняя сильнее «штрафует» за неправильный ответ (когда модель слабо уверена в выборе класса, который на самом деле является правильным). А вот MSE в свою очередь хорошо подходит для регрессии.
Структура будущей модели
Теперь, разобрав основные слои сети, мы можем представить примерный вид будущей модели:
- Функция, которая извлекает из датасета следующее изображение/батч для проведения обучения;
- Первый слой сверточной сети, который на вход принимает изображение, на выходе отдает карты признаков;
- Слой макспулинга, который снижает размерность карт признаков;
- Второй слой сверточной сети принимает полученные на предыдущем шаге карты, и на выходе дает другие карты признаков;
- Сложение полученных на предыдущем шаге карт в один вектор;
- Первый слой полносвязной сети принимает вектор, производит вычисления, которые дают значения для скрытого полносвязного слоя;
- Второй слой полносвязной сети, количество выходных нейронов которого равно количеству классов в используемом датасете;
- Выход всей модели подается в функцию потерь, которая сравнивает прогнозируемое значение с истинным, и вычисляет разницу между этими значениями.
Итоговая функция потерь является своего рода количественным “штрафом”, который можно рассматривать как меру качества прогноза модели. Это значение мы и будем использовать для обучения модели с помощью backpropagation — обратного распространения ошибки. Формулы, которые используют эту ошибку и “протягивают” ее сквозь все слои для обновления параметров и обучения модели, мы рассмотрим в следующей части статьи.
Следующие статьи серии:
Сверточная сеть на python. Часть 2. Вывод формул для обучения модели
Сверточная сеть на python. Часть 3. Применение модели