Простейший генератор текста на цепях Маркова

Простейший генератор текста на цепях Маркова

Выдаёт любой текст на любую тему.

В прошлый раз мы разбирались с теорией про цепи Маркова. Вот основные тезисы:

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

Сегодня попробуем это в деле и напишем самый простой генератор текста на цепях Маркова. Это будет похоже на работу нейросети, но на самом деле никаких «нейро» там нет — просто сети, которые сделаны на алгоритме цепей Маркова. А сеть — это просто таблица со связями между элементами. 

Короче: никакого искусственного интеллекта, просто озверевшие алгоритмы вслепую дёргают слова. 

Логика проекта

Код будем писать на Python, потому что от отлично подходит под задачи такого плана — обработка текста и построение моделей со сложными связями.

Логика будет такой:

  1. Берём файл с исходным текстом и разбиваем его на слова.
  2. Все слова, которые стоят рядом, соединяем в пары.
  3. Используя эти пары, составляем словарь цепочек, где указано первое слово и все, которые могут идти после него.
  4. Выбираем случайное слово для старта.
  5. Задаём длину текста на выходе и получаем результат.

Сделаем всё по шагам, как обычно.

Проверяем, что у нас есть Python

Python не так-то просто запустить, поэтому, если вы ещё ничего не делали на Python, прочитайте нашу статью в тему. Там всё описано по шагам. 

Разбиваем исходный текст

Для тренировки мы взяли восьмой том полного собрания сочинений Чехова — повести и рассказы. В нём примерно 150 тысяч слов, поэтому должно получиться разнообразно. Этот файл нужно сохранить как che.txt и положить в ту же папку, что и код программы.

👉 Чтобы быстро работать с большими массивами данных, будем использовать библиотеку numpy — она написана специально для биг-даты, работы с нейросетями и обработки больших матриц. Для установки можно использовать команду pip3 install numpy:

Простейший генератор текста на цепях Маркова
# подключаем библиотеку numpy
import numpy as np

# отправляем в переменную всё содержимое текстового файла
text = open('che.txt', encoding='utf8').read()

# разбиваем текст на отдельные слова (знаки препинания останутся рядом со своими словами)
corpus = text.split()

Генерируем пары

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

👉 Мы разберём генераторы более подробно в отдельной статье, а пока просто используем их в нашем коде.

# делаем новую функцию-генератор, которая определит пары слов
def make_pairs(corpus):
    # перебираем все слова в корпусе, кроме последнего
    for i in range(len(corpus)-1):
        # генерируем новую пару и возвращаем её как результат работы функции
        yield (corpus[i], corpus[i+1])
        
# вызываем генератор и получаем все пары слов
pairs = make_pairs(corpus)

В результате мы получаем все пары слов, которые идут друг за другом — с повторениями и в том порядке, как они расположены в исходном тексте. Теперь можно составлять словарь для цепочек.

Составляем словарь

Пойдём по самому простому пути: не будем высчитывать вероятности продолжения для каждого слова, а просто укажем вторым элементом в паре все слова, которые могут быть продолжением. Например, у нас в переменной pairs  есть такие пары:

привет → это

привет → друг

привет → как

привет → друг

привет → друг

Видно, что «друг» встречается в 3 раза чаще остальных слов, поэтому вероятность его появления — ⅗. Но чтобы не считать вероятности, мы сделаем так:

  1. Составим пару привет → (это, друг, как, друг, друг).
  2. При выборе мы просто случайным образом выберем одно из значений для продолжения.

👉 Это, конечно, не так изящно, как в серьёзных алгоритмах с матрицами и вероятностями, зато работает точно так же и более просто в реализации.

Вот блок с этим кодом на Python:

# словарь, на старте пока пустой
word_dict = {}

# перебираем все слова попарно из нашего списка пар
for word_1, word_2 in pairs:
    # если первое слово уже есть в словаре
    if word_1 in word_dict.keys():
        # то добавляем второе слово как возможное продолжение первого
        word_dict[word_1].append(word_2)
    # если же первого слова у нас в словаре не было
    else:
        # создаём новую запись в словаре и указываем второе слово как продолжение первого
        word_dict[word_1] = [word_2]

Выбираем слово для старта

Чтобы было совсем непредсказуемо, начальное слово тоже будем выбирать случайным образом. Главное требование к начальному слову — первая заглавная буква. Выполним это условие так:

  1. Случайно выберем первое слово.
  2. Проверим, есть ли в нём большие буквы. Для простоты допустим, что если есть, то они стоят в начале и нам подходят.
  3. Если есть — отлично, если нет — выбираем слово заново и повторяем все шаги.
  4. Делаем так до тех пор, пока не найдём подходящее слово.

# случайно выбираем первое слово для старта
first_word = np.random.choice(corpus)

# если в нашем первом слове нет больших букв 
while first_word.islower():
    # то выбираем новое слово случайным образом
    # и так до тех пор, пока не найдём слово с большой буквой
    first_word = np.random.choice(corpus)

Запускаем алгоритм

У нас почти всё готово для запуска. Единственное, что нам осталось сделать — установить количество слов в готовом тексте. После этого наш алгоритм возьмёт первое слово, добавит в цепочку, потом выберет для этого слова случайное продолжение, потом выберет случайное продолжение уже для второго слова и так далее. Так он будет делать, пока не наберёт нужное количество слов, после чего остановится.

# делаем наше первое слово первым звеном
chain = [first_word]

# сколько слов будет в готовом тексте
n_words = 100

# делаем цикл с нашим количеством слов
for i in range(n_words):
    # на каждом шаге добавляем следующее слово из словаря, выбирая его случайным образом из доступных вариантов
    chain.append(np.random.choice(word_dict[chain[-1]]))

# выводим результат
print(' '.join(chain))

Результат

После обработки Чехова наш алгоритм выдал такое:

В октябре 1894 г. Текст статьи, написанные за вечерним чаем сидела за ивы. Они понятия о равнодушии к себе в целом — бич божий! Егор Семеныч и боялась. В повести пассивности, пессимизма, равнодушия («формализма») писали это она отвечала она не застав его лоб. Он пишет, что сам Песоцкий впервые явилась мысль о ненормальностях брака. Поймите мои руки; он, — а женщин небось поставил крест на о. Сахалине (см.: М. — Нет, вы тоже, согласитесь, сытость есть две ночи и белые, пухлые руки и мог не содержащем единой и не заслуживает «ни закрепления, ни мне не знаю, для меня с 50 рисунками

Здесь нет смысла, хотя все слова связаны друг с другом. Чтобы результат был более читабельным, нам нужно увеличить количество слов в парах и оптимизировать алгоритм. Это сделаем в другой раз, на сегодня пока всё.

Неправильно ты, Дядя Фёдор, на Питоне кодишь

Опытные питонисты абсолютно справедливо сделают нам замечание: нужно не писать новый алгоритм для обработки цепей Маркова, а использовать какую-нибудь готовую библиотеку типа Markovify

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

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

Но в другой раз сделаем на библиотеке, окей.

# подключаем библиотеку numpy
import numpy as np

# отправляем в переменную всё содержимое текстового файла
text = open('che.txt', encoding='utf8').read()

# разбиваем текст на отдельные слова (знаки препинания останутся рядом со своими словами)
corpus = text.split()

# делаем новую функцию-генератор, которая определит пары слов
def make_pairs(corpus):
    # перебираем все слова в корпусе, кроме последнего
    for i in range(len(corpus)-1):
        # генерируем новую пару и возвращаем её как результат работы функции
        yield (corpus[i], corpus[i+1])
        
# вызываем генератор и получаем все пары слов
pairs = make_pairs(corpus)

# словарь, на старте пока пустой
word_dict = {}

# перебираем все слова попарно из нашего списка пар
for word_1, word_2 in pairs:
    # если первое слово уже есть в словаре
    if word_1 in word_dict.keys():
        # то добавляем второе слово как возможное продолжение первого
        word_dict[word_1].append(word_2)
    # если же первого слова у нас в словаре не было
    else:
        # создаём новую запись в словаре и указываем второе слово как продолжение первого
        word_dict[word_1] = [word_2]
 
# случайно выбираем первое слово для старта
first_word = np.random.choice(corpus)

# если в нашем первом слове нет больших букв 
while first_word.islower():
    # то выбираем новое слово случайным образом
    # и так до тех пор, пока не найдём слово с большой буквой
    first_word = np.random.choice(corpus)

# делаем наше первое слово первым звеном
chain = [first_word]

# сколько слов будет в готовом тексте
n_words = 100

# делаем цикл с нашим количеством слов
for i in range(n_words):
    # на каждом шаге добавляем следующее слово из словаря, выбирая его случайным образом из доступных вариантов
    chain.append(np.random.choice(word_dict[chain[-1]]))

# выводим результат
print(' '.join(chain))

Текст:

Михаил Полянин

Редактура:

Максим Ильяхов

Художник:

Даня Берковский

Корректор:

Ирина Михеева

Вёрстка:

Мария Дронова

Соцсети:

Олег Вешкурцев

Любишь Python? Зарабатывай на нём!
Изучите самый модный язык программирования и станьте крутым бэкенд-разработчиком. Старт — бесплатно.
Попробовать
Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём!
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
Uncaught SyntaxError: Unexpected token
Uncaught SyntaxError: Unexpected token — что это означает?

Самая популярная ошибка у новичков.

medium
Домашний кинотеатр на Raspberry Pi

Превращаем любой телевизор в умный гаджет.

easy
Делаем сами: адаптивный сайт

С котиками!

medium
Кнопка в виде ЛОДКИ
Кнопка в виде ЛОДКИ

Просто дурь на CSS. Разок можно.

easy
Защита важных файлов: автоматический бэкап за пять минут
Защита важных файлов: автоматический бэкап за пять минут

Первый шаг в информационной суверенности

medium
Запускаем телеграм-бота на сервере

Тогда он будет работать круглые сутки, а вы — отдыхать.

hard
Красивый цветной текст в CSS: как это сделать
Красивый цветной текст в CSS: как это сделать

Можно раскрасить хоть по диагонали

easy
Как поклеить обои

Поручим это машине.

easy
Python: как сделать многопоточную программу
Python: как сделать многопоточную программу

Оптимизируем простейший таймер.

medium
Эпидемия
Эпидемия
medium
Как убрать что угодно на любом сайте
Как убрать что угодно на любом сайте

Самый популярный приём разработчиков.

easy
Объективный таймер обратного отсчёта на PHP

Если нужно взять дату напрямую с сервера

medium
Что означает ошибка TypeError: string indices must be integers
Что означает ошибка TypeError: string indices must be integers

Ошибка на внимательность из мира строк в Python

easy
medium