Encoder-Decoder архитектура 🔄
Представь, что тебе нужно перевести русское предложение «Кошка сидит на окне» на английский — «The cat sits on the window». Четыре слова превратились в шесть. А «Спасибо» — это одно слово — переводится как «Thank you», два слова. «Я пошёл домой» — три слова — становится «I went home», тоже три, но чисто случайно. Ни для одной пары языков длина входного предложения не определяет длину перевода: она может быть больше, меньше, а иногда меняется непредсказуемо в зависимости от грамматики, устойчивых выражений и просто стиля.
Все архитектуры, которые ты видел в этом курсе раньше, устроены иначе. Полносвязная сеть и свёрточная сеть (уроки 326–338) принимают вход фиксированного размера и выдают выход фиксированного размера. Даже обычная рекуррентная сеть из урока 339, обученная на анализ отзывов, читает последовательность произвольной длины, но выдаёт всего один финальный ответ — «позитивный» или «негативный». Ни одна из этих схем не решает задачу «на входе последовательность произвольной длины, на выходе — тоже последовательность, но другой, заранее неизвестной длины». А ведь именно так устроен машинный перевод, суммаризация длинного текста в короткое резюме, генерация подписи к изображению или ответ диалогового бота: входа и выхода не просто разные по длине — их длины даже не связаны никакой простой формулой, которую можно было бы вычислить заранее.
Такой класс задач называют sequence-to-sequence (последовательность-в-последовательность), сокращённо seq2seq: на входе последовательность, на выходе последовательность, длины независимы. В 2014 году для этого класса задач было предложено архитектурное решение, которое на несколько лет стало индустриальным стандартом и, что важнее для тебя как для будущего специалиста, напрямую подготовило почву для механизма внимания и трансформеров — тем следующих двух уроков. Идея на удивление простая: раз нельзя напрямую связать вход и выход разной длины, нужно разбить задачу на два шага — сначала «прочитать и понять» весь вход целиком, сжав его смысл в некоторое внутреннее представление, а затем «написать» выход с нуля, опираясь на это представление. Первый шаг выполняет кодировщик (encoder), второй — декодировщик (decoder), а всю связку называют архитектурой encoder-decoder (архитектура кодировщик-декодировщик) — этот русский перевод мы и будем использовать дальше по тексту.
Сегодняшний урок разбирает четыре вещи по порядку: саму архитектуру кодировщик-декодировщик и то, как из неё рождается единственный вектор, несущий весь смысл входа; проблему бутылочного горлышка этого вектора — фундаментальное ограничение архитектуры, которое проявляется именно на длинных последовательностях; технику teacher forcing, без которой обучить такую модель на практике было бы значительно труднее; и, наконец, ту самую логическую цепочку, которая привела исследователей от осознания проблемы бутылочного горлышка к изобретению механизма внимания — главной темы следующего урока.
История
В сентябре 2014 года три исследователя из Google — Илья Суцкевер, Ориол Виньялс и Куок Ле — опубликовали статью «Sequence to Sequence Learning with Neural Networks» («Обучение последовательность-в-последовательность с помощью нейронных сетей»). В ней они впервые системно показали, что одна сквозная (end-to-end) нейросеть, состоящая из двух LSTM — кодирующей и декодирующей, — способна обучаться переводить с одного языка на другой напрямую, без единой строчки вручную написанных лингвистических правил. Почти одновременно похожую по духу идею — рекуррентный кодировщик-декодировщик для статистического машинного перевода — предложил Кёнхюн Чо (Kyunghyun Cho) с соавторами; забавное совпадение в том, что тот же Чо годом позже станет соавтором архитектуры GRU, которую ты уже встречал в уроке 340.
До этих работ машинный перевод строился на статистических методах: системы собирали статистику соответствий фраз между языками по огромным параллельным корпусам, вручную настраивали десятки признаков и компонентов — модель языка, модель перевода фраз, модель перестановки слов — и склеивали всё это в сложный, тяжело поддерживаемый конвейер. Идея «обучить единственную нейросеть переводить целиком, от сырого текста до сырого текста, минимизируя один-единственный показатель ошибки» казалась в тот момент по-хорошему дерзкой. Суцкевер и коллеги показали на задаче англо-французского перевода (соревнование WMT’14), что подход на основе encoder-decoder способен конкурировать с лучшими на тот момент статистическими системами — и это стало заметным сигналом для всей индустрии.
Google быстро подхватила идею: уже к 2016 году компания представила Google Neural Machine Translation (GNMT) — полностью нейросетевую систему перевода, заменившую прежний статистический конвейер в продакшене Google Переводчика. Но интересная деталь обнаружилась ещё в оригинальной статье 2014 года: авторы заметили, что если перевернуть порядок слов во входном предложении перед подачей в кодировщик, качество перевода заметно улучшается. Приём работал, но выглядел как заплатка поверх более глубокой проблемы — и именно эта проблема, проблема бутылочного горлышка вектора контекста, оказалась главным ограничением архитектуры, разобранным дальше в этом уроке.
Задача sequence-to-sequence и архитектура кодировщик-декодировщик
Интуиция
Представь двух людей, которые вместе переводят книгу, но работают по очереди и не видят друг друга напрямую. Первый — назовём его Читателем — прочитывает всё исходное предложение целиком, от первого до последнего слова, и в конце формулирует его смысл одной компактной запиской — скажем, коротким кодом на листке бумаги. Второй — Писатель — этой записки никогда не видел исходного предложения, он получает только листок Читателя и, опираясь исключительно на него, пишет перевод слово за словом, каждый раз оглядываясь на то, что сам уже написал до этого. Читатель — это кодировщик, листок с запиской — вектор контекста, а Писатель — декодировщик. Ключевое свойство этой схемы: Читателю совершенно не важно, сколько слов было во входном предложении — три или тридцать, — он всегда сдаёт Писателю листок одного и того же размера. А Писателю не важно, сколько слов было у Читателя — он пишет ровно столько слов перевода, сколько нужно, пока не решит, что закончил.
Формула
Архитектура кодировщик-декодировщик. Кодировщик — рекуррентная сеть (RNN или, на практике почти всегда, LSTM/GRU из урока 340), которая читает входную последовательность $x_1, x_2, \dots, x_T$ и на каждом шаге обновляет скрытое состояние:
$$h_t = f_{enc}(x_t, h_{t-1}), \qquad t = 1, \dots, T$$Финальное скрытое состояние $h_T$ (для LSTM — пара скрытого состояния и состояния ячейки $(h_T, C_T)$) становится вектором контекста $c$ — единственным каналом, по которому информация о входе передаётся от кодировщика к декодировщику:
$$c = h_T$$Декодировщик — отдельная рекуррентная сеть со своими весами, инициализированная вектором контекста как начальным состоянием $s_0 = c$, которая генерирует выходную последовательность $y_1, y_2, \dots, y_{T'}$ токен за токеном:
$$s_t = f_{dec}(y_{t-1}, s_{t-1}), \qquad P(y_t \mid y_{Генерация продолжается, пока декодировщик не выдаст специальный токен конца последовательности </s>, либо пока не будет достигнута заранее заданная максимальная длина.
Разбор примеров
Пример 1 (полный проход через схему кодировщик → вектор контекста → декодировщик). Пусть входное предложение — «Я люблю кофе», токенизированное как $x_1=$«Я», $x_2=$«люблю», $x_3=$«кофе», $x_4=$</s>. Однослойная LSTM-кодировщик обрабатывает токены по одному, последовательно обновляя скрытое состояние: $h_0\to h_1$ (после «Я»)$\to h_2$ (после «люблю»)$\to h_3$ (после «кофе»)$\to h_4$ (после </s>). Финальное состояние $h_4$ становится вектором контекста $c$ — условно, скажем, набором из нескольких сотен чисел, в которых закодирован весь смысл предложения. Декодировщик стартует с $s_0=c$ и на первом шаге получает на вход специальный токен начала <s>: $s_1=f_{dec}(\texttt{}, s_0)$, и softmax по словарю английского языка выдаёт наибольшую вероятность токену «I» — он становится $y_1$. Дальше $y_1=$«I» подаётся как вход следующего шага: $s_2=f_{dec}(\text{«I»}, s_1)$ даёт «love» ($y_2$). Аналогично $s_3=f_{dec}(\text{«love»}, s_2)$ даёт «coffee» ($y_3$), а $s_4=f_{dec}(\text{«coffee»}, s_3)$ выдаёт </s> — сигнал остановки. Итоговый перевод — «I love coffee»: каждый токен сгенерирован строго последовательно, на основе всех предыдущих сгенерированных токенов и единственного вектора $c$, пришедшего от кодировщика.
Пример 2 (разная длина входа и выхода на практике). Пусть входное предложение состоит всего из одного содержательного токена — «Спасибо» ($x_1$) плюс </s> ($x_2$), то есть длина входа $T=2$. Кодировщик обрабатывает эти два токена и выдаёт вектор контекста $c=h_2$ — той же размерности, что и в примере 1, независимо от того, что вход был вдвое короче. Декодировщик, инициализированный этим же по размеру вектором $c$, генерирует уже два содержательных токена: «Thank» ($y_1$), «you» ($y_2$), затем </s> ($y_3$) — длина выхода $T'=3$. Ключевое наблюдение: размерность вектора $c$ фиксирована и не зависит ни от длины входа $T$, ни от длины выхода $T'$ — именно это свойство и делает архитектуру в принципе способной работать с произвольными, независимыми длинами входа и выхода. Но, как ты увидишь в следующем разделе, это же свойство станет источником главной проблемы архитектуры.
Пример 3 (суммаризация текста — та же схема, другая асимметрия длин). Тот же принцип работает и для суммаризации: кодировщик читает длинный абзац из, скажем, шестидесяти токенов, сжимая всю его смысловую структуру в вектор контекста той же размерности, что и в примерах 1–2, а декодировщик генерирует короткое резюме из восьми-десяти токенов. В отличие от перевода, где входная и выходная последовательности обычно сопоставимы по длине, суммаризация систематически требует сильного сжатия «много токенов входа → один вектор $c$ → мало токенов выхода» — и именно на такой выраженной асимметрии длин проблема бутылочного горлышка, разбираемая дальше, проявляется особенно остро.
Почему это важно
Архитектура кодировщик-декодировщик — первая по-настоящему сквозная нейросетевая схема для задач, где вход и выход не совпадают по длине заранее известным образом. Она обучается целиком, от сырого входного текста до сырого выходного, единственной функцией потерь через обратное распространение ошибки — без единой строчки вручную заданных лингвистических признаков или правил перестановки слов, на которых держались статистические системы перевода. Именно это архитектурное решение впервые открыло дорогу нейросетям сразу к целому классу задач: машинному переводу, суммаризации, генерации подписей к изображениям, диалоговым системам — всюду, где вход и выход являются последовательностями разной, заранее неизвестной длины.
Вектор контекста и проблема бутылочного горлышка
Интуиция
Представь, что тебя попросили пересказать целую книгу одним-единственным предложением, а затем кто-то другой, никогда не читавший книгу и видевший только твоё предложение, должен написать по нему новую книгу заново. Чем длиннее и сложнее исходная книга, тем очевиднее, что одно предложение физически не способно вместить все сюжетные линии, диалоги и детали — что-то неизбежно потеряется. Ровно то же самое происходит с вектором контекста: он имеет фиксированный размер, заданный архитектурой заранее (например, несколько сотен чисел), и в этот же фиксированный размер должна поместиться вся информация о входной последовательности — будь она длиной в три слова или в триста.
Формула
Проблема бутылочного горлышка (bottleneck problem) вектора контекста. Вектор контекста $c\in\mathbb{R}^d$ имеет фиксированную размерность $d$, заданную архитектурой заранее (например, $d=512$), — независимо от длины входной последовательности $T$. Вся информация обо всех $T$ входных токенах должна пройти через этот единственный канал шириной $d$:
$$x_1, x_2, \dots, x_T \;\longrightarrow\; c\in\mathbb{R}^d \;\longrightarrow\; y_1, y_2, \dots, y_{T'}$$При росте $T$ объём информации, условно приходящийся на каждую «единицу ёмкости» вектора $c$, падает: кодировщик вынужден неявно решать, какую часть смысла входа сохранить, а какую отбросить, причём декодировщик уже не может «вернуться» и перечитать исходный вход на этапе генерации — у него есть только один, единожды вычисленный вектор $c$.
Разбор примеров
Пример 1 (грубая арифметика «ёмкости на токен»). Пусть $d=512$. Для предложения из $T=5$ токенов на кодирование каждого токена условно приходится $512/5\approx102$ числа вектора $c$. Для предложения из $T=50$ токенов — уже только $512/50\approx10$ чисел на токен, в десять раз меньше. Кодировщик, разумеется, не буквально делит вектор на непересекающиеся куски по токенам — вся информация в реальности перемешана и распределена нелинейно по всем координатам $c$, — но эта грубая арифметика хорошо иллюстрирует суть проблемы: чем длиннее вход, тем меньше «места» в векторе фиксированного размера физически может достаться каждому фрагменту смысла, и тем сильнее неизбежная потеря детализации.
Пример 2 (потеря согласования в длинном предложении). Рассмотрим предложение: «Студент, который несколько месяцев готовился к экзамену по высшей математике, сдал его на отлично». Кодировщику нужно сжать в один вектор $c$ и подлежащее («студент»), и придаточное про подготовку, и сам факт сдачи экзамена, и оценку. На практике архитектуры кодировщик-декодировщик без дополнительных механизмов на предложениях такой длины и длиннее систематически теряют часть этой информации: типичная ошибка декодировщика — к моменту генерации конца предложения, где нужно согласовать род и число с подлежащим, модель уже «забыла», что подлежащее было именно «студент» в единственном числе, потому что вся информация о начале предложения давно растворилась в одном и том же неизменном векторе $c$, который не обновляется и не пересматривается по ходу генерации выхода.
Пример 3 (переворот входа как обходной приём, а не решение). Именно с этим эффектом столкнулись Суцкевер и коллеги в 2014 году: качество перевода, измеряемое метрикой BLEU (автоматической оценкой близости машинного перевода к эталонному переводу человека), заметно ухудшалось по мере роста длины входного предложения. Найденный ими обходной приём — переворот порядка слов во входной последовательности перед подачей в кодировщик, то есть кодировщик как бы читает предложение «с конца», — эмпирически улучшал качество, вероятно потому что сокращал эффективное расстояние между началом переведённого предложения и соответствующими ему словами исходного текста внутри развёрнутой по времени сети. Но это был именно обходной приём, а не решение самой проблемы: чем длиннее вход, тем более фундаментально не хватало ёмкости одного вектора $c$, чтобы удержать всю необходимую информацию, вне зависимости от того, в каком порядке эту информацию подавали.
Почему это важно
Проблема бутылочного горлышка — не побочный эффект недостаточного обучения или неудачно подобранных гиперпараметров, а структурное ограничение самой архитектуры: пока весь вход обязан пройти через единственный вектор фиксированного размера, качество перевода длинных последовательностей будет упираться в потолок, который простым увеличением количества эпох обучения или объёма данных не пробить. Это ограничение — прямая причина, по которой исследователи начали искать способ дать декодировщику доступ не к одному сжатому вектору, а к более богатому представлению всего входа целиком, — и этот поиск, как ты увидишь в конце урока, прямиком привёл к механизму внимания.
Teacher forcing: как на самом деле обучают декодировщик
Интуиция
Представь, что ты учишься писать сочинение по одному предложению за раз, и после каждого твоего предложения учитель сразу же говорит: «Хорошо, а вот как на самом деле должно было звучать это предложение — теперь пиши следующее, отталкиваясь именно от правильного варианта, а не от своего». Такой способ обучения резко отличается от ситуации, где тебе приходится строить каждое следующее предложение на основе того, что ты сам, возможно ошибочно, написал раньше — в этом случае одна ранняя ошибка тянет за собой цепочку следующих, и разобраться, что именно пошло не так, куда сложнее. При обучении декодировщика с нуля, когда его веса ещё случайны, он в самом начале обучения генерирует практически случайный, бессмысленный текст — и если кормить его собственным же бессмысленным выходом на следующем шаге, качество сигнала для обучения стремительно ухудшается по мере того, как генерация продвигается дальше по последовательности.
Формула
Teacher forcing (принудительное обучение по правильным предыдущим токенам). При обучении на шаге $t$ на вход декодировщику подаётся истинный токен целевой последовательности $y_{t-1}^{\text{true}}$, а не токен $\hat{y}_{t-1}$, который сама модель предсказала бы на предыдущем шаге:
$$s_t = f_{dec}\bigl(y_{t-1}^{\text{true}}, s_{t-1}\bigr)$$Функция потерь — сумма отрицательных логарифмов вероятностей правильных токенов на каждом шаге (кросс-энтропия), усреднённая по обучающей выборке пар последовательностей:
$$L = -\sum_{t=1}^{T'} \log P\bigl(y_t^{\text{true}} \mid y_{Разбор примеров
Пример 1 (teacher forcing на конкретном предложении). При обучении на паре («Я люблю кофе», «I love coffee») на шаге генерации токена после «I love» декодировщику подаётся именно истинное «love» — а не то, что модель могла бы сама предсказать, скажем, ошибочное «like», если веса ещё плохо обучены. Функция потерь на этом шаге штрафует модель за недостаточно уверенное предсказание истинного следующего слова «coffee», исходя из истинного «love», а не из собственной, возможно ошибочной, догадки. Если бы вместо этого использовался собственный выход модели, и на первом шаге модель случайно выдала бы «Me» вместо «I», второй шаг декодировщика получил бы на вход уже некорректное продолжение — и вся оставшаяся часть последовательности обучалась бы на основе испорченного контекста, что резко замедлило бы и дестабилизировало обучение.
Пример 2 (exposure bias — расхождение обучения и инференса). На инференсе, то есть когда модель переводит новое, ранее не виданное предложение, истинных токенов попросту не существует — их ещё только предстоит сгенерировать. Декодировщик вынужден на каждом шаге подавать себе на вход собственное предыдущее предсказание $\hat{y}_{t-1}$, а не $y_{t-1}^{\text{true}}$. Это расхождение между режимом обучения (всегда истинные токены) и режимом реального использования (всегда собственные предсказания) называется exposure bias (смещение по опыту, на котором модель фактически обучалась): модель никогда не видела во время обучения ситуацию «что делать, если предыдущий токен ошибочен», потому что при teacher forcing такой ситуации просто не бывает.
Пример 3 (scheduled sampling как частичный компромисс). Один из способов смягчить exposure bias — scheduled sampling (планируемая выборка): в начале обучения модели почти всегда подают истинный предыдущий токен, как при чистом teacher forcing, но по мере прогресса обучения постепенно, по заданному расписанию, увеличивают вероятность того, что на вход декодировщику вместо истинного токена подставят собственное предсказание модели. Так модель постепенно, ещё во время обучения, привыкает работать в условиях, приближённых к реальному инференсу, а не сталкивается с этим впервые только на этапе применения.
Почему это важно
Без teacher forcing обучение глубоких моделей кодировщик-декодировщик сходилось бы значительно медленнее или вовсе не сходилось бы за разумное время: ранняя ошибка декодировщика при обучении на собственных предсказаниях компаундируется — каждая следующая ошибка строится на предыдущей, и обучающий сигнал для поздних токенов последовательности становится всё менее информативным. Teacher forcing разрывает эту цепочку, позволяя каждому шагу декодировщика обучаться на чистом, не испорченном предыдущими ошибками контексте. Эта техника остаётся стандартной практикой и сегодня — в том или ином виде она используется при обучении практически всех авторегрессионных моделей последовательностей, вплоть до современных больших языковых моделей.
От бутылочного горлышка к решению: как рождается идея внимания
Интуиция
Соберём то, что мы установили. Задача seq2seq решена архитектурно: кодировщик и декодировщик, связанные единственным вектором контекста. Но этот вектор — узкое, структурное ограничение: чем длиннее вход, тем меньше информации о нём фактически доходит до декодировщика, и переворот входной последовательности лишь смягчает, но не устраняет проблему. Естественный следующий вопрос звучит почти по-детективному: а что, если вина лежит не в том, КАК кодировщик сжимает информацию, а в самом требовании сжимать её в единственный вектор? Что, если вместо этого декодировщику дать доступ ко всем промежуточным скрытым состояниям кодировщика — не только к последнему, $h_T$, но и к $h_1, h_2, \dots, h_{T-1}$ — и позволить ему самому, на каждом шаге генерации, решать, на какие именно из этих состояний «посмотреть» сейчас, с разным весом для разных моментов?
Формула
Идея, которая приведёт к механизму внимания (урок 342). Вместо единственного вектора контекста $c=h_T$, вычисленного один раз и используемого на всех шагах декодирования одинаково, декодировщику передаются все скрытые состояния кодировщика $h_1, h_2, \dots, h_T$, и на каждом шаге генерации $t$ вычисляется собственный, динамический вектор контекста $c_t$ — взвешенная комбинация всех $h_1,\dots,h_T$, где веса зависят от того, что именно декодировщик генерирует прямо сейчас:
$$c_t = \sum_{i=1}^{T} \alpha_{t,i}\, h_i$$Как именно вычисляются веса $\alpha_{t,i}$ — предмет отдельного урока 342. Уже сама идея «вместо одного фиксированного $c$ — много разных $c_t$, зависящих от текущего шага декодирования» и есть смысловое ядро механизма внимания.
Разбор примеров
Пример 1 (тот же перевод, но с доступом ко всем состояниям). Вернёмся к предложению «Я люблю кофе» → «I love coffee». В архитектуре без внимания декодировщик на всех трёх шагах генерации использует один и тот же вектор $c=h_3$. С вниманием на шаге генерации «coffee» декодировщик мог бы получить возможность «посмотреть» с наибольшим весом именно на скрытое состояние $h_3$, соответствующее слову «кофе» во входе, а не полагаться на смесь всей информации о предложении, усреднённую в единственном векторе $c$. Интуитивно это ближе к тому, как переводит человек: не пересказывая по памяти всё предложение целиком перед тем как написать каждое слово перевода, а поглядывая на нужную часть исходного текста в момент, когда переводится соответствующее слово.
Пример 2 (возвращаемся к потере согласования в длинном предложении). Вспомним пример из раздела про бутылочное горлышко — «Студент, который несколько месяцев готовился к экзамену по высшей математике, сдал его на отлично» — и потерю согласования с «студент» к концу перевода. Если бы декодировщик на шаге генерации финального слова мог динамически обратиться к скрытому состоянию, соответствующему слову «студент» в начале входного предложения, а не полагаться исключительно на единожды вычисленный и больше не обновляемый вектор $c$, вероятность потери согласования резко снижалась бы — потому что нужная информация была бы доступна напрямую, а не должна была бы «выжить» через всю цепочку сжатия в единственный узкий канал.
Пример 3 (численный аргумент про рост доступной ёмкости). В примере из раздела про бутылочное горлышко мы посчитали, что для входа из $T=50$ токенов на каждый токен условно приходится всего $512/50\approx10$ чисел вектора $c$ фиксированного размера $d=512$. Если вместо одного вектора декодировщик получает доступ ко всем $T=50$ скрытым состояниям $h_1,\dots,h_{50}$, суммарная доступная ёмкость растёт линейно вместе с длиной входа — $50\times512$ вместо $512$ — и сама постановка проблемы «чем длиннее вход, тем меньше места на каждый токен» перестаёт существовать в том виде, в каком она стояла для единственного вектора контекста.
Почему это важно
Именно эта логическая цепочка — от конкретной, измеримой проблемы (качество перевода падает с длиной предложения) через диагностику её причины (единственный вектор контекста фиксированного размера) к архитектурному решению (дать декодировщику доступ ко всем состояниям кодировщика и позволить ему динамически выбирать, на что опираться) — и есть путь, которым в 2014–2015 годах исследователи пришли к механизму внимания. Следующий урок разберёт эту идею во всех деталях: как именно вычисляются веса $\alpha_{t,i}$, какие для этого существуют формулы и почему это изменение оказалось настолько мощным, что несколько лет спустя привело к архитектуре, которая полностью отказалась от рекуррентности в пользу одного лишь внимания, — трансформеру (урок 343).
Практика: 30 заданий
Базовые задания (1–10)
Задание 1: Кодировщик читает входную последовательность длиной $T=6$ токенов и генерирует выходную последовательность длиной $T'=4$. Скрытый размер LSTM равен $d=256$. Какова размерность вектора контекста $c$?
Задание 2: Словарь входного языка содержит $V=10\,000$ токенов, размерность эмбеддинга $e=256$. Сколько параметров в матрице эмбеддингов кодировщика?
Задание 3: Скрытый размер декодировщика $d=512$, словарь выходного языка $V=8\,000$. Сколько параметров в выходном softmax-слое (с учётом смещений)?
Задание 4 (машинное обучение): Обычная RNN, обученная на классификацию отзывов (урок 339), читает последовательность произвольной длины и выдаёт один финальный ответ. Почему такая схема не подходит напрямую для машинного перевода?
Задание 5: Верно ли утверждение: «Вектор контекста $c$ не зависит от длины входной последовательности»?
Задание 6: Может ли архитектура кодировщик-декодировщик обработать пару, где вход длиной $T=3$, а выход длиной $T'=5$?
Задание 7 (машинное обучение): Что происходит с «информационной плотностью» вектора контекста (условным числом чисел вектора на один входной токен) при увеличении длины входа $T$ и фиксированной размерности $d$?
Задание 8: Чем отличается вектор контекста, передаваемый LSTM-кодировщиком, от вектора контекста, передаваемого обычной RNN-кодировщиком?
Задание 9: При обучении с teacher forcing, что подаётся на вход декодировщику на шаге $t$?
Задание 10 (машинное обучение): Почему обучение декодировщика без teacher forcing (то есть исключительно на собственных предсказаниях с самого начала обучения) сходится значительно медленнее?
Средние задания (11–20)
Задание 11: Размерность вектора контекста $d=512$. Сравни условную плотность информации на токен для входа длиной $T=8$ и $T=80$.
Задание 12 (машинное обучение): Батч из предложений разной длины дополняется специальными токенами-заполнителями (padding) до одной максимальной длины $T_{max}=20$. Почему важно вычислять финальное состояние кодировщика именно на реальном последнем токене каждого предложения, а не на позиции $T_{max}$ (используя, например,
pack_padded_sequenceв PyTorch), если предложение короче $T_{max}$?
Задание 13: Реализуй скелет кодировщика на PyTorch:
nn.Embedding+ однослойнаяnn.LSTM, возвращающая финальные состояния.import torch.nn as nn class Encoder(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim): super().__init__() # твой код здесь pass def forward(self, x): # твой код здесь: верни (hidden, cell) pass
Задание 14: При обучении с teacher forcing декодировщик выдал вероятности правильных токенов на трёх шагах: $p_1=0{,}7$, $p_2=0{,}4$, $p_3=0{,}9$. Посчитай кросс-энтропийную функцию потерь $L=-\sum_t \ln p_t$.
Задание 15 (машинное обучение): Опиши конкретный сценарий exposure bias: декодировщик на инференсе на первом шаге ошибочно генерирует не то слово. Как это влияет на дальнейшую генерацию?
Задание 16: LSTM-кодировщик: размерность эмбеддинга $e=128$, скрытый размер $d=256$. Число параметров одного слоя LSTM считается как $4\times\bigl(d\times(e+d)+d\bigr)$ (четыре гейта). Посчитай.
Задание 17: Посчитай число параметров обычной (не LSTM) RNN с теми же $e=128$, $d=256$ (один гейт, без множителя 4), и сравни с результатом задания 16.
Задание 18: Целевая последовательность длиной $T'=6$ токенов, словарь выходного языка $V=5\,000$. Сколько раз при обучении с teacher forcing вычисляется softmax-распределение и какой размерности каждый раз?
Задание 19 (машинное обучение): Scheduled sampling задаёт вероятность $p$ подачи истинного токена (вместо предсказания модели), которая линейно убывает от $1$ до $0$ по ходу обучения. Опиши, что происходит с поведением модели по мере обучения.
Задание 20: Дополни скелет обучающего цикла с teacher forcing (декодировщик генерирует токены по одному, каждый раз получая истинный предыдущий токен).
def train_step(decoder, initial_state, target_seq): state = initial_state loss = 0 for t in range(len(target_seq) - 1): # вход на шаге t: истинный токен target_seq[t] # твой код здесь pass return lossПродвинутые задания (21–30)
Задание 21: Размерность вектора контекста $d=512$, длина входа $T=100$ (длинный документ для суммаризации). Посчитай условную плотность информации на токен и сравни с результатом для $T=50$ из основного текста урока.
Задание 22 (машинное обучение): Почему простое увеличение размерности $d$ (например, с $512$ до $2\,048$) не решает проблему бутылочного горлышка полностью для произвольно длинных последовательностей?
Задание 23: Игрушечная конфигурация: словарь входа $V_{in}=6\,000$, словарь выхода $V_{out}=5\,000$, эмбеддинг $e=128$, скрытый размер $d=256$, однослойные LSTM и у кодировщика, и у декодировщика, плюс выходной softmax-слой. Посчитай суммарное число параметров всей модели.
Задание 24 (машинное обучение): Сравни архитектуру кодировщик-декодировщик без внимания и с вниманием (урок 342) с точки зрения соотношения «решение проблемы бутылочного горлышка» и «дополнительная вычислительная стоимость».
Задание 25: Дополни скелет декодировщика на PyTorch, который на каждом шаге принимает предыдущий токен и предыдущее состояние, а возвращает логиты и новое состояние.
import torch.nn as nn class Decoder(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim): super().__init__() # твой код здесь pass def forward(self, input_token, state): # твой код здесь: верни (logits, new_state) pass
Задание 26 (машинное обучение): Возьми предложение из пяти токенов $x_1,\dots,x_5$. Оцени, на сколько шагов «сближаются» позиции $x_1$ и первого декодированного токена $y_1$ при перевороте входной последовательности перед подачей в кодировщик, по сравнению с обычным порядком.
Задание 27: Scheduled sampling с линейным убыванием $p_i=\max(0,\,1-i/N)$, $N=100$. Найди $p$ на шагах $i=25$, $i=50$, $i=90$.
Задание 28 (машинное обучение): Почему teacher forcing невозможно применить на инференсе? Как декодировщик решает, какой токен выбрать, если не подаётся истинный ответ?
Задание 29 (машинное обучение): Почему архитектуру кодировщик-декодировщик с вниманием (урок 342) считают прямым концептуальным предком трансформера (урок 343), даже несмотря на то, что трансформер полностью отказывается от рекуррентности?
Задание 30 (машинное обучение): Опиши своими словами полную причинно-следственную цепочку: от задачи seq2seq до трансформера. Проверь себя по модельному ответу.
Частые ошибки
Путают вектор контекста с самим переводом. Вектор контекста — это внутреннее, не интерпретируемое напрямую человеком числовое представление смысла входа, а не текстовый перевод и не какой-либо человекочитаемый промежуточный результат; декодировщик ещё должен «развернуть» его в текст шаг за шагом.
Думают, что кодировщик и декодировщик — это одна и та же сеть, применённая дважды. На самом деле это две отдельные сети с собственными, независимо обучаемыми весами; их объединяет только то, что финальное состояние первой становится начальным состоянием второй.
Считают, что увеличение размерности вектора контекста полностью решает проблему бутылочного горлышка. Увеличение $d$ смягчает проблему, но не устраняет её принципиально: для достаточно длинного входа любое фиксированное $d$ снова окажется недостаточным (задание 22), а неограниченно увеличивать $d$ нельзя из-за роста вычислительных затрат.
Путают teacher forcing с тем, что модель «знает ответы» и на инференсе. Teacher forcing — исключительно техника обучения; на инференсе, когда переводится новый текст, эталонных ответов не существует, и декодировщик вынужден опираться на собственные предсказания (задание 28).
Считают, что обычная RNN или LSTM без разделения на кодировщик и декодировщик способна напрямую решать задачи seq2seq. Рекуррентная сеть сама по себе выдаёт по одному выходу на каждый входной шаг (или один финальный выход на всю последовательность) — она не умеет генерировать выходную последовательность произвольной, не совпадающей с длиной входа длины без явного разделения на две сети, связанные вектором контекста.
Думают, что механизм внимания (следующий урок) заменяет архитектуру кодировщик-декодировщик целиком. На самом деле внимание — это усовершенствование внутри той же самой парадигмы кодировщик-декодировщик: кодировщик и декодировщик остаются, меняется лишь то, как декодировщик получает доступ к представлению входа — не через единственный вектор, а через динамически взвешенную комбинацию нескольких состояний.
Главное запомнить
Sequence-to-sequence (seq2seq) — класс задач, где вход и выход являются последовательностями разной, заранее неизвестной длины: машинный перевод, суммаризация текста, генерация подписей, диалоговые системы.
Архитектура кодировщик-декодировщик решает эту задачу, разделяя работу на две отдельные рекуррентные сети: кодировщик читает вход и сжимает его в вектор контекста, декодировщик генерирует выход токен за токеном, используя этот вектор как начальное состояние.
Вектор контекста $c$ имеет фиксированную размерность, не зависящую от длины входа или выхода, — именно это позволяет архитектуре работать с последовательностями произвольной и разной длины.
Проблема бутылочного горлышка: вся входная последовательность, сколь угодно длинная, должна быть сжата в вектор фиксированного размера — с ростом длины входа информация неизбежно теряется, что проявляется в заметной деградации качества на длинных предложениях.
Переворот порядка слов входной последовательности (приём из статьи Суцкевера и др., 2014) сокращает эффективное расстояние между началом входа и началом генерации выхода, но не устраняет проблему бутылочного горлышка принципиально — это обходной приём, а не решение.
Teacher forcing — техника обучения, при которой декодировщику на каждом шаге подаётся истинный, а не самостоятельно предсказанный предыдущий токен; это резко ускоряет и стабилизирует обучение, не давая ранним ошибкам компаундироваться вдоль последовательности.
Exposure bias — расхождение между обучением (всегда истинные токены) и инференсом (всегда собственные предсказания модели); scheduled sampling — один из способов частично смягчить это расхождение.
Идея механизма внимания рождается напрямую из диагностики проблемы бутылочного горлышка: вместо одного вектора контекста декодировщику дают доступ ко всем скрытым состояниям кодировщика и позволяют динамически, на каждом шаге генерации, вычислять свой собственный взвешенный вектор контекста.
Архитектура кодировщик-декодировщик с вниманием — прямой концептуальный предок трансформера: идея динамического взвешенного комбинирования представлений сохраняется, хотя трансформер полностью отказывается от рекуррентной обработки последовательности во времени.
Связь с темами курса
Сегодняшний урок напрямую опирается на два предыдущих. Урок 339 ввёл саму идею рекуррентной сети — скрытое состояние, передающее информацию из прошлого в будущее внутри одной последовательности, — и именно этот механизм лежит в основе того, как кодировщик шаг за шагом накапливает представление входа, а декодировщик шаг за шагом генерирует выход. Урок 340 разобрал LSTM и GRU — гейтовые механизмы, решающие проблему затухающих градиентов при работе с длинными последовательностями; на практике архитектуру кодировщик-декодировщик почти никогда не реализуют на основе простой RNN именно потому, что входные и выходные последовательности в реальных задачах перевода или суммаризации достаточно длинны, чтобы простая RNN не справилась.
Урок 342 — прямое продолжение сегодняшней темы: механизм внимания решает ровно ту проблему бутылочного горлышка, которую мы сформулировали и разобрали на конкретных примерах в этом уроке, — давая декодировщику доступ ко всем скрытым состояниям кодировщика вместо единственного сжатого вектора. А урок 343 покажет, что идея внимания настолько мощна, что на ней можно построить архитектуру — трансформер — вообще без рекуррентности: сегодняшний урок, таким образом, закладывает концептуальный фундамент сразу для двух следующих ключевых архитектур курса.
Интересные факты
Кёнхюн Чо, соавтор одной из двух параллельных статей 2014 года о рекуррентном кодировщике-декодировщике для машинного перевода, годом позже стал соавтором архитектуры GRU (урок 340) — можно проследить прямую исследовательскую линию от первых экспериментов с encoder-decoder к упрощённым гейтовым механизмам рекуррентных сетей.
Оригинальная архитектура Суцкевера, Виньялса и Ле (2014) использовала глубокие четырёхслойные LSTM с тысячей скрытых нейронов в каждом слое — по меркам 2014 года это была весьма крупная модель, обучение которой занимало заметное время даже на тогдашнем специализированном оборудовании Google.
Приём с переворотом входной последовательности — один из немногих случаев в истории глубокого обучения, когда простое изменение порядка данных на входе, без единого изменения в самой архитектуре сети, заметно улучшило качество модели; это подчеркнуло, насколько чувствительны рекуррентные сети к тому, «как далеко» по времени приходится нести важную информацию.
К 2016 году Google перевела свою продакшен-систему перевода на полностью нейросетевой подход (Google Neural Machine Translation) — по собственным заявлениям компании, переход сократил количество ошибок перевода на значительную величину для целого ряда языковых пар, что стало одним из первых по-настоящему массовых, видимых пользователям результатов архитектуры кодировщик-декодировщик.
Лайфхаки
Прежде чем добавлять механизм внимания в свою реализацию seq2seq, сначала обучи простую версию без внимания и сознательно проверь её именно на длинных примерах — устойчивая деградация качества с ростом длины входа при нормальном качестве на коротких примерах является характерным диагностическим признаком именно проблемы бутылочного горлышка, а не признаком нехватки данных или эпох обучения.
Всегда обучай модель с teacher forcing, но валидируй качество через реальную автономную генерацию (без подсказок истинных токенов) — иначе метрика на валидации будет обманчиво хорошей: она измеряет качество предсказания на чистом, эталонном контексте, а не то, как модель на самом деле поведёт себя при инференсе.
В PyTorch аккуратно используй
pack_padded_sequence/pad_packed_sequenceпри работе с батчами последовательностей разной длины — без этого токены-заполнители незаметно испортят финальное скрытое состояние кодировщика, а значит и вектор контекста (см. задание 12).При отладке собственной реализации кодировщика-декодировщика первым делом проверяй совпадение формы тензоров на стыке двух сетей: у LSTM это пара
(hidden, cell), а не один тензор, и одна из самых частых ошибок начинающих — забыть передать декодировщику именно оба состояния, а не только скрытое.Если экспериментируешь с рекуррентным кодировщиком-декодировщиком без внимания на коротком учебном проекте, попробуй классический приём переворота входной последовательности перед кодированием — дешёвый, ничего не стоящий с точки зрения архитектуры способ немного облегчить работу декодировщику, прежде чем переходить к полноценному вниманию.
Прежде чем реализовывать внимание с нуля, полезно явно посчитать (как в заданиях 21–24 этого урока) конкретные числа: во сколько раз падает плотность информации в векторе контекста для типичной длины твоих последовательностей — это помогает интуитивно понять, при каких длинах входа архитектура без внимания скорее всего начнёт давать сбои.
Мы только что прошли путь настоящего детектива: заметили симптом (перевод длинных предложений заметно хуже коротких), нашли подозреваемого (единственный вектор контекста фиксированного размера, через который обязана пройти вся входная информация) и даже увидели, как первые исследователи пытались замаскировать симптом обходным приёмом — переворотом входа, — не устраняя причину. Но настоящее решение, которое навсегда изменит архитектуры для работы с последовательностями, ты увидишь только в следующем уроке: механизм внимания перестанет заставлять декодировщик полагаться на одну-единственную сжатую «записку» и даст ему возможность на каждом шаге генерации самому решать, куда именно во входе посмотреть прямо сейчас. Это открытие настолько фундаментально, что несколько лет спустя на нём одном — без единой рекуррентной связи — будет построена архитектура, которая перевернёт весь мир обработки естественного языка. Готовься: дальше — внимание.
Понял тему? Закрепи в боте! 🚀
Попрактикуйся на задачах и получи персональные рекомендации от AI
💪 Начать тренировку💬 Нашёл ошибку или есть вопрос по уроку? Написать в Telegram →