Как стать автором
Обновить
528.28
Open Data Science
Крупнейшее русскоязычное Data Science сообщество

Сверточная сеть на python. Часть 3. Применение модели

Время на прочтение7 мин
Количество просмотров33K

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

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

Прямое прохождение через сеть

1) Первый слой сверточной сети
2) Слой макспулинга
3) Второй слой сверточной сети
4) Сложение всех карт признаков в один вектор (это не совсем “полноценный” слой, но все-таки занимает здесь важное место)
5) Первый слой fc-сети
6) Второй слой fc-сети
7) Вычисление значение loss-функции

Обратное прохождение через сеть и обновление параметров (проходим через все слои в обратном порядке)

8) Backprop через loss
9) Второй слой fc-сети
10) Первый слой fc-сети
11) Разворачивание карт признаков из вектора (также не является “полноценным” слоем)
12) Второй слой сверточной сети
13) Слой макспулинга
14) Первый слой сверточной сети

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

numpy_model.py — здесь хранятся все основные функции, из которых составлена сеть. Многие из функций мы уже рассмотрели в прошлых статьях.

train_numpy_model.py — сама сеть и входные параметры для нее. Сеть строится из функций, определенных в model.py, и выглядит буквально таким же образом, как 14 пунктов, которые мы определили в начале статьи:



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

На pytorch написать аналогичную сеть было, конечно, значительно менее трудозатрано:
train_torch_model.py

Чтобы убедиться, что результаты обеих моделей совпадают, нужно изначально убедиться, что их стартовые веса идентичны. Я придумал такую хитрость: создать веса для данной модели pytorch, сохранить их, и далее использовать как стартовые веса для обоих моделей. В итоге, такой скрипт выглядит таким образом:
make_init_weights.py

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

Следующее: карты признаков рассматриваются как каналы одного, так сказать, изображения, а не как независимые друг от друга изображения. Изначально я предполагал, что, если мы хотим на выходе четыре feature maps, то и ядра должно быть четыре, но выходит все немного по-другому. Так как все карты признаков предыдущего слоя рассматриваются как одно изображение со множеством каналов, то для получения только одной карты на следующем слое нужно создать столько матриц весов, сколько каналов на предыдущем слое (соответсвтенно, для двух карт — вдвое больше матриц весов). Затем получить соответствующее число “промежуточных” карт и сложить их в одну — это и будет искомый один feature map. Вот, скажем, если на предыдущем слое получилось две карты, и мы на следующем слое хотим получить четыре, то выглядеть это будет примерно так:


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


Здесь мы из одной RGB картинки хотим получить две feature maps. И еще отмечу: при обратном распространении ошибки те “промежуточные” карты вообще никак не задействуются: в понимании программы их вообще не существует. Каждая матрица весов и карта признаков из предыдущего слоя завязаны на соответствующие им “финальные” карты следующего слоя.

Следующей особенностью вычислений pytorch было то, что внутри функции torch.nn.Conv2d происходит на самом деле кросс-корреляция и об это даже написано в документации pytorch:
is the valid 2D cross-correlation operator

Но это не стало большой проблемой, так как в нашей реализации на numpy достаточно поменять True на False. Вот пример кода, с помощью которого можно убедиться, что используется именно кросс-корреляция:
compare_conv_on_torch_and_numpy_model.ipynb

Также здесь можно попробовать разные параметры stride, padding и kernel size и посмотреть на результаты numpy-конволюции и torch.

Итак, наконец мы разобрали все те нюансы, что приводили к отличиям результатов, учли все это в модели from scratch и можем сравнить лоссы обеих сетей: первой, реализованной с помощью numpy, и второй — библиотеки на pytorch.

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

Итак, попробуем запустить обе модели и сравнить результаты: loss и accuracy, усредненных по каждым 1000 изображениям:


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

По прошествии одной эпохи обучения (то есть 60 тысяч итераций прямого и обратного прохождения сети), получились такие графики для loss и accuracy:


И на тестовой выборке accuracy составляет 89%

Как можно видеть, loss для сборной numpy-модели на тестовой и обучающей выборках очень похожи на результаты сети на pytorch. Отличие же в accuracy на тестовой выборке по прошествии одной эпохи обучения составляет всего одно изображение на десять тысяч! Но все же сами матрицы весов отличаются в тысячных знаках (тогда как на первых этапах обучения не было никакой разницы) — различие, скорее всего, обусловлено разной точностью расчетов в numpy и pytorch, либо другими неучтенными нюансами, которые вносят незначительные отличия в расчетах моделей.

Accuracy 89% было достигнуто всего после прохождения одной эпохи. Но после уже четырех эпох на этой же модели результаты на тестовой выборке были бы значительно выше — 96%. Но попробовал я это только на pytorch, на numpy-модели дальнейшее обучение заняло было слишком много времени, но в целом можно заключить, что подобранная архитектура не так плоха, как могла бы быть!

Если быть точным, одна эпоха на numpy-модели заняла примерно 9 часов. На модели, собранной с помощью pytorch, эта же эпоха проходит всего за пару минут. Разница колоссальна! На что же уходит все время в собранной вручную модели и где узкое место? Чтобы понять это, достаточно взглянуть на график времени, которое занимает каждая функция в numpy-модели (суммарно для прямого и обратного прохождений по сети):


Как видно, почти все 9 часов обучаются несколько ядер свертки. Именно поэтому в параметрах модели у меня так мало карт признаков на выходе сверточных слоев. На полносвязные слои почти не уходит времени (особенно на второй fc-слой с меньшим количеством параметров), так как внутри происходит оптимизированное перемножение numpy-матриц, тогда как сверточные функции писались исходя из формул и буквального “движения” ядра по матрице. Вычисления pytorch гораздо оптимизированнее и конволюционные слои также представлены в виде перемножения матриц. Здесь можно прочесть подробнее.

Конечно, numpy-модель не годится для настоящих расчетов и использовать следует pytorch или другие библиотеки для машинного обучения. Я не могу сказать, что совершенно уверен в отсутствии ошибок в реализации на python, что учтены все тонкости и нюансы обучения, которые абсолютно точно есть в pytorch. Но главное — это время обучения. То, что на python занимает два часа, на pytorch — всего минуту.

Для улучшения же полученных на MNIST результатов нужно использовать больше конволюционных слоев, генерировать больше feature maps для адекватного извлечения сетью признаков из изображений (все это, конечно, серьезно отразится на времени обучения, если использовать модель на numpy). Также следует использовать батч больше, чем в одно изображение, классический метод оптимизации SGD заменить на, например, Adam (о которых можно почитать здесь или здесь). Полученные нами в статье результаты, конечно, не впечатляющие (лидерборд можно увидеть здесь), зато видно, что модель, написанная буквально согласно формулам, действительно учится, работает. Если вы хотите воспроизвести результаты, можете попробовать запустить обучение модели самостоятельно, используя код к статье.

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

UPD 2021
Отрефакторил код к статье и сделал его более понятным и доступным для воспроизведения (добавил readme и запуск через dockerfile), поправил незначительные баги. Также сейчас результаты сравниваются с pytorch-библотекой (ранее было с tensorflow).
Теги:
Хабы:
Всего голосов 62: ↑61 и ↓1+60
Комментарии8

Публикации

Информация

Сайт
ods.ai
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия

Истории