Python для НЛП: нейронный машинный перевод с помощью Seq2Seq в Керасе

Это 22-я статья из моей серии статей о Python для НЛП. В одной из моих предыдущих статей о решении проблем последовательности с помощью Keras [/ resolve-sequence-issues-with-lstm-in-keras-part-2 /] я объяснил, как решить многие или многие задачи последовательности, в которых как входы, так и выходы разделены на несколько временных шагов. Архитектура seq2seq [https://google.github.io/seq2seq/] представляет собой тип моделирования последовательности "многие ко многим" и обычно используется для различных задач, таких как текстовое суммирование,

Это 22-я статья из моей серии статей о Python для НЛП. В одной из моих предыдущих статей о решении проблем последовательности с помощью Keras я объяснил, как решить многие и многие проблемы последовательности, когда и входы, и выходы разделены на несколько временных шагов. Архитектура seq2seq представляет собой тип моделирования последовательности «многие ко многим» и обычно используется для различных задач, таких как текстовое обобщение, разработка чат-ботов, диалоговое моделирование, нейронный машинный перевод и т. Д.

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

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

Библиотеки и параметры конфигурации

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

 import os, sys 
 
 from keras.models import Model 
 from keras.layers import Input, LSTM, GRU, Dense, Embedding 
 from keras.preprocessing.text import Tokenizer 
 from keras.preprocessing.sequence import pad_sequences 
 from keras.utils import to_categorical 
 import numpy as np 
 import matplotlib.pyplot as plt 

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

 BATCH_SIZE = 64 
 EPOCHS = 20 
 LSTM_NODES =256 
 NUM_SENTENCES = 20000 
 MAX_SENTENCE_LENGTH = 50 
 MAX_NUM_WORDS = 20000 
 EMBEDDING_SIZE = 100 

Набор данных

Модель языкового перевода, которую мы собираемся разработать в этой статье, будет переводить английские предложения на их французские аналоги. Чтобы разработать такую модель, нам нужен набор данных, содержащий предложения на английском языке и их французские переводы. К счастью, такой набор данных находится в свободном доступе по этой ссылке . Загрузите файл fra-eng.zip и распакуйте его. После этого вы увидите файл fra.txt В каждой строке текстового файла содержится английское предложение и его французский перевод, разделенные табуляцией. Первые 20 строк fra.txt выглядят так:

 Go. Va ! 
 Hi. Salut ! 
 Hi. Salut. 
 Run! Cours ! 
 Run! Courez ! 
 Who? Qui ? 
 Wow! Ça alors ! 
 Fire! Au feu ! 
 Help! À l'aide ! 
 Jump. Saute. 
 Stop! Ça suffit ! 
 Stop! Stop ! 
 Stop! Arrête-toi ! 
 Wait! Attends ! 
 Wait! Attendez ! 
 Go on. Poursuis. 
 Go on. Continuez. 
 Go on. Poursuivez. 
 Hello! Bonjour ! 
 Hello! Salut ! 

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

Предварительная обработка данных

Модели нейронного машинного перевода часто основаны на архитектуре seq2seq. Архитектура seq2seq - это архитектура кодера-декодера, которая состоит из двух сетей LSTM: кодировщика LSTM и декодера LSTM. Входными данными кодировщика LSTM является предложение на исходном языке; вход декодера LSTM - это предложение на переведенном языке с токеном начала предложения. Результатом является фактическое целевое предложение с токеном конца предложения.

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

 input_sentences = [] 
 output_sentences = [] 
 output_sentences_inputs = [] 
 
 count = 0 
 for line in open(r'/content/drive/My Drive/datasets/fra.txt', encoding="utf-8"): 
 count += 1 
 
 if count > NUM_SENTENCES: 
 break 
 
 if '\t' not in line: 
 continue 
 
 input_sentence, output = line.rstrip().split('\t') 
 
 output_sentence = output + ' <eos>' 
 output_sentence_input = '<sos> ' + output 
 
 input_sentences.append(input_sentence) 
 output_sentences.append(output_sentence) 
 output_sentences_inputs.append(output_sentence_input) 
 
 print("num samples input:", len(input_sentences)) 
 print("num samples output:", len(output_sentences)) 
 print("num samples output input:", len(output_sentences_inputs)) 

Примечание : вам, вероятно, потребуется изменить путь к fra.txt на вашем компьютере, чтобы это работало.

В приведенном выше скрипте мы создаем три списка input_sentences[] , output_sentences[] и output_sentences_inputs[] . Далее, в for цикла в fra.txt файл считывается построчно . Каждая строка разделена на две подстроки в том месте, где находится табуляция. Левая подстрока (английское предложение) вставляется в список input_sentences[] . Подстрока справа от вкладки - это соответствующее переведенное французское предложение. <eos> , который отмечает конец предложения, ставится перед переведенным предложением, а результирующее предложение добавляется в список output_sentences[] . Точно так же <sos> , который означает «начало предложения», объединяется в начале переведенного предложения, и результат добавляется в список output_sentences_inputs[] . Цикл завершается, если количество предложений, добавленных в списки, превышает NUM_SENTENCES переменной NUM_SENTENCES, т. Е. 20 000.

Наконец, в выводе отображается количество образцов в трех списках:

 num samples input: 20000 
 num samples output: 20000 
 num samples output input: 20000 

Давайте теперь случайным образом напечатаем предложение из input_sentences[] , output_sentences[] и output_sentences_inputs[] :

 print(input_sentences[172]) 
 print(output_sentences[172]) 
 print(output_sentences_inputs[172]) 

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

 I'm ill. 
 Je suis malade. <eos> 
 <sos> Je suis malade. 

Вы можете увидеть исходное предложение, т.е. I'm ill ; соответствующий перевод в выводе, то есть Je suis malade. <eos> . Обратите внимание, здесь в конце предложения <eos> Точно так же для входа в декодер у нас есть <sos> Je suis malade.

Токенизация и заполнение

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

Для токенизации, то Tokenizer класс от keras.preprocessing.text можно использовать библиотеку. Класс tokenizer выполняет две задачи:

  • Делит предложение на соответствующий список слов
  • Затем он преобразует слова в целые числа

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

 input_tokenizer = Tokenizer(num_words=MAX_NUM_WORDS) 
 input_tokenizer.fit_on_texts(input_sentences) 
 input_integer_seq = input_tokenizer.texts_to_sequences(input_sentences) 
 
 word2idx_inputs = input_tokenizer.word_index 
 print('Total unique words in the input: %s' % len(word2idx_inputs)) 
 
 max_input_len = max(len(sen) for sen in input_integer_seq) 
 print("Length of longest sentence in input: %g" % max_input_len) 

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

 Total unique words in the input: 3523 
 Length of longest sentence in input: 6 

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

 output_tokenizer = Tokenizer(num_words=MAX_NUM_WORDS, filters='') 
 output_tokenizer.fit_on_texts(output_sentences + output_sentences_inputs) 
 output_integer_seq = output_tokenizer.texts_to_sequences(output_sentences) 
 output_input_integer_seq = output_tokenizer.texts_to_sequences(output_sentences_inputs) 
 
 word2idx_outputs = output_tokenizer.word_index 
 print('Total unique words in the output: %s' % len(word2idx_outputs)) 
 
 num_words_output = len(word2idx_outputs) + 1 
 max_out_len = max(len(sen) for sen in output_integer_seq) 
 print("Length of longest sentence in the output: %g" % max_out_len) 

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

 Total unique words in the output: 9561 
 Length of longest sentence in the output: 13 

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

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

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

 encoder_input_sequences = pad_sequences(input_integer_seq, maxlen=max_input_len) 
 print("encoder_input_sequences.shape:", encoder_input_sequences.shape) 
 print("encoder_input_sequences[172]:", encoder_input_sequences[172]) 

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

 encoder_input_sequences.shape: (20000, 6) 
 encoder_input_sequences[172]: [ 0 0 0 0 6 539] 

Поскольку во входных данных 20 000 предложений и каждое входное предложение имеет длину 6, форма ввода теперь будет (20000, 6). Если вы посмотрите на целочисленную последовательность для предложения с индексом 172 входного предложения, вы увидите, что есть три нуля, за которыми следуют значения 6 и 539. Вы можете вспомнить, что исходное предложение с индексом 172: « I'm ill . Токенизатор разделил предложение на два слова, I'm и ill , преобразовал их в целые числа, а затем применил предварительное заполнение, добавив три нуля в начале соответствующей целочисленной последовательности для предложения с индексом 172 входного списка.

Чтобы убедиться, что целочисленные значения для i'm и ill равны 6 и 539 соответственно, вы можете передать слова в word2index_inputs , как показано ниже:

 print(word2idx_inputs["i'm"]) 
 print(word2idx_inputs["ill"]) 

Выход:

 6 
 539 

Таким же образом выходы декодера и входы декодера дополняются следующим образом:

 decoder_input_sequences = pad_sequences(output_input_integer_seq, maxlen=max_out_len, padding='post') 
 print("decoder_input_sequences.shape:", decoder_input_sequences.shape) 
 print("decoder_input_sequences[172]:", decoder_input_sequences[172]) 

Выход:

 decoder_input_sequences.shape: (20000, 13) 
 decoder_input_sequences[172]: [ 2 3 6 188 0 0 0 0 0 0 0 0 0] 

Предложение с индексом 172 на входе декодера - это <sos> je suis malade. . Если вы напечатаете соответствующие целые числа из word2idx_outputs , вы должны увидеть 2, 3, 6 и 188, напечатанные на консоли, как показано здесь:

 print(word2idx_outputs["<sos>"]) 
 print(word2idx_outputs["je"]) 
 print(word2idx_outputs["suis"]) 
 print(word2idx_outputs["malade."]) 

Выход:

 2 
 3 
 6 
 188 

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

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

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

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

Есть два основных различия между представлением одного целого числа и встраиванием слов. При целочисленном представлении слово представляется только одним целым числом. В векторном представлении слово представлено вектором 50, 100, 200 или любых других размеров, которые вам нравятся. Следовательно, вложения слов захватывают гораздо больше информации о словах. Во-вторых, одноцелочисленное представление не фиксирует отношения между разными словами. Напротив, вложения слов сохраняют отношения между словами. Вы можете использовать собственные вложения слов или предварительно обученные вложения слов.

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

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

 from numpy import array 
 from numpy import asarray 
 from numpy import zeros 
 
 embeddings_dictionary = dict() 
 
 glove_file = open(r'/content/drive/My Drive/datasets/glove.6B.100d.txt', encoding="utf8") 
 
 for line in glove_file: 
 records = line.split() 
 word = records[0] 
 vector_dimensions = asarray(records[1:], dtype='float32') 
 embeddings_dictionary[word] = vector_dimensions 
 glove_file.close() 

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

 num_words = min(MAX_NUM_WORDS, len(word2idx_inputs) + 1) 
 embedding_matrix = zeros((num_words, EMBEDDING_SIZE)) 
 for word, index in word2idx_inputs.items(): 
 embedding_vector = embeddings_dictionary.get(word) 
 if embedding_vector is not None: 
 embedding_matrix[index] = embedding_vector 

Давайте сначала напечатаем слово embeddings для слова ill используя словарь встраивания слов GloVe.

 print(embeddings_dictionary["ill"]) 

Выход:

 [ 0.12648 0.1366 0.22192 -0.025204 -0.7197 0.66147 
 0.48509 0.057223 0.13829 -0.26375 -0.23647 0.74349 
 0.46737 -0.462 0.20031 -0.26302 0.093948 -0.61756 
 -0.28213 0.1353 0.28213 0.21813 0.16418 0.22547 
 -0.98945 0.29624 -0.62476 -0.29535 0.21534 0.92274 
 0.38388 0.55744 -0.14628 -0.15674 -0.51941 0.25629 
 -0.0079678 0.12998 -0.029192 0.20868 -0.55127 0.075353 
 0.44746 -0.71046 0.75562 0.010378 0.095229 0.16673 
 0.22073 -0.46562 -0.10199 -0.80386 0.45162 0.45183 
 0.19869 -1.6571 0.7584 -0.40298 0.82426 -0.386 
 0.0039546 0.61318 0.02701 -0.3308 -0.095652 -0.082164 
 0.7858 0.13394 -0.32715 -0.31371 -0.20247 -0.73001 
 -0.49343 0.56445 0.61038 0.36777 -0.070182 0.44859 
 -0.61774 -0.18849 0.65592 0.44797 -0.10469 0.62512 
 -1.9474 -0.60622 0.073874 0.50013 -1.1278 -0.42066 
 -0.37322 -0.50538 0.59171 0.46534 -0.42482 0.83265 
 0.081548 -0.44147 -0.084311 -1.2304 ] 

В предыдущем разделе мы видели, что целочисленное представление слова ill - 539. Давайте теперь проверим 539-й индекс матрицы встраивания слов.

 print(embedding_matrix[539]) 

Выход:

 [ 0.12648 0.1366 0.22192 -0.025204 -0.7197 0.66147 
 0.48509 0.057223 0.13829 -0.26375 -0.23647 0.74349 
 0.46737 -0.462 0.20031 -0.26302 0.093948 -0.61756 
 -0.28213 0.1353 0.28213 0.21813 0.16418 0.22547 
 -0.98945 0.29624 -0.62476 -0.29535 0.21534 0.92274 
 0.38388 0.55744 -0.14628 -0.15674 -0.51941 0.25629 
 -0.0079678 0.12998 -0.029192 0.20868 -0.55127 0.075353 
 0.44746 -0.71046 0.75562 0.010378 0.095229 0.16673 
 0.22073 -0.46562 -0.10199 -0.80386 0.45162 0.45183 
 0.19869 -1.6571 0.7584 -0.40298 0.82426 -0.386 
 0.0039546 0.61318 0.02701 -0.3308 -0.095652 -0.082164 
 0.7858 0.13394 -0.32715 -0.31371 -0.20247 -0.73001 
 -0.49343 0.56445 0.61038 0.36777 -0.070182 0.44859 
 -0.61774 -0.18849 0.65592 0.44797 -0.10469 0.62512 
 -1.9474 -0.60622 0.073874 0.50013 -1.1278 -0.42066 
 -0.37322 -0.50538 0.59171 0.46534 -0.42482 0.83265 
 0.081548 -0.44147 -0.084311 -1.2304 ] 

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

Следующий скрипт создает слой внедрения для ввода:

 embedding_layer = Embedding(num_words, EMBEDDING_SIZE, weights=[embedding_matrix], input_length=max_input_len) 

Создание модели

Пришло время разработать нашу модель. Первое, что нам нужно сделать, это определить наши выходные данные, поскольку мы знаем, что выходными данными будет последовательность слов. Напомним, что общее количество уникальных слов в выходных данных составляет 9562. Следовательно, каждое слово в выходных данных может быть любым из 9562 слов. Длина выходного предложения - 13. И для каждого входного предложения нам нужно соответствующее выходное предложение. Таким образом, окончательная форма вывода будет такой:

 (number of inputs, length of the output sentence, the number of words in the output) 

Следующий скрипт создает пустой выходной массив:

 decoder_targets_one_hot = np.zeros(( 
 len(input_sentences), 
 max_out_len, 
 num_words_output 
 ), 
 dtype='float32' 
 ) 

Следующий скрипт печатает форму декодера:

 decoder_targets_one_hot.shape 

Выход:

 (20000, 13, 9562) 

Чтобы делать прогнозы, последний слой модели будет плотным слоем, поэтому нам нужны выходные данные в виде векторов с горячим кодированием, поскольку мы будем использовать функцию активации softmax на плотном слое. Чтобы создать такой вывод с горячим кодированием, следующим шагом является присвоение 1 номеру столбца, который соответствует целочисленному представлению слова. Например, целочисленное представление для <sos> je suis malade - [ 2 3 6 188 0 0 0 0 0 0 0 ] . В decoder_targets_one_hot во втором столбце первой строки будет вставлена 1. Точно так же в третий индекс второй строки будет вставлена еще одна 1 и так далее.

Взгляните на следующий сценарий:

 for i, d in enumerate(decoder_output_sequences): 
 for t, word in enumerate(d): 
 decoder_targets_one_hot[i, t, word] = 1 

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

Следующий сценарий определяет кодировщик:

 encoder_inputs_placeholder = Input(shape=(max_input_len,)) 
 x = embedding_layer(encoder_inputs_placeholder) 
 encoder = LSTM(LSTM_NODES, return_state=True) 
 
 encoder_outputs, h, c = encoder(x) 
 encoder_states = [h, c] 

Следующим шагом является определение декодера. Декодер будет иметь два входа: скрытое состояние и состояние ячейки из кодировщика и входное предложение, которое фактически будет выходным предложением с добавленным в начале токеном <sos>

Следующий скрипт создает декодер LSTM:

 decoder_inputs_placeholder = Input(shape=(max_out_len,)) 
 
 decoder_embedding = Embedding(num_words_output, LSTM_NODES) 
 decoder_inputs_x = decoder_embedding(decoder_inputs_placeholder) 
 
 decoder_lstm = LSTM(LSTM_NODES, return_sequences=True, return_state=True) 
 decoder_outputs, _, _ = decoder_lstm(decoder_inputs_x, initial_state=encoder_states) 

Наконец, выходные данные декодера LSTM проходят через плотный слой для прогнозирования выходных данных декодера, как показано здесь:

 decoder_dense = Dense(num_words_output, activation='softmax') 
 decoder_outputs = decoder_dense(decoder_outputs) 

Следующим шагом будет компиляция модели:

 model = Model([encoder_inputs_placeholder, 
 decoder_inputs_placeholder], decoder_outputs) 
 model.compile( 
 optimizer='rmsprop', 
 loss='categorical_crossentropy', 
 metrics=['accuracy'] 
 ) 

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

 from keras.utils import plot_model 
 plot_model(model, to_file='model_plot4a.png', show_shapes=True, show_layer_names=True) 

Выход:

{.ezlazyload}

Из вывода вы можете видеть, что у нас есть два типа ввода. input_1 - это входной заполнитель для кодировщика, который встроен и передан через lstm_1 , который в основном является кодировщиком LSTM. Есть три выхода из lstm_1 : выход, скрытый слой и состояние ячейки. Однако декодеру передаются только состояние ячейки и скрытое состояние.

Здесь lstm_2 - это декодер LSTM. input_2 содержит выходные предложения с добавленным в начале токеном <sos> input_2 также проходит через слой внедрения и используется в качестве входных данных для декодера LSTM, lstm_2 . Наконец, выходные данные декодера LSTM проходят через плотный слой для прогнозирования.

Следующим шагом будет обучение модели с помощью метода fit()

 r = model.fit( 
 [encoder_input_sequences, decoder_input_sequences], 
 decoder_targets_one_hot, 
 batch_size=BATCH_SIZE, 
 epochs=EPOCHS, 
 validation_split=0.1, 
 ) 

Модель обучается на 18 000 записей и тестируется на оставшихся 2 000 записей. Модель обучена для 20 эпох, вы можете изменить количество эпох, чтобы увидеть, сможете ли вы получить лучшие результаты. После 20 эпох я получил точность обучения 90,99% и точность проверки 79,11%, что показывает, что модель переобучена. Чтобы уменьшить переоснащение, вы можете добавить отсев или больше записей. Мы обучаем только 20 000 записей, поэтому вы можете добавить больше записей, чтобы уменьшить переобучение.

Изменение модели для прогнозов

Во время обучения мы знаем фактические входные данные декодера для всех выходных слов в последовательности. Вот пример того, что происходит во время тренировки. Предположим, у нас есть предложение « i'm ill . Предложение переводится следующим образом:

 // Inputs on the left of Encoder/Decoder, outputs on the right. 
 
 Step 1: 
 I'm ill -> Encoder -> enc(h1,c1) 
 
 enc(h1,c1) + <sos> -> Decoder -> je + dec(h1,c1) 
 
 step 2: 
 
 enc(h1,c1) + je -> Decoder -> suis + dec(h2,c2) 
 
 step 3: 
 
 enc(h2,c2) + suis -> Decoder -> malade. + dec(h3,c3) 
 
 step 3: 
 
 enc(h3,c3) + malade. -> Decoder -> <eos> + dec(h4,c4) 

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

Однако во время предсказаний следующее слово будет предсказано на основе предыдущего слова, которое, в свою очередь, также предсказано на предыдущем временном шаге. Теперь вы поймете назначение токенов <sos> и <eos> При выполнении реальных прогнозов полная выходная последовательность недоступна, на самом деле это то, что мы должны прогнозировать. Во время предсказания нам доступно только слово <sos> поскольку все выходные предложения начинаются с <sos> .

Пример того, что происходит во время прогнозирования, выглядит следующим образом. Снова переведем фразу i'm ill :

 // Inputs on the left of Encoder/Decoder, outputs on the right. 
 
 Step 1: 
 
 I'm ill -> Encoder -> enc(h1,c1) 
 
 enc(h1,c1) + <sos> -> Decoder -> y1(je) + dec(h1,c1) 
 
 step 2: 
 
 enc(h1,c1) + y1 -> Decoder -> y2(suis) + dec(h2,c2) 
 
 step 3: 
 
 enc(h2,c2) + y2 -> Decoder -> y3(malade.) + dec(h3,c3) 
 
 step 3: 
 
 enc(h3,c3) + y3 -> Decoder -> y4(<eos>) + dec(h4,c4) 

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

На шаге 1 скрытое состояние и состояние ячейки кодера, а также <sos> используются в качестве входных данных для декодера. Декодер предсказывает слово y1 которое может быть верным, а может и нет. Однако, согласно нашей модели, вероятность правильного прогноза составляет 0,7911. На шаге 2 скрытое состояние декодера и состояние ячейки из шага 1 вместе с y1 используется в качестве входных данных для декодера, который предсказывает y2 . Процесс продолжается до тех пор, пока не будет обнаружен токен <eos> Затем все предсказанные выходные данные декодера объединяются для формирования окончательного выходного предложения. Давайте изменим нашу модель, чтобы реализовать эту логику.

Модель кодировщика осталась прежней:

 encoder_model = Model(encoder_inputs_placeholder, encoder_states) 

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

 decoder_state_input_h = Input(shape=(LSTM_NODES,)) 
 decoder_state_input_c = Input(shape=(LSTM_NODES,)) 
 decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c] 

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

 decoder_inputs_single = Input(shape=(1,)) 
 decoder_inputs_single_x = decoder_embedding(decoder_inputs_single) 

Далее нам нужно создать заполнитель для выходных данных декодера:

 decoder_outputs, h, c = decoder_lstm(decoder_inputs_single_x, initial_state=decoder_states_inputs) 

Чтобы делать прогнозы, выходные данные декодера проходят через плотный слой:

 decoder_states = [h, c] 
 decoder_outputs = decoder_dense(decoder_outputs) 

Последний шаг - определить обновленную модель декодера, как показано здесь:

 decoder_model = Model( 
 [decoder_inputs_single] + decoder_states_inputs, 
 [decoder_outputs] + decoder_states 
 ) 

Давайте теперь построим наш модифицированный декодер LSTM, который делает прогнозы:

 from keras.utils import plot_model 
 plot_model(decoder_model, to_file='model_plot_dec.png', show_shapes=True, show_layer_names=True) 

Выход:

{.ezlazyload}

На изображении выше lstm_2 - модифицированный декодер LSTM. Вы можете видеть, что он принимает предложение с одним словом, как показано в input_5 , а также состояния скрытых и ячеек из предыдущего вывода ( input_3 и input_4 ). Вы можете видеть, что форма входного предложения теперь (none,1) поскольку на входе декодера будет только одно слово. Напротив, во время обучения форма входного предложения была (None,6) поскольку входные данные содержали полное предложение с максимальной длиной 6.

Прогнозы

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

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

 idx2word_input = {v:k for k, v in word2idx_inputs.items()} 
 idx2word_target = {v:k for k, v in word2idx_outputs.items()} 

Далее мы создадим метод, то есть translate_sentence() . Метод примет английское предложение с последовательностью ввода (в целочисленной форме) и вернет переведенное французское предложение. Посмотрите на метод translate_sentence() :

 def translate_sentence(input_seq): 
 states_value = encoder_model.predict(input_seq) 
 target_seq = np.zeros((1, 1)) 
 target_seq[0, 0] = word2idx_outputs['<sos>'] 
 eos = word2idx_outputs['<eos>'] 
 output_sentence = [] 
 
 for _ in range(max_out_len): 
 output_tokens, h, c = decoder_model.predict([target_seq] + states_value) 
 idx = np.argmax(output_tokens[0, 0, :]) 
 
 if eos == idx: 
 break 
 
 word = '' 
 
 if idx > 0: 
 word = idx2word_target[idx] 
 output_sentence.append(word) 
 
 target_seq[0, 0] = idx 
 states_value = [h, c] 
 
 return ' '.join(output_sentence) 

В приведенном выше сценарии мы передаем входную последовательность в encoder_model , которая предсказывает скрытое состояние и состояние ячейки, которые хранятся в переменной states_value

Затем мы определяем переменную target_seq , которая представляет собой матрицу всех нулей 1 x 1 target_seq содержит первое слово модели декодера, то есть <sos> .

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

Затем мы выполняем цикл for Число циклов выполнения для for цикла равна длине самого длинного предложения в выходе. Внутри цикла, в первой итерации, decoder_model предсказывает выходные данные, а также скрытые состояния и состояния ячеек, используя скрытые и ячейки состояния кодировщика и входной токен, то есть <sos> . Индекс предсказанного слова сохраняется в переменной idx Если значение прогнозируемого индекса равно <eos> , цикл завершается. В противном случае, если прогнозируемый индекс больше нуля, соответствующее слово извлекается из idx2word и сохраняется в word , которая затем добавляется к списку output_sentence states_value обновляется новым скрытым состоянием и состоянием ячейки декодера, а индекс предсказанного слова сохраняется в переменной target_seq В следующем цикле цикла обновленные скрытые состояния и состояния ячеек вместе с индексом ранее предсказанного слова используются для создания новых прогнозов. Цикл продолжается до тех пор, пока не будет достигнута максимальная длина выходной последовательности или пока не будет обнаружен токен <eos>

Наконец, слова в output_sentence объединяются с использованием пробела, и результирующая строка возвращается вызывающей функции.

Тестирование модели

Чтобы проверить код, мы случайным образом выберем предложение из input_sentences , получим соответствующую дополненную последовательность для предложения и передадим ее методу translate_sentence() Метод вернет переведенное предложение, как показано ниже.

Вот сценарий для проверки работоспособности модели:

 i = np.random.choice(len(input_sentences)) 
 input_seq = encoder_input_sequences[i:i+1] 
 translation = translate_sentence(input_seq) 
 print('-') 
 print('Input:', input_sentences[i]) 
 print('Response:', translation) 

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

 - 
 Input: You're not fired. 
 Response: vous n'êtes pas viré. 

Гениально, не правда ли? Наша модель успешно перевела фразу " You're not fired на французский. Вы также можете проверить это с помощью Google Translate. Попробуем еще один.

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

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

 - 
 Input: I'm not a lawyer. 
 Response: je ne suis pas avocat. 

Модель успешно перевела еще одно английское предложение на французский.

Заключение и перспектива

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

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

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

comments powered by Disqus