Во втором разделе туториола о моделях sequence-to-sequence с использованием PyTorch и TorchText мы будем реализовывать модель из работы Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation. Эта нейронная сеть позволит достичь лучшей точности при использовании только однослойной RNN как в кодере, так и в декодере. Как и ранее, если визуальный формат поста вас не удовлетворяет, то ниже ссылки на английскую и русскую версию jupyter notebook: Исходная версия (Open jupyter notebook In Colab) Русская версия (Open jupyter notebook In Colab) Напомним общую модель кодера-декодера. Мы используем наш кодировщик (зеленый) поверх исходной последовательности, прошедшей слой эмбеддинга (желтый), чтобы создать вектор контекста (красный). Затем мы передаём этот вектор контекста в декодер (синий) с линейным слоем (фиолетовый) для генерации целевого предложения. В предыдущей модели мы использовали многослойную LSTM сеть в качестве кодера и декодера. Одним из недостатков модели в прошлой части является то, что декодер пытается втиснуть большое количество информации в скрытые состояния. Во время декодирования скрытое состояние должно содержать информацию обо всей исходной последовательности, а также обо всех токенах, которые были декодированы на данный момент. Улучшив сжатие этой информации, мы сможем создать улучшенную модель! Мы будем использовать сеть GRU (Gated Recurrent Unit) вместо LSTM (Long Short-Term Memory). Почему? В основном потому, что так авторы сделали в статье (в этой же статье была представлена GRU сеть), а также потому, что в прошлый раз мы использовали LSTM. Отличия GRU (и LSTM) от стандартных RNN подробно рассмотрены здесь. Резонный вопрос, GRU лучше LSTM? Исследование показало, что они почти одинаковы и одновременно, чем стандартные RNN. Вся подготовка данных будет почти такой же, как и в прошлый раз, поэтому мы очень кратко опишем, что делает каждый блок кода. Более развётнутое описание смотрите в предыдущей части. Мы импортируем PyTorch, TorchText, spaCy и несколько стандартных модулей. Затем установите случайное начальное число для детерминированной воспроизводимости результатов. Для Google Colab используем следующие команды (После загрузки не забывайте перезапустите colab runtime! Наибыстрейший способ через короткую комаду： Ctrl + M + .): Создаём экземпляры наших немецких и английских spaCy моделей. Ранее мы поменяли местами исходное (немецкое) предложение, однако в статье, которую мы реализуем, они этого не делают, и мы не будем. Далее мы создаем токенизирующие функции. Они могут быть переданы в torchtext и будут принимать предложение в виде строки, а возвращать предложение в виде списка токенов. Загрузка наших данных. Мы распечатаем пример, чтобы проверить, не перевернут ли он. Затем создаём наш словарь, преобразовав все токены, встречающиеся менее двух раз, в <unk> токены. Наконец, определим device и создаём наши итераторы. Кодер аналогичен предыдущему, но многослойный LSTM заменен на однослойный GRU. Кроме того, мы не передаем дропаут в качестве аргумента GRU, поскольку этот дропаут используется между слоями многослойной RNN. Поскольку у нас есть только один слой, PyTorch отобразит предупреждение, если мы попытаемся передать ему значение дропаута. Еще одна вещь, которую следует отметить в отношении GRU, заключается в том, что он требует и возвращает только скрытое состояние, не нуждаясь в состоянии ячейки, как в LSTM. Из приведенных выше уравнений видно, что RNN и GRU идентичны. Однако внутри GRU есть несколько запорных механизмовs, которые контролируют поток информации в скрытое состояние и из него (похожий на LSTM). Опять же, для получения дополнительной информации обращайтесь сюда. Он идентичен кодировщику общей модели seq2seq, со всей "магией", происходящей внутри GRU (зеленый). Реализация данного декодер значительно отличается от декодера предыдущей модели, и в текущем декодере мы усилили сжатие некоторой информации. GRU в декодере принимает не только целевой токен из эмбеддинга  и предыдущее скрытое состояние в качестве входных данных, но также и вектор контекста . Обратите внимание, как этот вектор контекста не имеет индекса , это означает, что мы повторно используем один и тот же вектор контекста, возвращаемый кодировщиком, для каждого временного шага в декодере. Раньше мы предсказывали следующий токен с линейным слоем , используя только выход декодер верхнего уровня, скрытый на этом временном шаге как . Теперь мы также передаем текущий токен эмбеддинга и вектор контекста в линейный слой. Таким образом, наш декодер теперь выглядит примерно так: Обратите внимание, начальное скрытое состояние  по-прежнему является вектором контекста , поэтому при генерации первого токена мы фактически вводим два идентичных вектора контекста в GRU. Как эти два изменения уменьшают сжатие информации? Гипотетически скрытым состояниям декодер  больше нет необходимости содержать информацию об исходной последовательности, поскольку она всегда доступна в качестве входных данных. Таким образом, они должены содержать только информацию о том, какие токены они уже сгенерировали. Передача в линейный уровень (через эмбеддинг ) означает, что этот уровень может напрямую видеть входной токен, без необходимости получать информацию о нём из скрытого состояния. Однако эта гипотеза — всего лишь гипотеза, невозможно определить, как модель на самом деле использует предоставленную ей информацию (не слушайте никого, кто говорит иначе). Тем не менее это хорошая догадка, и результаты, кажется, указывают на то, что эта модификации является хорошей идеей! В рамках реализации мы передадим и в GRU объединив их вместе, так что входные размеры в GRU были emb_dim + hid_dim (поскольку вектор контекста будет иметь размер hid_dim). Линейный слой принимает , и объединения их вместе, поэтому входные размеры теперь emb_dim + hid_dim*2. Мы также не передаем значение дропаута в GRU, поскольку оно используется только на входном уровене. Соединяя кодировщик и декодер, получаем: Снова, в этой реализации нам нужно обеспечить одинаковые скрытые размеры в кодировщике и декодере. Кратко пройдемся по всем этапам: тензор outputs создан для хранения всех прогнозов  исходная последовательность подается в кодировщик для получения вектора контекста context начальное скрытое состояние декодера установлено как вектор context  мы используем <sos> в качестве входных токенов input  затем декодируем в цикле: передача входного токена , предыдущего скрытого состояния , и вектора контекста в декодер получение прогноза и нового скрытого состояния  Затем мы решаем, собираемся ли мы использовать обучение с принуждением или нет, устанавливая следующий вход соответствующим образом (либо следующий истинный токен в целевой последовательности, либо самый вероятный следующий токен) Остальная часть этого раздела очень похожа на аналогичную часть из предыдущей части. Мы инициализируем наш кодер, декодер и модель seq2seq (поместив его на графический процессор, если он у нас есть). Как и раньше, размеры эмбеддинга и величина дропаута могут быть разными для кодера и декодера, но скрытые размеры должны оставаться такими же. Затем мы инициализируем наши параметры. В исходной статье говорится, что параметры инициализируются из нормального распределения со средним значением 0 и стандартным отклонением0 .01, т.е. . В ней также говорится, что мы должны инициализировать повторяющиеся параметры специальным образом, однако для простоты мы инициализируем их в виде . Распечатываем количество параметров. Несмотря на то, что у нас есть только однослойная RNN для нашего кодера и декодера, на самом деле у нас есть больше параметры, чем в предыдущей модели. Это связано с увеличенным размером входов в GRU и линейный слой. Однако это незначительное увеличение параметров не приводит увеличению времени обучения (~3 секунд на дополнительную эпоху). Мы инициализируем наш оптимизатор. Мы также инициализируем функцию потерь, игнорируя потерю на токенах <pad>. Затем мы создаем цикл обучения ... ...и цикл оценки, не забывая установить модель в режим eval и выключить обучение с принуждением. Мы также определим функцию, которая вычисляет, сколько времени занимает эпоха. Затем мы обучаем нашу модель, сохраняя параметры, которые дают нам наименьшие потери при проверке. Наконец, мы тестируем модель на тестовой выборке, используя эти «лучшие» параметры. Если посмотреть на выигрыш в тесте, то видно улучшение производительность по сравнению с предыдущей моделью. Это довольно хороший признак того, что эта архитектура модели что-то делает лучше! Ослабление сжатия информации кажется неплохим подходом, и в следующем разделе мы пойдём по этому пути еще дальше с помощью внимания.