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

Используем NumPy для разработки сверточной нейронной сети.

Bohdan Balov 🇺🇦
13 min readFeb 26, 2019

--

Внимание! Данная статья является переводом. Оригинал можно найти по этой ссылке.

Когда Ян ЛеКун опубликовал свою работу, посвященную новой нейросетевой архитектуре [1], получившей название CNN (Convolutional Neural Network), она не произвела достаточного впечатления на мир науки и техники и долгое время оставалась незамеченной. Потребовалось 14 лет и огромные усилия команды исследователей из Торонтского университета, чтобы донести до общества всю ценность открытий Яна ЛеКуна.

Все изменилось в 2012 году, когда состоялись соревнования по компьютерному зрению на основе базы данных ImageNet. Алекс Крижевский и его команда разработали сверточную нейронную сеть, которая способна классифицировать миллионы изображений из тысяч различных категорий с ошибкой всего в 15.8% [2].

В наше время сверточные сети развились до такого уровня, что они превосходят человеческие способности! [3] Взгляните на статистику на рис. 1.

Рис. 1. Статистика ошибок, совершенных в процессе анализа данных ImageNet за 2010—2015. Источник.

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

Ричард Фейнман как-то отметил: “Я не могу понять то, что не могу построить”. Вот я и “построил” с нуля сверточную нейронную сеть на Python, чтобы самостоятельно “прощупать” все интересующие меня моменты. Как только я закончил с программированием, стало ясно, что нейронные сети не настолько и сложные, как это кажется на первый взгляд. Вы сами в этом убедитесь, когда пройдете со мной этот путь от начала и до конца.

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

Задача

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

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

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

Рис. 2. Образец цифр из базы данных MNIST. Источник.

Давайте по отдельности рассмотрим компоненты, формирующие сверточную нейронную сеть, и объединим эти знания, чтобы в конечном итоге понять, каким образом формируются предсказания. После рассмотрения каждого компонента мы запрограммируем нейронную сеть на Python с использованием библиотеки NumPy и обучим ее. (Готовый код можно найти тут.)

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

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

Если вы готовы, давайте двигаться дальше.

Как обучаются сверточные нейронные сети

Свертки

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

Рис. 3. Операция свертки. Источник.

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

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

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

Рис. 4. Фильтр “ищет” левосторонние кривые. Результат положительный. Источник.
Рис. 5. Фильтр “ищет” левосторонние кривые. Результат отрицательный. Источник.

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

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

Следует обратить внимание на то, что количество каналов фильтра должно соответствовать количеству каналов исходного изображения; только тогда операция свертки будет производить должный эффект. Например, если исходная картинка состоит из трех каналов (RGB: Red, Green, Blue), фильтр также должен иметь три канала.

Рис. 6. Процесс свертки двумерной матрицы. Источник.

Фильтр может перемещаться вдоль матрицы входных сигналов с шагом, отличным от единицы. Шаг перемещения фильтра называется страйдом (stride). Страйд определяет, на какое количество пикселов должен сместиться фильтр за один присест.

Ф-ла 1. Позволяет рассчитать кол-во выходных значений после операции свертки.

Количество выходных значений после операции свертки может быть рассчитано по формуле 1. Где n_in — кол-во входных пикселов, f — кол-во пикселов в фильтре, s — страйд. Для примера на рис. 6 даную формулу следует применить таким образом: (25-9)/2+1=3.

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

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

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

Часто в качестве функции активации используют ReLU (Rectified Linear Unit). Ее график изображен на рис. 7.

Рис. 7. Функция ReLU. Источник.

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

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

Программирование сверток

Благодаря библиотеке NumPy программирование сверток не составит большого труда.

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

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

Сниппет 1. Операция свертки.

Значение filt инициализировано с помощью нормального распределения. Смещение bias — вектор, заполненный нулями.

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

Даунсемплинг

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

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

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

Рис. 8.Операция максимального объединения. Источник.

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

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

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

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

Программирование “максимального объединения”

Реализация операции максимального объединения сводится к применению цикла for на самом верхнем уровне, в рамках которого будут присутствовать два цикла while. Цикл for нужен для того, чтобы перебрать все каналы изображения. Задача же циклов while заключается в перемещении “просеивающего окна” вдоль изображения. На каждом этапе отбора пиксела захваченного фрагмента матрицы будет применена функция max, входящая в состав библиотеки NumPy с целью поиска максимального значения.

Сниппет 2. Функция для выполнения “максимального объединения”.

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

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

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

Рис. 9. Развертывание двумерной матрицы в вектор. Источник.

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

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

Рис. 10. Взаимодействие сверточных слоев с многослойным перцептроном. Источник.

Стоит отметить, что Ян ЛеКун в этом посте на Фейсбуке сказал, что “в сверточных нейронных сетях нет такого понятия, как полносвязный слой”. И он совершенно прав! Если внимательно присмотреться, то станет совершенно очевидно, что принцип работы полносвязного слоя аналогичен тому, что имеет место в сверточном слое с кернелом размерностью 1×1. То есть, если в нашем распоряжении 128 фильтров размерностью n×n, которые будут взаимодействовать с изображением размерностью n×n, на выходе мы получим вектор, в котором будет 128 элементов.

Программирование полносвязного слоя

P.S. В оригинальной статье существуют попытки разделения понятий “fully connected layer” и “dense layer”. В действительности же данные понятия являются синонимами. Хорошее объяснения этих (и некоторых других) “скользких” понятий приведено на этом форуме. (Простите, но там все на английском.)

Благодаря NumPy программирование полносвязного слоя — задача чересчур простая. Как видно из нижеприведенного сниппета, будет достаточно всего нескольких строк кода. (Обратите внимание на метод reshape; он всегда облегчает жизнь программистам нейронных сетей.)

Сниппет 3. “Разворачивание” матрицы в вектор.

Развернутые матрицы далее пропускаются через полносвязный слой. Данная конструкция имеет огромное количество связей. Например, если в полносвязный слой, состоящий из 100 нейронов, входит 500 сигналов, количество связей будет равно 100×500=50000. В сниппете 4 перцептрон, являющийся полносвязной нейронной сетью, состоит из двух слоев.

Сниппет 4. Реализация полносвязного слоя.

Выходной слой

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

Рис. 11. Функции активации. Источник.
Формула 3. Вычисление функции softmax.

Программирование функции активации

Как и всегда, благодаря NumPy задача будет решена в несколько строчек кода.

Сниппет 5. Активация сигналов с помощью функции softmax.

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

Чтобы определить, насколько точно нейронная сеть определяет написанные от руки цифры, мы будем использовать функцию потерь. Ответом функции потерь является вещественное число, характеризующие качество ответа нейронной сети. Обычно для оценки качества классификаторов применяют категориальную крос-энтропийную функцию потерь (Categorical Cross-Entropy Loss Function, или CCELF).

Формула 4. Категориальная крос-энтропийная функция потерь.

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

Программирование функции потерь

Код реализации CCELF невероятно прост.

Сниппет 6. Категориальная крос-энтропийная функция потерь.

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

Нейронная сеть

В нашем распоряжении относительно небольшое количество классов (10, если быть точным). Размер изображений в обучаемом множестве составляет 28x28 пикселов. По этим причинам архитектура нейронной сети будет довольно простой:

  1. два сверточных слоя и один слой максимального объединения (max pooling layer) для извлечения характеристик исходного изображения;
  2. многослойный перцептрон (Multi-Layer Perceptron, MLP), задачей которого является классификация полученных ранее характеристик изображения.
Рис. 12. Архитектура нейронной сети.

Программирование нейронной сети

Давайте следовать представленной выше архитектуре и воплощать ее в жизнь слой за слоем.

Вы также можете воспользоваться готовым кодом из моего репозитория.

Шаг 1. Извлечение данных

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

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

Сниппет 7. Извлечение тренировочных данных и соответствующих им маркеров.

Шаг 2. Инициализация параметров

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

Сниппет 8. Функции инициализации параметров нейронной сети.

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

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

Сниппет 9. Функция обратного распространения градиента через сверточный и пулинговый слои.

Шаг 4. Построение сети

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

Сниппет 10. Прямое и обратное распространение сигналов в сверточных слоях.

Шаг 5. Обучение нейронной сети

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

P.S. На хабре есть классная статья по алгоритмам оптимизации.

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

Полное описание алгоритма оптимизации Adam вы сможете найти тут.

Сниппет 11. Программная реализация алгоритма оптимизации Adam.

Вот и все! Если хотите проверить результат прямо сейчас, клонируйте этот репозиторий и запустите в терминале такую команду:

$ python3 train_cnn.py '<file_name>.pkl'

Эта команда запустит процесс обучения нейронной сети, в результате которого вы получите файл <file_name>.pkl с обученными параметрами. Не забудьте заменить <file_name> на удобное для вас имя файла.

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

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

На моем MacBook Pro обучение нейронной сети заняло примерно 5 часов. Обученные параметры уже есть на GitHub под именем params.pkl.

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

python3 measure_performance.py '<file_name>.pkl'

Вместо <file_name> укажите имя файла с параметрами обученной нейронной сети. Если хотите использовать полученные мною параметры, укажите имя params.

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

Рис. 14. Оценка эффективности нейронной сети.

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

pip install -r requirements.txt

Результаты

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

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

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

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

Вот вам наглядный пример:

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

График на рис. 16 показывает характеристику памяти нейронной сети для каждой цифры.

Рис. 16. Характеристика памяти нейронной сети для классов входных образов.

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

Заключение

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

Ссылки

[1]: Lecun, Y., et al. “Gradient-Based Learning Applied to Document Recognition.” Proceedings of the IEEE, vol. 86, no. 11, 1998, pp. 2278–2324., doi:10.1109/5.726791.

[2]: Krizhevsky, Alex, et al. “ImageNet Classification with Deep Convolutional Neural Networks.” Communications of the ACM, vol. 60, no. 6, 2017, pp. 84–90., doi:10.1145/3065386.

[3]: He, Kaiming, et al. “Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification.” 2015 IEEE International Conference on Computer Vision (ICCV), 2015, doi:10.1109/iccv.2015.123.

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

--

--

Bohdan Balov 🇺🇦

Lead Software Engineer at EPAM Systems | Mentor | Writer | Crazy Runner from Brave Ukraine