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

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

Архитектура seq2seq-это тип моделирования последовательностей “многие ко многим”. В этой статье мы создадим модель машинного перевода на Python с помощью Keras.

Автор оригинала: Usman Malik.

Это 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

Набор данных

Модель языкового перевода, которую мы собираемся разработать в этой статье, будет переводить английские предложения на их французские аналоги. Для разработки такой модели нам нужен набор данных, содержащий английские предложения и их французские переводы. К счастью, такой набор данных находится в свободном доступе по ссылке this link . Скачать файл 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 + ' '
    output_sentence_input = ' ' + 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 [] . Подстрока справа от вкладки-это соответствующее переведенное французское предложение. Маркер , который отмечает конец предложения, префиксируется к переведенному предложению, а результирующее предложение добавляется к списку output_sentences [] . Аналогично, токен , который означает "начало предложения", объединяется в начале переведенного предложения, и результат добавляется в список output_sentences_inputs [] . Цикл завершается, если число предложений, добавленных в списки, больше, чем переменная 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. 
 Je suis malade.

You can see the original sentence, т. е. I'm ill ; its corresponding translation in the output, i. е Я болен. <Эосы> . Notice, here we have token at the end of the sentence. Similarly, for the input to the decoder, we have Я болен.

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

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

Для токенизации можно использовать класс 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 возвращает словарь word-to-index, где слова являются ключами, а соответствующие целые числа-значениями. Приведенный выше скрипт также выводит количество уникальных слов в словаре и длину самого длинного предложения во входных данных:

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 , преобразовал их в целые числа, а затем применил предварительное заполнение, добавив три нуля в начале соответствующей целочисленной последовательности для предложения с индексом 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 входного сигнала декодера – je suis malade. . Если вы выведете соответствующие целые числа из словаря word2idx_outputs , то увидите на консоли 2, 3, 6 и 188, как показано здесь:

print(word2idx_outputs[""])
print(word2idx_outputs["je"])
print(word2idx_outputs["suis"])
print(word2idx_outputs["malade."])

Выход:

2
3
6
188

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

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

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

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

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

В этой статье для английских предложений, то есть входных данных, мы будем использовать вложения слов 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

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

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 в словаре перчаток, что подтверждает, что строки в матрице встраивания представляют соответствующие вложения слов из словаря встраивания перчаток. Эта матрица встраивания слов будет использоваться для создания слоя встраивания для нашей модели 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 номеру столбца, который соответствует целочисленному представлению слова. Например, целочисленное представление для 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]

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

Следующий сценарий создает декодер 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)

Выход:

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

Здесь слой lstm_2 является декодером LSTM. input_2 содержит выходные предложения с маркером , добавленным в начале. 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 записях и тестируется на оставшихся 2000 записях. Модель обучается в течение 20 эпох, вы можете изменить количество эпох, чтобы увидеть, сможете ли вы получить лучшие результаты. После 20 эпох я получил точность обучения 90,99% и точность проверки 79,11%, что показывает, что модель переоснащена. Чтобы уменьшить переобучение, вы можете добавить отсев или несколько записей. Мы тренируемся только на 20 000 записях, так что вы можете добавить больше записей, чтобы уменьшить переобучение.

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

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

// Inputs on the left of Encoder/Decoder, outputs on the right.

Step 1:
I'm ill -> Encoder -> enc(h1,c1)

enc(h1,c1) +  -> 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 ->  + dec(h4,c4)

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

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

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

// Inputs on the left of Encoder/Decoder, outputs on the right.

Step 1:

I'm ill -> Encoder -> enc(h1,c1)

enc(h1,c1) +  -> 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() + dec(h4,c4)

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

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

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

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)

Выход:

На изображении выше 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['']
    eos = word2idx_outputs['']
    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 содержит первое слово модели декодера, которое является .

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

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

Наконец, слова в списке 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é.

Блестяще, не правда ли? Наша модель успешно перевела предложение Вы не уволены на французский. Вы также можете проверить это в Google Translate. Давай попробуем еще раз.

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

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

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

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

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

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

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

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