Вступление

В данном посте мы расскажем о нашем фреймворке, который по фотографии еды определяет, из каких ингредиентов состоит блюдо, а затем предлагает несколько вариантов рецептов его приготовления. Кстати, весь код проекта есть на 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, приглашаю всех желающих на наши бесплатные демоуроки, зарегистрироваться на которые можно по ссылкам ниже:

Комментарии (18)


  1. third112
    02.11.2021 07:02

    Видно, что проделана огромная работа, но простите мой скептицизм.


    Случай 1. В 1й стакан наливаем воду, во второй — спирт, в 3й — водку, в 4й — бензин.
    Ваш ИИ сможет только по фото узнать, где что? — Я химик, но не узнаю, не понюхав...


    Случай 2. Яичницу с колбасой, яичницу-глазунью, яичницу-глазунью с поджареным хлебом, бифштекс с яйцом Ваш ИИ различит?


    Еще раз простите, но вспоминается фраза: легко питание, но верится с трудом(с). В статье явно не хватает фото с рецептами их приготовления.


    1. mashkka_t Автор
      02.11.2021 09:02

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


      1. major-general_Kusanagi
        02.11.2021 09:17
        +1

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


        1. mashkka_t Автор
          02.11.2021 09:44

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


          1. Kanut
            02.11.2021 14:18

            Cordon bleu от обычного шнитцеля ваша программа отличить может? Венгерский гуляш от чешского? Или хотя бы предлагает и то и другое? Если я ей солянку покажу, то какие 5 из 100500 рецептов она мне покажет?


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


            1. mashkka_t Автор
              10.11.2021 09:16

              Здесь все еще зависит от базы рецептов, в данном случае мы ориентировались на русскую кухню (о чем мы писали в начале), для того чтобы фреймворк показывал хорошие результаты на кухнях мира, надо будет взять другую базу рецептов.


    1. CyaN
      26.11.2021 16:41

      Как химик-химику: для случая 1 не фото надо будет анализировать, а видео - вязкость у этих жидкостей разная. Кстати, тоже задачка вполне интересная.

      P.S. вспомнилась известная байка про сантехника, который так отличал бутылки с этанолом от бутылок с метанолом.


      1. third112
        26.11.2021 17:12

        Как химик-химику не уверен, что Вы без ГЖХ или раскаленной медной проволоки сможете отличить метанол. Ну, еще есть метод протестировать на себе или угостить товарища — если не отравился, то не метанол.


        1. third112
          26.11.2021 17:57

          PS ИМХО на метанол нужно резко поднять цену, чтобы стоил больше этанола. Во многих случаях этанол заменяет метанол.


        1. CyaN
          30.11.2021 09:50

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


          1. third112
            30.11.2021 16:28

            Очень интересно! А можно подробнее: как отличить метанол от этанола по фотографии? У меня чисто научный интерес, но СМИ периодически сообщают об отравлениях со смертельным исходом, поэтому Ваша инструкция будет жизненно важной для многих.


            1. CyaN
              01.12.2021 10:38

              По фото - никак (только если надпись на бутылке распознавать). По видео - вполне можно, если бутылка прозрачная и жидкость поболтать.


              1. third112
                01.12.2021 11:55

                Спасибо, но что толку от болтания жидкости? Нужен прибор определения вязкости, т.е. жидкости нужно болтать с равной скоростью. А если растворы? Водка 40% и фальшивая?


                1. CyaN
                  01.12.2021 12:11

                  Если выбор только между неразбавленными этанолом и метанолом, то особой точности измерения и не потребуется. Про остальное вроде бы по-русски написал:

                  Вот если иные жидкости также возможны, либо их смеси, то тут без хроматографии либо иных физико-химических способов никак не обойтись.


                  1. third112
                    01.12.2021 13:03

                    Возможно я со школы испорчен приборами и не верю собственным ощущениям, но отвечу любимой фразой великого Станиславского, который не химик: не верю!


                    Вспоминаю историю моего препода во дворце пионеров на кружке электроники — она говорила, что 1940х просто засунув два пальца в розетку определяла напряжение 127 или 220 В. Нам не советовала, но и в то время такой проблемы уже не было — Московская сеть перешла на 220.


                    Но это вне темы — в заголовке:"по одному фото"!


                    1. third112
                      01.12.2021 13:39

                      PS Конечно, если поставить задачу: отличить позрачную колбу с насыщенным раствором медного купороса от колбы с чистой водой по фото, то на любом универсальном ЯП такая прога займет несколько строчек, и не надо никаких нейросетей и обучения.


  1. belch84
    02.11.2021 13:31

    Для тестирования предлагаю набор супов-пюре и крем-супов, только надо будет присыпку убрать

    Супы-пюре
    image


    1. mashkka_t Автор
      10.11.2021 09:15

      Оу, вот это будет challenge!