Это 21-я статья из моей серии статей о Python для НЛП. В предыдущей статье я объяснил, как использовать библиотеку FastText Facebook для поиска семантического сходства и выполнения классификации текста. В этой статье вы увидите, как сгенерировать текст с помощью техники глубокого обучения на Python с использованием библиотеки Keras .
Генерация текста - одно из самых современных приложений НЛП. Методы глубокого обучения используются для множества задач по созданию текста, таких как написание стихов, создание сценариев для фильмов и даже для написания музыки. Тем не менее, в этой статье мы увидим очень простой пример генерации текста, где по входной строке слов мы предсказываем следующее слово. Мы будем использовать необработанный текст из знаменитого романа Шекспира «Макбет» и будем использовать его для предсказания следующего слова с учетом последовательности вводимых слов.
После прочтения этой статьи вы сможете выполнять генерацию текста, используя выбранный вами набор данных. Итак, начнем без лишних слов.
Импорт библиотек и наборов данных
Первый шаг - импортировать библиотеки, необходимые для выполнения сценариев, описанных в этой статье, вместе с набором данных. Следующий код импортирует необходимые библиотеки:
import numpy as np
from keras.models import Sequential, load_model
from keras.layers import Dense, Embedding, LSTM, Dropout
from keras.utils import to_categorical
from random import randint
import re
Следующим шагом будет загрузка набора данных. Мы будем использовать библиотеку Python NLTK для загрузки набора данных. Мы будем использовать набор данных Гутенберга , который содержит 3036 английских книг, написанных 142 авторами, включая «Макбет» Шекспира.
Следующий скрипт загружает набор данных Гутенберга и печатает имена всех файлов в наборе данных.
import nltk
nltk.download('gutenberg')
from nltk.corpus import gutenberg as gut
print(gut.fileids())
Вы должны увидеть следующий результат:
['austen-emma.txt', 'austen-persuasion.txt', 'austen-sense.txt', 'bible-kjv.txt', 'blake-poems.txt', 'bryant-stories.txt', 'burgess-busterbrown.txt', 'carroll-alice.txt', 'chesterton-ball.txt', 'chesterton-brown.txt', 'chesterton-thursday.txt', 'edgeworth-parents.txt', 'melville-moby_dick.txt', 'milton-paradise.txt', 'shakespeare-caesar.txt', 'shakespeare-hamlet.txt', 'shakespeare-macbeth.txt', 'whitman-leaves.txt']
Файл shakespeare-macbeth.txt
содержит необработанный текст романа
«Макбет». Чтобы прочитать текст из этого файла, можно использовать raw
метод из класса gutenberg
macbeth_text = nltk.corpus.gutenberg.raw('shakespeare-macbeth.txt')
Напечатаем первые 500 символов из нашего набора данных:
print(macbeth_text[:500])
Вот результат:
[The Tragedie of Macbeth by William Shakespeare 1603]
Actus Primus. Scoena Prima.
Thunder and Lightning. Enter three Witches.
1. When shall we three meet againe?
In Thunder, Lightning, or in Raine?
2. When the Hurley-burley's done,
When the Battaile's lost, and wonne
3. That will be ere the set of Sunne
1. Where the place?
2. Vpon the Heath
3. There to meet with Macbeth
1. I come, Gray-Malkin
All. Padock calls anon: faire is foule, and foule is faire,
Houer through
Вы можете видеть, что текст содержит много специальных символов и цифр. Следующим шагом будет очистка набора данных.
Предварительная обработка данных
Чтобы удалить знаки препинания и специальные символы, мы определим
функцию с именем preprocess_text()
:
def preprocess_text(sen):
# Remove punctuations and numbers
sentence = re.sub('[^a-zA-Z]', ' ', sen)
# Single character removal
sentence = re.sub(r"\s+[a-zA-Z]\s+", ' ', sentence)
# Removing multiple spaces
sentence = re.sub(r'\s+', ' ', sentence)
return sentence.lower()
Функция preprocess_text
принимает текстовую строку в качестве
параметра и возвращает очищенную текстовую строку в нижнем регистре.
Теперь очистим наш текст и снова выведем первые 500 символов:
macbeth_text = preprocess_text(macbeth_text)
macbeth_text[:500]
Вот результат:
the tragedie of macbeth by william shakespeare actus primus scoena prima thunder and lightning enter three witches when shall we three meet againe in thunder lightning or in raine when the hurley burley done when the battaile lost and wonne that will be ere the set of sunne where the place vpon the heath there to meet with macbeth come gray malkin all padock calls anon faire is foule and foule is faire houer through the fogge and filthie ayre exeunt scena secunda alarum within enter king malcom
Преобразование слов в числа
Модели глубокого обучения основаны на статистических алгоритмах. Следовательно, чтобы работать с моделями глубокого обучения, нам нужно преобразовывать слова в числа.
В этой статье мы будем использовать очень простой подход, при котором
слова будут преобразованы в отдельные целые числа. Прежде чем мы сможем
преобразовывать слова в целые числа, нам нужно токенизировать наш текст
в отдельные слова. Для этого можно использовать метод word_tokenize()
nltk.tokenize
Следующий скрипт токенизирует текст в нашем наборе данных, а затем печатает общее количество слов в наборе данных, а также общее количество уникальных слов в наборе данных:
from nltk.tokenize import word_tokenize
macbeth_text_words = (word_tokenize(macbeth_text))
n_words = len(macbeth_text_words)
unique_words = len(set(macbeth_text_words))
print('Total Words: %d' % n_words)
print('Unique Words: %d' % unique_words)
Результат выглядит так:
Total Words: 17250
Unique Words: 3436
В нашем тексте 17250 слов, из которых 3436 слов уникальны. Для
преобразования лексических слов чисел, Tokenizer
класса от
keras.preprocessing.text
можно использовать модуль. Вам нужно вызвать
fit_on_texts
и передать ему список слов. Будет создан словарь, в
котором ключи будут представлять слова, а целые числа будут представлять
соответствующие значения словаря.
Взгляните на следующий сценарий:
from keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer(num_words=3437)
tokenizer.fit_on_texts(macbeth_text_words)
Для доступа к словарю, содержащему слова и соответствующие индексы,
word_index
атрибут word_index объекта токенизатора:
vocab_size = len(tokenizer.word_index) + 1
word_2_index = tokenizer.word_index
Если вы проверите длину словаря, он будет содержать 3436 слов, что составляет общее количество уникальных слов в нашем наборе данных.
Теперь напечатаем 500-е уникальное слово вместе с его целочисленным
значением из словаря word_2_index
print(macbeth_text_words[500])
print(word_2_index[macbeth_text_words[500]])
Вот результат:
comparisons
1456
Здесь слову «сравнения» присвоено целое значение 1456.
Изменение формы данных
Генерация текста относится к категории проблем с последовательностью «многие к одному», поскольку входные данные - это последовательность слов, а выходные - одно слово. Мы будем использоватьсеть долгосрочной краткосрочной памяти (LSTM), которая представляет собой тип рекуррентной нейронной сети для создания нашей модели генерации текста. LSTM принимает данные в трехмерном формате (количество выборок, количество временных шагов, характеристики на временной шаг). Поскольку вывод будет состоять из одного слова, форма вывода будет двумерной (количество образцов, количество уникальных слов в корпусе).
Следующий сценарий изменяет форму входных последовательностей и соответствующих выходных данных.
input_sequence = []
output_words = []
input_seq_length = 100
for i in range(0, n_words - input_seq_length , 1):
in_seq = macbeth_text_words[i:i + input_seq_length]
out_seq = macbeth_text_words[i + input_seq_length]
input_sequence.append([word_2_index[word] for word in in_seq])
output_words.append(word_2_index[out_seq])
В приведенном выше скрипте мы объявляем два пустых списка
input_sequence
и output_words
. Для input_seq_length
установлено
значение 100, что означает, что наша входная последовательность будет
состоять из 100 слов. Затем мы выполняем цикл, в котором на первой
итерации целые значения для первых 100 слов текста добавляются к списку
input_sequence
101-е слово добавляется в список output_words
Во
время второй итерации последовательность слов, которая начинается со
2-го слова в тексте и заканчивается 101-м словом, сохраняется в
input_sequence
, а 102-е слово сохраняется в output_words
и так
далее. Всего будет сгенерировано 17150 входных последовательностей,
поскольку всего в наборе данных 17250 слов (на 100 меньше, чем общее
количество слов).
Теперь напечатаем значение первой последовательности в списке
input_sequence
print(input_sequence[0])
Выход:
[1, 869, 4, 40, 60, 1358, 1359, 408, 1360, 1361, 409, 265, 2, 870, 31, 190, 291, 76, 36, 30, 190, 327, 128, 8, 265, 870, 83, 8, 1362, 76, 1, 1363, 1364, 86, 76, 1, 1365, 354, 2, 871, 5, 34, 14, 168, 1, 292, 4, 649, 77, 1, 220, 41, 1, 872, 53, 3, 327, 12, 40, 52, 1366, 1367, 25, 1368, 873, 328, 355, 9, 410, 2, 410, 9, 355, 1369, 356, 1, 1370, 2, 874, 169, 103, 127, 411, 357, 149, 31, 51, 1371, 329, 107, 12, 358, 412, 875, 1372, 51, 20, 170, 92, 9]
Давайте нормализуем наши входные последовательности, разделив целые числа в последовательностях на наибольшее целочисленное значение. Следующий скрипт также преобразует вывод в двухмерный формат.
X = np.reshape(input_sequence, (len(input_sequence), input_seq_length, 1))
X = X / float(vocab_size)
y = to_categorical(output_words)
Следующий скрипт печатает форму входов и соответствующих выходов.
print("X shape:", X.shape)
print("y shape:", y.shape)
Выход:
X shape: (17150, 100, 1)
y shape: (17150, 3437)
Обучение модели
Следующим шагом будет обучение нашей модели. Не существует жесткого правила относительно того, какое количество слоев и нейронов следует использовать для обучения модели. Мы случайным образом выберем размер слоя и нейрона. Вы можете поиграть с гиперпараметрами, чтобы увидеть, сможете ли вы добиться лучших результатов.
Мы создадим три слоя LSTM по 800 нейронов в каждом. Последний плотный слой с 1 нейроном будет добавлен для предсказания индекса следующего слова, как показано ниже:
model = Sequential()
model.add(LSTM(800, input_shape=(X.shape[1], X.shape[2]), return_sequences=True))
model.add(LSTM(800, return_sequences=True))
model.add(LSTM(800))
model.add(Dense(y.shape[1], activation='softmax'))
model.summary()
model.compile(loss='categorical_crossentropy', optimizer='adam')
Поскольку выходное слово может быть одним из 3436 уникальных слов, наша
задача является классификация мульти-класса, поэтому
categorical_crossentropy
используется функция потерь. В случае
бинарной классификации binary_crossentropy
функция
binary_crossentropy. После выполнения вышеуказанного сценария вы должны
увидеть сводку модели:
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm_1 (LSTM) (None, 100, 800) 2566400
_________________________________________________________________
lstm_2 (LSTM) (None, 100, 800) 5123200
_________________________________________________________________
lstm_3 (LSTM) (None, 800) 5123200
_________________________________________________________________
dense_1 (Dense) (None, 3437) 2753037
=================================================================
Total params: 15,565,837
Trainable params: 15,565,837
Non-trainable params: 0
Чтобы обучить модель, мы можем просто использовать метод fit()
.
model.fit(X, y, batch_size=64, epochs=10, verbose=1)
Здесь вы снова можете поиграть с разными значениями для batch_size
и
epochs
. Модель может потренироваться некоторое время.
Прогнозы
Чтобы делать прогнозы, мы случайным образом выберем последовательность
из input_sequence
, преобразуем ее в трехмерную форму и затем
передадим ее predict()
обученной модели. Модель вернет массив с
горячим кодированием, где индекс, содержащий 1, будет значением индекса
следующего слова. Затем значение индекса передается в index_2_word
,
где индекс слова используется в качестве ключа. index_2_word
вернет
слово, принадлежащее индексу, переданному как ключ в словарь.
Следующий скрипт случайным образом выбирает последовательность целых чисел и затем печатает соответствующую последовательность слов:
random_seq_index = np.random.randint(0, len(input_sequence)-1)
random_seq = input_sequence[random_seq_index]
index_2_word = dict(map(reversed, word_2_index.items()))
word_sequence = [index_2_word[value] for value in random_seq]
print(' '.join(word_sequence))
Для сценария в этой статье случайным образом была выбрана следующая последовательность. Сгенерированная для вас последовательность, скорее всего, будет отличаться от этой:
amen when they did say god blesse vs lady consider it not so deepely mac but wherefore could not pronounce amen had most need of blessing and amen stuck in my throat lady these deeds must not be thought after these wayes so it will make vs mad macb me thought heard voyce cry sleep no more macbeth does murther sleepe the innocent sleepe sleepe that knits vp the rauel sleeue of care the death of each dayes life sore labors bath balme of hurt mindes great natures second course chiefe nourisher in life feast lady what doe you meane
В приведенном выше сценарии index_2_word
создается путем простого
обращения словаря word_2_index
В этом случае обращение словаря
относится к процессу обмена ключами со значениями.
Затем мы напечатаем следующие 100 слов, которые следуют за приведенной выше последовательностью слов:
for i in range(100):
int_sample = np.reshape(random_seq, (1, len(random_seq), 1))
int_sample = int_sample / float(vocab_size)
predicted_word_index = model.predict(int_sample, verbose=0)
predicted_word_id = np.argmax(predicted_word_index)
seq_in = [index_2_word[index] for index in random_seq]
word_sequence.append(index_2_word[ predicted_word_id])
random_seq.append(predicted_word_id)
random_seq = random_seq[1:len(random_seq)]
word_sequence
теперь содержит нашу входную последовательность слов
вместе со следующими 100 предсказанными словами. word_sequence
содержит последовательность слов в виде списка. Мы можем просто
объединить слова в списке, чтобы получить окончательную выходную
последовательность, как показано ниже:
final_output = ""
for word in word_sequence:
final_output = final_output + " " + word
print(final_output)
Вот окончательный результат:
amen when they did say god blesse vs lady consider it not so deepely mac but wherefore could not pronounce amen had most need of blessing and amen stuck in my throat lady these deeds must not be thought after these wayes so it will make vs mad macb me thought heard voyce cry sleep no more macbeth does murther sleepe the innocent sleepe sleepe that knits vp the rauel sleeue of care the death of each dayes life sore labors bath balme of hurt mindes great natures second course chiefe nourisher in life feast lady what doe you meane and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and
Результат пока выглядит не очень хорошо, и кажется, что наша модель
только учится на последнем слове, т.е. and
. Однако вы получили
представление о том, как создать модель генерации текста с помощью
Keras. Для улучшения результатов у меня для вас есть следующие
рекомендации:
- Измените гиперпараметры, включая размер и количество слоев LSTM и количество эпох, чтобы увидеть, получите ли вы лучшие результаты.
- Попробуйте удалить стоп-слова, такие как
is
,am
,are
из обучающего набора, чтобы генерировать слова, отличные от стоп-слов, в тестовом наборе (хотя это будет зависеть от типа приложения). - Создайте модель генерации текста на уровне символов, которая
предсказывает следующие
N
символов.
Чтобы попрактиковаться дальше, я бы порекомендовал вам попытаться разработать модель генерации текста с другими наборами данных из корпуса Гутенберга.
Заключение
В этой статье мы увидели, как создать модель генерации текста с помощью глубокого обучения с библиотекой Python Keras. Хотя модель, разработанная в этой статье, не идеальна, статья передает идею создания текста с помощью глубокого обучения.