Плотность на квадратный пиксел

Привет, Хабр.

Меня зовут Михаил, и обычно в Itransition я выполняю роль Java-разработчика. Но иногда меня привлекают для RnD-процессов — в частности, связанных с ML и нейронными сетями. 

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

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

И.О. Деда Мороза приходит с сюрпризом

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

image-loader.svg

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

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

Школа железных мозгов

Небольшое лирическое отступление для тех, кто еще не знает, как проходит обучение нейросетей под задачи классификации: для начала вы берете большое количество образцов, например, фотографий, и для каждой определяете координаты прямоугольников с интересующими вас объектами. Руками. Вот IMG_023410282018.JPG, по координатам 0,0–230,168 у нас объект с тэгом «кошка». И так далее. Пачка из размеченных фотографий называется набором.

image-loader.svg

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

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

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

Мы же тем временем решали архитектурные вопросы. Data science-команда рекомендовала попробовать open-source сеть YOLO третьей версии. Главным условием была возможность сети работать с аппаратным ускорением. Как выяснилось — у YOLO настолько много форков, что найти на гитхабе тот, что построен поверх TensorFlow, не было проблемой. Ну, а TensorFlow, в свою очередь, хорошо «дружит» с Nvidia CUDA. Получился типовой ML-стек: CUDA/TensorFlow/Python.

Забегая наперед — для доставки MVP мы воспользовались Nvidia Container Runtime, который позволяет пробросить GPU в контейнеризованное приложение, а в качестве базового взяли образ TensorFlow также от Nvidia. Оставшаяся часть была довольно тривиальной — обернуть модель в REST API при помощи Flask и проверить хотя бы на «стоковой» конфигурации. А там подъехали и наборы данных от заказчика.

Закат солнца по Page Down

Наблюдать за поведением свинок мне, как человеку, выросшему вдали от деревень и выпасов, было весьма любопытно. Если зажать кнопку Page Down — можно было увидеть, как проходит «свинский» день жизни. Колорита добавляли солнечные блики на фотографиях, проползавшие из угла в угол в зависимости от времени суток. Эти же блики попили нам крови после того, как мы расправились с этапом определения местоположения меток в кадре.

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

image-loader.svg

И снова лирическое отступление. Обучение — процесс не только долгий, но и дорогой. И не только потому, что надо заплатить за разметку фотографий, но и за железо. Тут всего два варианта — пользоваться GPU-инстансом в облаке и надеяться, что стоимость аренды не вылетит из бюджета, либо купить топовую железку хотя бы потребительского класса (GTX, RTX) и воткнуть в импровизированный ML-сервер. И тут, в первую очередь, решают даже не терафлопсы, а гигабайты. Потому что развернутая TF-модель может не во всякий лимит памяти влезть. Меньше 10 гигабайт GDDR уже в 2019 году смысла брать не было. 

И хотя метки на Full HD кадре занимали примерно 80×150 пикселов, и при дефолтном размере скользящего окна YOLO в 204×204 это давало очень плохие результаты. Расширение до 800×800 исправило ситуацию, а вот на попытку воткнуть 1024×1024 CUDA сказала, что «у неё лапки» и высыпалась с ошибкой «ваш тензор не пролезает в мою память, умерьте пыл».

А дальше оставалось только ждать. Потому что, если тебе нечего править в коде, а осталось только обучить сеть — это в чистом виде «режим Хатико» на несколько часов. Даже если у тебя субтоповая на тот момент RTX 2080. В день можно было провести три-четыре итерации, и еще одна на ночь, чтобы с утра оценить очередную попытку.

Наконец настал тот день, когда сеть у нас лихо справлялась с определением меток. И пришла очередь распознавать цвет. Теперь надо было вырезать пикселы и определить, к какому из эталонных цветов относится конкретная метка. Я говорил, что освещение попило нам кровь? Подержи моё пиво.

Цифровой дальтоник: инструкция по сборке

Самой очевидной идеей было измерить цветовое расстояние по RGB. Нам были известны HEX-коды цветов меток по каталогу Pantone, у нас был PyOpenCV, NumPy, Pandas… Не то, чтобы это все было нужно, но когда всерьез начал заниматься машинным обучением, — надо идти в своей одержимости до конца.

image-loader.svg

Цвета меток оказались неравномерно разбросаны по цветовому кубу, и, в случае различных вариаций освещения, вектор легко прилипал к ошибочному цвету, выдавая cyan вместо yellow, или magenta вместо red. Окей, с кубом не прокатило. Мы выкинули RGB и поставили цветовое колесо, нормализуя яркость. Помогло, но не сильно. В итоге, после пары убитых дней, мой датасатанист с воплем «Да как так-то!» отложил на полчаса свой текущий проект и быстренько набросал двухслойный перцептрон на шесть выходов. И вы знаете — сработало! Конечно, перцептрон тоже пришлось обучить, но это было уже сильно проще и быстрее.

Настал день икс. Мы сели показывать заказчику «товар лицом». Чуть-чуть не дотянули по надежности распознавания до заданной. А вот с цветом уже проблем не было — эту планку мы взяли, о чем и было честно сказано и показано во время презентации. И через несколько дней заказчик принял решение стартовать второй этап.

Press F for next level

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

— Вес по фото. Прямо какая-то диагностика по аватарке. Это как? Это нам как-то нормализовать и усреднять объем свиньи по множеству её ракурсов? А потом считать плотность свиньи на квадратный пиксел?!
— Прекрасная метрика, — пробормотал я, — Пожалуй, запишу.

image-loader.svg

Если бы на этом все закончилось. Но нет. Через пару дней ко мне зашел другой коллега.

— Слушай, вы тут какую-то штуку для измерения свиней прототипировали…
— Да-да, могу в красках все рассказать!
— Расскажи на чем она у вас работает, мне ее портировать придется.
— Ну, э … Стандартный x86–64 Linux, TensorFlow, CUDA, Docker с пробросом GPU. А на что портировать?
— На Raspberry Pi 3. RTX 2070  дорого, говорят. Надо в пятьдесят долларов уложиться.

Что-то больно ударило меня по ноге, и это была моя челюсть. Забегая вперед — таки да, этот парень впихнул нашу модель на «малинку» и добился того, что распознавание на ней занимало не более десятка секунд — тогда как на видеокарте мы добились минимум 2–3 секунды. Но это уже другая история. Не переключайтесь.

© Habrahabr.ru