Beard shape predictor dat что это
Можно ли загрузить / прочитать shape_predictor_68_face_landmarks.dat во время компиляции?
этот shape_predictor_68_face_landmarks.dat Это обученная модель для 68 ориентиров для обнаружения входного изображения, которую необходимо загружать во время выполнения каждый раз для обнаружения. Я пытаюсь сделать следующие вещи.
Есть ли способ упаковать этот файл в мое приложение, чтобы потребовалось меньше физической памяти для запуска.
Обновить:
Как я могу хранить это shape_predictor_68_face_landmarks.dat файл в статическом буфере, чтобы каждый раз shape_predictor можно читать из этого буфера.
Решение
Да, это возможно, но зависит от Visual Studio, а не кросс-платформенный
Вы должны создать файл ресурсов и включить hape_predictor_68_face_landmarks.dat в ваш проект. Увидеть https://msdn.microsoft.com/ru-ru/library/7zxb70x7.aspx для деталей. Это заставит компилятор поместить этот файл в ваш exe / dll
Создать поток памяти (std :: istream) из указателя.
Вот минимальный пример, но без чтения ресурса:
И про использование памяти.
Прежде всего вы должны знать, что shape_predictor :: operator () является const, и в документации сказано, что безопасно использовать один shape_predictor для разных потоков.
Таким образом, вы можете создать один shape_predictor в начале программы и использовать его много раз, даже из разных потоков.
Затем, поместив предиктор формы в ресурс, он будет загружен в ОЗУ при запуске программы, но десериализация его из ресурса сделает копию этой памяти, что приведет к накладным расходам на использование ОЗУ. Если вам нужно минимально возможное использование ОЗУ — загрузите его из файла
И последний ваш вопрос — как его инициализировать компилятором. Для него не существует готового к использованию решения, но вы можете использовать код из функции shape_predictor.h / deserialize и загрузить его вручную. Я думаю, это плохое решение, потому что вы не будете использовать меньше оперативной памяти по сравнению с загрузкой файла
Поэтому я рекомендую загрузить из файла один shape_predictor и использовать его глобально для всех потоков.
Вы и Брэд Питт похожи на 99%
Мы в отделе аналитики онлайн-кинотеатра Okko любим как можно сильнее автоматизировать подсчёты сборов фильмов Александра Невского, а в освободившееся время учиться новому и реализовывать классные штуки, которые почему-то обычно выливаются в ботов для Телеграма. К примеру, перед началом чемпионата мира по футболу 2018 мы выкатили в рабочий чат бота, который собирал ставки на распределение итоговых мест, а после финала подсчитал результаты по заранее придуманной метрике и определил победителей. Хорватию в четвёрку не поставил никто.
Недавнее же свободное от составления ТОП-10 российских комедий время мы посвятили созданию бота, который находит знаменитость, на которую пользователь больше всего похож лицом. В рабочем чате идею все настолько оценили, что мы решили сделать бота общедоступным. В этой статье мы кратко вспомним теорию, расскажем о создании нашего бота и о том, как сделать такого самому.
Немного теории (в основном в картинках)
Подробно о том, как устроены системы распознавания лиц я рассказывал в одной из предыдущих своих статей. Интересующийся читатель может перейти по ссылке, а я изложу ниже лишь основные моменты.
Итак, у вас есть фотография, на которой, возможно, даже изображено лицо и вы хотите понять, чьё же оно. Для этого вам необходимо выполнить 4 простых шага:
Выделение лица
И хотя свёрточные нейронные сети в последнее время уже научились находить лица на изображении не хуже классических методов, они всё-ещё уступают классическому HOG-у по скорости и удобству использования.
HOG — Histograms of Oriented Gradients. Каждому пикселю исходного изображения этот парень ставит в соответствие его градиент — вектор, в направлении которого яркость пикселей изменяется сильнее всего. Преимущество такого подхода в том, что ему не важны абсолютные значения яркости пикселей, достаточно лишь их отношения. Поэтому и нормальное, и затемнённое, и плохо освещённое, и шумное лицо отобразится в примерно одинаковую гистограмму градиентов.
Выделение ключевых точек
Ключевые точки — точки, которые помогают идентифицировать лицо в пространстве. Слабым и неуверенным в себе дата саентистам обычно нужно 68 ключевых точек, а в особо запущенных случаях и того больше. Нормальным же и уверенным в себе пацанам, зарабатывающим 300к в секунду, всегда было достаточно пяти: внутренние и внешние уголки глаз и нос.
Извлечь такие точки можно, например, каскадом регрессоров.
Выравнивание лица
Клеил в детстве аппликации? Тут всё точно так же: строишь аффинное преобразование, которое переводит три произвольные точки в их стандартные позиции. Нос можно оставить как есть, а для глаз посчитать их центры — вот и три точки готовы.
Преобразование изображения лица в вектор
С момента публикации статьи про FaceNet прошло уже три года, за это время появилось множество интересных схем обучения и функций потерь, но среди доступных OpenSource решений доминирует именно она. Видимо, всё дело в комбинации простоты понимания, реализации и приличных достигаемых результатах. Спасибо хоть на том, что архитектуру за эти три года таки сменили на ResNet.
FaceNet учится на тройках примеров: (anchor, positive, negative). Anchor и positive примеры принадлежат одному человеку, negative же выбирается как лицо другого человека, которое сеть почему-то располагает слишком близко к первому. Функция потерь спроектирована таким образом, чтобы исправить это недоразумение, сблизить нужные примеры и отодвинуть от них ненужный.
Выход последнего слоя сети называется эмбедингом — репрезентативным представлением лица в некотором пространстве малой размерности (как правило, 128-мерном).
Сравнение лиц
Прелесть хорошо обученных эмбедингов в том, что лица одного человека отображается в некоторую небольшую окрестность пространства, отдалённую от эмбедингов лиц других людей. А значит, для этого пространства можно ввести меру схожести, обратную расстоянию: евклидову или косинусному, в зависимости от того, с помощью какой дистанции сеть обучали.
Таким образом, нам заранее нужно построить эмбединги для всех людей, среди которых будет производиться поиск, а затем, для каждого запроса, найти среди них ближайший вектор. Или, по-другому, решить задачу нахождения k ближайших соседей, где k может быть равно одному, а может и нет, если мы хотим использовать какую-нибудь более продвинутую бизнес-логику. Лицо, которому принадлежит вектор-результат, и будет самым похожим на лицо-запрос.
Какую библиотеку использовать?
Делаем собственного бота
Данная секция уже описывалась буквально в каждом руководстве по созданию ботов, но раз и мы пишем такое же, придется повториться. Пишем @BotFather и просим у него токен для нашего нового бота.
Надеюсь, ни у кого на данном этапе не возникнет раздумий при выборе языка программирования. Конечно же, писать надо на Хаскелле. Начнём с главного модуля.
Как видно из кода, в дальнейшем мы будем использовать специальный DSL для написания телеграм ботов. Код на этом DSL пишется в отдельных файлах. Установим доменный язык и всё необходимое.
python-telegram-bot на данный момент является самым удобным фреймворком для создания ботов. Он простой в освоении, гибкий, масштабируемый, поддерживает многопоточность. К сожалению, на данный момент не существует ни одного нормального асинхронного фреймворка и вместо божественных корутин приходится использовать древние потоки.
Начать писать бота с python-telegram-bot очень просто. Добавим в bot.py следующий код.
Такой простой бот способен поддержать минимальную беседу, а следовательно, легко может устроиться работать фронтенд разработчиком.
Но фронтенд разработчиков и так слишком много, поэтому убьём его поскорее и приступим к реализации главной функциональности. В целях простоты наш бот будет отвечать только на сообщения, содержащие фотографии и игнорировать любые остальные. Изменим код на следующий.
Настало время dlib. За пределами функции создадим детектор лиц.
А внутри функции используем его.
Второй параметр функции означает увеличение, которое необходимо применить перед попыткой обнаружения лиц. Чем он больше, тем более мелкие и сложные лица детектор сможет обнаружить, но тем дольше он будет работать. face_detects — список лиц, отсортированный в порядке убывания уверенности детектора в том, что перед ним находится лицо. В реальном приложении вам, скорее всего, захочется применить некоторую логику выбора главного лица, а в учебном примере мы ограничимся лишь выбором первого.
Переходим к следующему этапу — поиску ключевых точек. Скачиваем обученную модель и выносим её загрузку за пределы функции.
Находим ключевые точки.
Дело осталось за малым: выровнять лицо, прогнать его через ResNet и получить 128-мерный эмбединг. К счастью, dlib позволяет сделать всё это одним вызовом. Нужно только скачать предобученную модель.
Только посмотрите, в какое прекрасное время мы живём. Вся сложность свёрточных нейронных сетей, метода опорных векторов и аффинных преобразований, применённых к распознаванию лиц, инкапсулирована в три библиотечных вызова.
Так как мы пока не умеем делать ничего осмысленного, давайте возвращать пользователю среднее значение его эмбединга, умноженное на тысячу.
Добавим загрузку эмбедингов в код нашего бота.
И методом полного перебора найдём, на кого же всё-таки похож наш пользователь.
Обратите внимание, что в качестве расстояния мы используем евклидову дистанцию, т.к. сеть в dlib была обучена именно с помощью неё.
Вот и всё, поздравляю! Мы с вами создали простейшего бота, который умеет определять, на кого из знаменитостей похож пользователь. Осталось найти побольше фотографий, добавить брендирования, масштабируемости, щепотку логгирования и всё, можно выпускать в продакшен. Все эти темы слишком объёмные, чтобы подробно рассказывать про них с огромными листингами кода, поэтому я просто изложу основные моменты в формате вопроса-ответа в следующем разделе.
Полный код учебного бота доступен на GitHub.
Рассказываем про нашего бота
Сколько у вас в базе знаменитостей? Где вы их нашли?
Самым логичным решением при создании бота показалось взять данные о знаменитостях из нашей внутренней контентной базы. Она в формате графа хранит фильмы и все сущности, которые с фильмами связаны, в том числе актёров и режиссёров. Для каждой персоны нам известны её имя, логин и пароль от iCloud, связанные фильмы и alias, который можно использовать для генерации ссылки на сайт. После очистки и извлечения только необходимой информации остаётся json файл следующего содержания:
Таких записей в каталоге оказалось 22000. Кстати, не каталог, а каталог.
Где найти фотографии для всех этих людей?
Ну знаете, то тут, то там. Есть, например, прекрасная библиотека, которая позволяет загружать картинки-результаты запроса из гугла. 22 тысячи человек — не так уж и много, используя 56 потоков нам удалось скачать фотографии для них меньше чем за час.
Среди скаченных фотографий нужно отбросить битые, шумные, фотографии в неправильном формате. Затем оставить только те, где есть лица и где эти лица удовлетворяют определённым условиям: минимальному расстоянию между глаз, наклоном головы. Всё это оставляет нас с 12000 фотографий.
Из 12 тысяч знаменитостей пользователи на данный момент обнаружили только 2. То есть существуют примерно 8 тысяч знаменитостей пока ещё ни на кого не похожих. Не оставляйте это просто так! Открывайте телеграм и найдите их всех.
Как определить процент схожести для евклидовой дистанции?
Отличный вопрос! Действительно, евклидово расстояние, в отличии от косинусного, не ограничено сверху. Поэтому возникает резонный вопрос, как показать пользователю нечто более осмысленное, чем «Поздравляем, расстояние между вашим эмбедингом и эмбедингом Анжелины Джоли составляет 0.27635462738»? Один из членов нашей команды предложил следующее простое и гениальное решение. Если построить распределение расстояний между эмбедингами, оно окажется нормальным. А значит, для него можно посчитать среднее и стандартное отклонение, а затем для каждого пользователя по этим параметрам считать сколько процентов людей менее похожи на своих знаменитостей, чем он. Это эквивалентно интегрированию функции плотности вероятности от d до плюс бесконечности, где d — дистанция между эмбедингами пользователя и знаменитости.
Вот точная функция, которую мы используем:
Неужели нужно перебирать список всех эмбедингов, чтобы найти совпадение?
Конечно нет, это не оптимально и занимает кучу времени. Простейший способ оптимизировать вычисления — использовать матричные операции. Вместо того, чтобы вычитать вектора один из другого, можно составить из них матрицу и вычитать из матрицы вектор, а затем посчитать L2 норму по строкам.
Это уже даёт гигантский прирост производительности, но, оказывается, можно ещё быстрее. Поиск можно значительно ускорить, немного проиграв в его точности, используя библиотеку nmslib. Она использует метод HNSW для приближенного поиска k ближайших соседей. По всем имеющимся векторам должен быть построен так называемый индекс, в котором затем и будет производиться поиск. Создать и сохранить на диск индекс для евклидовой дистанции можно следующим образом:
Параметры M и efConstruction подробно описаны в документации и подбираются экспериментально исходя из требуемой точности, времени построения индекса и скорости поиска. Перед использованием индекс необходимо загрузить:
В нашем случае nmslib работает в 20 раз быстрее, чем векторизованная линейная версия, а один запрос выполняется в среднем 0.005 секунды.
Как сделать моего бота готовым к продакшену?
1. Асинхронность
Для начала, необходимо сделать функцию handle_photo асинхронной. Как я уже говорил, python-telegram-bot предлагает для этого воспользоваться многопоточностью и реализует удобный декоратор.
Я не упомянул dlib неспроста. Библиотека, вызывающая нативный код, не обязана отпускать GIL и dlib этим правом пользуется. Ей не нужна эта блокировка, она просто её не отпускает. Автор говорит, что с радостью примет соответствующий Pull Request, но мне лень.
2. Многопроцессность
Простейший способ разобраться с dlib — инкапсулировать модель в отдельную сущность и запускать её в отдельном процессе. А лучше в пуле процессов.
3. Железо
Если ваш бот должен постоянно читать фотографии с диска, позаботьтесь о том, чтобы этим диском был SSD. Или вообще смонтируйте их в оперативную память. Так же важен пинг до серверов телеграма и качество канала.
4. Flood control
Телеграм не позволяет ботам отправлять больше 30 сообщений в секунду. Если ваш бот популярен и им одновременно пользуется куча народу, то очень легко поймать бан на несколько секунд, который обернётся разочаровнием от ожидания для множества пользователей. Для решения этой проблемы python-telegram-bot предлагает нам очередь, которая способна не отправлять больше заданного лимита сообщений в секунду, выдерживая равные интервалы между отправкой.
5. Web hooks
В продуктовом окружении в качестве способа получения обновлений от серверов Телеграма вместо Long Polling всегода стоит использовать Web Hooks. Что это вообще такое и как это использовать можно почитать здесь.
6. Мелочи
Полезной мелочью стала настройка времени ожидания для всех блокирующих IO-операций: загрузки и отправки фотографий, отправки сообщений, получения обновлений. Его увеличение не сказалось на производительности, но сильно повысило стабильность бота и уменьшило число ошибок.
6. Аналитика
Нельзя улучшить то, что нельзя измерить. Собирайте статистику по действиям пользователя при использовании бота, замеряйте время ответа, время ожидания, собирайте подробную информацию по каждой произошедшей ошибке. Так вы всегда сможете корректно оценивать пользовательский опыт, заранее замечать аварийные ситуации и быстро их исправлять.
Мы, например, настроили импорт логов в наш BI-tool Splunk и постоянно следим за ситуацией.
Вот и всё, о чём бы нам хотелось рассказать в этой статье. Мы вспомнили основы распознавания лиц, показали вам как создать собственного простого бота для распознавания лиц на основе открытых технологий и постарались кратко рассказать про создание нашего собственного бота.
Автоматизируйте всё что автоматизируется, уделяйте время проектам для саморазвития и приходите домой пораньше. Ну и конечно же заходите посмотреть, что у нас получилось: @OkkoFaceBot.
Можете ещё фильмы в отличном качестве посмотреть нет, это уже слишком много рекламы. Кстати, у нас в сервисе нет рекламы.
Ищем знакомые лица
Идентификация Борна (и не только Борна)
Например, если требуется отделить лица людей от лиц обезьян, то это задача классификации — имеются два класса, для каждого объекта можно указать класс и сделать репрезентативную выборку из обоих классов. Если требуется определить какому человеку принадлежит изображение лица и люди эти — конечное фиксированное множество, то это тоже задача классификации.
А теперь представьте, что вы разрабатываете приложение, которое должно определять человека по фотографии его лица, причём множество запомненных в базе людей постоянно меняется и, естественно, во время использования приложение будет видеть людей, которых не было в обучающем множестве — реальная задача, которой в современном мире уже никого не удивишь. Однако, она уже не сводится к задаче классификации. Как же её решать?
Чтобы уметь узнавать человека, необходимо его хотя бы раз увидеть. А точнее, иметь хотя бы одну его фотографию или запомнить её в форме некоторого образа. Тогда, когда нам покажут новую, неизвестную доселе фотографию, мы сравним её со всеми запомненными образами и сможем дать ответ: видели мы уже этого человека и можем его идентифицировать или же этот человек нам ещё ни разу не встречался и мы можем разве что его запомнить. Таким образом изложенная выше задача сводится к следующей: имея две фотографии определить, относятся ли они к одному человеку или нет. Другими словами, обладают ли они свойством принадлежности одному человеку:
Вот и определение задачи идентификации: по обучающей выборке (в примере: множество пар лиц ) построить идентификатор
, который сможет определять, обладает объект требуемым признаком или нет. Но дискретность это скучно, гораздо интереснее узнать степень выраженности признака
у объекта, равную вероятности
.
Поставим последнюю задачу из примера чуть более формально и героически её решим, позвав на помощь arxiv.org, Python и Keras. Фотографии лица — матрицы из . По двум лицам мы хотим знать вероятность их принадлежности одному человеку. Вероятность — вещественное число от 0 до 1. Значит, мы ищем функцию
. Так как на дворе уже 2017 год, искать мы её будем методами машинного обучения. Обучающим множеством, однако, у нас будет не множество пар из определения, а множество фотографий лиц разных людей с метками, кому какое принадлежит — прямо как для задачи классификации. Множества эти эквивалентны, однако со вторым проще работать.
Превосходство Борна
Что важнее всего в решении задач машинного обучения? Думаете, умение искать ответ? Нет, главное — умение этот ответ верифицировать. Функция , возвращающая случайные числа из интервала
является вполне корректным решением поставленной задачи, однако на практике она бесполезна. Как это узнать, не прибегая к методу «на глаз»? В задаче классификации мы можем посмотреть accuracy на валидационном множестве — долю правильно классифицированных примеров. Для задачи идентификации такая метрика не применима. Так как же объективно оценить пригодность решения нашей задачи?
Давайте введём понятие target-попытки и imposter-попытки. Первым мы назовём объект для которого известно, что
, т.е. объект обладает требуемым свойством с вероятностью 1 (в нашей задаче — пара лиц
, принадлежащих одному человеку), а вторым, соответственно, объект
такой, что
. Соответственно, рассмотрим множества
и
, которые вместе будут являться нашим валидационным множеством:
. Соображения по его выбору абсолютно стандартны, как и для любой задачи машинного обучения — оно должно быть репрезентативно, т.е. отражать всю требуемую вариативность и иметь достаточный размер. Например, если ваша система распознавания лиц предполагает работу в условиях разного освещения, эти условия должны быть представлены в валидационном множестве (как и в обучающем, конечно).
Теперь возьмём нашу построенную функцию и построим
— результат применения
ко всем target-попыткам. Получим множество вещественных чисел из интервала
или score-ов. Эти значения — мера того, насколько наше решение хорошо работает в тех случаях, когда его не пытаются обмануть и ждут от него положительный ответ. Причём
— некоторая выборка, которую мы считаем репрезентативной. Построим её эмпирическую плотность — гистограмму.
Так она выглядит для :
Согласитесь, толку от такого идентификатора немного — в половине случаев он будет угадывать ответ, а в половине — ошибаться.
Такое распределение нам уже больше подходит:
Для target-попыток такая функция будет скорее уверена в их правильности, чем нет.
Но рассмотрение распределения таргетов без распределения импостеров бессмысленно. Проделаем ту же операцию и для них: построим плотность распределения и отобразим на том же графике. Получим подобную картину:
Становится видно, что для imposter-попыток наша функция будет в большинстве случаев склоняться к правильному ответу. Но это всё ещё лишь визуальные наблюдения, они не дают нам никакой объективной оценки.
Допустим, нашей системе на вход подаётся пара изображений. Она может посчитать для них вероятность того, что это target-попытка. Но требуется от неё однозначный ответ: один и тот же это человек или нет, пускать его на секретный объект или нет. Давайте зададимся некоторым порогом и если
, будем отвечать отрицательно, иначе — положительно. Если
, система никогда никого не узнает, а если
, то любых двух человек станет считать одинаковыми. По графику видно, что наши распределения не разделимы и нельзя подобрать
так, чтобы добиться идеальной работы в обоих случаях. Что будет, например, если в примере выше установить
?
В скольких случаях наша система будет ошибаться при target-попытках? Легко посчитать: — количество target-попыток, которые будут неверно классифицированы как imposter-попытки. Аналогично и для последних. А теперь дадим им имена и сделаем не абсолютными, а относительными:
Теперь для любого выбранного расстояния можно сказать, какая доля target-попыток будет отклоняться и какая доля imposter-попыток приниматься. И наоборот, можно выбирать исходя из задачи. Например, для верификации на охраняемом объекте, где важно не пропустить чужака, по понятным причинам требуется
, обеспечивающий минимально возможный FAR. Если же вы хотите, чтобы компьютер вас узнавал и желал доброго утра, а по квартире обычно ходите только вы, можно остановиться на низком FRR и достаточно высоком FAR — не случится ничего плохого, если компьютер поздоровается с кем-то, назвав его вашим именем.
Обратите внимание на точку пересечения графиков. Значение в ней называется EER (Equal Error Rate).
Если мы выберем , значения FAR и FRR окажутся равны. EER — тот самый объективный критерий, к которому мы шли. Он позволяет оценить качество идентификации в целом: это средняя ошибка на нашем валидационном множестве. Ещё можно смотреть на FAR при фиксированном FRR и наоборот, но чаще всего в качестве метрики используют именно EER.
В примере выше EER = 0.067. Значит, в среднем 6.7% всех target-попыток будет отклоняться и 6.7% всех imposter-попыток — приниматься.
Ещё одним важным понятием является DET-кривая — зависимость FAR от FRR в логарифмическом масштабе. По её форме легко судить о качестве системы в целом, оценивать какое значение одного критерия можно получить при фиксированном втором, и, что самое главное, сравнивать системы.
ERR здесь — пересечение DET-кривой с прямой .
Наивная реализация на Python (можно более оптимально, если учесть, что FAR и FRR меняются только в точках из ):
С контролем разобрались: теперь, какую бы функцию мы ни выбрали, можем посчитать для неё FAR, FRR и ERR на валидационном множестве и построить несколько наглядных графиков.
Важно: в задачах идентификации то, что мы выше называли валидационным множеством, принято называть development set (development-множество, devset). Будем придерживаться этого обозначения и в дальнейшем.
Важно: так как любой интервал вещественной оси можно отобразить в отрезок
, совершенно не обязательно, чтобы значение нашей функции находилось в пределах от нуля до единицы. Даже без отображения в единичный отрезок можно рассматривать любой диапазон значений без влияния на результаты.
Подготовка базы
Существует множество датасетов для распознавания лиц. Некоторые платные, некоторые выдаются по запросу. Некоторые содержат большую вариативность по освещению, другие — по положению лица. Некоторые сняты в лабораторных условиях, другие собраны из фотографий, снятых в естественной среде обитания. Чётко сформулировав требования к данным, можно легко выбрать подходящий датасет или собрать его из нескольких. Для меня в рамках данной учебной задачи требования были таковы: датасет должен быть легко доступен для скачивания, содержать не очень много данных и содержать вариативность по положению лица. Требованиям удовлетворили три набора данных, которые я объединил в один:
В полученной таким образом базе оказалось 277 субъектов и
4000 изображений, в среднем по 14 изображений на человека. Возьмём 5-10% субъектов для development-множества, остальных используем для обучения. Система при обучении должна видеть лишь примеры из второго множества, а проверять её (считать EER) мы будем на первом.
Код для разделения данных доступен по ссылке. Необходимо лишь указать путь до распакованных датасетов, перечисленных выше.
Теперь данные необходимо предобработать. Для начала, выделить лица. Сами мы этим заниматься не будем, а используем библиотеку dlib.
Как видите, используя эту библиотеку, можно получить ограничивающий лицо прямоугольник за пару строчек кода. И работает детектор dlib, в отличии от OpenCV, очень хорошо: из всей базы лишь десяток лиц он не смог обнаружить и не создал ни одного ложного срабатывания.
Формальная постановка нашей задачи подразумевает, что все лица должны быть одного размера. Удовлетворим это требование, заодно выровняв все лица так, чтобы ключевые точки (глаза, нос, губы) всегда находились в одном и том же месте изображения. Ясно, что такая мера может нам помочь безотносительно выбранного способа обучения и уж точно не навредит. Алгоритм прост:
Для поиска ключевых точек на изображении лица опять-же воспользуемся dlib, используя уже обученную модель, которую можно найти там-же, в примерах (shape_predictor_68_face_landmarks.dat, скачать здесь).
Аффинное преобразование однозначно задаётся тремя точками:
Пусть — исходные точки, которые мы хотим перевести в
. Тогда аффинное преобразование, выраженное матрицей
можно найти из соотношения
И применим к изображению с помощью библиотеки scipy-image:
Полный код препроцессинга, обёрнутый в удобный api, можно найти в файле preprocessing.py.
Финальным аккордом подготовки данных станет нормализация: посчитаем среднее и стандартное отклонение по базе обучения и нормируем каждое изображение на них. Не забудем и про development-множество. Код смотрите здесь.
Собранные, разделённые, выровненные и нормированные данные можно скачать здесь.
Ультиматум Борна
Данные нашли и подготовили, с методикой тестирования разобрались. Полдела сделано, осталось самое лёгкое — найти , решающую задачу с хорошим EER. Давайте сразу определимся, что EER = 10% нас вполне устроит для такой учебной задачи. На самом деле, такая система даже сможет быть использована в простых приложениях вроде поиска одинаковых лиц на двух фотографиях.
Монетка
Начнём поиски с того самого примера плохой функции . Для каждой пары фотографий из development-множества получим случайное значение, построим по ним DET-кривую и найдём EER.
C EER = 49.5% такой идентификатор не лучше монетки, которую бы мы подбрасывали при принятии каждого решения. Конечено, это понятно и без графиков, но наша цель — научиться решать задачи идентификации и уметь объективно оценивать любые решения, даже заведомо плохие. К тому же, будет от чего оттолкнуться.
Расстояние
Какая функция от двух векторов из , возвращающая вещественное число, приходит на ум первой? Уверен, большинство из вас на этот вопрос ответят — расстояние. Действительно,
— метрическое пространство, а значит для любых двух элементов
и
из него определено расстояние
, которое можно вводить разными способами. Вот только рассматривать его нужно с минусом, так как расстояние изменяется от нуля до плюс бесконечности, а в принятой нами формализации должно быть наоборот.
Возьмём, например, косинусную дистанцию:
И проделаем все те же самые операции над development-множеством:
Получим такую DET-кривую:
EER уменьшился на 16% и стал равен 34.18%. Лучше, но всё-ещё не применимо. Конечно, ведь мы до сих пор лишь подбирали функцию, никак не используя обучающее множество и методы машинного обучения. Однако, идея с расстоянием здравая: давайте его оставим и представим нашу функцию в виде
где — косинусная дистанция, а
— некоторая функция, которую мы назовём embedder, а результаты её работы из пространства
— embeddings. Она «встраивает» наши изображения в некоторое пространство другой (не обязательно меньшей) размерности, учитывая апостериорный опыт, добытый из обучающего множества.
Отлично, мы с вами только-что ещё сильнее упростили задачу. Осталось лишь найти хорошую функцию , все остальные части системы уже на месте. Не будем ходить вокруг да около — все мы знаем, что на текущий момент нет модели, лучше справляющейся с изображениями, чем CNN (Convolutional Neural Networks). Давайте построим конволюционную сеть какой-нибудь не очень сложной архитектуры, например такой:
И обучим её решать классическую задачу классификации на нашем обучающем множестве: определять, кому из 250 субъектов принадлежит фотография лица. Такую простую задачу решать умеют все, в keras для этого помимо приведённого выше кода понадобится ещё строчек 5-6. Скажу лишь, что для базы обучения, описанной в этой статье, жизненно необходимо применить аугментацию, иначе данных не хватит, чтобы достичь хороших результатов.
Спрашиваете, при чём здесь задача классификации и как нам поможет её решение? Правильно делаете! Чтобы описанные далее действия имели смысл, необходимо сделать очень важное предположение: если сеть научится хорошо решать задачу классификации в закрытом множестве, то на предпоследнем слое размерностью 256 будет сосредоточена вся важная информация об изображении лица, даже если субъекта не было в обучающем множестве.
Такая техника извлечения характеристик низкой размерности из последних слоёв обученной сети широко распространена и носит название bottleneck. Кстати, код для работы с bottleneck в keras находится по ссылке.
Сеть обучили, 256-мерные признаки из development-множества извлекли. Смотрим на DET-кривую:
Предположение оказалось верным, уменьшили EER ещё на 13%, достигнув результата 21.6%. В два раза лучше, чем подбрасывание монетки. А можно ещё лучше? Конечно, можно собрать базу побольше и повариативнее, построить более глубокую CNN, применить различные методы регуляризации… Но мы с вами рассматриваем концептуальные подходы, качественные. А количество всегда можно нарастить. Ещё один козырь в рукаве у меня всё-таки остался, но перед тем как выложить его на стол, придётся немного отвлечься.
Эволюция Борна
Ключ к улучшению наших результатов кроется в осознании того факта, что оптимизируя можно использовать информацию не только из обучающего множества, но и информацию о функции
. Действительно, давайте зафиксируем
и будем обучать
исходя из априорного знания о том, как затем embedding-и будут использованы для получения score. Впервые такой подход предложили ребята из Google в работе FaceNet: A Unified Embedding for Face Recognition and Clustering.
Подход, предложенный ими, получил название TDE (Triplet Distance Embedding) и состоял в следующем: давайте построим как сеть из исходного пространства
в пространство embedding-ов
без необходимости решать промежуточную задачу классификации, зафиксируем
как евклидово расстояние и учтём его в loss-функции. Каким образом? Вполне интуитивным: мы хотим, чтобы вектора одного субъекта были в целевом пространстве как можно ближе к друг другу и при этом, дальше от векторов других субъектов.
Обучать такую сеть было предложено с помощью троек , где
(anchor) и
(positive) принадлежат одному субъекту, а
(negative) — другому. Для всех трёх векторов построим embedding-и
,
и
. Заранее зададимся некоторым параметром
. Будем считать, что тройка хорошая, если выполняется соотношение
которое означает, что для данного anchor-а между сферами, на которых лежит positive и negative имеется зазор . Если такое соотношение выполняется для всех возможных троек из обучающего множества, мы идеально разделили данные. А обучать сеть имеет смысл только на тех тройках, для которых это неравенство нарушается. Исходя из неравенства, можно построить loss-функцию для сети
:
Используя такой подход, авторы уменьшили ошибку на 30% на датасетах Labeled Faces in the Wild и YouTube Faces DB, что, несомненно, очень круто. Однако, есть у подхода и проблемы:
Зачем вводить лишний параметр , когда мы можем потребовать соблюдения гораздо более простого неравенства? Вот оно:
Оно проще исходного и легко интерпретируется: мы хотим, чтобы самый близкий к нам negative-пример лежал дальше, чем самый далёкий от нас positive-пример, но между ними не обязательно должен быть какой-либо зазор. Благодаря тому, что мы не прекращаем обновлять сеть, когда расстояние достигнуто, группы embedding-ов могут быть разнесены в пространстве
ещё лучше.
Можем посчитать вероятность того, что триплет удовлетворяет приведённому неравенству:
Разделим на :
Будем максимизировать логарифм вероятности, поэтому loss-функция будет выглядеть следующим образом:
А в качестве функции авторы предлагают использовать не гигантскую CNN, а простую матрицу:
и учить её на bottleneck признаках, уже полученных нами. Вот результаты, которых достигли авторы работы:
Как видно, такой подход работает лучше оригинального и имеет множество плюсов:
Если вы захотите использовать TPE в своих проектах, не поленитесь прочитать оригинальные работы, так как я не осветил самый главный вопрос обучения триплетами — вопрос их отбора. Для нашей небольшой задачи хватит и случайного выбора, однако это скорее исключение, чем правило.
Обучим TPE на наших bottleneck-ах и взглянем на DET-кривую последний на сегодня раз:
EER равный 12% — очень близко к тому, что мы хотели. Это в два раза лучше, чем просто использование CNN и в 5 раз лучше случайного выбора. Результат, конечно, можно улучшить, использовав более глубокую архитектуру и большую базу, но для понимания принципа и такой результат удовлетворителен.
Сравнение DET-кривых всех рассмотренных методов:
Осталось прикрутить к нашей системе всяческую машинерию и интерфейс, будь то web-интерфейс или приложение на Qt, и программа для поиска одинаковых лиц на фотографиях готова.
С приложением можно ознакомиться на GitHub.
Спасибо за прочтение! Ставьте лайки, подписывайтесь на профиль, оставляйте комментарии, учите машин хорошему. Дополнения приветствуются.