28 · Версия материала 2
Закройте доступ к будущим ключам каузальной маской
Разберите, как нижнетреугольная каузальная маска с разрешённой диагональю закрывает доступ к будущим ключам Transformer, делает их вероятности внимания строго нулевыми и сохраняет выходы предыдущих позиций.
Предскажите видимый треугольник
Добавьте к примеру из главы 27 третью позицию:
Здесь один пакет, три позиции токенов, ширина запросов и ключей равна двум, ширина значений — тоже двум. Следовательно,
До маскирования строки исходных оценок равны , и . Сначала отметьте ячейки, доступные каждому запросу. Запросу доступен только ключ ; запросу — ключи и ; запросу — все три ключа. Итого получается шесть доступных и три закрытые ячейки. Все три диагональные ячейки входят в число доступных.
Примените маску до softmax
Для строки запроса и столбца ключа используйте аддитивную маску
Диагональ намеренно остаётся доступной. При обучении декодера целевые входы сдвинуты на одну позицию. Поэтому представление на диагонали содержит уже известный предыдущий токен, а не цель, которую модель предсказывает. Сдвиг и маска вместе сохраняют авторегрессионную зависимость.
Пусть тензор масштабированных оценок равен
До нормировки добавьте ограничение видимости, затем смешайте значения:
Для этого примера
Вероятность внимания к каждому закрытому ключу строго равна нулю. Сумма вероятностей по каждому доступному префиксу равна единице:
Получаются строки
Нельзя сначала применить обычный softmax ко всем ключам, а затем обнулить вероятности будущих ключей: в знаменателе вероятностей доступных ключей по-прежнему будут учтены закрытые ключи. Большое по модулю конечное отрицательное число можно использовать как приближение идеальной маски, но математически оно не равно .
Не путайте ограничение видимости с позиционной информацией
- — тензор масштабированных оценок соответствия запросов и ключей до маскирования.
- — аддитивная маска видимости.
- обозначает позицию запроса, а — позицию ключа.
- содержит вероятности внимания после исключения будущих ключей.
- , и сохраняют роли запросов, ключей и значений из главы 27.
- содержит по одной взвешенной смеси доступных строк значений для каждой позиции запроса.
- — размер пакета, — число токенов, — ширина запросов и ключей, а — ширина значений.
Полное правило для форм остаётся таким:
Для доступной ячейки нормировка строки имеет вид
а для закрытой ячейки
Это правило задаёт видимость, но не положение. Чтобы различать порядок, строкам нужен отдельный абсолютный или относительный позиционный сигнал. Маски заполнения, переменная длина последовательности, несколько голов, выходная проекция и кеш ключей и значений — тоже отдельные задачи.
От рекуррентного состояния префикса к явной маске декодера
Грейвс, Generating Sequences With Recurrent Neural Networks обучает модель предсказывать следующий элемент, последовательно обрабатывая элементы последовательности. При генерации каждый выбранный элемент становится следующим рекуррентным входом, поэтому будущих элементов ещё не существует. Граница префикса возникает из последовательной рекуррентной обработки, где состояние приходится обновлять шаг за шагом. Это утверждение относится к генерации текста в статье, а не к отдельной модели синтеза рукописного текста с дополнительным условным входом.
Васвани и соавторы, Attention Is All You Need объединяют запросы в матрицы и маскируют самовнимание декодера так, чтобы строка не могла обратиться к последующим позициям. Недопустимым оценкам до softmax присваивается ; вместе с эмбеддингами выходной последовательности, сдвинутыми на одну позицию, это означает, что строка зависит только от известных выходов до позиции .
Поэтому при обучении строки внимания для известных целевых позиций можно вычислять вместе, не позволяя более ранней строке использовать более позднюю цель. При этом обычная авторегрессионная генерация по-прежнему добавляет по одному токену. Декодерный Transformer применяет эту каузальную границу в каждом слое самовнимания. Маска задаёт границу видимости, но не кодирует положение.
Оставьте маску доступной для проверки, а записанные значения — конечными
causal_additive_mask строит обычный тензор формы . В доступных ячейках
находится , а в будущих — :
rust/crates/llm-from-scratch/src/attention/causal_mask.rs#causal-mask-construction /// Builds an additive square mask with zero for `key <= query` and negative
/// infinity for future keys.
pub fn causal_additive_mask(tokens: usize) -> Result<Tensor, CausalMaskingError> {
let elements = tokens
.checked_mul(tokens)
.ok_or(CausalMaskingError::MaskTensor(TensorError::ShapeOverflow))?;
let mut values = Vec::new();
values
.try_reserve_exact(elements)
.map_err(|_| CausalMaskingError::MaskAllocationFailed { elements })?;
for query in 0..tokens {
for key in 0..tokens {
values.push(if key <= query { 0.0 } else { f64::NEG_INFINITY });
}
}
Tensor::from_vec(vec![tokens, tokens], values).map_err(CausalMaskingError::MaskTensor)
} Дифференцируемый TensorValue отклоняет неконечные данные листовых тензоров.
Операция causal_softmax реализует ту же математическую границу, не записывая
: она читает только , вычитает максимум доступного префикса,
нормирует доступные ячейки и записывает точное значение с плавающей точкой
во все закрытые ячейки.
rust/crates/llm-from-scratch/src/autograd/model_ops.rs#causal-softmax-forward fn causal_softmax_forward(input: &Tensor) -> Result<Tensor, TensorAutodiffError> {
if input.rank() < 2 {
return Err(ModelOpError::CausalSoftmaxRank { rank: input.rank() }.into());
}
let queries = input.shape()[input.rank() - 2];
let keys = input.shape()[input.rank() - 1];
if queries != keys {
return Err(ModelOpError::CausalSoftmaxNonSquare { queries, keys }.into());
}
if queries == 0 {
return Err(ModelOpError::CausalSoftmaxEmptyTokens.into());
}
let mut probabilities = zeros(input.shape())?;
let grids = input.len() / (queries * keys);
for grid in 0..grids {
for query in 0..queries {
let row_start = (grid * queries + query) * keys;
let allowed = &input.as_slice()[row_start..=row_start + query];
let maximum = allowed.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let mut exponential_tail = 0.0;
let mut skipped_one_maximum = false;
for &score in allowed {
let shifted = score - maximum;
if shifted == 0.0 && !skipped_one_maximum {
skipped_one_maximum = true;
} else {
exponential_tail += shifted.exp();
}
}
debug_assert!(skipped_one_maximum);
let denominator = 1.0 + exponential_tail;
for (key, &score) in allowed.iter().enumerate() {
let probability = (score - maximum).exp() / denominator;
probabilities.as_mut_slice()[row_start + key] =
if probability == 0.0 { 0.0 } else { probability };
}
}
}
Ok(probabilities)
} Операция принимает тензоры оценок ранга не меньше двух, у которых размеры последних двух осей равны. Поэтому она поддерживает форму , которая понадобится далее, не смешивая ведущие оси. Операция отклоняет ранг меньше двух, разные размеры последних осей, пустую ось токенов и уже освобождённый операнд.
causal_scaled_dot_product_self_attention повторно использует построение
оценок из главы 27, применяет causal_softmax и умножает результат на :
rust/crates/llm-from-scratch/src/attention/causal_mask.rs#causal-self-attention-forward /// Inspectable evidence from one causally masked attention head.
#[derive(Clone, Debug)]
pub struct CausalSelfAttentionForward {
raw_scores: TensorValue,
scaled_scores: TensorValue,
additive_mask: Tensor,
weights: TensorValue,
output: TensorValue,
scale: f64,
key_width: usize,
value_width: usize,
}
impl CausalSelfAttentionForward {
pub fn raw_scores(&self) -> &TensorValue {
&self.raw_scores
}
pub fn dot_products(&self) -> &TensorValue {
&self.raw_scores
}
pub fn scaled_scores(&self) -> &TensorValue {
&self.scaled_scores
}
/// The plain additive mask. It is intentionally not a tape value because
/// its blocked cells contain negative infinity.
pub const fn additive_mask(&self) -> &Tensor {
&self.additive_mask
}
pub fn weights(&self) -> &TensorValue {
&self.weights
}
pub fn probabilities(&self) -> &TensorValue {
&self.weights
}
pub fn output(&self) -> &TensorValue {
&self.output
}
pub const fn scale(&self) -> f64 {
self.scale
}
pub const fn key_width(&self) -> usize {
self.key_width
}
pub const fn value_width(&self) -> usize {
self.value_width
}
pub fn into_output(self) -> TensorValue {
self.output
}
}
/// Computes one scaled self-attention head with an inclusive-prefix mask.
pub fn causal_scaled_dot_product_self_attention(
query: &TensorValue,
key: &TensorValue,
value: &TensorValue,
) -> Result<CausalSelfAttentionForward, CausalMaskingError> {
let prepared = scaled_self_attention_scores(query, key, value)?;
let tokens = query.shape()[1];
let additive_mask = causal_additive_mask(tokens)?;
let weights = prepared
.scaled_scores
.causal_softmax()
.map_err(autodiff_error(CausalMaskingStage::MaskedSoftmax))?;
let output = weights
.matmul(value)
.map_err(autodiff_error(CausalMaskingStage::ValueMixture))?;
Ok(CausalSelfAttentionForward {
raw_scores: prepared.raw_scores,
scaled_scores: prepared.scaled_scores,
additive_mask,
weights,
output,
scale: prepared.scale,
key_width: prepared.key_width,
value_width: prepared.value_width,
})
} Для доступной оценки обратный проход подчиняется формуле
а закрытая оценка получает
Все шесть координат каждого из тензоров , и согласуются с центральными разностями при шаге и допуске . Среди проверенных граничных случаев также есть один токен, пустые пакеты, тензоры оценок ранга два и четыре, независимые ведущие оси, очень большие закрытые оценки, типизированные ошибки, операнды из освобождённой ленты и побитное совпадение при повторном запуске.
Исполняемый пример выводит проверенные данные: маску, вероятности, выходы, результат проверки неизменности префикса, градиенты и граничные случаи:
rust/demos/ch28-causal-masking/src/main.rs fn main() -> Result<(), Box<dyn std::error::Error>> {
let evidence = ch28_causal_masking::learner_evidence()?;
print!("{}", ch28_causal_masking::render_report(&evidence));
Ok(())
} Выполните cargo run --quiet --locked -p ch28-causal-masking, чтобы увидеть
полный результат примера.
Проследите путь внимания по нижнему треугольнику
Проследите каузальную границу в каждой строке внимания
Проследите путь строк запросов, ключей и значений через нижнетреугольное ограничение видимости, затем сравните исходные выходы с выходами после замены суффикса и изучите градиенты.
- Доступно: сплошная рамка
- Закрыто: штриховая рамка
- Разрешённая диагональ: двойная рамка
- Побитно совпадает с исходным
- Изменилось
Проследите один расчёт по нижнему треугольнику
В каждой строке доступны диагональная ячейка и все предыдущие столбцы ключей; различия обозначены и рамкой, и текстом.
Начните со строк запросов, ключей и значений
-
Строки запросов: какую часть префикса может использовать эта позиция?
Форма:
-
Строки ключей: с какими доступными позициями можно сопоставить запрос?
Форма:
-
Строки значений: какое содержимое могут внести видимые позиции?
Форма:
Отметьте нижний треугольник вместе с диагональю
| Диагональ | Закрыто | Закрыто | |
| Доступно | Диагональ | Закрыто | |
| Доступно | Доступно | Диагональ |
Исключите будущие оценки до нормировки
| Диагональ | Закрыто | Закрыто | |
| Доступно | Диагональ | Закрыто | |
| Доступно | Доступно | Диагональ |
Нормируйте каждый доступный префикс
| Диагональ | Закрыто | Закрыто | |
| Доступно | Диагональ | Закрыто | |
| Доступно | Доступно | Диагональ |
- :
- :
- :
Смешайте только доступные строки значений
| Слагаемые после умножения на веса | Строка выхода | |
|---|---|---|
Измените суффикс и проверьте предыдущие выходы
Изменяются только последний ключ и последнее значение; первые две строки выхода побитно совпадают с исходными.
Замените последний ключ и последнее значение
До замены: После замены:
До замены: После замены:
| Исходный выход | После замены суффикса | Результат для префикса | |
|---|---|---|---|
| Побитно совпадает с исходным | |||
| Побитно совпадает с исходным | |||
| Изменилось |
Проверьте обратный проход и граничные случаи
Начальные градиенты по всем выходам и только по префиксу показывают, куда градиент может пройти, а куда — нет.
Начальный градиент только по префиксу
Для функции потерь только по префиксу изменённый суффикс получает нулевой градиент.
ПровереноГраничный случай с одним токеном
Сохранение форм для пустого пакета
→
ПровереноПроверенные каузальные свойства
Записанные значения для автоматического дифференцирования остаются конечными: Проверено
Вероятности будущих ключей: Строго равны нулю
Предыдущие выходы после замены суффикса: Побитно совпадают с исходными
Отклонённые недопустимые входы
- Внимание без позиций токенов
empty-tokensОтклонено - Ранг тензора оценок меньше двух
causal-softmax-rank: rank=1Отклонено - Последние две оси тензора оценок имеют разную длину
causal-softmax-non-square: queries=2|keys=3Отклонено - Неверный ранг тензора запросов
score-input-rank: input=query|rank=2Отклонено - Число токенов в запросах, ключах и значениях различается
score-token-mismatch: query=3|key=2|value=3Отклонено - Операнд оценок относится к уже освобождённой ленте операций
released-operand: operation=causal-softmax|operand=0Отклонено
От префикса при рекуррентной генерации к декодеру Transformer
Рекуррентные вычисления неявно ограничивают модель префиксом, а самовнимание декодера Transformer задаёт эту границу явно.
-
Префикс при рекуррентной генерации
При рекуррентной генерации существует только уже созданный префикс, а рекуррентное состояние продвигается на одну позицию за шаг.
-
Явная маска декодера Transformer
При обучении сдвинутые входы декодера и каузальная маска позволяют вместе обрабатывать известные целевые позиции, не давая более ранним строкам использовать более поздние цели; генерация остаётся последовательной.
Треугольные таблицы показывают, какие оценки сохраняются до нормировки. Сплошная, штриховая и двойная рамки различают доступные, закрытые и диагональные ячейки, не полагаясь только на цвет.
Если изменить только
то и побитно совпадут с исходными, а
Сначала сделайте прогноз, затем откройте ответы
- Запишите множество индексов доступных ключей для строк запросов , и .
- Предскажите, может ли замена только и изменить или .
- Объясните, почему все три диагональные ячейки доступны, хотя модель не может получить доступ к токену, который нужно предсказать.
- Предскажите градиенты запроса и ключа для каузальной головы самовнимания с одним токеном.
- Объясните, почему после обнуления будущих вероятностей вслед за обычным softmax сумма строки перестаёт быть равной единице.
- Определите, позволяет ли маска параллельно обрабатывать позиции токенов при обучении, когда целевые токены известны, при авторегрессионной генерации, в обоих случаях или ни в одном.
- Назовите отдельный механизм, который появится в главе 29, не изменяя нижнетреугольную границу.
Проверьте прогнозы
- Множества равны , и .
- Ни один из предыдущих выходов не изменится; последний ключ и последнее значение доступны только .
- Входы декодера сдвинуты на одну целевую позицию, поэтому на диагонали находится уже известный предыдущий токен, а не предсказываемая цель.
- Единственная вероятность постоянно равна , поэтому оба градиента строго равны нулю.
- Обнуление после softmax удаляет часть вероятностной массы, но не пересчитывает знаменатель по доступному префиксу.
- При обучении строки для известных целевых позиций можно вычислять вместе; при генерации токены по-прежнему добавляются по одному.
- Относительная информация о позициях должна задавать порядок, сохраняя прежнее каузальное правило видимости.
Сохраняйте границу префикса по мере роста декодера
Теперь накопительный декодер создаёт выход в позиции , используя только ключи и значения до позиции включительно. Если функция потерь зависит лишь от первых двух позиций, градиент по будущему суффиксу строго равен нулю:
Именно такая информационная граница нужна авторегрессионному декодеру. В главе 29 появится относительная информация о позициях, но граница не расширится.