Рубрики
Без рубрики

Генерация текста с помощью Python и TensorFlow/Keras

Автор оригинала: Dan Nelson.

Генерация текста с помощью Python и TensorFlow/Keras

Вступление

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

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

Определение Терминов

Для начала давайте определимся с нашими терминами. Может оказаться трудным понять, почему выполняются определенные строки кода, если у вас нет приличного понимания концепций, которые объединяются.

Тензорный поток

TensorFlow-одна из наиболее часто используемых библиотек машинного обучения в Python, специализирующаяся на создании глубоких нейронных сетей. Глубокие нейронные сети превосходно справляются с такими задачами, как распознавание образов и распознавание паттернов речи. TensorFlow был разработан Google Brain, и его сила заключается в способности объединять множество различных узлов обработки.

Керас

Между тем, Keras-это интерфейс прикладного программирования или API. Keras использует функции и возможности TensorFlow, но он упрощает реализацию функций TensorFlow, делая построение нейронной сети намного проще и проще. Основополагающими принципами Keras являются модульность и удобство использования, а это означает, что, хотя Keras довольно мощный, он прост в использовании и масштабировании.

Обработка естественного языка

Обработка естественного языка (НЛП) – это именно то, на что это похоже, методы, используемые для того, чтобы компьютеры могли понимать естественный человеческий язык, а не взаимодействовать с людьми через языки программирования. Обработка естественного языка необходима для таких задач, как классификация документов Word или создание чат-бота.

книга

Тело

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

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

Кодирование

Кодирование иногда называют представлением слов и оно относится к процессу преобразования текстовых данных в форму, понятную модели машинного обучения. Нейронные сети не могут работать с необработанными текстовыми данными, символы/слова должны быть преобразованы в ряд чисел, которые сеть может интерпретировать.

Фактический процесс преобразования слов в числовые векторы называется “токенизацией”, потому что вы получаете токены, представляющие фактические слова. Существует несколько способов кодирования слов в виде числовых значений. Основными методами кодирования являются горячее кодирование и создание плотно встроенных векторов .

Мы рассмотрим разницу между этими методами в разделе теории ниже.

Рекуррентная нейронная сеть

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

Напротив, Рекуррентная нейронная сеть отличается от “ванильной” нейронной сети своей способностью запоминать предыдущие входы из предыдущих слоев нейронной сети.

Иными словами, на выходы слоев в рекуррентной нейронной сети влияют не только веса и выход предыдущего слоя, как в обычной нейронной сети, но и “контекст”, который до сих пор является производным от предыдущих входов и выходов.

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

Длительная Кратковременная Память

Сети Long Short-Term Memory (Lstm) – это особый тип рекуррентных нейронных сетей. LSTMs имеют преимущества перед другими рекуррентными нейронными сетями. В то время как рекуррентные нейронные сети обычно могут запоминать предыдущие слова в предложении, их способность сохранять контекст более ранних входных данных со временем ухудшается.

Чем длиннее входной ряд, тем больше сеть “забывает”. Нерелевантные данные накапливаются с течением времени, и это блокирует релевантные данные, необходимые для сети, чтобы сделать точные прогнозы о структуре текста. Это называется проблемой исчезающего градиента .

Вам не нужно понимать алгоритмы, которые имеют дело с проблемой исчезающего градиента (хотя вы можете прочитать об этом подробнее здесь ), но знайте, что LSTM может справиться с этой проблемой, выборочно “забывая” информацию, считающуюся несущественной для данной задачи. Подавляя несущественную информацию, LSTM способен сосредоточиться только на той информации, которая действительно имеет значение, заботясь о проблеме исчезающего градиента. Это делает LSTMS более надежными при обработке длинных строк текста.

Теория/Подход К генерации текста

Кодировка Пересмотрена

Одно-Горячее кодирование

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

Процесс однократного кодирования относится к способу представления текста в виде ряда единиц и нулей. Создается вектор, содержащий все возможные слова, которые вас интересуют, часто все слова в корпусе, и одно слово представляется значением “один” в соответствующей позиции. Между тем все остальные позиции (все другие возможные слова) имеют нулевое значение. Такой вектор создается для каждого слова в наборе признаков, и когда векторы соединяются вместе, результатом является матрица, содержащая двоичные представления всех слов признаков.

Вот еще один способ думать об этом: любое данное слово представлено вектором единиц и нулей с одним значением в уникальной позиции. Вектор по существу связан с ответом на вопрос: “Это целевое слово?” Если слово в списке слов признаков является целевым, то там вводится положительное значение (единица), а во всех остальных случаях слово не является целевым, поэтому вводится ноль. Таким образом, у вас есть вектор, который представляет только целевое слово. Это делается для каждого слова в списке функций.

Однократные кодировки полезны , когда вам нужно создать пакет слов или представление слов, учитывающее их частоту встречаемости. Модели Bag of words полезны, потому что, хотя они являются простыми моделями, они все же содержат много важной информации и достаточно универсальны, чтобы использоваться для многих различных задач, связанных с НЛП.

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

Встраивание слов

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

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

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

Генерация на уровне слов против генерации на уровне символов

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

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

Напротив, языковые модели на уровне символов часто быстрее обучаются, требуют меньше памяти и имеют более быстрый вывод, чем словесные модели. Это связано с тем, что “словарный запас” (количество обучающих функций) для модели, скорее всего, будет намного меньше в целом, ограничен несколькими сотнями символов, а не сотнями тысяч слов.

Символьные модели также хорошо работают при переводе слов между языками, потому что они захватывают символы, которые составляют слова, а не пытаются захватить семантические качества слов. Здесь мы будем использовать модель уровня персонажа, отчасти из-за ее простоты и быстрого вывода.

Использование RNN/LSTM

Когда дело доходит до реализации ITSM в Keras, процесс аналогичен реализации других нейронных сетей, созданных с помощью последовательной модели. Вы начинаете с объявления типа структуры модели, которую собираетесь использовать, а затем добавляете слои к модели по одному. Слои LSTM легко доступны нам в Keras, нам просто нужно импортировать слои, а затем добавить их с помощью model.add .

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

Последовательности и особенности

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

Функции для нашей модели-это просто слова, которые мы заинтересованы в анализе, как это представлено в пакете слов. Куски, на которые мы делим корпус, будут последовательностями слов, и вы можете думать о каждой последовательности как об отдельном учебном экземпляре/примере в традиционной задаче машинного обучения.

Реализация LSTM для генерации текста

Теперь мы будем внедрять LSTM и делать с ним генерацию текста. Во-первых, нам нужно получить некоторые текстовые данные и предварительно обработать их. После этого мы создадим модель LSTM и обучим ее на основе полученных данных. Наконец, мы оценим сеть.

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

Давайте начнем с импорта всех библиотек, которые мы собираемся использовать. Нам нужно numpy преобразовать наши входные данные в массивы, которые может использовать наша сеть, и мы, очевидно, будем использовать несколько функций из Keras.

Нам также нужно будет использовать некоторые функции из набора инструментов Natural Language Toolkit (NLTK) для предварительной обработки нашего текста и подготовки его к обучению. Наконец, нам понадобится библиотека sys для обработки печати нашего текста:

import numpy
import sys
from nltk.tokenize import RegexpTokenizer
from nltk.corpus import stopwords
from keras.models import Sequential
from keras.layers import Dense, Dropout, LSTM
from keras.utils import np_utils
from keras.callbacks import ModelCheckpoint

Для начала нам нужны данные для обучения нашей модели. Вы можете использовать для этого любой текстовый файл, но мы будем использовать часть книги Мэри Шелли “Франкенштейн”, которая доступна для скачивания по адресу Project Gutenburg , где размещаются тексты общественного достояния.

Мы будем обучать сеть тексту из первых 9 глав:

file = open("frankenstein-2.txt").read()

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

Мы будем писать все в нижнем регистре и не будем беспокоиться о капитализации в этом примере. Мы также будем использовать NLTK для создания токенов из слов во входном файле. Давайте создадим экземпляр токенизатора и используем его в нашем входном файле.

Наконец, мы собираемся отфильтровать наш список токенов и сохранить только те токены, которые не входят в список стоп-слов или общих слов, которые дают мало информации о рассматриваемом предложении. Мы сделаем это с помощью lambda , чтобы сделать быструю выбрасываемую функцию и назначить слова нашей переменной только в том случае, если их нет в списке стоп-слов, предоставленном NLTK.

Давайте создадим функцию для обработки всего этого:

def tokenize_words(input):
    # lowercase everything to standardize it
    input = input.lower()

    # instantiate the tokenizer
    tokenizer = RegexpTokenizer(r'\w+')
    tokens = tokenizer.tokenize(input)

    # if the created token isn't in the stop words, make it part of "filtered"
    filtered = filter(lambda token: token not in stopwords.words('english'), tokens)
    return " ".join(filtered)

Теперь мы вызываем функцию в нашем файле:

# preprocess the input data, make tokens
processed_inputs = tokenize_words(file)

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

chars = sorted(list(set(processed_inputs)))
char_to_num = dict((c, i) for i, c in enumerate(chars))

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

input_len = len(processed_inputs)
vocab_len = len(chars)
print ("Total number of characters:", input_len)
print ("Total vocab:", vocab_len)

Вот результат:

Total number of characters: 100581
Total vocab: 42

Теперь, когда мы преобразовали данные в форму, в которой они должны быть, мы можем начать делать из них набор данных, который мы будем подавать в нашу сеть. Нам нужно определить, какой длины мы хотим, чтобы была отдельная последовательность (одно полное отображение входных символов в виде целых чисел). На данный момент мы установим длину 100 и объявим пустые списки для хранения наших входных и выходных данных:

seq_length = 100
x_data = []
y_data = []

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

# loop through inputs, start at the beginning and go until we hit
# the final character we can create a sequence out of
for i in range(0, input_len - seq_length, 1):
    # Define input and output sequences
    # Input is the current character plus desired sequence length
    in_seq = processed_inputs[i:i + seq_length]

    # Out sequence is the initial character plus total sequence length
    out_seq = processed_inputs[i + seq_length]

    # We now convert list of characters to integers based on
    # previously and add the values to our lists
    x_data.append([char_to_num[char] for char in in_seq])
    y_data.append(char_to_num[out_seq])

Теперь у нас есть входные последовательности символов и выходные, то есть символ, который должен появиться после окончания последовательности. Теперь у нас есть функции и метки обучающих данных, хранящиеся как x_data и y_data. Давайте сохраним наше общее количество последовательностей и проверим, сколько всего входных последовательностей у нас есть:

n_patterns = len(x_data)
print ("Total Patterns:", n_patterns)

Вот результат:

Total Patterns: 100481

Теперь мы пойдем дальше и преобразуем наши входные последовательности в обработанный массив numpy, который может использовать наша сеть. Нам также нужно будет преобразовать значения массива numpy в поплавки, чтобы сигмоидная функция активации, используемая нашей сетью, могла интерпретировать их и выводить вероятности от 0 до 1:

X = numpy.reshape(x_data, (n_patterns, seq_length, 1))
X = X/float(vocab_len)

Теперь мы будем горячо кодировать наши данные этикетки:

y = np_utils.to_categorical(y_data)

Поскольку наши функции и метки теперь готовы к использованию в сети, давайте продолжим и создадим нашу модель LSTM. Мы указываем тип модели, которую мы хотим сделать (a sequential one), а затем добавляем наш первый слой.

Мы сделаем отсев, чтобы предотвратить переобучение, а затем еще один или два слоя. Затем мы добавим последний слой, плотно связанный слой, который выведет вероятность того, каким будет следующий символ в последовательности:

model = Sequential()
model.add(LSTM(256, input_shape=(X.shape[1], X.shape[2]), return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(256, return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(128))
model.add(Dropout(0.2))
model.add(Dense(y.shape[1], activation='softmax'))

Сейчас мы компилируем модель, и она готова к обучению:

model.compile(loss='categorical_crossentropy', optimizer='adam')

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

filepath = "model_weights_saved.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor='loss', verbose=1, save_best_only=True, mode='min')
desired_callbacks = [checkpoint]

Теперь мы подгоним модель, и пусть идет дождь.

model.fit(X, y, epochs=4, batch_size=256, callbacks=desired_callbacks)

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

filename = "model_weights_saved.hdf5"
model.load_weights(filename)
model.compile(loss='categorical_crossentropy', optimizer='adam')

Поскольку мы преобразовали символы в числа ранее, нам нужно определить переменную словаря, которая преобразует выходные данные модели обратно в числа:

num_to_char = dict((i, c) for i, c in enumerate(chars))

Чтобы генерировать символы, нам нужно предоставить нашей обученной модели случайный начальный символ, из которого она может генерировать последовательность символов:

start = numpy.random.randint(0, len(x_data) - 1)
pattern = x_data[start]
print("Random Seed:")
print("\"", ''.join([num_to_char[value] for value in pattern]), "\"")

Вот пример случайного семени:

" ed destruction pause peace grave succeeded sad torments thus spoke prophetic soul torn remorse horro "

Теперь, чтобы окончательно сгенерировать текст, мы будем перебирать выбранное вами количество символов и преобразовывать наши входные данные (случайное семя) в значения float .

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

for i in range(1000):
    x = numpy.reshape(pattern, (1, len(pattern), 1))
    x = x / float(vocab_len)
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    result = num_to_char[index]

    sys.stdout.write(result)

    pattern.append(index)
    pattern = pattern[1:len(pattern)]

Давайте посмотрим, что он породил.

"er ed thu so sa fare ver ser ser er serer serer serer serer serer serer serer serer serer serer serer serer serer serer serer serer serer serer...."

Не кажется ли вам это несколько разочаровывающим? Да, текст, который был сгенерирован, не имеет никакого смысла, и через некоторое время он, кажется, начинает просто повторять шаблоны. Однако чем дольше вы тренируете сеть, тем лучше будет сгенерированный текст.

Например, когда количество учебных эпох было увеличено до 20, результат выглядел примерно так:

"ligther my paling the same been the this manner to the forter the shempented and the had an ardand the verasion the the dears conterration of the astore"

Модель теперь генерирует реальные слова, даже если большая их часть все еще не имеет смысла. Тем не менее, всего для 100 строк кода это неплохо.

Теперь вы можете сами поиграть с моделью и попробовать настроить параметры, чтобы получить лучшие результаты.

Вывод

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

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

Если вы хотите узнать больше о том, как работают элементы, вы можете прочитать на эту тему здесь . Изучение того, как параметры модели влияют на производительность модели, поможет вам выбрать, какие параметры или гиперпараметры следует корректировать. Вы также можете прочитать о методах обработки текста и инструментах, подобных тем, которые предоставляет NLTK.

Если вы хотите узнать больше об обработке естественного языка в Python, у нас есть серия из 12 частей, которая углублена: Python for NLP .

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