В этой части мы добавим несколько улучшений — упакованные дополненные последовательности и маскировка — к модели из предыдущего раздела. Упакованные дополненные последовательности используются, чтобы сообщить нашей RNN, что нужно пропускать маркеры заполнения в нашем кодировщике. Маскировка явно заставляет модель игнорировать определенные значения, такие как внимание к элементам с заполнением. Оба эти метода обычно используются в обработке естественного языка (NLP). Кроме того, мы рассмотрим как использовать нашу модель для вывода целевого предложения, давая ей входное предложение, видя результат её перевода, и выясняя, на что именно она обращает внимание при переводе каждого слова. Наконец, мы будем использовать метрику BLEU для измерения качества наших переводов. Как и ранее, если визуальный формат поста вас не удовлетворяет, то ниже ссылки на английскую и русскую версию jupyter notebook: Исходная версия Open jupyter notebook In Colab Русская версия Open jupyter notebook In Colab Замечание: русская версия jupyter notebook отличается от исходной добавленным в конце тестом на инверсию предложения. Очень простая для человека задача, но, порой, непреодолима для искусственных систем. Сначала мы импортируем все модули, как и раньше, с добавлением модулей matplotlib, используемых для просмотра вектора внимания. Затем мы установим случайное начальное число для воспроизводимости. Загрузите немецкую и английскую модели spaCy. Для загрузки в Google Colab используем следующие команды (После загрузки обязательно перезапустите colab runtime! Наибыстрейший способ через короткую комаду： Ctrl + M + .): Как и раньше, мы импортируем spaCy и определим токенизаторы для немецкого и английского языков. При использовании упакованных дополненных последовательностей нам нужно сообщить PyTorch, какова длина фактических (не дополненных) последовательностей. К счастью для нас, объекты TorchText Field позволяют нам использовать аргумент include_lengths, это приведет к тому, что наш batch.src будет кортежем. Первый элемент кортежа такой же, как и раньше, пакет числовых исходных предложений в качестве тензора, а второй элемент — это длины без дополнений каждого исходного предложения в пакете. Затем мы загружаем данные. И заполняем словарь. Далее мы подготавливаем итераторы. Одна из особенностей упакованных последовательностей с дополнениями заключается в том, что все элементы (предложения) в пакете должны быть отсортированы по их длине без дополнений в порядке убывания, т.е. первое предложение в пакете должно быть самым длинным. Мы используем два аргумента итератора для обработки этого: sort_within_batch, который сообщает итератору, что содержимое пакета необходимо отсортировать, и sort_key - функцию, которая сообщает итератору, как сортировать элементы в пакете. Здесь мы сортируем по длине предложения src. Далее мы определяем кодировщик. Все изменения здесь происходят в методе forward. Теперь он принимает длину исходных предложений и сами предложения. После того, как исходное предложение (автоматически дополненное итератором) было пропущено через эмбеддинг, мы можем использовать для него pack_padded_sequence с длинами предложений. Обратите внимание, что тензор, содержащий длины последовательностей, должен быть тензором CPU в последней версии PyTorch, что мы явно делаем с помощью to ('cpu'). Packaged_embedded будет нашей упакованной последовательностью. Затем передаём в нашу RNN как обычно, и она возвращает packed_outputs, упакованный тензор, содержащий все скрытые состояния из последовательности, и hidden - это просто последнее скрытое состояние из нашей последовательности. hidden стандартный тензор и никак не упакованный, единственная разница в том, что на входе была упакованная последовательность, этот тензор из последнего элемента без дополнений в последовательности. Затем мы распаковываем наши packed_outputs с использованием pad_packed_sequence который возвращает outputs и длины каждого, которые нам не нужны. Первое измерение outputs - это длины дополненных последовательностей.  Однако из-за использования упакованной дополненной последовательности значения тензоров будут нулевыми, в случае когда на входе был маркер заполнения. Модуль внимания — это та часть, в которой мы вычисляем значения внимания по исходному предложению. Ранее мы позволяли этому модулю «обращать внимание» на токены заполнения в исходном предложении. Однако, используя маскировку, мы можем заставить внимание концентрироваться только на элементах, не являющиеся заполнением. Метод forward теперь принимает на входе mask. Это тензор , который равен 1, если токен исходного предложения не является токеном заполнения, и 0, когда это маркер заполнения. Например, если исходное предложение ["hello", "how", "are", "you", "?", <pad>, <pad>], то маска будет [1, 1, 1, 1, 1, 0, 0]. Мы применяем маску после того, как внимание было рассчитано, но до того, как оно будет нормализовано функцией softmax. Это реализовано с использованием masked_fill. Тензор заполняется для каждого элемента, для которого выполняется условие для первого аргумента mask == 0, значением, заданным вторым аргументом  -1e10. Другими словами, он примет ненормализованные значения внимания и изменит их для элементов заполнения на-1e10. Поскольку эти числа будут мизерными по сравнению с другими значениями, они станут равными нулю при прохождении через слой softmax, что гарантирует отсутствие внимания к маркерам заполнения в исходном предложении. В декодере нужно внести лишь несколько небольших изменений. Ему необходимо принять маску исходного предложения и передать ее модулю внимания. Поскольку мы хотим просмотреть значения внимания во время вывода, мы возвращаем и тензор внимания. Общая модель seq2seq нуждается в некоторых изменениях для упакованных дополненных последовательностей, маскирования и логического вывода. Нам нужно сообщить модели, каковы индексы для токена заполнения, а также передать длину исходного предложения в качестве входных данных в метод forward. Мы используем индекс токена заполнения для создания масок, создавая тензор маски, равный 1, везде, где исходное предложение не равно токену заполнения. Все это делается с помощью функции create_mask. Длины последовательностей, которые необходимо передать кодеру для использования упакованных дополненных последовательностей. Внимание на каждом временном шаге сохраняется в attentions Затем инициализируем модель и размещаем ее на GPU. Затем мы инициализируем параметры модели. Мы распечатаем количество обучаемых параметров в модели, заметив, что она имеет точно такое же количество параметров, что и модель без улучшений. Затем мы определяем наш оптимизатор и критерий оптимизации. ignore_index для критерия должен быть индексом токена заполнения для целевого языка, но не для исходного. Далее мы определим наши циклы обучения и оценки. Поскольку мы используем include_lengths = True для нашего исходного поля, batch.src теперь является кортежем, первый элемент которого представляет собой числовой тензор, представляющий предложение, а второй элемент — длины каждого предложения в пакете. Наша модель возвращает, в том числе, векторы внимания для батча исходных предложений на каждом временном шаге декодирования. Мы не будем использовать их во время обучения и оценки точности обучения, но мы будем использовать их позже для вывода. Для отображения прогресса в Google Colab устанавливаем tensorboard Затем мы определим функцию для определения продолжительности эпох. Запускаем tensorboard. Отображение данных начнётся после запуска обучения. Обновить данные можно кликнув по иконке справа вверху Предпоследний шаг — обучить нашу модель. Обратите внимание, что это занимает только почти половину времени по сравнению с нашей моделью без улучшений, добавленных в этот раздел. Наконец, мы загружаем параметры из нашей лучшей модели и проводим проверку на тестовом наборе. Мы получаем модель с лучшими результатами, но при этом почти вдвое быстрее! Теперь мы можем использовать нашу обученную модель для создания переводов. Примечание: эти переводы будут хуже, чем показанные в статье, поскольку авторы в статье использовали скрытые размеры 1000 и тренировали модеь в течение 4 дней! Они были подобраны для того, чтобы продемонстрировать, как должно выглядеть внимание на модели достаточно большого размера. Наш translate_sentence будет делать следующее: убедится, что наша модель находится в режиме оценки, который всегда должен быть включён для вывода токенизируем исходное предложение, если оно не было токенизировано (является строкой) "оцифрует" исходное предложение преобразует его в тензор и добавить размер батча получит длину исходного предложения и преобразует его в тензор передаст исходное предложение в кодировщик создаст маску для исходного предложения создаст список для хранения выходного предложения, инициализированного токеном <sos> создаст тензор для удержания значений внимания пока мы не достигли максимальной длины получит входной тензор, который должен быть либо <sos>, либо последним предсказанным токеном передаст входной тензор, все выходы кодировщика, скрытое состояние и маску в декодер сохранит значения внимания получит предсказанный следующий токен добавит предсказание к предсказанию текущего выходного предложения прервёт выполнение, если предсказание было токеном <eos> преобразует выходное предложение из индексов в токены вернёт выходное предложение (с удаленным токеном <sos>) и значения внимания для последовательности Затем мы создадим функцию, которая отображает внимание модели к исходному предложению для каждого сгенерированного целевого токена. Теперь мы возьмем несколько переводов из нашего набора данных и посмотрим, насколько хорошо работает наша модель. Обратите внимание: здесь мы собираемся выбрать некоторые рафинированные примеры, чтобы было что-то интересное, но не стесняйтесь изменять значение example_idx, чтобы посмотреть на другие примеры. Сначала мы получим исходное и целевое предложения из нашего набора данных. Затем мы воспользуемся функцией translate_sentence, чтобы получить предсказанный перевод и внимание. Мы показываем это графически, располагая исходное предложение на оси x и прогнозируемый перевод на оси y. Чем светлее квадрат на пересечении двух слов, тем больше внимания модель уделяла этому исходному слову при переводе этого целевого слова. Ниже приведен пример, который модель пыталась перевести, она получает правильный перевод, за исключением некоторых слов. Переводы из обучающей выборки модель могла просто запомнить. Так что справедливо, что мы также смотрим на переводы из набора для проверки и тестирования. Раньше мы заботились только о потерях/достоверности модели. Однако есть показатели, специально разработанные для измерения качества перевода. Наиболее популярным из них является BLEU. Не вдаваясь в подробности, BLEU рассматривает совпадение предсказанных и фактических целевых последовательностей с точки зрения их n-грамм. Он даст нам число от 0 до 1 для каждой последовательности, где 1 означает полное совпадение, то есть идеальный перевод (хотя обычно отображают в процентах от 0 до 100). BLEU был разработан для работы с несколькими кандидатами переводов для каждой исходной последовательности, однако в нашем наборе данных у есть только один кандидат. Мы определяем функцию calculate_bleu, которая вычисляет оценку BLEU по предоставленному набору данных TorchText. Эта функция создает корпус фактического и прогнозируемого перевода для каждого исходного предложения, а затем вычисляет оценку BLEU. Мы получаем BLEU около 28. Если мы сравним этот результат со статьёй, из которой пытаемся воспроизвести модель с внимания, то там авторы достигают 26,75 балла по шкале BLEU. Это похоже на нашу оценку, однако они используют совершенно другой набор данных, а размер их модели намного больше - 1000 скрытых состояний, обучение которых занимает 4 дня! - так что мы не можем сравнивать результаты с ними. Это число на самом деле не поддается интерпретации, мы не можем сказать о нем много. Самая полезная часть оценки BLEU заключается в том, что её можно использовать для сравнения разных моделей в одном наборе данных, где модель с более высокой оценкой BLEU «лучше». В конце приведу один из моих любимых тестов: тест на инверсию предложения. Очень простая для человека задача (ученики начальной школы обучаются за 10-15 примеров), но, порой, непреодолима для искусственных систем. Для Google Colab скачаем обучающие последовательности В начале обучим сеть инверсии и посмотрим на результат. В следующих разделах мы отойдем от использования рекуррентных нейронных сетей и начнем искать другие способы построения моделей sequence-to-sequence. В частности, в следующей части мы будем использовать свёрточные нейронные сети.