Вступление
В данном посте мы расскажем о нашем фреймворке, который по фотографии еды определяет, из каких ингредиентов состоит блюдо, а затем предлагает несколько вариантов рецептов его приготовления. Кстати, весь код проекта есть на github.
Наш алгоритм состоит из двух частей. На первом этапе он определяет состав блюда по фото, а затем на основании предсказанных ингредиентов предлагает наиболее подходящий рецепт блюда из базы рецептов. То, что рецепты подбираются именно из базы, а не генерируются нейросетью, позволяет избежать рекомендации несуществующих рецептов. А также не возникает проблем с тем, что сочетание несочетаемых продуктов (упс, нейросеть ошиблась) в рецепте может вызвать нежелательные побочные эффекты при пищеварении. Более того, то, что алгоритм состоит из двух отдельных этапов позволяет легко адаптировать его под особенности той или иной кухни: достаточно просто заменить коллекцию рецептов.
Photo2Ingredients
Итак, на первом этапе наш алгоритм с помощью нейросетевых методов определяет состав блюда, изображенного на фото, то есть решает задачу photo2ingredients. Однако для создания такой нейросети, способной решать photo2ingredients task, необходим большой обучающий датасет: в нашем случае это должны быть фото еды с размеченными ингредиентами. Чтобы собрать такой набор данных мы взяли за основу скреппер из проекта ChefNet и с помощью него скоскрепили данные с американского сайта рецептов allrecipes.com. Помимо самих рецептов на сайте содержатся фотографии готовых блюд, а также списки ингредиентов, необходимых для их приготовления. Их мы и взяли в качестве состава блюда. Еще раз подчеркнем, что данные с сайта мы использовали именно для обучения модели первого этапа, которая распознает еду на фотографии, а для рекомендаций рецептов мы выбрали другую базу данных, на русском языке и в бОльшей степени ориентированную на русскую кухню, чем рецепты с американского сайта.
По итогу с помощью нашего скреппера мы смогли собрать порядка 40000 уникальных рецептов, каждый из которых содержал от 1 до 12 фотографий. Затем мы удалили из данных рецепты только с одним фото, чтобы избежать переобучения, и рецепты только с одним ингредиентом, так как они были слишком простые. Получившееся распределение числа фотографий и ингредиентов по рецептам изображено на гистограммах ниже:
После фильтрации датасет содержал 31372 уникальных рецептов и 203195 фотографий. Эти данные мы разделили на обучающую и валидационную выборки. При этом для равномерности разбиения мы разбили данные внутри каждого рецепта, то есть для каждого рецепта часть фото пошла в train, а часть в val (напомню, на предыдущем шаге мы отфильтровали данные так, чтобы для каждого рецепта было больше одной фотографии).
Получившиеся train и val датасеты содержат 162547 (80%) и 40648 (20%) фото соответственно, и при этом все блюда присутствуют в обеих выборках, как обучающей, так и валидационной.
Предобработка изображений
Изначально все изображения в датасете имели разрешение 250×250 пикселей. На этапе предобработки мы сжали изображения до размера 224×224 и вычли среднее значение для каждого пикселя. Такая техника широко используется в компьютерном зрении. При обучении нейросети каждый пиксель рассматривается как отдельный признак, и получается, что после вычитания среднего все признаки будут центрированы вокруг 0. Подобная предобработка позволяет избежать взрыва градиентов и делает обучение более эффективным.
Извлечение ингредиентов
Для извлечения ингредиентов по фотографиям мы рассмотрели две различные нейросетевые архитектуры: encoder-decoder и encoder-classifier.
Обе архитектуры состоят из двух подмодулей. Первый подмодуль (энкодер) кодирует изображение в вектор признаков - convolutional features, а затем второй подмодуль, используя их, предсказывает ингредиенты.
Первая модель основана на архитектуре из статьи. Она использует предобученную сверточную нейронную нейросеть (CNN) в качестве энкодера. В экспериментах мы использовали ResNet-50 без последнего полносвязного слоя. Выход ResNet-50 подается на GRU слой с hidden_size = 512. Архитектура модели представлена на рисунке ниже:
Для оптимизации мы использовали алгоритм AdaDelta. Эту модель мы обучали 40 эпох со случайной перестановкой изображений перед каждой эпохой.
Описанная архитектура имеет один существенный недостаток. Она генерирует ингредиенты последовательно, используя предыдущее состояние GRU. Однако предыдущее состояние GRU хранит информацию о ранее сгенерированных ингредиентах. Таким образом, эта модель учитывает порядок ингредиентов, который на самом деле является случайным.
Чтобы побороть этот минус, мы предложили другую архитектуру, которая не учитывает порядок ингредиентов. Она также состоит из двух подмодулей: в качестве энкодера используется предобученная CNN, а в качестве второго подмодуля - используется классификатор, предсказывающий ингредиенты, входящие в состав блюда, на основе convolutional features.
В качестве энкодера мы попробовали три предобученные модели: Resnet- 50, Resnet-101 и Densenet161. Densenet161 показала наилучший результат, поэтому мы остановились на ней. Классификатор состоит из трех полносвязанных слоев, а на выход последнего слоя навешивается сигмоида. Затем все ингредиенты, для которых выходное значение больше порога (мы взяли порог 0.25, который был установлен эмпирическим путем), добавляются в итоговый список ингредиентов. Архитектура второй модели представлена на рисунке ниже:
Аналогично предыдущей архитектуре мы обучали вторую модель 40 эпох со случайной перестановкой изображений перед каждой эпохой.
Весь код написан на Python, на Pytorch.
Изменение метрик качества на валидации представлено на рисунках ниже:
Результаты первой модели:
Результаты второй модели:
Эксперименты показывают, что вторая модель значительно превосходит первую. Она достигает значения F1 равного 0,53, в то время как первая только 0,24.
Ingredients2recipe
Итак, на первом этапе мы извлекли ингредиенты блюд из фотографий, а на втором - будем сопоставлять их с наиболее подходящими рецептами. Для этого мы собрали базу данных рецептов, а затем протестировали три различных метода матчинга для поиска наилучшего рецепта, соответствующего списку ингредиентов.
Скрепинг рецептов
Из открытых источников нам удалось собрать базу данных из 68416 рецептов на русском языке. Скачанные рецепты были разбиты на категории, которые содержали такие группы блюд как десерты, паста, ризотто, напитки и другие. Каждый рецепт содержит основную информацию о блюде (название, фото, категория блюда), список входящих в него ингредиентов и непосредственно сам рецепт, то есть инструкцию по приготовлению. Ниже приведена структура собранного датасета:
Чтобы избежать дисбаланса категорий и преобладания наиболее популярных блюд, мы ограничили количество загруженных рецептов для каждой категории шестьюстами рецептами на категорию. Полученный датасет мы затем использовали в качестве базы рецептов для рекомендаций.
Матчинг рецептов
Следующий этап - по ингредиентам подобрать наилучший рецепт для блюда. Этот этап можно представить как задачу ранжирования, когда алгоритм принимает список предполагаемых ингредиентов (и в некоторых случаях исходное изображение) в качестве входных данных и выбирает наиболее подходящие рецепты из базы данных. Таким образом, на выходе получается набор самых подходящих рецептов. Стоит отметить, что для одного блюда может быть несколько рецептов приготовления, а не один, поэтому мы выдаем не просто лучшее соответствие, а ранжируем кандидатов и выдаем топ лучших.
Для матчинга мы реализовали и протестировали три разных метода:
1) подход на основе предобученных эмбенддингов;
2) подход на основе TF-IDF;
3) подход с использованием изображений.
Давайте обсудим каждый метод подробнее.
Подход на основе предобученных эмбенддингов
Первый метод основан на идее поиска рецептов с помощью предобученных векторных представлений слов. А именно мы вычисляем косинусную близость (cosine similarity) между средним вектором ингредиентов и средним вектором рецептов, а затем выбираем наиболее похожие рецепты. В экспериментах мы сравнили три вида предобученных векторных представлений слов:
1) word2vec. Модель Skipgram для русского языка, обученная на Национальном Корпусе Русского языка (НКРЯ) от RusVectores (проект NLPL). Тексты рецептов и ингредиенты были предварительно обработаны с помощью UDpipe для получения лемм и pos-тегов.
2) fasttext. Модель, предобученная на НКРЯ, из NLPL.
3) ELMO. Модель от DeepPavlov, предобученная на Википедии для русского языка.
Подход на основе TF-IDF
Второй метод матчинга рецептов основан на векторизации списков ингредиентов с помощью TF-IDF. Списки ингредиентов для каждого рецепта нормализуются (например, унифицируются формы слов), а затем векторизуются с помощью TF-IDF, обученного на текстах рецептов из базы данных. Для поиска наиболее подходящих рецептов как и в первом методе используется косинусная близость между TF-IDF векторами. Для улучшения эффективности метода, мы использовали аугментацию наиболее важных ингредиентов. Идея основана на простой гипотезе: наиболее важные ингредиенты блюда чаще других упоминаются в тексте рецепта. Поэтому мы добавили к исходному списку ингредиентов ингредиенты, упомянутые в инструкции по приготовлению. Таким образом, мы увеличили количество вхождений наиболее важных ингредиентов в состав и как следствие их TF-IDF вес. Это в свою очередь позволило значительно улучшить качество матчинга.
Подход с использованием изображений
Как мы уже упоминали, собранная база данных рецептов содержит фотографии блюд. Их также можно использовать при матчинге рецептов. Так, третий подход основан на идее, что исходную фотографию блюда можно также использовать для подбора наилучших рецептов. Для этого мы сравниваем вектор ингредиентов (или вектор признаков), предсказанных по исходному фото на этапе photo2ingredients, с векторами признаков, извлеченных из фотографий блюд из коллекции рецептов, и выбираем наиболее близкие варианты по L1 норме. При этом в качестве фото для каждого блюда из базы данных мы взяли основное фото со страницы рецепта, а вектор признаков из него получили с помощью модели, обученной на этапе photo2ingredients.
Сравнение методов матчинга
Для оценки качества методов матчинга мы выбрали случайных 50 рецептов из базы собранной по allrecipes.com и использовали соотвествующие им ингредиенты, переведенные на русский язык, для поиска наиболее подходящих рецептов. При этом мы предлагали топ-10 блюд для каждого примера. Затем затем мы вручную (с перекрытием аннотаторов равным трем) оценили релевантность предложенных рецептов и вычислили MAP@10, усреднив оценки между аннотаторами. Результаты приведены в таблице ниже:
Из таблицы видно, что второй метод, основанный на TF-IDF, превосходит остальные. Среди методов первой группы, использующий предобученные векторные представления, Word2Vec показывает наилучшее качество и лишь немного отстает от TF-IDF подхода. К нашему удивлению, третий метод, основанный на извлечении признаков из фотографий, показал очень низкие результаты, и большинство предложенных им рецептов оказались нерелевантными.
Единый framework
По результатам всех экспериментов, мы собрали лучшие компоненты алгоритма в единый фреймворк: для извлечения ингредиентов из фотографий на этапе photo2ingredients мы выбрали вторую архитектуру (encoder-classifier), а для матчинга рецептов из коллекции - векторизацию с помощью TF-IDF (второй метод). Итоговый алгоритм и код фреймворка вы можете найти в нашем GitHub репозитории.
Заключение
В данном посте мы рассказали о нашем фреймворке, который по фотографии еды определяет, из каких ингредиентов состоит блюдо на фото, а затем на основе предсказанных ингредиентов подбирает несколько вариантов рецептов из базы данных. При этом наш алгоритм всегда предлагает лишь существующие рецепты и учитывает особенности русской кухни, подбирает рецепты, не свойственные для американской и европейской кухонь.
В будущем мы планируем улучшить фреймворк. Например, в планах использовать более сложные методы ранжирования для матчинга рецептов на втором этапе.
Благодарность
Эта работа результат совместных трудов большой команды из шести человек: @IlyaShchuka, @saydashtatar, @vladislove_p, @alenusch и я, ваша покорная слуга и рассказчица @mashkka_t. Спасибо вам огромное за совместную работу над проектом и креативные идеи!
Обучение
Так как я являюсь руководителем курсов по Machine Learning в OTUS, приглашаю всех желающих на наши бесплатные демоуроки, зарегистрироваться на которые можно по ссылкам ниже:
Интенсив: Построение пайплайнов с помощью sklearn или как выделиться на фоне остальных. День 1
Интенсив: Построение пайплайнов с помощью sklearn или как выделиться на фоне остальных. День 2
Интенсив: Деплой ML модели: от грязного кода в ноутбуке к рабочему сервису. День 1
Интенсив: Деплой ML модели: от грязного кода в ноутбуке к рабочему сервису. День 2