Данная статья является кратким обзором возможностей dnn — модуля OpenCV, предназначенного для работы с нейросетями. Если вам интересно, что это такое, что оно умеет и как быстро работает, добро пожаловать под кат.
Пожалуй, многие согласятся, что OpenCV является наиболее известной библиотекой компьютерного зрения. За долгое время своего существования она приобрела обширную аудиторию пользователей и стала, де-факто, стандартом в области компьютерного зрения. Множество алгоритмов, работающих «из коробки», открытость исходного кода, замечательная поддержка, большое сообщество пользователей и разработчиков, возможность пользоваться библиотекой на языках C, C++, Python (а также Matlab, C#, Java) под различными операционными системами — это далеко не полный список того, что позволяет OpenCV оставаться востребованной. Но OpenCV не стоит на месте — постоянно добавляется функционал. И сегодня я хочу рассказать о новых возможностях OpenCV в области Deep Learning.
Загрузка и получение результатов (предсказаний) с помощью моделей, созданных в любом из трех популярных фреймворков (Caffe, TensorFlow, Torch), быстрая работа на CPU, поддержка основных слоев нейронных сетей и, как всегда, кроссплатформенность, открытость исходного кода и поддержка — об этом я собираюсь рассказать в данной статье.
Прежде всего, хотелось бы представиться. Меня зовут Рыбников Александр. Я являюсь инженером компании Intel и занимаюсь реализацией функциональности Deep Learning в библиотеке OpenCV.
Пару слов о том, как устроена OpenCV. Эта библиотека представляет собой набор модулей, каждый из которых связан с определенной областью компьютерного зрения. Существует стандартный набор модулей — так сказать, «must have» для любой задачи компьютерного зрения. Реализуя известные алгоритмы, данные модули хорошо проработаны и протестированы. Все они представлены в основном репозитории OpenCV. Также существует репозиторий с дополнительными модулями, реализующими экспериментальную или новую функциональность. Требования к экспериментальным модулям, по понятным причинам, мягче. И, как правило, когда какой-то из таких модулей становится достаточно развитым, сформировавшимся и востребованным, он может быть перенесен в основной репозиторий.
Данная статья связана с одним из модулей, совсем недавно занявшим почетное место в основном репозитории — с модулем dnn (далее просто dnn).
(N+1)-й фреймворк для deep learning, это вообще зачем?
Зачем вообще понадобился Deep Learning в OpenCV? В последние годы во многих областях глубокое обучение (в некоторых источниках глубинное обучение) показывает результаты, значительно превосходящие аналогичные у классических алгоритмов. Это касается и области компьютерного зрения, где масса задач решается с применением нейронных сетей. В свете данного факта кажется логичным дать пользователям OpenCV возможность работы с нейросетями.
Почему был выбран путь написания чего-то своего вместо использования уже существующих реализаций? Этому есть несколько причин.
Во-первых, так можно добиться легковесности решения. Оставляя только возможность выполнения прямого прохода (forward pass) по сети, можно упростить код, ускорить процесс установки и сборки.
Во-вторых, имея свою реализацию, можно свести внешние зависимости к минимуму. Это упростит распространение приложений, использующих dnn. И, если ранее в проекте использовалась библиотека OpenCV, не составит труда добавить в такой проект поддержку глубоких сетей.
Так же, разрабатывая свое решение, есть возможность сделать его универсальным, не привязанным к какому-то конкретному фреймворку, его ограничениям и недостаткам. При наличии собственной имплементации доступны все пути для оптимизации и ускорения кода.
Собственный модуль для запуска глубоких сетей значительно упрощает процедуру создания гибридных алгоритмов, сочетающих в себе быстроту классического компьютерного зрения и замечательную обобщающую способность глубоких нейронных сетей.
Стоит заметить, что модуль не является, строго говоря, полноценным фреймворком для глубокого обучения. На данный момент в модуле представлена исключительно возможность получения результатов работы сети.
Основные возможности
Основная возможность dnn заключается, конечно же, в загрузке и запуске нейронных сетей (inference). При этом модель может быть создана в любом из трех фреймворков глубокого обучения — Caffe, TensorFlow или Torch; способ ее загрузки и использования сохраняется независимо от того, где она была создана.
Поддерживая сразу три популярных фреймворка, мы можем достаточно просто комбинировать результаты работы загруженных из них моделей без необходимости создавать все заново в одном единственном фреймворке.
При загрузке происходит конвертация моделей во внутреннее представление, близкое к используемому в Caffe. Так произошло в силу исторических причин — поддержка Caffe была добавлена самой первой. Однако взаимно однозначного соответствия между представлениями нет.
Поддерживаются все основные слои: начиная от базовых (Convolution и Fully connected) и заканчивая более специализированными — всего более 30.
Список поддерживаемых слоев
AbsVal
AveragePooling
BatchNormalization
Concatenation
Convolution (with dilation)
Crop
DetectionOutput
Dropout
Eltwise
Flatten
FullConvolution
FullyConnected
LRN
LSTM
MaxPooling
MaxUnpooling
MVN
NormalizeBBox
Padding
Permute
Power
PReLU
PriorBox
ReLU
RNN
Scale
Shift
Sigmoid
Slice
Softmax
Split
TanH
Если вы не обнаружили в этом списке слой, который требуется именно вам, не стоит отчаиваться. Вы можете создать запрос на добавление поддержки интересующего вас слоя (и наша команда постарается помочь вам в ближайшее время), либо реализовать все самостоятельно и подать pull request.
AveragePooling
BatchNormalization
Concatenation
Convolution (with dilation)
Crop
DetectionOutput
Dropout
Eltwise
Flatten
FullConvolution
FullyConnected
LRN
LSTM
MaxPooling
MaxUnpooling
MVN
NormalizeBBox
Padding
Permute
Power
PReLU
PriorBox
ReLU
RNN
Scale
Shift
Sigmoid
Slice
Softmax
Split
TanH
Если вы не обнаружили в этом списке слой, который требуется именно вам, не стоит отчаиваться. Вы можете создать запрос на добавление поддержки интересующего вас слоя (и наша команда постарается помочь вам в ближайшее время), либо реализовать все самостоятельно и подать pull request.
Кроме поддержки отдельных слоев, важна также и поддержка конкретных архитектур нейронных сетей. Модуль содержит примеры для классификации (AlexNet, GoogLeNet, ResNet, SqueezeNet), сегментации (FCN, ENet), детектирования объектов (SSD); многие из указанных моделей проверены на исходных датасетах, но об этом позднее.
Сборка
Если вы — опытный пользователь OpenCV, то можете смело пропустить этот раздел. Если нет, то я постараюсь максимально кратко рассказать о том, как же получить работающие примеры из исходного кода для Linux или Windows.
Краткая инструкция по сборке
Предварительно потребуется установить git (либо Git Bash для Windows), [cmake](http://cmake.org) и компилятор С++ (Visual Studio под Windows, Xcode на Mac, clang либо gcc под Linux). Если вы собираетесь использовать OpenCV из Python, то нужно также установить сам Python (подойдут последние версии 2.7.x или 3.x) и соответствующую ему версию numpy.
Начнем с клонирования репозитория:
На Windows клонирование репозиториев также можно выполнить, например, с помощью TortoiseGit или SmartGit. Далее, приступим к генерации файлов для сборки:
(для Windows здесь и далее нужно заменить cmake на полный путь до запускаемого файла cmake, например на «C:\Program Files\CMake\bin\cmake.exe» или использовать cmake GUI)
Теперь непосредственно сборка:
После этого dnn готов к использованию.
Приведенная выше инструкция достаточно краткая, поэтому приведу также ссылки на пошаговые инструкции по установке OpenCV на Windows и Linux.
Начнем с клонирования репозитория:
mkdir git && cd git
git clone https://github.com/opencv/opencv.git
На Windows клонирование репозиториев также можно выполнить, например, с помощью TortoiseGit или SmartGit. Далее, приступим к генерации файлов для сборки:
cd ..
mkdir build && cd build
cmake ../git/opencv -DBUILD_EXAMPLES=ON
(для Windows здесь и далее нужно заменить cmake на полный путь до запускаемого файла cmake, например на «C:\Program Files\CMake\bin\cmake.exe» или использовать cmake GUI)
Теперь непосредственно сборка:
make -j5 (Linux)
cmake --build . --config Release -- /m:5 (Windows)
После этого dnn готов к использованию.
Приведенная выше инструкция достаточно краткая, поэтому приведу также ссылки на пошаговые инструкции по установке OpenCV на Windows и Linux.
Примеры использования
По хорошей традиции, каждый модуль OpenCV включает в себя примеры использования. dnn — не исключение, примеры на С++ и Python доступны в поддиректории samples в репозитории с исходным кодом. В примерах присутствуют комментарии, да и в целом все достаточно просто.
Приведу здесь краткий пример, выполняющий классификацию изображений с помощью модели GoogLeNet. На языке Python наш пример будет выглядеть следующим образом:
import numpy as np
import cv2 as cv
# read names of classes
with open('synset_words.txt') as f:
classes = [x[x.find(' ') + 1:] for x in f]
image = cv.imread('space_shuttle.jpg')
# create tensor with 224x224 spatial size and subtract mean values (104, 117, 123)
# from corresponding channels (R, G, B)
input = cv.dnn.blobFromImage(image, 1, (224, 224), (104, 117, 123))
# load model from caffe
net = cv.dnn.readNetFromCaffe('bvlc_googlenet.prototxt', 'bvlc_googlenet.caffemodel')
# feed input tensor to the model
net.setInput(input)
# perform inference and get output
out = net.forward()
# get indices with the highest probability
indexes = np.argsort(out[0])[-5:]
for i in reversed(indexes):
print('class:', classes[i], ' probability:', out[0][i])
Данный код загружает картинку, проводит небольшую предобработку и получает для изображения выход сети. Предобработка заключается в масштабировании изображения таким образом, чтобы наименьшая из сторон стала равной 224, вырезании центральной части и вычитании среднего значения из элементов каждого канала. Данные операции необходимы, так как модель была натренирована на изображениях заданного размера (224 x 224) с именно такой предобработкой.
Выходной тензор интерпретируется как вектор вероятностей принадлежности изображения к тому или иному классу и имена для 5 классов с наибольшими вероятностями выводятся в консоль.
Выглядит несложно, не так ли? Если записать то же самое на C++, код получится немного более длинным. Однако, самое главное — имена функций и логика работы с модулем — останутся одними и теми же.
Точность
Как понять, что одна натренированная модель лучше другой? Необходимо сравнить метрики качества для обеих моделей. Очень часто борьба на вершине рейтинга лучших моделей идет за доли процентов качества. Поскольку dnn читает и преобразует модели из различных фреймворков в свое внутреннее представление, возникают вопросы сохранения качества после преобразования модели: не «испортилась» ли модель после загрузки? Без ответов на эти вопросы, а значит без проверки сложно говорить о полноценном использовании dnn.
Я провел тестирование моделей из имеющихся примеров для различных фреймворков и различных задач: AlexNet (Caffe), GoogLeNet (Caffe), GoogLeNet (TensorFlow), ResNet-50 (Caffe), SqueezeNet v1.1 (Caffe) для задачи классификации объектов; FCN (Caffe), ENet (Torch) для задачи семантической сегментации. Результаты приведены в Таблицах 1 и 2.
Модель (исходный фреймворк) |
Опубликованное значение acc@top-5 |
Измеренное значение acc@top-5 в исходном фреймворке |
Измеренное значение acc@top-5 в dnn |
Средняя разница на элемент между выходными тензорами фреймворка и dnn |
Максимальная разница между выходными тензорами фреймворка и dnn |
AlexNet (Caffe) |
80.2% |
79.1% |
79.1% |
6.5E-10 |
3.01E-06 |
GoogLeNet (Caffe) |
88.9% |
88.5% |
88.5% |
1.18E-09 |
1.33E-05 |
GoogLeNet (TensorFlow) |
— | 89.4% |
89.4% |
1.84E-09 |
1.47E-05 |
ResNet-50 (Caffe) |
92.2% |
91.8% |
91.8% |
8.73E-10 |
4.29E-06 |
SqueezeNet v1.1 (Caffe) |
80.3% |
80.4% |
80.4% |
1.91E-09 |
6.77E-06 |
Модель (фреймворк) |
Опубликованное значение mean IOU |
Измеренное значение mean IOU в исходном фреймворке |
Измеренное значение mean IOU в dnn |
Средняя разница на элемент между выходными тензорами фреймворка и dnn |
Максимальная разница между выходными тензорами фреймворка и dnn |
FCN (Caffe) |
65.5% |
60.402874% |
60.402879% |
3.1E-7 |
1.53E-5 |
ENet (Torch) |
58.3% |
59.1368% |
59.1369% |
3.2E-5 |
1.20 |
Результаты для FCN вычислены для валидационного набора сегментационной части PASCAL VOC 2012 (736 примеров). Результаты для ENet вычислены на валидационном наборе Cityscapes (500 примеров).
Следует сказать несколько слов о том, какой смысл имеют указанные выше числа. Для задач классификации общепринятой метрикой качества моделей является точность для топ-5 ответов сети (accuracy@top-5, [1]): если правильный ответ имеется среди 5 ответов сети с максимальными показателями уверенности (confidence), то данный ответ сети засчитывается как верный. Соответственно, точность — это отношение числа верных ответов к числу примеров. Данный способ измерения позволяет учесть не всегда корректную разметку данных, когда, например, отмечается объект, занимающий далеко не центральное положение на кадре.
Для задач семантической сегментации используются несколько метрик — попиксельная точность (pixel accuracy) и среднее по классам отношение пересечения к объединению (mean intersection over union, mean IOU) [5]. Попиксельная точность — это отношение количества правильно классифицированных пикселей к количеству всех пикселей. mean IOU — более сложная характеристика: это усредненное по классам отношение правильно отмеченных пикселей к сумме числа пикселей данного класса и числа пикселей, отмеченных как данный класс.
Из таблиц следует, что для задач классификации и сегментации разница в точности между запусками модели в оригинальном фреймворке и в dnn отсутствует. Этот замечательный факт означает, что модуль можно смело использовать, не опасаясь непредсказуемых результатов. Все скрипты для тестирования также доступны здесь, так что можно самостоятельно убедиться в правильности полученных результатов.
Разницу между опубликованными и полученными в экспериментах числами можно объяснить тем, что авторы моделей проводят все вычисления с использованием GPU, в то время как я использовал CPU-реализации. Также было замечено, что различные библиотеки могут по-разному декодировать формат jpeg. Это могло сказаться на результатах для FCN, так как датасет PASCAL VOC 2012 содержит изображения именно данного формата, а модели для семантической сегментации оказываются достаточно чувствительны к изменению распределения входных данных.
Как вы заметили, в Таблице 2 присутствует аномально большая максимальная разница выходов dnn и Torch для модели ENet. Меня также заинтересовал данный факт и далее я кратко расскажу о причинах его возникновения.
Почему возникает большое различие между dnn и Torch для ENet?
Модель ENet использует несколько операций MaxPooling. Данная операция выбирает максимальный элемент в окрестности каждой позиции и записывает в выходной тензор это максимальное значение, а также передает далее индексы выбранных максимальных элементов. Эти индексы далее используются операцией, в некотором смысле обратной данной — MaxUnpooling. Эта операция записывает элементы входного тензора в позиции выходного, соответствующие тем самым индексам. В этом месте и возникает большая ошибка: в определенной окрестности операция MaxPooling выбирает элемент с неправильным индексом; при этом разница между правильным выходом Torch и выходом dnn для данного слоя лежит в пределах вычислительной погрешности (10E-7), а разница в индексах соответствует соседним элементам окрестности. То есть, в результате небольшой флуктуации соседний элемент стал несколько больше, чем элемент с правильным индексом. Результат операции MaxUnpooling, при этом, зависит не только от выхода предыдущего слоя, но и от индексов соответствующей операции MaxPooling, которая располагается намного раньше (в начале вычислительного графа модели). Таким образом, MaxUnpooling записывает элемент с правильным значением в неверную позицию. В результате, накапливается ошибка.
К сожалению, устранить данную ошибку не представляется возможным, так как первопричины ее появления связаны, скорее всего, с немного различающимися реализациями алгоритмов, использованных при тренировке и при inference и не связаны с наличием ошибки в реализации.
Однако, справедливо отметить, что средняя ошибка на элемент выходного тензора остается низкой (см. Таблицу 2) — то есть ошибки в индексах возникают достаточно редко. Более того, наличие данной ошибки не приводит к ухудшению качества работы модели, о чем свидетельствуют числа в той же Таблице 2.
К сожалению, устранить данную ошибку не представляется возможным, так как первопричины ее появления связаны, скорее всего, с немного различающимися реализациями алгоритмов, использованных при тренировке и при inference и не связаны с наличием ошибки в реализации.
Однако, справедливо отметить, что средняя ошибка на элемент выходного тензора остается низкой (см. Таблицу 2) — то есть ошибки в индексах возникают достаточно редко. Более того, наличие данной ошибки не приводит к ухудшению качества работы модели, о чем свидетельствуют числа в той же Таблице 2.
Производительность
Одна из целей, которую мы ставим перед собой, разрабатывая dnn, заключается в достижении достойной производительности модуля на различных архитектурах. Не так давно была проведена оптимизация под CPU, в результате чего сейчас dnn показывает неплохие результаты по скорости работы.
Я провел замеры времени работы для различных моделей при их использовании — результаты в Таблице 3.
Модель (исходный фреймворк) |
Разрешение изображения |
Производительность исходного фреймворка, CPU (библиотека акселерации); потребление памяти |
Производительность dnn, CPU (ускорение относительно исходного фреймворка); потребление памяти |
AlexNet (Caffe) |
227x227 |
23.7 мс (MKL); 945 МБ |
14.7 мс (1.6x); 713 МБ |
GoogLeNet (Caffe) |
224x224 |
44.6 мс (MKL); 197 МБ |
20.1 мс (2.2x); 172 МБ |
ResNet-50 (Caffe) |
224x224 |
70.2 мс (MKL); 386 МБ |
58.8 мс (1.2x); 224 МБ |
SqueezeNet v1.1 (Caffe) |
227x227 |
12.4 мс (MKL); 113 МБ |
5.3 мс (2.3x); 38 МБ |
GoogLeNet (TensorFlow) |
224x224 |
17.9 мс (Eigen); 310 МБ |
21.1 мс (0.8x); 135 МБ |
FCN (Caffe) |
различное (500x350 в среднем) |
3873.6 мс (MKL); 4453 МБ |
1229.8 мс (3.1x); 1332 МБ |
ENet (Torch) |
1024x512 |
1105.0 мс; 828 МБ |
218.7 мс (5.1x); 190 МБ |
Замеры времени производились с усреднением по 50-ти запускам и выполнялись следующим образом: для dnn использовался встроенный в OpenCV таймер; для Caffe использовалась утилита caffe time; для Torch и TensorFlow использовались существующие функции замера времени.
Как следует из Таблицы 3, dnn в большинстве случаев превосходит по производительности оригинальные фреймворки. Актуальные данные по производительности dnn из OpenCV на различных моделях в сравнении с другими фреймворками также можно найти здесь.
Дальнейшие планы
Глубокое обучение заняло в компьютерном зрении значительное место и, соответственно, у нас есть большие планы по развитию этой функциональности в OpenCV. Они касаются улучшения удобства использования, переработки внутренней архитектуры самого модуля и улучшения производительности.
В улучшении user experience мы ориентируемся, в первую очередь, на пожелания самих пользователей. Мы стремимся добавить функциональность, которая требуется разработчикам и исследователям в реальных задачах. Помимо этого, в планах есть добавление визуализации сетей, а также расширение набора поддерживаемых слоев.
Что касается производительности, то несмотря на многие выполненные оптимизации, у нас все еще есть идеи, как улучшить результаты. Одна из таких идей — уменьшить разрядность вычислений. Данная процедура носит название квантизации. Грубо говоря, выкинуть часть разрядов у входа и весов слоя перед вычислением сверток (fp32→fp16), либо вычислить масштабирующие коэффициенты, переводящие диапазон входных чисел в диапазон int или short. При этом возрастет скорость (за счет использования более быстрых операций с целыми числами), но, возможно, немного пострадает точность. Однако публикации и эксперименты в этой области показывают, что даже достаточно сильная квантизация в определенных случаях не приводит к заметному падению качества.
Параллельное выполнение слоев — еще одна из идей оптимизации. В текущей реализации в каждый момент времени работает только один слой. Каждый слой по возможности максимально использует распараллеливание при проведении вычислений. Однако, в некоторых случаях граф вычислений может быть распараллелен на уровне самих слоев. Потенциально это может дать каждому потоку больше работы, уменьшив тем самым накладные расходы.
Сейчас к релизу готовится нечто достаточно интересное. Думаю, немногие слышали о языке программирования Halide. Он не является Тьюринг-полным — некоторые конструкции реализовать на нем не получится; возможно поэтому он и не пользуется популярностью. Однако указанный недостаток является одновременно и его преимуществом — написанный на нем исходный код может быть автоматически превращен в высокооптимизированный под разные «железки»: CPU, GPU, DSP. При этом нет нужды быть гуру оптимизации — специальный компилятор все сделает за вас. Уже сейчас Halide позволяет получить ускорение некоторых моделей — и, например, семантическая сегментация с моделью ENet работает 25 fps для разрешения 512x256 на Intel Core i7-6700k (против 22 fps у dnn без Halide). И, что самое приятное, без переписывания кода можно задействовать интегрированную в процессор GPU, получив дополнительно еще пару кадров в секунду.
В действительности, мы возлагаем большие надежды на Halide. Благодаря своим уникальным характеристикам он позволит получать ускорение работы, не требуя от пользователя дополнительных манипуляций. Мы стремимся к тому, чтобы для использования Halide вместе с OpenCV у пользователя не возникало необходимости в установке дополнительного программного обеспечения для использования Halide — принцип работы «из коробки» должен сохраняться. И, как показывают наши эксперименты, у нас есть все шансы реализовать это.
Заключение
Уже сейчас dnn имеет все, чтобы оказаться полезным. И с каждым днем все большее число пользователей открывает для себя его возможности. Тем не менее, нам еще есть над чем трудиться. Я продолжу свою работу над модулем, расширяя его возможности и совершенствуя функционал. Надеюсь, что данная статья оказалась для вас интересной и полезной.
Если у вас есть вопросы, появились предложения, возникли проблемы или вы хотите внести свой вклад путем подачи pull request — добро пожаловать в github-репозиторий, а также на наш форум, где я и мои коллеги постараемся вам помочь. Если ни один из указанных способов не подошел, на нашем сайте можно найти дополнительные пути коммуникации. Я всегда буду рад сотрудничеству, конструктивным замечаниям и предложениям. Спасибо за внимание!
P.S. Выражаю огромную благодарность моим коллегам за помощь в работе и написании данной статьи.
Ссылки
- ImageNet Classification with Deep Convolutional Neural Networks
- Going deeper with convolutions
- Deep Residual Learning for Image Recognition
- SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and <0.5MB model size
- Fully Convolutional Networks for Semantic Segmentation
- ENet: A Deep Neural Network Architecture for Real-Time Semantic Segmentation
- SSD: Single Shot MultiBox Detector
- Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding
- OpenCV github
- Официальный сайт OpenCV
- Форум OpenCV
- Halide
- Caffe
- TensorFlow
- Torch