← Все главы

27 · Версия материала 2

Вычислите одну голову самовнимания без маски

Разберите, как одна голова самовнимания Transformer без маски сопоставляет запросы с ключами, нормирует каждую строку и смешивает значения на основе проверяемого примера на Rust.

Предскажите, какое значение учтёт каждый запрос

Продолжим непосредственно с результата главы 26. Три проекции дали

Q=[0321],K=[3012],V=[3313].Q=\begin{bmatrix}0&3\\2&-1\end{bmatrix},\quad K=\begin{bmatrix}3&0\\-1&2\end{bmatrix},\quad V=\begin{bmatrix}3&-3\\1&3\end{bmatrix}.

В пакете две позиции токенов. Ширина признаков запросов и ключей, как и ширина значений, равна двум, поэтому Q,K,V1×2×2Q,K,V\in\mathbb{R}^{1\times2\times2}.

До нормировки softmax предскажите, строка какого значения получит больший вес для каждого запроса. Для первого запроса два скалярных произведения равны

q0K=[0,6],q_0K^\top=[0,6],

поэтому больший вес должно получить v1=[1,3]v_1=[1,3]. Для второго запроса получаем

q1K=[6,4],q_1K^\top=[6,-4],

поэтому намного больший вес должно получить v0=[3,3]v_0=[3,-3]. Важны знаки и разности оценок: после нормировки каждая строка станет набором весов внимания.

Вычислите оценки, масштабируйте, нормируйте и смешайте значения

Одна голова внимания без маски на основе масштабированного скалярного произведения вычисляется по формуле

A=softmax(QKdk),O=AVA=\operatorname{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right),\quad O=AV

В разобранном примере матрицы оценок до и после масштабирования равны

QK=[0664],QK2[04.2426414.2426412.828427].QK^\top=\begin{bmatrix}0&6\\6&-4\end{bmatrix},\qquad \frac{QK^\top}{\sqrt{2}}\approx \begin{bmatrix}0&4.242641\\4.242641&-2.828427\end{bmatrix}.

Для каждого запроса softmax независимо нормирует строку по позициям ключей:

A[0.0141660.9858340.9991510.000849],jAij=1.A\approx \begin{bmatrix} 0.014166&0.985834\\ 0.999151&0.000849 \end{bmatrix},\qquad \sum_j A_{ij}=1.

Первая взвешенная смесь значений равна

o0=0.014166[3,3]+0.985834[1,3][1.028332,2.915004].o_0 =0.014166[3,-3]+0.985834[1,3] \approx[1.028332,2.915004].

Вторая равна

o1=0.999151[3,3]+0.000849[1,3][2.998303,2.994908].o_1 =0.999151[3,-3]+0.000849[1,3] \approx[2.998303,-2.994908].

Итак,

O[1.0283322.9150042.9983032.994908].O\approx \begin{bmatrix} 1.028332&2.915004\\ 2.998303&-2.994908 \end{bmatrix}.

Не смешивайте строки запросов со столбцами ключей

  • В QQ каждой позиции токена соответствует строка запроса: какое содержимое ей следует учесть?
  • В KK находятся строки ключей-кандидатов: по каким признакам позицию можно сопоставить с запросом?
  • dkd_k — общая ширина запросов и ключей.
  • В AA каждому запросу соответствует одна нормированная строка весов внимания.
  • В VV находятся строки значений с содержимым для смешивания.
  • В OO каждому запросу соответствует итоговая смесь.
  • BB — размер пакета, TT — число токенов, а dvd_v — ширина значения.

Полное правило для форм имеет вид

Q,KB×T×dk,VB×T×dv,AB×T×T,OB×T×dv.Q,K\in\mathbb{R}^{B\times T\times d_k},\quad V\in\mathbb{R}^{B\times T\times d_v},\quad A\in\mathbb{R}^{B\times T\times T},\quad O\in\mathbb{R}^{B\times T\times d_v}.

При фиксированных элементе пакета bb и позиции запроса ii позиция ключа jj меняется внутри одной строки вероятностей:

Sbij=qbikbjdk,Abij=exp(Sbij)r=0T1exp(Sbir).S_{bij}=\frac{q_{bi}\cdot k_{bj}}{\sqrt{d_k}},\qquad A_{bij}=\frac{\exp(S_{bij})}{\sum_{r=0}^{T-1}\exp(S_{bir})}.

Из-за такой построчной нормировки AbijA_{bij} служит весом внимания. Это не калиброванная вероятность того, что токен или утверждение верны.

Васвани и соавторы обосновывают знаменатель при конкретных предположениях: если компоненты запросов и ключей независимы, имеют нулевое среднее и единичную дисперсию, то дисперсия их скалярного произведения равна dkd_k. При этих предположениях деление на dk\sqrt{d_k} не позволяет дисперсии входа softmax расти вместе с dkd_k. Это не универсальная теорема, не гарантия от переполнения и не доказательство того, что масштаб задаёт оптимальную температуру.

Наглядный эффект виден в примере с двумя координатами. Для оценок [1,0][1,0] вес первой координаты без масштабирования равен

punscaled=e1e1+e00.731059.p_{\mathrm{unscaled}}=\frac{e^1}{e^1+e^0}\approx0.731059.

При dk=2d_k=2 после масштабирования на квадратный корень тот же вес равен

pscaled=e1/2e1/2+e00.669762.p_{\mathrm{scaled}} =\frac{e^{1/\sqrt{2}}}{e^{1/\sqrt{2}}+e^0} \approx0.669762.

В этом примере распределение стало менее резким. Приведённые выше предположения объясняют пользу знаменателя, но не задают резкость любой строки внимания.

От рекуррентного контекста к учёту всех доступных позиций

Это история развития нейронного внимания на пути к современным LLM, а не история языков программирования.

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

Бахданау, Чо и Бенжио, Neural Machine Translation by Jointly Learning to Align and Translate предложили важный промежуточный шаг. Бахданау, Чо и Бенжио описывают возможное узкое место базовой модели энкодера–декодера в виде вектора фиксированной длины и вычисляют каждый новый контекст как взвешенную по softmax сумму аннотаций энкодера, оценки для которых зависят от предыдущего состояния декодера.

Название аддитивное внимание для этой обучаемой функции оценивания — более поздняя ретроспективная классификация, которую используют Васвани и соавторы. В ранней работе нет обозначений запросов, ключей и значений из этой главы, масштабированных скалярных произведений или матрицы оценок внутри одной последовательности.

Transformer объединяет множество запросов в матрицы и с помощью самовнимания на основе масштабированного скалярного произведения сопоставляет позиции одной доступной последовательности, поэтому слой может сформировать всю матрицу оценок пакетными матричными операциями.

Васвани и соавторы, Attention Is All You Need сделали следующий шаг. Васвани и соавторы определяют внимание на основе масштабированного скалярного произведения как нормированную по softmax матрицу масштабированных скалярных произведений запросов и ключей, применяемую к значениям, объединяют одновременно обрабатываемые запросы в матрицу и определяют самовнимание как установление связей между позициями одной последовательности.

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

Одновременное формирование матрицы оценок не делает общий объём работы постоянным и не распараллеливает авторегрессионную генерацию токенов. Важное историческое изменение относится к структуре модели: рекуррентное выравнивание вычисляет новый контекст по мере продвижения декодера, а самовнимание связывает все доступные позиции внутри одного слоя.

Следующий исполняемый пример выделяет различие в том, откуда берутся позиции для запросов и для ключей со значениями:

Сопоставьте источники выравнивания энкодера и декодера с парами запросов и ключей из одной последовательности rust/demos/ch27-self-attention/src/lib.rs#historical-attention-contrast
fn attention_pairs<'a>(queries: &'a [&'a str], keys: &'a [&'a str]) -> Vec<(&'a str, &'a str)> {
    queries
        .iter()
        .flat_map(|query| keys.iter().map(move |key| (*query, *key)))
        .collect()
}

fn historical_attention_contrast() -> HistoryEvidence {
    let decoder_states = ["decoder-state-0", "decoder-state-1"];
    let encoder_annotations = ["encoder-annotation-0", "encoder-annotation-1"];
    let hidden_sequence = ["hidden-position-0", "hidden-position-1"];

    let encoder_decoder_alignment = attention_pairs(&decoder_states, &encoder_annotations);
    let self_attention = attention_pairs(&hidden_sequence, &hidden_sequence);
    assert!(
        encoder_decoder_alignment
            .iter()
            .all(|(query, key)| query.starts_with("decoder") && key.starts_with("encoder"))
    );
    assert!(
        self_attention
            .iter()
            .all(|(query, key)| query.starts_with("hidden") && key.starts_with("hidden"))
    );

    HistoryEvidence {
        earlier: "recurrent-fixed-context",
        bridge: "additive-encoder-decoder-alignment",
        transformer: "scaled-dot-product-self-attention",
        comparison: "all-sequence-positions",
    }
}

Соберите голову из дифференцируемых операций над тензорами

scaled_dot_product_self_attention принимает три объекта TensorValue третьего ранга. Функция последовательно применяет уже реализованные транспонирование, пакетный matmul, умножение на скаляр, устойчивый log_softmax, exp и второй matmul. Результат позволяет проверить оценки до масштабирования, масштабированные оценки, вероятности, выход, коэффициент масштабирования, ширину ключей и ширину значений:

Соберите одну доступную для проверки голову внимания без маски на основе масштабированного скалярного произведения rust/crates/llm-from-scratch/src/attention/self_attention.rs#self-attention-forward
/// Shared, validated score preparation for unmasked and causally masked heads.
#[derive(Clone, Debug)]
pub(crate) struct ScaledSelfAttentionScores {
    pub(crate) raw_scores: TensorValue,
    pub(crate) scaled_scores: TensorValue,
    pub(crate) scale: f64,
    pub(crate) key_width: usize,
    pub(crate) value_width: usize,
}

/// Inspectable evidence from one unmasked attention head.
#[derive(Clone, Debug)]
pub struct SelfAttentionForward {
    raw_scores: TensorValue,
    scaled_scores: TensorValue,
    weights: TensorValue,
    output: TensorValue,
    scale: f64,
    key_width: usize,
    value_width: usize,
}

impl SelfAttentionForward {
    /// The unnormalized matrix `Q K^T` with shape `[batch, tokens, tokens]`.
    pub fn raw_scores(&self) -> &TensorValue {
        &self.raw_scores
    }

    /// Alias that emphasizes that each raw cell is one query-key dot product.
    pub fn dot_products(&self) -> &TensorValue {
        &self.raw_scores
    }

    /// The raw scores divided by the square root of the query/key width.
    pub fn scaled_scores(&self) -> &TensorValue {
        &self.scaled_scores
    }

    /// Row-normalized probabilities over key positions.
    pub fn weights(&self) -> &TensorValue {
        &self.weights
    }

    /// Alias for the row-normalized attention weights.
    pub fn probabilities(&self) -> &TensorValue {
        &self.weights
    }

    /// The weighted value rows with shape `[batch, tokens, value_width]`.
    pub fn output(&self) -> &TensorValue {
        &self.output
    }

    /// The fixed score multiplier `1 / sqrt(key_width)` used by this pass.
    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
    }

    pub fn into_parts(self) -> (TensorValue, TensorValue, TensorValue, TensorValue) {
        (
            self.raw_scores,
            self.scaled_scores,
            self.weights,
            self.output,
        )
    }
}

/// Computes one unmasked scaled dot-product self-attention head.
///
/// Q, K, and V must describe the same batch and token positions. Q and K share
/// one nonzero feature width; V may use a different nonzero output width.
pub fn scaled_dot_product_self_attention(
    query: &TensorValue,
    key: &TensorValue,
    value: &TensorValue,
) -> Result<SelfAttentionForward, SelfAttentionError> {
    let prepared = scaled_self_attention_scores(query, key, value)?;
    let log_weights = prepared
        .scaled_scores
        .log_softmax(2)
        .map_err(autodiff_error(SelfAttentionStage::LogSoftmax))?;
    let weights = log_weights
        .exp()
        .map_err(autodiff_error(SelfAttentionStage::Probabilities))?;
    let output = weights
        .matmul(value)
        .map_err(autodiff_error(SelfAttentionStage::ValueMixture))?;

    Ok(SelfAttentionForward {
        raw_scores: prepared.raw_scores,
        scaled_scores: prepared.scaled_scores,
        weights,
        output,
        scale: prepared.scale,
        key_width: prepared.key_width,
        value_width: prepared.value_width,
    })
}

pub(crate) fn scaled_self_attention_scores(
    query: &TensorValue,
    key: &TensorValue,
    value: &TensorValue,
) -> Result<ScaledSelfAttentionScores, SelfAttentionError> {
    let query_shape = query.shape();
    let key_shape = key.shape();
    let value_shape = value.shape();

    for (input, shape) in [
        (SelfAttentionInput::Query, query_shape.as_slice()),
        (SelfAttentionInput::Key, key_shape.as_slice()),
        (SelfAttentionInput::Value, value_shape.as_slice()),
    ] {
        if shape.len() != 3 {
            return Err(SelfAttentionError::InputRank {
                input,
                rank: shape.len(),
            });
        }
    }

    if query_shape[0] != key_shape[0] || query_shape[0] != value_shape[0] {
        return Err(SelfAttentionError::BatchMismatch {
            query: query_shape[0],
            key: key_shape[0],
            value: value_shape[0],
        });
    }
    if query_shape[1] != key_shape[1] || query_shape[1] != value_shape[1] {
        return Err(SelfAttentionError::TokenMismatch {
            query: query_shape[1],
            key: key_shape[1],
            value: value_shape[1],
        });
    }
    if query_shape[1] == 0 {
        return Err(SelfAttentionError::EmptyTokens);
    }
    if query_shape[2] == 0 {
        return Err(SelfAttentionError::EmptyFeatureWidth {
            input: SelfAttentionInput::Query,
        });
    }
    if key_shape[2] == 0 {
        return Err(SelfAttentionError::EmptyFeatureWidth {
            input: SelfAttentionInput::Key,
        });
    }
    if query_shape[2] != key_shape[2] {
        return Err(SelfAttentionError::QueryKeyWidthMismatch {
            query: query_shape[2],
            key: key_shape[2],
        });
    }
    if value_shape[2] == 0 {
        return Err(SelfAttentionError::EmptyFeatureWidth {
            input: SelfAttentionInput::Value,
        });
    }

    let key_transposed = key
        .transpose(1, 2)
        .map_err(autodiff_error(SelfAttentionStage::KeyTranspose))?;
    let raw_scores = query
        .matmul(&key_transposed)
        .map_err(autodiff_error(SelfAttentionStage::RawScores))?;

    let scale = 1.0 / (query_shape[2] as f64).sqrt();
    let scale_tensor =
        Tensor::from_vec(Vec::new(), vec![scale]).map_err(|source| SelfAttentionError::Tensor {
            stage: SelfAttentionStage::ScaleTensor,
            source,
        })?;
    let scale_value = TensorValue::constant(scale_tensor)
        .map_err(autodiff_error(SelfAttentionStage::ScaleTensor))?;
    let scaled_scores = raw_scores
        .mul(&scale_value)
        .map_err(autodiff_error(SelfAttentionStage::ScaledScores))?;
    Ok(ScaledSelfAttentionScores {
        raw_scores,
        scaled_scores,
        scale,
        key_width: query_shape[2],
        value_width: value_shape[2],
    })
}

Тензоры запросов, ключей и значений должны иметь третий ранг, а размеры пакета и оси токенов должны совпадать. Ось токенов и все оси признаков должны быть непустыми. Ширины запросов и ключей должны совпадать, но dvd_v может отличаться от dkd_k. Пустой пакет допустим. Типизированные ошибки сохраняют сведения о ранге входа, размерах пакета и оси токенов, пустой оси токенов, ширине признаков и этапе прямого прохода:

Отклоните недопустимые входы самовнимания, не возвращая незавершённый результат rust/crates/llm-from-scratch/src/attention/self_attention.rs#self-attention-errors
/// One of the three inputs to a self-attention head.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SelfAttentionInput {
    Query,
    Key,
    Value,
}

impl fmt::Display for SelfAttentionInput {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Query => "query",
            Self::Key => "key",
            Self::Value => "value",
        })
    }
}

/// The forward stage at which a cumulative tensor operation failed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SelfAttentionStage {
    KeyTranspose,
    RawScores,
    ScaleTensor,
    ScaledScores,
    LogSoftmax,
    Probabilities,
    ValueMixture,
}

impl fmt::Display for SelfAttentionStage {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::KeyTranspose => "key transpose",
            Self::RawScores => "raw query-key scores",
            Self::ScaleTensor => "score scale",
            Self::ScaledScores => "scaled query-key scores",
            Self::LogSoftmax => "row log-softmax",
            Self::Probabilities => "attention probabilities",
            Self::ValueMixture => "weighted value mixture",
        })
    }
}

/// A rejected Q/K/V shape or cumulative tensor operation.
#[derive(Clone, Debug, PartialEq)]
pub enum SelfAttentionError {
    InputRank {
        input: SelfAttentionInput,
        rank: usize,
    },
    BatchMismatch {
        query: usize,
        key: usize,
        value: usize,
    },
    TokenMismatch {
        query: usize,
        key: usize,
        value: usize,
    },
    QueryKeyWidthMismatch {
        query: usize,
        key: usize,
    },
    EmptyTokens,
    EmptyFeatureWidth {
        input: SelfAttentionInput,
    },
    Tensor {
        stage: SelfAttentionStage,
        source: TensorError,
    },
    Autodiff {
        stage: SelfAttentionStage,
        source: TensorAutodiffError,
    },
}

impl fmt::Display for SelfAttentionError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InputRank { input, rank } => write!(
                formatter,
                "self-attention {input} must have rank three [batch, tokens, features], got rank {rank}"
            ),
            Self::BatchMismatch { query, key, value } => write!(
                formatter,
                "self-attention batch sizes must match, got query {query}, key {key}, value {value}"
            ),
            Self::TokenMismatch { query, key, value } => write!(
                formatter,
                "unmasked self-attention token counts must match, got query {query}, key {key}, value {value}"
            ),
            Self::QueryKeyWidthMismatch { query, key } => write!(
                formatter,
                "self-attention query and key widths must match, got query {query}, key {key}"
            ),
            Self::EmptyTokens => formatter.write_str(
                "unmasked self-attention needs at least one token so every probability row has a key",
            ),
            Self::EmptyFeatureWidth { input } => write!(
                formatter,
                "self-attention {input} needs a nonzero feature width"
            ),
            Self::Tensor { stage, source } => {
                write!(formatter, "self-attention {stage}: {source}")
            }
            Self::Autodiff { stage, source } => {
                write!(formatter, "self-attention {stage}: {source}")
            }
        }
    }
}

impl Error for SelfAttentionError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Tensor { source, .. } => Some(source),
            Self::Autodiff { source, .. } => Some(source),
            _ => None,
        }
    }
}

fn autodiff_error(
    stage: SelfAttentionStage,
) -> impl FnOnce(TensorAutodiffError) -> SelfAttentionError {
    move |source| SelfAttentionError::Autodiff { stage, source }
}

В примере обратного прохода черта сверху обозначает градиент в обратном режиме:

Xˉ=LX.\bar X=\frac{\partial L}{\partial X}.

Зададим входящий градиент по выходу

Oˉ=[1001],\bar O=\begin{bmatrix}1&0\\0&1\end{bmatrix},

который задаёт скалярную целевую функцию

L=O,Oˉ=O00+O111.966576.L=\langle O,\bar O\rangle=O_{00}+O_{11}\approx-1.966576.

Сначала обратный проход через матричное умножение, вычисляющее выход, даёт

Vˉb=Ab𝖳Oˉb,Aˉb=OˉbVb𝖳.\bar V_b=A_b^{\mathsf T}\bar O_b,\qquad \bar A_b=\bar O_bV_b^{\mathsf T}.

Для каждой строки запроса правило обратного прохода через softmax имеет вид

Sˉbij=Abij(AˉbijrAbirAˉbir).\bar S_{bij} =A_{bij}\left(\bar A_{bij}-\sum_r A_{bir}\bar A_{bir}\right).

Затем масштабированные скалярные произведения передают градиенты запросам и ключам:

Qˉb=SˉbKbdk,Kˉb=Sˉb𝖳Qbdk.\bar Q_b=\frac{\bar S_bK_b}{\sqrt{d_k}},\qquad \bar K_b=\frac{\bar S_b^{\mathsf T}Q_b}{\sqrt{d_k}}.

Здесь 𝖳\mathsf T означает транспонирование последних двух осей отдельно внутри каждого элемента пакета.

Для разобранных входов эти уравнения дают

Qˉ[0.0790000.0395000.0143890.007195],\bar Q\approx \begin{bmatrix} 0.079000&-0.039500\\ -0.014389&0.007195 \end{bmatrix}, Kˉ[0.0071950.0628470.0071950.062847],\bar K\approx \begin{bmatrix} -0.007195&0.062847\\ 0.007195&-0.062847 \end{bmatrix}, Vˉ[0.0141660.9991510.9858340.000849].\bar V\approx \begin{bmatrix} 0.014166&0.999151\\ 0.985834&0.000849 \end{bmatrix}.

Все четыре координаты каждого входа совпадают с центральными разностями при шаге 10610^{-6} и допуске 2×1062\times10^{-6}. Одинаковые ключи дают одинаковые веса; у единственного токена вес единственного значения равен единице; одинаковая перестановка QQ, KK и VV так же переставляет выход; элементы пакета остаются независимыми. Для пустого пакета тензоры Q,KQ,K имеют форму [0,2,2][0,2,2], а VV — форму [0,2,3][0,2,3]; результатом служат AA формы [0,2,2][0,2,2] и OO формы [0,2,3][0,2,3]. Ширина значений может отличаться от ширины запросов и ключей: значения формы [1,2,1][1,2,1] дают выход формы [1,2,1][1,2,1].

Полный пример объединяет данные прямого и обратного проходов, масштабирования, форм, границ и воспроизводимости:

Соберите данные разобранного примера самовнимания из ранее реализованных операций над тензорами rust/demos/ch27-self-attention/src/lib.rs#self-attention-fixture
pub fn learner_evidence() -> Result<LearnerEvidence, FixtureError> {
    let primary = primary_once()?;
    let replay = primary_once()?;
    let (query_checks, key_checks, value_checks, gradcheck_passed) = gradient_evidence(&primary)?;
    Ok(LearnerEvidence {
        replay_bitwise: primary == replay,
        scale: scale_evidence()?,
        single_token: single_token_evidence()?,
        shapes: shape_evidence()?,
        errors: error_evidence()?,
        history: historical_attention_contrast(),
        primary,
        query_checks,
        key_checks,
        value_checks,
        gradcheck_passed,
    })
}
Запустите полный пример самовнимания без маски rust/demos/ch27-self-attention/src/main.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let evidence = ch27_self_attention::learner_evidence()?;
    print!("{}", ch27_self_attention::render_report(&evidence));
    Ok(())
}

Команда cargo run --quiet --locked -p ch27-self-attention выводит те же оценки, вероятности, смеси, градиенты, формы и результаты проверки границ.

Проследите путь каждой строки внимания от оценок до выхода

Диаграмма проводит точные входы через оценки до и после масштабирования, строки вероятностей, взвешенные слагаемые значений, выходы, градиенты, формы, отклонённые входы и исторический переход к самовниманию:

Проследите путь каждой оценки до взвешенной смеси значений

Проследите точные запросы, ключи, значения, оценки, вероятности, смеси, градиенты, формы, исторические этапы и отклонённые недопустимые входы одной головы внимания без маски.

  • Сплошная рамка запроса
  • Штриховая рамка ключа
  • Двойная рамка значения
  • Пунктирная рамка оценки
  • Двойная рамка вероятности

Проследите полный расчёт внимания без маски

Строки и столбцы точно сохраняют расчёт для двух токенов; рамки и подписи передают все различия без опоры на цвет.

Начните со строк запросов, ключей и значений
  1. Строки запросов: какое содержимое следует учесть для каждой позиции?

    QQ

    00 11
    q0q_0 0.0000000.0000003.0000003.000000
    q1q_1 2.0000002.0000001.000000-1.000000

    Форма: [1,2,2][1,2,2]

  2. Строки ключей: по каким признакам сопоставлять позиции?

    KK

    00 11
    k0k_0 3.0000003.0000000.0000000.000000
    k1k_1 1.000000-1.0000002.0000002.000000

    Форма: [1,2,2][1,2,2]

  3. Строки значений: какое содержимое может внести каждая позиция?

    VV

    00 11
    v0v_0 3.0000003.0000003.000000-3.000000
    v1v_1 1.0000001.0000003.0000003.000000

    Форма: [1,2,2][1,2,2]

Сопоставьте каждый запрос с каждым ключом

QKQK^\top

k0k_0 k1k_1
q0q_0 0.0000000.0000006.0000006.000000
q1q_1 6.0000006.0000004.000000-4.000000
Масштабируйте матрицу оценок

S=QK/dkS=QK^\top/\sqrt{d_k}

k0k_0 k1k_1
q0q_0 0.0000000.0000004.2426414.242641
q1q_1 4.2426414.2426412.828427-2.828427
Масштаб внимания
1/dk=0.7071071/\sqrt{d_k}=0.707107
Форма
[1,2,2][1,2,2]
Нормируйте каждую строку запроса по ключам
  1. q0q_0

    A0,:=[0.014166,0.985834]A_{0,:}=[0.014166,0.985834]

    Проверка суммы строки вероятностей
    jA0j=1.000000\sum_j A_{0j}=1.000000
    Ось нормировки
    key
  2. q1q_1

    A1,:=[0.999151,0.000849]A_{1,:}=[0.999151,0.000849]

    Проверка суммы строки вероятностей
    jA1j=1.000000\sum_j A_{1j}=1.000000
    Ось нормировки
    key
Смешайте строки значений в выходные строки
  1. o0o_0

    Нормируйте каждую строку запроса по ключам
    [0.014166,0.985834][0.014166,0.985834]
    Слагаемые после умножения на веса
    [0.042498,0.042498][0.042498,-0.042498] +[0.985834,2.957502]+[0.985834,2.957502]
    Выходная строка
    o0=[1.028332,2.915004]o_0=[1.028332,2.915004]
  2. o1o_1

    Нормируйте каждую строку запроса по ключам
    [0.999151,0.000849][0.999151,0.000849]
    Слагаемые после умножения на веса
    [2.997454,2.997454][2.997454,-2.997454] +[0.000849,0.002546]+[0.000849,0.002546]
    Выходная строка
    o1=[2.998303,2.994908]o_1=[2.998303,-2.994908]

Проверьте градиенты, формы и отклонение недопустимых входов

Градиенты, формы и граничные случаи показывают, что сохраняет и что отклоняет одна и та же операция внимания.

Данные обратного прохода
Oˉ\bar O [1.000000,0.000000,0.000000,1.000000][1.000000,0.000000,0.000000,1.000000]
Qˉ\bar Q [0.079000,0.039500,0.014389,0.007195][0.079000,-0.039500,-0.014389,0.007195]
Kˉ\bar K [0.007195,0.062847,0.007195,0.062847][-0.007195,0.062847,0.007195,-0.062847]
Vˉ\bar V [0.014166,0.999151,0.985834,0.000849][0.014166,0.999151,0.985834,0.000849]
Независимость элементов пакета и формы
Q,K,VQ,K,V
[2,2,2][2,2,2]
AA
[2,2,2][2,2,2]
OO
[2,2,2][2,2,2]
Независимость элементов пакета
Проверено
У одного токена нет другого ключа для сравнения

A=[1.000000]A=[1.000000]

O=[5.000000,2.000000]O=[5.000000,-2.000000]

Градиент запроса: Qˉ=0\bar Q=0

Градиент ключа: Kˉ=0\bar K=0

Граница видимости

Видны все позиции ключей

dk=2,dv=2d_k=2,\quad d_v=2

Отклонённые недопустимые входы
  • Отклонено Вид ошибки: input-rank Во входе запросов должны быть явно заданы оси пакета, токенов и признаков. Недопустимая форма входа: operand=query|rank=2
  • Отклонено Вид ошибки: batch-mismatch У тензоров запросов, ключей и значений должен совпадать размер пакета. Недопустимая форма входа: query=1|key=2|value=1
  • Отклонено Вид ошибки: token-mismatch У тензоров запросов, ключей и значений должно совпадать число токенов. Недопустимая форма входа: query=2|key=3|value=2
  • Отклонено Вид ошибки: empty-token-axis Для нормировки каждой строки вниманию нужна хотя бы одна позиция ключа. Недопустимая форма входа: tokens=0
  • Отклонено Вид ошибки: query-key-width-mismatch Для скалярного произведения ширины признаков запросов и ключей должны совпадать. Недопустимая форма входа: query=2|key=3
Численные проверки
Проверка суммы строки вероятностей
0.0000000000010.000000000001
Проверка градиентов
gradcheck=true
Проверенные координаты градиентов
4+4+44+4+4
Допуск проверки градиентов
0.0000020.000002
Точная воспроизводимость
replay=bitwise

Проследите развитие нейронного внимания на пути к современным LLM

Сравнивается устройство моделей, а не история языков программирования или производительность оборудования.

  1. Фиксированный рекуррентный контекст

    Один вектор исходной последовательности фиксированного размера повторно используется на всех рекуррентных шагах декодера.

  2. Аддитивное выравнивание энкодера и декодера

    На каждом шаге декодер получает новый взвешенный контекст из аннотаций энкодера.

  3. Самовнимание на основе масштабированного скалярного произведения

    Один слой формирует оценку для каждой пары позиций запроса и ключа в доступной последовательности.

    В этой голове без маски каждому запросу разрешены все доступные позиции ключей.

Сначала прочитайте строку вероятностей вдоль позиций ключей, затем проследите, как она превращается в соответствующие взвешенные слагаемые значений. Сплошные, штриховые, двойные и пунктирные рамки различают запросы, ключи, значения, оценки и вероятности без опоры на цвет. Пока видны все позиции; глава 28 добавит каузальную границу.

Сначала сделайте предсказания

  1. Вычислите все четыре элемента QKQK^\top для разобранного примера.
  2. До применения softmax предскажите, какой ключ получит больший вес для каждого запроса.
  3. Объясните, почему сумма каждой строки AA, а не каждого столбца, равна единице.
  4. Предскажите форму выхода для Q,K4×7×3Q,K\in\mathbb{R}^{4\times7\times3} и V4×7×5V\in\mathbb{R}^{4\times7\times5}.
  5. Предскажите вероятность внимания и выход для одного токена со значением [5,2][5,-2].
  6. Определите, что произойдёт, если одинаково переставить две позиции токенов в QQ, KK и VV.
  7. Предскажите вероятности для двух одинаковых строк ключей.
  8. Укажите, какой доступ к будущему привёл бы к утечке целевой информации при каузальном обучении декодера.
  9. Сопоставьте рекуррентное выравнивание со структурой матрицы оценок самовнимания.
Проверить предсказания
  1. Строки оценок равны [0,6][0,6] и [6,4][6,-4].
  2. Запрос с номером ноль отдаёт больший вес ключу один, а запрос один — ключу ноль.
  3. Каждый запрос выбирает среди позиций ключей, поэтому softmax нормирует его ось ключей.
  4. Форма выхода равна [4,7,5][4,7,5].
  5. Единственная вероятность равна [1][1], а выход — [5,2][5,-2].
  6. Совместная перестановка строк QQ, KK и VV так же переставляет строки выхода. Входы, зависящие от позиции, и маску нужно рассматривать отдельно.
  7. Одинаковые оценки дают равномерную строку [0.5,0.5][0.5,0.5].
  8. Запрос может использовать более поздние целевые позиции, потому что у этой головы нет маски; при авторегрессионном обучении такой доступ привёл бы к утечке целевой информации.
  9. Рекуррентное выравнивание вычисляет новый контекст по мере продвижения декодера, а самовнимание устанавливает связи между всеми доступными позициями внутри одного слоя. Это не делает общий объём работы постоянным и не распараллеливает авторегрессионную генерацию.

Далее скройте будущие ключи

Накопительный декодер теперь преобразует одну тройку спроецированных тензоров запросов, ключей и значений в выход головы внимания без маски. В главе 28 будущие позиции ключей будут исключены до нормировки каждой строки оценок.

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