← Все главы

18 · Версия материала 7

Сопоставьте ID токенов с обучаемыми векторами

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

Предскажите три выбранные строки и один общий градиент

Возьмём обучаемую таблицу из четырёх строк словаря, по 22 признака в каждой:

E=[1011202130314041]E=\begin{bmatrix} 10 & 11 \\ 20 & 21 \\ 30 & 31 \\ 40 & 41 \end{bmatrix}

До запуска прочитайте ID [[2,1,2]][[2,1,2]] слева направо. Первая и третья позиции выбирают строку 22, а средняя — строку 11. Значит, ожидаемый результат равен [[[30,31],[20,21],[30,31]]][[[30,31],[20,21],[30,31]]] и имеет форму [1,3,2][1,3,2]. Повторение копирует значение в две позиции, но не создаёт вторую обучаемую строку.

One-hot-обозначение наглядно показывает этот выбор. Например, ID 22 соответствует [0,0,1,0][0,0,1,0]. При умножении такого индикатора на EE остаётся [30,31][30,31]. Программа явно создаёт весь индикатор, включая нулевые значения, только в исполняемом сравнении с one-hot-вектором:

Умножьте явные one-hot-строки на маленькую таблицу как алгебраический эталон rust/demos/ch18-token-embeddings/src/lib.rs#one-hot-baseline
/// Materializes the tiny historical algebraic baseline for comparison only.
pub fn explicit_one_hot_product(table: &Tensor, token_ids: &[u32]) -> (Vec<Vec<u8>>, Vec<f64>) {
    assert_eq!(table.shape().len(), 2);
    let vocabulary_size = table.shape()[0];
    let width = table.shape()[1];
    let mut indicators = Vec::with_capacity(token_ids.len());
    let mut output = Vec::with_capacity(token_ids.len() * width);

    for &token_id in token_ids {
        let selected = usize::try_from(token_id).expect("u32 token ID must fit usize");
        assert!(selected < vocabulary_size);
        let mut one_hot = vec![0_u8; vocabulary_size];
        one_hot[selected] = 1;
        for feature in 0..width {
            let value = one_hot
                .iter()
                .enumerate()
                .map(|(row, &active)| f64::from(active) * table.as_slice()[row * width + feature])
                .sum();
            output.push(value);
        }
        indicators.push(one_hot);
    }
    (indicators, output)
}

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

Выберите строки по повторяющимся ID и сравните точный результат с эталоном rust/demos/ch18-token-embeddings/src/lib.rs#known-token-lookup
    let embedding = known_embedding();
    let output = embedding.forward(&TOKEN_IDS, &TOKEN_SHAPE)?;
    let (_, one_hot_output) = explicit_one_hot_product(&known_table(), &TOKEN_IDS);
    let one_hot_matches = output.value().as_slice() == one_hot_output;

Выбирайте строки в прямом проходе и накапливайте вклады в обратном

Полное правило выбора строки и обратного прохода имеет вид:

Xb,t,:=Ezb,t,:,Eˉi,:=(b,t):zb,t=iXˉb,t,:X_{b,t,:}=E_{z_{b,t},:},\quad \bar{E}_{i,:}=\sum_{(b,t):z_{b,t}=i}\bar{X}_{b,t,:}

В прямом проходе в каждую выходную позицию копируется одна выбранная строка. Обозначим скалярную функцию потерь через LL. Черта сверху — краткое обозначение сопряжённой величины обратного режима: Xˉb,t,:=L/Xb,t,:\bar{X}_{b,t,:}=\partial L/\partial X_{b,t,:} — градиент, приходящий в выходную позицию, а Eˉi,:=L/Ei,:\bar{E}_{i,:}=\partial L/\partial E_{i,:} — градиент строки ii таблицы. При обратном проходе градиенты передаются по тем же связям от выходных позиций к выбранным строкам. Если несколько позиций выбрали одну строку, поступившие в эти позиции векторы градиента складываются по признакам в общей строке параметров.

При заданной начальной сопряжённой величине [[[1,0],[0,2],[3,4]]][[[1,0],[0,2],[3,4]]] строка 11 получает [0,2][0,2], а строка 22[1,0]+[3,4]=[4,4][1,0]+[3,4]=[4,4]. Неиспользованные строки 00 и 33 получают нули. Градиента по ID токенов нет: это дискретные селекторы, а не операнды ленты.

Не смешивайте оси словаря и признаков

  • EE — обучаемая таблица токенов формы [V,d][V,d].
  • VV — размер словаря и, следовательно, число строк в EE.
  • dd — ширина эмбеддинга и, следовательно, число признаков в каждой строке.
  • zb,tz_{b,t} — целочисленный ID токена в элементе пакета bb и позиции последовательности tt.
  • bb обозначает один элемент пакета, а tt — одну позицию его последовательности.
  • :: означает все координаты на последней оси признаков.
  • Xb,t,:X_{b,t,:} — вектор ширины dd, выбранный в позиции (b,t)(b,t).
  • Xˉb,t,:=L/Xb,t,:\bar{X}_{b,t,:}=\partial L/\partial X_{b,t,:} — вектор входящей сопряжённой величины в этой выходной позиции.
  • Eˉi,:=L/Ei,:\bar{E}_{i,:}=\partial L/\partial E_{i,:} — градиент строки таблицы после накопления вкладов всех совпадающих позиций.
  • ii — индекс одной строки словаря.
  • Сумма перебирает все (b,t)(b,t), для которых zb,tz_{b,t} равно ii.

Числовое расстояние между ID не является смысловым расстоянием. ID 11 и 22 соседствуют только в нумерации токенизатора; станут ли соответствующие обучаемые векторы похожими, зависит от обучения.

От разреженного one-hot-кода к векторам на входе Transformer

Разреженное one-hot-представление слова отводит одну координату каждому элементу словаря, но не выражает степень сходства между словами. Кроме того, явно создавать и обрабатывать такой вектор размером со словарь расточительно, когда нужна лишь одна строка.

Bengio et al., A Neural Probabilistic Language Model: Bengio и соавторы задают отображение индекса словарного слова в набор распределённых признаков с помощью обучаемой матрицы: каждому элементу словаря соответствует строка, а каждому обучаемому признаку — столбец. Одна и та же матрица используется для всех позиций контекста и обучается вместе с моделью предсказания следующего слова.

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

Vaswani et al., Attention Is All You Need: Vaswani и соавторы используют для токенов BPE или WordPiece обучаемые эмбеддинги, ширина которых совпадает с шириной модели, и перед стеком Transformer добавляют к ним позиционное кодирование. Масштабирование эмбеддингов в прямом проходе не относится к инициализации параметров.

ID токенов поступают на числовой вход декодера: по ним выбираются строки одной обучаемой таблицы «словарь на признаки». Все вхождения повторяющегося ID используют одну и ту же строку параметров, поэтому их градиентные вклады при обратном проходе складываются. Позиционная информация, масштабирование эмбеддингов в прямом проходе, внимание и совместное использование весов с выходной проекцией рассматриваются позже.

One-hot-векторы явно указывают выбранный токен, но имеют размер словаря и почти целиком состоят из нулей. Плотные обучаемые признаки позволяют нейросетевым языковым моделям использовать статистические сведения об одних словах при оценке других. В Transformer обучаемые эмбеддинги служат числовыми представлениями токенов на входе последующих слоёв обработки последовательности. Алгебраическое тождество для one-hot-вектора объясняет прямой выбор строки, а общая обучаемая строка — сложение градиентов повторяющихся токенов.

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

Проверьте ID токенов один раз и повторно используйте правило выбора строк

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

Сделайте ошибки создания эмбеддинга и проверки селекторов типизированными и детерминированными rust/crates/llm-from-scratch/src/nn/embedding.rs#embedding-errors
/// A rejected embedding table, token layout, selector, or delegated operation.
#[derive(Clone, Debug, PartialEq)]
pub enum EmbeddingError {
    Initialization(InitializationError),
    Autodiff(TensorAutodiffError),
    TableRank {
        rank: usize,
    },
    EmptyVocabulary,
    ZeroEmbeddingWidth,
    TokenShape(TensorError),
    TokenCountMismatch {
        expected: usize,
        actual: usize,
    },
    TokenIdOutOfBounds {
        position: usize,
        id: u32,
        vocabulary_size: usize,
    },
    IndexAllocationFailed {
        elements: usize,
    },
}

impl fmt::Display for EmbeddingError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Initialization(error) => error.fmt(formatter),
            Self::Autodiff(error) => error.fmt(formatter),
            Self::TableRank { rank } => {
                write!(
                    formatter,
                    "embedding table must have rank two, got rank {rank}"
                )
            }
            Self::EmptyVocabulary => {
                formatter.write_str("embedding vocabulary must contain at least one row")
            }
            Self::ZeroEmbeddingWidth => {
                formatter.write_str("embedding width must be greater than zero")
            }
            Self::TokenShape(error) => write!(formatter, "invalid token-ID shape: {error}"),
            Self::TokenCountMismatch { expected, actual } => write!(
                formatter,
                "token-ID shape needs {expected} IDs, but received {actual}"
            ),
            Self::TokenIdOutOfBounds {
                position,
                id,
                vocabulary_size,
            } => write!(
                formatter,
                "token ID {id} at flat position {position} is out of bounds for vocabulary size {vocabulary_size}"
            ),
            Self::IndexAllocationFailed { elements } => write!(
                formatter,
                "could not reserve {elements} converted embedding indices"
            ),
        }
    }
}

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

impl From<InitializationError> for EmbeddingError {
    fn from(error: InitializationError) -> Self {
        Self::Initialization(error)
    }
}

impl From<TensorAutodiffError> for EmbeddingError {
    fn from(error: TensorAutodiffError) -> Self {
        Self::Autodiff(error)
    }
}

Из внешнего кода получить Embedding можно тремя способами: вызвать new, вызвать from_parameter или клонировать уже проверенный слой. Оба конструктора гарантируют, что таблица имеет ранг два и ненулевые размеры, клон сохраняет эту таблицу, а закрытые поля не позволяют заменить её извне. При создании слоя размер словаря VV и ширина признаков dd сохраняются отдельно:

Создайте одну именованную таблицу ранга два и сохраните размер словаря и ширину признаков rust/crates/llm-from-scratch/src/nn/embedding.rs#embedding-layer
/// One named trainable `[vocabulary_size, embedding_width]` token table.
#[derive(Debug)]
pub struct Embedding {
    table: NamedParameter,
    vocabulary_size: usize,
    embedding_width: usize,
}

impl Clone for Embedding {
    /// Clones the layer handle while preserving the table's tape-leaf identity.
    fn clone(&self) -> Self {
        Self {
            table: self.table.clone(),
            vocabulary_size: self.vocabulary_size,
            embedding_width: self.embedding_width,
        }
    }
}

impl Embedding {
    /// Initializes one table transactionally with Chapter 17's shape-based policy.
    ///
    /// The complete parameter name is used as supplied. Validation checks the
    /// name before the vocabulary and width, and every error preserves `rng`.
    pub fn new(
        parameter_name: impl Into<String>,
        vocabulary_size: usize,
        embedding_width: usize,
        rng: &mut SplitMix64,
    ) -> Result<Self, EmbeddingError> {
        let mut trial = rng.clone();
        let table = NamedParameter::xavier_uniform(
            parameter_name,
            vocabulary_size,
            embedding_width,
            &mut trial,
        )
        .map_err(|error| match error {
            InitializationError::ZeroFanIn => EmbeddingError::EmptyVocabulary,
            InitializationError::ZeroFanOut => EmbeddingError::ZeroEmbeddingWidth,
            other => EmbeddingError::Initialization(other),
        })?;
        let embedding = Self::from_parameter(table)?;
        *rng = trial;
        Ok(embedding)
    }

    /// Gives embedding semantics to an existing named trainable rank-two table.
    pub fn from_parameter(table: NamedParameter) -> Result<Self, EmbeddingError> {
        let shape = table.tensor().shape();
        if shape.len() != 2 {
            return Err(EmbeddingError::TableRank { rank: shape.len() });
        }
        if shape[0] == 0 {
            return Err(EmbeddingError::EmptyVocabulary);
        }
        if shape[1] == 0 {
            return Err(EmbeddingError::ZeroEmbeddingWidth);
        }
        Ok(Self {
            table,
            vocabulary_size: shape[0],
            embedding_width: shape[1],
        })
    }
}

Embedding::forward — публичная точка входа для ID токенов, поступающих как значения типа u32. К моменту вызова форма таблицы [V,d][V,d] уже проверена. Метод выполняет остальные проверки в неизменном порядке:

  1. вычисляет число позиций, заданное token_shape, и сообщает об ошибке формы или переполнении при вычислении;
  2. требует, чтобы длина token_ids в точности совпадала с этим числом; и
  3. просматривает ID в плоском порядке и сообщает о первом значении u32, которое нельзя представить как usize или которое не попадает в диапазон строк 0i<V0 \le i < V.

Только после успешной проверки всех ID метод резервирует Vec<usize> и преобразует селекторы. Затем лента проверяет, что операнд с таблицей ещё доступен. Если он доступен, метод создаёт значение закрытого типа RowGatherPlan из главы 16 и передаёт ему вектор селекторов во владение; этот тип доступен только внутри крейта. Полученный проверенный план выбора строк по индексам владеет преобразованными селекторами и их логической формой, а форму выхода вычисляет с проверкой переполнения. Внутренний конструктор не проверяет повторно ранг таблицы, число селекторов и их границы. Ранг таблицы гарантируют конструкторы Embedding; при клонировании этот инвариант сохраняется. Текущий вызов forward уже проверил логическую форму, число и границы селекторов:

Один раз проверьте форму и ID токенов и передайте преобразованные селекторы во владение проверенному плану выбора строк по индексам rust/crates/llm-from-scratch/src/nn/embedding.rs#embedding-forward-boundary
impl Embedding {
    /// Selects one table row per `u32` token ID and appends the feature axis.
    ///
    /// Token IDs remain integer selectors rather than differentiable operands.
    /// After this boundary validates and converts them, the shared gather kernel
    /// consumes the sealed facts without scanning the selectors again.
    pub fn forward(
        &self,
        token_ids: &[u32],
        token_shape: &[usize],
    ) -> Result<TensorValue, EmbeddingError> {
        let (_, expected) =
            checked_row_major_layout(token_shape).map_err(EmbeddingError::TokenShape)?;
        if token_ids.len() != expected {
            return Err(EmbeddingError::TokenCountMismatch {
                expected,
                actual: token_ids.len(),
            });
        }

        for (position, &id) in token_ids.iter().enumerate() {
            let valid = usize::try_from(id)
                .ok()
                .is_some_and(|index| index < self.vocabulary_size);
            if !valid {
                return Err(EmbeddingError::TokenIdOutOfBounds {
                    position,
                    id,
                    vocabulary_size: self.vocabulary_size,
                });
            }
        }

        let mut indices = Vec::new();
        indices
            .try_reserve_exact(expected)
            .map_err(|_| EmbeddingError::IndexAllocationFailed { elements: expected })?;
        for &id in token_ids {
            indices.push(usize::try_from(id).expect("validated u32 token ID must fit usize"));
        }
        self.table
            .tensor()
            .gather_rows_with_plan(move |table| {
                RowGatherPlan::from_validated_indices(table, indices, token_shape.to_vec())
            })
            .map_err(EmbeddingError::Autodiff)
    }
}

Повторно использовать результаты этих проверок без новой проверки может только внутренний код, получивший проверенный план выбора строк по индексам. Публичный метод TensorValue::gather_rows по-прежнему принимает произвольные входные данные, поэтому сам проверяет ранг таблицы, форму и число селекторов, а также первый индекс за пределами таблицы. Наличие плана ещё не означает, что память уже выделена: создание буфера выходных значений всё равно может завершиться ошибкой. После успешного выделения памяти общее ядро копирует строки и сохраняет те же данные для обратного прохода. Поэтому слой избегает второго просмотра селекторов, но не дублирует ни выбор строк, ни VJP. Изменились только владение проверенными данными и их повторное использование. Значения и формы выхода, порядок ошибок, формула выбора строк, данные, сохранённые для VJP, и накопление градиентов повторяющихся ID остались прежними.

Точная неоднородная начальная сопряжённая величина делает накопление повторяющихся вкладов наблюдаемым:

Проведите обратный проход через повторяющийся выбор и прочитайте сохранённый градиент таблицы rust/demos/ch18-token-embeddings/src/lib.rs#repeated-token-gradient
    let upstream = Tensor::from_vec(vec![1, 3, 2], UPSTREAM_VALUES.to_vec())?;
    output.backward_with_seed(&upstream.view(), GraphRetention::Retain)?;
    let table_gradient = embedding
        .table()
        .tensor()
        .gradient_snapshot()
        .expect("trainable table stores its gradient");

При создании слоя используется матричный инициализатор из главы 17, которому VV и dd передаются как два размера таблицы. Это явное соглашение об инициализации: оно не следует ни из операции выбора строки, ни из работы о Transformer. Другая реализация может выбрать иной инициализатор, не меняя уравнений прямого и обратного прохода. Повторный запрос с тем же начальным значением генератора воспроизводит значения, но отдельно созданные слои остаются разными листьями. Клонирование слоя намеренно сохраняет идентичность параметра:

Инициализируйте таблицу воспроизводимым образом и проверьте сохранение идентичности параметра при клонировании rust/demos/ch18-token-embeddings/src/lib.rs#initialized-token-embedding
    let mut first_rng = SplitMix64::from_seed(18);
    let mut second_rng = SplitMix64::from_seed(18);
    let initialized = Embedding::new("token_embedding.weight", 4, 2, &mut first_rng)?;
    let reproduced = Embedding::new("token_embedding.weight", 4, 2, &mut second_rng)?;
    let initialized_reproducible =
        *initialized.table().tensor().value() == *reproduced.table().tensor().value();
    let clone_same_node = initialized
        .table()
        .tensor()
        .is_same_node(initialized.clone().table().tensor());

Исполняемый пример также проверяет вход формы [0][0], не содержащий ни одного ID, и первый ID за пределами таблицы:

Примите пустую форму токенов и отклоните первый ID за пределами таблицы rust/demos/ch18-token-embeddings/src/lib.rs#embedding-edge-cases
    let empty_output = embedding.forward(&[], &[0])?.value_snapshot();
    let bounds_rejected = matches!(
        embedding.forward(&[4], &[1]),
        Err(EmbeddingError::TokenIdOutOfBounds {
            position: 0,
            id: 4,
            vocabulary_size: 4,
        })
    );

Вывод исполняемого примера содержит выбранные строки и накопленный градиент таблицы. Скалярный ID формы [][] даёт вектор формы [2][2]; к любой пустой ведущей форме добавляется ширина эмбеддинга, но значений не появляется. Например, форма [0][0] превращается в [0,2][0,2]. Ошибки формы и количества сообщаются раньше первого недопустимого селектора. Проверка всех координат конечными разностями с шагом 10610^{-6} совпадает с аналитическим градиентом таблицы в пределах абсолютного допуска 2×1062\times10^{-6}.

Выведите выбранные векторы и градиент общей таблицы эмбеддингов rust/demos/ch18-token-embeddings/src/main.rs#learner-token-embeddings-output
    let report = learner_report()?;

    println!(
        "table: {} shape={}",
        report.table_name,
        shape(&report.table_shape)
    );
    println!(
        "ids: shape={} values={}",
        shape(&report.token_shape),
        report
            .token_ids
            .iter()
            .map(u32::to_string)
            .collect::<Vec<_>>()
            .join(",")
    );
    println!(
        "output: shape={} values={}",
        shape(report.output.shape()),
        fixed_list(report.output.as_slice())
    );
    println!(
        "one-hot multiplication equals lookup: {}",
        report.one_hot_matches
    );
    println!(
        "upstream: shape={} values={}",
        shape(report.upstream.shape()),
        fixed_list(report.upstream.as_slice())
    );
    println!(
        "table gradient: shape={} values={}",
        shape(report.table_gradient.shape()),
        fixed_list(report.table_gradient.as_slice())
    );

Проследите каждый выбор обратно до общей строки

В прямой части каждая позиция сопоставляется со своим ID токена, one-hot-тождеством, выбранной строкой таблицы и выходным вектором. В обратной части каждый вектор входящего градиента сопоставляется со строкой, которая его получает. Строку 22 выбирают в двух позициях, поэтому различие видно явно: в прямом проходе её значение копируется в два выхода, а в обратном оба градиентных вклада складываются в одной строке параметров.

Соберите строки таблицы, выборы и накопленные градиенты разобранного примера rust/demos/ch18-token-embeddings/src/diagram_trace.rs#token-embeddings-trace
pub fn render_trace() -> Result<String, Box<dyn Error>> {
    let embedding = known_embedding();
    let output = embedding.forward(&TOKEN_IDS, &TOKEN_SHAPE)?;
    let upstream = Tensor::from_vec(vec![1, 3, 2], UPSTREAM_VALUES.to_vec())?;
    output.backward_with_seed(&upstream.view(), GraphRetention::Retain)?;
    let gradient = embedding
        .table()
        .tensor()
        .gradient()
        .expect("trainable table stores its gradient");
    let (one_hot_rows, baseline) = explicit_one_hot_product(&known_table(), &TOKEN_IDS);
    assert_eq!(baseline, output.value().as_slice());
    let mut repeated_ids = TOKEN_IDS
        .iter()
        .copied()
        .filter(|candidate| TOKEN_IDS.iter().filter(|id| *id == candidate).count() > 1)
        .collect::<Vec<_>>();
    repeated_ids.sort_unstable();
    repeated_ids.dedup();
    assert_eq!(
        repeated_ids.len(),
        1,
        "fixture must contain one repeated token ID"
    );
    let repeated_id = repeated_ids[0];
    let repeated_positions = TOKEN_IDS
        .iter()
        .enumerate()
        .filter_map(|(position, id)| (*id == repeated_id).then_some(position))
        .collect::<Vec<_>>();

    let mut trace = String::new();
    writeln!(trace, "TRACE token-embeddings-v1 BEGIN")?;
    writeln!(
        trace,
        "FIXTURE name=known-table-repeated-id parameter={} vocabulary={} width={} table-shape={} id-shape={} output-shape={} upstream-shape={} gradient-shape={} accumulation=scatter-add",
        embedding.table().name(),
        embedding.vocabulary_size(),
        embedding.embedding_width(),
        shape(embedding.table().tensor().shape().as_slice()),
        shape(&TOKEN_SHAPE),
        shape(output.shape().as_slice()),
        shape(upstream.shape()),
        shape(gradient.shape()),
    )?;
    writeln!(
        trace,
        "IDS values={} repeated-id={repeated_id} repeated-flat-positions={}",
        token_list(&TOKEN_IDS),
        integer_list(&repeated_positions),
    )?;

    for row in 0..TABLE_SHAPE[0] {
        let uses = TOKEN_IDS
            .iter()
            .filter(|&&id| usize::try_from(id).ok() == Some(row))
            .count();
        let state = match uses {
            0 => "unused",
            1 => "selected-once",
            _ => "selected-repeated",
        };
        let start = row * embedding.embedding_width();
        writeln!(
            trace,
            "TABLE row={row} uses={uses} state={state} values={}",
            fixed_list(
                &embedding.table().tensor().value().as_slice()
                    [start..start + embedding.embedding_width()]
            )
        )?;
    }

    for (flat, (&id, one_hot)) in TOKEN_IDS.iter().zip(&one_hot_rows).enumerate() {
        let start = flat * embedding.embedding_width();
        let coordinate = row_major_coordinate(flat, &TOKEN_SHAPE);
        let uses = TOKEN_IDS
            .iter()
            .filter(|&&candidate| candidate == id)
            .count();
        writeln!(
            trace,
            "LOOKUP flat={flat} coordinate={} id={id} sharing={} one-hot={} selected-row={id} output={} upstream={}",
            integer_list(&coordinate),
            if uses > 1 {
                "repeated-row"
            } else {
                "single-row"
            },
            integer_list(
                &one_hot
                    .iter()
                    .map(|&value| usize::from(value))
                    .collect::<Vec<_>>()
            ),
            fixed_list(&output.value().as_slice()[start..start + embedding.embedding_width()]),
            fixed_list(&UPSTREAM_VALUES[start..start + embedding.embedding_width()])
        )?;
    }

    for row in 0..TABLE_SHAPE[0] {
        let positions = TOKEN_IDS
            .iter()
            .enumerate()
            .filter_map(|(position, &id)| {
                (usize::try_from(id).ok() == Some(row)).then_some(position)
            })
            .collect::<Vec<_>>();
        let positions_text = if positions.is_empty() {
            "none".to_owned()
        } else {
            integer_list(&positions)
        };
        let contributions_text = if positions.is_empty() {
            "none".to_owned()
        } else {
            positions
                .iter()
                .map(|position| {
                    let start = position * TABLE_SHAPE[1];
                    fixed_list(&UPSTREAM_VALUES[start..start + TABLE_SHAPE[1]])
                })
                .collect::<Vec<_>>()
                .join("|")
        };
        let start = row * embedding.embedding_width();
        let rule = match positions.len() {
            0 => "unused-zero",
            1 => "single-copy",
            _ => "repeated-sum",
        };
        writeln!(
            trace,
            "ROW-GRADIENT row={row} flat-positions={positions_text} contributions={contributions_text} rule={rule} accumulated={}",
            fixed_list(&gradient.as_slice()[start..start + embedding.embedding_width()])
        )?;
    }
    writeln!(trace, "TRACE token-embeddings-v1 END")?;
    Ok(trace)
}

Повторяющийся ID в общей таблице

Сопоставьте ID из примера на Rust со строками и выходами: one-hot-умножение даёт те же значения. Проследите сложение обратных вкладов в общих строках.

Именованный параметр
token_embedding.weight
Строки словаря
44
Ширина эмбеддинга
22
Форма таблицы
[4,2]\left[4,2\right]
Форма ID токенов
[1,3]\left[1,3\right]
Форма выхода
[1,3,2]\left[1,3,2\right]
Форма градиента таблицы
[4,2]\left[4,2\right]

Целочисленные ID токенов

ID указывают номера строк и не записываются на ленту дифференцирования. Повторяющийся ID снова выбирает ту же строку параметров.

Позиция ID токена Статус
(0,0)\left(0,0\right) 22 Общая с другой позицией
(0,1)\left(0,1\right) 11 Строка без повторов
(0,2)\left(0,2\right) 22 Общая с другой позицией

Общая обучаемая таблица

Строка таблицы Обучаемый вектор Число выборов Статус
E0,:E_{0,:} [10.000000000000,11.000000000000]\left[10.000000000000,11.000000000000\right] 00 Строка не выбрана
E1,:E_{1,:} [20.000000000000,21.000000000000]\left[20.000000000000,21.000000000000\right] 11 Выбрана один раз
E2,:E_{2,:} [30.000000000000,31.000000000000]\left[30.000000000000,31.000000000000\right] 22 Общая для повторов
E3,:E_{3,:} [40.000000000000,41.000000000000]\left[40.000000000000,41.000000000000\right] 00 Строка не выбрана

Выбор строк без хранения нулей

Единица one-hot-индикатора выбирает одну строку таблицы; реализация получает эту строку напрямую.

Позиция ID токена One-hot-индикатор Равносильное произведение Выбранная строка Выходной вектор Входящий градиент
(0,0)\left(0,0\right) 22 [0,0,1,0]\left[0,0,1,0\right] e2Ee_{2}E E2,:E_{2,:} [30.000000000000,31.000000000000]\left[30.000000000000,31.000000000000\right] [1.000000000000,0.000000000000]\left[1.000000000000,0.000000000000\right]
(0,1)\left(0,1\right) 11 [0,1,0,0]\left[0,1,0,0\right] e1Ee_{1}E E1,:E_{1,:} [20.000000000000,21.000000000000]\left[20.000000000000,21.000000000000\right] [0.000000000000,2.000000000000]\left[0.000000000000,2.000000000000\right]
(0,2)\left(0,2\right) 22 [0,0,1,0]\left[0,0,1,0\right] e2Ee_{2}E E2,:E_{2,:} [30.000000000000,31.000000000000]\left[30.000000000000,31.000000000000\right] [3.000000000000,4.000000000000]\left[3.000000000000,4.000000000000\right]

Накопление в общих строках

В обратном проходе каждый входящий вектор поступает в выбранную строку. В повторно выбранной строке два вектора складываются.

Строка таблицы Плоские позиции, давшие вклад Вклады по признакам Обратное правило Накопленный градиент строки
Eˉ0,:\bar E_{0,:} Нет Нет Нет выборов — ноль [0.000000000000,0.000000000000]\left[0.000000000000,0.000000000000\right]
Eˉ1,:\bar E_{1,:} 11 [0.000000000000,2.000000000000]\left[0.000000000000,2.000000000000\right] Один выбор — скопировать его вклад [0.000000000000,2.000000000000]\left[0.000000000000,2.000000000000\right]
Eˉ2,:\bar E_{2,:} 0,20,2 [1.000000000000,0.000000000000]\left[1.000000000000,0.000000000000\right] ++ [3.000000000000,4.000000000000]\left[3.000000000000,4.000000000000\right] Повторы — сложить вклады [4.000000000000,4.000000000000]\left[4.000000000000,4.000000000000\right]
Eˉ3,:\bar E_{3,:} Нет Нет Нет выборов — ноль [0.000000000000,0.000000000000]\left[0.000000000000,0.000000000000\right]

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

Сначала предскажите, затем сверьтесь с исполняемым примером

  1. Предскажите все шесть выходных значений для ID [[2,1,2]][[2,1,2]].
  2. Запишите one-hot-строку длины четыре для ID 22 и умножьте её на EE.
  3. Предскажите все значения градиента таблицы для заданной начальной сопряжённой величины.
  4. Объясните, почему вклады складываются в градиент строки 22, а не образуют градиенты двух независимых параметров.
  5. Предскажите формы выхода для форм ID [2,3][2,3], [][] и [0][0] при d=2d=2.
  6. Укажите порядок, в котором метод сообщает об ошибках: неверная форма токенов, несовпадение числа ID и ID вне таблицы. Затем найдите первый недопустимый ID в [1,4,9][1,4,9] для таблицы из четырёх строк.
  7. Объясните, почему после своих проверок Embedding::forward может передать преобразованные селекторы внутреннему проверенному плану выбора строк по индексам, а публичная операция выбора строк обязана сама проверить исходные селекторы. Какое выделение памяти всё ещё может завершиться ошибкой после создания плана?
  8. Предскажите, создаёт ли клонирование слоя эмбеддингов новый обучаемый листовой узел-параметр.
  9. Объясните, почему ID не получают градиент и почему соседние номера ID не означают смыслового сходства.
  10. Определите, требует ли работа Bengio и соавторов явно создавать one-hot-векторы.
  11. Определите, получает ли повторное вхождение токена новый параметр эмбеддинга.
Сверить ответы
  1. Результат равен [[[30,31],[20,21],[30,31]]][[[30,31],[20,21],[30,31]]].
  2. ID 22 соответствует [0,0,1,0][0,0,1,0], поэтому умножение на EE даёт [30,31][30,31].
  3. Градиент таблицы равен [[0,0],[0,2],[4,4],[0,0]][[0,0],[0,2],[4,4],[0,0]].
  4. Оба вхождения выбрали одну и ту же строку 22 общей таблицы параметров, поэтому [1,0][1,0] и [3,4][3,4] складываются в градиенте этой строки.
  5. Формы выхода равны [2,3,2][2,3,2], [2][2] и [0,2][0,2].
  6. Сначала метод проверяет форму токенов, затем точное число ID и только после этого просматривает ID в плоском порядке. ID 44 в плоской позиции 11 — первый недопустимый селектор, потому что допустимы строки от 00 до 33.
  7. Каждый Embedding содержит таблицу ранга два, полученную через публичный конструктор или сохранённую при клонировании, а forward уже проверил форму, число и границы ID до их преобразования. Проверенный план выбора строк по индексам владеет преобразованными селекторами и их логической формой. У публичного вызова TensorValue::gather_rows таких гарантий нет, поэтому он выполняет все проверки сам. Даже после создания плана при выделении буфера выходных значений всё ещё может возникнуть ошибка.
  8. Нет. Клон — отдельный объект слоя, но он хранит тот же именованный листовой узел-параметр таблицы; нового обучаемого листа не возникает.
  9. ID — дискретные селекторы вне ленты, а их нумерация не задаёт геометрии.
  10. Нет. One-hot-умножение здесь служит только алгебраическим пояснением.
  11. Нет. Повторение снова использует одну строку таблицы; последующий учёт позиции различает вхождения, не создавая отдельных параметров эмбеддинга для каждой позиции.

Используйте ширину последней оси как входную ширину обучаемой проекции

Теперь совокупная модель умеет превращать тензоры ID токенов в дифференцируемые тензоры признаков: к исходной форме добавляется последняя ось, размер которой равен ширине эмбеддинга, а одна общая именованная таблица «словарь на признаки» остаётся параметром. В главе 19 размер последней оси эмбеддинга станет входной шириной обучаемой проекции: выбор строки по индексу извлекает вектор токена, а линейный слой смешивает его координаты.

Так декодер получает числовые представления токенов, пока ещё без учёта позиции. Глава 19 сохраняет все ведущие оси пакета и последовательности и применяет обучаемую матрицу к последней оси признаков. В следующих главах RoPE сделает внимание позиционно-зависимым, поворачивая уже спроецированные запросы и ключи; повторяющиеся вхождения по-прежнему используют одну строку таблицы эмбеддингов.