Глубокое обучение в гараже — Возвращение смайлов
Это третья статья из серии про определение смайла по выражению лица.
Глубокое обучение в гараже — Братство данных
Глубокое обучение в гараже — Две сети
Глубокое обучение в гараже — Возвращение смайлов
Так что же со смайлами?
Фух, ну наконец, детекция лиц работает, можно учить сеть распознавания смайла. Только вот на чем учить? Открытых наборов данных нет. А из того, как долго в предыдущей части я добирался до, собственно, обучения моделей вы уже должны были понять, что в глубоком обучении данные решают все. И их нужно много.
Есть открытые наборы данных с размеченными эмоциями. Но это мне не подходит, ведь мне хочется понимать не выражения лиц, а каррикатурные, специально состроенные выражения лиц, а это совсем не то, что настоящие эмоции.
Ну что ж, надо собирать свою тренировочную выборку! Фотографироваться самому? Но одного человека точно не хватит. После блужданий по картинкам Гугла и Яндекса я пошел на Ютуб. И вот, среди полчищ котиков и лэтсплеев мне попалось вот это видео:
Ура! То, что надо! В результате недолгих поисков я нарыл еще парочку подобных видео и принялся за разметку, что тоже довольно весело, и дало повод разобраться с OpenCV для питона, но да статья не об этом.
В итоге получился такой датасет, что я совершенно не верил в успех операции:
- Он очень маленький (около трех тысяч фоток).
- Всего около двадцати человек.
- Только девушки (это было плюсом во время разметки, но по результату мужиков сеть не понимает совсем)
- Я размечал лицо почти на каждом кадре, так что получилось по десятку-полтора очень похожих фоток каждой пары (девушка, смайл).
- Довольно много классов на такое число фоток: 17.
- Есть похожие смайлы. По факту, в результате сеть их периодически путает.
Но раз уж работа проделана, чего ж не попробовать? Я взял архитектуру большой сети из модуля детекции, заменил классификатор наверху с двух классов на семнадцать и пошел учить.
def build_net_smiles(input):
network = lasagne.layers.InputLayer(shape=(None, 3, 48, 48), input_var=input)
network = lasagne.layers.dropout(network, p=.1)
network = conv(network, num_filters=64, filter_size=(5, 5), nolin=relu)
network = batchnorm.batch_norm(network)
network = max_pool(network)
network = conv(network, num_filters=64, filter_size=(5, 5), nolin=relu)
network = batchnorm.batch_norm(network)
network = max_pool(network)
network = conv(network, num_filters=64, filter_size=(3, 3), nolin=relu)
network = batchnorm.batch_norm(network)
network = max_pool(network)
network = conv(network, num_filters=64, filter_size=(3, 3), nolin=relu)
network = batchnorm.batch_norm(network)
network = max_pool(network)
network = DenseLayer(lasagne.layers.dropout(network, p=.3), num_units=256, nolin=relu)
network = batchnorm.batch_norm(network)
network = DenseLayer(lasagne.layers.dropout(network, p=.3), num_units=17, nolin=linear)
return network
И я был прав! Даже картинку показывать не буду, получилось ужасно. Однако, наученный опытом я не отчаялся сразу, а с питоном наперевес ринулся кодить максимально разнообразную аугментацию данных, стремясь хотя бы приблизиться к парадигме «эффективно бесконечного» набора данных, аналогичную аугментации для детекции. Алгоритм приводить не буду, он точно такой же, какой был для детекции. Сразу скажу, что тут эффективность дополнения сильно меньше, чем для детекции потому, что по факту уникальных фоток очень мало, а все те три тысячи — это соседние кадры, которые мало отличаются и сами, по сути, похожи на эту самую аугментацию.
И снова провал: на валидационных девушках (т.е. девушках, выделенных целиком в валидационное множество) определяется очень плохо. Как я и предполагал, 20 девушек — это слишком мало, чтобы все хорошо работало, так что я решил немного ослабить требования и не выделять целых девушек в валидационное множество, в результате чего от модели ожидается переобучиться на конкретных людей из тренировочной выборки и будет плохо работать на других людях, что и произошло;, но не переобучиться на конкретные изображения (данных-то с аугментацией достаточно много!).
В таких условиях получилось даже хорошо:
Видно, что расколбас сходится, и мои предыдущие опыты подсказывают, что увеличение объема данных — это самый кошерный способ это починить, но, увы, данных больше у меня нет: их довольно непросто найти и еще сложнее размечать.
Система
Ну и, наконец, весь пайплайн. Одна картинка вместо тысячи слов:
Тут замечу, что в конце детекции лиц получилось два квадрата, а не один потому, что площади их пересечения недостаточно, чтобы алгоритм понял, что они относятся к одному лицу. Над этим я еще работаю и тут нет однозначного решения. Необходимо придумывать алгоритм под задачу, что я и сделал для демо: выбрал один лучший квадрат для классификации на нем смайла.
Так же, если просто взять квадрат, найденный детектором и получать смайл по нему, то получается плохо: ведь эта сетка была обучена на слишком малом объеме данных, и она очень чувствительна к изменениям входов. А детектор как раз вполне может выдавать очень разные результаты: и покрывающие лицо, и центр лица, а порой даже просто кусок лица. На это классификатор смайлов очень плохо реагирует, так что вместо того, чтобы брать один квадрат, я беру 45 рядом стоящих и провожу голосование. Число слева внизу — это как раз какой процент от максимума набрал этот смайл. Для каждого из 45 окон классификатор выдает распределение по смайлам, которые я просто векторно суммирую по всем окнам и делю на сумму уже по всем смайлам (которая должна быть равна 45, так как от сумма по смайлам от каждого окна, будучи распределением вероятностей, равна единице).
smile_probs_sum = T.sum(lasagne.layers.get_output(net_smiles, deterministic=True), axis=0) # sum probabilities over all windows
classify_smiles = theano.function([T.tensor4("input")], [T.argmax(smile_probs_sum), T.max(smile_probs_sum)]) # get best class and its score
def score(frames):
smile_cls, smile_val = classify_smiles(*frames)
smile_val = float(smile_val)
return smile_cls, smile_val / 17
Покажи уже результат!
В качестве теста работы системы я прогнал ее на оригинальном видео, одном из тех, на которых учился (да-да, это не полный вин, но так было задумано, для полного вина нужно собрать много данных от большого числа людей). Заодно, видео было замедлено в несколько раз, чтобы успевать следить за работой сети:
Бонус
Как я сказал выше, данных для обучения я собрал недостаточно для обучения полноценной модели, которая бы работала на произвольных людях. Поэтому, было решено сделать краудфандинг-платформу для сбора этих данных (ха-ха, какое громкое название)!
Итого, был сделан небольшой сервис на Go под названием Smielfy v0.1, который предлагает вам изобразить смайл, показывает пример, как это делали до вас в видео на ютубе и дает возможность сделать фото и отправить понравившиеся фото на сервер, где оно аккуратно положится в папку, которую я, если накопится достаточно данных, смогу использовать для обучения более крутой модели, которая будет уже по-честному способна справиться с определением смайла на любом человеке и которую можно будет задеплоить в (некоммерческое) приложение!
Сервис использует самоподписанный SSL-сертификат, который нужен только для того, чтобы браузер разрешал использовать API web-камеры.
Благодарности
Спасибо Zoberg, который сделал сервис Smielfy буквально за один день вчера и который помогал мне размечать смайлы.
Спасибо GeniyX, который прямо сейчас делает редизайн Smielfy и который был основным разработчиком DeepEvent v2.0 — новой версии системы мониторинга и визуализации обучения нейронных сетей.