Когда-то меня очень радовал один паблик в соцсети ВК. По заявлениям администрации нейросеть генерировала рецепты, которые и составляли 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('\n\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 шт, морковь натираем на пару часов или ужина. Особенно он превращается в разогретую до готовности.

???? Салат «Венеция»
Ингредиенты:
● 350-400 г. оливок;
● 60 г. слабосоленой семги;
● 40-50 г. слабосоленой семги;
● 40-50 г. куриной тушки.

соль
ИНГРЕДИЕНТЫ:
● 1 ст. л.

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

Ингредиенты:
1 банка (140 г.);
● майонез.
Приготовление:
Лук очистить от Светланы Гуаговой
Натереть рыбу сыром.
Нарезаем полукольцами луком, смазывать им гостей. Готовится торт что-то простое в духовке.
Выпекать булочки 25-30 мин до полного застывания.

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

ИНГРЕДИЕНТЫ:
● Свежий (500г ) не суп!

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


  1. savostin
    00.00.0000 00:00
    +1

    Надо попробовать так анекдоты генерить на базе anekdot.ru...


    1. venanen
      00.00.0000 00:00
      +1

      Не получится. Точнее, как получится, иногда выдает шедевры - но редко, и дело вот в чем: у нейросети нет понятия юмора или сарказма, нейросеть просто не сможет предсказать, что медведь сгорел в машине. Перплексия сети. Так что это либо будет полностью бред, либо полностью несмешная история.
      Впрочем, несколько шедевров есть, из цензурных:
      1) Купил мужик шляпу, а она ему как раз два три четыре пять шесть семь восемь девять десять.
      2) Физик-ядерщик изобрел реактор, который не просто горит, а взрывается.
      3) -Алло, это полиция?!
      - Да! Что у вас случилось?
      - У нас тут пожар.


      1. savostin
        00.00.0000 00:00

        Типа рецепты не бредовые получаются...


        1. Iskorkin Автор
          00.00.0000 00:00
          +1

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

          Когда на одну бутылку пива.
          — Извините, но ты не живут. Не был я - ну ты взяла, дорогая?
          — Тебя постоянно окружают умные и гонял по дзюдо".

          Выше примеры от нейросети, вот они хороши


  1. Ihariharihar
    00.00.0000 00:00
    +2

    Интересно. Спасибо.


  1. myswordishatred
    00.00.0000 00:00
    +2

    Если речь в начале поста про "Нейрокухню", то советую автору найти в телеграме "Нейрорецепты". Там дело Супочки живёт.


    1. Iskorkin Автор
      00.00.0000 00:00

      Оо, даст бог вам здоровья! Да, речь шла про нее