Привет, Хабр!

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

В этой статье в общих деталях рассмотрим то, как реализуются Seq2Seq модели.

Основные компоненты

Seq2Seq модели состоят из двух основных частей: энкодера и декодера.

Энкодер

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

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

Основу энкодера RNN, обычно реализованные с использованием LSTM или GRU. Эти сети обрабатывают входную последовательность пошагово:

  1. На каждом шаге RNN принимает эмбеддинговое представление текущего токена и скрытое состояние от предыдущего шага.

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

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

Для улучшения качества представления входной последовательности часто используются двунаправленные RNN. В этом случае два RNN работают параллельно: один — слева направо, другой — справа налево. Их состояния объединяются на каждом шаге, что позволяет учитывать как предшествующие, так и последующие слова для каждого токена в последовательности.

Пример реализации энкодера на Keras с LSTM:

from keras.layers import Input, LSTM, Embedding
from keras.models import Model

# размерность эмбеддингов и скрытых состояний
embed_size = 256
latent_dim = 512

# входной слой
encoder_inputs = Input(shape=(None,))
# эмбеддинговый слой
encoder_embedding = Embedding(input_dim=vocab_size, output_dim=embed_size)(encoder_inputs)
# LSTM слой
encoder_lstm = LSTM(latent_dim, return_state=True)
encoder_outputs, state_h, state_c = encoder_lstm(encoder_embedding)
# финальные состояния
encoder_states = [state_h, state_c]

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

Декодер

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

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

Декодер также реализован с использованием RNN, как LSTM или GRU. На каждом шаге декодер принимает:

  1. Контекстный вектор от энкодера.

  2. Предыдущий предсказанный токен (или начальный токен для первого шага).

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

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

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

Пример реализации декодера на Keras с LSTM и механизмом внимания:

from keras.layers import Input, LSTM, Dense, Embedding
from keras.models import Model

# размерность эмбеддингов и скрытых состояний
embed_size = 256
latent_dim = 512

# входные слои для декодера
decoder_inputs = Input(shape=(None,))
decoder_embedding = Embedding(input_dim=vocab_size, output_dim=embed_size)(decoder_inputs)
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_embedding, initial_state=encoder_states)
# механизм внимания
attention = Dense(1, activation='tanh')(decoder_outputs)
attention = Flatten()(attention)
attention = Activation('softmax')(attention)
attention = RepeatVector(latent_dim)(attention)
attention = Permute([2, 1])(attention)
decoder_combined_context = Concatenate(axis=-1)([decoder_outputs, attention])

# выходной слой
decoder_dense = Dense(vocab_size, activation='softmax')
decoder_outputs = decoder_dense(decoder_combined_context)

# модель декодера
decoder_model = Model([decoder_inputs] + encoder_states, decoder_outputs)

Декодер принимает начальные состояния от энкодера и генерирует выходную последовательность.

Реализация трех Seq2Seq моделей

Машинный перевод

Машинный перевод — это одна из наиболее базовых задач для Seq2Seq моделей. Реализуем Seq2Seq модельку для перевода с английского на французский язык с использованием Keras.

Для этой задачи будем использовать датасеты французских и английских предложений (они есть на kaggle):

import numpy as np
import pandas as pd
from keras.models import Model
from keras.layers import Input, LSTM, Dense, Embedding
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences

# чтение данных
data_path = 'fra.txt'
lines = open(data_path, encoding='utf-8').read().strip().split('\n')
eng_texts = []
fra_texts = []
for line in lines[: min(10000, len(lines) - 1)]:
    eng_text, fra_text = line.split('\t')
    eng_texts.append(eng_text)
    fra_texts.append(fra_text)

# токенизация
eng_tokenizer = Tokenizer()
eng_tokenizer.fit_on_texts(eng_texts)
eng_sequences = eng_tokenizer.texts_to_sequences(eng_texts)
eng_word_index = eng_tokenizer.word_index

fra_tokenizer = Tokenizer()
fra_tokenizer.fit_on_texts(fra_texts)
fra_sequences = fra_tokenizer.texts_to_sequences(fra_texts)
fra_word_index = fra_tokenizer.word_index

# паддинг последовательностей
max_len_eng = max([len(txt) for txt in eng_sequences])
max_len_fra = max([len(txt) for txt in fra_sequences])

encoder_input_data = pad_sequences(eng_sequences, maxlen=max_len_eng, padding='post')
decoder_input_data = pad_sequences(fra_sequences, maxlen=max_len_fra, padding='post')

# смещение на один токен вправо для decoder_target_data
decoder_target_data = np.zeros((len(fra_texts), max_len_fra, len(fra_word_index) + 1), dtype='float32')
for i, seqs in enumerate(fra_sequences):
    for t, token in enumerate(seqs):
        if t > 0:
            decoder_target_data[i, t - 1, token] = 1.0

# гиперпараметры
latent_dim = 256
num_encoder_tokens = len(eng_word_index) + 1
num_decoder_tokens = len(fra_word_index) + 1

# энкодер
encoder_inputs = Input(shape=(None,))
encoder_embedding = Embedding(input_dim=num_encoder_tokens, output_dim=latent_dim)(encoder_inputs)
encoder_lstm, state_h, state_c = LSTM(latent_dim, return_state=True)(encoder_embedding)
encoder_states = [state_h, state_c]

# декодер
decoder_inputs = Input(shape=(None,))
decoder_embedding = Embedding(input_dim=num_decoder_tokens, output_dim=latent_dim)(decoder_inputs)
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_embedding, initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)

# модель Seq2Seq
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
model.fit([encoder_input_data, decoder_input_data], decoder_target_data, batch_size=64, epochs=30, validation_split=0.2)

Текстовое суммирование

Следующий пример - текстовое суммирование. Это задача генерации краткого представления текста. Реализуем Seq2Seq модель с использованием механизма внимания.

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

import numpy as np
import pandas as pd
from keras.models import Model
from keras.layers import Input, LSTM, Dense, Embedding, Concatenate, Attention
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences

# Чтение данных
data_path = 'news_summary.csv'
df = pd.read_csv(data_path)
articles = df['text'].values
summaries = df['summary'].values

# токенизация
article_tokenizer = Tokenizer()
article_tokenizer.fit_on_texts(articles)
article_sequences = article_tokenizer.texts_to_sequences(articles)
article_word_index = article_tokenizer.word_index

summary_tokenizer = Tokenizer()
summary_tokenizer.fit_on_texts(summaries)
summary_sequences = summary_tokenizer.texts_to_sequences(summaries)
summary_word_index = summary_tokenizer.word_index

# паддинг последовательностей
max_len_article = 300
max_len_summary = 50

encoder_input_data = pad_sequences(article_sequences, maxlen=max_len_article, padding='post')
decoder_input_data = pad_sequences(summary_sequences, maxlen=max_len_summary, padding='post')

# смещение на один токен вправо для decoder_target_data
decoder_target_data = np.zeros((len(summaries), max_len_summary, len(summary_word_index) + 1), dtype='float32')
for i, seqs in enumerate(summary_sequences):
    for t, token in enumerate(seqs):
        if t > 0:
            decoder_target_data[i, t - 1, token] = 1.0

# гиперпараметры
latent_dim = 256
num_encoder_tokens = len(article_word_index) + 1
num_decoder_tokens = len(summary_word_index) + 1

# энкодер
encoder_inputs = Input(shape=(None,))
encoder_embedding = Embedding(input_dim=num_encoder_tokens, output_dim=latent_dim)(encoder_inputs)
encoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
encoder_outputs, state_h, state_c = encoder_lstm(encoder_embedding)
encoder_states = [state_h, state_c]

# декодер
decoder_inputs = Input(shape=(None,))
decoder_embedding = Embedding(input_dim=num_decoder_tokens, output_dim=latent_dim)(decoder_inputs)
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_embedding, initial_state=encoder_states)

# механизм внимания
attention = Attention()([decoder_outputs, encoder_outputs])
decoder_combined_context = Concatenate(axis=-1)([decoder_outputs, attention])

# выходной слой
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_combined_context)

# модель Seq2Seq
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
model.fit([encoder_input_data, decoder_input_data], decoder_target_data, batch_size=64, epochs=30, validation_split=0.2)

Генерация описаний к изображениям

Реализуем генерацию описаний к изображениям — это задача, где Seq2Seq модели используются для генерации текста, описывающего содержание изображения. Будем использовать предобученную модель InceptionV3 для экстракции признаков изображения и Seq2Seq модельку для генерации текста:

import numpy as np
import pandas as pd
from keras.models import Model
from keras.layers import Input, LSTM, Dense, Embedding, Concatenate
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.applications.inception_v3 import InceptionV3
from keras.applications.inception_v3 import preprocess_input
from keras.preprocessing import image
from keras.models import Model

# загрузка предобученной модели InceptionV3
base_model = InceptionV3(weights='imagenet')
model = Model(base_model.input, base_model.layers[-2].output)

def extract_features(img_path, model):
    img = image.load_img(img_path, target_size=(299, 299))
    x = image.img_to_array(img)
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)
    features = model.predict(x)
    return features

# чтение данных
captions_data_path = 'captions.txt'
images_path = 'images/'

captions = open(captions_data_path, 'r').read().strip().split('\n')
image_ids = []
captions_list = []
for line in captions:
    img_id, caption = line.split('\t')
    image_ids.append(img_id)
    captions_list.append(caption)

# токенизация
caption_tokenizer = Tokenizer()
caption_tokenizer.fit_on_texts(captions_list)
caption_sequences = caption_tokenizer.texts_to_sequences(captions_list)
caption_word_index = caption_tokenizer.word_index

# паддинг последовательностей
max_len_caption = max([len(txt) for txt in caption_sequences])

decoder_input_data = pad_sequences(caption_sequences, maxlen=max_len_caption, padding='post')

# смещение на один токен вправо для decoder_target_data
decoder_target_data = np.zeros((len(captions_list), max_len_caption, len(caption_word_index) + 1), dtype='float32')
for i, seqs in enumerate(caption_sequences):
    for t, token in enumerate(seqs):
        if t > 0:
            decoder_target_data[i, t - 1, token] = 1.0

# экстракция признаков изображений
image_features = np.zeros((len(image_ids), 2048))
for i, img_id in enumerate(image_ids):
    img_path = images_path + img_id
    image_features[i] = extract_features(img_path, model)

# гиперпараметры
latent_dim = 256
num_decoder_tokens = len(caption_word_index) + 1

# вход для признаков изображения
image_input = Input(shape=(2048,))

# вход для декодера
decoder_inputs = Input(shape=(None,))
decoder_embedding = Embedding(input_dim=num_decoder_tokens, output_dim=latent_dim)(decoder_inputs)
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_embedding, initial_state=[image_input, image_input])
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)

# модель Seq2Seq
model = Model([image_input, decoder_inputs], decoder_outputs)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
model.fit([image_features, decoder_input_data], decoder_target_data, batch_size=64, epochs=30, validation_split=0.2)

Заключение

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

В завершение хочу порекомендовать бесплатные вебинары курса ML Advanced:

Комментарии (0)