Рубрики
Без рубрики

Сверточные нейронные сети: интуитивно понятный учебник

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

Нейронные сети (2 серии деталей)

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

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

В некотором смысле, это очень круто. Сеть изучает все самостоятельно, не только шаблоны в данных, но и саму структуру входных данных. Тем не менее, это идет по цене: количество весов и предубеждений в таком Полностью подключен Сеть растет очень быстро. Каждое изображение Mnist имеет 28 × 28 пиксели, поэтому входной слой имеет 28 × 28 , или 784 нейроны. Допустим, мы настроили полностью связанный скрытый слой с 30 нейроны. Это означает, что теперь у нас есть 28 × 28 × 30 , или 23 520 Веса, плюс 30 Предвзятость, чтобы наша сеть отслеживала. Это уже составляет 23 550 Параметры! Представьте себе количество параметров, которые нам понадобятся для цветных изображений 4K Ultra HD!

Введение сверстников

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

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

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

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

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

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

Если наше изображение M × n Сетка, и наше наложение – это Я × j Сетка (и мы используем длину шага 1 ), затем перемещение наложения таким образом будет создавать (M-I+1) × (J-N+1) сетка. Например, если у нас есть 4 × 5 Входная сетка и 2 × 2 наложение, результатом будет 3 × 4 сетка. Колочка – это то, что мы называем этой операцией генерации новой сетки, перемещая наложение по входной матрице.

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

Активация карты функций

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

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

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

Чтобы получить сырую активацию z Для соответствующего нейрона функции нам просто нужно добавить смещение к этому значению. Затем мы применяем функцию активации σ (z) Чтобы получить A Анкет Надеемся, что это выглядит знакомо: чтобы генерировать активацию для одного нейрона в карте функций, мы выполняем тот же расчет, который мы использовали в предыдущей статье – разница в том, что мы применяем его к небольшой области ввода на этот раз. Эта идея показана на диаграмме ниже:

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

Обратите внимание, что мы продолжаем использовать ту же матрицу весов, что и фильтр по всей входной матрице. Это важный трюк: мы поддерживаем только один предвзятость и один набор весов, которые разделяются среди всех нейронов в данной карте признаков. Это спасает нас много параметров! Допустим, у нас то же самое 28 × 28 , или 784 Входные нейроны И мы выбираем 4 × 4 наложение. Это будет производить 25×25 карта функции. Эта карта функций будет иметь только 16 Общие веса и 1 Общий предвзятость. Обычно мы устанавливаем несколько независимых карт функций в первом сверточный слой Анкет Предположим, что мы настроили 16 Особенности карты в этом случае. Это означает, что у нас есть 17 × 16 , или 272 Параметры в этом сверточном слое, гораздо меньше, чем 23 550 Параметры, которые мы рассмотрели ранее для полностью подключенного слоя.

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

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

Код, который создает активации для карты функций, показан ниже (полный код доступен в разделе кода в конце статьи):

self.z = sp.signal.correlate2d(self.a_prev, self.w, mode="valid") + self.b
self.a = sigmoid(self.z)

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

Важно отметить, что, поскольку все нейроны на карте признаков имеют свои веса и предвзятость, они в каком -то смысле являются одним и тем же нейроном. Каждый ищет одну и ту же функцию в разных частях ввода. Это известно как Трансляционная инвариантность Анкет Например, допустим, мы хотим распознать изображение, представляющее букву U , как показано ниже:

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

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

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

Обратное распространение через карту функций

Далее, давайте выясним, как сделать обратный процесс через карту функций. Давайте использовать тот же простой пример 3 × 3 Входная матрица и 2 × 2 Веса фильтра. Поскольку наша карта функций также является 2 × 2 Матрица, мы можем ожидать получить ∂c/da L как 2 × 2 Матрица от следующего слоя во время обработки обработки.

Градиент смещения

Первый шаг в обратном распространении – рассчитать ∂c/db L . Мы знаем следующее уравнение, полученное с помощью правила цепи:

В этом контексте мы видим, что для каждого нейрона функции мы можем умножить его σ ‘(z) Значение по его ∂c/da ценность. Это дает 2 × 2 Матрица, которая говорит нам о значении ∂c/db , производная стоимости в отношении смещения, для каждого нейрона. На приведенной ниже диаграмме показан этот результат:

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

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

  • b -= ∂c/db 1 * размер шага
  • b -= ∂c/db 2 * размер шага
  • b -= ∂c/db 3 * размер шага
  • b -= ∂c/db 4 * размер шага

На самом деле, мы можем сделать именно это. Мы объединяем значения ∂c/db Для каждой особенности нейрон. Мы видим, что корректировка смещения с использованием этой суммы дает тот же результат, что и мы видим в приведенных выше уравнениях, благодаря ассоциативности добавления:

b -= (∂c/db 1 + ∂c/дБ 2 + ∂c/дБ 3 + ∂c/дБ 4 ) * размер шага

Теперь, когда у нас есть некоторая интуиция для этого расчета, можем ли мы найти простой способ выразить его математически? На самом деле, мы можем думать об этом как о другом, очень простой, кросс-корреляции. У нас есть 2 × 2 Матрица для ∂c/da L и 2 × 2 Матрица для σ ‘(z L ) . Поскольку они одинакового размера, кросс-коррелирование их вместе дает одно значение. Крестная корреляция умножает каждую ячейку в наложении на соответствующую ячейку в подложке, затем добавляет эти продукты вместе, что является кумулятивным значением ∂c/db L мы хотим. Мы также сохраним четыре составляющие значения ∂c/db Для использования в последующих расчетах обратного процесса.

Следующая строка кода демонстрирует этот расчет (полный список кодов находится в разделе кода в конце статьи):

b_gradient = sp.signal.correlate2d(sigmoid_prime(self.z), a_gradient, mode="valid")

Градиент веса

Следующим шагом в обратном распространении является расчет ∂c/dw L . Цепное правило говорит нам:

Можем ли мы найти способ применить эту идею к нашей карте функций таким образом, чтобы иметь интуитивно понятный смысл? Мы знаем, что каждый нейрон в карте признаков соответствует 2 × 2 часть активаций предыдущего слоя. Мы можем умножить локальное значение ∂C/дБ для каждого нейрона функции на каждую из соответствующих активаций в предыдущем слое. Это дает четыре 2 × 2 матрицы. Каждая матрица представляет компонент ∂c/dw L Для данного нейрона в карте признаков. Как и прежде, мы можем добавить все это вместе, чтобы получить кумулятивную ценность ∂c/dw L Для этой карты функций. Диаграмма ниже иллюстрирует эту идею:

Оказывается, что мы можем кратко выразить этот расчет как кросс-корреляцию. Мы можем взять 3 × 3 Матрица активаций в предыдущем слое и кросс-коррелат с его 2 × 2 Матрица, представляющая компоненты ∂C/дБ. Это дает то же самое 2 × 2 Матрица как сумма матриц на предыдущей диаграмме. Код для этой логики ниже (полный список кодов находится в разделе кода в конце статьи):

w_gradient = sp.signal.correlate2d(self.a_prev, b_gradient_components, mode="valid")

Градиент активации для предыдущего слоя

Последний шаг в обратном распространении – рассчитать ∂c/da L-1 . Из правила цепи мы знаем:

Как мы можем сделать эту работу с нашей картой сверточной функции? Ранее мы разработали компоненты ∂c/db Для каждого нейрона в карте признаков. Здесь мы отображаем эти значения обратно с наложениями, с которыми они соответствуют входной матрице. Мы умножаем каждый компонент ∂c/db по соответствующему весу для этой позиции в наложении. Для каждой функции нейрона мы устанавливаем части входной матрицы, которые не покрыты нулю. Таким образом, четыре нейроны карты функций производят 4 3 × 3 матрицы. Это компоненты ∂c/da L-1 соответствует каждому нейрону карты признаков. Еще раз, чтобы получить кумулятивное значение, мы добавляем их вместе, чтобы получить единый 3 × 3 Матрица, представляющая кумулятивное значение для ∂c/da L-1 . Диаграмма ниже иллюстрирует этот процесс:

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

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

Мы можем видеть, что для получения желаемого результата мы можем применить этот процесс, используя компоненты 2 × 2 Матрица для ∂c/db В качестве наложения поверх 2 × 2 Матрица общих весов W Анкет Поскольку мы начинаем с наложения, покрывающего только единственный вес в верхнем левом углу, результатом будет 3 × 3 Матрица, что мы хотим для ∂c/da L-1 Анкет

Чтобы наши расчеты соответствовали расчетам, показанным ранее, нам нужно повернуть ∂c/db Матрица фильтра на 180 ° первой, хотя. Таким образом, мы начинаем с ∂c/db 0,0 покрытие W 0,0 Анкет Если вы выполните этот расчет, вы обнаружите, что конечный результат такой же, как и сумма четырех 3 × 3 Матрицы на предыдущей диаграмме. Мы использовали операцию по перекрестной корреляции до сих пор. Здесь, поскольку мы должны повернуть фильтр, мы фактически выполняем надлежащую операцию свертки. На приведенной ниже диаграмме показана начальная позиция полной свертки ∂c/db с матрицей веса w .

Код для этого выглядит следующим образом (Полный список кодов находится в разделе кода в конце статьи):

a_prev_gradient = sp.signal.convolve2d(self.w, b_gradient_components, mode="full")

Цепочка сверточных слоев

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

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

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

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

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

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

Чтобы понять, почему мы получим двухмерную матрицу, рассмотрим случай перекрестного коррелирования или свертывания двух двухмерных матриц в допустимом режиме, которые имеют одинаковую ширину. Результатом будет 1-D матрица. Например, если у нас есть 7 × 3 Матрица и мы пересекаем это с 2 × 3 Матрица, мы получаем 6 × 1 матрица. Здесь это глубина трехмерных матриц, которые соответствуют, поэтому во время кросс-корреляции или свертки значения составляются вместе по глубине и разкладываются в отдельные значения.

Бэкпропагирование должно быть применением всех принципов, которые мы разработали до сих пор:

  • Мы используем наш обычный метод для получения двухмерной матрицы, представляющей компоненты ∂c/db Для данной карты функций в следующем слое.
  • Рассчитать градиент для фильтров, ∂c/dw Для этой карты функций следующего уровня мы перекрестно коррелируем матрицу активации трехмерной карты из предыдущего слоя с нашим 2-D ∂c/db Матрица, представляющая компоненты градиента смещения. Это дает нам трехмерную матрицу для ∂c/dw Для текущей карты функций в следующем слое – каждый срез соответствует весам для одной из карт признаков в предыдущем слое.
  • Для ∂c/da L-1 , мы конвертируем нашу трехмерную матрицу веса, W , с нашим 2-D ∂c/db Матрица для данной карты функций в следующем слое. Это дает нам трехмерную матрицу для ∂c/da L-1 Это представляет производную стоимости в отношении активаций каждой карты функций в предыдущем уровне (соответствует нашей текущей карте функций в следующем уровне). Мы повторяем этот расчет для каждой карты функций в следующем слое и складываем полученные матрицы. Каждый ломтик этой конечной матрицы представляет значение ∂c/da Для соответствующей карты функций в предыдущем слое.

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

Макс объединение

Другая техника, которая иногда используется с сверточными слоями, – это Макс объединение Анкет Идея довольно проста: мы перемещаем наложение на карту функций так же, как и в свертке. Тем не менее, каждый нейрон в максимальном обмене картировании просто берет нейрон из соответствующего наложения, который имеет самую высокую активацию и передает эту активацию к следующему слою. Это явно еще больше уменьшает количество параметров, так что это одно преимущество этой техники. Я считаю, что, абстрагируя вход, это также может помочь избежать переосмысление Проблема, проблема, которая часто возникает в машинном обучении.

Правопропаживание для Max Pooling простое. Для нейрона на максимальной карте объединения мы просто передаем значение ∂c/da к нейрону с максимальной активацией в соответствующем наложении из предыдущего слоя. Другие значения градиента в наложении установлены на 0 , поскольку эти нейроны не передавали свои активации и, следовательно, не способствовали стоимости. На приведенной ниже диаграмме показаны шаги распространения вперед и назад для карты максимального объединения:

Дискуссия

Сверточные нейронные сети, или CNNS , представляют собой значительный практическое продвижение в возможностях нейронных сетей. Такие сети могут достичь лучшей точной, а также улучшения скорости обучения. В Майкла Нильсена Нейронные сети и глубокое обучение , он объединяет CNN с некоторыми другими методами для достижения более чем 99% Точность распознавая цифры Mnist! Это значительное улучшение по сравнению с 95%, достигнутым с использованием полностью подключенной сети.

Тем не менее, стоит отметить, что CNN не являются панацеей. Например, в то время как CNN хорошо справляются с обработкой трансляционной инвариантности по всему полю, они не обрабатывают вращения.

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

  • Функция квадратичной стоимости заменяется чем -то другим, например A перекрестная энтропия Функция стоимости
  • Вместо сигмоида различные функции активации используются как Relu , Softmax , и т.д.
  • Регуляризация применяется к весам сети, чтобы уменьшить переосмысление

Код

Ниже я внедрил несколько классов для демонстрационных целей. Есть FeatureMap Это реализует вперед и обратно для одной функции. Несколько таких карт функций обычно используются для составления одного сверточного слоя. Также есть Maxpoolingmap который реализует максимальный объединение с карты функций. Наконец, есть Полностью концентрированная layer , который реализует логику, обсуждаемую в предыдущей статье. В CNN обычно есть несколько сверточных слоев, а затем полностью связанный слой в качестве последнего скрытого слоя. Этот полностью подключенный слой эффективно агрегирует все этапы построения объектов, которые предшествуют ему, прежде чем отправлять свои активации на выходной слой (мне приходит в голову, что мы также можем реализовать это как сверточный слой, где каждая карта функций является 1 × 1 Матрица).

import numpy as np
import scipy as sp
from scipy import signal

class FeatureMap:
    def __init__(self, a_prev, overlay_shape):
        # 2d matrix representing input from previous layer
        self.a_prev = a_prev

        # shared weights and bias for this layer
        self.w = np.random.randn(*overlay_shape)
        self.b = np.random.randn(1,1)

    def feed_forward(self):
        self.z = sp.signal.correlate2d(self.a_prev, self.w, mode="valid") + self.b
        self.a = sigmoid(self.z)

        return self.a

    def propagate_backward(self, a_gradient, step_size):
        b_gradient_components = dc_db(self.z, a_gradient)

        b_gradient = sp.signal.correlate2d(sigmoid_prime(self.z), a_gradient, mode="valid")
        w_gradient = sp.signal.correlate2d(self.a_prev, b_gradient_components, mode="valid")
        a_prev_gradient = sp.signal.convolve2d(self.w, b_gradient_components, mode="full")

        self.b -= b_gradient * step_size
        self.w -= w_gradient * step_size
        self.a_prev_gradient = a_prev_gradient

        return self.a_prev_gradient

class MaxPoolingMap:
    def __init__(self, a_prev, overlay_shape):
        self.a_prev = a_prev
        self.overlay_shape = overlay_shape

    def feed_forward(self):
        self.max_values, self.max_positions = max_values_and_positions(
            self.a_prev, self.overlay_shape)

        return self.max_values

    def propagate_backward(self, a_gradient):
        a_prev_gradient = np.zeros(self.a_prev.shape)

        rows, cols = self.max_values.shape
        for r in xrange(rows):
            for c in xrange(cols):
                max_position = self.max_positions[r][c]
                a_prev_gradient[max_position] += a_gradient[r][c]

        self.a_prev_gradient = a_prev_gradient

        return self.a_prev_gradient

class FullyConnectedLayer:
    def __init__(self, a_prev, num_neurons):
        self.a_prev = a_prev
        self.num_neurons = num_neurons

        self.w = np.random.randn(num_neurons, a_prev.size)
        self.b = np.random.randn(num_neurons,1)

    def feed_forward(self):
        a_prev = as_col(self.a_prev)

        self.z = raw_activation(self.w, a_prev, self.b)
        self.a = sigmoid(self.z)

        return self.a

    def propagate_backward(self, a_gradient, step_size):
        b_gradient = dc_db(self.z, a_gradient)

        a_prev = as_col(self.a_prev)
        weights_gradient = dc_dw(a_prev, b_gradient)

        a_prev_gradient = dc_da_prev(self.w, b_gradient)
        self.a_prev_gradient = a_prev_gradient.reshape(self.a_prev.shape)

        self.b -= b_gradient * step_size
        self.w -= weights_gradient * step_size

        return self.a_prev_gradient

# utility functions

def sigmoid(z):
    return 1.0/(1.0+np.exp(-z))

def sigmoid_prime(z):
    return sigmoid(z)*(1-sigmoid(z))

def dc_db(z, dc_da):
    return sigmoid_prime(z) * dc_da

def get_feature_map_shape(input_data_shape, overlay_shape):
    input_num_rows, input_num_cols = input_data_shape
    overlay_num_rows, overlay_num_cols = overlay_shape
    num_offsets_for_row = input_num_rows-overlay_num_rows+1
    num_offsets_for_col = input_num_cols-overlay_num_cols+1

    return (num_offsets_for_row, num_offsets_for_col)

def get_max_value_position(matrix):
    max_value_index = matrix.argmax()
    return np.unravel_index(max_value_index, matrix.shape)

def max_values_and_positions(a_prev, overlay_shape):
    feature_map_shape = get_feature_map_shape(a_prev.shape, overlay_shape)
    max_values = np.zeros(feature_map_shape)
    max_positions = np.zeros(feature_map_shape, dtype=object)

    overlay_num_rows, overlay_num_cols = overlay_shape
    feature_map_rows, feature_map_cols = feature_map_shape
    for r in xrange(feature_map_rows):
        for c in xrange(feature_map_cols):
            overlay = a_prev[r:r+overlay_num_rows, c:c+overlay_num_cols]
            max_value = np.amax(overlay)
            max_value_overlay_row, max_value_overlay_col = get_max_value_position(overlay)
            max_value_row = r+max_value_overlay_row
            max_value_col = c+max_value_overlay_col

            max_values[r][c] = max_value
            max_positions[r][c] = (max_value_row, max_value_col)

    return (max_values, max_positions)

def raw_activation(w, a, b):
    return np.dot(w,a) + b

def dc_dw(a_prev, dc_db):
    return np.dot(dc_db, a_prev.transpose())

def dc_da_prev(w, dc_db):
    return np.dot(w.transpose(), dc_db)

def as_col(matrix):
    return matrix.reshape(matrix.size, 1)

input_data = np.arange(20).reshape(4,5) # 4x5 array
overlay_shape = (2, 2)
cl = FeatureMap(input_data, overlay_shape)
cl.feed_forward()
fl_shape = get_feature_map_shape(input_data.shape, overlay_shape)
cl.propagate_backward(np.random.randn(*fl_shape), 0.1)

max_pool_input_data = np.array([[8,0,11,1,6],[10,2,4,14,17],[5,16,19,15,7],[12,13,9,18,3]])
mpl = MaxPoolingMap(max_pool_input_data, overlay_shape)
mpl.feed_forward()
fl_shape = get_feature_map_shape(max_pool_input_data.shape, overlay_shape)
mpl.propagate_backward(np.random.randn(*fl_shape))

fcl = FullyConnectedLayer(input_data, 10)
fcl.feed_forward()
fcl.propagate_backward(as_col(np.random.randn(10)), 0.1)

Приложение A: Действительно против полного режима

Приведенный ниже код Python показывает, как использовать меж-корреляцию и свертку с использованием Действительно и Полный режимы. Обратите внимание, как Convolute2d дает тот же результат, что и Correlate2d с фильтром, который вращается на 180 °.

>>> import numpy as np
>>> import scipy as sp
>>> from scipy import signal
>>> values = np.array([[1,2,3],[4,5,6],[7,8,9]])
>>> values
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
>>> f = np.array([[10,20],[30,40]])
>>> f
array([[10, 20],
       [30, 40]])
>>> sp.signal.correlate2d(values,f,mode="valid")
array([[370, 470],
       [670, 770]])
>>> sp.signal.convolve2d(values,f,mode="valid")
array([[230, 330],
       [530, 630]])
>>> f_rot180 = np.rot90(np.rot90(f))
>>> f_rot180
array([[40, 30],
       [20, 10]])
>>> sp.signal.correlate2d(values,f_rot180,mode="valid")
array([[230, 330],
       [530, 630]])
>>> sp.signal.correlate2d(values,f,mode="full")
array([[ 40, 110, 180,  90],
       [180, 370, 470, 210],
       [360, 670, 770, 330],
       [140, 230, 260,  90]])
>>> sp.signal.convolve2d(values,f,mode="full")
array([[ 10,  40,  70,  60],
       [ 70, 230, 330, 240],
       [190, 530, 630, 420],
       [210, 520, 590, 360]])
>>> sp.signal.correlate2d(values,f_rot180,mode="full")
array([[ 10,  40,  70,  60],
       [ 70, 230, 330, 240],
       [190, 530, 630, 420],
       [210, 520, 590, 360]])

Приложение B: суммирование 2-D против 3-D Stack:

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

>>> feature_map1 = np.array([[1,2,3],[4,5,6],[7,8,9]])
>>> feature_map2 = np.array([[9,8,7],[6,5,4],[3,2,1]])
>>> filter1 = np.array([[1,2],[3,4]])
>>> filter2 = np.array([[5,6],[7,8]])
>>> feature_map1
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
>>> feature_map2
array([[9, 8, 7],
       [6, 5, 4],
       [3, 2, 1]])
>>> filter1
array([[1, 2],
       [3, 4]])
>>> filter2
array([[5, 6],
       [7, 8]])
>>> result1 = sp.signal.correlate2d(feature_map1, filter1, mode="valid")
>>> result1
array([[37, 47],
       [67, 77]])
>>> result2 = sp.signal.correlate2d(feature_map2, filter2, mode="valid")
>>> result2
array([[175, 149],
       [ 97,  71]])
>>> sum_of_results = result1 + result2
>>> sum_of_results
array([[212, 196],
       [164, 148]])
>>> feature_maps_stacked = np.array([feature_map1, feature_map2])
>>> feature_maps_stacked
array([[[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]],

       [[9, 8, 7],
        [6, 5, 4],
        [3, 2, 1]]])
>>> filters_stacked = np.array([filter1, filter2])
>>> filters_stacked
array([[[1, 2],
        [3, 4]],

       [[5, 6],
        [7, 8]]])
>>> stacked_results = signal.sp.correlate(feature_maps_stacked, filters_stacked, mode="valid")
>>> stacked_results.reshape(2,2) # same as sum_of_results
array([[212, 196],
        [164, 148]])

Нейронные сети (2 серии деталей)

Оригинал: “https://dev.to/nestedsoftware/convolutional-neural-networks-an-intuitive-primer-k1k”