← Все главы

24 · Версия материала 3

Для каждого обучаемого обновления сохраняйте тождественный путь

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

Предскажите, что сохраняет тождественный путь

Возьмите x=[2,1]x=[2,-1] и квадратную линейную ветвь без смещения, выход которой равен F(x)=[1,2.25]F(x)=[-1,-2.25]. Перед запуском примера предскажите y=x+F(x)y=x+F(x). Поэлементное сложение даёт [1,3.25][1,-3.25] и сохраняет форму [2][2].

Теперь обнулите веса ветви. После этого ветвь тождественно равна нулю, а не только обращается в ноль на данном входе. Для входящего градиента yˉ=[1,1]\bar y=[1,1] предскажите три величины: выход, градиент по входу и градиент весов ветви. Первые две в точности следуют тождественному пути. Градиент весов при этом может оставаться ненулевым, поскольку изменение нулевого веса изменило бы выход ветви.

Добавьте обучаемое обновление к неизменённому потоку

Вся остаточная связь задаётся формулой:

y=x+F(x)y=x+F(x)

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

shape(F(x))=shape(x)=shape(y)\operatorname{shape}(F(x))=\operatorname{shape}(x)=\operatorname{shape}(y)

Это условие строже обычного согласования форм (broadcasting). Ветвь формы [2][2] можно согласовать с входом формы [2,2][2,2] при обычном сложении, но она не принадлежит тому же остаточному потоку, поэтому в этой главе такое сложение отклоняется.

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

xˉ=yˉ+JF(x)yˉ\bar{x}=\bar{y}+J_F(x)^\top\bar{y}

Тождественный путь напрямую вносит yˉ\bar y. Ветвь вносит своё векторно-якобианное произведение JF(x)yˉJ_F(x)^\top\bar y. Эти вклады могут усиливать друг друга, взаимно сокращаться или увеличивать итог, поэтому тождественный член не гарантирует, что каждый градиент останется большим или будет вести себя устойчиво.

Для понимания полезна форма с коэффициентом:

y=x+αF(x)y=x+\alpha F(x)

При α=0\alpha=0 в прямом проходе остаётся только тождественный член; при α=1\alpha=1 получается обычная остаточная связь, реализованная в этой главе. В этой главе отдельная политика масштабирования остаточной ветви не вводится.

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

  • xx — входной тензор, который тождественный путь передаёт без изменений.
  • FF — обучаемое отображение ветви; позднее им может стать внимание или сеть прямого распространения.
  • F(x)F(x) — обновление ветви, форма которого в точности совпадает с формой xx.
  • yy — их поэлементная сумма той же формы.
  • yˉ\bar y — входящая сопряжённая величина выхода, а xˉ\bar x — накопленная сопряжённая величина входа.
  • JF(x)yˉJ_F(x)^\top\bar y — вклад ветви при обратном проходе.
  • α\alpha — необязательный поясняющий коэффициент, а не обучаемое значение в этой реализации.

Сложение — это не конкатенация: размер оси признаков не увеличивается. Это также не нормализация: среднее, дисперсия и среднеквадратическое значение не вычисляются.

От глубоких преобразований без остаточных связей к остаточному потоку Transformer

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

Этот результат описан в первоисточнике — Хэ и соавторы, Deep Residual Learning for Image Recognition. Хэ и соавторы описывают деградацию глубоких сетей без остаточных связей и представляют блок неизменной размерности как сумму обучаемой остаточной функции и тождественной обходной связи без параметров. Остаточное обучение явно выделило тождественный путь, а позднее в Transformer остаточное сложение стало охватывать каждый подслой внимания и сети прямого распространения при общей ширине модели.

Следующий первоисточник — Васвани и соавторы, Attention Is All You Need. Васвани и соавторы помещают остаточную связь вокруг каждого подслоя энкодера и декодера и сохраняют общую ширину выходов подслоёв, необходимую для сложения. В исходной архитектуре Transformer за каждой суммой следует LayerNorm. В этом курсе позднее будет собран декодер с предварительной RMSNorm, поэтому статья не служит подтверждением такого порядка операций.

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

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

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

Сравните повторяющиеся обычные и остаточные преобразования rust/demos/ch24-residual-connections/src/lib.rs#residual-stack
fn stack_fixture(use_residual: bool) -> Result<StackFixture, Box<dyn Error>> {
    let prefix = if use_residual { "residual" } else { "plain" };
    let input = TensorValue::parameter(tensor(&INPUT_SHAPE, &INPUT_VALUES))?;
    let mut current = input.clone();
    let mut values = vec![current.value_snapshot()];
    let mut layers = Vec::new();
    for depth in 0..4 {
        let layer = named_linear(
            format!("{prefix}.stack.{depth}.branch.weight"),
            &STACK_WEIGHT_VALUES,
        )?;
        let branch = layer.forward(&current)?;
        current = if use_residual {
            residual_add(&current, &branch)?
        } else {
            branch
        };
        values.push(current.value_snapshot());
        layers.push(layer);
    }
    current.backward_with_seed(
        &tensor(&INPUT_SHAPE, &UPSTREAM_VALUES).view(),
        GraphRetention::Retain,
    )?;
    let parameter_names = layers
        .iter()
        .map(|layer| layer.weight().name().to_owned())
        .collect();
    let parameter_gradients_finite_nonzero = layers.iter().all(|layer| {
        layer.weight().tensor().gradient().is_some_and(|gradient| {
            gradient.as_slice().iter().all(|value| value.is_finite())
                && gradient.as_slice().iter().any(|value| *value != 0.0)
        })
    });
    Ok(StackFixture {
        values,
        input_gradient: input.gradient_snapshot().expect("stack input gradient"),
        parameter_names,
        parameter_gradients_finite_nonzero,
    })
}

Перед сложением отклоните несовпадающие формы

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

Потребуйте точного совпадения форм и сохраните оба ребра графа rust/crates/llm-from-scratch/src/nn/residual.rs#residual-add
/// Adds an identity path to one same-shaped branch output.
///
/// The lower-level tensor addition supports broadcasting. A residual connection
/// does not: the branch must return exactly the residual stream's complete shape.
/// This utility owns no parameters and preserves both operands' tape edges.
pub fn residual_add(
    identity: &TensorValue,
    branch_output: &TensorValue,
) -> Result<TensorValue, ResidualError> {
    let identity_shape = identity.shape();
    let branch_shape = branch_output.shape();
    if identity_shape != branch_shape {
        return Err(ResidualError::ShapeMismatch {
            identity: identity_shape,
            branch: branch_shape,
        });
    }
    Ok(identity.add(branch_output)?)
}

Пример с квадратным слоем Linear и заданными значениями владеет параметром residual.branch.weight. В прямом проходе он выдаёт F(x)=[1,2.25]F(x)=[-1,-2.25]. При yˉ=[1,1]\bar y=[1,1] тождественный вклад равен [1,1][1,1], вклад ветви — [0.5,2.25][-0.5,2.25], а их сумма — xˉ=[0.5,3.25]\bar x=[0.5,3.25]. Градиент матрицы весов ветви в построчном порядке равен [2,2,1,1][2,2,-1,-1]:

Покажите известные градиенты остаточной ветви и ветви с нулевыми весами rust/demos/ch24-residual-connections/src/lib.rs#residual-fixture
fn primary_fixture() -> Result<PrimaryFixture, Box<dyn Error>> {
    let layer = named_linear("residual.branch.weight", &WEIGHT_VALUES)?;
    let input = TensorValue::parameter(tensor(&INPUT_SHAPE, &INPUT_VALUES))?;
    let branch_output = layer.forward(&input)?;
    let output = residual_add(&input, &branch_output)?;
    let upstream = tensor(&INPUT_SHAPE, &UPSTREAM_VALUES);
    output.backward_with_seed(&upstream.view(), GraphRetention::Retain)?;

    let branch_probe_layer = named_linear("residual.branch_probe.weight", &WEIGHT_VALUES)?;
    let branch_probe_input = TensorValue::parameter(tensor(&INPUT_SHAPE, &INPUT_VALUES))?;
    branch_probe_layer
        .forward(&branch_probe_input)?
        .backward_with_seed(&upstream.view(), GraphRetention::Retain)?;

    Ok(PrimaryFixture {
        input: input.value_snapshot(),
        branch_parameter_name: layer.weight().name().to_owned(),
        branch_weight: layer.weight().tensor().value_snapshot(),
        branch_output: branch_output.value_snapshot(),
        residual_output: output.value_snapshot(),
        upstream: upstream.clone(),
        identity_gradient: upstream,
        branch_input_gradient: branch_probe_input
            .gradient_snapshot()
            .expect("branch probe input gradient"),
        input_gradient: input.gradient_snapshot().expect("residual input gradient"),
        weight_gradient: layer
            .weight()
            .tensor()
            .gradient_snapshot()
            .expect("residual branch weight gradient"),
    })
}

fn zero_branch_fixture() -> Result<(Tensor, Tensor, Tensor, bool), Box<dyn Error>> {
    let layer = named_linear("residual.zero.weight", &ZERO_WEIGHT_VALUES)?;
    let input = TensorValue::parameter(tensor(&INPUT_SHAPE, &INPUT_VALUES))?;
    let branch_output = layer.forward(&input)?;
    let output = residual_add(&input, &branch_output)?;
    output.backward_with_seed(
        &tensor(&INPUT_SHAPE, &UPSTREAM_VALUES).view(),
        GraphRetention::Retain,
    )?;
    let weight_gradient = layer
        .weight()
        .tensor()
        .gradient_snapshot()
        .expect("zero branch weight gradient");
    let nonzero = weight_gradient
        .as_slice()
        .iter()
        .all(|value| value.is_finite())
        && weight_gradient.as_slice().iter().any(|value| *value != 0.0);
    Ok((
        output.value_snapshot(),
        input
            .gradient_snapshot()
            .expect("zero branch input gradient"),
        weight_gradient,
        nonzero,
    ))
}

Ветвь с нулевыми весами выдаёт F(x)=0F(x)=0 для любого входа, поэтому её вклад в градиент по входу здесь равен нулю. Градиент её весов остаётся [2,2,1,1][2,2,-1,-1]. Нельзя переносить этот вывод на ветвь, выход которой случайно обращается в ноль только в одной точке: из F(x)=0F(x)=0 само по себе не следует JF(x)=0J_F(x)=0.

Численная проверка вычисляет независимую реализацию скалярной целевой функции i(x+xW)i2\sum_i(x+xW)_i^2 на обычных тензорах в каждой возмущённой координате, а затем проверяет обе координаты входа и все четыре координаты весов:

Проверьте градиенты входа и весов ветви численно rust/demos/ch24-residual-connections/src/lib.rs#residual-gradcheck
fn numeric_gradient_fixture() -> Result<(usize, usize, bool), Box<dyn Error>> {
    let layer = named_linear("residual.gradcheck.weight", &WEIGHT_VALUES)?;
    let input = TensorValue::parameter(tensor(&INPUT_SHAPE, &INPUT_VALUES))?;
    let branch = layer.forward(&input)?;
    let output = residual_add(&input, &branch)?;
    output.mul(&output)?.sum_axis(0, false)?.backward()?;
    let analytic_input = input.gradient().expect("analytic input gradient");
    let analytic_weight = layer
        .weight()
        .tensor()
        .gradient()
        .expect("analytic weight gradient");

    let frozen_weight = tensor(&WEIGHT_SHAPE, &WEIGHT_VALUES);
    let mut input_probe = tensor(&INPUT_SHAPE, &INPUT_VALUES);
    let input_check = sampled_tensor_gradient_check(
        &mut input_probe,
        &analytic_input.view(),
        GRADCHECK_STEP,
        GRADCHECK_TOLERANCE,
        2,
        |probe| squared_residual_objective(probe, &frozen_weight),
    )?;

    let frozen_input = tensor(&INPUT_SHAPE, &INPUT_VALUES);
    let mut weight_probe = tensor(&WEIGHT_SHAPE, &WEIGHT_VALUES);
    let weight_check = sampled_tensor_gradient_check(
        &mut weight_probe,
        &analytic_weight.view(),
        GRADCHECK_STEP,
        GRADCHECK_TOLERANCE,
        4,
        |probe| squared_residual_objective(&frozen_input, probe),
    )?;

    Ok((
        input_check.checks.len(),
        weight_check.checks.len(),
        input_check.passed && weight_check.passed,
    ))
}

Функция формирования отчёта принимает проверенный пример и печатает детерминированный текст:

Сформируйте полный отчёт по главе 24 rust/demos/ch24-residual-connections/src/lib.rs#learner-residual-report
pub fn render_learner_report() -> Result<String, Box<dyn Error>> {
    let report = learner_report()?;
    let mut output = String::new();
    writeln!(output, "chapter=24-residual-connections")?;
    writeln!(
        output,
        "prediction=zero branch preserves output and input gradient but its weight gradient can be nonzero"
    )?;
    writeln!(
        output,
        "input=shape:{} values:{}",
        bracketed_shape(report.input.shape()),
        bracketed_values(&report.input)
    )?;
    writeln!(
        output,
        "branch_parameter=name:{} shape:{} values:{}",
        report.branch_parameter_name,
        bracketed_shape(report.branch_weight.shape()),
        bracketed_values(&report.branch_weight)
    )?;
    writeln!(
        output,
        "branch_output=shape:{} values:{}",
        bracketed_shape(report.branch_output.shape()),
        bracketed_values(&report.branch_output)
    )?;
    writeln!(
        output,
        "residual_output=shape:{} values:{}",
        bracketed_shape(report.residual_output.shape()),
        bracketed_values(&report.residual_output)
    )?;
    writeln!(
        output,
        "upstream=shape:{} values:{}",
        bracketed_shape(report.upstream.shape()),
        bracketed_values(&report.upstream)
    )?;
    writeln!(
        output,
        "identity_gradient={}",
        bracketed_values(&report.identity_gradient)
    )?;
    writeln!(
        output,
        "branch_input_gradient={}",
        bracketed_values(&report.branch_input_gradient)
    )?;
    writeln!(
        output,
        "input_gradient={}",
        bracketed_values(&report.input_gradient)
    )?;
    writeln!(
        output,
        "weight_gradient=shape:{} values:{}",
        bracketed_shape(report.weight_gradient.shape()),
        bracketed_values(&report.weight_gradient)
    )?;
    writeln!(
        output,
        "zero_branch=output:{} input_gradient:{} weight_gradient_nonzero:{}",
        bracketed_values(&report.zero_output),
        bracketed_values(&report.zero_input_gradient),
        report.zero_weight_gradient_nonzero
    )?;
    writeln!(
        output,
        "shape_error=identity:{} branch:{} broadcastable:{} rejected:{}",
        bracketed_shape(&report.mismatch_identity_shape),
        bracketed_shape(&report.mismatch_branch_shape),
        report.generic_add_broadcasts,
        report.residual_mismatch_rejected
    )?;
    for row in &report.stack {
        writeln!(
            output,
            "stack[{}]=plain:{} residual:{}",
            row.depth,
            bracketed_values(&row.plain),
            bracketed_values(&row.residual)
        )?;
    }
    writeln!(
        output,
        "stack_input_gradients=plain:{} residual:{}",
        bracketed_values(&report.plain_stack_input_gradient),
        bracketed_values(&report.residual_stack_input_gradient)
    )?;
    writeln!(
        output,
        "stack_parameters={}",
        report.stack_parameter_names.join(",")
    )?;
    writeln!(
        output,
        "numeric_gradient=input_checks:{} weight_checks:{} tolerance:{:.6} passed:{}",
        report.input_gradient_checks,
        report.weight_gradient_checks,
        GRADCHECK_TOLERANCE,
        report.numeric_gradient_passed
    )?;
    writeln!(
        output,
        "historical=plain_depth4_retention:{} residual_depth4_retention:{}",
        fixed(report.plain_stack_input_gradient.as_slice()[0]),
        fixed(report.residual_stack_input_gradient.as_slice()[0])
    )?;
    writeln!(
        output,
        "same_fixture_replays_bitwise={}",
        report.same_fixture_replays_bitwise
    )?;
    writeln!(
        output,
        "next=normalize each residual branch input with RMSNorm"
    )?;
    Ok(output)
}
Выведите детерминированный отчёт rust/demos/ch24-residual-connections/src/main.rs
use std::error::Error;

use ch24_residual_connections::render_learner_report;

fn main() -> Result<(), Box<dyn Error>> {
    print!("{}", render_learner_report()?);
    Ok(())
}

Выполните cargo run --quiet --locked -p ch24-residual-connections. Стандартный вывод зафиксирован в rust/demos/ch24-residual-connections/expected.txt.

Проследите оба пути и не потеряйте место сложения

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

Выведите точные записи прямого и обратного проходов, цепочек и проверенных свойств rust/demos/ch24-residual-connections/src/diagram_trace.rs#residual-connections-trace
/// Renders the exact Rust-owned evidence consumed by the static chapter diagram.
pub fn render_trace() -> Result<String, Box<dyn Error>> {
    let report = learner_report()?;
    if report.stack.len() != 5 {
        return Err("residual trace requires depths zero through four".into());
    }
    let mut trace = String::new();
    writeln!(trace, "TRACE residual-connections-v1 BEGIN")?;
    writeln!(
        trace,
        "CONFIG name=known-residual-linear shape={} branch-parameter={}",
        x_shape(report.input.shape()),
        report.branch_parameter_name
    )?;
    writeln!(
        trace,
        "FORWARD input={} branch={} output={}",
        bracketed_values(&report.input),
        bracketed_values(&report.branch_output),
        bracketed_values(&report.residual_output)
    )?;
    writeln!(
        trace,
        "BACKWARD upstream={} identity={} branch={} input={}",
        bracketed_values(&report.upstream),
        bracketed_values(&report.identity_gradient),
        bracketed_values(&report.branch_input_gradient),
        bracketed_values(&report.input_gradient)
    )?;
    writeln!(
        trace,
        "PARAMETER name={} shape={} gradient={}",
        report.branch_parameter_name,
        x_shape(report.weight_gradient.shape()),
        bracketed_values(&report.weight_gradient)
    )?;
    writeln!(
        trace,
        "ZERO-BRANCH output={} input-gradient={} weight-gradient={} weight-gradient-nonzero={}",
        bracketed_values(&report.zero_output),
        bracketed_values(&report.zero_input_gradient),
        bracketed_values(&report.zero_weight_gradient),
        report.zero_weight_gradient_nonzero
    )?;
    writeln!(
        trace,
        "SHAPE-ERROR identity={} branch={} broadcastable={} rejected={}",
        bracketed_shape(&report.mismatch_identity_shape),
        bracketed_shape(&report.mismatch_branch_shape),
        report.generic_add_broadcasts,
        report.residual_mismatch_rejected
    )?;
    for row in &report.stack {
        writeln!(
            trace,
            "STACK depth={} plain={} residual={}",
            row.depth,
            bracketed_values(&row.plain),
            bracketed_values(&row.residual)
        )?;
    }
    writeln!(
        trace,
        "STACK-GRADIENT plain={} residual={} parameters={}",
        bracketed_values(&report.plain_stack_input_gradient),
        bracketed_values(&report.residual_stack_input_gradient),
        report.stack_parameter_names.join(",")
    )?;
    writeln!(
        trace,
        "GRADCHECK input-checks={} weight-checks={} tolerance={:.6} passed={}",
        report.input_gradient_checks,
        report.weight_gradient_checks,
        GRADCHECK_TOLERANCE,
        report.numeric_gradient_passed
    )?;
    writeln!(
        trace,
        "PROOF identity=exact gradient=added parameters=branch-owned broadcast=forbidden"
    )?;
    writeln!(trace, "TRACE residual-connections-v1 END")?;
    Ok(trace)
}

Проследите тождественный путь и обучаемое обновление

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

Разведите и снова объедините пути в прямом проходе

  • Сплошная линия — тождественный путь
  • Пунктирная линия — обучаемая ветвь
  • Двойная рамка — сложение
Вход x=[2.000000,1.000000]x=[2.000000,-1.000000]
Тождественный путь x=[2.000000,1.000000]x=[2.000000,-1.000000]
Обучаемая ветвь F(x)=[1.000000,2.250000]F(x)=[-1.000000,-2.250000]
Сложение ++
Выход остаточной связи y=[1.000000,3.250000]y=[1.000000,-3.250000]

Точные значения одинаковой формы в прямом проходе

Сложите два вклада обратного прохода

  • Сплошная линия — тождественный путь
  • Пунктирная линия — обучаемая ветвь
  • Двойная рамка — сложение
Входящий градиент yˉ=[1.000000,1.000000]\bar y=[1.000000,1.000000]
Вклад тождественного пути yˉ=[1.000000,1.000000]\bar y=[1.000000,1.000000]
Вклад ветви JF(x)yˉ=[0.500000,2.250000]J_F(x)^\top\bar y=[-0.500000,2.250000]
Сложение ++
Градиент по входу xˉ=[0.500000,3.250000]\bar x=[0.500000,3.250000]

Тождественный вклад и векторно-якобианное произведение ветви складываются

Проверьте параметры, тождественный путь и формы

Параметры ветви

residual.branch.weight

W2×2W\in\mathbb{R}^{2\times2}

Градиент весов

Wˉ=[2.000000,2.000000,1.000000,1.000000]\bar W=[2.000000,2.000000,-1.000000,-1.000000]

Ветвь с нулевыми весами

F(x)=0,y=x=[2.000000,1.000000]F(x)=0,\quad y=x=[2.000000,-1.000000]

xˉ=yˉ=[1.000000,1.000000]\bar x=\bar y=[1.000000,1.000000]

Wˉ=[2.000000,2.000000,1.000000,1.000000]0\bar W=[2.000000,2.000000,-1.000000,-1.000000]\ne0

В прямом проходе выход совпадает со входом, полный градиент по входу равен входящему градиенту, а градиент весов ветви остаётся ненулевым.

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

Формы остаточных путей должны точно совпадать

[2,2][2][2,2]\ne[2]

Обычное сложение
Допустимо broadcastable=true
Остаточное сложение
Отклонено rejected=true
Выборочная проверка градиента

nx=2n_x=2 nW=4n_W=4 τ=0.000002\tau=0.000002

Допустимо passed=true

Проверенные свойства остаточной связи

Пример с заданными значениями known-residual-linear

shape(x)=[2]\operatorname{shape}(x)=[2]

Параметры ветви residual.branch.weight

identity=exact

gradient=added

parameters=branch-owned

broadcast=forbidden

Сравните повторяющиеся обычные и остаточные преобразования

Точные значения на каждом уровне глубины
Глубина Без остаточных связей С остаточными связями
00 [2.000000,1.000000][2.000000,-1.000000] [2.000000,1.000000][2.000000,-1.000000]
11 [0.500000,0.250000][-0.500000,0.250000] [1.500000,0.750000][1.500000,-0.750000]
22 [0.125000,0.062500][0.125000,-0.062500] [1.125000,0.562500][1.125000,-0.562500]
33 [0.031250,0.015625][-0.031250,0.015625] [0.843750,0.421875][0.843750,-0.421875]
44 [0.007812,0.003906][0.007812,-0.003906] [0.632812,0.316406][0.632812,-0.316406]
Градиент по входу [0.003906,0.003906][0.003906,0.003906] [0.316406,0.316406][0.316406,0.316406]
  • residual.stack.0.branch.weight
  • residual.stack.1.branch.weight
  • residual.stack.2.branch.weight
  • residual.stack.3.branch.weight

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

Каждая из четырёх диагональных матриц ветвей умножает свой вход на 0.25-0.25. После четырёх обычных слоёв градиент по входу сохраняет множитель 0.003906250.00390625, а после четырёх остаточных — 0.316406250.31640625. Эти точные значения показывают влияние явных тождественных членов в данном примере. Они не задают универсальной границы для оптимизации.

Сначала сделайте предсказания

  1. Вычислите yy для заданных xx и F(x)F(x).
  2. Разделите xˉ\bar x на тождественный вклад и вклад ветви.
  3. Предскажите выход ветви с нулевыми весами, градиент по входу и градиент весов.
  4. Определите, образуют ли формы [2,2][2,2] и [2][2] допустимую остаточную связь.
  5. Предскажите градиент, если обоими операндами сложения служит один и тот же отслеживаемый тензор.
  6. Вычислите множители обычной и остаточной цепочек глубины четыре при коэффициенте ветви 0.25-0.25.
  7. Объясните, почему остаточное сложение не конкатенирует и не нормализует признаки.
  8. Определите, гарантирует ли тождественный путь успешное обучение глубокой модели.
Проверить предсказания
  1. y=[1,3.25]y=[1,-3.25].
  2. xˉ=[1,1]+[0.5,2.25]=[0.5,3.25]\bar x=[1,1]+[-0.5,2.25]=[0.5,3.25].
  3. Выход равен [2,1][2,-1], градиент по входу — [1,1][1,1], а градиент весов — [2,2,1,1][2,2,-1,-1]; последний ненулевой.
  4. Нет. Обычное сложение может согласовать эти формы, но остаточное сложение требует их точного совпадения.
  5. Два разных ребра операндов приходят к одному листовому тензору, поэтому он получает удвоенный входящий градиент.
  6. Для обычной цепочки множитель равен (0.25)4=0.00390625(-0.25)^4=0.00390625, а для остаточной — (10.25)4=0.31640625(1-0.25)^4=0.31640625.
  7. Сложение сохраняет ширину признаков и не вычисляет статистик нормализации.
  8. Нет. Тождественный член создаёт прямой путь, но якобиан ветви по-прежнему может сократить, усилить или дестабилизировать итоговый градиент.

Далее нормализуйте вход ветви

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

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