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

Введение Вы заинтересованы в использовании нейронной сети для генерации текста? TensorFlow [https://www.tensorflow.org/] и Keras [https://keras.io/] можно использовать для некоторых удивительных приложений методов обработки естественного языка, включая создание текста. В этом руководстве мы рассмотрим теорию генерации текста с использованием рекуррентных нейронных сетей, в частности, сети с долгосрочной краткосрочной памятью, реализуем эту сеть на Python и используем ее для генерации текста. Определение терминов

Вступление

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

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

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

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

TensorFlow

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

Керас

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

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

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

книга{.ezlazyload}

Корпус

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

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

Кодирование

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

Фактический процесс преобразования слов в числовые векторы называется «токенизацией», потому что вы получаете токены, которые представляют фактические слова. Есть несколько способов кодирования слов как числовых значений. Основные методы кодирования - это однократное кодирование и создание плотно встроенных векторов .

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

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

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

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

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

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

Долговременная кратковременная память

{.ezlazyload}

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

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

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

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

Возвращение к кодировке

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

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

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

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

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

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

Вложения слов

{.ezlazyload}

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

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

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

Генерация на уровне слов vs генерация на уровне персонажа

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

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

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

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

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

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

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

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

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

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

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

{.ezlazyload}

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

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

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

Нам также нужно будет использовать некоторые функции из набора инструментов естественного языка (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. Мы указываем тип модели, которую хотим создать ( sequential ), а затем добавляем наш первый слой.

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

 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') 

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

 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 строк кода это неплохо.

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

Заключение

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

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

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

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

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

comments powered by Disqus