Seq2seq и машинный перевод: энкодер и декодер

Чтобы переводить и отвечать, нужна архитектура, превращающая одну последовательность в другую — это seq2seq из двух RNN.

Seq2seq (sequence-to-sequence) — архитектура из двух частей: энкодер сжимает входную последовательность в вектор смысла, декодер разворачивает его в выходную последовательность.

Задача: вход и выход разной длины

Перевод «я люблю котов» → «I love cats» устроен сложнее классификации: и вход, и выход — последовательности, причём разной длины и порядка. Классификатор выдаёт одну метку, а тут нужно породить целую фразу. Для этого придумали seq2seq.

Две части: энкодер и декодер

  • Энкодер — RNN/LSTM, которая читает входное предложение слово за словом и сжимает его смысл в один вектор (context vector, «вектор мысли»).
  • Декодер — другая RNN/LSTM, которая получает этот вектор и порождает выход слово за словом, опираясь на уже сгенерированное.
Вход:  [я] [люблю] [котов]
          ↓    ↓      ↓
       ЭНКОДЕР  →  context vector (сжатый смысл)
                       ↓
       ДЕКОДЕР  →  [I] [love] [cats] [конец]

Как декодер порождает слова

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

# Игрушечный декодер: по «вектору смысла» достаём перевод из словарика
context = "я люблю котов"   # как будто энкодер сжал это
phrasebook = {"я люблю котов": ["I", "love", "cats"]}

def decode(context):
    output = []
    words = phrasebook.get(context, ["?"])
    for w in words:
        output.append(w)          # на каждом шаге — одно слово
        print("шаг %d -> сгенерировали:" % len(output), w)
    output.append("<конец>")
    return output

result = decode(context)
print("Перевод:", " ".join(result[:-1]))

Вывод:

шаг 1 -> сгенерировали: I
шаг 2 -> сгенерировали: love
шаг 3 -> сгенерировали: cats
Перевод: I love cats

Конечно, настоящий декодер не достаёт ответ из словаря, а вычисляет распределение вероятностей над всем словарём на каждом шаге и выбирает слово. Но структура та же: пошаговая авторегрессивная генерация на основе сжатого смысла.

Узкое место: один вектор на всё

У классического seq2seq есть критическая слабость. Весь смысл входного предложения, каким бы длинным оно ни было, нужно впихнуть в один context-вектор фиксированного размера. Для короткой фразы нормально, но для длинного предложения это бутылочное горлышко: вектор не вмещает все детали, и качество перевода падает с ростом длины.

Образно: энкодер должен пересказать целую главу одним предложением, а декодер — восстановить главу из этого пересказа. Часть информации неизбежно теряется. Именно эту проблему решит механизм внимания в следующем уроке — он позволит декодеру «подсматривать» во все слова входа, а не только в один сжатый вектор.

Где применяется seq2seq

  • Машинный перевод — исходная и главная задача.
  • Суммаризация — длинный текст → краткое изложение.
  • Вопросно-ответные системы и чат-боты — вопрос → ответ.
  • Исправление и перефразирование текста.

Итог

  • Seq2seq превращает одну последовательность в другую (вход и выход разной длины).
  • Энкодер сжимает вход в context-вектор, декодер разворачивает его в выход.
  • Декодер генерирует слова по очереди (авторегрессивно) до токена «конец».
  • Слабость — один вектор на всё предложение: горлышко на длинных текстах.
  • Эту проблему решает механизм внимания.
Проверьте себя
1. Что делает энкодер в архитектуре seq2seq?
AПорождает выходную последовательность слово за словом
BСжимает входную последовательность в вектор смысла (context vector)
CСчитает метрики precision и recall
DУдаляет стоп-слова из входа
2. Что означает «авторегрессивная» генерация в декодере?
AДекодер выдаёт всё предложение за один шаг
BДекодер на каждом шаге использует ранее сгенерированные слова для предсказания следующего
CДекодер не использует context-вектор
DДекодер работает только с числами
3. В чём узкое место классического seq2seq на длинных предложениях?
AДекодер работает слишком быстро
BВесь смысл входа надо уместить в один context-вектор фиксированного размера
CЭнкодер не умеет читать текст
DВыход всегда длиннее входа
Поддержать проект