← Все главы

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

Начальные веса: воспроизводимые значения и обоснованный масштаб

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

Верному градиенту всё ещё нужны начальные веса

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

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

Разберите одну выбранную проекцию

Форма проекции — [2,2]: две строки соответствуют входным координатам, а два столбца — выходным. Начальное значение генератора — 17. При входной ширине 22 и выходной ширине 22 схема Ксавье задаёт целевую дисперсию 1/21/2. Стандартное отклонение равно 1/21/\sqrt{2}, а граница равномерного распределения с центром в нуле — 3/2\sqrt{3/2}. Четыре веса записаны построчно; при округлении до двенадцати знаков после запятой они выглядят так:

 0.004950883736  -0.265932089217
-0.420504358848  -0.676313443233

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

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

Создать разобранную проекцию с начальным значением 17, повторить её и сравнить с запросом для 18 rust/demos/ch17-parameter-initialization/src/lib.rs#fixed-seed-parameter
    let scale = xavier_scale(2, 2)?;
    let mut rng = SplitMix64::from_seed(FIXTURE_SEED);
    let projection = projection_parameter(&mut rng)?;
    let weights = projection.tensor().value_snapshot();

    let mut matching_rng = SplitMix64::from_seed(FIXTURE_SEED);
    let matching = projection_parameter(&mut matching_rng)?;
    let mut alternate_rng = SplitMix64::from_seed(ALTERNATE_SEED);
    let alternate = projection_parameter(&mut alternate_rng)?;

Обе ширины матрицы определяют начальный разброс

В хранимой матрице WW вес WijW_{ij} соединяет входную координату ii с выходной координатой jj. Входная ширина — число взвешенных входов, суммируемых в каждом выходе; выходная ширина — число выходов, в которые поступает каждый вход.

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

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

Инициализация Глоро, или Ксавье, использует компромисс:

Var⁡(Wij)=2fan⁡in+fan⁡out\operatorname{Var}(W_{ij})=\frac{2}{\operatorname{fan}_{in}+\operatorname{fan}_{out}}

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

Чтобы получить веса обоих знаков с таким разбросом, используем равномерное распределение с центром в нуле на интервале [−a,a)[-a,a). Его дисперсия — a2/3a^2/3. Приравнивая её к целевой, получаем a=6/(fan⁡in+fan⁡out)a=\sqrt{6/(\operatorname{fan}_{in}+\operatorname{fan}_{out})}. Стандартное отклонение — квадратный корень из целевой дисперсии. И оно, и граница измеряются в единицах веса, но граница задаёт край интервала, а не характерный разброс около среднего.

SiLU не вполне соответствует допущению об активации. На будущий декодер также влияют нормализация, остаточные связи, данные и оптимизация. Формула не является теоремой о сохранении дисперсии во всём декодере.

Получите воспроизводимые числа в Rust

Граница сама по себе не выбирает отдельные веса. Воспроизводимые числа выдаёт SplitMix64. Его начальное значение — исходное состояние u64 до первого увеличения, поэтому ноль допустим. Циклическая целочисленная арифметика продвигает и перемешивает состояние. Целое число из старших 53 бит после перемешивания делится на число возможных 53-битных значений: получается дробь binary64 на интервале [0,1)[0,1). Одно состояние даёт одно продолжение. Это детерминированная псевдослучайная выборка, а не криптографическая случайность или идеальная непрерывная выборка.

Воспроизводить каждое значение binary64 на единичном интервале через явное состояние и старшие 53 бита rust/crates/llm-from-scratch/src/nn/init.rs#deterministic-prng
/// A small deterministic generator with an explicit resumable 64-bit state.
///
/// This generator is suitable for reproducible teaching fixtures. It is not a
/// cryptographically secure random-number generator.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SplitMix64 {
    state: u64,
}

impl SplitMix64 {
    /// Treats `seed` as the raw state before the first increment and draw.
    pub const fn from_seed(seed: u64) -> Self {
        Self { state: seed }
    }

    /// Resumes directly from a previously recorded raw state.
    pub const fn from_state(state: u64) -> Self {
        Self { state }
    }

    /// Returns the raw state that the next draw will advance.
    pub const fn state(&self) -> u64 {
        self.state
    }

    /// Advances and mixes one exactly specified 64-bit value.
    pub fn next_u64(&mut self) -> u64 {
        self.state = self.state.wrapping_add(SPLITMIX_INCREMENT);
        let mut value = self.state;
        value = (value ^ (value >> 30)).wrapping_mul(SPLITMIX_MIX_ONE);
        value = (value ^ (value >> 27)).wrapping_mul(SPLITMIX_MIX_TWO);
        value ^ (value >> 31)
    }

    /// Maps the high 53 bits of one draw to the binary64 interval [0,1).
    pub fn next_unit_f64(&mut self) -> f64 {
        ((self.next_u64() >> 11) as f64) * BINARY64_UNIT_SCALE
    }
}

xavier_scale сначала отклоняет нулевые ширины и переполнение их целочисленной суммы. Затем целевая дисперсия записывается в target_variance, её квадратный корень — в standard_deviation, а граница выборки — в uniform_limit. Генератор выборки удваивает полученное число из единичного интервала, вычитает единицу, чтобы центрировать значение, и умножает результат на uniform_limit. На каждый вес в построчном порядке расходуется одно число; форма матрицы определяет их общее количество.

Выбрать масштаб по ширинам до получения одного числа для каждого построчно записанного веса rust/crates/llm-from-scratch/src/nn/init.rs#xavier-initialization
/// The formula-derived scale for one [fan-in, fan-out] matrix.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct XavierScale {
    fan_in: usize,
    fan_out: usize,
    target_variance: f64,
    standard_deviation: f64,
    uniform_limit: f64,
}

impl XavierScale {
    pub const fn fan_in(self) -> usize {
        self.fan_in
    }

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

    pub const fn target_variance(self) -> f64 {
        self.target_variance
    }

    pub const fn standard_deviation(self) -> f64 {
        self.standard_deviation
    }

    pub const fn uniform_limit(self) -> f64 {
        self.uniform_limit
    }
}

/// Calculates the Xavier variance, standard deviation, and uniform bound.
pub fn xavier_scale(fan_in: usize, fan_out: usize) -> Result<XavierScale, InitializationError> {
    if fan_in == 0 {
        return Err(InitializationError::ZeroFanIn);
    }
    if fan_out == 0 {
        return Err(InitializationError::ZeroFanOut);
    }
    let fan_sum = fan_in
        .checked_add(fan_out)
        .ok_or(InitializationError::FanSumOverflow { fan_in, fan_out })?;
    let target_variance = 2.0 / fan_sum as f64;
    Ok(XavierScale {
        fan_in,
        fan_out,
        target_variance,
        standard_deviation: target_variance.sqrt(),
        uniform_limit: (6.0 / fan_sum as f64).sqrt(),
    })
}

fn checked_element_count(fan_in: usize, fan_out: usize) -> Result<usize, InitializationError> {
    fan_in
        .checked_mul(fan_out)
        .ok_or(InitializationError::ShapeProductOverflow { fan_in, fan_out })
}

pub(crate) fn validate_name(name: &str) -> Result<(), InitializationError> {
    if name.is_empty() {
        return Err(InitializationError::EmptyName);
    }

    let mut previous_was_dot = true;
    for (index, byte) in name.bytes().enumerate() {
        if byte == b'.' {
            if previous_was_dot {
                return Err(InitializationError::EmptyNameSegment { index });
            }
            previous_was_dot = true;
            continue;
        }

        if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') {
            return Err(InitializationError::InvalidNameCharacter { index, byte });
        }
        previous_was_dot = false;
    }

    if previous_was_dot {
        return Err(InitializationError::EmptyNameSegment { index: name.len() });
    }
    Ok(())
}

fn initialized_values(
    rng: &mut SplitMix64,
    scale: XavierScale,
) -> Result<Vec<f64>, InitializationError> {
    let elements = checked_element_count(scale.fan_in, scale.fan_out)?;
    let mut values = Vec::new();
    values
        .try_reserve_exact(elements)
        .map_err(|_| InitializationError::AllocationFailed { elements })?;
    for _ in 0..elements {
        let centered = 2.0 * rng.next_unit_f64() - 1.0;
        values.push(scale.uniform_limit * centered);
    }
    Ok(values)
}

Масштаб и веса вычисляются в f64. Целочисленные проверки предшествуют преобразованию, но достаточно большое допустимое значение usize может округлиться при переводе в f64. Деление, квадратный корень и умножение тоже используют представимую арифметику. Поэтому конечная сетка генератора и хранимые значения отличаются от идеального непрерывного распределения; печать двенадцати знаков округляет хранимое значение ещё раз. Побитное воспроизведение — свойство реализации, а не требование точного равенства дисперсии конечной матрицы математической цели. Малые примеры не проверяют антипереполнение из-за слишком малых чисел или переполнение чисел с плавающей точкой; проверяемые ошибки переполнения относятся к целочисленным размерностям.

Ширины — это количества связей

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

СимволСмысл в этом правиле инициализации
WWМатрица весов связей, которой задают начальные значения до обучения.
iiВходная координата, соответствующая строке веса.
jjВыходная координата, соответствующая столбцу веса.
WijW_{ij}Выбранный коэффициент, передающий вклад входной координаты ii в выходную координату jj.
Var⁡(Wij)\operatorname{Var}(W_{ij})Целевой разброс распределения в квадратных единицах веса, а не измеренная дисперсия конечной матрицы.
fan⁡in\operatorname{fan}_{in}Число входных координат, взвешенные вклады которых суммируются в каждом выходе.
fan⁡out\operatorname{fan}_{out}Число выходных координат, в которые вносит вклад каждый вход.
22Числитель, уравновешивающий условия по дисперсии для входной ширины в прямом проходе и выходной в обратном.

При входной ширине 22 и выходной ширине 22 стандартное отклонение равно 0.7071067811870.707106781187, а равномерная граница — 1.2247448713921.224744871392. Если увеличить входную ширину до 44, оставив выходную равной 22, целевая дисперсия станет 1/31/3, стандартное отклонение — 0.5773502691900.577350269190, а граница — 1.0000000000001.000000000000. Большее число входных слагаемых требует меньшего начального разброса по этому компромиссному правилу. Значит, ширины нужно знать до выборки.

Сохраните воспроизводимость создания параметров

Выбранная матрица должна стать обучаемым листовым узлом TensorValue, чтобы последующие обратные проходы могли накапливать её градиенты. NamedParameter также задаёт этому узлу стабильное внешнее имя, например decoder.block.0.attention.query.weight. Имя обозначает роль параметра при повторном создании, но не является самим узлом времени выполнения. Клон параметра использует тот же листовой узел ленты; новый узел с равными значениями — уже другой узел.

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

Отклонённый запрос не должен сдвигать поток. Проверки имени и ширин выполняются до выборки; проверяемая арифметика размерностей не допускает переполнения количества хранимых элементов. Для выборки используется пробный клон генератора. Его состояние переносится в генератор вызывающего кода только после успешного создания тензора и обучаемого листового узла. Поэтому каждая возвращённая ошибка выборки оставляет исходное состояние неизменным. Исправленный запрос может использовать предназначенное ему начальное состояние.

Создать именованный обучаемый узел до переноса состояния генератора и сохранить однозначный порядок коллекции rust/crates/llm-from-scratch/src/nn/init.rs#named-parameters
/// One immutable external name paired with one trainable tensor-tape leaf.
#[derive(Clone, Debug)]
pub struct NamedParameter {
    name: String,
    tensor: TensorValue,
}

impl NamedParameter {
    /// Wraps an already-created tensor as a named trainable leaf.
    pub fn from_tensor(
        name: impl Into<String>,
        tensor: Tensor,
    ) -> Result<Self, InitializationError> {
        let name = name.into();
        validate_name(&name)?;
        Ok(Self {
            name,
            tensor: TensorValue::parameter(tensor)?,
        })
    }

    /// Samples one [fan-in, fan-out] trainable matrix transactionally.
    pub fn xavier_uniform(
        name: impl Into<String>,
        fan_in: usize,
        fan_out: usize,
        rng: &mut SplitMix64,
    ) -> Result<Self, InitializationError> {
        let name = name.into();
        validate_name(&name)?;
        let scale = xavier_scale(fan_in, fan_out)?;
        let elements = checked_element_count(fan_in, fan_out)?;

        let mut trial = rng.clone();
        let values = initialized_values(&mut trial, scale)?;
        debug_assert_eq!(values.len(), elements);
        let tensor = Tensor::from_vec(vec![fan_in, fan_out], values)?;
        let parameter = Self {
            name,
            tensor: TensorValue::parameter(tensor)?,
        };
        *rng = trial;
        Ok(parameter)
    }

    /// Returns the stable external identity used by layers and checkpoints.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Borrows the trainable tape leaf without duplicating its tensor storage.
    pub fn tensor(&self) -> &TensorValue {
        &self.tensor
    }
}

/// A duplicate-checked, declaration-ordered set of named parameters.
#[derive(Clone, Debug, Default)]
pub struct NamedParameters {
    parameters: Vec<NamedParameter>,
}

impl NamedParameters {
    pub fn try_new(parameters: Vec<NamedParameter>) -> Result<Self, InitializationError> {
        for repeated in 0..parameters.len() {
            if let Some(first) = parameters[..repeated]
                .iter()
                .position(|parameter| parameter.name() == parameters[repeated].name())
            {
                return Err(InitializationError::DuplicateName {
                    name: parameters[repeated].name().to_owned(),
                    first,
                    repeated,
                });
            }
        }
        Ok(Self { parameters })
    }

    pub fn len(&self) -> usize {
        self.parameters.len()
    }

    pub fn is_empty(&self) -> bool {
        self.parameters.is_empty()
    }

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

    pub fn iter(&self) -> impl ExactSizeIterator<Item = &NamedParameter> {
        self.parameters.iter()
    }

    pub fn get(&self, name: &str) -> Option<&NamedParameter> {
        self.parameters
            .iter()
            .find(|parameter| parameter.name() == name)
    }
}

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

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

Ранний пример языковой модели представлен работой Bengio et al., A Neural Probabilistic Language Model. Bengio и соавторы совместно обучают матрицу признаков слов и матрицы нейросети для предсказания следующего слова. Они сообщают, что признаки слов инициализируются случайно, как и веса нейросети.

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

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

Анализ с учётом дисперсии приведён в работе Glorot and Bengio, Understanding the difficulty of training deep feedforward neural networks. Glorot и Bengio при упрощающих допущениях выводят компромисс между условиями по дисперсии для входной и выходной ширины: целевая дисперсия весов равна 2, делённой на сумму ширин. Его реализует нормированное равномерное распределение, симметричное относительно нуля.

Более поздняя архитектура описана в Vaswani et al., Attention Is All You Need. Vaswani и соавторы используют обучаемые эмбеддинги и повторяют в слоях Transformer проекции запросов, ключей, значений, выхода внимания и две проекции сети прямого распространения. Работа не предписывает инициализатор параметров.

В нашем декодере матрицы получают воспроизводимые равномерные выборки по схеме Ксавье и стабильные имена, необязательные смещения — нули, а коэффициенты RMSNorm — единицы. Таблица токенов использует тот же генератор выборки с учётом формы матрицы. Это явно принятые правила создания параметров: работа о Transformer их не предписывает, и они не гарантируют точного сохранения дисперсии при SiLU, нормализации или остаточных связях.

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

Разберите равные градиенты и гарантии создания параметров

В выбранном запросе столбцы случайной проекции различаются. Нулевой пример показывает, почему это существенно. Возьмём x=[1,−1]x=[1,-1], нулевую входную матрицу [2,2], применим SiLU и объединим два скрытых значения одинаковыми выходными весами [1,1]. Оба скрытых значения и скалярный выход равны нулю.

Начальный градиент этого скалярного выхода для обратного прохода равен единице. Одинаковые выходные веса передают обоим нейронам один множитель. Производная SiLU в нуле равна 1/21/2, а для каждого входного веса этот множитель умножается на соответствующую входную координату. Поэтому оба столбца градиента равны [0.5,−0.5]⊤[0.5,-0.5]^\top, а построчная запись — [0.5,0.5,-0.5,-0.5]. Обратный проход даёт ненулевой градиент, но одинаковые обновления сохраняют равенство столбцов.

Проследить путь SiLU с нулевыми весами от одинаковой выходной обработки до равных ненулевых столбцов градиента rust/demos/ch17-parameter-initialization/src/lib.rs#zero-symmetry-probe
pub fn zero_symmetry_probe() -> Result<SymmetryProbe, Box<dyn Error>> {
    let input = TensorValue::constant(tensor(&[1, 2], &[1.0, -1.0])?)?;
    let input_weights = TensorValue::parameter(tensor(&[2, 2], &[0.0; 4])?)?;
    let output_weights = TensorValue::constant(tensor(&[2, 1], &[1.0, 1.0])?)?;

    let hidden = input.matmul(&input_weights)?.silu()?;
    let output = hidden.matmul(&output_weights)?;
    let seed = tensor(&[1, 1], &[1.0])?;
    output.backward_with_seed(&seed.view(), GraphRetention::Retain)?;

    let output_value = output.value().as_slice()[0];
    let gradient = input_weights
        .gradient_snapshot()
        .expect("the input weights are a trainable leaf");
    let columns_equal = gradient.as_slice()[0] == gradient.as_slice()[1]
        && gradient.as_slice()[2] == gradient.as_slice()[3];
    Ok(SymmetryProbe {
        output: output_value,
        gradient,
        columns_equal,
    })
}

Условие здесь — одинаковая обработка, а не само наличие нуля. Разная обработка на выходе устранила бы именно этот аргумент равенства. Нулевые необязательные смещения и единичные коэффициенты нормализации — другие правила: они не превращают разные выбранные столбцы признаков в одинаковые.

Отклоните запрос, не меняя последующие числа

Допустимое имя состоит из непустых сегментов со строчными латинскими буквами ASCII, цифрами и подчёркиваниями; сегменты разделены точками. Конструктор проверяет имя, нулевую входную ширину, нулевую выходную ширину, переполнение суммы ширин, переполнение произведения размеров формы, резервирование памяти, создание тензора и создание обучаемого листового узла — именно в этом порядке. Если неверны несколько входов, порядок фиксирует первую возвращаемую ошибку. Гарантия относится к возвращённым ошибкам, а не к восстановлению после любого аварийного завершения процесса при выделении памяти.

Различить недопустимые запросы, ошибки памяти и листовых узлов, а также повторяющиеся имена коллекции rust/crates/llm-from-scratch/src/nn/init.rs#parameter-init-errors
/// A deterministic rejection while constructing or collecting a parameter.
#[derive(Clone, Debug, PartialEq)]
pub enum InitializationError {
    EmptyName,
    EmptyNameSegment {
        index: usize,
    },
    InvalidNameCharacter {
        index: usize,
        byte: u8,
    },
    ZeroFanIn,
    ZeroFanOut,
    FanSumOverflow {
        fan_in: usize,
        fan_out: usize,
    },
    ShapeProductOverflow {
        fan_in: usize,
        fan_out: usize,
    },
    AllocationFailed {
        elements: usize,
    },
    DuplicateName {
        name: String,
        first: usize,
        repeated: usize,
    },
    Tensor(TensorError),
    Autodiff(TensorAutodiffError),
}

impl fmt::Display for InitializationError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyName => formatter.write_str("parameter name must not be empty"),
            Self::EmptyNameSegment { index } => write!(
                formatter,
                "parameter name has an empty dot-separated segment at byte {index}"
            ),
            Self::InvalidNameCharacter { index, byte } => write!(
                formatter,
                "parameter name byte {index} must be lowercase ASCII, a digit, underscore, or dot; got 0x{byte:02x}"
            ),
            Self::ZeroFanIn => formatter.write_str("fan-in must be greater than zero"),
            Self::ZeroFanOut => formatter.write_str("fan-out must be greater than zero"),
            Self::FanSumOverflow { fan_in, fan_out } => write!(
                formatter,
                "fan-in {fan_in} plus fan-out {fan_out} does not fit usize"
            ),
            Self::ShapeProductOverflow { fan_in, fan_out } => write!(
                formatter,
                "matrix shape [{fan_in},{fan_out}] does not fit usize"
            ),
            Self::AllocationFailed { elements } => write!(
                formatter,
                "could not reserve storage for {elements} initialized values"
            ),
            Self::DuplicateName {
                name,
                first,
                repeated,
            } => write!(
                formatter,
                "parameter name {name:?} first appears at index {first} and repeats at index {repeated}"
            ),
            Self::Tensor(error) => error.fmt(formatter),
            Self::Autodiff(error) => error.fmt(formatter),
        }
    }
}

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

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

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

NamedParameter::from_tensor создаёт такой же именованный обучаемый листовой узел из вручную заданных конечных значений, проверяя имя до узла. NamedParameters::try_new просматривает порядок объявления и сообщает самое раннее повторное имя вместе с индексом его первого вхождения. Метод не сортирует параметры и не объединяет их незаметно.

Пример сначала создаёт проекцию запросов, а затем таблицу токенов [4,2] на продолжении потока генератора. Клон проекции использует исходный листовой узел. Независимо воспроизведённая равная проекция имеет те же значения, но другой узел времени выполнения.

Перечислить проекцию запросов до таблицы токенов и отличить общий узел от равных значений rust/demos/ch17-parameter-initialization/src/lib.rs#named-parameter-enumeration
    let token_table = NamedParameter::xavier_uniform("token_embedding.weight", 4, 2, &mut rng)?;
    let parameters = NamedParameters::try_new(vec![projection.clone(), token_table])?;

    let projection_clone = projection.clone();
    let clone_same_node = projection.tensor().is_same_node(projection_clone.tensor());
    let recreated_same_node = projection.tensor().is_same_node(matching.tensor());

Имя Decoder.weight отклоняется из-за прописного символа. Допустимое имя decoder.invalid.weight с нулевой входной шириной даёт ZeroFanIn. Коллекция из двух клонов проекции повторяет их имя; отклонение повтора — операция коллекции, которая не расходует числа генератора. Пример подтверждает, что отклонённые вызовы выборки оставляют состояние продолжающегося генератора неизменным.

Разобрать недопустимое имя, нулевую ширину и повтор имени вместе с неизменным состоянием генератора rust/demos/ch17-parameter-initialization/src/lib.rs#initialization-errors-example
    let state_before_errors = rng.state();
    let invalid_name =
        NamedParameter::xavier_uniform("Decoder.weight", 2, 2, &mut rng).unwrap_err();
    let zero_fan_in =
        NamedParameter::xavier_uniform("decoder.invalid.weight", 0, 2, &mut rng).unwrap_err();
    let duplicate_name =
        NamedParameters::try_new(vec![projection.clone(), projection.clone()]).unwrap_err();
    let rng_unchanged = rng.state() == state_before_errors;

Запустите полный пример из корня репозитория:

./course run cargo run --quiet --locked -p ch17-parameter-initialization

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

Вывести масштаб, веса, симметрию, имена, сравнения узлов и результат проверки неизменного состояния rust/demos/ch17-parameter-initialization/src/main.rs#learner-parameter-initialization-output
    let report = learner_report()?;

    println!("seed: 17");
    println!("projection: shape=2x2 fan_in=2 fan_out=2");
    println!("target variance: {}", fixed(report.scale.target_variance()));
    println!("uniform limit: {}", fixed(report.scale.uniform_limit()));
    println!(
        "weights: {}",
        report
            .weights
            .as_slice()
            .iter()
            .map(|value| fixed(*value))
            .collect::<Vec<_>>()
            .join(",")
    );
    println!("same seed reproduces: {}", report.same_seed_reproduces);
    println!("different seed differs: {}", report.different_seed_differs);
    println!(
        "zero symmetry: output={} columns-equal={} gradient={}",
        fixed(report.symmetry.output),
        report.symmetry.columns_equal,
        report
            .symmetry
            .gradient
            .as_slice()
            .iter()
            .map(|value| fixed(*value))
            .collect::<Vec<_>>()
            .join(",")
    );
    println!(
        "parameters: {}",
        report
            .parameters
            .iter()
            .map(|parameter| format!(
                "{}[{}]",
                parameter.name(),
                shape(&parameter.tensor().shape())
            ))
            .collect::<Vec<_>>()
            .join(" | ")
    );
    println!(
        "identity: clone-same-node={} recreated-same-node={}",
        report.clone_same_node, report.recreated_same_node
    );
    println!(
        "validation: invalid-name | duplicate-name | zero-fan-in; rng-unchanged={}",
        report.rng_unchanged
    );
    println!("chapter 18 handoff: initialize a trainable token table");

Измеренные веса и ожидаемая линейная дисперсия — разные свидетельства

Четыре веса удобны для проверки воспроизведения и различия столбцов, но плохо показывают разброс распределения. Схема использует три матрицы [64,64], по 40964096 весов в каждой. Нулевая матрица не расходует числа генератора. Матрица с завышенным масштабом удваивает каждый вес Ксавье для начального значения 17. Поэтому две равномерные матрицы используют одни исходные числа и различаются только масштабом. Их гистограммы выделяют влияние удвоенной границы.

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

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

Записать связанные совокупности весов и отдельные последовательности ожидаемой линейной дисперсии rust/demos/ch17-parameter-initialization/src/diagram_trace.rs#parameter-initialization-trace
pub fn render_trace() -> Result<String, Box<dyn Error>> {
    let scale = xavier_scale(WIDTH, WIDTH)?;
    let mut rng = SplitMix64::from_seed(FIXTURE_SEED);
    let xavier = NamedParameter::xavier_uniform("diagnostic.weight", WIDTH, WIDTH, &mut rng)?
        .tensor()
        .value()
        .as_slice()
        .to_vec();
    let oversized: Vec<_> = xavier.iter().map(|value| value * 2.0).collect();
    let zero = vec![0.0; SAMPLE_COUNT];

    let zero_statistics = statistics(&zero);
    let oversized_statistics = statistics(&oversized);
    let xavier_statistics = statistics(&xavier);
    let zero_histogram = histogram(&zero);
    let oversized_histogram = histogram(&oversized);
    let xavier_histogram = histogram(&xavier);

    let mut same_seed_rng = SplitMix64::from_seed(FIXTURE_SEED);
    let same_seed =
        NamedParameter::xavier_uniform("diagnostic.weight", WIDTH, WIDTH, &mut same_seed_rng)?
            .tensor()
            .value()
            .as_slice()
            .to_vec();
    let mut alternate_rng = SplitMix64::from_seed(ALTERNATE_SEED);
    let alternate =
        NamedParameter::xavier_uniform("diagnostic.weight", WIDTH, WIDTH, &mut alternate_rng)?
            .tensor()
            .value()
            .as_slice()
            .to_vec();

    let mut trace = String::new();
    writeln!(trace, "TRACE parameter-initialization-v2 BEGIN")?;
    writeln!(
        trace,
        "FIXTURE name=fixed-seed-width64 generator=splitmix64 mapping=top53-affine seed=17 shape=64x64 samples=4096 fan-in=64 fan-out=64 statistic=population-two-pass layers=0,1,2,3,4 propagation=expected-linear-independent input-variance=1.000000000000 display-input-variance=1"
    )?;
    writeln!(
        trace,
        "BINNING edges={} display-edges={} width=0.100000000000 display-width=0.10 closure=left-closed-right-open-last-closed",
        fixed_list(&EDGES),
        DISPLAY_EDGES.join(",")
    )?;
    writeln!(
        trace,
        "{}",
        distribution_line("zero", "none", 0.0, &zero_statistics)
    )?;
    writeln!(trace, "{}", histogram_line("zero", &zero_histogram))?;
    writeln!(
        trace,
        "{}",
        distribution_line(
            "oversized",
            "17",
            scale.uniform_limit() * 2.0,
            &oversized_statistics,
        )
    )?;
    writeln!(
        trace,
        "{}",
        histogram_line("oversized", &oversized_histogram)
    )?;
    writeln!(
        trace,
        "{}",
        distribution_line("xavier", "17", scale.uniform_limit(), &xavier_statistics)
    )?;
    writeln!(trace, "{}", histogram_line("xavier", &xavier_histogram))?;
    writeln!(
        trace,
        "PAIRING seed=17 base-draws-equal=yes oversized-to-xavier-limit=2.000000000000"
    )?;
    writeln!(
        trace,
        "PROPAGATION kind=zero variances={} display-variances={}",
        fixed_list(&expected_variances(0.0)),
        display_integer_list(&expected_variances(0.0))
    )?;
    writeln!(
        trace,
        "PROPAGATION kind=oversized variances={} display-variances={}",
        fixed_list(&expected_variances(4.0)),
        display_integer_list(&expected_variances(4.0))
    )?;
    writeln!(
        trace,
        "PROPAGATION kind=xavier variances={} display-variances={}",
        fixed_list(&expected_variances(1.0)),
        display_integer_list(&expected_variances(1.0))
    )?;
    writeln!(
        trace,
        "REPRODUCIBILITY seed=17 same-seed-equal={} alternate-seed=18 alternate-seed-different={}",
        if xavier == same_seed { "yes" } else { "no" },
        if xavier != alternate { "yes" } else { "no" },
    )?;
    writeln!(trace, "TRACE parameter-initialization-v2 END")?;
    Ok(trace)
}

Начальный масштаб весов и линейная дисперсия

Гистограммы конечных выборок: нули и равномерные веса с границей Ксавье или двойной. Ожидаемая дисперсия сигнала — по целевым дисперсиям весов независимых линейных слоёв равной ширины входа/выхода. Веса независимы между собой и от входов; исходные входы независимы. Средние нулевые; дисперсия весов общая, входов единичная.

Нач. значение
17
Матрица
64x64
Весов/матр.
4096
Входов
64
Выходов
64
Дисп. входа
11
Генератор и отображение
splitmix64 / top53-affine
Дисперсия
Все веса; два прохода

Измеренные веса при трёх масштабах

Общие интервалы равной ширины включают левую границу; только последний включает правую. Δ=0.10\Delta=0.10

Нулевые веса

Нач. значение
Без выборки
Граница выборки
0.0000
Минимум весов
0.0000
Максимум весов
0.0000
Среднее
0.0000
Дисперсия
0.0000

Граница Ксавье удвоена; равномерно

Нач. значение
17
Граница выборки
0.4330
Минимум весов
-0.4330
Максимум весов
0.4326
Среднее
-0.0067
Дисперсия
0.0632

Равномерные веса Ксавье

Нач. значение
17
Граница выборки
0.2165
Минимум весов
-0.2165
Максимум весов
0.2163
Среднее
-0.0034
Дисперсия
0.0158

Общие числа

Общие числа; граница и каждый вес Ксавье удвоены.

Нач. значение
17
Отношение границ
2.000000000000
Правило весов
0 0.0% 0 0.0% 0 0.0% 0 0.0% 4096 100.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
409 10.0% 498 12.2% 482 11.8% 472 11.5% 469 11.5% 445 10.9% 476 11.6% 443 10.8% 402 9.8%
0 0.0% 0 0.0% 674 16.5% 962 23.5% 919 22.4% 930 22.7% 611 14.9% 0 0.0% 0 0.0%

Ожидаемая линейная дисперсия

Теория: дисперсия сигнала по целевой дисперсии весов. Независимые линейные слои: вход/выход одной ширины. Веса независимы между собой и от входов; исходные входы независимы. Средние нулевые; дисперсия весов общая, входов единичная. Не гарантия в нелинейном остаточном декодере.

Слоёв
0 11 11 11
1 00 44 11
2 00 1616 11
3 00 6464 11
4 00 256256 11

Повтор и смена начального значения

Ксавье: запрос тот же, веса те же

Нач. значение
17

Тот же запрос Ксавье: разные веса для 17 и 18

Нач. значение
18

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

Необязательная практика: воспроизведите и разберите объяснённые результаты

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

  1. Воспроизведите целевую дисперсию, стандартное отклонение и равномерную границу для входной ширины 22 и выходной ширины 22.
  2. Пересчитайте эти величины для входной ширины 44 и выходной ширины 22 и объясните уменьшение начального масштаба.
  3. Разберите граф SiLU с нулевыми весами и выведите два столбца градиента входных весов из одинаковых выходных весов и знаков входа.
  4. Воспроизведите одинаковые начальное значение, форму, обе ширины и порядок создания; сравните хранимые значения тензора, а не только печатные десятичные числа.
  5. Проверьте выбранное сравнение тензоров для начальных значений 18 и 17 при форме [2,2], входной ширине 22 и выходной ширине 22; объясните, почему наблюдаемое различие хранимых весов не устанавливает уникальность для всех начальных значений.
  6. Объясните, почему измеренная дисперсия конечного тензора [2,2] не обязана точно равняться 1/21/2.
  7. Разберите коллекцию проекции запросов и таблицы токенов; укажите порядок объявления и отличите клон от равного повторного создания.
  8. Разберите вызовы с недопустимым именем, нулевой шириной и повтором; объясните неизменность состояния вызывающего кода и укажите индексы повтора.
  9. Проверьте границы утверждений об инициализаторе в работе о Transformer и отличите инициализацию от масштабирования оценок внимания или эмбеддингов в прямом проходе.
  10. Объясните, как допущения о почти линейном режиме и независимости ограничивают рассуждение о дисперсии, различая целевую дисперсию распределения весов и гарантию сохранения дисперсии любого сигнала декодера.
Проверенные ответы к практике по инициализации
  1. При входной ширине 22 и выходной ширине 22 целевая дисперсия — 0.50.5, стандартное отклонение — 0.7071067811870.707106781187, равномерная граница — 1.2247448713921.224744871392. Стандартное отклонение характеризует разброс распределения, а граница — его край.
  2. При входной ширине 44 и выходной ширине 22 целевая дисперсия — 1/31/3, стандартное отклонение — 0.5773502691900.577350269190, граница — 1.0000000000001.000000000000. Большее число суммируемых входов уменьшает требуемый разброс весов по компромиссному правилу.
  3. В нулевом графе SiLU вход x=[1,−1]x=[1,-1], одинаковые выходные веса [1,1] и начальный градиент скалярного выхода, равный единице, дают обоим столбцам градиента входных весов [0.5,−0.5]⊤[0.5,-0.5]^\top. SiLU вычисляется в нуле, где производная равна 1/21/2. Выходные веса передают обоим нейронам один множитель; умножение на две входные координаты даёт разные знаки.
  4. Одинаковые реализация, исходное состояние генератора, форма, обе ширины и порядок создания воспроизводят все биты тензора. Печать двенадцати знаков округляет значения и потому даёт менее сильную проверку равенства.
  5. При неизменных форме [2,2], входной ширине 22, выходной ширине 22 и порядке создания выбранные значения тензора для 18 отличаются от значений для 17. Одно сравнение не доказывает уникальность тензора для каждой возможной пары начального значения и запроса.
  6. Целевая дисперсия весов — свойство распределения выборки. Конечная матрица может иметь ненулевое измеренное среднее и другую измеренную дисперсию, не нарушая правила.
  7. Сначала идёт decoder.block.0.attention.query.weight, затем — token_embedding.weight. Клон использует исходный листовой узел ленты; независимо созданный равный тензор имеет другой узел.
  8. Недопустимое имя отклоняется до проверки ширин и получения чисел; нулевая входная ширина — до выборки. Вызывающий код получает пробное состояние только при успехе. В коллекции двух клонов первое вхождение имеет индекс 0, повторное — 1.
  9. Vaswani и соавторы не предписывают инициализатор параметров. Масштабирование оценок внимания и эмбеддингов — операции прямого вычисления модели, а не выбор начальных значений.
  10. Рассуждение о масштабе прямого и обратного проходов предполагает независимые веса с нулевым средним, независимые входные признаки с нулевым средним и поступающие градиенты с нулевым средним. У каждой группы своя общая дисперсия. Веса независимы от входов и поступающих градиентов; активация симметрична и почти линейна с единичным наклоном. Цель относится к распределению выборки, конечные выборки не обязаны точно ей соответствовать. Нелинейности, нормализация, остаточные связи, глубина, данные и оптимизация влияют на реальные сигналы декодера, поэтому точная дисперсия его сигналов не гарантируется.

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

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

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

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

Если несколько параметров используют общий поток инициализации, задавайте порядок создания осознанно. В главе 21 перемешивание данных получит отдельный поток, глава 22 введёт состояние оптимизатора, а глава 35 сохранит значения параметров и происхождение состояния обучения. Здесь добавлено воспроизводимое создание обучаемых листовых узлов; получение строки эмбеддинга по заданному ID токена и обновление параметров — последующие операции.