19 · Версия материала 5
Смешайте признаки каждого токена с помощью одной обучаемой проекции
Постройте на Rust обучаемый линейный слой, сохраните ведущие оси токенов, сравните аффинную проекцию с проекцией без смещения и проверьте точные градиенты обратного прохода.
Предскажите два выхода одной общей матрицы
После главы 18 в каждой позиции токена находится вектор признаков. Возьмите два вектора и как тензор формы и примените к обеим позициям одни и те же параметры:
До запуска примера предскажите шесть выходных значений. Для три взвешенные суммы равны , а после добавления получается . Для взвешенные суммы равны , и то же смещение даёт . Поэтому полный выход имеет форму и значения .
Один скалярный взвешенный отклик удобен как небольшое алгебраическое сравнение. Он совпадает с первой координатой выхода, но не является отдельной реализацией матричной операции:
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
} В точном примере один общий слой вычисляет оба варианта: аффинный и выбранный для декодера вариант без смещения:
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(); Спроецируйте последнюю ось признаков
Общая формула прямого прохода:
Матрица задаёт взвешенную комбинацию всех входных признаков для каждого выходного признака. Необязательный вектор добавляется к каждой ведущей позиции; когда смещение отключено, этого слагаемого нет. Такой модуль принято называть линейным слоем, однако при ненулевом смещении отображение является аффинным, а не строго линейным.
Для обратного режима обозначим через градиент скалярной функции потерь по , а через — любую ведущую позицию. Тогда
Результат прямого прохода в каждой позиции зависит только от этой позиции. Поскольку параметры общие, в и складываются вклады всех позиций.
Для выбранного входящего градиента точные значения таковы:
Не смешивайте ведущие оси с осью признаков
- — входной тензор формы .
- — обучаемая матрица формы .
- — необязательное смещение ширины , применяемое к каждой ведущей позиции.
- — выходной тензор формы .
- — ширина входных признаков и последняя ось .
- — ширина выходных признаков и последняя ось .
- обозначает любые сохраняемые ведущие оси: пакета, последовательности или другие.
Матрица формы изменяет последнюю ширину. Она не смешивает токены лишь потому, что операция называется матричным умножением. Поэтому при одной и той же матрице весов входы форм , и дают выходы форм , и .
От адаптивных откликов к проекциям во всём Transformer
Один скалярный взвешенный отклик — лишь локальная арифметика внутри адаптивной системы. Языковой модели нужны векторы скрытых активаций и оценки для всего словаря в каждой позиции контекста; если представлять каждый выход отдельным скалярным элементом, общий матричный расчёт остаётся неявным.
Розенблатт, The Perceptron: Розенблатт описывает архитектуру адаптивного отклика, в которой суммируются возбуждающие и тормозящие сигналы, а подкрепление влияет на выбранный отклик. Эта работа подтверждает лишь описание ранних адаптивных откликов; аффинную формулу и API курса она не задаёт.
Бенжио и соавторы выражают вычисления скрытого слоя и выхода нейросетевой языковой модели через обучаемые матрицы и аддитивные смещения. Затем Transformer многократно использует обучаемые проекции для запросов, ключей, значений, выходов внимания, преобразований сети прямого распространения, одинаковых для каждой позиции, и оценок следующего токена.
Бенжио и соавторы, A Neural Probabilistic Language Model: Бенжио и соавторы вычисляют ненормализованные оценки следующего слова по формуле , явно вводя обучаемые матричные произведения и аддитивные смещения в нейросетевой языковой модели.
Васвани и соавторы, 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 хранит именованную матрицу весов формы и
необязательное именованное смещение. Фактическое матричное произведение и
сложение с согласованием форм выполняют операции автодифференцирования из главы
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
}
}
} Неравномерный входящий градиент показывает все три пути обратного прохода:
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()); Полный отчёт также показывает поведение вектора, последовательности, мини-пакета, пустой ведущей оси и ошибочных входов:
rust/demos/ch19-linear-layers/src/main.rs#learner-linear-layers-output let report = learner_report()?; Запустите cargo run --quiet --locked -p ch19-linear-layers, чтобы увидеть весь
отчёт. Он демонстрирует точные значения прямого и обратного проходов,
восстановление формы вектора, стабильный порядок и идентичность параметров,
безопасную инициализацию и согласие с выборочными конечными разностями при шаге
и абсолютном допуске .
Проследите позиции, произведения, политику смещения и градиенты
Пример на 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- Входных признаков
- Выходных признаков
- Смещение
- Включено
- Скалярных параметров
- Форма входа
- Форма выхода
- Форма входящего градиента
Сохраните позиции, измените ширину признаков
Ведущие координаты пакета и последовательности не меняются. Только последняя ось признаков расширяется с двух координат до трёх.
Ведущие оси сохраняются
Смешивается только ось признаков
Используйте одну матрицу весов и смещение
| Параметр | Значение |
|---|---|
| Смещение |
Спроецируйте каждую позицию независимо
| Позиция | Ведущая координата | Входной вектор | Выход |
|---|---|---|---|
Рассчитайте одну координату выхода
Первая координата выхода зависит от обоих входных признаков: их произведения дают взвешенную сумму, к которой добавляется смещение.
- Ведущая координата
- Результат
Сравните вариант со смещением и без него
Аффинный вариант добавляет по одной общей координате смещения к каждому выходному признаку. В целевом декодере проекции внимания, SwiGLU и словаря смещение не используют.
| Позиция | Выход |
|---|---|
| Аффинная проекция Скалярных параметров: Проекция без смещения Скалярных параметров: | |
| Аффинная проекция Скалярных параметров: Проекция без смещения Скалярных параметров: |
Накопите градиенты общих параметров
Каждая входная позиция получает собственный градиент. Общие веса и смещение накапливают вклады обеих позиций.
Сначала прочитайте строки позиций: каждая ведущая координата обрабатывается независимо. Затем сравните выходы со смещением и без него, а в конце сопоставьте локальные градиенты входа с суммами, накопленными в общих параметрах.
Сначала предскажите, затем сверьтесь с вычислениями
- Предскажите форму выхода для при матрице весов .
- Для рассматриваемого слоя предскажите аффинный выход для и выход без смещения.
- Определите, может ли изменение вектора одного токена изменить выход другой позиции в этом слое.
- Для отдельной проекции с найдите , если три позиции получают входящие градиенты , и .
- Предскажите число параметров проекции со смещением и без него.
- Определите, допустимы ли матрица весов и вход для слоя .
- Объясните, почему преобразование с ненулевым смещением неточно называть строго линейным.
- Укажите, какой источник подтверждает аффинные вычисления внутри языковой модели и какие правила реализации не задаёт ни одна из статей.
Сверить предсказания
- Форма выхода — .
- Аффинный выход равен , а без смещения получается .
- Нет. Общая матрица действует независимо в каждой ведущей координате.
- Градиент смещения равен сумме по позициям .
- Слой со смещением содержит девять скаляров, а без смещения — шесть.
- Нулевая ширина выхода недопустима; у формы пустая ведущая ось и допустимая последняя ширина, поэтому выход имеет форму .
- При ненулевом аддитивном смещении отображение не сохраняет начало координат, поэтому оно аффинное.
- Бенжио и соавторы подтверждают аффинные вычисления в нейросетевой языковой модели. Ориентация, ошибки, имена, начальное значение генератора, API и целевая политика смещений — решения курса.
Передайте переиспользуемые проекции первому блоку с вентилем
Совокупная модель теперь содержит именованную дифференцируемую проекцию из в . Та же абстракция создаст запросы, ключи, значения, выходы внимания, ветви сети прямого распространения и оценки словаря. В целевом декодере эти пути не используют смещение.
Последовательность проекций без смещения всё ещё сворачивается в одну проекцию: . В главе 20 между проекциями появится нелинейный вентиль SiLU, после чего такое свёртывание станет невозможным.