← Все главы

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

Смешайте признаки каждого токена с помощью одной обучаемой проекции

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

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

После главы 18 в каждой позиции токена находится вектор признаков. Возьмите два вектора [1,2][1,2] и [1,3][-1,3] как тензор XX формы [1,2,2][1,2,2] и примените к обеим позициям одни и те же параметры:

W=[10120.51],b=[0.50.51].W= \begin{bmatrix} 1 & 0 & -1 \\ 2 & 0.5 & 1 \end{bmatrix}, \qquad b=\begin{bmatrix}0.5 & -0.5 & 1\end{bmatrix}.

До запуска примера предскажите шесть выходных значений. Для [1,2][1,2] три взвешенные суммы равны [5,1,1][5,1,1], а после добавления bb получается [5.5,0.5,2][5.5,0.5,2]. Для [1,3][-1,3] взвешенные суммы равны [5,1.5,4][5,1.5,4], и то же смещение bb даёт [5.5,1,5][5.5,1,5]. Поэтому полный выход имеет форму [1,2,3][1,2,3] и значения [[[5.5,0.5,2],[5.5,1,5]]][[[5.5,0.5,2],[5.5,1,5]]].

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

Вычислить один скалярный взвешенный отклик для краткого исторического сопоставления с матричной проекцией rust/demos/ch19-linear-layers/src/lib.rs#scalar-weighted-unit
/// Computes one historical scalar weighted response for comparison.
pub fn scalar_weighted_response(input: &[f64], weights: &[f64], bias: f64) -> f64 {
    assert_eq!(input.len(), weights.len());
    input
        .iter()
        .zip(weights)
        .map(|(feature, weight)| feature * weight)
        .sum::<f64>()
        + bias
}

В точном примере один общий слой вычисляет оба варианта: аффинный и выбранный для декодера вариант без смещения:

Применить проекции со смещением и без него из примера главы 19 rust/demos/ch19-linear-layers/src/lib.rs#known-linear-layer
    let layer = known_linear(true);
    let input = TensorValue::parameter(tensor(&INPUT_SHAPE, &INPUT_VALUES))?;
    let output = layer.forward(&input)?;
    let bias_free = known_linear(false);
    let bias_free_output = bias_free
        .forward(&TensorValue::constant(tensor(&INPUT_SHAPE, &INPUT_VALUES))?)?
        .value_snapshot();

Спроецируйте последнюю ось признаков

Общая формула прямого прохода:

Y=XW+bY=XW+b

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

Для обратного режима обозначим через G=L/YG=\partial L/\partial Y градиент скалярной функции потерь LL по YY, а через pp — любую ведущую позицию. Тогда

dXp=GpW,dW=pXpGp,db=pGp.\begin{aligned} dX_p &= G_pW^\top, \\ dW &= \sum_p X_p^\top G_p, \\ db &= \sum_p G_p. \end{aligned}

Результат прямого прохода в каждой позиции зависит только от этой позиции. Поскольку параметры общие, в dWdW и dbdb складываются вклады всех позиций.

Для выбранного входящего градиента точные значения таковы:

G=[[1010.521]],dX=[[210.53]],dW=[0.5223.561],db=[1.520].\begin{aligned} G &= \left[ \begin{bmatrix} 1 & 0 & -1 \\ 0.5 & 2 & 1 \end{bmatrix} \right], \\ dX &= \left[ \begin{bmatrix} 2 & 1 \\ -0.5 & 3 \end{bmatrix} \right], \\ dW &= \begin{bmatrix} 0.5 & -2 & -2 \\ 3.5 & 6 & 1 \end{bmatrix}, \\ db &= \begin{bmatrix}1.5 & 2 & 0\end{bmatrix}. \end{aligned}

Не смешивайте ведущие оси с осью признаков

  • XX — входной тензор формы [,din][\ldots,d_{in}].
  • WW — обучаемая матрица формы [din,dout][d_{in},d_{out}].
  • bb — необязательное смещение ширины doutd_{out}, применяемое к каждой ведущей позиции.
  • YY — выходной тензор формы [,dout][\ldots,d_{out}].
  • dind_{in} — ширина входных признаков и последняя ось XX.
  • doutd_{out} — ширина выходных признаков и последняя ось YY.
  • \ldots обозначает любые сохраняемые ведущие оси: пакета, последовательности или другие.

Матрица формы [din,dout][d_{in},d_{out}] изменяет последнюю ширину. Она не смешивает токены лишь потому, что операция называется матричным умножением. Поэтому при одной и той же матрице весов входы форм [2][2], [2,2][2,2] и [1,2,2][1,2,2] дают выходы форм [3][3], [2,3][2,3] и [1,2,3][1,2,3].

От адаптивных откликов к проекциям во всём Transformer

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

Розенблатт, The Perceptron: Розенблатт описывает архитектуру адаптивного отклика, в которой суммируются возбуждающие и тормозящие сигналы, а подкрепление влияет на выбранный отклик. Эта работа подтверждает лишь описание ранних адаптивных откликов; аффинную формулу и API курса она не задаёт.

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

Бенжио и соавторы, A Neural Probabilistic Language Model: Бенжио и соавторы вычисляют ненормализованные оценки следующего слова по формуле y=b+Wx+Utanh(d+Hx)y=b+Wx+U \tanh(d+Hx), явно вводя обучаемые матричные произведения и аддитивные смещения в нейросетевой языковой модели.

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

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

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

Объедините существующие дифференцируемые операции в именованный слой

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

Задать типизированные ошибки создания и применения линейного слоя rust/crates/llm-from-scratch/src/nn/linear.rs#linear-errors
/// A rejected parameter set, input shape, allocation, or delegated tape operation.
#[derive(Clone, Debug, PartialEq)]
pub enum LinearError {
    Initialization(InitializationError),
    Autodiff(TensorAutodiffError),
    WeightRank { rank: usize },
    ZeroInputWidth,
    ZeroOutputWidth,
    BiasRank { rank: usize },
    BiasWidthMismatch { expected: usize, actual: usize },
    InputRank { rank: usize },
    InputWidthMismatch { expected: usize, actual: usize },
    BiasAllocationFailed { elements: usize },
}

impl fmt::Display for LinearError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Initialization(error) => error.fmt(formatter),
            Self::Autodiff(error) => error.fmt(formatter),
            Self::WeightRank { rank } => {
                write!(
                    formatter,
                    "linear weight must have rank two, got rank {rank}"
                )
            }
            Self::ZeroInputWidth => {
                formatter.write_str("linear input width must be greater than zero")
            }
            Self::ZeroOutputWidth => {
                formatter.write_str("linear output width must be greater than zero")
            }
            Self::BiasRank { rank } => {
                write!(formatter, "linear bias must have rank one, got rank {rank}")
            }
            Self::BiasWidthMismatch { expected, actual } => write!(
                formatter,
                "linear bias width must equal output width {expected}, got {actual}"
            ),
            Self::InputRank { rank } => write!(
                formatter,
                "linear input must have at least one feature axis, got rank {rank}"
            ),
            Self::InputWidthMismatch { expected, actual } => write!(
                formatter,
                "linear input final width must equal {expected}, got {actual}"
            ),
            Self::BiasAllocationFailed { elements } => write!(
                formatter,
                "could not reserve storage for {elements} linear bias values"
            ),
        }
    }
}

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

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

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

Linear хранит именованную матрицу весов формы [din,dout][d_{in},d_{out}] и необязательное именованное смещение. Фактическое матричное произведение и сложение с согласованием форм выполняют операции автодифференцирования из главы 16. Вход ранга один временно получает дополнительную ось лишь на время уже существующего матричного умножения, после чего снова становится вектором:

Хранить именованные параметры проекции и сохранять все ведущие оси rust/crates/llm-from-scratch/src/nn/linear.rs#linear-layer
/// One `[input_width, output_width]` feature projection with optional bias.
#[derive(Clone, Debug)]
pub struct Linear {
    parameters: NamedParameters,
    input_width: usize,
    output_width: usize,
    has_bias: bool,
}

impl Linear {
    /// Initializes a named weight and optional zero bias without partially advancing `rng`.
    pub fn new(
        parameter_prefix: impl Into<String>,
        input_width: usize,
        output_width: usize,
        with_bias: bool,
        rng: &mut SplitMix64,
    ) -> Result<Self, LinearError> {
        let parameter_prefix = parameter_prefix.into();
        let mut trial = rng.clone();
        let weight = NamedParameter::xavier_uniform(
            format!("{parameter_prefix}.weight"),
            input_width,
            output_width,
            &mut trial,
        )
        .map_err(|error| match error {
            InitializationError::ZeroFanIn => LinearError::ZeroInputWidth,
            InitializationError::ZeroFanOut => LinearError::ZeroOutputWidth,
            other => LinearError::Initialization(other),
        })?;

        let bias = if with_bias {
            let mut values = Vec::new();
            values.try_reserve_exact(output_width).map_err(|_| {
                LinearError::BiasAllocationFailed {
                    elements: output_width,
                }
            })?;
            values.resize(output_width, 0.0);
            let tensor = Tensor::from_vec(vec![output_width], values)
                .map_err(InitializationError::Tensor)?;
            Some(NamedParameter::from_tensor(
                format!("{parameter_prefix}.bias"),
                tensor,
            )?)
        } else {
            None
        };

        let layer = Self::from_parameters(weight, bias)?;
        *rng = trial;
        Ok(layer)
    }

    /// Gives layer semantics to an existing weight and optional bias.
    pub fn from_parameters(
        weight: NamedParameter,
        bias: Option<NamedParameter>,
    ) -> Result<Self, LinearError> {
        let weight_shape = weight.tensor().shape();
        if weight_shape.len() != 2 {
            return Err(LinearError::WeightRank {
                rank: weight_shape.len(),
            });
        }
        let input_width = weight_shape[0];
        let output_width = weight_shape[1];
        if input_width == 0 {
            return Err(LinearError::ZeroInputWidth);
        }
        if output_width == 0 {
            return Err(LinearError::ZeroOutputWidth);
        }

        if let Some(parameter) = &bias {
            let bias_shape = parameter.tensor().shape();
            if bias_shape.len() != 1 {
                return Err(LinearError::BiasRank {
                    rank: bias_shape.len(),
                });
            }
            if bias_shape[0] != output_width {
                return Err(LinearError::BiasWidthMismatch {
                    expected: output_width,
                    actual: bias_shape[0],
                });
            }
        }

        let has_bias = bias.is_some();
        let mut parameters = vec![weight];
        if let Some(bias) = bias {
            parameters.push(bias);
        }
        Ok(Self {
            parameters: NamedParameters::try_new(parameters)?,
            input_width,
            output_width,
            has_bias,
        })
    }

    /// Projects only the final feature axis and preserves every leading axis.
    pub fn forward(&self, input: &TensorValue) -> Result<TensorValue, LinearError> {
        let input_shape = input.shape();
        if input_shape.is_empty() {
            return Err(LinearError::InputRank { rank: 0 });
        }
        let actual_width = *input_shape.last().expect("nonempty input shape");
        if actual_width != self.input_width {
            return Err(LinearError::InputWidthMismatch {
                expected: self.input_width,
                actual: actual_width,
            });
        }

        let projected = if input_shape.len() == 1 {
            let promoted = input.reshape(&[1, self.input_width])?;
            let output = promoted.matmul(self.weight().tensor())?;
            let output = match self.bias() {
                Some(bias) => output.add(bias.tensor())?,
                None => output,
            };
            output.reshape(&[self.output_width])?
        } else {
            let output = input.matmul(self.weight().tensor())?;
            match self.bias() {
                Some(bias) => output.add(bias.tensor())?,
                None => output,
            }
        };
        Ok(projected)
    }

    pub fn weight(&self) -> &NamedParameter {
        &self.parameters.as_slice()[0]
    }

    pub fn bias(&self) -> Option<&NamedParameter> {
        self.has_bias.then(|| &self.parameters.as_slice()[1])
    }

    pub fn parameters(&self) -> &[NamedParameter] {
        self.parameters.as_slice()
    }

    pub const fn input_width(&self) -> usize {
        self.input_width
    }

    pub const fn output_width(&self) -> usize {
        self.output_width
    }

    pub const fn has_bias(&self) -> bool {
        self.has_bias
    }

    pub const fn parameter_count(&self) -> usize {
        let weight_count = self.input_width * self.output_width;
        if self.has_bias {
            weight_count + self.output_width
        } else {
            weight_count
        }
    }
}

Неравномерный входящий градиент показывает все три пути обратного прохода:

Провести обратный проход через аффинную проекцию из примера главы 19 rust/demos/ch19-linear-layers/src/lib.rs#linear-gradients
    let upstream = tensor(&UPSTREAM_SHAPE, &UPSTREAM_VALUES);
    output.backward_with_seed(&upstream.view(), GraphRetention::Retain)?;
    let input_gradient = input
        .gradient_snapshot()
        .expect("trainable input stores its exact gradient");
    let weight_gradient = layer
        .weight()
        .tensor()
        .gradient_snapshot()
        .expect("trainable weight stores its exact gradient");
    let bias_gradient = layer
        .bias()
        .expect("affine fixture owns bias")
        .tensor()
        .gradient_snapshot()
        .expect("trainable bias stores its exact gradient");

Инициализация использует детерминированную схему Ксавье из главы 17. Запрошенное смещение начинается с точного нуля, одинаковые начальные значения генератора воспроизводят значения весов, а клон намеренно сохраняет те же листья параметров:

Воспроизводимо инициализировать слой и проверить, что клон ссылается на те же листья параметров rust/demos/ch19-linear-layers/src/lib.rs#initialized-linear-layer
    let mut first_rng = SplitMix64::from_seed(19);
    let mut second_rng = SplitMix64::from_seed(19);
    let initialized = Linear::new("token_projection", 2, 3, true, &mut first_rng)?;
    let reproduced = Linear::new("token_projection", 2, 3, true, &mut second_rng)?;
    let initialized_reproducible =
        *initialized.weight().tensor().value() == *reproduced.weight().tensor().value();
    let initialized_bias_zero = initialized
        .bias()
        .expect("requested bias")
        .tensor()
        .value()
        .as_slice()
        .iter()
        .all(|&value| value == 0.0);
    let cloned = initialized.clone();
    let clone_shares_weight = initialized
        .weight()
        .tensor()
        .is_same_node(cloned.weight().tensor());
    let clone_shares_bias = initialized
        .bias()
        .expect("requested bias")
        .tensor()
        .is_same_node(cloned.bias().expect("cloned bias").tensor());

Полный отчёт также показывает поведение вектора, последовательности, мини-пакета, пустой ведущей оси и ошибочных входов:

Подготовить детерминированный отчёт по линейным слоям главы 19 rust/demos/ch19-linear-layers/src/main.rs#learner-linear-layers-output
    let report = learner_report()?;

Запустите cargo run --quiet --locked -p ch19-linear-layers, чтобы увидеть весь отчёт. Он демонстрирует точные значения прямого и обратного проходов, восстановление формы вектора, стабильный порядок и идентичность параметров, безопасную инициализацию и согласие с выборочными конечными разностями при шаге 10610^{-6} и абсолютном допуске 2×1062\times10^{-6}.

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

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

Записать точные вклады проекции, политику смещения, оси и градиенты rust/demos/ch19-linear-layers/src/diagram_trace.rs#linear-layers-trace
pub fn render_trace() -> Result<String, Box<dyn Error>> {
    let layer = known_linear(true);
    let input = TensorValue::parameter(tensor(&INPUT_SHAPE, &INPUT_VALUES))?;
    let output = layer.forward(&input)?;
    let upstream = tensor(&UPSTREAM_SHAPE, &UPSTREAM_VALUES);
    output.backward_with_seed(&upstream.view(), GraphRetention::Retain)?;
    let input_gradient = input.gradient().expect("input gradient");
    let weight_gradient = layer.weight().tensor().gradient().expect("weight gradient");
    let bias_gradient = layer
        .bias()
        .expect("affine fixture bias")
        .tensor()
        .gradient()
        .expect("bias gradient");
    let input_value = input.value();
    let output_value = output.value();
    let weight_value = layer.weight().tensor().value();
    let bias_value = layer.bias().expect("affine fixture bias").tensor().value();
    let bias_free = known_linear(false);
    let bias_free_output = bias_free
        .forward(&TensorValue::constant(tensor(&INPUT_SHAPE, &INPUT_VALUES))?)?
        .value_snapshot();

    let mut trace = String::new();
    writeln!(trace, "TRACE linear-layers-v1 BEGIN")?;
    writeln!(
        trace,
        "FIXTURE name=known-affine-projection parameter-prefix=token_projection input-width={} output-width={} bias={} parameter-count={} input-shape={} output-shape={} upstream-shape={}",
        layer.input_width(),
        layer.output_width(),
        layer.has_bias(),
        layer.parameter_count(),
        shape(input_value.shape()),
        shape(output_value.shape()),
        shape(&UPSTREAM_SHAPE),
    )?;
    writeln!(trace, "INPUT values={}", fixed_list(input_value.as_slice()))?;
    for input_feature in 0..layer.input_width() {
        let start = input_feature * layer.output_width();
        writeln!(
            trace,
            "WEIGHT-ROW input-feature={input_feature} values={}",
            fixed_list(&weight_value.as_slice()[start..start + layer.output_width()])
        )?;
    }
    writeln!(trace, "BIAS values={}", fixed_list(bias_value.as_slice()))?;

    for position in 0..INPUT_SHAPE[1] {
        let input_start = position * INPUT_SHAPE[2];
        let output_start = position * layer.output_width();
        let position_input =
            &input_value.as_slice()[input_start..input_start + layer.input_width()];
        for (output_feature, &bias) in bias_value.as_slice().iter().enumerate() {
            let result = output_value.as_slice()[output_start + output_feature];
            writeln!(
                trace,
                "CELL position={position} coordinate=0,{position} output-feature={output_feature} input={} products={} weighted-sum={} bias={} result={}",
                fixed_list(position_input),
                products(
                    position_input,
                    output_feature,
                    weight_value.as_slice(),
                    layer.output_width(),
                ),
                fixed(result - bias),
                fixed(bias),
                fixed(result)
            )?;
        }
        writeln!(
            trace,
            "POSITION-GRADIENT position={position} coordinate=0,{position} upstream={} input-gradient={}",
            fixed_list(&UPSTREAM_VALUES[output_start..output_start + layer.output_width()]),
            fixed_list(&input_gradient.as_slice()[input_start..input_start + layer.input_width()])
        )?;
    }

    writeln!(
        trace,
        "WEIGHT-GRADIENT shape={} values={}",
        shape(weight_gradient.shape()),
        fixed_list(weight_gradient.as_slice())
    )?;
    writeln!(
        trace,
        "BIAS-GRADIENT shape={} values={}",
        shape(bias_gradient.shape()),
        fixed_list(bias_gradient.as_slice())
    )?;
    writeln!(
        trace,
        "POLICY affine-parameters={} bias-free-parameters={} bias-free-output={}",
        layer.parameter_count(),
        bias_free.parameter_count(),
        fixed_list(bias_free_output.as_slice())
    )?;
    writeln!(
        trace,
        "AXES input-leading={} output-leading={} preserved=true mixed-axis=feature",
        shape(&input_value.shape()[..input_value.shape().len() - 1]),
        shape(&output_value.shape()[..output_value.shape().len() - 1]),
    )?;
    writeln!(trace, "TRACE linear-layers-v1 END")?;
    Ok(trace)
}

Проследите одну общую проекцию для двух позиций токенов

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

Именованный параметр
token_projection.weight
Входных признаков
22
Выходных признаков
33
Смещение
Включено
Скалярных параметров
99
Форма входа
[1,2,2]\left[1,2,2\right]
Форма выхода
[1,2,3]\left[1,2,3\right]
Форма входящего градиента
[1,2,3]\left[1,2,3\right]

Сохраните позиции, измените ширину признаков

Ведущие координаты пакета и последовательности не меняются. Только последняя ось признаков расширяется с двух координат до трёх.

Входной вектор [1,2,2]\left[1,2,2\right]

Ведущие оси сохраняются

Выход [1,2,3]\left[1,2,3\right]

Смешивается только ось признаков [1,2]=[1,2]\left[1,2\right]=\left[1,2\right]

Используйте одну матрицу весов и смещение

Параметр Значение
WW [10120.51]\begin{bmatrix}1&0&-1\\2&0.5&1\end{bmatrix}
Смещение [0.5,0.5,1]\left[0.5,-0.5,1\right]

Спроецируйте каждую позицию независимо

Позиция Ведущая координата Входной вектор Выход
00 (0,0)\left(0,0\right) [1,2]\left[1,2\right] [5.5,0.5,2]\left[5.5,0.5,2\right]
11 (0,1)\left(0,1\right) [1,3]\left[-1,3\right] [5.5,1,5]\left[5.5,1,5\right]

Рассчитайте одну координату выхода

Первая координата выхода зависит от обоих входных признаков: их произведения дают взвешенную сумму, к которой добавляется смещение.

Ведущая координата
(0,0),  y0\left(0,0\right),\;y_{0}
Результат
y0=11+22+0.5=5+0.5=5.5\begin{aligned}y_{0}&=1\cdot1+2\cdot2+0.5\\&=5+0.5=5.5\end{aligned}

Сравните вариант со смещением и без него

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

Позиция Выход
(0,0)\left(0,0\right)
Аффинная проекция Скалярных параметров: 99 [5.5,0.5,2]\left[5.5,0.5,2\right]
Проекция без смещения Скалярных параметров: 66 [5,1,1]\left[5,1,1\right]
(0,1)\left(0,1\right)
Аффинная проекция Скалярных параметров: 99 [5.5,1,5]\left[5.5,1,5\right]
Проекция без смещения Скалярных параметров: 66 [5,1.5,4]\left[5,1.5,4\right]

Накопите градиенты общих параметров

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

Градиенты отдельных позиций
pp (b,t)(b,t) GpG_p dXpdX_p
dX0dX_{0} (0,0)\left(0,0\right) [1,0,1]\left[1,0,-1\right] [2,1]\left[2,1\right]
dX1dX_{1} (0,1)\left(0,1\right) [0.5,2,1]\left[0.5,2,1\right] [0.5,3]\left[-0.5,3\right]
Градиенты общих параметров
θ\theta p\sum_p dθd\theta
dWdW pXpGp\sum_p X_p^\top G_p [2,3]\left[2,3\right] [0.5223.561]\begin{bmatrix}0.5&-2&-2\\3.5&6&1\end{bmatrix}
dbdb pGp\sum_p G_p [3]\left[3\right] [1.5,2,0]\left[1.5,2,0\right]

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

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

  1. Предскажите форму выхода для [4,7,2][4,7,2] при матрице весов [2,3][2,3].
  2. Для рассматриваемого слоя предскажите аффинный выход для [0,0][0,0] и выход без смещения.
  3. Определите, может ли изменение вектора одного токена изменить выход другой позиции в этом слое.
  4. Для отдельной проекции с dout=2d_{out}=2 найдите dbdb, если три позиции получают входящие градиенты [1,0][1,0], [0,2][0,2] и [3,4][3,4].
  5. Предскажите число параметров проекции [2,3][2,3] со смещением и без него.
  6. Определите, допустимы ли матрица весов [2,0][2,0] и вход [0,2][0,2] для слоя [2,3][2,3].
  7. Объясните, почему преобразование с ненулевым смещением неточно называть строго линейным.
  8. Укажите, какой источник подтверждает аффинные вычисления внутри языковой модели и какие правила реализации не задаёт ни одна из статей.
Сверить предсказания
  1. Форма выхода — [4,7,3][4,7,3].
  2. Аффинный выход равен [0.5,0.5,1][0.5,-0.5,1], а без смещения получается [0,0,0][0,0,0].
  3. Нет. Общая матрица действует независимо в каждой ведущей координате.
  4. Градиент смещения равен сумме по позициям [4,6][4,6].
  5. Слой со смещением содержит девять скаляров, а без смещения — шесть.
  6. Нулевая ширина выхода недопустима; у формы [0,2][0,2] пустая ведущая ось и допустимая последняя ширина, поэтому выход имеет форму [0,3][0,3].
  7. При ненулевом аддитивном смещении отображение не сохраняет начало координат, поэтому оно аффинное.
  8. Бенжио и соавторы подтверждают аффинные вычисления в нейросетевой языковой модели. Ориентация, ошибки, имена, начальное значение генератора, API и целевая политика смещений — решения курса.

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

Совокупная модель теперь содержит именованную дифференцируемую проекцию из [,din][\ldots,d_{in}] в [,dout][\ldots,d_{out}]. Та же абстракция создаст запросы, ключи, значения, выходы внимания, ветви сети прямого распространения и оценки словаря. В целевом декодере эти пути не используют смещение.

Последовательность проекций без смещения всё ещё сворачивается в одну проекцию: (XW1)W2=X(W1W2)(XW_1)W_2=X(W_1W_2). В главе 20 между проекциями появится нелинейный вентиль SiLU, после чего такое свёртывание станет невозможным.