Плотность на квадратный пиксел
Привет, Хабр.
Меня зовут Михаил, и обычно в Itransition я выполняю роль Java-разработчика. Но иногда меня привлекают для RnD-процессов – в частности, связанных с ML и нейронными сетями.
И сегодняшняя статья будет про учет и подсчет свиней при помощи современных технологий машинного зрения и машинного обучения.
Вообще, заголовок этой статьи должен был выглядеть так: “Средняя плотность свиной туши на квадратный пиксел”, но это было бы слишком длинно и непонятно. Во времена, когда интернет был прикован к проводам, появилось выражение, ставшее расхожим – “диагностика по аватарке”. Кто бы мне сказал, что спустя десяток лет я будут заниматься прототипированием этой самой “диагностики по аватарке” – я бы не поверил.
И.О. Деда Мороза приходит с сюрпризом
История эта, как и все невероятное, началась под Новый год. К нам обратился давний заказчик с весьма необычным запросом: ему хотелось с помощью видеокамеры обрабатывать свиней. Чтобы роботы, как пелось в детской песне, – вкалывали, а человек, хозяйствующий этих свиней, был счастлив.
– Нам нужна идентификация и учет аппетита свинок с помощью камеры.
– Как вы видите “учет аппетита с помощью камеры”? – уточнил я.
– Всё просто: камера смонтирована на потолке свинарника, смотрит на кормушку. Свиньи периодически подходят к кормушке и питаются. На первом этапе задача стоит “идентифицировать особей в кадре”.
– Есть какие-то метки?
– Да, цветные клипсы. Совершенно стандартная практика – крепятся по две на ухо, шесть цветов, итого – до тридцати шести комбинаций. На практике численность стада не превышает тридцати, так что хватает с избытком.
На первый взгляд, выглядело тривиально – взять нейросеть, обучить ее распознавать цветные метки, выдать комбинацию двух цветов с учетом максимального разброса положения меток в кадре. Ударили по рукам и разошлись. Заказчик занялся подготовкой обучающего и эталонного наборов, а мы – сели проектировать прототип сервиса распознавания.
Школа железных мозгов
Небольшое лирическое отступление для тех, кто еще не знает, как проходит обучение нейросетей под задачи классификации: для начала вы берете большое количество образцов, например, фотографий, и для каждой определяете координаты прямоугольников с интересующими вас объектами. Руками. Вот IMG_023410282018.JPG, по координатам 0,0-230,168 у нас объект с тэгом “кошка”. И так далее. Пачка из размеченных фотографий называется набором.
Коллеги из 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 – можно было увидеть, как проходит “свинский” день жизни. Колорита добавляли солнечные блики на фотографиях, проползавшие из угла в угол в зависимости от времени суток. Эти же блики попили нам крови после того, как мы расправились с этапом определения местоположения меток в кадре.
Свинки, к слову, вели себя отлично в качестве моделей. Вот одна стоит у кормушки и метки видны полностью. А вот эта – лежит на боку, и видны только торцы меток. А вон та – вообще кверху пузом валяется, и меток не видно. Единственное, что нас расстроило – это количество фотографий. Ни о каком десятке тысяч, конечно, речь не шла – всего несколько тысяч. Мы, скрепя сердце, поделили набор на две неравные кучки и принялись учить нейросеть.
И снова лирическое отступление. Обучение – процесс не только долгий, но и дорогой. И не только потому, что надо заплатить за разметку фотографий, но и за железо. Тут всего два варианта – пользоваться 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… Не то, чтобы это все было нужно, но когда всерьез начал заниматься машинным обучением, – надо идти в своей одержимости до конца.
Цвета меток оказались неравномерно разбросаны по цветовому кубу, и, в случае различных вариаций освещения, вектор легко прилипал к ошибочному цвету, выдавая cyan вместо yellow, или magenta вместо red. Окей, с кубом не прокатило. Мы выкинули RGB и поставили цветовое колесо, нормализуя яркость. Помогло, но не сильно. В итоге, после пары убитых дней, мой датасатанист с воплем “Да как так-то!” отложил на полчаса свой текущий проект и быстренько набросал двухслойный перцептрон на шесть выходов. И вы знаете – сработало! Конечно, перцептрон тоже пришлось обучить, но это было уже сильно проще и быстрее.
Настал день икс. Мы сели показывать заказчику “товар лицом”. Чуть-чуть не дотянули по надежности распознавания до заданной. А вот с цветом уже проблем не было – эту планку мы взяли, о чем и было честно сказано и показано во время презентации. И через несколько дней заказчик принял решение стартовать второй этап.
Press F for next level
– В идеале нам надо по фотографии свиньи находить разность и вычислять привес или потерю веса.
После созвона коллега озадаченно почесал бороду.
– Вес по фото. Прямо какая-то диагностика по аватарке. Это как? Это нам как-то нормализовать и усреднять объем свиньи по множеству её ракурсов? А потом считать плотность свиньи на квадратный пиксел?!
– Прекрасная метрика, – пробормотал я, – Пожалуй, запишу.
Если бы на этом все закончилось. Но нет. Через пару дней ко мне зашел другой коллега.
– Слушай, вы тут какую-то штуку для измерения свиней прототипировали…
– Да-да, могу в красках все рассказать!
– Расскажи на чем она у вас работает, мне ее портировать придется.
– Ну, э … Стандартный x86-64 Linux, TensorFlow, CUDA, Docker с пробросом GPU. А на что портировать?
– На Raspberry Pi 3. RTX 2070 дорого, говорят. Надо в пятьдесят долларов уложиться.
Что-то больно ударило меня по ноге, и это была моя челюсть. Забегая вперед – таки да, этот парень впихнул нашу модель на “малинку” и добился того, что распознавание на ней занимало не более десятка секунд – тогда как на видеокарте мы добились минимум 2-3 секунды. Но это уже другая история. Не переключайтесь.