13 · Версия материала 6
Проверяйте градиенты, прежде чем доверять обратному распространению
Сверяйте выбранные производные для обучения LLM по фактически представимым точкам: учитывайте требование локальной гладкости и погрешность с учётом масштаба, а координаты тензора выбирайте детерминированно в Rust.
Предскажите одну производную квадратичной функции
Начнём с небольшой скалярной проверки градиента:
Запрошенный шаг задаёт две точки, которые в ручном расчёте записаны как и
. Однако в f64 хранятся соседние представимые значения
и
. Поэтому реализация вычисляет фактические
расстояния и , а не считает каждое из них
равным запрошенному . При обычном округлении для ручного расчёта получаем
и , а трёхточечная формула даёт численную оценку
производной .
Этот результат проверяет аналитическое значение, а не создаёт его. Производная
, выведенная вручную, даёт значение , которое должно уложиться в
допуск. Значение тоже является вполне конечным числом, но сравнение с ним
должно завершиться с pass=false. Математическое несовпадение отличается от
недопустимого шага или неконечного значения целевой функции.
rust/demos/ch13-gradient-checking/src/lib.rs#quadratic-gradient-prediction pub fn quadratic(point: f64) -> f64 {
point * point
}
/// Checks either the expected derivative `6` or a deliberately wrong candidate.
pub fn quadratic_gradient_check(analytic: f64) -> Result<ScalarGradientCheck, GradCheckError> {
scalar_gradient_check(3.0, analytic, 0.1, STEP_TOLERANCE, quadratic)
} Используйте расстояния до значений, представимых компьютером
Пользователь задаёт положительную величину шага , а сложение и вычитание в формате с плавающей запятой дают фактически представимые точки вычисления
Обозначим положительные фактические расстояния от проверяемой точки через
Трёхточечная формула для неравных расстояний объединяет два односторонних наклона:
Только при равных фактических расстояниях вклад значения в центре сокращается, и формула принимает вид
В знаменатель входит фактическое расстояние , а не просто запрошенное .
Реализация вычисляет веса, не складывая сначала два потенциально огромных расстояния. При она находит и , а затем вес левого наклона и правого — . Эти отношения сохраняют коэффициенты формулы для неравных расстояний и не допускают переполнения суммы .
Если функция достаточно гладкая в окрестности точки — например, её третья производная непрерывна на всём отрезке от до , — погрешность усечения трёхточечной интерполяции имеет порядок . Уменьшение запрошенного шага помогает лишь до тех пор, пока главной причиной ошибки не станут округление точек вычисления и потеря точности при вычитании почти равных значений функции.
Линейная функция позволяет обнаружить ошибку в работе с округлением. Выберите конечную точку и шаг, для которых две операции с плавающей запятой дают разные ненулевые расстояния. Оба односторонних наклона в формуле по-прежнему равны , поэтому итоговая оценка должна совпасть с в пределах допуска. Деление на запрошенное может не пройти такую проверку: этот знаменатель не описывает фактические точки.
Локальная гладкость — предварительное условие, которое нельзя доказать одним совпадением. Для при симметричные точки дают численную оценку , и аналитическое значение может пройти проверку, хотя в точке излома производная не существует. Это ложное успешное сравнение за пределами области применимости метода; процедура не умеет безошибочно находить все изломы и другие негладкие точки.
compare_one_sided_slopes вычисляет расхождение с учётом масштаба между
записанными наклонами. Расхождение может указывать на излом, слишком большой для
локальной кривизны шаг или погрешность округления; совпадение само по себе не
доказывает дифференцируемость.
Реализация требует конечного , положительного конечного , конечного диапазона запрошенного шага, двух конечных точек, строго расположенных по разные стороны от , положительных конечных фактических расстояний и конечных значений функции во всех трёх точках. Если после округления одна из точек совпала с исходной, запрос отклоняется, а не выдаётся нулевая производная.
rust/crates/llm-from-scratch/src/autograd/gradcheck.rs#central-difference /// Approximates one derivative from the actual representable probe spacings.
///
/// The callback is evaluated exactly three times, in minus, center, plus order.
/// A meaningful gradient check requires a deterministic, side-effect-free
/// objective that is differentiable and sufficiently smooth throughout the
/// closed probe interval. Known or possible kinks must be excluded or examined
/// with [`compare_one_sided_slopes`]; a passing comparison is evidence for the
/// sampled smooth point, not a universal proof of a derivative.
pub fn central_difference(
point: f64,
step: f64,
mut evaluate: impl FnMut(f64) -> f64,
) -> Result<CentralDifference, GradCheckError> {
let geometry = perturbations(point, step)?;
let minus_value = evaluate(geometry.minus_point);
if !minus_value.is_finite() {
return Err(GradCheckError::NonFiniteEvaluation {
side: DifferenceSide::Minus,
value: minus_value,
});
}
let center_value = evaluate(point);
if !center_value.is_finite() {
return Err(GradCheckError::NonFiniteEvaluation {
side: DifferenceSide::Center,
value: center_value,
});
}
let plus_value = evaluate(geometry.plus_point);
if !plus_value.is_finite() {
return Err(GradCheckError::NonFiniteEvaluation {
side: DifferenceSide::Plus,
value: plus_value,
});
}
let left_slope = (center_value - minus_value) / geometry.minus_spacing;
if !left_slope.is_finite() {
return Err(GradCheckError::NonFiniteOneSidedSlope {
side: DifferenceSide::Minus,
value: left_slope,
});
}
let right_slope = (plus_value - center_value) / geometry.plus_spacing;
if !right_slope.is_finite() {
return Err(GradCheckError::NonFiniteOneSidedSlope {
side: DifferenceSide::Plus,
value: right_slope,
});
}
let derivative = geometry.left_weight * left_slope + geometry.right_weight * right_slope;
if !derivative.is_finite() {
return Err(GradCheckError::NonFiniteNumericalGradient { value: derivative });
}
Ok(CentralDifference {
point,
requested_step: step,
minus_point: geometry.minus_point,
plus_point: geometry.plus_point,
minus_spacing: geometry.minus_spacing,
plus_spacing: geometry.plus_spacing,
minus_value,
center_value,
plus_value,
left_slope,
right_slope,
left_weight: geometry.left_weight,
right_weight: geometry.right_weight,
stencil: geometry.stencil,
derivative,
})
} Назовите величины, используемые при проверке производной
| Символ | Практический смысл |
|---|---|
| Детерминированная скалярная функция потерь, производная которой проверяется. | |
| Конечный скалярный параметр или выбранная координата тензора. | |
| Положительная конечная запрошенная величина шага, по которой строятся обе точки вычисления в формате с плавающей запятой. | |
| Фактически представимые точки вычисления и . | |
| Фактические положительные расстояния и слева и справа. | |
| Производная в локально гладкой точке, приближённая трёхточечной формулой для неравных расстояний. |
Для аналитического значения и численной оценки процедура выбирает и записывает . При малых градиентах нижняя граница масштаба делает погрешность абсолютной; при больших она становится относительной. Аналитическое значение укладывается в допуск тогда и только тогда, когда нормированная погрешность не превышает заданный конечный неотрицательный допуск.
rust/crates/llm-from-scratch/src/autograd/gradcheck.rs#scale-aware-comparison /// Compares gradients after scaling both by the larger magnitude or one.
pub fn compare_gradients(
analytic: f64,
numerical: f64,
tolerance: f64,
) -> Result<GradientComparison, GradCheckError> {
validate_tolerance(tolerance)?;
if !analytic.is_finite() {
return Err(GradCheckError::NonFiniteAnalyticGradient { value: analytic });
}
if !numerical.is_finite() {
return Err(GradCheckError::NonFiniteNumericalGradient { value: numerical });
}
let scale = 1.0_f64.max(analytic.abs()).max(numerical.abs());
let absolute_error = (analytic - numerical).abs();
let scaled_error = (analytic / scale - numerical / scale).abs();
Ok(GradientComparison {
analytic,
numerical,
absolute_error,
scale,
scaled_error,
tolerance,
passed: scaled_error <= tolerance,
})
}
/// Compares the recorded left and right secant slopes without claiming proof.
///
/// A failed diagnostic can indicate a nondifferentiable kink, a step that is
/// too large for the local curvature, or floating-point rounding. A passing
/// diagnostic cannot establish differentiability on its own.
pub fn compare_one_sided_slopes(
difference: &CentralDifference,
tolerance: f64,
) -> Result<OneSidedSlopeComparison, GradCheckError> {
validate_tolerance(tolerance)?;
let left = difference.left_slope;
if !left.is_finite() {
return Err(GradCheckError::NonFiniteOneSidedSlope {
side: DifferenceSide::Minus,
value: left,
});
}
let right = difference.right_slope;
if !right.is_finite() {
return Err(GradCheckError::NonFiniteOneSidedSlope {
side: DifferenceSide::Plus,
value: right,
});
}
let scale = 1.0_f64.max(left.abs()).max(right.abs());
let absolute_gap = (left - right).abs();
let scaled_gap = (left / scale - right / scale).abs();
Ok(OneSidedSlopeComparison {
left,
right,
absolute_gap,
scale,
scaled_gap,
tolerance,
consistent: scaled_gap <= tolerance,
})
}
/// Runs a central difference and compares it with one analytic candidate.
///
/// The objective has the same smoothness, determinism, and side-effect
/// requirements as [`central_difference`].
pub fn scalar_gradient_check(
point: f64,
analytic: f64,
step: f64,
tolerance: f64,
evaluate: impl FnMut(f64) -> f64,
) -> Result<ScalarGradientCheck, GradCheckError> {
validate_tolerance(tolerance)?;
let difference = central_difference(point, step, evaluate)?;
let comparison = compare_gradients(analytic, difference.derivative, tolerance)?;
Ok(ScalarGradientCheck {
difference,
comparison,
})
} От обратного распространения для следующего слова к проверке производных при обучении Transformer
Нейронная языковая модель Бенжио и соавторов максимизирует логарифмическое правдоподобие следующего слова и явно выполняет этап обратного распространения и обновления параметров выходного и скрытого слоёв, а также обучаемых векторных представлений слов. Распространяемые назад производные позволяют многократно обновлять параметры, но реализованный путь их вычисления не служит независимой проверкой самого себя.
Эта процедура обучения описана в работе Bengio et al., A Neural Probabilistic Language Model. Бенжио и соавторы максимизируют логарифмическое правдоподобие следующего слова и описывают обратное распространение с обновлением параметров, при котором градиенты проходят через выходные элементы, веса скрытого слоя и обучаемые векторные представления слов.
Transformer с повторяющимися слоями внимания и полносвязными блоками обучают градиентным методом, выполняя 100 000 шагов Adam для базовой модели и 300 000 для большой. Байдин и соавторы различают численное дифференцирование и автоматическое дифференцирование в обратном режиме: конечные разности оценивают одну локальную производную по нескольким вычислениям функции, а обратный режим эффективно получает градиент скалярной цели по множеству параметров. Здесь отдельный путь численного вычисления помогает обнаружить локальную ошибку в проверяемой производной.
Сведения о более поздней модели взяты из работы Vaswani et al., Attention Is All You Need. Васвани и соавторы обучают базовые модели Transformer 100 000 шагов, а большие — 300 000 шагов, применяя Adam с явно заданным расписанием скорости обучения.
Различие между этими способами вычисления производных объясняется в обзоре Baydin et al., Automatic Differentiation in Machine Learning: a Survey. Байдин и соавторы описывают центральные конечные разности, компромисс между погрешностями усечения и округления при выборе шага, плохую масштабируемость полного численного градиента и эффективность обратного режима для скалярной цели с множеством параметров.
В этой главе трёхточечные конечные разности служат лишь медленной выборочной численной сверкой для локально гладких целевых функций, в том числе для выбранных производных среднего NLL по индексам из главы 12, прежде чем в главе 14 появится обратный режим. Успешная сверка свидетельствует только о выбранных координатах, точках вычисления, шаге, допуске и конкретном примере; она не доказывает правильность всего градиента или локальную дифференцируемость. Такая проверка не обучает и не запускает декодер, а её правила относятся к данной реализации курса.
Успешная сверка также не доказывает вывод формулы, полную правильность обеих реализаций или их работу в соседних точках и на других входных данных.
Ни одна из двух работ о языковых моделях не утверждает, что использует эту процедуру проверки. Конечные разности — не позднее изобретение для LLM, не замена алгоритма обучения и не часть работы декодера при генерации. Здесь они нужны только для одного: сопоставить результат будущего обратного прохода с выборочными прямыми вычислениями функции потерь по отдельному пути до обновления параметров языковой модели.
Реализуйте выборочную численную сверку
Типизированные ошибки отделяют неверную конфигурацию от корректно завершившегося сравнения, которое не уложилось в допуск. В сообщении об ошибке указано, на какой стороне возникла проблема, а к ошибкам тензора добавлена соответствующая координата. Весь набор выбранных координат проверяется до первого вызова целевой функции. Поэтому даже неконечное аналитическое значение в конце списка обнаруживается заранее и частичный результат не возвращается.
rust/crates/llm-from-scratch/src/autograd/gradcheck.rs#gradcheck-errors /// A rejected numerical derivative, comparison, sample request, or tensor read.
#[derive(Clone, Debug, PartialEq)]
pub enum GradCheckError {
InvalidStep {
step: f64,
},
NonFinitePoint {
point: f64,
},
PerturbationNotFinite {
side: DifferenceSide,
point: f64,
step: f64,
},
PerturbationUnchanged {
side: DifferenceSide,
point: f64,
step: f64,
},
InvalidActualSpacing {
side: DifferenceSide,
point: f64,
probe: f64,
spacing: f64,
},
NonFiniteStencilWeight {
side: DifferenceSide,
value: f64,
},
NonFiniteEvaluation {
side: DifferenceSide,
value: f64,
},
NonFiniteOneSidedSlope {
side: DifferenceSide,
value: f64,
},
NonFiniteNumericalGradient {
value: f64,
},
InvalidTolerance {
tolerance: f64,
},
NonFiniteAnalyticGradient {
value: f64,
},
GradientShapeMismatch {
parameters: Vec<usize>,
analytic: Vec<usize>,
},
ShapeOverflow,
EmptyTensor,
ZeroSamples,
SampleAllocationFailed {
samples: usize,
rank: usize,
},
View(TensorViewError),
AtCoordinate {
coordinate: Vec<usize>,
source: Box<GradCheckError>,
},
}
impl fmt::Display for GradCheckError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidStep { step } => write!(
formatter,
"central-difference requested step {step:?} must be positive and finite"
),
Self::NonFinitePoint { point } => {
write!(formatter, "gradient-check point {point:?} must be finite")
}
Self::PerturbationNotFinite { side, point, step } => write!(
formatter,
"{side} perturbation from point {point:?} by step {step:?} is not finite"
),
Self::PerturbationUnchanged { side, point, step } => write!(
formatter,
"{side} perturbation from point {point:?} by step {step:?} rounds back to the point"
),
Self::InvalidActualSpacing {
side,
point,
probe,
spacing,
} => write!(
formatter,
"{side} actual spacing from point {point:?} to probe {probe:?} must be positive and finite, got {spacing:?}"
),
Self::NonFiniteStencilWeight { side, value } => {
write!(formatter, "{side} stencil weight is not finite: {value:?}")
}
Self::NonFiniteEvaluation { side, value } => {
write!(formatter, "{side} function evaluation returned {value:?}")
}
Self::NonFiniteOneSidedSlope { side, value } => {
write!(formatter, "{side} one-sided slope is not finite: {value:?}")
}
Self::NonFiniteNumericalGradient { value } => {
write!(
formatter,
"central difference produced non-finite gradient {value:?}"
)
}
Self::InvalidTolerance { tolerance } => write!(
formatter,
"gradient-check tolerance {tolerance:?} must be finite and nonnegative"
),
Self::NonFiniteAnalyticGradient { value } => {
write!(formatter, "analytic gradient {value:?} must be finite")
}
Self::GradientShapeMismatch {
parameters,
analytic,
} => write!(
formatter,
"parameter shape {parameters:?} does not match analytic-gradient shape {analytic:?}"
),
Self::ShapeOverflow => formatter.write_str("sampled tensor shape overflows usize"),
Self::EmptyTensor => {
formatter.write_str("sampled tensor gradient check needs at least one value")
}
Self::ZeroSamples => {
formatter.write_str("sampled tensor gradient check needs at least one sample")
}
Self::SampleAllocationFailed { samples, rank } => write!(
formatter,
"cannot allocate {samples} sampled coordinates of rank {rank}"
),
Self::View(error) => error.fmt(formatter),
Self::AtCoordinate { coordinate, source } => {
write!(
formatter,
"gradient check at coordinate {coordinate:?} failed: {source}"
)
}
}
}
}
impl Error for GradCheckError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::View(error) => Some(error),
Self::AtCoordinate { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}
impl From<TensorViewError> for GradCheckError {
fn from(error: TensorViewError) -> Self {
Self::View(error)
}
} Пусть — число элементов непустого тензора, — максимальное запрошенное
число координат, а — фактическое число выбранных координат.
Аргумент Rust max_samples задаёт . Нулевой запрос и пустой тензор
отклоняются, поэтому .
При алгоритм вычисляет плоское смещение
для каждого целого
. Для формы [2,3] имеем ; при получаем
, а даёт смещения
[0,1,3,5]. Им соответствуют координаты [[0,0],[0,1],[1,0],[1,2]].
Генератор случайных чисел и скрытое состояние не нужны. При знаменатель
формулы был бы равен нулю, поэтому отдельная ветвь выбирает
; для чётного это большее из двух центральных плоских
смещений. Общий алгоритм использует промежуточное значение без риска
переполнения и те же произведения последующих размерностей, что и накопительное
тензорное хранилище.
rust/crates/llm-from-scratch/src/autograd/gradcheck.rs#sample-tensor-coordinates /// Selects unique, ordered coordinates without randomness or hidden state.
pub fn sample_tensor_coordinates(
shape: &[usize],
max_samples: usize,
) -> Result<Vec<SampledCoordinate>, GradCheckError> {
if max_samples == 0 {
return Err(GradCheckError::ZeroSamples);
}
let elements = element_count(shape)?;
if elements == 0 {
return Err(GradCheckError::EmptyTensor);
}
let samples = max_samples.min(elements);
let mut coordinates = Vec::new();
coordinates
.try_reserve_exact(samples)
.map_err(|_| GradCheckError::SampleAllocationFailed {
samples,
rank: shape.len(),
})?;
for sample in 0..samples {
let flat_index = if samples == 1 {
elements / 2
} else {
let numerator = (sample as u128) * ((elements - 1) as u128);
(numerator / ((samples - 1) as u128)) as usize
};
coordinates.push(SampledCoordinate {
flat_index,
coordinate: coordinate_from_offset(shape, flat_index)?,
});
}
Ok(coordinates)
} Для каждой выбранной координаты процедура вычисляет одну и ту же целевую функцию сначала в фактической точке слева , затем при исходном значении и после этого в фактической точке справа . После каждого временного изменения она сразу восстанавливает точные исходные биты — до анализа результата и до любого обычного возврата с ошибкой. Если переданная целевая функция паникует, ограниченная гарантия восстановления из этого учебного примера не действует.
rust/crates/llm-from-scratch/src/autograd/gradcheck.rs#sampled-tensor-gradient-check /// Checks deterministic tensor coordinates while restoring every probed value.
///
/// The parameter tensor is restored on every ordinary `Ok` or `Err` path. A
/// panic inside `objective` is deliberately outside this reference guarantee.
/// The objective must be deterministic, side-effect-free, and differentiable
/// with sufficient smoothness across every sampled probe interval; known or
/// possible kinks are outside this comparison's contract.
pub fn sampled_tensor_gradient_check(
parameters: &mut Tensor,
analytic: &TensorView<'_>,
step: f64,
tolerance: f64,
max_samples: usize,
mut objective: impl FnMut(&Tensor) -> f64,
) -> Result<TensorGradientCheck, GradCheckError> {
validate_step(step)?;
validate_tolerance(tolerance)?;
if parameters.shape() != analytic.shape() {
return Err(GradCheckError::GradientShapeMismatch {
parameters: parameters.shape().to_vec(),
analytic: analytic.shape().to_vec(),
});
}
let samples = sample_tensor_coordinates(parameters.shape(), max_samples)?;
let mut candidates = Vec::new();
candidates.try_reserve_exact(samples.len()).map_err(|_| {
GradCheckError::SampleAllocationFailed {
samples: samples.len(),
rank: parameters.rank(),
}
})?;
// Validate every selected coordinate before the first objective call.
for sample in &samples {
let point = parameters.as_slice()[sample.flat_index];
perturbations(point, step).map_err(|error| at_coordinate(&sample.coordinate, error))?;
let analytic_value = *analytic
.get(&sample.coordinate)
.map_err(GradCheckError::View)?;
if !analytic_value.is_finite() {
return Err(at_coordinate(
&sample.coordinate,
GradCheckError::NonFiniteAnalyticGradient {
value: analytic_value,
},
));
}
candidates.push((sample.clone(), point, analytic_value));
}
let mut checks = Vec::new();
checks.try_reserve_exact(candidates.len()).map_err(|_| {
GradCheckError::SampleAllocationFailed {
samples: candidates.len(),
rank: parameters.rank(),
}
})?;
for (sample, point, analytic_value) in candidates {
let difference = central_difference(point, step, |probe| {
parameters.as_mut_slice()[sample.flat_index] = probe;
let value = objective(parameters);
parameters.as_mut_slice()[sample.flat_index] = point;
value
})
.map_err(|error| at_coordinate(&sample.coordinate, error))?;
let comparison = compare_gradients(analytic_value, difference.derivative, tolerance)
.map_err(|error| at_coordinate(&sample.coordinate, error))?;
checks.push(CoordinateGradientCheck {
flat_index: sample.flat_index,
coordinate: sample.coordinate,
difference,
comparison,
});
}
Ok(TensorGradientCheck {
shape: parameters.shape().to_vec(),
requested_samples: max_samples,
passed: checks.iter().all(|check| check.comparison.passed),
checks,
})
} В численной целевой функции используется среднее NLL по индексам из главы 12.
Логиты формы [2,3] равны [0,1,-1,2,0,-2], цели — [0,2], а прямое
вычисление средней функции потерь даёт 2.775268796472. Отдельная аналитическая
реализация начинает с исходных логитов каждой строки. Для строки она
вычисляет
Для класса словаря и цели вручную выведенная производная имеет вид
В позиции целевого логита из вычисленной здесь вероятности вычитается единица,
после чего градиент каждой строки делится на число строк с целями, равное двум.
Этот аналитический путь не вызывает основные функции softmax или
indexed_mean_nll, а численная целевая функция вызывает indexed_mean_nll.
Такое разделение существенных вычислений помогает обнаружить ошибку, которая не
повторена в обоих путях, но не делает их полностью независимыми: у них общие
исходные логиты и цели, элементарная арифметика f64, хранилище Tensor и
соглашение о построчных индексах.
rust/demos/ch13-gradient-checking/src/lib.rs#hand-derived-nll-gradient /// Applies `(normalized probabilities - one_hot(target)) / batch_size` through a separate local path.
///
/// This frozen two-row analytic path deliberately implements its own row traversal,
/// maximum shift, exponential sum, normalization, target subtraction, and
/// batch scaling without calling the production probability or indexed-NLL
/// helpers. Both paths still share the input values and targets, IEEE `f64`
/// arithmetic and its primitive exponential, `Tensor` storage, and row-major
/// index conventions.
pub fn hand_derived_nll_gradient(logits: &Tensor) -> Result<Tensor, Box<dyn Error>> {
if logits.shape() != LOGIT_SHAPE {
return Err(format!(
"the local Chapter 13 analytic path requires shape {LOGIT_SHAPE:?}, got {:?}",
logits.shape()
)
.into());
}
let columns = LOGIT_SHAPE[1];
let mut values = vec![0.0; LOGIT_VALUES.len()];
for (row, &target) in TARGETS.iter().enumerate() {
let start = row * columns;
let row_logits = &logits.as_slice()[start..start + columns];
let maximum = row_logits.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if !maximum.is_finite() {
return Err(format!("oracle row {row} has no finite maximum").into());
}
let mut normalizer = 0.0;
for (column, &logit) in row_logits.iter().enumerate() {
if !logit.is_finite() {
return Err(format!("oracle logit at [{row}, {column}] is not finite").into());
}
let weight = (logit - maximum).exp();
values[start + column] = weight;
normalizer += weight;
}
if !normalizer.is_finite() || normalizer <= 0.0 {
return Err(
format!("oracle row {row} has invalid normalization {normalizer:?}").into(),
);
}
for column in 0..columns {
values[start + column] /= normalizer;
}
values[start + target] -= 1.0;
}
for gradient in &mut values {
*gradient /= TARGETS.len() as f64;
}
Ok(Tensor::from_vec(LOGIT_SHAPE.to_vec(), values)?)
} rust/demos/ch13-gradient-checking/src/lib.rs#sampled-nll-gradient-check /// Checks four deterministic vocabulary-logit coordinates against indexed NLL.
pub fn tiny_nll_gradient_example() -> Result<TinyNllGradientExample, Box<dyn Error>> {
let mut logits = logits()?;
let analytic = hand_derived_nll_gradient(&logits)?;
let loss = indexed_mean_nll(&logits.view(), 1, &TARGETS)?;
let original_bits = logits
.as_slice()
.iter()
.map(|value| value.to_bits())
.collect::<Vec<_>>();
let check = sampled_tensor_gradient_check(
&mut logits,
&analytic.view(),
TENSOR_STEP,
TENSOR_TOLERANCE,
TENSOR_SAMPLES,
|candidate| {
indexed_mean_nll(&candidate.view(), 1, &TARGETS)
.expect("the frozen finite logits and targets remain valid")
},
)?;
let restored_exactly = logits
.as_slice()
.iter()
.map(|value| value.to_bits())
.eq(original_bits);
Ok(TinyNllGradientExample {
logits,
analytic,
loss,
check,
restored_exactly,
})
} Пример подготавливает оба скалярных аналитических значения, перебор шести шагов, все четыре координаты NLL, точное восстановление и одну ошибку совпавшей после округления точки:
rust/demos/ch13-gradient-checking/src/main.rs#learner-gradient-check-output let correct = quadratic_gradient_check(6.0)?;
let wrong = quadratic_gradient_check(5.5)?;
let rounded = rounded_identity_gradient_check()?;
let kink = absolute_kink_diagnostic()?;
let scan = cubic_step_scan()?;
let nll = tiny_nll_gradient_example()?;
let collapsed = central_difference(1.0, 1.0e-20, |point| point * point).unwrap_err(); ./course run cargo run --quiet --locked -p ch13-gradient-checking
quadratic: theta=3.000000000000 requested_h=1.00000000000000006e-1 actual_h_minus=1.00000000000000089e-1 actual_h_plus=1.00000000000000089e-1 f_minus=8.410000000000 f_center=9.000000000000 f_plus=9.610000000000 left_slope=5.900000000000 right_slope=6.100000000000 left_weight=5.00000000000000000e-1 right_weight=5.00000000000000000e-1 numerical=6.000000000000
wrong candidate: analytic=5.500000000000 scaled_error=8.333333333333e-2 tolerance=1.000000000000e-6 pass=false
nll logits: shape=[2, 3] values=[0.0, 1.0, -1.0, 2.0, 0.0, -2.0] targets=[0, 2] loss=2.775268796472
sampled coordinates: [[0, 0], [0, 1], [1, 0], [1, 2]]
tensor restored exactly: true
Проследите, как уменьшение шага сначала помогает, а затем мешает
Для при аналитическая производная равна . Перебор в Rust начинается с запрошенного , улучшается при и и достигает наименьшей показанной нормированной погрешности при запрошенном : численная оценка равна , нормированная погрешность — , и результат укладывается в допуск. Затем из-за округления при запрошенном получается с нормированной погрешностью , поэтому результат не проходит проверку. При запрошенном численная оценка равна , а нормированная погрешность — ; этот результат также не укладывается в допуск. Глава не объявляет универсально лучшим шагом: это надёжная область только для данного примера.
Схема сопоставляет запрошенную величину шага с фактическими расстояниями до точек вычисления и показывает проверку линейной функции после округления, известную недифференцируемую точку при , отдельные аналитический и численный пути NLL с их общими предпосылками, все режимы погрешности, результаты сравнения, выбранные координаты и отклонённые запросы. Оценки и нормированные погрешности в схеме округлены только для удобства чтения; полный вывод исполняемого примера сохраняет их точные значения вместе с точками вычисления и значениями функции. Каждое показанное — запрошенный шаг, а производная вычисляется по двум фактическим расстояниям. Перебор показывает полезную область для гладкой функции между погрешностями усечения и округления, а координаты функции потерь — ограниченность выводов по успешной выборочной сверке.
rust/demos/ch13-gradient-checking/src/lib.rs#step-size-scan /// Runs the same central difference from truncation-dominated to rounded probes.
pub fn cubic_step_scan() -> Result<Vec<StepScanRecord>, GradCheckError> {
STEP_SCAN
.iter()
.zip(STEP_PHASES)
.map(|(&step, phase)| {
scalar_gradient_check(CUBIC_POINT, CUBIC_ANALYTIC, step, STEP_TOLERANCE, cubic)
.map(|check| StepScanRecord { phase, check })
})
.collect()
} Сопоставьте фактические расстояния, прежде чем доверять выборочной сверке
Схема сопоставляет запрошенную величину шага с фактическими расстояниями и показывает проверку линейной функции после округления, известную недифференцируемую точку, отдельные пути NLL с общими предпосылками, выбранные координаты функции потерь, точное восстановление и недопустимые входные данные.
- Численная производная
- 6.000000000000
- Точка для кубической функции
- Среднее NLL по индексам
- 2.775268796472
Проверьте фактические расстояния при значении параметра три
- Фактическая точка слева
- Фактическое расстояние слева
- 1.00000000000000089e-1
- Односторонний наклон слева
- 5.900000000000
- Центр
- Запрошенная величина шага
- 1.00000000000000006e-1
- Схема расположения точек
symmetric- 5.00000000000000000e-1 5.00000000000000000e-1
- Численный градиент
- 6.000000000000
- Фактическая точка справа
- Фактическое расстояние справа
- 1.00000000000000089e-1
- Односторонний наклон справа
- 6.100000000000
Переберите шесть величин шага при значении параметра полтора
- выше допуска
- Численный градиент
- Нормированная погрешность
- Фактическое расстояние слева / Фактическое расстояние справа
- 1.00000000000000000e0 1.00000000000000000e0
- Режим погрешности
- усечение
- выше допуска
- Численный градиент
- Нормированная погрешность
- Фактическое расстояние слева / Фактическое расстояние справа
- 1.00000000000000089e-1 1.00000000000000089e-1
- Режим погрешности
- усечение
- в допуске
- Численный градиент
- Нормированная погрешность
- Фактическое расстояние слева / Фактическое расстояние справа
- 9.99999999999889866e-4 9.99999999999889866e-4
- Режим погрешности
- сходимость
- в допуске
- Численный градиент
- Нормированная погрешность
- Фактическое расстояние слева / Фактическое расстояние справа
- 1.00000000000655120e-5 1.00000000000655120e-5
- Режим погрешности
- надёжная область
- выше допуска
- Численный градиент
- Нормированная погрешность
- Фактическое расстояние слева / Фактическое расстояние справа
- 9.99200722162640886e-14 9.99200722162640886e-14
- Режим погрешности
- округление
- выше допуска
- Численный градиент
- Нормированная погрешность
- Фактическое расстояние слева / Фактическое расстояние справа
- 1.11022302462515654e-15 1.11022302462515654e-15
- Режим погрешности
- округление
Сопоставьте два конечных значения градиента
Численный градиент Допуск
- Аналитическое значение в допуске
- Нормированная погрешность
- Аналитическое значение выше допуска
- Нормированная погрешность
Сверка четырёх выбранных координат NLL
Каждая запись свидетельствует только о выбранной координате, целевой функции, точках вычисления, шаге, допуске и конкретном примере.
Биты восстановлены да, точно
- ✓
- Аналитическое значение
- -0.377635764473
- Численный градиент
- -0.377635764481
- Нормированная погрешность
- ✓
- Аналитическое значение
- 0.332620477887
- Численный градиент
- 0.332620477894
- Нормированная погрешность
- ✓
- Аналитическое значение
- 0.433406666099
- Численный градиент
- 0.433406666087
- Нормированная погрешность
- ✓
- Аналитическое значение
- -0.492061880012
- Численный градиент
- -0.492061879994
- Нормированная погрешность
Что не доказывает успешная сверка
При неравных расстояниях после округления нужно использовать фактические расстояния; известный излом показывает ложное успешное сравнение, а выборочная сверка не является доказательством.
- Запрошенная величина шага
- 1.33226762955018780e-16
- Фактическое расстояние слева
- 1.11022302462515654e-16
- Фактическое расстояние справа
- 2.22044604925031308e-16
- Схема расположения точек
unequal- Численный градиент
- 1.000000000000
- Односторонний наклон слева
- -1.000000000000
- Односторонний наклон справа
- 1.000000000000
- Численный градиент
- 0.000000000000
- Допуск
- 1.000000000000e-12
Аналитический путь NLL реализован локально и не вызывает основные функции вероятностей или NLL по индексам; оба пути по-прежнему используют общие входные значения и цели, арифметику IEEE f64 и её базовую экспоненту, хранилище Tensor и соглашения о построчной индексации.
- Аналитическое значение
local-row-max-exp-sum-normalize-target-gradientobjective-pathindexed-mean-nllshared-primitivesf64-exp,frozen-inputs-and-targets
Недопустимые запросы
Недопустимый запрос отклоняется до принятия результата за производную.
- нулевой шаг
- точка не изменилась
слева
- неконечное значение
слева
- формы не совпадают
Сделайте предсказания до запуска Rust
- Вычислите и , а затем объясните, почему реализация должна получить и , а не делить на запрошенное .
- Для и разных ненулевых фактических расстояний предскажите оба односторонних наклона и результат взвешенной трёхточечной формулы.
- Предскажите результаты сравнения для аналитических значений и при допуске .
- Выберите результат с наименьшей погрешностью в этом переборе шести шагов, не объявляя соответствующий запрошенный шаг универсально лучшим.
- Объясните, почему запрошенные шаги и не проходят проверку после успешного результата с более крупным шагом из надёжной области.
- До ответа вычислите масштаб и нормированную погрешность для ошибочного аналитического значения.
- В тензоре элементов, а значение
max_samplesравно . Вычислите , затем для каждого найдите плоское смещение и сопоставьте четыре смещения с координатами формы[2,3]. - Предскажите поведение при нулевом шаге, точке, которая после округления не изменилась, и обычной ошибке после временного изменения тензора.
- Для при объясните, почему численная оценка может совпасть с аналитическим значением , но не установить существование производной.
- Проверьте понимание: что именно подтверждают четыре успешные сверки NLL, а какие общие предпосылки и вычисления остаются непроверенными?
Проверьте предсказания
- При округлении для ручного расчёта и . Операции с плавающей запятой могут расположить точки на разных расстояниях, поэтому в формулу должны входить эти расстояния, а не запрошенный .
- Оба отношения равны : и . Сумма весов равна единице, поэтому даже при результат совпадает с в пределах погрешности вычислений.
- Аналитическое значение укладывается в допуск. Для значения возвращается конечный результат сравнения с
pass=false. - В этом фиксированном переборе запрошенный шаг даёт наименьшую показанную нормированную погрешность: при численной оценке . Для другой функции, точки, числового типа или пары фактических точек надёжная область может отличаться.
- При запрошенном численная оценка имеет нормированную погрешность и не проходит проверку. При фактическое расстояние с каждой стороны равно лишь ; односторонние наклоны становятся равны и , поэтому их взвешенное значение имеет нормированную погрешность и также не проходит проверку. После округления соседние значения уже не сохраняют достаточно младших разрядов для надёжной оценки наклона.
- В точной арифметике и ; напечатанный результат
f64равен8.333333333333e-2и всё равно намного превышает . - Здесь . Для выражение даёт плоские смещения
[0,1,3,5], то есть координаты[[0,0],[0,1],[1,0],[1,2]]. - Нулевой недопустим; при и исходной точке смещённая влево точка после округления совпадает с исходной. При любой обычной ошибке временное значение тензора восстанавливается до возврата.
- Наклон симметричной секущей равен , но наклон слева равен , а справа — . В точке излома производная не существует, поэтому совпадение с нулём — ложное успешное сравнение за пределами требования локальной гладкости.
- Результаты свидетельствуют только о четырёх выбранных координатах для этой целевой функции, входных данных, фактических точек, запрошенного шага и допуска. Они не доказывают весь градиент, не обучают модель и не относятся к генерации. Кроме того, оба пути используют общие входы,
f64,Tensorи соглашения об индексах, поэтому эти предпосылки не проверяются сравнением.
После предсказаний запустите пример:
./course run cargo run --quiet --locked -p ch13-gradient-checking
Подготовьте автоматическое дифференцирование в обратном режиме
Теперь проект может сопоставлять выбранные производные по логитам словаря,
вычисленные отдельным локальным аналитическим путём, с результатами основной
реализации среднего NLL по индексам при изменённых входах. Аналитический путь не
вызывает основные функции softmax или indexed_mean_nll, но оба пути всё ещё
используют одни и те же исходные данные, арифметику f64, хранилище Tensor и
соглашения об индексах. Поэтому совпадение служит полезным выборочным
свидетельством, а не доказательством полной правильности обеих реализаций. В
главе 14 та же граница применится к скалярным производным в обратном режиме.
В следующей главе узлы скалярных вычислений будут хранить значения и локальные зависимости, обратный проход обработает их в обратном топологическом порядке, а для повторно использованных значений вклады суммируются. Сам по себе завершившийся обратный проход ничего не доказывает: его результаты по-прежнему нужно выборочно сопоставлять с трёхточечной оценкой из этой главы в локально гладких точках.