23 · Версия материала 4
Обучите нейронную языковую модель с фиксированным контекстом
Объедините эмбеддинги, скрытый слой SwiGLU, функцию потерь следующего токена с выбором по индексу, мини-пакеты и AdamW в детерминированной нейронной n-граммной модели со снижением валидационных потерь.
Предскажите один токен по полному контексту
Возьмите один полный двухтокенный контекст и единственный следующий за ним целевой токен . При , и заранее определите формы тензоров после выбора строк эмбеддингов по ID токенов, конкатенации, SwiGLU и проекции в пространство словаря, прежде чем смотреть на числовые результаты.
Для размера пакета получается:
Заданная начальная строка At кодируется буквальными программными ID
[67, 118]. Сначала найдите обе строки таблицы эмбеддингов и лишь затем
определяйте предсказание модели. При инициализации с заданным зерном логиты почти
равны; точный приходится на токен с ID 44,
но это ещё не выученный лингвистический вывод.
Перед скрытым слоем конкатенируйте эмбеддинги
Используйте точную общую формулу главы:
— одна обучаемая таблица, общая для всех позиций токенов. Каждый из индексов извлекает строку из . Скобки обозначают конкатенацию этих строк в хронологическом порядке, поэтому на вход SwiGLU поступают признаков. Преобразование возвращает , а матрица проецирует скрытый вектор в .
Для пакета итоговое умножение имеет вид . На выходе находится по одному логиту для каждого элемента словаря, а не по одному признаку эмбеддинга для каждого токена.
Не смешивайте оси контекста, признаков и словаря
- — целочисленный ID токена в позиции последовательности.
- — предсказываемая позиция, а — фиксированная длина её контекста.
- сопоставляет ID токена с обучаемыми признаками.
- конкатенирует строк эмбеддингов вдоль оси признаков.
- преобразует признаков контекста в скрытый вектор .
- переводит скрытых признаков в оценок элементов словаря.
- — вектор из логитов следующего токена.
Каждой строке контекста соответствует один целевой токен . Средняя функция потерь с выбором целевого логита по индексу равна
В главе 21 в каждой строке хранятся сдвинутых целевых токенов. Эта модель намеренно использует только — токен сразу после полного контекста. Если использовать все элементов, незаметно изменятся и целевая функция, и её знаменатель.
От разреженных счётчиков к обучаемым контекстам и вниманию
Классические счётные n-граммные модели оценивают каждый короткий контекст отдельно,
поэтому по мере роста числа возможных последовательностей для редких и
невстречавшихся сочетаний остаётся мало полезной статистики. В показанном ниже
расчёте historical_context_evidence биграмма, ключом которой служит только
последний токен ,
объединяет статистику по двум возможным следующим токенам, тогда как более
широкие контексты и сохраняют по одному различному следующему
токену.
A Neural Probabilistic Language Model описывает следующий, нейронный этап. Бенжио и соавторы преобразуют фиксированный контекст с помощью обучаемых распределённых представлений слов и сети прямого распространения в распределение вероятностей следующего слова. В этой модели распределённые векторы слов и функция сети прямого распространения, применяемая к конкатенированному фиксированному контексту, обучаются совместно. Благодаря этому сведения об одной последовательности помогают оценивать последовательности, составленные из слов с близкими векторными представлениями. Васвани и соавторы, Attention Is All You Need показывают следующий этап развития моделей последовательностей. Васвани и соавторы заменяют рекуррентные и свёрточные слои механизмом внимания и маскируют самовнимание декодера, чтобы при авторегрессионном предсказании позиция не могла использовать последующие позиции. Позднее модели Transformer заменяют смешивание фиксированного контекста маскированным механизмом самовнимания, сохраняя обучаемые эмбеддинги, преобразования сети прямого распространения, одинаково применяемые к каждой позиции, и проекцию в пространство словаря для предсказания следующего токена.
Нейронная n-граммная модель — важный связующий этап на пути к современным LLM: эмбеддинги позволяют разным токенам использовать общие признаки, обучаемое нелинейное преобразование объединяет полный контекст, а функция потерь следующего токена совместно обучает все матрицы. Позднее механизм внимания устраняет ограничение, связанное с конкатенацией фиксированного контекста.
В этих работах не заданы используемые в курсе словарь BPE, SwiGLU, размерности, константы оптимизатора, зёрна генератора, правило выбора последнего целевого токена и предел генерации. Этот расчёт показывает переход между моделями: точные таблицы счётчиков уступают место общим обучаемым признакам, а затем маскированное внимание устраняет фиксированное окно конкатенации.
rust/demos/ch23-neural-ngram/src/lib.rs#historical-context-road /// Count tables isolate exact contexts; learned embeddings can share features.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HistoricalContextEvidence {
pub bigram_followers: usize,
pub first_fixed_context_followers: usize,
pub second_fixed_context_followers: usize,
pub neural_context_width: usize,
}
pub fn historical_context_evidence() -> HistoricalContextEvidence {
let examples = [([10_u32, 11], 12_u32), ([20, 11], 13)];
let mut bigram_counts = BTreeMap::<u32, BTreeMap<u32, usize>>::new();
let mut fixed_context_counts = BTreeMap::<[u32; 2], BTreeMap<u32, usize>>::new();
for (context, target) in examples {
*bigram_counts
.entry(context[1])
.or_default()
.entry(target)
.or_default() += 1;
*fixed_context_counts
.entry(context)
.or_default()
.entry(target)
.or_default() += 1;
}
HistoricalContextEvidence {
bigram_followers: bigram_counts.get(&11).map_or(0, BTreeMap::len),
first_fixed_context_followers: fixed_context_counts.get(&[10, 11]).map_or(0, BTreeMap::len),
second_fixed_context_followers: fixed_context_counts
.get(&[20, 11])
.map_or(0, BTreeMap::len),
neural_context_width: CONTEXT_LENGTH * EMBEDDING_WIDTH,
}
} Храните единый набор параметров на всех шагах
Конфигурация и ошибки позволяют обнаружить неверные размерности модели, переполнение производных размеров, несоответствие параметров, некорректные пакеты, выход целевых токенов за границы словаря и неконечные оценки жадного выбора до того, как их можно будет принять за результат обучения:
rust/crates/llm-from-scratch/src/models/neural_ngram.rs#neural-ngram-config-and-errors /// The four widths that determine every fixed-context model shape.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NeuralNgramConfig {
vocabulary_size: usize,
context_length: usize,
embedding_width: usize,
hidden_width: usize,
context_feature_width: usize,
parameter_count: usize,
}
impl NeuralNgramConfig {
pub fn new(
vocabulary_size: usize,
context_length: usize,
embedding_width: usize,
hidden_width: usize,
) -> Result<Self, NeuralNgramError> {
if vocabulary_size == 0 {
return Err(NeuralNgramError::EmptyVocabulary);
}
if context_length == 0 {
return Err(NeuralNgramError::ZeroContextLength);
}
if embedding_width == 0 {
return Err(NeuralNgramError::ZeroEmbeddingWidth);
}
if hidden_width == 0 {
return Err(NeuralNgramError::ZeroHiddenWidth);
}
let context_feature_width = context_length.checked_mul(embedding_width).ok_or(
NeuralNgramError::ContextFeatureWidthOverflow {
context_length,
embedding_width,
},
)?;
let embedding_parameters = vocabulary_size.checked_mul(embedding_width);
let branch_parameters = context_feature_width.checked_mul(hidden_width);
let down_parameters = hidden_width.checked_mul(hidden_width);
let output_parameters = hidden_width.checked_mul(vocabulary_size);
let parameter_count = embedding_parameters
.and_then(|count| branch_parameters.and_then(|branch| count.checked_add(branch)))
.and_then(|count| branch_parameters.and_then(|branch| count.checked_add(branch)))
.and_then(|count| down_parameters.and_then(|down| count.checked_add(down)))
.and_then(|count| output_parameters.and_then(|output| count.checked_add(output)))
.ok_or(NeuralNgramError::ParameterCountOverflow)?;
Ok(Self {
vocabulary_size,
context_length,
embedding_width,
hidden_width,
context_feature_width,
parameter_count,
})
}
pub const fn vocabulary_size(self) -> usize {
self.vocabulary_size
}
pub const fn context_length(self) -> usize {
self.context_length
}
pub const fn embedding_width(self) -> usize {
self.embedding_width
}
pub const fn hidden_width(self) -> usize {
self.hidden_width
}
pub const fn context_feature_width(self) -> usize {
self.context_feature_width
}
pub const fn parameter_count(self) -> usize {
self.parameter_count
}
}
/// A rejected model shape, parameter set, batch, selector, or delegated operation.
#[derive(Clone, Debug, PartialEq)]
pub enum NeuralNgramError {
EmptyVocabulary,
ZeroContextLength,
ZeroEmbeddingWidth,
ZeroHiddenWidth,
ContextFeatureWidthOverflow {
context_length: usize,
embedding_width: usize,
},
ParameterCountOverflow,
EmptyBatch,
ContextTokenCountOverflow {
batch_size: usize,
context_length: usize,
},
ContextTokenCountMismatch {
expected: usize,
actual: usize,
},
BatchContextLengthMismatch {
expected: usize,
actual: usize,
},
ParameterCountMismatch {
expected: usize,
actual: usize,
},
ParameterNameMismatch {
index: usize,
expected: &'static str,
actual: String,
},
ParameterShapeMismatch {
name: String,
expected: Vec<usize>,
actual: Vec<usize>,
},
MissingTargetRow {
row: usize,
},
TargetIdOutOfBounds {
row: usize,
id: u32,
vocabulary_size: usize,
},
TargetIdDoesNotFitUsize {
row: usize,
id: u32,
},
MaskedTokenOutOfBounds {
id: u32,
vocabulary_size: usize,
},
NoUnmaskedToken,
NonFiniteLogit {
token_id: usize,
value: f64,
},
Initialization(InitializationError),
Embedding(EmbeddingError),
SwiGlu(SwiGluError),
Linear(LinearError),
Autodiff(TensorAutodiffError),
}
impl fmt::Display for NeuralNgramError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyVocabulary => {
formatter.write_str("neural n-gram vocabulary must not be empty")
}
Self::ZeroContextLength => {
formatter.write_str("neural n-gram context length must be greater than zero")
}
Self::ZeroEmbeddingWidth => {
formatter.write_str("neural n-gram embedding width must be greater than zero")
}
Self::ZeroHiddenWidth => {
formatter.write_str("neural n-gram hidden width must be greater than zero")
}
Self::ContextFeatureWidthOverflow {
context_length,
embedding_width,
} => write!(
formatter,
"context length {context_length} times embedding width {embedding_width} overflows usize"
),
Self::ParameterCountOverflow => {
formatter.write_str("neural n-gram parameter count overflows usize")
}
Self::EmptyBatch => formatter.write_str("neural n-gram batch must contain a row"),
Self::ContextTokenCountOverflow {
batch_size,
context_length,
} => write!(
formatter,
"batch size {batch_size} times context length {context_length} overflows usize"
),
Self::ContextTokenCountMismatch { expected, actual } => write!(
formatter,
"neural n-gram forward needs {expected} context IDs, but received {actual}"
),
Self::BatchContextLengthMismatch { expected, actual } => write!(
formatter,
"mini-batch context length must equal model context length {expected}, got {actual}"
),
Self::ParameterCountMismatch { expected, actual } => write!(
formatter,
"neural n-gram needs {expected} named parameters, but received {actual}"
),
Self::ParameterNameMismatch {
index,
expected,
actual,
} => write!(
formatter,
"parameter {index} must be named {expected}, got {actual}"
),
Self::ParameterShapeMismatch {
name,
expected,
actual,
} => write!(
formatter,
"parameter {name} must have shape {expected:?}, got {actual:?}"
),
Self::MissingTargetRow { row } => {
write!(formatter, "mini-batch target row {row} is missing")
}
Self::TargetIdOutOfBounds {
row,
id,
vocabulary_size,
} => write!(
formatter,
"target ID {id} in row {row} is out of bounds for vocabulary size {vocabulary_size}"
),
Self::TargetIdDoesNotFitUsize { row, id } => write!(
formatter,
"target ID {id} in row {row} does not fit this platform's selector type"
),
Self::MaskedTokenOutOfBounds {
id,
vocabulary_size,
} => write!(
formatter,
"masked token ID {id} is out of bounds for vocabulary size {vocabulary_size}"
),
Self::NoUnmaskedToken => {
formatter.write_str("greedy selection needs at least one unmasked token")
}
Self::NonFiniteLogit { token_id, value } => {
write!(
formatter,
"logit for token {token_id} is not finite: {value}"
)
}
Self::Initialization(error) => error.fmt(formatter),
Self::Embedding(error) => error.fmt(formatter),
Self::SwiGlu(error) => error.fmt(formatter),
Self::Linear(error) => error.fmt(formatter),
Self::Autodiff(error) => error.fmt(formatter),
}
}
}
impl Error for NeuralNgramError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Initialization(error) => Some(error),
Self::Embedding(error) => Some(error),
Self::SwiGlu(error) => Some(error),
Self::Linear(error) => Some(error),
Self::Autodiff(error) => Some(error),
_ => None,
}
}
} Модель владеет ровно пятью матричными узлами. Упорядоченный реестр параметров и объекты слоёв — эмбеддинги, SwiGLU и выходная проекция — ссылаются на одни и те же узлы. AdamW записывает проверенные новые значения в существующие узлы, поэтому следующий прямой проход использует обновлённые параметры через уже созданные слои. Повторно собирать слои после шага оптимизатора не нужно:
rust/crates/llm-from-scratch/src/models/neural_ngram.rs#neural-ngram-parameter-owner /// One fixed-context language model whose layers share one live parameter registry.
#[derive(Debug)]
pub struct NeuralNgram {
config: NeuralNgramConfig,
embedding: Embedding,
feed_forward: SwiGlu,
output: Linear,
parameters: Vec<NamedParameter>,
}
impl NeuralNgram {
/// Initializes all five matrices transactionally from one deterministic stream.
pub fn new(config: NeuralNgramConfig, rng: &mut SplitMix64) -> Result<Self, NeuralNgramError> {
let mut trial = rng.clone();
let embedding = Embedding::new(
NEURAL_NGRAM_PARAMETER_NAMES[0],
config.vocabulary_size,
config.embedding_width,
&mut trial,
)
.map_err(NeuralNgramError::Embedding)?;
let feed_forward = SwiGlu::new(
"ngram.ffn",
config.context_feature_width,
config.hidden_width,
config.hidden_width,
&mut trial,
)
.map_err(NeuralNgramError::SwiGlu)?;
let output = Linear::new(
"ngram.output",
config.hidden_width,
config.vocabulary_size,
false,
&mut trial,
)
.map_err(NeuralNgramError::Linear)?;
let model = Self::from_parts(config, embedding, feed_forward, output)?;
*rng = trial;
Ok(model)
}
/// Validates one ordered parameter set and builds persistent aliased layer handles.
pub fn from_parameters(
config: NeuralNgramConfig,
parameters: Vec<NamedParameter>,
) -> Result<Self, NeuralNgramError> {
validate_parameter_set(config, ¶meters)?;
let embedding = Embedding::from_parameter(parameters[0].clone())
.map_err(NeuralNgramError::Embedding)?;
let feed_forward = SwiGlu::from_parameters(
parameters[1].clone(),
parameters[2].clone(),
parameters[3].clone(),
)
.map_err(NeuralNgramError::SwiGlu)?;
let output = Linear::from_parameters(parameters[4].clone(), None)
.map_err(NeuralNgramError::Linear)?;
Ok(Self {
config,
embedding,
feed_forward,
output,
parameters,
})
}
pub const fn config(&self) -> NeuralNgramConfig {
self.config
}
pub fn parameters(&self) -> &[NamedParameter] {
&self.parameters
}
fn from_parts(
config: NeuralNgramConfig,
embedding: Embedding,
feed_forward: SwiGlu,
output: Linear,
) -> Result<Self, NeuralNgramError> {
let parameters = embedding
.parameters()
.iter()
.chain(feed_forward.parameters())
.chain(output.parameters())
.cloned()
.collect::<Vec<_>>();
validate_parameter_set(config, ¶meters)?;
Ok(Self {
config,
embedding,
feed_forward,
output,
parameters,
})
}
}
fn validate_parameter_set(
config: NeuralNgramConfig,
parameters: &[NamedParameter],
) -> Result<(), NeuralNgramError> {
if parameters.len() != NEURAL_NGRAM_PARAMETER_NAMES.len() {
return Err(NeuralNgramError::ParameterCountMismatch {
expected: NEURAL_NGRAM_PARAMETER_NAMES.len(),
actual: parameters.len(),
});
}
let expected_shapes = [
vec![config.vocabulary_size, config.embedding_width],
vec![config.context_feature_width, config.hidden_width],
vec![config.context_feature_width, config.hidden_width],
vec![config.hidden_width, config.hidden_width],
vec![config.hidden_width, config.vocabulary_size],
];
for (index, ((parameter, expected_name), expected_shape)) in parameters
.iter()
.zip(NEURAL_NGRAM_PARAMETER_NAMES)
.zip(expected_shapes)
.enumerate()
{
if parameter.name() != expected_name {
return Err(NeuralNgramError::ParameterNameMismatch {
index,
expected: expected_name,
actual: parameter.name().to_owned(),
});
}
let actual = parameter.tensor().shape();
if actual != expected_shape {
return Err(NeuralNgramError::ParameterShapeMismatch {
name: expected_name.to_owned(),
expected: expected_shape,
actual,
});
}
}
Ok(())
} Прямой проход обеспечивает точную цепочку форм, выбирает только последний сдвинутый целевой токен, вычисляет среднее отрицательное логарифмическое правдоподобие с выбором по индексу вдоль оси словаря и использует для жадной генерации стабильное разрешение ничьей в пользу меньшего ID:
rust/crates/llm-from-scratch/src/models/neural_ngram.rs#neural-ngram-forward impl NeuralNgram {
/// Looks up and concatenates each complete context before prediction.
pub fn forward(
&self,
context_ids: &[u32],
batch_size: usize,
) -> Result<NeuralNgramForward, NeuralNgramError> {
if batch_size == 0 {
return Err(NeuralNgramError::EmptyBatch);
}
let expected = batch_size.checked_mul(self.config.context_length).ok_or(
NeuralNgramError::ContextTokenCountOverflow {
batch_size,
context_length: self.config.context_length,
},
)?;
if context_ids.len() != expected {
return Err(NeuralNgramError::ContextTokenCountMismatch {
expected,
actual: context_ids.len(),
});
}
let embeddings = self
.embedding
.forward(context_ids, &[batch_size, self.config.context_length])
.map_err(NeuralNgramError::Embedding)?;
let concatenated = embeddings
.reshape(&[batch_size, self.config.context_feature_width])
.map_err(NeuralNgramError::Autodiff)?;
let hidden = self
.feed_forward
.forward(&concatenated)
.map_err(NeuralNgramError::SwiGlu)?;
let logits = self
.output
.forward(&hidden)
.map_err(NeuralNgramError::Linear)?;
Ok(NeuralNgramForward {
embeddings,
concatenated,
hidden,
logits,
})
}
/// Scores only the token following each complete context row.
pub fn loss(&self, batch: &MiniBatch) -> Result<TensorValue, NeuralNgramError> {
if batch.context_length() != self.config.context_length {
return Err(NeuralNgramError::BatchContextLengthMismatch {
expected: self.config.context_length,
actual: batch.context_length(),
});
}
let batch_size = batch.batch_width();
if batch_size == 0 {
return Err(NeuralNgramError::EmptyBatch);
}
let mut targets = Vec::with_capacity(batch_size);
for row in 0..batch_size {
let target_row = batch
.target_row(row)
.ok_or(NeuralNgramError::MissingTargetRow { row })?;
let id = target_row[self.config.context_length - 1];
let target = usize::try_from(id)
.map_err(|_| NeuralNgramError::TargetIdDoesNotFitUsize { row, id })?;
if target >= self.config.vocabulary_size {
return Err(NeuralNgramError::TargetIdOutOfBounds {
row,
id,
vocabulary_size: self.config.vocabulary_size,
});
}
targets.push(target);
}
self.forward(batch.inputs(), batch_size)?
.into_logits()
.indexed_mean_nll(1, &targets)
.map_err(NeuralNgramError::Autodiff)
}
/// Selects the greatest finite next-token logit, breaking exact ties by ID.
pub fn greedy_next(
&self,
context_ids: &[u32],
masked_ids: &[u32],
) -> Result<u32, NeuralNgramError> {
let mut masked = vec![false; self.config.vocabulary_size];
for &id in masked_ids {
let index = usize::try_from(id)
.ok()
.filter(|index| *index < masked.len())
.ok_or(NeuralNgramError::MaskedTokenOutOfBounds {
id,
vocabulary_size: self.config.vocabulary_size,
})?;
masked[index] = true;
}
let forward = self.forward(context_ids, 1)?;
let logits = forward.logits().value();
let mut best: Option<(usize, f64)> = None;
for (token_id, &value) in logits.as_slice().iter().enumerate() {
if !value.is_finite() {
return Err(NeuralNgramError::NonFiniteLogit { token_id, value });
}
if masked[token_id] {
continue;
}
if best.is_none_or(|(_, best_value)| value > best_value) {
best = Some((token_id, value));
}
}
let (token_id, _) = best.ok_or(NeuralNgramError::NoUnmaskedToken)?;
u32::try_from(token_id).map_err(|_| NeuralNgramError::NoUnmaskedToken)
}
} Зафиксированный пример обучает восемь правил BPE только по обучающим документам,
не читает текст тестовых документов, создаёт один порядок пакетов с зерном 23
и использует первые пакетов. AdamW использует каждый исходный градиент при
вычислении обновления и оставляет этот градиент в том же узле параметра. После
обновления пример явно вызывает zero_grad() для всех пяти узлов, прежде чем
начинать следующий прямой проход. Полные целевые функции на
обучающей и валидационной выборках вычисляются на шагах , и с
взвешиванием по фактическому числу строк. Два независимо инициализированных
запуска должны побитово совпасть по всем проверяемым значениям:
rust/demos/ch23-neural-ngram/src/lib.rs#chapter-neural-ngram-fixture /// Trains two independent seeded runs and keeps one complete evidence record.
pub fn learner_evidence() -> Result<LearnerEvidence, FixtureError> {
let first = run_once()?;
let replay = run_once()?;
let replay_bitwise = replay_equal(&first, &replay);
require(
replay_bitwise,
"same-seed training did not replay bit for bit",
)?;
require(
first.parameter_nodes_preserved,
"AdamW did not preserve every model parameter node",
)?;
require(
first.gradients_cleared,
"training did not clear every post-update parameter gradient",
)?;
let initial = first
.checkpoints
.first()
.ok_or(FixtureError::Invariant("initial checkpoint is missing"))?;
let final_checkpoint = first
.checkpoints
.last()
.ok_or(FixtureError::Invariant("final checkpoint is missing"))?;
require(
final_checkpoint.train_loss < initial.train_loss,
"training loss did not improve",
)?;
require(
final_checkpoint.validation_loss + 0.01 < initial.validation_loss,
"validation loss did not improve by 0.01 nat",
)?;
Ok(LearnerEvidence {
checkpoints: first.checkpoints,
probe: first.probe,
gradient_l1: first.gradient_l1,
generation: first.generation,
parameter_nodes_preserved: first.parameter_nodes_preserved,
gradients_cleared: first.gradients_cleared,
replay_bitwise,
test_text_encoded_or_scored: first.test_text_encoded_or_scored,
})
} Исполняемый файл выводит точный отчёт для учащегося:
rust/demos/ch23-neural-ngram/src/main.rs#learner-neural-ngram-output fn main() -> Result<(), Box<dyn std::error::Error>> {
print!("{}", ch23_neural_ngram::learner_report()?);
Ok(())
} Запустите cargo run --quiet --locked -p ch23-neural-ngram. Стандартный вывод
зафиксирован в rust/demos/ch23-neural-ngram/expected.txt, поэтому формы, функции
потерь, сгенерированные ID и данные повторного запуска можно точно сопоставить со
значениями, разобранными в главе.
Проследите один контекст и функцию потерь на отложенных данных
Точная трассировка содержит все пять этапов первого прямого прохода, три измерения целевой функции на полных выборках, заданное итоговое сравнение, ID сгенерированных токенов и записи проверок. Совместное чтение этих записей связывает один прямой проход с измеренным улучшением на отложенных данных, не добавляя второй набор значений, вычисленных вручную:
rust/demos/ch23-neural-ngram/src/diagram_trace.rs#neural-ngram-trace pub fn diagram_trace() -> Result<String, FixtureError> {
let evidence = learner_evidence()?;
let initial = evidence.checkpoints[0];
let final_checkpoint = *evidence
.checkpoints
.last()
.expect("fixture always records a final checkpoint");
let mut lines = vec![
format!(
"CONFIG|vocabulary={VOCABULARY_SIZE}|merges={REQUESTED_MERGES}|context={CONTEXT_LENGTH}|embedding={EMBEDDING_WIDTH}|concatenated={}|swiglu_inner={HIDDEN_WIDTH}|hidden={HIDDEN_WIDTH}|parameters=3384|batch={BATCH_SIZE}|evaluation_batch={EVALUATION_BATCH_SIZE}|init_seed={INIT_SEED}|shuffle_seed={SHUFFLE_SEED}|max_steps={MAX_STEPS}|lr={LEARNING_RATE:.6}|beta1={BETA1:.6}|beta2={BETA2:.6}|epsilon={EPSILON:.9}|weight_decay={WEIGHT_DECAY:.6}",
CONTEXT_LENGTH * EMBEDDING_WIDTH,
),
format!(
"SPLIT|train_documents=8|validation_documents=2|test_text_used=no|train_contexts={TRAIN_CONTEXTS}|validation_contexts={VALIDATION_CONTEXTS}|train_batches={TRAIN_BATCHES}|train_evaluation_batches={TRAIN_EVALUATION_BATCHES}|validation_evaluation_batches={VALIDATION_EVALUATION_BATCHES}"
),
format!(
"STAGE|index=0|name=context_ids|shape=[1, 2]|ids={}",
format_ids(&evidence.probe.context_ids)
),
format!(
"STAGE|index=1|name=embeddings|shape=[1, 2, 4]|values={}",
format_values(&evidence.probe.embeddings)
),
format!(
"STAGE|index=2|name=concatenated|shape=[1, 8]|values={}",
format_values(&evidence.probe.concatenated)
),
format!(
"STAGE|index=3|name=hidden|shape=[1, 8]|values={}",
format_values(&evidence.probe.hidden)
),
format!(
"STAGE|index=4|name=logits|shape=[1, 266]|preview={}|argmax={}|argmax_logit={:.6}",
format_values(&evidence.probe.logits_preview),
evidence.probe.argmax,
evidence.probe.argmax_logit,
),
];
lines.extend(evidence.checkpoints.iter().map(|checkpoint| {
format!(
"LOSS|step={}|train={:.6}|validation={:.6}",
checkpoint.step, checkpoint.train_loss, checkpoint.validation_loss
)
}));
lines.extend([
format!(
"RESULT|step={}|initial_validation={:.6}|final_validation={:.6}|improvement={:.6}",
final_checkpoint.step,
initial.validation_loss,
final_checkpoint.validation_loss,
initial.validation_loss - final_checkpoint.validation_loss,
),
format!(
"GENERATE|prompt=At|prompt_ids={}|ids={}|stop={}",
format_ids(&evidence.generation.prompt_ids),
format_ids(&evidence.generation.generated_ids),
evidence.generation.stop.label(),
),
format!(
"PROOF|replay={}|test_text={}|target=final_shifted|gradient_l1={}|parameter_nodes={}|gradients={}|generation=deterministic",
if evidence.replay_bitwise { "bitwise" } else { "changed" },
if evidence.test_text_encoded_or_scored {
"encoded_or_scored"
} else {
"not_encoded_or_scored"
},
if evidence
.gradient_l1
.iter()
.all(|value| value.is_finite() && *value > 0.0)
{
"five_positive_finite"
} else {
"invalid"
},
if evidence.parameter_nodes_preserved {
"preserved"
} else {
"replaced"
},
if evidence.gradients_cleared {
"cleared"
} else {
"retained"
},
),
]);
Ok(lines.join("\n") + "\n")
} Проследите путь одного контекста через обучение
Проследите, как точные ID токенов из вывода Rust проходят через эмбеддинги, конкатенацию, скрытое состояние и логиты словаря, а затем сравните функции потерь на полных обучающей и валидационной выборках.
- Заданная конфигурация модели
- Зафиксированное разбиение данных
- Заданные настройки оптимизатора
1 · Преобразуйте один полный контекст
Преобразуйте один полный контекст
Все тензоры и показанные значения получены при первом прямом проходе с заданным зерном инициализации. Конкатенация меняет расположение данных, но не их значения.
-
0 Целочисленные индексы
ID токенов контекста
- Форма
- Значения Rust
-
-
1 Обучаемые признаки
Строки эмбеддингов
- Форма
- Значения Rust
-
-
2 Обучаемые признаки
Конкатенированные признаки
- Форма
- Значения Rust
-
-
3 Обучаемые признаки
Скрытое состояние SwiGLU
- Форма
- Значения Rust
-
-
4 Оценки классов
Логиты словаря
- Форма
- Значения Rust
-
- ID токена с максимальным начальным логитом
- Максимальный начальный логит
2 · Сравните функции потерь на полных выборках
Сравните функции потерь на полных выборках
Каждая функция потерь охватывает всю соответствующую выборку и взвешивает каждый пакет по фактическому числу строк. Конечный шаг задан до оценки.
-
Контрольное измерение
Шаг оптимизатора
- Функция потерь на обучающей выборке
- Функция потерь на валидационной выборке
-
Контрольное измерение
Шаг оптимизатора
- Функция потерь на обучающей выборке
- Функция потерь на валидационной выборке
-
Заданный конечный шаг
Шаг оптимизатора
- Функция потерь на обучающей выборке
- Функция потерь на валидационной выборке
3 · Измерьте улучшение на отложенных данных
Измерьте улучшение на отложенных данных
- Начальная функция потерь на валидации
- Итоговая функция потерь на валидации
- Снижение функции потерь на валидации
4 · Продолжите последовательность обученной моделью
Продолжите последовательность обученной моделью
Это ID токенов и сохранённые байты намеренно маленькой недообученной модели, а не утверждение о получении читаемого текста.
- Начальный текст
At- ID начального текста
- Причина остановки
limit
5 · Проверьте границы обучения
Проверьте границы обучения
- Независимый повторный запуск
bitwise- Граница тестового текста
not_encoded_or_scored- Правило выбора цели
final_shifted- Проверка градиентов матриц на первом шаге
five_positive_finite- Сохранение узлов параметров
preserved- Градиенты после явного обнуления
cleared- Правило генерации
deterministic
Начальные строки контекста конкатенируются без изменения порядка чисел. После обновлений AdamW полная функция потерь на обучающей выборке снижается с до , а на отложенной валидационной выборке — с до . При показанной точности снижение составляет нат. Это не означает монотонного снижения и не выбирает лучший контрольный шаг: шаг был задан до просмотра результатов валидации.
Жадное продолжение выдаёт ID токенов и останавливается после двенадцати новых токенов. После первого токена сохранённые байты не образуют корректную строку UTF-8, поэтому схема показывает их как ID намеренно маленькой недообученной модели, а не приписывает им вымышленный читаемый текст.
Сначала сделайте предсказания
- Определите все формы для , , , и .
- Выберите целевой токен из сдвинутой строки ширины .
- Определите, у каких из пяти матриц после первого шага должна быть положительная конечная -норма градиента.
- Объясните, как итоговый пакет из строк входит в среднее по всей обучающей выборке.
- Решите, обязана ли функция потерь на валидации снижаться на каждом контрольном шаге.
- Решите, что увидят постоянный объект слоя и копия дескриптора
NamedParameterпосле обновления их общего узла с помощью AdamW, а также обнуляет ли AdamW градиент этого узла. - Предскажите результат жадной генерации, если наибольший логит принадлежит BOS.
- Укажите операцию, которая привела бы к утечке тестовых данных.
Проверьте предсказания
- Цепочка форм: .
- Используется только элемент строки целей с индексом — токен после полного контекста.
- Матрицы эмбеддингов, вентильной ветви, ветви расширения, проекции сжатия и выходной проекции имеют положительные конечные -нормы градиентов.
- Умножьте среднее по этому пакету на , прибавьте взвешенные суммы остальных пакетов и разделите на все контекстов.
- Нет. В этом примере итоговая функция потерь на валидации должна быть меньше начальной более чем на нат, но монотонность не требуется.
- При копировании
NamedParameterкопируется дескриптор, а не данные тензора и не сам узел. Копия дескриптора и постоянный объект слоя по-прежнему ссылаются на один узелTensorValue, поэтому оба увидят новое значение. AdamW оставляет накопленный градиент в этом узле, а цикл обучения явно вызываетzero_grad()после обновления. Для независимого повторного запуска создаётся отдельная модель с тем же зерном инициализации. - BOS маскируется, поэтому выбирает наибольший из оставшихся конечных логитов; при точном равенстве побеждает меньший ID.
- Границу нарушило бы чтение текста тестовых документов для подгонки, кодирования, выбора, ранней остановки, оценки или показа.
Далее замените фиксированный контекст каузальным обменом информацией
Теперь собранные компоненты обучают полную модель следующего токена с фиксированным контекстом, используя зафиксированное разбиение данных и AdamW, а затем детерминированно генерируют токены жадным алгоритмом. В главах 24–32 конкатенацию фиксированного контекста заменит каузальное смешивание информации между позициями последовательности на основе остаточных связей, нормализации и механизма внимания.
Модель уже работоспособна, но намеренно ограничена: каждое предсказание видит ровно предшествующих ID независимо от объёма более раннего контекста. Следом появятся остаточные пути, а затем нормализация и маскированное самовнимание расширят вычисления по последовательности.