25 · Версия материала 5
Нормализуйте масштаб, не вычитая среднее
Реализуйте RMSNorm по последней оси, проследите градиенты по входу и коэффициенту масштаба и отделите идеальную инвариантность от поведения вблизи нуля, где преобладает эпсилон.
Предскажите RMS, затем проверьте границу влияния эпсилона
Возьмите , обучаемый коэффициент масштаба и . Прежде чем запускать пример, вычислите среднее квадратов:
Величина, обратная RMS, приблизительно равна . Предскажите нормализованный вектор до применения коэффициента, а затем покоординатно умножьте его на . Пример должен дать примерно до применения коэффициента и после него.
Теперь умножьте вход на положительный коэффициент . Чтобы проверить инвариантность к масштабу, рассматривайте — вектор после масштабирования по RMS, но до применения обучаемого коэффициента . Итоговый выход после применения в это сравнение не входит. Сначала предскажите при , затем — при . Повторите то же сравнение для , по-прежнему не применяя обучаемый коэффициент; среднее квадратов этого вектора меньше стабилизирующей добавки.
Нормализуйте последнюю ось признаков
Для одного вектора на последней оси RMSNorm имеет вид
Одна и та же статистика используется для всех координат этого вектора. Затем обучаемый коэффициент отдельно масштабирует каждый признак. Среднее по признакам не вычитается, а статистики одного примера не зависят от других.
Для положительного скаляра и ненулевого вектора идеальная формула с нулевым эпсилоном удовлетворяет равенству
При конечном эпсилоне знаменатель становится равен : эпсилон не растёт вместе с сигналом. Среднее квадратов нормализованного вектора равно
Когда сигнал значительно больше эпсилона, это значение приближается к ; когда эпсилон преобладает — к . Поэтому на практике RMSNorm приближённо инвариантна к масштабу вдали от нуля, но не обладает точной инвариантностью для всех входов.
Не смешивайте масштаб, коэффициент и оси
- — один вектор признаков, взятый с последней оси входа.
- — ненулевая ширина этой оси, а выбирает одну координату.
- — координата , а — среднее квадратов координат вектора .
- стабилизирует вычисление обратного квадратного корня.
- — вектор после масштабирования по RMS, но до обучаемого покоординатного масштабирования.
- — обучаемый вектор коэффициентов масштаба, а обозначает покоординатное умножение.
- Полная форма выхода совпадает с формой входа.
RMSNorm не приводит модуль каждой координаты к . Она управляет совокупным среднеквадратичным масштабом вектора. Кроме того, RMSNorm не центрирует вектор, не обрезает значения и не смешивает примеры мини-батча.
От статистик мини-пакета к языковым моделям с предварительной RMSNorm
В BatchNorm результат для обучающего примера зависит от статистик мини-батча. LayerNorm устраняет зависимость между примерами, но перед масштабированием по-прежнему вычисляет и вычитает среднее по признакам отдельного примера.
Первый ранний первоисточник — Иоффе и Сегеди, Batch Normalization. Иоффе и Сегеди включают нормализацию в архитектуру и вычисляют её статистики отдельно для каждого мини-батча при обучении.
Второй ранний источник — Ба, Кирос и Хинтон, Layer Normalization. Ба, Кирос и Хинтон вычисляют среднее и дисперсию по суммарным входным сигналам нейронов слоя для одного обучающего примера, устраняя зависимость между примерами.
RMSNorm отказывается от вычитания среднего и сохраняет масштабирование вектора признаков по его среднеквадратичному значению; позднее в LLaMA RMSNorm применили перед каждым подслоем Transformer.
Первый более поздний источник — Чжан и Зеннрих, Root Mean Square Layer Normalization. Чжан и Зеннрих исключают среднее из статистик, нормализуют по среднеквадратичному значению и сохраняют инвариантность формулы без эпсилона к положительному масштабированию, отказавшись от инвариантности к центрированию. В формуле статьи эпсилона нет. В этой реализации положительный эпсилон добавлен, чтобы нулевые и очень малые векторы оставались конечными; поведение вблизи нуля следует из этого изменённого знаменателя.
Второй более поздний источник — Туврон и соавторы, LLaMA. Туврон и соавторы нормализуют вход каждого подслоя Transformer и используют для этого RMSNorm.
В декодере с предварительной нормализацией в каждую ветвь внимания или сети прямого распространения поступает остаточный поток с контролируемым масштабом, а тождественный путь проходит в обход нормализации.
Исполняемое сравнение фиксирует опорный вектор и меняет только другой пример в том же батче. Выход BatchNorm для опорного вектора меняется; LayerNorm и RMSNorm обрабатывают каждый пример независимо. LayerNorm центрирует опорный вектор, а RMSNorm сохраняет ненулевое среднее выхода:
rust/demos/ch25-rmsnorm/src/lib.rs#historical-normalization-contrast fn batch_normalize_anchor(anchor: &[f64], companion: &[f64], epsilon: f64) -> Vec<f64> {
anchor
.iter()
.zip(companion)
.map(|(&anchor, &companion)| {
let mean = (anchor + companion) / 2.0;
let variance = ((anchor - mean).powi(2) + (companion - mean).powi(2)) / 2.0;
(anchor - mean) / (variance + epsilon).sqrt()
})
.collect()
}
fn layer_normalize(values: &[f64], epsilon: f64) -> Vec<f64> {
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance = values
.iter()
.map(|value| (value - mean).powi(2))
.sum::<f64>()
/ values.len() as f64;
values
.iter()
.map(|value| (value - mean) / (variance + epsilon).sqrt())
.collect()
}
fn historical_evidence() -> Result<HistoryEvidence, FixtureError> {
let anchor = [1.0, 3.0];
let batch_anchor_a = batch_normalize_anchor(&anchor, &[5.0, 7.0], EPSILON);
let batch_anchor_b = batch_normalize_anchor(&anchor, &anchor, EPSILON);
let layer_norm = layer_normalize(&anchor, EPSILON);
let rms_norm = normalized(&anchor, IDEAL_EPSILON)?;
let rms_mean = rms_norm.iter().sum::<f64>() / rms_norm.len() as f64;
Ok(HistoryEvidence {
batch_anchor_a,
batch_anchor_b,
layer_norm,
rms_norm,
rms_mean,
})
} Соберите RMSNorm из уже реализованных операций ленты
RmsNorm владеет одним одномерным NamedParameter и не имеет смещения. Сначала
проверяются эпсилон, имя и ширина коэффициента масштаба, ранг входа и ширина
последней оси. Затем прямой проход вычисляет RMSNorm как последовательность уже
реализованных операций: умножения, среднего по последней оси с сохранением
размерности, добавления скаляра, логарифма, экспоненты, согласования форм
(broadcasting) и ещё одного умножения. Поэтому и вход, и коэффициент остаются
связаны с общей лентой
обратного автоматического дифференцирования:
rust/crates/llm-from-scratch/src/nn/rmsnorm.rs#rmsnorm-layer /// Inspectable tensors from one composed RMSNorm forward pass.
#[derive(Clone, Debug)]
pub struct RmsNormForward {
mean_square: TensorValue,
inverse_rms: TensorValue,
normalized: TensorValue,
output: TensorValue,
}
impl RmsNormForward {
pub fn mean_square(&self) -> &TensorValue {
&self.mean_square
}
pub fn inverse_rms(&self) -> &TensorValue {
&self.inverse_rms
}
pub fn normalized(&self) -> &TensorValue {
&self.normalized
}
pub fn output(&self) -> &TensorValue {
&self.output
}
pub fn into_output(self) -> TensorValue {
self.output
}
}
/// One learned gain applied after final-axis root-mean-square rescaling.
#[derive(Clone, Debug)]
pub struct RmsNorm {
epsilon: f64,
feature_width: usize,
parameters: NamedParameters,
}
impl RmsNorm {
/// Creates a gain vector initialized to one.
pub fn new(
gain_name: impl Into<String>,
feature_width: usize,
epsilon: f64,
) -> Result<Self, RmsNormError> {
validate_epsilon(epsilon)?;
if feature_width == 0 {
return Err(RmsNormError::EmptyFeatureWidth);
}
let gain_name = gain_name.into();
validate_name(&gain_name)?;
let mut values = Vec::new();
values.try_reserve_exact(feature_width).map_err(|_| {
RmsNormError::GainAllocationFailed {
elements: feature_width,
}
})?;
values.resize(feature_width, 1.0);
let gain = NamedParameter::from_tensor(
gain_name,
Tensor::from_vec(vec![feature_width], values).map_err(InitializationError::from)?,
)?;
Self::from_gain(gain, epsilon)
}
/// Builds the layer from one externally named rank-one trainable gain.
pub fn from_gain(gain: NamedParameter, epsilon: f64) -> Result<Self, RmsNormError> {
validate_epsilon(epsilon)?;
let shape = gain.tensor().shape();
if shape.len() != 1 {
return Err(RmsNormError::GainRank { shape });
}
let feature_width = shape[0];
if feature_width == 0 {
return Err(RmsNormError::EmptyFeatureWidth);
}
Ok(Self {
epsilon,
feature_width,
parameters: NamedParameters::try_new(vec![gain])?,
})
}
/// Normalizes the final feature axis and returns only the scaled output.
pub fn forward(&self, input: &TensorValue) -> Result<TensorValue, RmsNormError> {
self.forward_with_intermediates(input)
.map(RmsNormForward::into_output)
}
/// Normalizes the final feature axis and preserves each teaching value.
pub fn forward_with_intermediates(
&self,
input: &TensorValue,
) -> Result<RmsNormForward, RmsNormError> {
let shape = input.shape();
let Some(&actual_width) = shape.last() else {
return Err(RmsNormError::InputRankZero);
};
if actual_width != self.feature_width {
return Err(RmsNormError::InputWidthMismatch {
expected: self.feature_width,
actual: actual_width,
});
}
let feature_axis = shape.len() - 1;
let squared = input
.mul(input)
.map_err(autodiff_error(RmsNormStage::Square))?;
let mean_square = squared
.mean_axis(feature_axis, true)
.map_err(autodiff_error(RmsNormStage::MeanSquare))?;
if self.epsilon == 0.0 {
for (row, value) in mean_square.value().as_slice().iter().enumerate() {
if *value == 0.0 {
return Err(RmsNormError::ZeroEnergyRow { row });
}
}
}
let epsilon = scalar_constant(self.epsilon, RmsNormStage::EpsilonConstant)?;
let stabilized = mean_square
.add(&epsilon)
.map_err(autodiff_error(RmsNormStage::Stabilize))?;
let log_mean_square = stabilized
.log()
.map_err(autodiff_error(RmsNormStage::LogMeanSquare))?;
let exponent = scalar_constant(-0.5, RmsNormStage::ExponentConstant)?;
let scaled_log = log_mean_square
.mul(&exponent)
.map_err(autodiff_error(RmsNormStage::ScaleLog))?;
let inverse_rms = scaled_log
.exp()
.map_err(autodiff_error(RmsNormStage::ReciprocalRoot))?;
let normalized = input
.mul(&inverse_rms)
.map_err(autodiff_error(RmsNormStage::Normalize))?;
let output = normalized
.mul(self.gain().tensor())
.map_err(autodiff_error(RmsNormStage::ApplyGain))?;
Ok(RmsNormForward {
mean_square,
inverse_rms,
normalized,
output,
})
}
pub fn gain(&self) -> &NamedParameter {
&self.parameters.as_slice()[0]
}
pub fn parameters(&self) -> &[NamedParameter] {
self.parameters.as_slice()
}
pub const fn feature_width(&self) -> usize {
self.feature_width
}
pub const fn epsilon(&self) -> f64 {
self.epsilon
}
} При положительном эпсилоне полностью нулевая строка превращается в конечные нули. Если эпсилон равен нулю, строка с вычисленным средним квадратов, равным нулю, отклоняется до логарифма. Это относится и к ненулевой строке, квадраты элементов которой обнулились из-за потери значащих разрядов. Типизированные ошибки делают эту границу явной:
rust/crates/llm-from-scratch/src/nn/rmsnorm.rs#rmsnorm-errors /// A rejected RMSNorm configuration, input, or delegated tape operation.
#[derive(Clone, Debug, PartialEq)]
pub enum RmsNormError {
InvalidEpsilon {
value: f64,
},
EmptyFeatureWidth,
GainRank {
shape: Vec<usize>,
},
GainAllocationFailed {
elements: usize,
},
InputRankZero,
InputWidthMismatch {
expected: usize,
actual: usize,
},
ZeroEnergyRow {
row: usize,
},
Initialization(InitializationError),
Autodiff {
stage: RmsNormStage,
source: TensorAutodiffError,
},
}
impl fmt::Display for RmsNormError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidEpsilon { value } => write!(
formatter,
"RMSNorm epsilon must be finite and nonnegative, got {value:?}"
),
Self::EmptyFeatureWidth => {
formatter.write_str("RMSNorm feature width must be greater than zero")
}
Self::GainRank { shape } => {
write!(
formatter,
"RMSNorm gain must have rank 1, got shape {shape:?}"
)
}
Self::GainAllocationFailed { elements } => write!(
formatter,
"could not reserve storage for {elements} RMSNorm gain values"
),
Self::InputRankZero => formatter.write_str("RMSNorm input must have at least one axis"),
Self::InputWidthMismatch { expected, actual } => write!(
formatter,
"RMSNorm input feature width must be {expected}, got {actual}"
),
Self::ZeroEnergyRow { row } => write!(
formatter,
"RMSNorm epsilon is zero but feature row {row} has zero mean square"
),
Self::Initialization(source) => source.fmt(formatter),
Self::Autodiff { stage, source } => {
write!(formatter, "RMSNorm {stage}: {source}")
}
}
}
}
impl Error for RmsNormError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Initialization(source) => Some(source),
Self::Autodiff { source, .. } => Some(source),
_ => None,
}
}
}
impl From<InitializationError> for RmsNormError {
fn from(error: InitializationError) -> Self {
Self::Initialization(error)
}
} В основном примере обратный проход получает . Результат —
и
. Пример также проверяет независимость строк
для входа формы и пустой внешний батч. Параметр с фиксированным именем
decoder.block.0.attention_norm.gain входит только во множество без затухания
AdamWParameterGroups. Такая группировка относится к политике оптимизатора, а
не к формуле RMSNorm:
rust/demos/ch25-rmsnorm/src/lib.rs#rmsnorm-fixture pub fn learner_evidence() -> Result<LearnerEvidence, FixtureError> {
let primary = primary_once()?;
let replay = primary_once()?;
let ideal_scale = scale_evidence("ideal", &INPUT_VALUES, IDEAL_EPSILON)?;
let production_scale = scale_evidence("production", &INPUT_VALUES, EPSILON)?;
let near_zero_scale = scale_evidence("near-zero", &TINY_VALUES, EPSILON)?;
let zero_layer = layer(&[1.0, 1.0], EPSILON)?;
let zero_output = zero_layer
.forward(&TensorValue::constant(tensor(&[2], &[0.0, 0.0]))?)?
.value()
.as_slice()
.to_vec();
let batch_output = layer(&GAIN_VALUES, EPSILON)?
.forward(&TensorValue::constant(tensor(
&[2, 2],
&[3.0, 4.0, 0.0, 5.0],
))?)?
.value_snapshot();
let groups = AdamWParameterGroups::new([] as [&str; 0], [GAIN_NAME])?;
let no_decay = groups.decayed_names().count() == 0
&& groups.excluded_names().collect::<Vec<_>>() == [GAIN_NAME];
let (input_checks, gain_checks, gradcheck_passed) = gradient_evidence(&primary)?;
Ok(LearnerEvidence {
replay_bitwise: primary_matches(&primary, &replay),
primary,
ideal_scale,
production_scale,
near_zero_scale,
zero_output,
batch_output,
history: historical_evidence()?,
errors: error_evidence()?,
input_checks,
gain_checks,
gradcheck_passed,
no_decay,
})
} Центральные разности проверяют обе координаты входа и обе координаты коэффициента масштаба с шагом и допуском :
rust/demos/ch25-rmsnorm/src/lib.rs#rmsnorm-gradcheck fn gradient_evidence(primary: &PrimaryEvidence) -> Result<(usize, usize, bool), FixtureError> {
let upstream = &primary.upstream;
let input_report = sampled_tensor_gradient_check(
&mut primary.input.clone(),
&primary.input_gradient.view(),
STEP,
TOLERANCE,
INPUT_VALUES.len(),
|probe| {
dot_output(
&layer(&GAIN_VALUES, EPSILON).expect("fixture layer is valid"),
probe,
upstream,
)
},
)?;
let gain_report = sampled_tensor_gradient_check(
&mut primary.gain.clone(),
&primary.gain_gradient.view(),
STEP,
TOLERANCE,
GAIN_VALUES.len(),
|probe| {
dot_output(
&RmsNorm::from_gain(
NamedParameter::from_tensor(GAIN_NAME, probe.clone())
.expect("probe gain is finite"),
EPSILON,
)
.expect("probe gain shape is valid"),
&primary.input,
upstream,
)
},
)?;
Ok((
input_report.checks.len(),
gain_report.checks.len(),
input_report.passed && gain_report.passed,
))
} rust/demos/ch25-rmsnorm/src/main.rs fn main() -> Result<(), Box<dyn std::error::Error>> {
let evidence = ch25_rmsnorm::learner_evidence()?;
print!("{}", ch25_rmsnorm::render_report(&evidence));
Ok(())
} Выполните cargo run --quiet --locked -p ch25-rmsnorm, чтобы вывести те же
проверенные значения прямого и обратного проходов, масштабирования и граничных
случаев.
Сравните масштабирование по обе стороны границы влияния эпсилона
На схеме собраны проверенные значения основного примера, градиенты, сравнения масштабов, поведение нулевого входа и батча, результаты исторических методов и отклонённые запросы:
Проследите один вектор признаков через RMSNorm
Проследите точные значения из примера на Rust: от входа через масштабирование по RMS и обучаемый коэффициент до результатов сравнения масштабов, исторических методов, градиентов и отклонённых граничных случаев.
- Сплошная рамка — вход
- Пунктирная рамка — масштабирование по RMS
- Двойная рамка — применение коэффициента
Измените масштаб одного вектора признаков последней оси
Вход
Среднее квадратов
Величина, обратная RMS
Вектор после масштабирования по RMS
Обучаемый коэффициент масштаба
Выход после применения коэффициента
Длины полос служат локальными ориентирами; точные значения указаны числами.
Сравните случаи с нулевым и конечным эпсилоном
Идеальный случай с нулевым эпсилоном
- Исходный вектор
- Вход, умноженный на десять
- Наибольшая абсолютная разность
Обычный вход с рабочим эпсилоном
- Исходный вектор
- Вход, умноженный на десять
- Наибольшая абсолютная разность
Вход, для которого преобладает эпсилон
- Исходный вектор
- Вход, умноженный на десять
- Наибольшая абсолютная разность
Для обычного входа разность мала, а для входа вблизи нуля хорошо заметна.
Сравните, откуда берётся статистика каждого метода
| Выход после применения коэффициента | Первый дополнительный пример | Второй дополнительный пример |
|---|---|---|
| BatchNorm | ||
| LayerNorm | ||
| RMSNorm |
Среднее выхода RMSNorm:
Проверьте градиенты, формы и граничные случаи
Результаты обратного прохода
- Градиент по входу
- Градиент коэффициента масштаба
Параметр масштаба с фиксированным именем
decoder.block.0.attention_norm.gain
Группировка параметров оптимизатора: no_decay=true
Полностью нулевой вход
Принято · finite=true
Независимые строки последней оси
axis=last
Отклонённые граничные случаи
- Отклонено
rank-zeroУ входа должна быть хотя бы одна ось. - Отклонено
width-mismatchШирина последней оси признаков должна совпадать с шириной коэффициента масштаба. - Отклонено
zero-energy-epsilon-zeroПри нулевом эпсилоне нельзя нормализовать строку с нулевым средним квадратов.
Проверки из примера на Rust
Результаты обратного прохода
input_checks=2
gain_checks=2
Принято
gradcheck=true
replay=bitwise
Читайте точные значения слева направо: вход, общий коэффициент RMS, масштабированный по RMS вектор, обучаемый коэффициент и итоговый выход. Сплошная, пунктирная и двойная рамки различают три состояния вектора, не полагаясь только на цвет. Затем карточки масштабирования показывают, почему умножение обычного вектора на десять почти не меняет результат с конечным эпсилоном, а такое же действие выводит очень малый вектор далеко из режима, в котором преобладает эпсилон.
Сначала сделайте предсказания
- Вычислите среднее квадратов и величину, обратную RMS, для .
- Примените к нормализованному вектору.
- Для ненулевого вектора и предскажите вектор после умножения входа на и масштабирования по RMS; рассматривайте его до применения обучаемого коэффициента .
- Объясните, почему положительный намного сильнее меняет вектор вблизи нуля.
- Предскажите результат для полностью нулевой строки при положительном и нулевом эпсилоне.
- Определите, вычитает ли RMSNorm среднее по признакам или смешивает примеры батча.
- Предскажите форму выхода и форму градиента коэффициента масштаба для входа .
- Объясните, почему исключение из затухания относится к политике оптимизатора, а не к формуле.
- Укажите место RMSNorm относительно тождественного и обучаемого путей в остаточном блоке с предварительной нормализацией.
Проверить предсказания
- Среднее квадратов равно , а величина, обратная RMS, — примерно .
- Выход приблизительно равен .
- При положительный множитель, на который умножен вход, сокращается при вычислении . Поэтому до применения обучаемого коэффициента этот вектор не меняется.
- Конечный эпсилон не масштабируется вместе с сигналом и потому преобладает в знаменателе очень малого вектора.
- Положительный эпсилон возвращает ; при нулевом эпсилоне строка, у которой среднее квадратов равно нулю, отклоняется до логарифма.
- Ни то ни другое: используются только признаки последней оси одного примера, а их среднее не вычитается.
- Форма выхода — , а форма градиента коэффициента масштаба — .
- Оптимизатор решает, для каких именованных параметров применять затухание; в уравнении RMSNorm этого нет.
- Обучаемая ветвь получает выход RMSNorm, а тождественный путь переносит остаточный поток без изменений.
Далее спроецируйте нормализованные признаки в тензоры запросов, ключей и значений
В накопительном декодере появилась дифференцируемая RMSNorm по последней оси для каждой обучаемой остаточной ветви, а её коэффициент с фиксированным именем можно исключить из затухания весов при обучении. В главе 26 нормализованные признаки будут преобразованы в отдельные проекции запросов, ключей и значений без смещений, с явно заданными размерностями внимания.