Главная » Хабрахабр » Вы и Брэд Питт похожи на 99%

Вы и Брэд Питт похожи на 99%

Завтра в отпуск

К примеру, перед началом чемпионата мира по футболу 2018 мы выкатили в рабочий чат бота, который собирал ставки на распределение итоговых мест, а после финала подсчитал результаты по заранее придуманной метрике и определил победителей. Мы в отделе аналитики онлайн-кинотеатра Okko (Мама, привет, я теперь работаю на Rambler) любим как можно сильнее автоматизировать подсчёты сборов фильмов Александра Невского, а в освободившееся время учиться новому и реализовывать классные штуки, которые почему-то обычно выливаются в ботов для Телеграма. Хорватию в четвёрку не поставил никто.

В рабочем чате идею все настолько оценили, что мы решили сделать бота общедоступным. Недавнее же свободное от составления ТОП-10 российских комедий время мы посвятили созданию бота, который находит знаменитость, на которую пользователь больше всего похож лицом. В этой статье мы кратко вспомним теорию, расскажем о создании нашего бота и о том, как сделать такого самому.

Интересующийся читатель может перейти по ссылке, а я изложу ниже лишь основные моменты. Подробно о том, как устроены системы распознавания лиц я рассказывал в одной из предыдущих своих статей.

Для этого вам необходимо выполнить 4 простых шага: Итак, у вас есть фотография, на которой, возможно, даже изображено лицо и вы хотите понять, чьё же оно.

  1. Выделить прямоугольник, ограничивающий лицо.
  2. Выделить ключевые точки лица.
  3. Выровнять и обрезать лицо.
  4. Преобразовать изображение лица в некоторое машинно интерпретируемое представление.
  5. Сравнить это представление с другими, имеющимися у вас в наличии.

Выделение лица

И хотя свёрточные нейронные сети в последнее время уже научились находить лица на изображении не хуже классических методов, они всё-ещё уступают классическому HOG-у по скорости и удобству использования.

Каждому пикселю исходного изображения этот парень ставит в соответствие его градиент — вектор, в направлении которого яркость пикселей изменяется сильнее всего. HOG — Histograms of Oriented Gradients. Поэтому и нормальное, и затемнённое, и плохо освещённое, и шумное лицо отобразится в примерно одинаковую гистограмму градиентов. Преимущество такого подхода в том, что ему не важны абсолютные значения яркости пикселей, достаточно лишь их отношения.

Гистограмма направленных градиентов лица лица

По полученному векторному полю затем можно пройтись некоторым детектором с окном и определить для каждого окна, насколько вероятно в нём находится лицо. Считать градиент для каждого пикселя нет необходимости, достаточно посчитать средний градиент для каждого маленького квадратика n на n. В качестве детектора может выступать и SVM, и случайный лес, и вообще всё что угодно.

Детекция лица лица

Выделение ключевых точек

Ключевые точки лица лица

Слабым и неуверенным в себе дата саентистам обычно нужно 68 ключевых точек, а в особо запущенных случаях и того больше. Ключевые точки — точки, которые помогают идентифицировать лицо в пространстве. Нормальным же и уверенным в себе пацанам, зарабатывающим 300к в секунду, всегда было достаточно пяти: внутренние и внешние уголки глаз и нос.

Старый мем

Извлечь такие точки можно, например, каскадом регрессоров.

Выравнивание лица

Тут всё точно так же: строишь аффинное преобразование, которое переводит три произвольные точки в их стандартные позиции. Клеил в детстве аппликации? Нос можно оставить как есть, а для глаз посчитать их центры — вот и три точки готовы.

Вращаем лицо лица

Преобразование изображения лица в вектор

Менее старый мем

Видимо, всё дело в комбинации простоты понимания, реализации и приличных достигаемых результатах. С момента публикации статьи про FaceNet прошло уже три года, за это время появилось множество интересных схем обучения и функций потерь, но среди доступных OpenSource решений доминирует именно она. Спасибо хоть на том, что архитектуру за эти три года таки сменили на ResNet.

Новый мем

Anchor и positive примеры принадлежат одному человеку, negative же выбирается как лицо другого человека, которое сеть почему-то располагает слишком близко к первому. FaceNet учится на тройках примеров: (anchor, positive, negative). Функция потерь спроектирована таким образом, чтобы исправить это недоразумение, сблизить нужные примеры и отодвинуть от них ненужный.

GucciNet

Лица лица и Дмитрия Маликова

Выход последнего слоя сети называется эмбедингом — репрезентативным представлением лица в некотором пространстве малой размерности (как правило, 128-мерном).

Сравнение лиц

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

Совершенно искуственный пример эмбедингов

Или, по-другому, решить задачу нахождения k ближайших соседей, где k может быть равно одному, а может и нет, если мы хотим использовать какую-нибудь более продвинутую бизнес-логику. Таким образом, нам заранее нужно построить эмбединги для всех людей, среди которых будет производиться поиск, а затем, для каждого запроса, найти среди них ближайший вектор. Лицо, которому принадлежит вектор-результат, и будет самым похожим на лицо-запрос.

Похожесть лица лица на лицо не лица

Какую библиотеку использовать?

Находить лица и ключевые точки умеют dlib и OpenCV, а предобученные версии сетей можно найти для любого крупного нейросетевого фреймворка. Выбор открытых библиотек, которые реализуют различные части пайплайна велик. Но только одна библиотека позволяет реализовать все 5 пунктов распознавания лиц в вызовах трёх высокоуровневых функций: dlib. Существует проект OpenFace, где можно подобрать архитектуру под свои требования скорости и качества. Наш выбор пал именно на неё. При этом она написана на современном C++, использует BLAS, имеет обёртку для Python, не требует GPU и достаточно быстро работает на CPU.

Пишем @BotFather и просим у него токен для нашего нового бота. Данная секция уже описывалась буквально в каждом руководстве по созданию ботов, но раз и мы пишем такое же, придется повториться.

Размыл токен хз зачем

Он необходим для авторизации при каждом запросе к API Телеграм ботов. Токен выглядит примерно так: 643075690:AAFC8ola8WRdhGbJtzjmkOhne1FGfu1BFg.

Конечно же, писать надо на Хаскелле. Надеюсь, ни у кого на данном этапе не возникнет раздумий при выборе языка программирования. Начнём с главного модуля.

import System.Process main :: IO ()
main = do (_, _, _, handle) <- createProcess (shell "python bot.py") _ <- waitForProcess handle putStrLn "Done!"

Код на этом DSL пишется в отдельных файлах. Как видно из кода, в дальнейшем мы будем использовать специальный DSL для написания телеграм ботов. Установим доменный язык и всё необходимое.

python -m venv .env
source .env/bin/activate
pip install python-telegram-bot

Он простой в освоении, гибкий, масштабируемый, поддерживает многопоточность. python-telegram-bot на данный момент является самым удобным фреймворком для создания ботов. К сожалению, на данный момент не существует ни одного нормального асинхронного фреймворка и вместо божественных корутин приходится использовать древние потоки.

asyncio is my only god

Добавим в bot.py следующий код. Начать писать бота с python-telegram-bot очень просто.

from telegram.ext import Updater
from telegram.ext import MessageHandler, Filters # здесь должен быть ваш токен
TOKEN = '<TOKEN>' def echo(bot, update): bot.send_message(chat_id=update.message.chat_id, text=update.message.text) updater = Updater(token=TOKEN)
dispatcher = updater.dispatcher
echo_handler = MessageHandler(Filters.text, echo)
dispatcher.add_handler(echo_handler)

В отладочных целях это можно делать командой python bot.py, не запуская Хаскелльный код. Запустим бота.

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

Типичный диалог с фронтенд разработчиком

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

from telegram.ext import Updater
from telegram.ext import MessageHandler, Filters # здесь должен быть ваш токен
TOKEN = '<TOKEN>' def handle_photo(bot, update): bot.send_message(chat_id=update.message.chat_id, text='nice') updater = Updater(token=TOKEN)
dispatcher = updater.dispatcher
photo_handler = MessageHandler(Filters.photo, handle_photo)
dispatcher.add_handler(photo_handler) updater.start_polling()
updater.idle()

Уже не фронтенд разработчик

Бот в свою очередь может скачать изображение любого размера из тех, что содержатся в списке message.photo отсортированные по возрастанию. Когда картинка попадает на сервера Telegram, она автоматически подгоняется под несколько заранее заданных размеров. Конечно, в продуктовой среде нужно думать о нагрузке на сеть и времени загрузки и выбирать изображение минимально подходящего размера. Самый простой вариант: взять наибольшее изображение. Добавим код скачивания изображения в начало функции handle_photo.

import io

message = update.message
photo = message.photo[~0] with io.BytesIO() as fd: file_id = bot.get_file(photo.file_id) file_id.download(out=fd) fd.seek(0)

Для его интерпретации и представления в виде матрицы интенсивности пикселей воспользуемся библиотеками Pillow и numpy. Изображение скачано и находится в памяти.

from PIL import Image
import numpy as np

Следующий код необходимо добавить в блок with.

image = Image.open(fd)
image.load()
image = np.asarray(image)

За пределами функции создадим детектор лиц. Настало время dlib.

import dlib

face_detector = dlib.get_frontal_face_detector()

А внутри функции используем его.

face_detects = face_detector(image, 1)

Чем он больше, тем более мелкие и сложные лица детектор сможет обнаружить, но тем дольше он будет работать. Второй параметр функции означает увеличение, которое необходимо применить перед попыткой обнаружения лиц. В реальном приложении вам, скорее всего, захочется применить некоторую логику выбора главного лица, а в учебном примере мы ограничимся лишь выбором первого. face_detects — список лиц, отсортированный в порядке убывания уверенности детектора в том, что перед ним находится лицо.

if not face_detects: bot.send_message(chat_id=update.message.chat_id, text='no faces') face = face_detects[0]

Скачиваем обученную модель и выносим её загрузку за пределы функции. Переходим к следующему этапу — поиску ключевых точек.

shape_predictor = dlib.shape_predictor('path/to/shape_predictor_5_face_landmarks.dat')

Находим ключевые точки.

landmarks = shape_predictor(image, face)

К счастью, dlib позволяет сделать всё это одним вызовом. Дело осталось за малым: выровнять лицо, прогнать его через ResNet и получить 128-мерный эмбединг. Нужно только скачать предобученную модель.

face_recognition_model = dlib.face_recognition_model_v1('path/to/dlib_face_recognition_resnet_model_v1.dat')

embedding = face_recognition_model.compute_face_descriptor(image, landmarks)
embedding = np.asarray(embedding)

Вся сложность свёрточных нейронных сетей, метода опорных векторов и аффинных преобразований, применённых к распознаванию лиц, инкапсулирована в три библиотечных вызова. Только посмотрите, в какое прекрасное время мы живём.

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

bot.send_message( chat_id=update.message.chat_id, text=f'yours embedding mean: '
)

Я не знаю что я делаю

В наш учебный бот мы добавим всего 10 знаменитостей, найдя их фотографии руками и сложив в директорию photos. Чтобы наш бот умел определять на кого из знаменитостей похож пользователей, нам теперь необходимо найти хотя-бы по одной фотографии каждой знаменитости, построить по ней эмбединг и куда-нибудь его сохранить. Вот так примерно это должно выглядеть:

Смотрите, мне не хватило денег на макбук

Теперь создадим утилиту build_embeddings.py, используя уже известные нам вызовы dlib и сохраним эмбединги знаменитостей вместе с их именами в бинарном формате. Если вы хотите иметь в базе миллион знаменитостей, всё будет выглядеть точно так же, только файлов побольше и искать их руками уже вряд ли получится.

import os
import dlib
import numpy as np
import pickle
from PIL import Image face_detector = dlib.get_frontal_face_detector()
shape_predictor = dlib.shape_predictor('assets/shape_predictor_5_face_landmarks.dat')
face_recognition_model = dlib.face_recognition_model_v1('assets/dlib_face_recognition_resnet_model_v1.dat') fs = os.listdir('photos')
es = [] for f in fs: print(f) image = np.asarray(Image.open(os.path.join('photos', f))) face_detects = face_detector(image, 1) face = face_detects[0] landmarks = shape_predictor(image, face) embedding = face_recognition_model.compute_face_descriptor(image, landmarks, num_jitters=10) embedding = np.asarray(embedding) name, _ = os.path.splitext(f) es.append((name, embedding)) with open('assets/embeddings.pickle', 'wb') as f: pickle.dump(es, f)

Добавим загрузку эмбедингов в код нашего бота.

import pickle

with open('assets/embeddings.pickle', 'rb') as f: star_embeddings = pickle.load(f)

И методом полного перебора найдём, на кого же всё-таки похож наш пользователь.

ds = [] for name, emb in star_embeddings: distance = np.linalg.norm(embedding - emb) ds.append((name, distance)) best_match, best_distance = min(ds, key=itemgetter(1)) bot.send_message( chat_id=update.message.chat_id, text=f'your look exactly like *{best_match}*', parse_mode='Markdown'
)

сеть в dlib была обучена именно с помощью неё. Обратите внимание, что в качестве расстояния мы используем евклидову дистанцию, т.к.

Я разочаровался в статье

Мы с вами создали простейшего бота, который умеет определять, на кого из знаменитостей похож пользователь. Вот и всё, поздравляю! Все эти темы слишком объёмные, чтобы подробно рассказывать про них с огромными листингами кода, поэтому я просто изложу основные моменты в формате вопроса-ответа в следующем разделе. Осталось найти побольше фотографий, добавить брендирования, масштабируемости, щепотку логгирования и всё, можно выпускать в продакшен.

Полный код учебного бота доступен на GitHub.

Сколько у вас в базе знаменитостей? Где вы их нашли?

Она в формате графа хранит фильмы и все сущности, которые с фильмами связаны, в том числе актёров и режиссёров. Самым логичным решением при создании бота показалось взять данные о знаменитостях из нашей внутренней контентной базы. После очистки и извлечения только необходимой информации остаётся json файл следующего содержания: Для каждой персоны нам известны её имя, логин и пароль от iCloud, связанные фильмы и alias, который можно использовать для генерации ссылки на сайт.

[ { "name": "Тильда Суинтон", "alias": "tilda-swinton", "role": "actor", "n_movies": 14 }, { "name": "Майкл Шеннон", "alias": "michael-shannon", "role": "actor", "n_movies": 22 },
...
]

Кстати, не каталог, а каталог. Таких записей в каталоге оказалось 22000.

Где найти фотографии для всех этих людей?

Интересно, разрешат ли мне оставить эту картинку

Есть, например, прекрасная библиотека, которая позволяет загружать картинки-результаты запроса из гугла. Ну знаете, то тут, то там. 22 тысячи человек — не так уж и много, используя 56 потоков нам удалось скачать фотографии для них меньше чем за час.

Затем оставить только те, где есть лица и где эти лица удовлетворяют определённым условиям: минимальному расстоянию между глаз, наклоном головы. Среди скаченных фотографий нужно отбросить битые, шумные, фотографии в неправильном формате. Всё это оставляет нас с 12000 фотографий.

То есть существуют примерно 8 тысяч знаменитостей пока ещё ни на кого не похожих. Из 12 тысяч знаменитостей пользователи на данный момент обнаружили только 2. Открывайте телеграм и найдите их всех. Не оставляйте это просто так!

Как определить процент схожести для евклидовой дистанции?

Действительно, евклидово расстояние, в отличии от косинусного, не ограничено сверху. Отличный вопрос! 27635462738"? Поэтому возникает резонный вопрос, как показать пользователю нечто более осмысленное, чем "Поздравляем, расстояние между вашим эмбедингом и эмбедингом Анжелины Джоли составляет 0. Если построить распределение расстояний между эмбедингами, оно окажется нормальным. Один из членов нашей команды предложил следующее простое и гениальное решение. Это эквивалентно интегрированию функции плотности вероятности от d до плюс бесконечности, где d — дистанция между эмбедингами пользователя и знаменитости. А значит, для него можно посчитать среднее и стандартное отклонение, а затем для каждого пользователя по этим параметрам считать сколько процентов людей менее похожи на своих знаменитостей, чем он.

Это не seaborn

Вот точная функция, которую мы используем:

def _transform_dist_to_sim(self, dist): p = 0.5 * (1 + erf((dist - self._dist_mean) / (self._dist_std * 1.4142135623730951))) return max(min(1 - p, 1.0), self._min_similarity)

Неужели нужно перебирать список всех эмбедингов, чтобы найти совпадение?

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

scores = np.linalg.norm(emb - embeddings, axis=1)
best_idx = scores.argmax()

Поиск можно значительно ускорить, немного проиграв в его точности, используя библиотеку nmslib. Это уже даёт гигантский прирост производительности, но, оказывается, можно ещё быстрее. По всем имеющимся векторам должен быть построен так называемый индекс, в котором затем и будет производиться поиск. Она использует метод HNSW для приближенного поиска k ближайших соседей. Создать и сохранить на диск индекс для евклидовой дистанции можно следующим образом:

import nmslib index = nmslib.init(method='hnsw', space='l2', data_type=nmslib.DataType.DENSE_VECTOR) for idx, emb in enumerate(embeddings): index.addDataPoint(idx, emb) index_time_params = { 'indexThreadQty': 4, 'skip_optimized_index': 0, 'post': 2, 'delaunay_type': 1, 'M': 100, 'efConstruction': 2000
} index.createIndex(index_time_params, print_progress=True)
index.saveIndex('./assets/embeddings.bin')

Перед использованием индекс необходимо загрузить: Параметры M и efConstruction подробно описаны в документации и подбираются экспериментально исходя из требуемой точности, времени построения индекса и скорости поиска.

index = nmslib.init(method='hnsw', space='l2', data_type=nmslib.DataType.DENSE_VECTOR)
index.loadIndex('./assets/embeddings.bin') query_time_params = {'efSearch': 400}
index.setQueryTimeParams(query_time_params)

Теперь можно делать запросы. Параметр efSearch влияет на точность и скорость запросов и может не совпадать с efConstruction.

ids, dists = index.knnQuery(embedding, k=1) best_dx = ids[0] best_dist = dists[0]

005 секунды. В нашем случае nmslib работает в 20 раз быстрее, чем векторизованная линейная версия, а один запрос выполняется в среднем 0.

Как сделать моего бота готовым к продакшену?

1. Асинхронность

Как я уже говорил, python-telegram-bot предлагает для этого воспользоваться многопоточностью и реализует удобный декоратор. Для начала, необходимо сделать функцию handle_photo асинхронной.

from telegram.ext.dispatcher import run_async @run_async
def handle_photo(bot, update): ...

Размер пула задаётся при создании Updater-а. Теперь фреймворк сам запустит ваш обработчик в отдельном потоке в своём пуле. И это не совсем правда. "Но в питоне же нет многопоточности!" уже воскликнули самые нетерпеливые из вас. Из-за GIL обычный Python-код действительно не может исполнятся параллельно, но GIL отпускается для ожидания всех IO-операций, а так же может отпускаться библиотеками, которые используют расширения на C.

А теперь проанализируйте нашу функцию handle_photo: она только и состоит что из ожидания IO-операций (загрузка фотографии, отправка ответа, чтение фотографии с диска и т.п.) и вызовов функций из библиотек numpy, nmslib и Pillow.

Библиотека, вызывающая нативный код, не обязана отпускать GIL и dlib этим правом пользуется. Я не упомянул dlib неспроста. Автор говорит, что с радостью примет соответствующий Pull Request, но мне лень. Ей не нужна эта блокировка, она просто её не отпускает.

2. Многопроцессность

А лучше в пуле процессов. Простейший способ разобраться с dlib — инкапсулировать модель в отдельную сущность и запускать её в отдельном процессе.

def _worker_initialize(config): global model model = Model(config) model.load_state() def _worker_do(image): return model.process_image(image) pool = multiprocessing.Pool(8, initializer=_worker_initialize, initargs=(config,))

result = pool.apply(_worker_do, (image,))

3. Железо

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

4. Flood control

Если ваш бот популярен и им одновременно пользуется куча народу, то очень легко поймать бан на несколько секунд, который обернётся разочаровнием от ожидания для множества пользователей. Телеграм не позволяет ботам отправлять больше 30 сообщений в секунду. Для решения этой проблемы python-telegram-bot предлагает нам очередь, которая способна не отправлять больше заданного лимита сообщений в секунду, выдерживая равные интервалы между отправкой.

from telegram.ext.messagequeue import MessageQueue

Для её использования нужно определить собственного бота и подменить его при создании Updater-а.

from telegram.utils.promise import Promise class MQBot(Bot): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._message_queue = MessageQueue( all_burst_limit=30, all_time_limit_ms=1000 ) def __del__(self): try: self._message_queue.stop() finally: super().__del__() def send_message(self, *args, **kwargs): is_group = kwargs.get('chat_id', 0) >= 0 return self._message_queue(Promise(super().send_message, args, kwargs), is_group)

bot = MQBot(token=TOKEN)
updater = Updater(bot=bot)

5. Web hooks

Что это вообще такое и как это использовать можно почитать здесь. В продуктовом окружении в качестве способа получения обновлений от серверов Телеграма вместо Long Polling всегода стоит использовать Web Hooks.

6. Мелочи

Чтобы ускорить этот процесс, можно вместо стандартной библиотеки использовать ultrajson. Во время общения с сервером телеграма ежесекундно разбираются тонны тонны сообщений в формате json.

Его увеличение не сказалось на производительности, но сильно повысило стабильность бота и уменьшило число ошибок. Полезной мелочью стала настройка времени ожидания для всех блокирующих IO-операций: загрузки и отправки фотографий, отправки сообщений, получения обновлений.

6. Аналитика

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

Мы, например, настроили импорт логов в наш BI-tool Splunk и постоянно следим за ситуацией.

Реклама спланка (пожалуйста расширьте нам лицензию)

Мы вспомнили основы распознавания лиц, показали вам как создать собственного простого бота для распознавания лиц на основе открытых технологий и постарались кратко рассказать про создание нашего собственного бота. Вот и всё, о чём бы нам хотелось рассказать в этой статье.

Ну и конечно же заходите посмотреть, что у нас получилось: @OkkoFaceBot. Автоматизируйте всё что автоматизируется, уделяйте время проектам для саморазвития и приходите домой пораньше.

Кстати, у нас в сервисе нет рекламы. Можете ещё фильмы в отличном качестве посмотреть нет, это уже слишком много рекламы.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Из песочницы] Валидация сложных форм React. Часть 1

Для начала надо установить компонент react-validation-boo, предполагаю что с react вы знакомы и как настроить знаете. npm install react-validation-boo Чтобы много не болтать, сразу приведу небольшой пример кода. import React, from 'react'; import {connect, Form, Input, logger} from 'react-validation-boo'; class ...

[Перевод] Микросервисы на Go с помощью Go kit: Введение

Эта статья — введение в Go kit. В этой статье я опишу использование Go kit, набора инструментов и библиотек для создания микросервисов на Go. Первая часть в моем блоге, исходный код примеров доступен здесь. Когда вы разрабатываете облачно-ориентированную распределенную систему, ...