Данная статья является кратким обзором возможностей 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.
Список поддерживаемых слоев
Кроме поддержки отдельных слоев, важна также и поддержка конкретных архитектур нейронных сетей. Модуль содержит примеры для классификации (AlexNet, GoogLeNet, ResNet, SqueezeNet), сегментации (FCN, ENet), детектирования объектов (SSD); многие из указанных моделей проверены на исходных датасетах, но об этом позднее.
Сборка
Если вы — опытный пользователь OpenCV, то можете смело пропустить этот раздел. Если нет, то я постараюсь максимально кратко рассказать о том, как же получить работающие примеры из исходного кода для Linux или Windows.
Краткая инструкция по сборке
Примеры использования
По хорошей традиции, каждый модуль 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
Таблица 1. Результаты оценки качества для задачи классификации. Измерения проводились на валидационном наборе ImageNet 2012 (ILSVRC2012 val, 50000 примеров).
Модель (фреймворк)
Опубликованное значение 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
Таблица 2. Результаты оценки качества для задачи семантической сегментации. Объяснение большой максимальной разницы для ENet далее в тексте.
Результаты для 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?
Производительность
Одна из целей, которую мы ставим перед собой, разрабатывая 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 МБ
Таблица 3. Результаты замеров времени работы различных моделей. Эксперименты проводились с использованием Intel Core i7-6700k.
Замеры времени производились с усреднением по 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. Выражаю огромную благодарность моим коллегам за помощь в работе и написании данной статьи.