VGG16 — сверточная сеть для выделения признаков изображений

23 ноября 2018
vgg16 нейронная сеть

VGG16 — сверточная сеть для выделения признаков изображений

VGG16 — модель сверточной нейронной сети, предложенная K. Simonyan и A. Zisserman из Оксфордского университета в статье “Very Deep Convolutional Networks for Large-Scale Image Recognition”. Модель достигает точности 92.7% —…

VGG16 — модель сверточной нейронной сети, предложенная K. Simonyan и A. Zisserman из Оксфордского университета в статье “Very Deep Convolutional Networks for Large-Scale Image Recognition”. Модель достигает точности 92.7% — топ-5, при тестировании на ImageNet в задаче распознавания объектов на изображении. Этот датасет состоит из более чем 14 миллионов изображений, принадлежащих к 1000 классам.

VGG16 — одна из самых знаменитых моделей, отправленных на соревнование ILSVRC-2014. Она является улучшенной версией AlexNet, в которой заменены большие фильтры (размера 11 и 5 в первом и втором сверточном слое, соответственно) на несколько фильтров размера 3х3, следующих один за другим. Сеть VGG16 обучалась на протяжении нескольких недель при использовании видеокарт NVIDIA TITAN BLACK.

слои vgg16

Датасет

ImageNet — набор данных, состоящий из более чем 15 миллионов размеченных высококачественных изображений, разделенных на 22000 категорий. Изображения были взяты из интернета и размечены вручную людьми-разметчиками с помощью краудсорсинговой площадки Mechanical Turk от Amazon.

В 2010 году, как часть Pascal Visual Object Challenge, началось ежегодное соревнование — ImageNet Large-Scale Visual Recognition Challenge (ILSVRC). В ILSVRC используется подвыборка из ImageNet размером 1000 изображений в каждой из 1000 категорий. Таким образом, тренировочный сет состоял из примерно 1.2 миллионов изображений, проверочный — 50000 изображений, тестовый — 150000 изображений. Так как ImageNet состоит из изображений разного размера, то их необходимо было привести к единому размеру 256х256. Если изображение представляет из себя прямоугольник, то оно масштабируется и из него вырезается центральная часть размером 256х256.

Архитектура

Архитектура VGG16 представлена на рисунке ниже.

Архитектура нейросети vgg16

На вход слоя conv1 подаются RGB изображения размера 224х224. Далее изображения проходят через стек сверточных слоев, в которых используются фильтры с очень маленьким рецептивным полем размера 3х3 (который является наименьшим размером для получения представления о том,где находится право/лево, верх/низ, центр).

В одной из конфигураций используется сверточный фильтр размера 1х1, который может быть представлен как линейная трансформация входных каналов (с последующей нелинейностью). Сверточный шаг фиксируется на значении 1 пиксель. Пространственное дополнение (padding) входа сверточного слоя выбирается таким образом, чтобы пространственное разрешение сохранялось после свертки, то есть дополнение равно 1 для 3х3 сверточных слоев. Пространственный пулинг осуществляется при помощи пяти max-pooling слоев, которые следуют за одним из сверточных слоев (не все сверточные слои имеют последующие max-pooling). Операция max-pooling выполняется на окне размера 2х2 пикселей с шагом 2.

После стека сверточных слоев (который имеет разную глубину в разных архитектурах) идут три полносвязных слоя: первые два имеют по 4096 каналов, третий — 1000 каналов (так как в соревновании ILSVRC требуется классифицировать объекты по 1000 категориям; следовательно, классу соответствует один канал). Последним идет soft-max слой. Конфигурация полносвязных слоев одна и та же во всех нейросетях.

Все скрытые слои снабжены ReLU. Отметим также, что сети (за исключением одной) не содержат слоя нормализации (Local Response Normalisation), так как нормализация не улучшает результата на датасете ILSVRC, а ведет к увеличению потребления памяти и времени исполнения кода.

Конфигурация

Конфигурации сверточных сетей представлены на рисунке 2. Каждая сеть соответствует своему имени (A-E). Все конфигурации имеют общую конструкцию, представленную в архитектуре, и различаются только глубиной: от 11 слоев с весами в сети A (8 сверточных и 3 полносвязных слоя) до 19 (16 сверточных и 3 полносвязных слоя). Ширина сверточных слоев (количество каналов) относительно небольшая: от 64 в первом слое до 512 в последнем с увеличением количества каналов в 2 раза после каждого max-pooling слоя.

vgg16
Рисунок 2

Реализация

К сожалению, сеть VGG имеет два серьезных недостатка:

  1. Очень медленная скорость обучения.
  2. Сама архитектура сети весит слишком много (появляются проблемы с диском и пропускной способностью)

Из-за глубины и количества полносвязных узлов, VGG16 весит более 533 МБ. Это делает процесс развертывания VGG утомительной задачей. Хотя VGG16 и используется для решения многих проблем классификации при помощи нейронных сетей, меньшие архитектуры более предпочтительны (SqueezeNet, GoogLeNet и другие). Несмотря на недостатки, данная архитектура является отличным строительным блоком для обучения, так как её легко реализовать.

[Pytorch]

[Tensorflow]

[Keras]

Результаты

VGG16 существенно превосходит в производительности прошлые поколения моделей в соревнованиях ILSVRC-2012 and ILSVRC-2013. Достигнутый VGG16 результат сопоставим с победителем соревнования по классификации (GoogLeNet с ошибкой 6.7%) в 2014 году и значительно опережает результат Clarifai победителя ILSVRC-2013, который показал ошибку 11.2% с внешними тренировочными данными и 11.7% без них. Что касается одной сети, архитектура VGG16 достигает наилучшего результата (7.0% ошибки на тесте), опережаю одну сеть GoogLeNet на 0.9%.

Было показано, что глубина представления положительно влияет на точность классификации, и state-of-the-art результат на соревновательном датасете ImageNet может быть достигнут с помощью обычной сверточной нейронной сети с значительно большей глубиной.

Сверточная нейронная сеть на PyTorch: пошаговое руководство

26 октября 2018

Сверточная нейронная сеть на PyTorch: пошаговое руководство

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

В предыдущем вводном туториале по нейронным сетям была создана трехслойная архитектура для классификации рукописных символов датасета MNIST. В конце туториала была показана точность приблизительно 86%. Для простого датасета, как MNIST, это плохое качество. Дальнейшая оптимизация смогла улучшить результат плотно соединенной сети до 97-98% точности. Это уже намного лучше, но всё еще не достаточно для MNIST. Требуется более современный метод, который может действительно называться глубоким обучением. В данном туториале представлен такой метод — сверточная нейронная сеть (Convolutional Neural Network, CNN), который достигает высоких результатов в задачах классификации картинок. В частности, будет рассмотрена и теория, и практика реализации CNN при помощи PyTorch.

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

Особенности CNN

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

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

Принцип работы CNN

Свертка — фактически главное, что необходимо понять о сверточных нейронных сетях. Этот замысловатый математический термин нужен для движущегося окна или фильтра по исследуемому изображению. Перемещающееся окно применяется к определенному участку узлов, как показано ниже. Где примененный фильтр — ( 0.5 * значение в узле):

 

 

 

 

 

 

 

На диаграмме показаны только два выходных значения, каждое из которых отображает входной квадрат размера 2×2. Вес отображения для каждого входного квадрата, как ранее упоминалось, равен 0.5 для всех четырех входов (inputs). Поэтому выход может быть посчитан так:

 

 

 

 

 

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

 

 

 

 

 

 

 

 

 

 

 

Первое положение связей движущегося фильтра показано синей линией, второе — зеленой. Веса для каждых таких соединений равны 0.5.

Вот несколько вещей в сверточном шаге, которые ускоряют процесс тренировки, сокращая количество параметров, весов:

  • Редкие связи — не каждый узел в первом (входном) слое соединен с каждым узлом во втором слое. Этим отличается архитектура CNN от полностью связанной нейронной сети, где каждый узел соединен со всем другими в следующем слое.
  • Постоянные параметры фильтра. Другими словами, при движении фильтра по изображению одинаковые веса применяются для каждого 2 х 2 набора узлов. Каждый фильтр может быть обучен для выполнения специфичных трансформаций входного пространства. Следовательно, каждый фильтр имеет определенный набор весов, которые применяются для каждой операции свертки. Этот процесс уменьшает количество параметров. Нельзя говорить, что любой вес постоянен внутри отдельного фильтра. В примере выше веса были [0.5, 0.5, 0.5, 0.5], но ничего не мешало им быть и [0.25, 0.1, 0.8, 0.001]. Выбор конкретных значений зависит от обучения каждого фильтра.

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

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

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

Отображение признаков и мультиканальность

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

 

 

 

 

 

 

 

Теперь в правой части рисунка можно видеть несколько сложенных (stacked) выходов операции свертки. Их несколько, потому что существует несколько обучаемых фильтров, каждый из которых производит собственный 2D выход (в случае 2D изображения). Такое множество фильтров часто в глубоком обучении часто называют каналами. Каждый канал должен обучаться для выделения на изображении определенного ключевого признака. Выход сверточного слоя для черно-белого изображения, как в датасете MNIST, имеет 3 измерения — 2D для каждого из каналов и еще одно для их числа.

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

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

Объединение (Pooling)

Основными преимуществами для пулинга в сверточной нейронной сети являются:

  • Уменьшение количества параметров в вашей модели благодаря процессу даунсемплинга (down-sampling).
  • Детектирование признаков становится более правильным при изменении ориентации или размера объекта.

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

 

 

 

 

 

 

 

Давайте пройдемся по некоторым пунктам, связанным с диаграммой:

Основы

На диаграмме можно наблюдать действие max pooling. Для первого окна голубого цвета max pooling выдает значение 3.0, которое является максимальным значением узла в 2х2 окне. Таким же образом зеленое окно выводит максимальное значение, равно 5.0, а для красного окна максимальное значение — 7.0. Здесь всё просто и понятно.

Шаги и даунсемплинг

На диаграмме сверху можно заметить, что пулинговое окно каждый раз перемещается на 2 места. Можем говорить, что шаг равен 2. На диаграмме показаны шаги только вдоль оси x,  но для задачи предотвращения перекрытия окна, шаг должен быть также равен 2 и в направлении y. Другими словами, шаг обозначается как [2,2]. Следует упомянуть, если во время пулинга шаг больше 1, тогда размер выхода будет уменьшен. Как можно видеть на диаграмме, входной объект размера 5×5 уменьшается до 3х3 на выходе. И это хорошо — такое явление называется даунсемплингом и уменьшает количество обучаемых параметров в модели.

Padding

Важно отметить также, что в пулинговой диаграмме есть дополнительный столбец и строка, добавленные к входу размера 5х5, делающие эффективный размер пулингового пространства равным 6х6. Это делается для того, чтобы пулинговое окно размером 2х2 корректно работало с шагом [2,2]. Такой прием называется padding. Padding-узлы зачастую фиктивные, так как значения на них равны 0, и операция max pooling их не видит. Этот факт нужно будет учитывать при создании нашей сверточной сети на PyTorch.

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

Использование пулинга в сверточных нейронных сетях

В дополнении к функции даунсемплинга пулинг используется в CNN, чтобы детектировать определенные признаки, инвариантные к изменениям размера или ориентации. Другой способ представить действие пулинга  — он обобщает низкоуровневую, сложно структурированную информацию. Представим случай, когда у нас есть сверточный фильтр, который во время тренировки обучается распознавать знак «9» в различных положениях на входном изображении. Чтобы сверточная сеть научилась корректно классифицировать появление «9» на картинке, требуется каким-то образом активировать сеть каждый раз, когда эта цифра появляется на изображении независимо от размера и ориентации (кроме случая, когда «9» напоминает «9»). Пулинг может помочь в такой задаче выбора высокоуровневых, обобщенных признаков. Этот процесс иллюстрирован ниже:

 

 

 

 

 

 

 

Диаграмма — стилизованное представление операции пулинга. Если мы считаем, что маленький участок входного изображения содержит цифру 9 (зеленый квадрат), и предполагаем, что пытаемся детектировать эту цифру на изображении. В таком случае несколько сверточных фильтров обучаются активироваться с помощью ReLU функции каждый раз, когда они видят «9» на картинке. Однако, они будут активироваться более или менее сильно в зависимости от того, как она расположена. Мы хотим научить сеть обнаруживать цифру на изображении независимо от ее ориентации. Здесь наступает черед пулинга. Он«смотрит» на выходы трех фильтров и берет настолько высокое значение, насколько высокий порог функций активации этих фильтров.

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

Общий взгляд

Ниже представлено изображение из Википедии, которое показывает структуру полностью разработанной сверточной нейронной сети:

 

 

 

 

 

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

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

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

Рассматривая предыдущую диаграмму, на выходе имеем несколько каналов x x y тензоров/матриц. Эти каналы необходимо привести к одному (N x 1) тензору. Возьмем пример: имеем 100 каналов с 2х2 матрицами, отображающими выход последней пулинг операции в сети. В PyTorch можно легко осуществить преобразование в 2х2х100 = 400 строк, как будет показано ниже.

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

Реализация CNN на PyTorch

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

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

 

 

 

 

 

 

В самом начале на вход подаются черно-белые представления символов размером 28х28 пикселей каждое. Первый слой состоит из 32 каналов сверточных фильтров размера 5х5 + активационная функция ReLU, затем идет 2х2 max pooling с даунсемплингом с шагом 2 (этот слой выводит данные размером 14х14). На следующий слой подается выход с первого слоя размера 14х14, который сканируется снова 5х5 сверточными фильтрами с 64 каналов, затем следует 2х2 max pooling с даунсемплингом для генерирования выхода размером 7х7.

После сверточной части сети следует:

  • операция выравнивания, которая создает 7х7х64=3164 узлов
  • средний слой из 1000 полносвязных улов
  • операция softmax над крайними 10 узлами для генерирования вероятностей классов.

Эти слои представлены в выходном классификаторе.

Загрузка датасета

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

num_epochs = 5 
num_classes = 10 
batch_size = 100 
learning_rate = 0.001
DATA_PATH = 'C:\\Users\Andy\PycharmProjects\MNISTData'
MODEL_STORE_PATH = 'C:\\Users\Andy\PycharmProjects\pytorch_models\\'

Прежде всего, устанавливаем тренировочные гиперпараметры. Далее идет спецификация пути папки для хранения датасета MNIST (PyTorch автоматически загрузит датасет в эту папку) и локации для тренировочных параметров модели после того, как обучение будет завершено.

Далее задаем преобразование для применения к MNIST и переменные для данных:

trans = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]) 

train_dataset = torchvision.datasets.MNIST(root=DATA_PATH, train=True, transform=trans, download=True) 
test_dataset = torchvision.datasets.MNIST(root=DATA_PATH, train=False, transform=trans)

Первое, на что необходимо обратить внимание в коде сверху, это функция transform.Compose(). Функция находится в  пакете torchvision. Она позволяет разработчику делать различные манипуляции с указанным датасетом. Численные трансформации могут быть соединены вместе в список при использовании функции Compose(). В этом случае сначала устанавливается преобразование, которое конвертирует входной датасет в PyTorch тензор. PyTorch тензор — особый тип данных, используемый в библиотеке для всех различных операций с данными и весами внутри нейросети. По сути, такой тензор — простая многомерная матрица. Но в любом случае, PyTorch требует преобразования датасета в тензор таким образом, что его можно использовать для тренировки и тестирования сети.

Следующий аргумент в списке Compose() — нормализация. Нейронная сеть обучается лучше, когда входные данные нормализованы так, что их значения находятся в диапазоне от -1 до 1 или от 0 до 1. Чтобы это сделать с помощью нормализации PyTorch, необходимо указать среднее и стандартное отклонение MNIST датасета, которые в этом случае равны 0.1307 и 0.3081 соответственно. Отметим, среднее значение и стандартное отклонение должны быть установлены для каждого входного канала. У MNIST есть только один канал, но уже для датасета CIFAR c 3 каналами (по одному на каждый цвет из RGB спектра) надо указывать среднее и стандартное отклонение для каждого.

Далее необходимо создать объекты  train_dataset и test_dataset, которые будут последовательно проходить через загрузчик данных. Чтобы создать такие датасеты из данных MNIST, требуется задать несколько аргументов. Первый — путь до папки, где хранится файл с данными для тренировки и тестирования. Логический аргумент train показывает, какой файл из train.pt или  test.pt стоит брать в качестве тренировочного сета. Следующий аргумент — transform, в котором мы указываем ранее созданный объект trans, который осуществляет преобразования. Наконец, аргумент загрузки просит функцию датасета MNIST загрузить при необходимости данные из онлайн источника.

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

train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size,shuffle=True) 
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)

Объект загрузчик данных в PyTorch обеспечивает несколько полезных функций при использовании тренировочных данных:

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

Как можно видеть, требуется задать три простых аргумента. Первый — данные, которые вы хотите загрузить; второй — желаемый размер партии; третий — перемешивать ли случайным образом датасет. Загрузчик может использоваться в качестве итератора. Поэтому для извлечения данных мы можем просто воспользоваться стандартным итератором Python, например, перечислением. Такая возможность будет позже продемонстрирована на практике в туториале.

Создание модели

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

class ConvNet(nn.Module): 
     def __init__(self): 
         super(ConvNet, self).__init__() 
         self.layer1 = nn.Sequential( nn.Conv2d(1, 32, kernel_size=5, stride=1, padding=2), 
            nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2)) 
         self.layer2 = nn.Sequential( nn.Conv2d(32, 64, kernel_size=5, stride=1, padding=2), 
            nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2)) 
         self.drop_out = nn.Dropout() 
         self.fc1 = nn.Linear(7 * 7 * 64, 1000) 
         self.fc2 = nn.Linear(1000, 10)

Это то место, где определяется модель. Наиболее простой способ создания структуры нейронной сети в PyTorch — создание наследственного класса от материнского nn.Module. Класс nn.Module очень полезен в PyTorch, он содержит всё необходимое для конструирования типичной глубокой нейронной сети. Кроме этого, класс имеет удобные функции: способы перемещения переменных и операций на GPU или обратно на CPU, применение рекурсивных функций через все свойства в классе (например, перенастройка всех весовых переменных), создание оптимизированных интерфейсов для тренировки и тому подобное. Все доступные методы можно посмотреть здесь.

Первый шаг — создание нескольких объектов последовательного слоя с помощью функции class _init_. Сначала создаем слой 1 (self.layer1) через создание объекта nn.Sequential. Этот метод позволяет создавать упорядоченные слои в сети и является удобным способом создания последовательности свертка + ReLu + пулинг. Как можно видеть, первый элемент в определении этой последовательности — метод Conv2d, который создает набор сверточных фильтров. В этом методе первый аргумент — количество входных каналов; в нашем случае изображения MNIST черно-белые и количество каналов равно 1. Второй аргумент в Conv2d — количество выходных каналов; как показано на диаграмме архитектуры модели, первый слой сверточных фильтров состоит из 32 каналов, поэтому значение второго аргумента равно 32.

Аргумент kernel_size отвечает за размер сверточного фильтра, в нашем случае мы хотим фильтр размеров 5х5, поэтому аргумент равен 5. Если бы мы хотели фильтры с разными размерными формами по оси х и y, необходимо было указать параметры в виде python tuple (размер по х, размер по y). Наконец, мы хотим установить padding, что делается несколько сложнее. Размер измерения на выходе от операции  сверточной фильтрации или пулинга может быть посчитан с помощью уравнения:

 

 

Где W(in) — ширина выхода, F — размер фильтра, P — паддинг, S — шаг. Такая же формула применима для расчета высоты.  В нашем случае изображение и фильтрация симметричны, поэтому формула применима и к ширине и к высоте. Если хотим оставить входные и выходные измерения без изменений с размером фильтра 5 и шагом 1, то исходя из верхней формулы паддинг нужно задать равным 2. Таким образом, аргумент для паддинга в Conv2d равен 2.

Следующий аргумент в последовательности — простая ReLU функция активации. Последний элемент в последовательном определении для self.layer1 — операция max pooling. Первый аргумент — размер объединения, который равен 2х2; следовательно, аргумент равен 2. Во втором аргументе мы хотим взять подвыборку из данных через уменьшение эффективного размера изображения с факторов 2. Чтобы это сделать используя формулу выше, устанавливаем шаг равным 2 и паддинг равным 0. Следовательно, шаговый аргумент равен 2. Паддинг аргумент по умолчанию равен 0, если не указано другое значение, что сделано в коде сверху. Из этих вычислений теперь понятно, что выход слоя self.layer1 имеет 32 канала и “изображения” размером 14х14.

Второй слой, self.layer2, определяется таким же образом, как и первый. Единственное отличие здесь — вход в функцию Conv2d теперь 32 канальный, а выход — 64 канальный. Следуя такой же логике и учитывая пулинг и даунсемплинг, выход из self.layer2 представляет из себя 64 канала изображения размера 7х7.

Далее определяем отсеивающий слой для предотвращения переобучения модели. В конце создаем два полносвязных слоя. Первый слой имеет размер 7х7х64 узла и соединяется со вторым слоем с 1000 узлами. Чтобы создать полносвязный слой в PyTorch, используем метод nn.Linear. Первый аргумент метода — число узлов в данном слое, второй аргумент — число узлов в следующем слое.

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

def forward(self, x): 
     out = self.layer1(x) 
     out = self.layer2(out) 
     out = out.reshape(out.size(0), -1) 
     out = self.drop_out(out) 
     out = self.fc1(out) 
     out = self.fc2(out) 
     return out

Здесь важно назвать функцию “forward”, так как она будет использовать базовую функцию прямого распространения в nn.Module и позволит всему функционалу nn.Module работать корректно. Как можно видеть, функция принимает на вход аргумент х, представляющий собой данные, которые должны проходить через модель (например, партия данных). Направляем эти данные в первый слой (self.layer1) и возвращаем выходные данные как out. Эти выходные данные поступают на следующий слой и так далее. Отметим, после self.layer2 применяем функцию преобразования формы к out, которая разглаживает размеры данных с 7х7х64 до 3164х1. После двух полносвязных слоев применяется dropout-слой и из функции возвращается финальное предсказание.

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

Обучение модели

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

model = ConvNet()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

Экземпляр класса ConvNet() создается под названием  model. Далее определяем операцию над потерями, которая будет использоваться для подсчета потерь. В нашем случае используем доступную в PyTorch функцию CrossEntropyLoss(). Вы уже могли заметить, что мы еще не задали активационную функцию SoftMax для последнего слоя, выполняющего классификацию. Это сделано потому, что функция  CrossEntropyLoss() объединяет и SoftMax и кросс-энтропийную функцию потерь в единую функцию. Далее определяем оптимизатор Adam. Первым аргументом функции являются параметры, которые мы хотим обучить оптимизатором. Это легко сделать с помощью класса nn.Module, из которого вы получается ConvNet. Всё что нужно сделать — снабдить функцию аргументом model.parameters() и PyTorch будет отслеживать все параметры нашей модели, которые необходимо обучить. Последним определяется скорость обучения.

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

total_step = len(train_loader)
loss_list = []
acc_list = []
for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):
        # Прямой запуск
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss_list.append(loss.item())

        # Обратное распространение и оптимизатор
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        # Отслеживание точности
        total = labels.size(0)
        _, predicted = torch.max(outputs.data, 1)
        correct = (predicted == labels).sum().item()
        acc_list.append(correct / total)

        if (i + 1) % 100 == 0:
            print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}, Accuracy: {:.2f}%'
                  .format(epoch + 1, num_epochs, i + 1, total_step, loss.item(),
                          (correct / total) * 100))

Самыми важными частями для начала являются два цикла. Первый цикл проходит по количеству эпох, в нём итерации проходят по train_loader используя перечисление. Во внутреннем цикле сначала считаются выходы прямого прохождения изображений (которые представляют собой партии нормализованных MNIST изображений из train_loader). Отметим, нам не требуется вызывать model.forward(images), так как nn.Module знает, что нужно вызывать forward при выполнении model(images).

Следующий шаг — отправление выходов модели и настоящих меток в функцию CrossEntropyLoss, определенную как criterion. Потери добавляются в список, который будет использован позже для отслеживания прогресса обучения. Затем — выполнение обратного распространения ошибки и оптимизированного шага тренировки. Сначала градиенты должны быть обнулены, что легко делается вызовом zero_grad() на оптимизаторе. Далее вызываем функцию .backward() на переменной loss для выполнения обратного распространения. Теперь, когда градиенты посчитаны при обратном распространении, просто вызываем optimizer.step() для выполнения шага обучения оптимизатора Adam. PyTorch делает процесс обучения модели очень легким и интуитивным.

Следующий набор строк отвечает за отслеживание точности на тренировочном сете. Предсказания модели могут быть определены с помощью функции torch.max(), которая возвращает индекс максимального значения в тензоре. Первый аргумент этой функции — тензор, который мы хотим исследовать; второй — ось, по которой определяется максимум. Выходящий из модели тензор должен иметь размер (batch_size,10). Чтобы определить предсказание модели, необходимо для каждого примера в партии найти максимальные значения из 10 выходных узлов. Каждый из этих узлов будет соответствовать одному рукописному символу (например, выход 2 равен символу «2» и так далее). Выходной узел с наибольшим значением и будет предсказанием модели. Следовательно, надо задать второй аргумент в функции torch.max() равным 1, что указывает функции максимума проверить ось выходного узла (axis = 0 соответствует размерности batch_size).

На этом шаге возвращается список целочисленных предсказаний модели, а следующая строка сравнивает эти предсказания с настоящими метками (predicted == labels) и суммирует правильные предсказания. Отметим, что на этом шаге выход функции sum() всё еще тензор, поэтому для оценки его значений требуется вызвать .item(). Делим количество правильных предсказаний на размер партии batch_size (эквивалентно labels.size(0)) для подсчета точности. Наконец, во время тренировки после каждых 100 итераций внутреннего цикла выводим прогресс.

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

Epoch [1/6], Step [100/600], Loss: 0.2183, Accuracy: 95.00%
Epoch [1/6], Step [200/600], Loss: 0.1637, Accuracy: 95.00%
Epoch [1/6], Step [300/600], Loss: 0.0848, Accuracy: 98.00%
Epoch [1/6], Step [400/600], Loss: 0.1241, Accuracy: 97.00%
Epoch [1/6], Step [500/600], Loss: 0.2433, Accuracy: 95.00%
Epoch [1/6], Step [600/600], Loss: 0.0473, Accuracy: 98.00%
Epoch [2/6], Step [100/600], Loss: 0.1195, Accuracy: 97.00%

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

Тестирование

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

model.eval()
with torch.no_grad():
    correct = 0
    total = 0
    for images, labels in test_loader:
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

    print('Test Accuracy of the model on the 10000 test images: {} %'.format((correct / total) * 100))

# Сохраняем модель и строим график
torch.save(model.state_dict(), MODEL_STORE_PATH + 'conv_net_model.ckpt')

В первой строке устанавливаем режим оценки используя model.eval(). Это удобная функция запрещает любые исключения или слои нормализации партии в модели, которые будут мешать объективной оценке. Конструкция torch.no_grad() выключает функцию автоградиента в модели, так как она не нужна при тестировании/оценке модели и замедляет вычисления. Остальная часть кода совпадает с вычислением точности во время тренировки, за исключением того, что в этом случае итерации проходят по test_loader.

Наконец, результат выводится в консоль, а модель сохраняется при помощи функции torch.save().

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

Точность на тестовой выборке на 10000 картинках составляет 99.03%

 

 

 

 

 

 

 

 

 

 

 

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

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

 


Интересные статьи:

Туториал по PyTorch: от установки до готовой нейронной сети

22 октября 2018
pytorch туториал

Туториал по PyTorch: от установки до готовой нейронной сети

Если вы уже пробовали создавать собственные глубокие нейронные сети с помощью TensorFlow и Keras, то, вероятно, знакомы с чувством разочарования при отлаживании этих библиотек. Хотя они имеют API на Python,…

Если вы уже пробовали создавать собственные глубокие нейронные сети с помощью TensorFlow и Keras, то, вероятно, знакомы с чувством разочарования при отлаживании этих библиотек. Хотя они имеют API на Python, всё еще трудно выяснить, что именно пошло не так при ошибке. Они также плохо работают вместе с библиотеками numpy, scipy, scikit-learn, Cython и другими. Библиотека глубокого обучения PyTorch имеет заявленное преимущество — хорошо работает с Python и создана для апологетов Python. Кроме того, приятное свойство PyTorch — построение вычислительного динамического графа, противоположно статическим вычислительным графам, представленным в TensorFlow и Keras. PyTorch сейчас находится на подъеме и используется в разработке Facebook, Twitter, NVIDIA и другими компаниями. Давайте обратимся к туториалу по использованию PyTorch.

цукерберг pytorch facebook

Перед вами перевод статьи «A PyTorch tutorial – deep learning in Python», ссылка на оригинал — в подвале статьи.

Первый вопрос для рассмотрения — действительно ли PyTorch лучше TensorFlow? Это субъективно, так как с точки зрения производительности нет больших различий. В любом случае, PyTorch стал серьезным соперником в соревновании между библиотеками глубокого обучения. Давайте начнем изучать библиотеку, оставив для размышлений вопрос о том, что же лучше.

Основы PyTorch

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

Полносвязная нейронная сеть
Полносвязная нейронная сеть

Установка на Windows

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

Вычислительные графы

Первое, что необходимо понять о любой библиотеке глубокого обучения — идея вычислительных графов. Вычислительный граф — набор вычислений, которые называются узлами, и которые соединены в прямом порядке вычислений. Другими словами, выбранный узел зависит от узлов на входе, который в свою очередь производит вычисления для других узлов. Ниже представлен простой пример вычислительного графа для вычисления выражения a = (b + c) * (c + 2). Можно разбить вычисление на следующие шаги:

Вычислительный графы
Простой вычислительный граф

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

Тензоры

Тензоры — подобные матрице структуры данных, которые являются неотъемлемыми компонентами в библиотеках глубокого обучения и используются для эффективных вычислений. Графические процессоры (GPU) эффективны при вычислении операций между тензорами, что стимулировало волну возможностей в глубоком обучении. В PyTorch тензоры могут определяться несколькими способами:

import torch
x = torch.Tensor(2, 3)

Этот код создает тензор размера (2,3), заполненный нулями. В данном примере первое число — количество рядов, второе — количество столбцов:

0 0 0
0 0 0
[torch.FloatTensor of size 2x3]

Мы также можем создать тензор, заполненный случайными float-значениями:

x = torch.rand(2, 3)

Умножение тензоров, сложение друг с другом и другие алгебраические операции просты:

x = torch.ones(2,3)
y = torch.ones(2,3) * 2
x + y

Код возвращает:

3 3 3
3 3 3
[torch.FloatTensor of size 2x3]

Также доступна работа с функцией slice в numpy. Например y[:,1]:

y[:,1] = y[:,1] + 1

Которая возвращает:

2 3 2
2 3 2
[torch.FloatTensor of size 2x3]

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

Автоматическое дифференцирование в PyTorch

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

Создадим переменную из простого тензора:

x = Variable(torch.ones(2, 2) * 2, requires_grad=True)

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

Далее, давайте создадим новую переменную на основе x.

z = 2 * (x * x) + 5 * x

Чтобы вычислить градиент этой операции по x, dz/dx, можно аналитически получить 4x + 5. Если все элементы x — двойки, то градиент dz/dx — тензор размерности (2,2), заполненный числами 13. Однако, сначала необходимо запустить операцию обратного распространения .backwards(), чтобы вычислить градиент относительно чего-либо. В нашем случае инициализируется единичный тензор (2,2), относительно которого считаем градиент. В таком случаем вычисление — просто операция d/dx:

z.backward(torch.ones(2, 2))
print(x.grad)

Результатом кода является следующее:

Variable containing:
13 13
13 13
[torch.FloatTensor of size 2x2]

Заметим, это в точности то, что мы предсказывали вначале. Отметим, градиент хранится в переменной x в свойстве .grad.

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

Создание нейронной сети в PyTorch

Этот раздел — основной в туториале. Полный код туториала лежит в этом репозитории на GitHub. Здесь мы создадим простую нейронную сеть с 4 слоями, включая входной и два скрытых слоя, для классификации рукописных символов в датасете MNIST. Архитектура, которую мы будем использовать, показана на картинке:

Архитектура полносвязной нейронной сети
Архитектура полносвязной нейронной сети

Входной слой состоит из 28 х 28 = 784 пикселей с оттенками серого, которые составляют входные данные в датасете MNIST. Входные данные далее проходят через два скрытых слоя, каждый из которых содержит 200 узлов, использующих линейную выпрямительную функцию активации (ReLU). Наконец, мы имеем выходной слой с десятью узлами, соответствующими десяти рукописным цифрам от 0 до 9. Для такой задачи классификации будем использовать выходной softmax-слой.

Класс для построения нейронной сети

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

import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
   def __init__(self):
       super(Net, self).__init__()
       self.fc1 = nn.Linear(28 * 28, 200)
       self.fc2 = nn.Linear(200, 200)
       self.fc3 = nn.Linear(200, 10)

В таком определении можно видеть наследование базового класса nn.Module. В первой строке инициализации класса def __init__(self)  мы имеем требуемую super() функцию языка Python, которая создает объект базового класса. В следующих трех строках создаем полностью соединенные слои как показано на диаграмме архитектуры. Полностью соединенный слой нейронной сети представлен объектом nn.Linear, в котором первый аргумент — определение количества узлов в i-том слое, а второй — количество узлов в i+1 слое. Из кода видно, первый слой принимает на входе 28×28 пикселей и соединяется с первым скрытым слоем с 200 узлами. Далее идет соединение с другим скрытым слоем с 200 узлами. И, наконец, соединение последнего скрытого слоя с выходным слоем с 10 узлами.

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

def forward(self, x):
   x = F.relu(self.fc1(x))
   x = F.relu(self.fc2(x))
   x = self.fc3(x)
   return F.log_softmax(x)

Для метода forward() берем входные данные x в качестве основного аргумента. Далее, загружаем всё в в первый полностью соединенный слой self.fc1(x) и применяем активационную функцию ReLU для узлов в этом слое, используя F.relu(). Из-за иерархической природы этой нейронной сети, заменяем x на каждой стадии и отправляем на следующий слой. Делаем эту процедуру на трех соединенных слоях, за исключением последнего. На последнем слое возвращаем не ReLU, а логарифмическую softmax активационную функцию. Это, в комбинации с функцией потери отрицательного логарифмического правдоподобия, дает многоклассовую на основе кросс-энтропии функцию потерь, которую мы будет использовать для тренировки сети.

Мы определили нейронную сеть. Следующим шагом будет создание экземпляра (instance) этой архитектуры:

net = Net()
print(net)

При выводе экземпляра класса Net получаем следующее:

Net (
(fc1): Linear (784 -> 200)
(fc2): Linear (200 -> 200)
(fc3): Linear (200 -> 10)
)

Что очень удобно, так как подтверждает структуру нашей нейронной сети.

Тренировка сети

Далее необходимо задать метод оптимизации и критерий качества:

# Осуществляем оптимизацию путем стохастического градиентного спуска
optimizer = optim.SGD(net.parameters(), lr=learning_rate, momentum=0.9)
# Создаем функцию потерь
criterion = nn.NLLLoss()

В первой строке создаем оптимизатор на основе стохастического градиентного спуска,  устанавливая скорость обучения (learning rate; в нашем случае определим этот показатель на уровне 0.01) и momentum. Еще в оптимизаторе необходимо определить все остальные параметры сети, но это делается легко в PyTorch благодаря методу .parameters() в базовом классе nn.Module, который наследуется из него в новый класс Net.

Далее устанавливается метрика контроля качества — функция потерь отрицательного логарифмического правдоподобия. Такой вид функции в комбинации с логарифмической softmax-функцией на выходе нейронной сети дает эквивалентную кросс-энтропийную потерю для 10 классов задачи классификации.

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

# запускаем главный тренировочный цикл
for epoch in range(epochs):
   for batch_idx, (data, target) in enumerate(train_loader):
       data, target = Variable(data), Variable(target)
# изменим размер с (batch_size, 1, 28, 28) на (batch_size, 28*28)
       data = data.view(-1, 28*28)
       optimizer.zero_grad()
       net_out = net(data)
       loss = criterion(net_out, target)
       loss.backward()
       optimizer.step()
       if batch_idx % log_interval == 0:
           print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                   epoch, batch_idx * len(data), len(train_loader.dataset),
                          100. * batch_idx / len(train_loader), loss.data[0]))

Внешний тренировочный цикл проходит по количеству эпох, а внутренний тренировочный цикл проходит через все тренировочные данные в партиях, размер которых задается в коде как batch_size. На следующей линии конвертируем данные и целевую переменную в переменные PyTorch. Входной датасет MNIST, который находится в пакете torchvision (который вам необходимо установить при помощи pip), имеет размер (batch_size, 1, 28, 28) при извлечении из загрузчика данных. Такой четырехмерный тензор больше подходит для архитектуры сверточной нейронной сети, чем для нашей полностью соединенной сети. Тем не менее, необходимо уменьшить размерность данных с (1,28,28)  до одномерного случая для 28 х 28 = 784 входных узла.

Функция .view() работает с переменными PyTorch и преобразует их форму. Если мы точно не знаем размерность данного измерения, можно использовать ‘-1’ нотацию в определении размера. Поэтому при использование data.view(-1,28*28) можно сказать, что второе измерение должно быть равно 28 x 28, а первое измерение должно быть вычислено из размера переменной оригинальных данных. На практике это означает, что данные теперь будут размера (batch_size, 784). Мы можем пропустить эту партию входных данных в нашу нейросеть, и магический PyTorch сделает за нас тяжелую работу, эффективно выполняя необходимые вычисления с тензорами.

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

net_out = net(data)
loss = criterion(net_out, target)

Первая строка, в которой подаем порцию данных на вход нашей модели, вызывает метод forward() в классе Net. После запуска строки переменная net_out будет иметь логарифмический softmax-выход из нашей нейронной сети для заданной партии данных.  Это одна из самых замечательных особенностей PyTorch, так как можно активировать любой стандартный отладчик Python, который вы обычно используете, и мгновенно узнать, что происходит в нейронной сети. Это противоположно другим библиотекам глубокого обучения, TensorFlow и Keras, в которых требуется  производить сложные отладочные действия, чтобы узнать, что ваша нейронная сеть действительно создает. Надеюсь, вы поиграете с кодом для этого туториала и поймете, насколько в PyTorch удобный отладчик.

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

Давайте посмотрим на следующие две строки:

loss.backward()
optimizer.step()

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

В следующей строке мы просим PyTorch выполнить градиентный спуск по шагам на основе вычисленных во время операции .backward() градиентов.

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

if batch_idx % log_interval == 0:
   print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                   epoch, batch_idx * len(data), len(train_loader.dataset),
                          100. * batch_idx / len(train_loader), loss.data[0]))

Эта функция выводит наш прогресс на протяжении эпох тренировки и показывает ошибку нейросети в этот момент. Отметим, что доступ к потерям находится в свойстве .data у переменной PyTorch, которая в данном случае будет массивом с единственным значением. Получаем скалярную потерю используя loss.data[0].

Запуская этот тренировочный цикл, получаем на выходе следующее:

Train Epoch: 9 [52000/60000 (87%)] Loss: 0.015086

Train Epoch: 9 [52000/60000 (87%)] Loss: 0.015086

Train Epoch: 9 [54000/60000 (90%)] Loss: 0.030631

Train Epoch: 9 [56000/60000 (93%)] Loss: 0.052631

Train Epoch: 9 [58000/60000 (97%)] Loss: 0.052678

После 10 эпох, значение потери по величине должно получиться меньше 0.05.

Тестирование сети

Чтобы проверить нашу обученную нейронную сеть на тестовом датасете MNIST, запустим следующий код:

test_loss = 0
correct = 0
for data, target in test_loader:
   data, target = Variable(data, volatile=True), Variable(target)
   data = data.view(-1, 28 * 28)
   net_out = net(data)
# Суммируем потери со всех партий
   test_loss += criterion(net_out, target).data[0]
   pred = net_out.data.max(1)[1]  # получаем индекс максимального значения
   correct += pred.eq(target.data).sum()

test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
       test_loss, correct, len(test_loader.dataset),
       100. * correct / len(test_loader.dataset)))

Этот цикл совпадает с тренировочным циклом до строки test_loss. Здесь мы извлекаем потери сети используя свойство .data[0] как и раньше, но только все в одной строке. Далее в строке pred используется метод data.max(1), который возвращает индекс наибольшего значения в определенном измерении тензора. Теперь выход нашей нейронной сети будет иметь размер (batch_size, 10), где каждое значение из второго измерения длины 10 — логарифмическая вероятность, которую нейросеть приписывает каждому выходному классу (то есть это логарифмическая вероятность принадлежности картинки к символу от 0 до 9). Поэтому для каждого входного образца в партии net_out.data будет выглядеть следующим образом:

[-1.3106e+01, -1.6731e+01, -1.1728e+01, -1.1995e+01, -1.5886e+01, -1.7700e+01, -2.4950e+01, -5.9817e-04, -1.3334e+01, -7.4527e+00]

Значение с наибольшей логарифмической вероятностью — цифра от 0 до 9, которую нейронная сеть распознает на входной картинке. Иначе говоря, это лучшее предсказание для заданного входного объекта. В примере net_out.data таким лучшим предсказанием является значение -5.9817e-04, которое соответствует цифре “7”. Поэтому для этого примера нейросеть предскажет знак  “7”. Функция .max(1) определяет это максимальное значение во втором пространстве (если мы хотим найти максимум в первом пространстве, мы должны аргумент функции изменить с 1 на 0) и возвращает сразу и максимальное найденное значение, и индекс ему соответствующий. Поэтому эта конструкция имеет размер (batch_size, 2). В данном случае, нас интересует индекс максимального найденного значения, к которому мы получаем доступ с помощью вызова .max(1)[1].

Теперь у нас есть предсказание нейронной сети для каждого примера в определенной партии входных данных, и можно сравнить его с настоящей меткой класса из тренировочного датасета. Это используется для подсчета количества правильных ответов. Чтобы сделать это в PyTorch, необходимо воспользоваться функцией .eq(), которая сравнивает значения в двух тензорах и при совпадении возвращает единицу. В противном случае, функция возвращает 0:

correct += pred.eq(target.data).sum()

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

test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
       test_loss, correct, len(test_loader.dataset),
       100. * correct / len(test_loader.dataset)))

После тренировки сети за 10 эпох получаем следующие результаты на тестовой выборке:

Test set: Average loss: 0.0003, Accuracy: 9783/10000 (98%)

Мы получили точность 98%. Весьма неплохо!

В туториале рассмотрены базовые принципы PyTorch, начиная c тензоров до функции автоматического дифференцирования (autograd) и заканчивая пошаговым руководством, как создать полностью соединенную нейронную сеть при помощи nn.Module.


Интересные статьи:

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

10 сентября 2018

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

Исследователи из MIT разработали алгоритм компьютерного зрения Dense Object Net, который помогает роботам в реальном времени распознавать, анализировать и определять назначение новых объектов. Теперь роботы смогут лучше манипулировать предметами — выбирать нужный предмет из нескольких, оценив его свойства. Как работает алгоритм…

Исследователи из MIT разработали алгоритм компьютерного зрения Dense Object Net, который помогает роботам в реальном времени распознавать, анализировать и определять назначение новых объектов. (далее…)

Cтуденты создали алгоритм, который на 40% эффективнее ИИ от Google

12 августа 2018
imagenet

Cтуденты создали алгоритм, который на 40% эффективнее ИИ от Google

Cтуденты из fast.ai разработали алгоритм, который на 40% эффективнее чем код Google для анализа и классификации изображений. Обучение нейросети заняло 18 минут и стоило $40. Команда обучила алгоритм на датасете…

Cтуденты из fast.ai разработали алгоритм, который на 40% эффективнее чем код Google для анализа и классификации изображений. Обучение нейросети заняло 18 минут и стоило $40.

Команда обучила алгоритм на датасете ImageNet до 93% точности за 18 минут, используя 16 облачных AWS, каждый из которых имеет 8 графических процессоров NVIDIA V100, работающих на библиотеках fastai и PyTorch. Разработчики научили модель обрезать изображения, что помогло ускорить тренировку на 23% по сравнению с другими подходами для достижения контрольной точности. Это новый рекорд скорости для обучения ImageNet с такой точностью на общедоступных инструментах.

Эффективность измеряли с помощью DAWBench, разработанного в Стэнфорде. Несмотря на то, что команда использовала более дешевое оборудование, результаты fast.ai на 40% лучше, чем у алгоритмов Google, обученных на чипах TPU Pod.

Оценка глубины на изображении при помощи Encoder-Decoder сетей

25 июня 2018
depth estimation using neural networks

Оценка глубины на изображении при помощи Encoder-Decoder сетей

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

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

(далее…)

“Видеть сквозь стены” — исследователи научились определять позу человека за стеной

21 июня 2018
Human Pose Estimation Under Occlusions

“Видеть сквозь стены” — исследователи научились определять позу человека за стеной

Возможность видеть сквозь стены всегда считалась сверхспособностью, и не раз становилась идеей научно-фантастических фильмов. В 2011 году исследователи из Массачусетского технологического института (MIT) объявили о разработке радара, показывающего в режиме…

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

Семь лет спустя исследователи из того же университета предложили новый метод “видения сквозь стены”

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

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

Описанный метод называется RF-Pose (RF — сокращение “Radio Frequency” = “радиочастотный”), он использует маломощный беспроводной сигнал (в 1000 раз слабее Wi-Fi). Для точного предсказания позы за препятствиями используются отражения радиосигналов, обработанные глубокой нейросетью. Считается, что стены представляют собой твердые предметы из бетона или дерева, способные блокировать или хотя бы ослаблять сигналы. Однако есть волны с определенной частотой, способные проходить сквозь них, например, Wi-Fi. Чтобы понять, что находится за бетонной стеной, нужно записать отраженный сигнал, который прошел сквозь нее и оттолкнулся от объекта. В RF-Pose применяется широко известная радарная система с антенными решетками и с непрерывным частотно-модулированным излучением (Frequency Modulated Continuous Wavе, FMCW). По сути, FMCW разделяет отраженные волны на основе расстояния от отражающего объекта, а антенные решетки — на основе направления в пространстве.

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

Heat Maps, RF-Pose method
Горизонтальные и вертикальные проекции тепловых карт

Метод

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

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

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

Определив специфические для поставленной задачи требования, исследователи предложили метод, основанный на обучении с учителем, где обучающие данные представлены разными типами (cross-modal supervision). Представленная архитектура “учитель-ученик” использует синхронизированные пары изображений RGB и проекции тепловых карт отражений радиосигнала.

Pose estimation under occlusions network architecture
Архитектура сети “учитель-ученик”

Cеть-учитель обучается на RGB-изображениях, ее цель — прогнозирование 14 ключевых точек, соответствующих анатомическим частям человеческого тела: голова, шея, плечи, локти, запястья, бедра, колени и лодыжки. Эти предсказанные карты достоверности (confidence maps) с ключевыми точками из сети-учителя используются для непосредственного обучения сети-ученика.

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

Formula

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

Для того, чтобы справиться с представлением данных, авторы закладывают архитектуру энкодера/декодера в сеть-ученика, которая “заставляет” ее учиться преобразовывать радиочастотные тепловые карты в изображение с камеры. Для этого реализованы две сети энкодера для горизонтальных и вертикальных тепловых карт (их несколько из-за использования группы снимков сразу), и одна сеть декодера, которая предсказывает карты достоверности контрольных точек, получая на вход закодированные данные по каналам с двух энкодеров.

В качестве входных данных сетям пространственно-временного сверточного кодирования поступает 100 кадров (3,3 секунды), каждая сеть имеет 10 слоев сверток с ядром 9x5x5. Сеть декодирования состоит из 4 слоев со свертками 3x6x6, и обе сети используют блоки ReLU и батч-нормализацию. Реализация была выполнена в PyTorch, при обучении batch size устанавливался равным 24.

OpenPose and RF-Pose comparison
Сравнение предложенного метода с другим существующим методом OpenPose

Оценки и выводы

Учитывая небольшой размер обучающей выборки, метод демонстрирует очень хорошие результаты. RF-Pose превосходит OpenPose на видимых сценах по метрике Average Precision(среднее из чисел, каждое из которых — средняя точность, при 10 различных порогах ОКS (object keypoint similarity) в диапазоне от 0,5 до 0,95). Сравнение между ними приведено в таблицах и графике ниже.

Comparison between RF-Pose and OpenPose
Сравнение RF-Pose и OpenPose

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

RF-Pose and OpenPose
Сравнение RF-Pose и OpenPose с учетом RGB изображения с камеры

Перевод — Эдуард Поконечный, оригинал — Dane Mitrev

Реконструкция фотографий методом частичной свертки от Nvidia

8 мая 2018
Image Inpainting for Irregular Holes Using Partial Convolutions

Реконструкция фотографий методом частичной свертки от Nvidia

“Image inpainting” — задача заполнения пробелов в изображении (реконструкция, дорисовка). Цель работы, проделанной авторами — предложить модель для реконструкции изображений, которая надежно работает на нерегулярных пробелах и создает семантически значимые прогнозы, которые сочетаются с…

“Image inpainting” — задача заполнения пробелов в изображении (реконструкция, дорисовка). Цель работы, проделанной авторами — предложить модель для реконструкции изображений, которая надежно работает на нерегулярных пробелах и создает семантически значимые прогнозы, которые сочетаются с остальной частью изображения без необходимости дополнительных операций пост-обработки или смешивания. Модель используется со многими приложениями, например, при редактировании изображений для удаления нежелательных деталей в изображении, заполняя приемлемым контентом.

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

Метод частичной свертки

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

Новшества:

  • Использование частичных сверток с автоматическим шагом обновления маски для достижения современных результатов в дорисовывании изображений (image-inpainting).
  • При помощи замены сверточных слоев частичными свертками и обновления масок удалось достичь самых высоких результатов в реконструкции изображений.
  • Продемонстрирована эффективность обучения image-inpainting моделей на пробелах неправильной формы.

Частичный сверточный слой

Модель использует операции стекирования частичной свертки и операции обновления маски для выполнения image inpainting. Операция частичной свертки и функция обновления маски работают совместно как частичный Сверточный слой.

Пусть W — вес фильтра свертки, а b — соответствующее смещение. X — значения пикселей для текущего окна свертки, а M — соответствующая бинарная маска. Частичная свертка в каждом месте выражается следующим образом:

Partial Convolutional Layer

Где ⊙ обозначает умножение по элементам. Как видно, выходные значения зависят только от немаскируемых входных значений. С помощью масштабного коэффициента 1/sum(M) подбирается подходящее масштабирование, чтобы скорректировать изменяющиеся действительные (незамаскированные) входные данные. После каждой операции частичной свертки маска обновляется. Правило unmasking простое: если с помощью свертки получилось вывести результат, по крайней мере, для одного допустимого входного значения, тогда маска для этого местоположения удаляется. Это выражается следующим образом:

Partial Convolutional Layer

и легко реализуется в любой системе глубокого обучения как часть forward pass.

Архитектура нейросети

Частичный слой свертки реализуется путем расширения существующего стандартного PyTorch. Прямая реализация заключается в определении двоичных масок размера C × H × W, того же размера с их связанными изображениями или элементами, а затем, чтобы обновить маску, требуется использование фиксированного слоя свертки с тем же размером ядра, что и операция частичной свертки , но с весами, равными 1, и нулевым смещением. Весь вывод сетью изображения 512 × 512 занимает 0,23 с на одном графическом процессоре NVIDIA V100, независимо от размера пробела.

Используется Unet-like архитектура, заменяющая сверточные слои частичными сверточными слоями, а также на этапе декодирования используется nearest neighbour up-sampling .

Network Architecture

ReLU используется на этапе кодирования, и LeakyReLU с альфа = 0,2 используется между декодирующимися слоями. Кодер состоит из восьми частичных сверточных слоев с шагом 2. Размеры ядра — 7, 5, 5, 3, 3, 3, 3 и 3. Размеры канала — 64, 128, 256, 512, 512, 512, 512 и 512. Входные данные последнего частичного сверточного слоя будут содержать конкатенацию исходного входного изображения с пробелами и оригинальной маской.

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

Функция потери направлена как на точность пиксельной реконструкции, так и на композицию, т. е. как плавно перетекают предсказанные значения пробелов в окружающее изображение. Учитывая входное изображение с пробелом Iin и маской M, предсказание сети Iout и ground truth изображение Igt, то потеря пикселя определяется как:

Loss Function

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

Функция perceptual loss вычисляет расстояния L1 одновременно между Iout и Icomp и ground truth. Для выполнения автокорреляции, на каждой карте характеристик вводится термин style loss.

Perceptual loss

Loss Function

Общая потеря — комбинация всех вышеперечисленных потерь:

Total Loss

Результаты

Частичная свертка превосходит другие методы. Чтобы доказать, что частичная свертка работает лучше, чем другие методы, используются I1-ошибка, пиковое отношение сигнала к шуму (peak signal-to-noise ratio — PSNR), индекс структурного сходства (Structural SIMilarity — SSIM) и начальная оценка (Inception score — IScore). Ниже в таблице показаны результаты сравнения. Видно, что метод PConv превосходит другие методы в этих измерениях на нерегулярных масках.

measurements on irregular masks

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

Сorresponding inpainted results
Рисунок 2: Верхняя строка: ввод; нижняя строка: соответствующие результаты
Comparison convolution layer and partial convolution results
Рисунок 3: Сравнение между типичными результатами на основе сверточного слоя (Conv) и результатами на основе частичного сверточного слоя (PConv)

Больше результатов метода частичной свертки (PConv):

PConv PConv

Руслан Хабибуллин