В прошлый раз мы разбирались с теорией про цепи Маркова. Вот основные тезисы:
- Цепь Маркова — это последовательность событий, где каждое новое событие зависит только от предыдущего. Например, после одного слова может стоять другое слово.
- Существуют алгоритмы, которые способны генерировать текст на основании цепей Маркова. Они изучают, какие связи могут быть между словами, и потом проходят по этим связям и составляют новый текст.
- Для нашей работы алгоритму всегда нужен исходный текст (он же корпус) — глядя на этот текст, алгоритм поймёт, какие слова обычно идут друг за другом.
- Чем больше размер исходного текста, тем больше связей между цепями и тем разнообразнее получается текст на выходе.
Сегодня попробуем это в деле и напишем самый простой генератор текста на цепях Маркова. Это будет похоже на работу нейросети, но на самом деле никаких «нейро» там нет — просто сети, которые сделаны на алгоритме цепей Маркова. А сеть — это просто таблица со связями между элементами.
Короче: никакого искусственного интеллекта, просто озверевшие алгоритмы вслепую дёргают слова.
Логика проекта
Код будем писать на Python, потому что от отлично подходит под задачи такого плана — обработка текста и построение моделей со сложными связями.
Логика будет такой:
- Берём файл с исходным текстом и разбиваем его на слова.
- Все слова, которые стоят рядом, соединяем в пары.
- Используя эти пары, составляем словарь цепочек, где указано первое слово и все, которые могут идти после него.
- Выбираем случайное слово для старта.
- Задаём длину текста на выходе и получаем результат.
Сделаем всё по шагам, как обычно.
Проверяем, что у нас есть 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 раза чаще остальных слов, поэтому вероятность его появления — ⅗. Но чтобы не считать вероятности, мы сделаем так:
- Составим пару привет → (это, друг, как, друг, друг).
- При выборе мы просто случайным образом выберем одно из значений для продолжения.
👉 Это, конечно, не так изящно, как в серьёзных алгоритмах с матрицами и вероятностями, зато работает точно так же и более просто в реализации.
Вот блок с этим кодом на 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]
Выбираем слово для старта
Чтобы было совсем непредсказуемо, начальное слово тоже будем выбирать случайным образом. Главное требование к начальному слову — первая заглавная буква. Выполним это условие так:
- Случайно выберем первое слово.
- Проверим, есть ли в нём большие буквы. Для простоты допустим, что если есть, то они стоят в начале и нам подходят.
- Если есть — отлично, если нет — выбираем слово заново и повторяем все шаги.
- Делаем так до тех пор, пока не найдём подходящее слово.
# случайно выбираем первое слово для старта
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))