Содержание курса 12 / 37

12 / 37 · Нейросетевые модели последовательностей

Рекуррентные нейронные сети

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

На этой странице 9

Свёрточный классификатор из предыдущей заметки видит окно из kk токенов и забывает позицию при пулинге. N-граммная языковая модель из более ранней заметки видит n−1n - 1 токенов и не умеет обобщать между ними. У обеих контекст фиксированного маленького размера. Эта заметка вводит модель, контекст которой в принципе — весь префикс: она читает токены по порядку и хранит их сводку в векторе, который обновляет на каждом шаге.

Обозначения. Входные векторы токенов x1,…,xT∈Rdx_1, \dots, x_T \in \mathbb R^{d}; скрытые состояния ht∈RHh_t \in \mathbb R^{H}; выходы yty_t. Матрицы весов Wx∈RH×dW_x \in \mathbb R^{H \times d}, Wh∈RH×HW_h \in \mathbb R^{H \times H}, Wy∈RK×HW_y \in \mathbb R^{K \times H} и смещения b,byb, b_y. ⊙\odot — поэлементное произведение.

Рекурсия#

ht=tanh⁡(Wxxt+Whht−1+b),h0=0,h_t = \tanh\big(W_x x_t + W_h h_{t-1} + b\big), \qquad h_0 = 0, yt=Wyht+by.y_t = W_y h_t + b_y .

WxW_x читает текущий токен, WhW_h переносит предыдущее состояние вперёд, а tanh⁡\tanh держит каждую координату hth_t в (−1,1)(-1, 1). На каждом шаге используются одни и те же Wx,Wh,bW_x, W_h, b, поэтому число параметров равно Hd+H2+HH d + H^2 + H при любом TT: последовательность из 5 токенов и последовательность из 500 прогоняют одну и ту же ячейку 5 или 500 раз. Развёрнутое во времени, вычисление — цепочка одинаковых ячеек, единственная связь между которыми — скрытое состояние.

Размерности для батча: вход [B,T,d][B, T, d], скрытые состояния [B,T,H][B, T, H], последнее состояние [B,H][B, H]. Модуль PyTorch nn.RNN(d, H, batch_first=True) возвращает и всю последовательность состояний, и последнее.

Разобранный пример#

Скалярный случай: d=H=1d = H = 1, Wx=1W_x = 1, Wh=0.5W_h = 0.5, b=0b = 0, входы x=(1,0,0)x = (1, 0, 0):

ttxtx_tWxxt+Whht−1W_x x_t + W_h h_{t-1}ht=tanh⁡(⋅)h_t = \tanh(\cdot)
111+0=11 + 0 = 10.7620.762
200+0.381=0.3810 + 0.381 = 0.3810.3630.363
300+0.182=0.1820 + 0.182 = 0.1820.1800.180

Вход шага 1 всё ещё виден в h3h_3, ослабленный в 0.50.5 раза на каждом шаге и ещё tanh⁡\tanh. При Wh=2W_h = 2 тот же вход рос бы: h2=tanh⁡(1.52)=0.909h_2 = \tanh(1.52) = 0.909, h3=tanh⁡(1.82)=0.949h_3 = \tanh(1.82) = 0.949, с насыщением у края диапазона tanh⁡\tanh. Рекуррентный вес решает, затухает прошлое или затапливает всё, и это один и тот же вес на каждом шаге, так что что бы он ни делал, он делает это TT раз.

Шаг за шагом#

Рисунок

Шаг рекуррентной сети: что куда идёт

Шаг RNN и пример из трёх шаговСлева один шаг: hₜ₋₁ через W_h и xₜ через W_x сходятся в сумматоре с b, затем tanh даёт hₜ, которое идёт вверх через W_y и softmax к выходу и вправо к следующему шагу. Справа три шага с числами: входы 1, 0, 0, состояния 0.762, 0.363, 0.180.(а) один шаг: что куда идётxtWx1ht−1Wh2+b3tanhht4Wysoftmaxŷt5t + 16(б) пример из текста: три шагаWx = 1, Wh = 0.5, b = 00h0×0.51x1×1+1tanh0.762h1×0.50x2×1+0.381tanh0.363h2×0.50x3×1+0.182tanh0.180h3входсостояниевеса и гейтывыходШаг RNN и пример из трёх шаговСлева один шаг: hₜ₋₁ через W_h и xₜ через W_x сходятся в сумматоре с b, затем tanh даёт hₜ, которое идёт вверх через W_y и softmax к выходу и вправо к следующему шагу. Справа три шага с числами: входы 1, 0, 0, состояния 0.762, 0.363, 0.180.(а) один шаг: что куда идётxtWx1ht−1Wh2+b3tanhht4Wysoftmaxŷt5t + 16(б) пример из текста: три шагаWx = 1, Wh = 0.5, b = 00h0×0.51x1×1+1tanh0.762h1×0.50x2×1+0.381tanh0.363h2×0.50x3×1+0.182tanh0.180h3входсостояниевеса и гейтывыход
(а) Один шаг для H = 3 и d = 4. Вход xₜ умножается на W_x (1), предыдущее состояние hₜ₋₁ — на W_h (2), результаты складываются с b (3), tanh даёт новое состояние hₜ (4). Если нужен выход, hₜ умножается на W_y и проходит softmax (5); само hₜ уходит на следующий шаг (6). (б) Те же операции с числами примера из текста: на каждом шаге одни и те же веса ×1 и ×0.5; единица, поданная на первом шаге, затухает от 0.762 до 0.180.

Номера на рисунке следуют за одним применением ячейки:

  1. Прочитать вход. WxxtW_x x_t — вклад текущего токена, вектор из HH чисел.
  2. Прочитать память. Whht−1W_h h_{t-1} — вклад предыдущего состояния; на первом шаге h0=0h_0 = 0, и это слагаемое пропадает.
  3. Сложить. Два вектора и смещение bb складываются покоординатно.
  4. Сжать. tanh⁡\tanh переводит каждую координату в (−1,1)(-1, 1); результат — новое состояние hth_t.
  5. Предсказать, если нужно. Wyht+byW_y h_t + b_y даёт оценки классов или токенов словаря, а softmax превращает их в вероятности.
  6. Передать состояние дальше. hth_t становится ht−1h_{t-1} следующего шага, который повторяет шаги 1–5 с теми же WxW_x, WhW_h и bb.

Модели следующих заметок сохраняют этот скелет — состояние, которое передаётся от шага к шагу и обновляется по текущему входу, — и каждая меняет одну его часть. LSTM меняет то, как обновляется состояние, seq2seq передаёт последнее состояние от одной сети другой, внимание позволяет второй сети оглядываться на каждое состояние первой, а self-attention отказывается от цепочки совсем. Их рисунки используют те же цвета: зелёный для входов, синий для состояний, янтарный для весов и гейтов, красный для выходов и янтарную рамку для того, что модель добавляет к предыдущей.

Два способа использовать состояния#

Рисунок

Одна рекурсия, два способа использовать состояния

RNN как классификатор и как языковая модель(а) Четыре ячейки RNN читают oil, price, soar, record; последнее состояние идёт в linear + softmax, ответ Business. (б) Четыре ячейки читают the, cat, sat, on; каждая выдаёт softmax, цели — cat, sat, on, the.(а) классификатор: последнее состояниеh₀RNNoilRNNpriceRNNsoarRNNrecordlinear + softmax → Business(б) языковая модель: каждое состояниеh₀RNNthesoftmaxcatRNNcatsoftmaxsatRNNsatsoftmaxonRNNonsoftmaxtheRNN как классификатор и как языковая модель(а) Четыре ячейки RNN читают oil, price, soar, record; последнее состояние идёт в linear + softmax, ответ Business. (б) Четыре ячейки читают the, cat, sat, on; каждая выдаёт softmax, цели — cat, sat, on, the.(а) классификатор: последнее состояниеh₀RNNoilRNNpriceRNNsoarRNNrecordlinear + softmax → Business(б) языковая модель: каждое состояниеh₀RNNthesoftmaxcatRNNcatsoftmaxsatRNNsatsoftmaxonRNNonsoftmaxthe
Обе модели — одна и та же ячейка, применённая к каждому токену по очереди; горизонтальные стрелки передают скрытое состояние hₜ дальше, начиная с h₀ = 0. (а) Классификатор читает весь документ и подаёт линейному слою только последнее состояние, которое зависит от всех токенов по порядку. (б) Языковая модель в каждой позиции выдаёт распределение по словарю и учится предсказывать следующий токен: входы — текст без последнего токена, цели — тот же текст, сдвинутый на один (teacher forcing). Функция потерь — сумма −log P(цель) по позициям, и её экспонента после усреднения — перплексия.

RNN как классификатор#

Прочитать документ целиком и взять последнее состояние в качестве его представления: y^=softmax(WyhT+by)\hat y = \mathrm{softmax}(W_y h_T + b_y). В отличие от max-pooling, hTh_T вычисляется по порядку, поэтому в принципе может отличить not good от good not. В постановке AG News из предыдущей заметки однослойная GRU (ячейка с гейтами из следующей заметки) с H=128H = 128 читает те же 100-мерные векторы Word2Vec и подаёт последнее состояние линейному слою. У неё 3(100⋅128+128⋅128+2⋅128)+128⋅4+4=88 8363(100 \cdot 128 + 128 \cdot 128 + 2 \cdot 128) + 128 \cdot 4 + 4 = 88\,836 параметров — столько же по порядку, сколько у свёрточных моделей.

Одна практическая деталь. Последнее состояние дополненной последовательности — это состояние после токенов паддинга, а не после последнего настоящего токена. Можно упаковать последовательности, чтобы рекурсия останавливалась на истинной длине, можно ставить паддинг в начало, а можно брать состояние с индексом length - 1 для каждого примера.

RNN как языковая модель#

Выдавать выход на каждом шаге и делать его распределением по словарю:

P(wt+1∣w≤t)=softmax(Wyht+by),Wy∈R∣V∣×H.P(w_{t+1} \mid w_{\le t}) = \mathrm{softmax}(W_y h_t + b_y), \qquad W_y \in \mathbb R^{|V| \times H}.

Это разложение по формуле умножения вероятностей из n-граммной заметки без марковского предположения: условная вероятность зависит от hth_t, которое зависит от всего префикса. Обучение использует teacher forcing: входная последовательность — текст без последнего токена, x=(w1,…,wT−1)x = (w_1, \dots, w_{T-1}), цели — y=(w2,…,wT)y = (w_2, \dots, w_T), а функция потерь — кросс-энтропия, просуммированная по позициям, то есть ровно отрицательный логарифм правдоподобия на токен, экспонента которого — перплексия. Позиции паддинга исключаются из функции потерь через ignore_index. При порождении на вход следующего шага подаётся собственный сэмпл модели, как в любой авторегрессионной модели.

Размерности: идентификаторы [B,T][B, T] → эмбеддинги [B,T,E][B, T, E] → состояния [B,T,H][B, T, H] → логиты [B,T,∣V∣][B, T, |V|]. Кросс-энтропия считается на развёрнутых логитах [B⋅T,∣V∣][B \cdot T, |V|] относительно целей [B⋅T][B \cdot T].

Возьмём модель над словарём BPE из ∣V∣=37 580|V| = 37\,580 токенов с E=256E = 256 и H=512H = 512: у неё 9.6 млн параметров во входном эмбеддинге, 0.39 млн в рекуррентной ячейке и 19.3 млн в выходном слое, всего 29.3 млн. Две трети модели — выходной softmax: та же структура затрат, которая привела к негативному сэмплированию в Word2Vec; в языковой модели полный softmax сохраняют, потому что выходом служит само распределение. К перплексиям нейросетевых языковых моделей на трансформере возвращается заметка о GPT.

Градиенты во времени#

Обучение разворачивает рекурсию и распространяет ошибку обратно через каждый шаг. Градиент функции потерь на шаге TT по состоянию на шаге tt проходит через T−tT - t применений ячейки:

∂hT∂ht=∏s=t+1T∂hs∂hs−1=∏s=t+1Tdiag(1−hs2) Wh.\frac{\partial h_T}{\partial h_t} = \prod_{s=t+1}^{T} \frac{\partial h_s}{\partial h_{s-1}} = \prod_{s=t+1}^{T} \mathrm{diag}\big(1 - h_s^2\big)\, W_h .

Каждый множитель — та же WhW_h, масштабированная производной tanh⁡\tanh (не больше 1). Если наибольшее сингулярное число WhW_h меньше 1, произведение убывает геометрически, и градиент затухает: функция потерь в конце предложения не может поправить то, что ячейка сделала в его начале. Если оно больше 1, произведение может расти, и градиент взрывается (Pascanu, Mikolov and Bengio, 2013 показывают, что это условие необходимое, но не достаточное). При скалярном рекуррентном весе w=2w = 2 и состояниях в линейной части tanh⁡\tanh, где её производная близка к 1, пятьдесят шагов умножают градиент на 250≈10152^{50} \approx 10^{15}. Рисунок ниже, общий с заметкой курса по генеративным моделям, где вычисление расписано подробно, показывает механизм.

Рисунок

Развёрнутая RNN с рекуррентным весом w = 2

Развёрнутая рекуррентная сеть со взрывающимися множителямиТри временных шага расположены друг под другом. На каждом шаге блок входа подаётся в рекуррентную ячейку со значком ReLU; первые две ячейки ведут к штриховым блокам выхода, третья — к сплошному. Изогнутые красные стрелки переносят скрытое состояние от каждой ячейки к следующей, и каждая подписана «умножить на 2»; под третьей ячейкой продолжается штриховая красная волнистая линия, тоже с подписью «умножить на 2 и так далее, взрыв». Пояснение говорит, что через tau шагов множитель равен w в степени tau.входw = 2выходx1x2x3ячейка ReLU×2×2×2 …взрывскрытое состояние hпроходит черезодно и то же wчерез τ шагов:×wτРазвёрнутая рекуррентная сеть со взрывающимися множителямиТри временных шага расположены друг под другом. На каждом шаге блок входа подаётся в рекуррентную ячейку со значком ReLU; первые две ячейки ведут к штриховым блокам выхода, третья — к сплошному. Изогнутые красные стрелки переносят скрытое состояние от каждой ячейки к следующей, и каждая подписана «умножить на 2»; под третьей ячейкой продолжается штриховая красная волнистая линия, тоже с подписью «умножить на 2 и так далее, взрыв». Пояснение говорит, что через tau шагов множитель равен w в степени tau.входw = 2выходx1x2x3ячейка ReLU×2×2×2 …взрывКрасные стрелки: на каждом шаге скрытоесостояние h проходит через одно и то же w.Через τ шагов множитель равен wτ.
На каждом временном шаге применяется один и тот же вес w = 2, поэтому каждая красная рекуррентная стрелка умножает на 2. Значит, чувствительность более позднего скрытого состояния к более раннему, а с ней и градиент при обратном распространении, равна 2 через один шаг, 4 через два, 8 через три и w в степени τ через τ шагов. Промежуточные выходы показаны штриховой линией, потому что здесь используется только последний.

Два следствия для практики. Обрезка градиентов (gradient clipping) масштабирует вектор градиента, когда его норма превышает порог, например 1.0; это не даёт взрывам разрушить веса, но ничего не делает с затуханием. А затухание — главное ограничение: обычная RNN выучивает зависимости на несколько токенов и теряет остальное, поэтому «контекст — весь префикс» верно для прямого прохода, но не для того, чему модель учится (Bengio, Simard and Frasconi, 1994). Следующая заметка меняет ячейку так, чтобы у состояния был аддитивный путь с множителем градиента около 1.

Что изменилось#

CNN-классификаторN-граммная ЯМRNN
Контекстkk токенов на фильтрn−1n - 1 токеновВесь префикс (в принципе)
Параметры и длинаНе зависятТаблица растёт с даннымиНе зависят
ПозицияТеряется при пулингеТолько последние n−1n-1Закодирована в порядке состояний
Обобщает между токенамиДа (эмбеддинги)НетДа
ПорождениеНетДаДа, с выходом на ∣V∣\lvert V \rvert классов
Параллельно по TTДа—Нет: hth_t требует ht−1h_{t-1}

Ограничения#

  • Короткая эффективная память. Градиенты затухают за десятки шагов; состояние нельзя обучить долго хранить информацию.
  • Последовательные вычисления. Шаг tt ждёт шага t−1t - 1, поэтому RNN не может использовать параллелизм GPU вдоль последовательности. Время обучения на пример растёт линейно по TT, и заметка о трансформере сделает это главным доводом против рекурсии.
  • Одно состояние на всё. Единственный вектор hTh_T должен подытожить весь вход. Для классификации этого достаточно; для перевода предложения из 30 слов это узкое место, в которое упирается заметка о seq2seq.
  • Паддинг взаимодействует с последним состоянием. Переменная длина требует упаковки или масок, которые не были нужны свёрточной модели.

Что нужно запомнить#

  • ht=tanh⁡(Wxxt+Whht−1+b)h_t = \tanh(W_x x_t + W_h h_{t-1} + b): одна и та же ячейка на каждом шаге, параметры не зависят от длины, состояние несёт префикс.
  • Классификатор: последнее состояние → линейный слой. Языковая модель: каждое состояние → softmax по ∣V∣|V|, обучение через teacher forcing на сдвинутой последовательности без учёта паддинга.
  • Градиент через T−tT - t шагов — произведение T−tT - t копий diag(1−h2)Wh\mathrm{diag}(1 - h^2)W_h; он затухает или взрывается геометрически. Обрезка справляется только со взрывом.
  • Рекурсия последовательна: вдоль последовательности нет параллелизма.

Ноутбук#

Два ноутбука. Номера ячеек считают все ячейки, включая текстовые, начиная с 1. Приведённые числа взяты из прогона, сохранённого в курсе; у ячеек, изменённых с тех пор под текущие версии библиотек, сохранённого выхода нет.

week02_NN_text_classification.ipynb, ячейки 40–43: SimpleRNN оборачивает nn.GRU(100, 128, batch_first=True) и линейный слой на последнем скрытом состоянии и обучается тем же циклом, что и свёрточные модели, 5 эпох. Итоговой распечатки в сохранённых выходах нет, поэтому в курсе нет доли правильных ответов, чтобы сравнить эту GRU со свёрточными классификаторами и с базовым уровнем TF-IDF. Классификатор берёт последнее состояние после токенов паддинга, а не после последнего настоящего токена — упрощение, разобранное в разделе RNN как классификатор.

week03_lm.ipynb:

  • Ячейки 46–49 токенизируют корпус анекдотов BPE и строят словарь из 37 580 записей с фиксированными идентификаторами для <pad> (0) и <unk> (1).
  • Ячейка 51 строит батчи: каждый анекдот обрезается до 128 токенов, входы — s[:-1], цели — s[1:], и те и другие дополняются нулями.
  • Ячейка 52 определяет SimpleRNNLM: nn.Embedding(V, 256, padding_idx=0), nn.RNN(256, 512, batch_first=True), nn.Linear(512, V) — модель на 29.3 млн параметров, посчитанная в разделе RNN как языковая модель.
  • Ячейка 53 прогоняет одну эпоху: кросс-энтропия с ignore_index=0, обрезка нормы градиента на 1.0 и перплексия как exp⁡\exp от средних по токенам потерь (выше потерь 20 она полагается равной ∞\infty, чтобы избежать переполнения). Ячейка 55 обучает три эпохи с AdamW при 3×10−33 \times 10^{-3} и батчами по 64; прогон, сохранённый в курсе, был прерван после нескольких батчей, поэтому сохранённых перплексий нет. Softmax на 37 580 токенов делает эту ячейку практичной только на GPU с CUDA, о чём говорит текстовая ячейка перед ней.

Дальше#

Рекурсия даёт модели память, а градиент её отнимает. Следующая заметка сохраняет рекурсию и перестраивает ячейку: отдельная линия памяти, которая обновляется аддитивно, и гейты, решающие, что записать, что сохранить и что прочитать, так что градиент вдоль линии памяти — произведение множителей, близких к 1, а не матриц WhW_h.

Литература#