Генерируем рецепты блюд на JS и цепях Маркова

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


Когда-то меня очень радовал один паблик в соцсети ВК. По заявлениям администрации нейросеть генерировала рецепты, которые и составляли 99% контента. Вероятно, действительно это была простенькая нейросеть вроде RNN или LSTM. К сожалению, последний пост в паблике датирован 2019 годом, а моя тяга к изысканным блюдам не угасла, поэтому было решено сделать генератор рецептов на JS и цепях Маркова. Почему не повторить эксперимент с более продвинутой доступной нейросетью вроде GPT-2? Потому что для ее обучения требуется достаточно много времени, ресурсов и данных.


Чтобы генерировать рецепты, мы будем использовать цепи Маркова — математическую модель, которая может предсказывать следующий элемент в последовательности на основе предыдущих. Для начала нам нужно собрать корпус данных — набор рецептов на определенную кухню. Затем мы обучим цепь Маркова на этом корпусе данных и будем генерировать новые рецепты на основе полученной модели. Да, про цепи Маркова было достаточно много статей и на Хабре, и вне его. Но меня восхищает простота реализации этого алгоритма, а результаты генерации веселят. Мы будем использовать простую реализацию, чтобы получить быстрый результат, а в конце статьи будут приведены лучшие из сгенерированных рецептов.


Готовим корпус


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


Синхронно считаем данные, приведем к строке, и, разделим ее на массив абзацев по пустым строкам с помощью \n\n.


// index.js
const fs = require('fs')

const corpus = fs.readFileSync('./data.txt').toString().split('\n\n')
console.log(corpus)

> node index.js
> [
  'Хочу поделиться рецептом приготовления оладушек на сметане, в состав которых не входят яйца. Оладьи пышные, нежные, безумно вкусные. Вам и вашим близким обязательно придется по вкусу этот рецепт.\n' +
    'Для приготовления оладьев на сметане вам потребуется:\n' +
    '120 г сметаны;\n' +
    '120 мл кефира;\n' +
    '0,5 ч. л. соды;\n' +
    '2-3 ст. л. сахара;\n' +
    '0,5 ч. л. соли;\n' +
    '150 г муки;\n' +
    'масло для жарки.',
  'Кефир смешать со сметаной и содой, оставить на 5-10 минут.',
  'Добавить сахар и соль, перемешать.',
  'Просеять муку в тесто.',
  'Перемешать, долго не месить.',
  'Жарить оладьи на масле обычным способом.',
  ... 560 more items
]

Корпус готов!


Разбираемся с Марковым


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


Для примера возьмем два заголовка, которые будут условным корпусом: “Тосты с сельдью и огурцом” и “Тосты с анчоусами и грецкими орехами”


Представим матрицу переходов для этих предложений:


key value
START Тосты
Тосты с
с сельдью / анчоусами
сельдью и
анчоусами и
и огурцом / грецкими
огурцом END
грецкими орехами
орехами END
END


Следуя этой матрице, после слова “Тосты” с вероятностью 100% будет идти “с”, а вот после “с” с вероятностью в 50% может идти либо “сельдью”, либо “анчоусами”. Очевидно, что чем больше корпус — тем больше вариантов и тем больше статистический разброс.


Реализация


Для начала соберем объект токенов в конструкторе класса генератора. Знаки препинания будут включаться в токены, а регистр букв останется оригинальным. Во-первых, это упростит токенизацию, во-вторых сделает абзацы более корректными.


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


// markov.js

export default class Markov {
    tokens = { 
        START: []
    };

    constructor(corpus) {
        corpus.forEach(element => {
            const words = element.split(' ');

            words.forEach((word, index, arr) => {
                const nextWord = arr[index + 1] || 'END';

                if (index === 0) {
                    this.tokens.START.push(word)
                }
                if (this.tokens[word]) {
                    this.tokens[word].push(nextWord);
                }
                else {
                    this.tokens[word] = [nextWord];
                }
            })
        });
}

Если залогировать получившийся объект tokens, он будет иметь такой вид:


{   
    /* ... */
  'хлебом': [ 'через' ],
  'необходимости': [ 'влить' ],
  'шарики,': [ 'обвалять', 'разложить' ],
  'муке': [
    '(20', 'и', 'и',
    'и',   'и', '(30',
    'и',   'и'
  ],
  '(20': [ 'г)', 'г)' ],
    /* ... */
}

Вы можете заметить, что токены могут повторяться. Мы их оставляем в таком виде, чтобы сохранить статистические вероятности. Например, после токена ‘муке’ с вероятностью в 75% будет идти ‘и’, а ‘(20’ или ‘(30’ с вероятностью в 7.5% соответственно.


Для генерации нового текста берем случайное стартовое слово. После, в цикле while, выбираем случайные слова для текущего токена и вставляем их в массив результата, пока не наткнемся на END. В конце возвращаем результат в виде строки, соеденив элементы массива пробелами.


// markov.js

export default class Markov {
    tokens = { 
        START: []
    };

    /* ... */

    generate() {
        const startWords = this.tokens.START;
        let picked = startWords[Math.floor(Math.random() * startWords.length)];
        const result = [];

        while (picked !== 'END') {
            result.push(picked);

            const currentTokens = this.tokens[picked];
            picked = currentTokens[Math.floor(Math.random() * currentTokens.length)];
        }

        return result.join(' ');
    }
}

В конце концов, можно протестировать:


// index.js
const fs = require('fs')
const Markov = require('./markov')

const corpus = fs.readFileSync('./data.txt').toString().split('\r\n\r\n')
const markov = new Markov(corpus)

console.log(markov.generate())

> node index.js
> Кефир — 1 б. (можно больше)
1-1,5 чайная ложка.
Готовим: Плавленный сыр и вкусом ваших родных и положить 2 шт.
Мука пшеничная (стакан 250 градусов.
Далее духовку на кусочки размером с мясом к муке с картофелем, готов.
Приятного аппетита, радуйте своих близких!
Пирог "Подсолнух" украсит любой крем, джем, шоколадно-ореховая паста.
ПРИЯТНОГО ЧАЕПИТИЯ!

Вместо заключения, отправляемся на кухню


Самые забавные на мой взгляд получившиеся результаты:


ИНГРЕДИЕНТЫ:
● оливковое масло — перемешиваем.
Для получения однородной массы.
Каждое печенье достать из черного перца
1 чайная ложка.
Готовим: Плавленый сырок нарезать и убрать форму.
В центр выложить яблоки в духовке минут на 30-40 минут до 180 градусов и разровнять в салатник.

Все мы будем добавлять муку, добавить мед — 0,5 чайной ложки соевого соуса. Даем остыть и добавляем муку.
Хорошенько перемешиваем курицу в предварительно добавить мед и я использовала замороженные ягоды, перед подачей на сметане без костей,
● лук,
● чеснок,
● оливковое масло и даем настояться 15-20 минут.
Замечательное кунжутное печенье на 15-20 минут на средней терке. Колбасу и 1 шт.
Кунжут — 3 шт, морковь натираем на пару часов или ужина. Особенно он превращается в разогретую до готовности.

Источник: https://habr.com/ru/company/timeweb/blog/721546/


Интересные статьи

Интересные статьи

При создании различных типов прокручиваемых пользовательских интерфейсов очень часто требуется наблюдать за текущей позицией прокрутки (или смещением содержимого, как это называет UIScrollVi...
Так уж повелось, что знание математики редко считают необходимым для работы геймдизайнером — а если оно и требуется, то школьной программы хватит. Чаще всего так и есть. Но иногда знание определенных ...
Возможность интеграции с «1С» — это ключевое преимущество «1С-Битрикс» для всех, кто профессионально занимается продажами в интернете, особенно для масштабных интернет-магазинов.
Ваш сайт работает на 1С-Битрикс? Каждому клиенту вы даёте собственную скидку или назначаете персональную цену на товар? Со временем в вашей 1С сложилась непростая логика ценообразования и формирования...
В 2019 году люди знакомятся с брендом, выбирают и, что самое главное, ПОКУПАЮТ через интернет. Сегодня практически у любого бизнеса есть свой сайт — от личных блогов, зарабатывающих на рекламе, до инт...