На прошлых лекциях рассматривались архитектуры рекуррентных нейросетей (RNN) и LSTM, которые принимали входные данные произвольного размера, что полезно для обработки текста
Рекуррентные нейросети имеют множество недостатков, такие как взрывающиеся или затухающие градиенты, трудности моделирования дальних зависимостей и отсутствие возможности полной параллелизации
У LSTM тоже есть проблемы, так как архитектура не масштабируется на очень длинные последовательности и большие данные
Поэтому появилась идея внимания - контекст важнее расстояния во времени. Вместо того чтобы полагаться только на последнее скрытое состояние, модель явно выбирает, на какие элементы последовательности смотреть
В классической архитектуре кодировщика и декодера всё работало так: последовательность $x_1, \dots, x_T$ проходила через кодировщик, и на выходе получался один вектор фиксированной размерности: $h_T = \mathrm{Encoder}(x_1, \dots, x_T)$
Этот вектор должен был сжать в себе всю информацию о входе. Затем декодер на каждом шаге генерации использовал его: $y_t = \mathrm{Decoder}(y_{<t}, h_T)$
Проблема в том, что один вектор $h_T$ обязан хранить всю информацию о длинной последовательности. При увеличении длины входа информация неизбежно теряется, и декодеру становится сложно учитывать дальние зависимости
Механизм внимания изменил эту схему. Вместо того чтобы полагаться только на последнее скрытое состояние, модель начинает явно выбирать, на какие элементы последовательности смотреть на каждом шаге генерации
Теперь кодировщик сохраняет все скрытые состояния: $h_1, h_2, \dots, h_T$
А декодер на шаге $t$ формирует динамический контекстный вектор - $c_t = \sum_{i=1}^{T} \alpha_{t,i} h_i$. Здесь $c_t$ - это контекст для текущего шага декодирования. Он является взвешенной суммой всех состояний кодировщика
Веса определяются через Softmax: $\alpha_{t,i} = \frac{\exp(e_{t,i})}{\sum_j \exp(e_{t,j})}$. Величина $e_{t,i}$ - это мера того, насколько важен элемент $h_i$ для текущего шага $t$. Обычно она вычисляется как функция текущего состояния декодера и состояния кодировщика (например, скалярное произведение или небольшая нейросеть)
Чтобы формализовать идею “запроса” и “сравнения”, вводится схема Query–Key–Value (Q, K, V). Это уже более обобщённая формулировка механизма внимания.
Каждый входной вектор преобразуется тремя различными линейными слоями: $Q = XW_Q, K = XW_K, V = XW_V$
Здесь:
Сравнение выполняется через скалярное произведение: $\text{scores} = QK^T$
Чем больше скалярное произведение, тем сильнее совпадение между запросом и ключом. Затем значения масштабируются, чтобы стабилизировать градиенты при большой размерности: $\frac{QK^T}{\sqrt{d_k}}$
После этого применяется softmax, и получаются веса внимания. Итоговая формула имеет вид: $\mathrm{Attention}(Q, K, V) = \mathrm{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$
Таким образом, веса $\alpha$ фактически вычисляются через сходство между $Q$ и $K$, а затем используются для взвешивания $V$
В более продвинутом подходе “самовнимания” (self-attention) значения $Q$, $K$ и $V$ получаются из одной и той же последовательности. Это означает, что каждый токен формирует запрос и сравнивает себя со всеми остальными токенами. В результате каждый элемент получает контекст, учитывающий всю последовательность
Таким образом, декодер больше не зависит от одного фиксированного вектора $h_T$. Он на каждом шаге получает динамический контекст из всех состояний кодировщика. А в более современной формулировке через $Q$, $K$, $V$ внимание становится универсальным механизмом сопоставления и агрегации информации, не зависящим от расстояния между элементами последовательности
Механизм внимания обладает некоторыми преимуществами перед рекуррентными нейросетями и LSTM:
O(T) до O(1)Тогда приходят к архитектуре трансформеров. Если внимание позволяет напрямую учитывать все элементы последовательности, то нет необходимости передавать информацию шаг за шагом через скрытое состояние, как в RNN.
В архитектуре трансформеров отсутствуют рекуррентность и сверточные операции, а вместо этого каждый токен на каждом слое может напрямую взаимодействовать со всеми остальными токенами через механизм самовнимания
Один слой трансформера состоит из двух основных блоков:
Multi-Head Self-Attention
На вход подаётся матрица представлений токенов $X$. Вычисляются матрицы: $Q = XW_Q$, $K = XW_K$ и $V = XW_V$
Далее применяется scaled dot-product attention: $\mathrm{Attention}(Q,K,V) = \mathrm{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$
Механизм multi-head означает, что таких преобразований выполняется несколько параллельно, после чего результаты конкатенируются
Самовнимание отвечает за взаимодействие токенов между собой
Feed-Forward Network (FFN)
После внимания к каждому токену независимо применяется двухслойная полносвязная сеть: $\mathrm{FFN}(x) = W_2 \sigma(W_1 x)$
Этот блок добавляет нелинейность и увеличивает выразительность модели
Каждый подблок обёрнут в остаточную связь и нормализацию слоя: $x \leftarrow x + \mathrm{SubLayer}(\mathrm{LayerNorm}(x))$
Это стабилизирует обучение и позволяет строить глубокие модели
Поскольку механизм самовнимания не учитывает порядок токенов, в модель добавляются позиционные кодировки: $X_i = \mathrm{Embedding}(token_i) + \mathrm{PosEncoding}(i)$
Позиционные векторы позволяют учитывать порядок слов в последовательности
Первоначальный трансформер состоял из двух частей:
Кодировщик, состоящий из $N$ одинаковых слоёв: Multi-Head Self-Attention + Feed-Forward Network
Декодер, каждый слой которого содержит:
Преимущества трансформера были такими:
Далее возникла идея сначала обучить большую модель на огромном корпусе текста (pretraining), а затем дообучать её под конкретную задачу (fine-tuning)
Так появились две ключевые линии развития:
BERT
В 2018 году Google представила BERT
Основная идея состоит в том, что бы использовать только кодировщик и обучать модель понимать текст, а не генерировать его
Случайные токены маскируются (например, слова удаляются), а модель предсказывает скрытые слова, используя левый и правый контекст. Это позволяет учитывать контекст с обеих сторон одновременно
GPT
Первая версия GPT была представлена в 2018 году
Ее основная идея состояла в том, что бы использовать только декодер и обучать модель предсказывать следующий токен
Это сработало, потому что модель учится статистической структуре языка, можно генерировать текст
GPT показал, что масштабирование с предобучением дает универсальные языковые представления
Так появились большие языковые модели (Large Language Model) - модели, понимающие естественные языки, содержащие множество параметров и способные предсказывать токены в последовательности. Они работают, потому что:
Ограничениями больших языковых моделей являются:
Для устранения ограничений придумали подход генерации, дополненной поиском (Retrieval-Augmented Generation, RAG)
Генерация, дополненная поиском, нужна для
Главная идея в том, что модель не должна хранить все знания в параметрах - она может получать актуальную информацию из внешнего источника во время ответа
Архитектура RAG состоит из двух частей:
Поиск
Генерация
Модель получает расширенный промпт, состоящий из вопроса пользователя и найденных документов, и генерирует ответ, опираясь на них
Детально реализация работается следующим образом:
Ограничения RAG: