← Все главы

10 · Версия материала 5

Согласуйте совместимые формы и агрегируйте значения по заданной оси

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

Предскажите, как один вектор смещения применяется к двум строкам токенов

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

tokens shape [2,3]
[[1, 2, 3],
 [4, 5, 6]]

У вектора смещения по признакам нет оси токенов:

bias shape [3]
[10, 20, 30]

Прежде чем смотреть ответ, предскажите форму результата и шесть сумм. Нужно ли отклонить вектор смещения из-за ранга 1, сначала скопировать его в новый входной тензор формы [2,3] или сопоставить каждой строке токенов по однозначному правилу совместимости?

Применим согласование форм (broadcasting), начиная с последних осей:

tokens: [2,3]
bias:   [1,3]   # отсутствующая начальная ось считается осью размера 1
result: [2,3]

Размер оси признаков 33 совпадает с 33. На первой согласованной оси единственная входная координата повторно используется для обеих координат оси результата размера 22. Поэтому координата результата (r,c)(r,c) обращается к координате токена (r,c)(r,c) и координате вектора смещения (c)(c). Получаем:

[123456]+[102030102030]=[112233142536]\begin{bmatrix} 1&2&3\\ 4&5&6 \end{bmatrix} + \begin{bmatrix} 10&20&30\\ 10&20&30 \end{bmatrix} = \begin{bmatrix} 11&22&33\\ 14&25&36 \end{bmatrix}

Теперь, прежде чем считать, предскажите три результата агрегирования значений по оси (редукции): сумму по оси токенов 0, среднее по оси признаков 1 с сохранённой редуцируемой осью и максимум по оси признаков 1 с удалением этой оси. Результаты соответственно равны: форма [3] и значения [25,47,69]; форма [2,1] и значения [22,25]; форма [2] и значения [33,36].

Сопоставьте координаты при согласовании форм и редуцируйте одну ось

Согласование форм и вычисление среднего можно записать вместе:

y𝐢=f(aβa(𝐢),bβb(𝐢)),μk(𝐢k)=1nkik=0nk1x𝐢y_{\mathbf{i}}=f(a_{\beta_a(\mathbf{i})},b_{\beta_b(\mathbf{i})}), \qquad \mu_k(\mathbf{i}_{-k})=\frac{1}{n_k}\sum_{i_k=0}^{n_k-1}x_{\mathbf{i}}

В первом выражении правила формы тензора отделены от скалярной арифметики. y𝐢y_{\mathbf{i}} — одно значение результата в полной координате 𝐢\mathbf{i}. Функция ff может складывать, умножать или выполнять другое скалярное вычисление. Она получает левое значение из тензора aa в координате βa(𝐢)\beta_a(\mathbf{i}) и правое значение из тензора bb в координате βb(𝐢)\beta_b(\mathbf{i}).

Каждое отображение β\beta удаляет оси результата, которых не было во входном тензоре, и выбирает нулевую координату на согласованной входной оси размера 1. Остальные координаты сохраняются. В зафиксированном примере:

βtokens(r,c)=(r,c),βbias(r,c)=(c)\beta_{\mathrm{tokens}}(r,c)=(r,c),\qquad \beta_{\mathrm{bias}}(r,c)=(c)

Во втором выражении координаты 𝐢k\mathbf{i}_{-k} вне оси kk зафиксированы, iki_k обходит nkn_k элементов этой оси, а их сумма в заданном порядке делится на nkn_k. Среднее определено только при nk>0n_k>0; иначе реализация возвращает типизированную ошибку пустой оси. Полная координата 𝐢\mathbf{i} составляется из фиксированных координат и текущего iki_k. При keep_dim=true выбранная ось остаётся на прежнем месте в форме результата и получает размер 1. Это не добавляет слагаемое к среднему и не меняет делитель.

Учтите каждую координату, размер оси и отображение

СимволПрактический смысл
y𝐢y_{\mathbf{i}}Значение результата в полной координате 𝐢\mathbf{i}.
𝐢\mathbf{i}Полная координата одного значения результата или входа редукции; все индексы отсчитываются от нуля.
ffСкалярная поэлементная функция, применяемая к одной согласованной паре значений.
aaЛевый входной тензор.
bbПравый входной тензор.
βa(𝐢)\beta_a(\mathbf{i})Отображение координаты результата в aa: отсутствующие начальные оси отбрасываются, а на расширяемых осях размера 1 используется ноль.
βb(𝐢)\beta_b(\mathbf{i})То же правило отображения для bb.
μk(𝐢k)\mu_k(\mathbf{i}_{-k})Среднее по оси kk при фиксированных координатах 𝐢k\mathbf{i}_{-k}.
𝐢k\mathbf{i}_{-k}Все координаты, которые остаются фиксированными при редукции по оси kk.
kkЯвно указанная ось редукции, отсчитываемая от нуля.
nkn_kРазмер редуцируемой оси kk.
iki_kКоордината на этой оси, принимающая значения от нуля до nk1n_k-1.
x𝐢x_{\mathbf{i}}Входное значение редукции в полной координате 𝐢\mathbf{i}, составленной из фиксированных 𝐢k\mathbf{i}_{-k} и изменяющейся iki_k.

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

Скаляр имеет форму [] и может быть согласован с любым результатом, поскольку у него нет несовместимых осей. Выполнить редукцию скаляра этой операцией нельзя: у него нет допустимой оси. Пустой результат не содержит координат, поэтому переданная скалярная функция не вызывается ни разу.

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

Бенжио и соавторы описывают n-граммные модели как таблицы условных вероятностей для фиксированного числа предыдущих слов; в их нейронной языковой модели обучаемые векторы признаков слов контекста конкатенируются, скрытый слой использует гиперболический тангенс, а вероятности следующего слова вычисляются с помощью softmax. Однако предсказание по-прежнему строится для одного выбранного окна фиксированной длины. В более поздних декодерах Transformer вычисления, напротив, охватывают доступный каждой позиции авторегрессионный префикс и организованы в тензоры с явными осями пакета, последовательности и голов внимания.

Для n-граммы порядка nn такой фиксированный контекст содержит n1n-1 предыдущих слов.

Более ранний источник — Bengio et al., A Neural Probabilistic Language Model.

Эта модель выходит за рамки отдельных строк таблицы на основе подсчётов: она обучает общие распределённые представления слов и параметры нейронной сети, однако на вход по-прежнему поступает выбранное окно фиксированной длины. Поэлементный гиперболический тангенс tanh\tanh и softmax по словарю действительно входят в вычисления модели; глава не утверждает, что в статье был задан универсальный интерфейс тензоров.

Более поздние источники — Vaswani et al., Attention Is All You Need и официальный файл model.py модели GPT-2 от OpenAI. Васвани и соавторы задают маскированное самовнимание декодера через матрицы запросов, ключей и значений, применяют softmax к масштабированным оценкам «запрос — ключ», используют для каждого подслоя остаточное соединение с последующей нормализацией слоя и отдельно, но одинаково применяют одну и ту же сеть прямого распространения к каждой позиции. В официальной реализации GPT-2 явно обозначены оси пакета, последовательности, признаков, голов внимания, позиций назначения и позиций источника. В softmax из значений вычитается максимум по последней оси, затем вычисляются экспоненты сдвинутых значений, и каждая из них делится на их сумму по той же оси; обе редукции сохраняют ось. Нормализация сначала вычисляет средние по последней оси, а затем применяет векторы масштаба и смещения, размер которых совпадает с размером оси признаков.

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

Согласование форм и редукции по явно указанным осям позволяют в этом курсе применять ко всему тензору декодера скаляры и параметры, размер которых совпадает с размером оси признаков, а также вычислять статистики по нужным осям для softmax в механизме внимания и нормализации признаков. Точное правило согласования начиная с последних осей, ошибки формы, поведение пустых осей, возможность сохранить редуцируемую ось и правила выделения памяти относятся к этой реализации. Источники по моделям задают сами вычисления, а руководство NumPy описывает вспомогательное правило согласования форм.

Небольшой пример на Rust подчёркивает эту границу. fixed_context_feature_step выполняет один расчёт для трёх признаков в контексте фиксированной ширины. Общий путь применяет тот же вектор смещения по признакам к двум строкам токенов и выполняет редукции по заданным осям. Этот небольшой расчёт связывает одно представление контекста с явными осями токенов и признаков, но не является новшеством Transformer или полной реализацией какой-либо из двух процитированных моделей.

Сравните один шаг для контекста фиксированной ширины с тем же расчётом по признакам на явных осях токенов rust/demos/ch10-broadcasting-reductions/src/lib.rs#tiny-token-feature-example
/// Applies one fixed-width feature offset to one context representation.
///
/// This shape-specific baseline stands for an earlier one-example calculation;
/// it is not presented as the exact equation of any cited model.
pub fn fixed_context_feature_step(context: [f64; 3], feature_bias: [f64; 3]) -> [f64; 3] {
    [
        context[0] + feature_bias[0],
        context[1] + feature_bias[1],
        context[2] + feature_bias[2],
    ]
}

/// Runs the same scalar operations over explicit token and feature axes.
pub fn tiny_token_feature_example() -> Result<TinyTokenFeatureExample, TensorOpError> {
    let tokens = Tensor::from_vec(TOKEN_SHAPE.to_vec(), TOKEN_VALUES.to_vec())?;
    let bias = Tensor::from_vec(BIAS_SHAPE.to_vec(), BIAS_VALUES.to_vec())?;
    let biased = map_binary(&tokens.view(), &bias.view(), |value, offset| value + offset)?;
    let squared = map_unary(&tokens.view(), |value| value * value)?;
    let sum_axis_0 = sum_axis(&biased.view(), 0, false)?;
    let mean_axis_1_kept = mean_axis(&biased.view(), 1, true)?;
    let max_axis_1 = max_axis(&biased.view(), 1, false)?;

    Ok(TinyTokenFeatureExample {
        tokens,
        bias,
        biased,
        squared,
        sum_axis_0,
        mean_axis_1_kept,
        max_axis_1,
    })
}

Сначала определите формы, затем вычисляйте значения

broadcast_shape только планирует форму. Функция согласует ранги начиная с последних осей, считает отсутствующие начальные оси осями размера 1 и обходит согласованные выходные оси слева направо. Равные размеры переходят в результат без изменений. Если один размер равен 1, выбирается другой. В противном случае ошибка содержит точную выходную ось и оба размера.

После проверки совместимости планировщик передаёт форму существующему коду компоновки тензора, который проверяет все произведения последующих размеров. Поэтому для [0,usize::MAX,1] и [1,1,2] ошибка ShapeOverflow возникает до выделения памяти: внешний ноль делает входную форму допустимой, но в запланированной форме usize::MAX умножается на 22, что не помещается в usize. Ошибка совместимости имеет приоритет над этой проверкой формы.

Согласуйте формы начиная с последних осей и отклоните первую несовместимую выходную ось до проверки переполнения rust/crates/llm-from-scratch/src/tensor/ops.rs#broadcast-planning
/// Computes the checked output shape for trailing-axis broadcasting.
///
/// Missing leading dimensions act as size one. Aligned dimensions are
/// compatible when they are equal or either one is size one. Compatibility is
/// reported from the leftmost aligned output axis before layout overflow.
pub fn broadcast_shape(left: &[usize], right: &[usize]) -> Result<Vec<usize>, TensorOpError> {
    let output_rank = left.len().max(right.len());
    let left_padding = output_rank - left.len();
    let right_padding = output_rank - right.len();
    let mut output = Vec::with_capacity(output_rank);

    for axis in 0..output_rank {
        let left_dimension = left
            .get(axis.wrapping_sub(left_padding))
            .copied()
            .unwrap_or(1);
        let right_dimension = right
            .get(axis.wrapping_sub(right_padding))
            .copied()
            .unwrap_or(1);
        let dimension = if left_dimension == right_dimension {
            left_dimension
        } else if left_dimension == 1 {
            right_dimension
        } else if right_dimension == 1 {
            left_dimension
        } else {
            return Err(TensorOpError::IncompatibleBroadcast {
                axis,
                left_dimension,
                right_dimension,
            });
        };
        output.push(dimension);
    }

    checked_row_major_layout(&output)?;
    Ok(output)
}

Правило совместимости совпадает с руководством NumPy по согласованию форм: при согласовании начиная с последних осей размеры совместимы, если они равны или один из них равен 1. Эта реализация дополнительно явно задаёт случай нуля и единицы: размер, отличный от 1, равен нулю, поэтому результат остаётся пустым. Руководство не служит источником исторического развития LLM в этой главе и не задаёт ошибки этой реализации, обход логического представления или правила владения результатом.

TensorView::get остаётся общедоступным способом прочитать одно значение по координате, переданной вызывающим кодом. Число компонентов такой координаты может не совпадать с рангом тензора, а индекс — выходить за границы своей оси, поэтому каждый вызов сначала проверяет координату и лишь затем читает хранилище. Поэлементные операции начинают работу с уже корректными представлениями; кроме того, map_binary до обхода значений проверяет правило «размеры равны или один из них равен 1». После этого внутренний план шагов один раз проверяется и используется на всём обходе.

Для каждой оси результата map_binary назначает каждому входу эффективный шаг по элементам. Для отсутствующей начальной оси входа и для согласованной входной оси размера 1 этот шаг равен 0: когда соответствующая координата результата меняется, нужно снова выбрать то же входное значение. На остальных согласованных осях сохраняется исходный шаг TensorView. В зафиксированном примере шаги тензора токенов [3,1] задают смещения в исходном хранилище [0,1,2,3,4,5], а эффективные шаги вектора смещения [0,1] задают [0,1,2,0,1,2].

Попарный обход этих двух последовательностей реализует шесть отображений координат, предсказанных выше. map_unary использует такой же проверенный обход по смещениям с собственными шагами входного представления. map_unary и map_binary не создают новый вектор координат для каждого значения и не возвращаются к общедоступной проверке координаты. Изменился только внутренний способ обхода: сохраняются порядок аргументов бинарной функции «левый, затем правый», логический построчный порядок вызовов, обычное индексирование среза в Rust с проверкой границ и выделение памяти для нового непрерывного результата. Для пустого результата переданная функция не вызывается. Транспонированное представление или срез задаёт другие шаги без неявного вызова materialize.

Выделение памяти для каждого результата выполняется с обработкой ошибки. Например, [usize::MAX,0] — допустимая пустая входная форма, но сумма по её пустой оси потребовала бы собственный результат из usize::MAX нулей. До начала обхода реализация возвращает OutputAllocationFailed { elements: usize::MAX }, а не допускает panic при запросе ёмкости.

Используйте проверенные курсоры смещений и нулевые эффективные шаги, а результат каждой поэлементной операции сохраняйте в собственном тензоре rust/crates/llm-from-scratch/src/tensor/ops.rs#elementwise-maps
/// Applies one scalar function in logical row-major order and owns the result.
pub fn map_unary<F>(input: &TensorView<'_>, mut operation: F) -> Result<Tensor, TensorOpError>
where
    F: FnMut(f64) -> f64,
{
    let mut values = output_buffer(input.len())?;
    for input_offset in input.logical_offsets() {
        values.push(operation(input.value_at_storage_offset(input_offset)));
    }
    Tensor::from_vec(input.shape().to_vec(), values).map_err(Into::into)
}

/// Applies one scalar function across two trailing-axis-compatible views.
pub fn map_binary<F>(
    left: &TensorView<'_>,
    right: &TensorView<'_>,
    mut operation: F,
) -> Result<Tensor, TensorOpError>
where
    F: FnMut(f64, f64) -> f64,
{
    let output_shape = broadcast_shape(left.shape(), right.shape())?;
    let (_, output_len) = checked_row_major_layout(&output_shape)?;
    let mut values = output_buffer(output_len)?;

    let left_strides = broadcast_effective_strides(left, output_shape.len());
    let right_strides = broadcast_effective_strides(right, output_shape.len());
    let left_offsets = left
        .projected_offsets(&output_shape, &left_strides, output_len)
        .expect("a compatible broadcast retains a valid left traversal plan");
    let right_offsets = right
        .projected_offsets(&output_shape, &right_strides, output_len)
        .expect("a compatible broadcast retains a valid right traversal plan");

    for (left_offset, right_offset) in left_offsets.zip(right_offsets) {
        let left_value = left.value_at_storage_offset(left_offset);
        let right_value = right.value_at_storage_offset(right_offset);
        values.push(operation(left_value, right_value));
    }

    Tensor::from_vec(output_shape, values).map_err(Into::into)
}

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

После проверки выбранной оси и формы результата каждой непустой выходной группе соответствует одно базовое смещение в исходном хранилище. Редукция обходит группу по возрастанию координаты выбранной оси, каждый раз прибавляя к текущему смещению шаг по элементам этой оси. Для непрерывного тензора результата формы [2,3] редукция по оси 0 использует базовые смещения [0,1,2] и шаг 3, поэтому получает группы смещений [0,3], [1,4] и [2,5]. Для оси 1 базовые смещения равны [0,3], а шаг — 1; группы равны [0,1,2] и [3,4,5]. Транспонированное представление или срез задаёт другие базовые смещения и шаги, но логические группы и порядок вычислений не меняются. Сумма по выбранной пустой оси записывает 0.0, не читая базовое смещение. Если нулевой размер имеет другая, не выбранная ось, в результате нет групп и чтение исходного хранилища не выполняется.

Для каждой непустой группы поиск максимума начинается со значения при координате ноль на выбранной оси. Оно заменяется только строго большим значением; первый встретившийся NaN явно сохраняется. При равенстве остаётся первый битовый шаблон, в том числе знак нуля в паре -0.0 и +0.0. Явное правило не позволяет случайно унаследовать обработку NaN или равных значений из вспомогательной функции.

Выполняйте редукцию от проверенных базовых смещений групп с одним шагом выбранной оси и явными правилами для пустых групп, порядка, NaN и равных значений rust/crates/llm-from-scratch/src/tensor/ops.rs#axis-reductions
/// Sums one explicit axis in ascending index order.
///
/// An empty selected axis uses the additive identity, so every output group is
/// `0.0`. `keep_dim` replaces the selected extent with one instead of removing
/// the axis.
pub fn sum_axis(
    input: &TensorView<'_>,
    axis: usize,
    keep_dim: bool,
) -> Result<Tensor, TensorOpError> {
    reduce_axis(input, axis, keep_dim, Reduction::Sum)
}

/// Averages one explicit nonempty axis in ascending index order.
pub fn mean_axis(
    input: &TensorView<'_>,
    axis: usize,
    keep_dim: bool,
) -> Result<Tensor, TensorOpError> {
    reduce_axis(input, axis, keep_dim, Reduction::Mean)
}

/// Selects the maximum over one explicit nonempty axis.
///
/// The fold propagates the first NaN and keeps the earlier value on equal
/// comparisons, including the earlier signed-zero bit pattern.
pub fn max_axis(
    input: &TensorView<'_>,
    axis: usize,
    keep_dim: bool,
) -> Result<Tensor, TensorOpError> {
    reduce_axis(input, axis, keep_dim, Reduction::Max)
}

#[derive(Clone, Copy)]
enum Reduction {
    Sum,
    Mean,
    Max,
}

fn reduce_axis(
    input: &TensorView<'_>,
    axis: usize,
    keep_dim: bool,
    reduction: Reduction,
) -> Result<Tensor, TensorOpError> {
    if axis >= input.rank() {
        return Err(TensorOpError::ReductionAxisOutOfBounds {
            axis,
            rank: input.rank(),
        });
    }

    let axis_len = input.shape()[axis];
    match reduction {
        Reduction::Mean if axis_len == 0 => {
            return Err(TensorOpError::EmptyMeanAxis { axis });
        }
        Reduction::Max if axis_len == 0 => {
            return Err(TensorOpError::EmptyMaxAxis { axis });
        }
        _ => {}
    }

    let output_shape = reduction_shape(input.shape(), axis, keep_dim);
    let (_, output_len) = checked_row_major_layout(&output_shape)?;
    let mut values = output_buffer(output_len)?;

    if axis_len == 0 {
        debug_assert!(matches!(reduction, Reduction::Sum));
        values.resize(output_len, 0.0);
        return Tensor::from_vec(output_shape, values).map_err(Into::into);
    }

    let group_strides = reduction_group_strides(input.strides(), axis, keep_dim);
    let group_offsets = input
        .projected_offsets(&output_shape, &group_strides, output_len)
        .expect("a checked reduction retains a valid group traversal plan");
    let axis_stride = input.strides()[axis];

    for group_offset in group_offsets {
        let value = match reduction {
            Reduction::Sum | Reduction::Mean => {
                let mut total = 0.0;
                let mut input_offset = group_offset;
                for index in 0..axis_len {
                    total += input.value_at_storage_offset(input_offset);
                    if index + 1 < axis_len {
                        input_offset = input_offset
                            .checked_add(axis_stride)
                            .expect("a checked view cannot overflow along a reduction axis");
                    }
                }
                if matches!(reduction, Reduction::Mean) {
                    total / axis_len as f64
                } else {
                    total
                }
            }
            Reduction::Max => {
                let mut input_offset = group_offset;
                let mut maximum = input.value_at_storage_offset(input_offset);
                for _ in 1..axis_len {
                    input_offset = input_offset
                        .checked_add(axis_stride)
                        .expect("a checked view cannot overflow along a reduction axis");
                    let candidate = input.value_at_storage_offset(input_offset);
                    if !maximum.is_nan() && (candidate.is_nan() || candidate > maximum) {
                        maximum = candidate;
                    }
                }
                maximum
            }
        };
        values.push(value);
    }

    Tensor::from_vec(output_shape, values).map_err(Into::into)
}

Исполняемый пример на Rust демонстрирует заданный расчёт с формой, характерной для модели, согласование со скаляром, пустой результат без вызовов переданной функции, нейтральный элемент пустой суммы и каждую типизированную ошибку. Целочисленные результаты можно сравнивать точно. Для среднего значений [0.1,0.2,0.3] подходит абсолютный допуск 1e-12, поскольку эти десятичные дроби не имеют точного представления двоичным числом с плавающей точкой.

Вычислите зафиксированные результаты и покажите согласование со скаляром, пустой результат, несовместимые формы и ошибку редукции скаляра rust/demos/ch10-broadcasting-reductions/src/main.rs#learner-broadcasting-output
    let example = tiny_token_feature_example()?;

    let scalar = Tensor::from_vec(vec![], vec![0.5])?;
    let scalar_broadcast = map_binary(&example.tokens.view(), &scalar.view(), |value, offset| {
        value + offset
    })?;

    let empty = empty_fixture()?;
    let mut closure_calls = 0;
    let empty_broadcast = map_binary(&empty.view(), &example.bias.view(), |value, offset| {
        closure_calls += 1;
        value + offset
    })?;
    let empty_sum = sum_axis(&empty.view(), 1, false)?;

    let incompatible = Tensor::from_vec(vec![2], vec![1.0, 2.0])?;
    let broadcast_error = map_binary(
        &example.tokens.view(),
        &incompatible.view(),
        |left, right| left + right,
    )
    .unwrap_err();
    let mean_error = mean_axis(&empty.view(), 1, false).unwrap_err();
    let max_error = max_axis(&empty.view(), 1, false).unwrap_err();
    let scalar_reduction_error = sum_axis(&scalar.view(), 0, false).unwrap_err();

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

Проследите повторное использование признаков и редукции по осям

Изучите схему в четыре прохода:

  1. Сравните каждую исходную форму с результатом согласования начиная с последних осей.
  2. Проследите все шесть координат результата до одной координаты токена и одной координаты вектора смещения. Обратите внимание: каждая координата смещения встречается в обеих строках токенов.
  3. Сравните плоские группы для суммы по оси 0 и среднего или максимума по оси 1. Перед чтением каждой формы результата проверьте поле «Сохранить редуцируемую ось».
  4. Разберите три отклонённых записи: несовместимые размеры 3 и 2, пустое среднее и пустой максимум.

Один вектор смещения для двух строк, затем редукция по заданным осям

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

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

Отсутствующая начальная ось вектора смещения имеет размер 1, поэтому его координаты повторно выбираются для обеих строк токенов. Маркер обозначает отображение координат, а не копирование.

  • Тензор: Форма тензора признаков токенов Исходная форма [2,3] Согласованная форма [2,3]
  • Координата на оси размера 1 используется повторно: Форма вектора смещения Исходная форма [3] Согласованная форма [1,3]
  • Форма результата согласования [2,3]
Координата результата Координата токена Координата смещения Результат
[0,0] [0,0] Координата на оси размера 1 используется повторно: [0] 11.0
[0,1] [0,1] Координата на оси размера 1 используется повторно: [1] 22.0
[0,2] [0,2] Координата на оси размера 1 используется повторно: [2] 33.0
[1,0] [1,0] Координата на оси размера 1 используется повторно: [0] 14.0
[1,1] [1,1] Координата на оси размера 1 используется повторно: [1] 25.0
[1,2] [1,2] Координата на оси размера 1 используется повторно: [2] 36.0
Операция отклонена:
broadcast
Запрос:
[2,3] / [2]
Данные проверки:
Ось 1, 323\ne2
Причина отклонения:
Согласованные размеры осей 3 и 2 различаются, и ни один из них не равен 1.

Выполняйте редукцию по одной заданной оси

Стрелка вниз отмечает значения, объединяемые по оси 0 или 1. В отклонённых строках объясняется, почему для выбранной пустой оси нельзя вычислить среднее или максимум.

Операция Ось Сохранить редуцируемую ось Форма результата Группа / Значения
Значения объединяются редукцией: sum 0 Нет [3]
Группа 0: [11.0,14.0] Результат: 25.0
Группа 1: [22.0,25.0] Результат: 47.0
Группа 2: [33.0,36.0] Результат: 69.0
Значения объединяются редукцией: mean 1 Да [2,1]
Группа 0: [11.0,22.0,33.0] Результат: 22.0
Группа 1: [14.0,25.0,36.0] Результат: 25.0
Значения объединяются редукцией: max 1 Нет [2]
Группа 0: [11.0,22.0,33.0] Результат: 33.0
Группа 1: [14.0,25.0,36.0] Результат: 36.0
Операция отклонена: mean 1 Не применяется Операция отклонена
Запрос: [2,0,3] Причина отклонения: Для выбранной пустой оси среднее не определено.
Операция отклонена: max 1 Не применяется Операция отклонена
Запрос: [2,0,3] Причина отклонения: Для выбранной пустой оси максимум не определён.

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

Предскажите допустимые формы и результаты редукции

Сначала запишите форму и отображение координат, а затем вычисляйте значения.

  1. Согласуйте [2,1,3] с [4,3]. Предскажите форму результата и обе исходные координаты для координаты результата [1,2,0].
  2. Определите, совместимы ли формы [2,3] и [2]. Если нет, назовите выходную ось и два несовместимых размера.
  3. Согласуйте [0,3] с [1,3]. Предскажите форму результата и число вызовов переданной скалярной функции.
  4. Для примера с вектором смещения предскажите сумму по оси 0, среднее по оси 1 с сохранённой редуцируемой осью и максимум по оси 1 без сохранения.
  5. Выполните сумму, среднее и максимум по выбранной пустой оси 1 тензора формы [2,0,3].
  6. Вычислите среднее значений [0.1,0.2,0.3] ранга 1 по оси 0, не сохраняя её. Предскажите форму результата и объясните необходимость допуска.
  7. Примените правило максимума к [1,NaN_A,NaN_B] и [-0.0,+0.0,-1.0]. Предскажите, какая полезная нагрузка NaN и какой знак нуля сохранятся.
  8. Для тензора токенов с шагами [3,1] и вектора смещения с шагом [1] запишите оба плана эффективных шагов и шесть смещений в исходном хранилище, которые выдаёт каждый план. Затем запишите базовые смещения редукции по оси 0, шаг выбранной оси и группы смещений. Почему эти внутренние планы можно использовать повторно, а TensorView::get должен проверять каждую координату, полученную от вызывающего кода?
Проверить восемь ответов о согласовании форм и редукции
  1. При согласовании начиная с последних осей получаем [2,1,3] и [1,4,3], поэтому форма результата равна [2,4,3]. Координата результата [1,2,0] сопоставляется левой координате [1,0,0], поскольку размер средней оси левого тензора равен 1, и правой координате [2,0], поскольку отсутствующая начальная ось правого тензора отбрасывается.
  2. Согласование начиная справа сравнивает 3 и 2 на выходной оси 1. Размеры различаются, и ни один из них не равен 1, поэтому возвращается IncompatibleBroadcast { axis: 1, left_dimension: 3, right_dimension: 2 }.
  3. Ноль и единица совместимы, а размер, отличный от 1, равен нулю. Поэтому форма результата — [0,3]. В нём нет логических значений, и переданная скалярная функция не вызывается ни разу.
  4. Группы суммы по оси 0 равны [11,14], [22,25] и [33,36]; результат имеет форму [3] и значения [25,47,69]. Среднее по оси 1 объединяет каждую строку и даёт сохранённую форму [2,1] со значениями [22,25]. Максимум по оси 1 удаляет ось и даёт форму [2] со значениями [33,36].
  5. Для каждой из шести выходных групп сумма использует нейтральный элемент сложения, поэтому возвращает форму [2,3], заполненную значением 0.0. Среднее возвращает EmptyMeanAxis { axis: 1 }, а максимум — EmptyMaxAxis { axis: 1 }: для выбранной пустой группы ни одно из этих значений не определено.
  6. После удаления единственной оси получается скалярная форма [] с одним значением, близким к 0.2. Десятичные дроби 0.1, 0.2 и 0.3 нельзя точно представить двоичным числом с плавающей точкой, поэтому вместо сравнения битовых шаблонов подходит абсолютный допуск 1e-12.
  7. При обходе по возрастанию координат NaN_A встречается первым, и далее сохраняется его полезная нагрузка. Во второй строке первым максимумом становится -0.0; +0.0 равен ему, поэтому сохраняется более ранний битовый шаблон отрицательного нуля.
  8. План тензора токенов равен [3,1] и выдаёт смещения [0,1,2,3,4,5]. В плане вектора смещения отсутствующей начальной оси соответствует нулевой шаг, поэтому план равен [0,1] и выдаёт [0,1,2,0,1,2]. Для редукции по оси 0 базовые смещения равны [0,1,2], шаг выбранной оси равен 3, а группы смещений — [0,3], [1,4] и [2,5]. Эти планы содержат только метаданные, вычисленные из корректного представления и проверенной операции, и не принимают новых координат от вызывающего кода. TensorView::get, напротив, получает новую координату, поэтому при каждом вызове должен проверить её ранг и границы осей.

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

Подготовьте примитивы для нормализации и softmax

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

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

Это вспомогательные части, а не готовые слои модели. Устойчивый softmax внимания будет вычитать максимум по оси, поэлементно вычислять экспоненту, суммировать по той же оси с её сохранением и делить. Для нормализации признаков понадобятся статистики по оси признаков и параметры того же размера. Полные алгоритмы будут построены в последующих главах.

Сначала глава 11 добавит матричное умножение. В отличие от согласования форм, внутренние размеры сомножителей, по которым выполняется суммирование, должны совпадать; это инвариант операции. Выходные размеры определяются строками и столбцами, а не правилом «равны или один равен 1». Явная граница не позволит удобному планировщику поэлементных операций незаметно подменить обучаемые линейные преобразования.