34 · Версия материала 7
Передайте тестовую выборку одному локальному оценщику и сохраните отчёт
Разберитесь, как в пределах одного запуска зафиксировать решения, принятые по валидации, проверить и сохранить упорядоченные пары входных и целевых токенов, а затем отделить корректное сравнение на намеренно выбранном фиксированном примере от независимой оценки способности модели обобщать и от доказательства общего превосходства архитектуры.
Зафиксируйте условия сравнения до открытия тестовой выборки
Оцените зафиксированный декодер, выбранный по валидации, через один локальный механизм доступа только к тестовой выборке. При открытии доступа проверьте и сохраните упорядоченные пары входных и целевых токенов, усредните потери по всем целевым токенам, а затем сравните результат с зафиксированной биграммной моделью, обученной на тех же обучающих токенах. В главе 33 контрольная точка уже была выбрана по значению функции потерь на валидации . Там же доказано, что попытка обратиться к тестовой выборке во время выбора была отклонена.
До создания механизма доступа к тестовой выборке пример фиксирует все оставшиеся решения:
- размер словаря и длина контекста ;
- снимок параметров, выбранный в главе 33;
- биграммная модель со сглаживанием с параметром один, обученная ровно на тех же двух срезах обучающих токенов;
- последовательные окна с шагом один и размер пакета — ;
- два тестовых документа разной длины.
test-a = [4,3,2,1,0,4,3,2,1]
test-b = [3,2,1,0,4,3,2]
Первый документ даёт позиций целевых токенов, второй — . Каждой цели
соответствует входная позиция, но в функции потерь остаётся одно слагаемое на
цель: входы не удваивают знаменатель. Поэтому , а не ID.
FinalEvaluator::evaluate_once открывает локальный доступ, проверяет
упорядоченные пары, оценивает обе модели, убеждается, что все биты параметров и
градиентов декодера остались прежними, и только после этого возвращает отчёт со
значениями
Таким образом, на этом примере результат декодера ниже на . Тестовые документы намеренно следуют синтетическому обучающему циклу в обратном направлении. Их выбрали именно ради записанного порядка результатов после того, как нейтральная отложенная выборка его не сохранила. Теперь этот точный порядок повторно проверяется как условие регрессии. Значения корректно характеризуют фиксированный учебный пример и исполняемую границу, но не являются независимой оценкой способности модели обобщать на ранее не использованных данных и не доказывают общего превосходства архитектуры декодера.
Усредняйте неожиданность по целевым токенам
Полное итоговое значение функции потерь декодера равно
Состояние уже зафиксировано до открытия локального доступа. В пределах этого запуска значение функции потерь на тестовой выборке может характеризовать состояние, но не может повлиять на , расписание скорости обучения, токенизатор, базовую модель или любое другое решение. Эта гарантия одного запуска не превращает намеренно выбранный и постоянно проверяемый пример из репозитория в независимую оценку способности модели обобщать.
Документы разной длины требуют взвешивания по числу токенов. Если документ даёт целевых позиций со средним значением , то
В этом запуске , и . Если усреднить два средних значения по документам, короткий документ ошибочно получит тот же вес, что и длинный.
Не смешивайте состояния, позиции и роли выборок
Для итоговой оценки важно не только число , но и то, какие пары «вход — цель» входят в этот набор. В каждой позиции входной контекст , доступный до предсказываемого токена, сопоставлен с наблюдаемым следующим токеном . Позицию однозначно задают документ, окно и место внутри окна. Поэтому при перекрывающихся окнах один и тот же переход в исходном документе может учитываться несколько раз.
- — среднее отрицательное логарифмическое правдоподобие на
Testс весами по числу целевых токенов. - — полное состояние декодера, выбранное до открытия тестовой выборки.
- — индекс контрольной точки, выбранной по валидации; здесь .
- — полное число позиций целевых токенов тестовой выборки; здесь . Каждой цели соответствует одна входная позиция.
- нумерует одну сопоставленную пару входной и целевой позиций в неизменном порядке документов, окон и мест внутри окна.
- — входной контекст декодера, доступный для цели в позиции .
- — наблюдаемый следующий токен, служащий целевым значением в позиции .
- — вероятность, которую зафиксированный декодер приписывает этому токену.
- — натуральный логарифм, поэтому потери измеряются в натах на один целевой токен.
В реализации Train, Validation и Test — конкретные варианты разбиения данных.
Их смысловые роли различаются: обучающая выборка подгоняет параметры,
валидационная выбирает состояние, а действительно не использованная ранее
тестовая выборка может дать независимую итоговую оценку. Если результат на
тестовой выборке изменяет какое-либо решение, он становится данными для выбора
модели. Известный результат, который постоянно запускают повторно, полезен для
регрессионной проверки, но при каждом запуске не становится новой независимой
оценкой.
Локальный механизм намеренно не гарантирует единственность доступа во всей системе. Другой процесс может создать ещё один оценщик для копии тех же данных. Для настоящей оценки также нужны контроль доступа, журналы аудита и правила управления наборами данных.
От результатов обучения к управляемой итоговой оценке LLM
В ранних исследованиях нейронных языковых моделей постепенно переходили от отчётов по обучающей выборке и адаптивного обращения к отложенным данным к протоколу с тремя ролями: обучающая выборка подгоняет параметры, валидационная выбирает состояние, а тестовая даёт итоговую оценку после фиксации решений. Этот идеальный протокол нужно отличать от известного фиксированного примера, результаты которого уже используются для регрессионной проверки.
A Neural Probabilistic Language Model показывает ранний пример такого разделения в нейронной языковой модели. Бенжио и соавторы используют отдельные обучающую, валидационную и тестовую части данных, опираются на валидацию при выборе модели и ранней остановке, а затем приводят тестовую перплексию ранней нейронной языковой модели. Результат на обучающей выборке измеряет качество на данных, к которым модель уже подгоняла параметры, а многократное адаптивное использование одной отложенной выборки может привести к переобучению на ней и лишить результат статуса независимого свидетельства. Работа подтверждает разделение трёх ролей, но не утверждает, что к тестовой выборке обращались ровно один раз.
Generalization in Adaptive Data Analysis and Holdout Reuse обосновывает предупреждение об адаптивном использовании данных. Дворк и соавторы показывают, что многократное адаптивное использование обычной отложенной выборки может привести к переобучению на самой этой выборке; этот общий вывод не устанавливает фактов об истории или результатах учебного примера из данного репозитория.
Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer показывает, как явно выбирать контрольную точку в более крупном исследовании. Раффель и соавторы сохраняют контрольные точки дообучения, выбирают их по результату на валидации и, как правило, проводят исследовательские сравнения на валидационных данных, чтобы не выбирать модель по тестовому набору до итогового отчёта. Конкретный критерий зависит от задачи: это не обязательно значение функции потерь на валидации.
Language Models are Few-Shot Learners показывает, почему при переходе к веб-масштабу особенно важно знать происхождение данных. Браун и соавторы исследуют пересечение данных веб-предобучения с тестовыми наборами, сравнивают очищенные подмножества и исключают несколько задач языкового моделирования со значительным пересечением между тестовыми данными и данными предобучения. Это показывает, почему формальных меток частей данных недостаточно, чтобы установить их происхождение. Таким образом, метка «тестовая выборка» сама по себе не доказывает независимость данных. Небольшие изменения результатов на большинстве очищенных подмножеств допускают несколько объяснений и не доказывают, что именно пересечение данных повысило оценку.
В исследованиях нейронных языковых моделей стали отдельно подгонять параметры, принимать решения по валидации и сообщать итоговый результат на тестовой выборке. В крупных работах по переносу обучения выбор контрольной точки по результатам валидации сделали явной частью процесса, а при предобучении на веб-данных к итоговой оценке добавили поиск пересечений между наборами данных и проверку их происхождения. Анализ адаптивных данных дополняет эту историю предупреждением о многократном использовании отложенной выборки. Для надёжного итогового сравнения LLM решения о модели и данных фиксируют до оценки, сохраняют сведения о происхождении данных и отличают независимую оценку на ранее не использованных данных от известного фиксированного примера, сохранённого для регрессионной проверки.
В этой главе строгое правило одного локального механизма доступа делает видимой границу внутри запуска. Оно не означает, что в процитированных работах тестовую выборку запрашивали ровно один раз, и не заменяет управление доступом к реальным наборам данных. На уровне репозитория документы с обратным циклом были намеренно выбраны ради записанного порядка результатов, который затем постоянно проверяется. Поэтому значения служат регрессионной проверкой фиксированного примера, а не независимой оценкой способности модели обобщать на ранее не использованных данных. Эти правила и факты о репозитории принадлежат курсу и не приписываются научным работам.
rust/crates/llm-from-scratch/src/evaluation.rs#once-only-final-evaluation /// Owns one test epoch and consumes its local scoring permission on first use.
///
/// This type is deliberately not cloneable. It enforces one access through one
/// owner; external dataset governance is still required to prevent another
/// process from constructing a separate owner over copied data. Construction
/// checks the epoch's stored `Test` enum value, not its external lineage.
#[derive(Debug)]
pub struct FinalEvaluator {
test_epoch: MiniBatchEpoch,
provenance: EvaluationProvenance,
access_count: u8,
}
impl FinalEvaluator {
pub fn new(
test_epoch: MiniBatchEpoch,
provenance: EvaluationProvenance,
) -> Result<Self, EvaluationError> {
if test_epoch.partition() != Partition::Test {
return Err(EvaluationError::WrongTestPartition {
actual: test_epoch.partition(),
});
}
if test_epoch.window_count() == 0 {
return Err(EvaluationError::EmptyTestEpoch);
}
if test_epoch.context_length() != provenance.context_length() {
return Err(EvaluationError::EpochContextMismatch {
expected: provenance.context_length(),
actual: test_epoch.context_length(),
});
}
Ok(Self {
test_epoch,
provenance,
access_count: 0,
})
}
pub const fn access_count(&self) -> u8 {
self.access_count
}
pub fn evaluate_once(
&mut self,
decoder: SelectedDecoder<'_>,
bigram: FrozenBigram<'_>,
) -> Result<FinalEvaluationReport, EvaluationError> {
if self.access_count != 0 {
return Err(EvaluationError::AlreadyEvaluated);
}
require_matching_provenance_assertions(&self.provenance, decoder.provenance())?;
require_matching_provenance_assertions(&self.provenance, bigram.provenance())?;
if !selected_state_matches_model(decoder.state(), decoder.model()) {
return Err(EvaluationError::SelectedStateMismatch);
}
let model_config = decoder.model().config();
if model_config.max_positions() != self.provenance.context_length() {
return Err(EvaluationError::ModelContextMismatch {
expected: self.provenance.context_length(),
actual: model_config.max_positions(),
});
}
let decoder_vocabulary = model_config.vocabulary_size();
let bigram_vocabulary = bigram.model().vocabulary_size();
if decoder_vocabulary != bigram_vocabulary {
return Err(EvaluationError::VocabularyMismatch {
decoder: decoder_vocabulary,
bigram: bigram_vocabulary,
});
}
// Every metadata error above leaves the test unopened. From this line on,
// even an error burns the local gate because token evidence is inspected.
self.access_count = 1;
let inspected = InspectedTestEpoch::inspect(&self.test_epoch, decoder_vocabulary)?;
let model = decoder.model();
let parameters_before = parameter_bits(model);
let gradients_before = gradient_bits(model)?;
let measured = evaluate_no_grad(model, inspected.epoch())?;
if measured.recorded_graphs() != 0 {
return Err(EvaluationError::GraphRecorded {
count: measured.recorded_graphs(),
});
}
let parameters_after = parameter_bits(model);
if parameters_after != parameters_before {
return Err(EvaluationError::DecoderParameterChanged);
}
let gradients_after = gradient_bits(model)?;
if gradients_after != gradients_before {
return Err(EvaluationError::DecoderGradientChanged);
}
let decoder_score = ModelScore::new(
EvaluatedModel::SelectedDecoder,
measured.mean_loss() * measured.token_count() as f64,
measured.token_count(),
measured.mean_loss(),
measured.mean_loss().exp(),
)?;
let bigram_score = score_bigram(bigram.model(), &inspected)?;
if inspected.evidence().window_target_slot_count != measured.token_count()
|| inspected.evidence().window_target_slot_count != bigram_score.target_count()
{
return Err(EvaluationError::TargetCountMismatch {
expected: inspected.evidence().window_target_slot_count,
decoder: measured.token_count(),
bigram: bigram_score.target_count(),
});
}
let evidence = inspected.into_evidence();
Ok(FinalEvaluationReport {
version: FINAL_EVALUATION_REPORT_VERSION,
selected_step: decoder.selected_step(),
selected_validation_loss: decoder.selected_validation_loss(),
provenance: self.provenance.clone(),
test_document_ids: evidence.document_ids,
window_slot_fingerprint: evidence.window_slot_fingerprint,
window_count: self.test_epoch.window_count(),
batch_count: self.test_epoch.batch_count(),
window_target_slot_count: evidence.window_target_slot_count,
document_transition_occurrence_count: evidence.document_transition_occurrence_count,
transition_multiplicity_counts: evidence.transition_multiplicity_counts,
decoder: decoder_score,
bigram: bigram_score,
access_count: self.access_count,
recorded_graphs: measured.recorded_graphs(),
parameters_unchanged: true,
gradients_unchanged: true,
})
}
} Реализуйте правила итоговой оценки в коде
EvaluationProvenance хранит три непустые строки, заданные вызывающим кодом и
названные отпечатками корпуса, разбиения и токенизатора, а также положительную
длину контекста. Оценщик проверяет точное совпадение этих строк, но сам не
вычисляет их и не сверяет с фактическим корпусом, способом разбиения или
токенизатором. Поэтому одинаковыми строками можно ошибочно пометить разные
данные или токенизаторы. Длина контекста проверяется строже: она должна также
совпадать с длиной контекста тестовой эпохи и максимально допустимой длиной
контекста декодера.
SelectedDecoder::new требует, чтобы вызывающий код передал Validation как
заявленный источник выбора. Значение selected_step не проверяется, а
selected_validation_loss проверяется только на конечность и
неотрицательность. Позже, перед открытием доступа, оценщик сверяет сохранённое
состояние с переданным декодером, но не восстанавливает историю выбора.
FrozenBigram::new требует
заявленную метку Train и ненулевое число обработанных документов, однако не
может определить, по каким документам были рассчитаны счётчики. FinalEvaluator
проверяет сохранённую в эпохе метку Test, но эта метка сама по себе не
доказывает независимость тестовых данных.
rust/crates/llm-from-scratch/src/evaluation.rs#evaluation-provenance /// Caller-supplied identifiers and context metadata shared by report participants.
///
/// Construction proves only that the three identifier strings are nonblank and
/// the context length is positive. Equality between values of this type proves
/// only that callers supplied matching assertions; it does not hash or inspect a
/// corpus, split construction, or tokenizer. The evaluator separately checks the
/// context value against the test epoch and decoder capacity.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EvaluationProvenance {
corpus_fingerprint: String,
split_fingerprint: String,
tokenizer_fingerprint: String,
context_length: usize,
}
impl EvaluationProvenance {
pub fn new(
corpus_fingerprint: impl Into<String>,
split_fingerprint: impl Into<String>,
tokenizer_fingerprint: impl Into<String>,
context_length: usize,
) -> Result<Self, EvaluationError> {
let provenance = Self {
corpus_fingerprint: corpus_fingerprint.into(),
split_fingerprint: split_fingerprint.into(),
tokenizer_fingerprint: tokenizer_fingerprint.into(),
context_length,
};
for (field, value) in [
(ProvenanceField::Corpus, provenance.corpus_fingerprint()),
(ProvenanceField::Split, provenance.split_fingerprint()),
(
ProvenanceField::Tokenizer,
provenance.tokenizer_fingerprint(),
),
] {
if value.trim().is_empty() {
return Err(EvaluationError::EmptyProvenance { field });
}
}
if context_length == 0 {
return Err(EvaluationError::ZeroContextLength);
}
Ok(provenance)
}
pub fn corpus_fingerprint(&self) -> &str {
&self.corpus_fingerprint
}
pub fn split_fingerprint(&self) -> &str {
&self.split_fingerprint
}
pub fn tokenizer_fingerprint(&self) -> &str {
&self.tokenizer_fingerprint
}
pub const fn context_length(&self) -> usize {
self.context_length
}
}
/// A borrowed decoder accompanied by the caller's validation-role assertion.
///
/// Construction checks the asserted role and the numeric shape of the supplied
/// selection values. It cannot reconstruct how `selected_step`,
/// `selected_validation_loss`, or the model were selected. Before test access,
/// the evaluator does mechanically compare the retained state with the borrowed
/// model's exact configuration, names, shapes, and value bits.
#[derive(Clone, Copy, Debug)]
pub struct SelectedDecoder<'a> {
state: &'a DecoderModelState,
model: &'a DecoderModel,
selected_step: usize,
selected_validation_loss: f64,
provenance: &'a EvaluationProvenance,
}
impl<'a> SelectedDecoder<'a> {
pub fn new(
state: &'a DecoderModelState,
model: &'a DecoderModel,
selected_step: usize,
selected_validation_loss: f64,
selection_partition_assertion: Partition,
provenance: &'a EvaluationProvenance,
) -> Result<Self, EvaluationError> {
if selection_partition_assertion != Partition::Validation {
return Err(EvaluationError::WrongSelectionPartition {
actual: selection_partition_assertion,
});
}
if !selected_validation_loss.is_finite() || selected_validation_loss < 0.0 {
return Err(EvaluationError::InvalidSelectionLoss {
value: selected_validation_loss,
});
}
Ok(Self {
state,
model,
selected_step,
selected_validation_loss,
provenance,
})
}
const fn model(self) -> &'a DecoderModel {
self.model
}
const fn state(self) -> &'a DecoderModelState {
self.state
}
pub const fn selected_step(self) -> usize {
self.selected_step
}
pub const fn selected_validation_loss(self) -> f64 {
self.selected_validation_loss
}
pub const fn provenance(self) -> &'a EvaluationProvenance {
self.provenance
}
}
/// A borrowed count baseline accompanied by the caller's training-role assertion.
///
/// Construction checks the asserted role and that the model reports at least one
/// fitted document. It cannot discover which documents produced the counts.
#[derive(Clone, Copy, Debug)]
pub struct FrozenBigram<'a> {
model: &'a BigramModel,
provenance: &'a EvaluationProvenance,
}
impl<'a> FrozenBigram<'a> {
pub fn new(
model: &'a BigramModel,
fit_partition_assertion: Partition,
provenance: &'a EvaluationProvenance,
) -> Result<Self, EvaluationError> {
if fit_partition_assertion != Partition::Train {
return Err(EvaluationError::WrongBaselinePartition {
actual: fit_partition_assertion,
});
}
if model.fitted_documents() == 0 {
return Err(EvaluationError::UnfittedBigram);
}
Ok(Self { model, provenance })
}
pub const fn model(self) -> &'a BigramModel {
self.model
}
pub const fn provenance(self) -> &'a EvaluationProvenance {
self.provenance
}
} В учебном примере главы 34 требуемая история обеспечивается в местах сборки
объектов. TrainingResult предоставляет фактическое сохранённое состояние из
главы 33 и соответствующий декодер. Пример получает точные обучающие срезы главы
33 через функцию только для чтения и до создания тестового оценщика обучает на
них прежнюю биграммную модель с аддитивным сглаживанием. Такие места вызова дают
больше оснований, чем метки универсальных конструкторов; для внешних наборов
данных всё равно нужны надёжное вычисление отпечатков, контроль доступа, журналы
аудита и управление данными.
Эта история сборки также определяет область применимости результата. Документы с обратным циклом намеренно выбрали так, чтобы значение функции потерь декодера было ниже, и учебная программа сохраняет этот порядок как условие регрессии. Универсальный оценщик при этом остаётся нейтральным: на другой фиксированной последовательности значение биграммной модели оказывается ниже, а оценка всё равно выполняется без графа и не меняет биты состояния.
rust/demos/ch34-final-evaluation/src/lib.rs#learner-evidence #[derive(Clone, Debug, PartialEq)]
pub struct LearnerEvidence {
pub report: FinalEvaluationReport,
pub selection_test_partition_rejected: bool,
pub gate_openings_before: u8,
pub baseline_alpha: f64,
pub baseline_documents: usize,
pub baseline_transitions: u64,
pub token_weighted: bool,
pub provenance_assertions_match: bool,
pub within_run_selection_isolated: bool,
}
/// Builds both frozen candidates before opening one owned test evaluator.
pub fn learner_evidence() -> Result<LearnerEvidence, FixtureError> {
let selected = selection_evidence()?;
require(
selected.test_partition_rejected,
"selection no longer rejects the test partition",
)?;
let provenance_assertions = fixture_provenance_assertions()?;
let training_documents = fixture_training_documents();
let baseline = BigramModel::fit_training_documents(
VOCABULARY_SIZE,
BIGRAM_ALPHA,
training_documents.iter().map(|(_, tokens)| *tokens),
)?;
require(
baseline.fitted_documents() == training_documents.len(),
"baseline training document count changed",
)?;
require(
baseline.fitted_transitions() == 22,
"baseline training transition count changed",
)?;
let decoder = SelectedDecoder::new(
selected.result.selected_state(),
selected.result.selected_model(),
selected.result.selected_step(),
selected.result.selected_validation_loss(),
Partition::Validation,
&provenance_assertions,
)?;
let bigram = FrozenBigram::new(&baseline, Partition::Train, &provenance_assertions)?;
let mut evaluator = FinalEvaluator::new(test_epoch()?, provenance_assertions.clone())?;
let gate_openings_before = evaluator.access_count();
require(gate_openings_before == 0, "test gate opened before scoring")?;
let report = evaluator.evaluate_once(decoder, bigram)?;
require(report.version() == 1, "report version changed")?;
require(
report.access_count() == 1,
"test gate-opening count changed",
)?;
require(report.target_count() == 24, "test target count changed")?;
require(report.window_count() == 12, "test window count changed")?;
require(report.batch_count() == 3, "test batch count changed")?;
require(
report.test_document_ids() == ["test-a", "test-b"],
"test document order changed",
)?;
require(
report.target_fingerprint() == "fnv1a64:dac4bb4d76beeb59",
"test evidence fingerprint changed",
)?;
require(
report.decoder().target_count() == report.bigram().target_count(),
"models did not score identical target counts",
)?;
require(
report.recorded_graphs() == 0
&& report.parameters_unchanged()
&& report.gradients_unchanged(),
"graph-free state-preservation proof changed",
)?;
require(
report.decoder_has_lower_loss(),
"fixed Chapter 34 fixture no longer records lower decoder loss than its bigram",
)?;
let token_weighted = [report.decoder(), report.bigram()]
.into_iter()
.all(|score| {
(score.total_nll() / score.target_count() as f64 - score.mean_nll()).abs() <= 1e-12
});
require(token_weighted, "model scores are no longer token weighted")?;
let provenance_assertions_match = report.provenance() == &provenance_assertions;
require(
provenance_assertions_match,
"report provenance assertions no longer match the fixture assertions",
)?;
let within_run_selection_isolated = selected.test_partition_rejected
&& gate_openings_before == 0
&& report.access_count() == 1
&& report.recorded_graphs() == 0
&& report.parameters_unchanged()
&& report.gradients_unchanged();
require(
within_run_selection_isolated,
"within-run selection isolation evidence changed",
)?;
Ok(LearnerEvidence {
report,
selection_test_partition_rejected: selected.test_partition_rejected,
gate_openings_before,
baseline_alpha: baseline.alpha(),
baseline_documents: baseline.fitted_documents(),
baseline_transitions: baseline.fitted_transitions(),
token_weighted,
provenance_assertions_match,
within_run_selection_isolated,
})
} FinalEvaluator владеет непустой эпохой с меткой Test: его нельзя ни
клонировать, ни копировать. Ошибки заявленной роли, согласованности заявленных сведений, несоответствия
сохранённого состояния выбранному декодеру, длины контекста или размера словаря
оставляют счётчик открытий доступа равным . После успешного завершения этих
проверок оценщик меняет счётчик на до чтения первого ID тестового токена.
Поэтому несовпадение длин входов и целей, выход ID за границы словаря или ошибка
в последующих вычислениях всё равно расходуют локальное разрешение; повторный вызов вернёт
AlreadyEvaluated. Сам декодер не расходуется: одноразовым ресурсом служит
разрешение начать работу с этой тестовой эпохой.
InspectedTestEpoch и его поля доступны только внутри модуля оценки. Сейчас
модуль создаёт это представление только через inspect, не предоставляет методов
изменения и хранит неизменяемую ссылку на исходную эпоху. Поэтому список ID
документов, упорядоченный отпечаток, число целей и пары
[input_index, target_index] остаются связаны с этой эпохой. В каждом пакете код
сначала проверяет равенство числа входов и целей. Затем inspect обходит позиции
в порядке следования в плоском массиве и для каждой позиции сначала проверяет
входной ID, а затем целевой ID. Ещё до открытия доступа проверяется равенство размеров словарей декодера и
биграммной модели, поэтому сохранённые индексы допустимы для обеих моделей. На
каждую из целевых позиций представление хранит два индекса, то есть
занимает дополнительной памяти.
rust/crates/llm-from-scratch/src/evaluation.rs#inspected-test-epoch #[derive(Debug)]
struct TestEvidence {
document_ids: Vec<String>,
window_slot_fingerprint: String,
window_target_slot_count: usize,
document_transition_occurrence_count: usize,
transition_multiplicity_counts: Vec<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct CheckedTokenPair {
input: usize,
target: usize,
}
/// One gate-opening inspection of a borrowed test epoch.
///
/// This type is private to the module. Current callers construct it only through
/// `inspect`; it exposes no mutation API and retains an immutable epoch borrow.
#[derive(Debug)]
struct InspectedTestEpoch<'a> {
epoch: &'a MiniBatchEpoch,
evidence: TestEvidence,
checked_pairs: Vec<CheckedTokenPair>,
}
fn append_checked_aligned_tokens(
batch: usize,
inputs: &[u32],
targets: &[u32],
vocabulary_size: usize,
checked_pairs: &mut Vec<CheckedTokenPair>,
) -> Result<(), EvaluationError> {
if inputs.len() != targets.len() {
return Err(EvaluationError::TargetAlignmentMismatch {
batch,
inputs: inputs.len(),
targets: targets.len(),
});
}
for (position, (&input, &target)) in inputs.iter().zip(targets).enumerate() {
let Some(input_index) = usize::try_from(input)
.ok()
.filter(|id| *id < vocabulary_size)
else {
return Err(EvaluationError::InputTokenOutOfRange {
batch,
position,
id: input,
vocabulary_size,
});
};
let Some(target_index) = usize::try_from(target)
.ok()
.filter(|id| *id < vocabulary_size)
else {
return Err(EvaluationError::TargetTokenOutOfRange {
batch,
position,
id: target,
vocabulary_size,
});
};
checked_pairs.push(CheckedTokenPair {
input: input_index,
target: target_index,
});
}
Ok(())
}
impl<'a> InspectedTestEpoch<'a> {
fn inspect(epoch: &'a MiniBatchEpoch, vocabulary_size: usize) -> Result<Self, EvaluationError> {
let mut document_ids = Vec::new();
let mut window_target_slot_count = 0_usize;
let mut transition_multiplicities = BTreeMap::<(String, usize), usize>::new();
let mut fingerprint = 14_695_981_039_346_656_037_u64;
let checked_pair_count = epoch
.batches()
.iter()
.map(|batch| batch.targets().len())
.sum();
let mut checked_pairs = Vec::with_capacity(checked_pair_count);
for (batch_index, batch) in epoch.batches().iter().enumerate() {
append_checked_aligned_tokens(
batch_index,
batch.inputs(),
batch.targets(),
vocabulary_size,
&mut checked_pairs,
)?;
for row in 0..batch.batch_width() {
let origin = &batch.provenance()[row];
if !document_ids
.iter()
.any(|existing| existing == origin.document_id())
{
document_ids.push(origin.document_id().to_owned());
}
fnv1a_bytes(&mut fingerprint, origin.document_id().as_bytes());
fnv1a_byte(&mut fingerprint, 0xff);
fnv1a_bytes(&mut fingerprint, &(origin.start() as u64).to_le_bytes());
let inputs = batch
.input_row(row)
.expect("batch row is constructed from complete inputs");
let targets = batch
.target_row(row)
.expect("batch row is constructed from complete targets");
for (slot, (&input, &target)) in inputs.iter().zip(targets).enumerate() {
fnv1a_bytes(&mut fingerprint, &input.to_le_bytes());
fnv1a_bytes(&mut fingerprint, &target.to_le_bytes());
window_target_slot_count += 1;
let absolute_target_position = origin.start() + slot + 1;
*transition_multiplicities
.entry((origin.document_id().to_owned(), absolute_target_position))
.or_default() += 1;
}
}
}
let document_transition_occurrence_count = transition_multiplicities.len();
let maximum_multiplicity = transition_multiplicities
.values()
.copied()
.max()
.unwrap_or(0);
let mut transition_multiplicity_counts = vec![0_usize; maximum_multiplicity];
for multiplicity in transition_multiplicities.into_values() {
transition_multiplicity_counts[multiplicity - 1] += 1;
}
debug_assert_eq!(window_target_slot_count, checked_pairs.len());
debug_assert_eq!(
window_target_slot_count,
transition_multiplicity_counts
.iter()
.enumerate()
.map(|(index, count)| (index + 1) * count)
.sum::<usize>()
);
Ok(Self {
epoch,
evidence: TestEvidence {
document_ids,
window_slot_fingerprint: format!("fnv1a64:{fingerprint:016x}"),
window_target_slot_count,
document_transition_occurrence_count,
transition_multiplicity_counts,
},
checked_pairs,
})
}
const fn epoch(&self) -> &'a MiniBatchEpoch {
self.epoch
}
const fn evidence(&self) -> &TestEvidence {
&self.evidence
}
fn checked_pairs(&self) -> &[CheckedTokenPair] {
&self.checked_pairs
}
fn into_evidence(self) -> TestEvidence {
self.evidence
}
} evaluate_no_grad по-прежнему отдельно обходит исходную эпоху при оценке
декодера. Этот вызов не записывает граф вычислений, а оценщик сравнивает биты
параметров и градиентов до и после него. Для оценки биграммной модели
используется только проверенное внутреннее представление: код читает сохранённые
пары индексов, включая повторы из-за перекрывающихся окон декодера, и не
проверяет исходные массивы ID повторно.
Доступный только внутри крейта метод биграммной модели доверяет этим индексам и
использует ту же арифметику числителя и знаменателя, что и публичный метод
вероятности. Публичный BigramModel::smoothed_probability принимает два ID типа
u32 и проверяет границы обоих. Остальные публичные методы, принимающие ID,
также сохраняют свои прежние проверки.
Таким образом, входные данные проверяются на одной границе — при открытии доступа
к тестовой эпохе. Результаты этой проверки используются и при сборе сведений для
отчёта, и при последующей оценке биграммной модели. Это не означает один
физический проход по памяти и не отменяет проверок, необходимых при оценке
декодера: сбор сведений, оценка декодера без графа и оценка биграммной модели
по-прежнему выполняют отдельные обходы для разных задач. Кроме того, «проверка
входных данных» здесь означает проверку длин и границ ID; вариант разбиения
Validation — это более ранний этап, на котором была выбрана контрольная точка
.
У отчёта есть методы чтения, но нет методов изменения или выбора модели. В нём хранятся версия схемы , номер выбранного шага, заявленные вызывающим кодом сведения о происхождении, отпечаток упорядоченных целевых позиций, счётчики, результаты обеих моделей, значение счётчика открытий доступа и результаты проверки сохранности состояния. Затем учебный вывод явно указывает область применимости:
evidence=scope:fixed-fixture-regression within_run_selection_isolated:true fixture_selected_for_ordering:true independent_generalization_estimate:false architecture_superiority_evidence:false
В трассировке схемы численное соотношение называется
decoder_lower_on_fixture=true; прежнее имя decoder_beats_bigram не является
актуальным свидетельством.
rust/crates/llm-from-scratch/src/bigram.rs#checked-bigram-probability /// Scores indices whose vocabulary bounds were established by a crate-owned boundary.
pub(crate) fn smoothed_probability_for_checked_indices(
&self,
from: usize,
to: usize,
) -> Result<f64, BigramError> {
debug_assert!(from < self.vocabulary_size);
debug_assert!(to < self.vocabulary_size);
let row_start = from * self.vocabulary_size;
let numerator = self.counts[row_start + to] as f64 + self.alpha;
let row_total = self.counts[row_start..row_start + self.vocabulary_size]
.iter()
.try_fold(0_u64, |total, count| {
total
.checked_add(*count)
.ok_or(BigramError::TooManyTransitions)
})?;
let denominator = row_total as f64 + self.alpha * self.vocabulary_size as f64;
Ok(numerator / denominator)
} rust/demos/ch34-final-evaluation/src/main.rs fn main() {
print!(
"{}",
ch34_final_evaluation::learner_report().expect("Chapter 34 fixture must remain valid")
);
} Выполните cargo run --quiet --locked -p ch34-final-evaluation. Отчёт показывает
отклонённую попытку обратиться к тестовой выборке во время выбора, значение
счётчика открытий доступа до и после итоговой оценки, оба значения функции потерь с
весами по числу целевых токенов, проверки сохранности состояния и область
применимости результата фиксированного примера.
Проследите одну информационную границу и одно сравнение
Данные отчёта объединяют состояние доступа, заявленные сведения о происхождении, проверенную последовательность пар «вход — цель», результаты обеих моделей, общее число целевых позиций, факты о сохранности состояния, проверенные самой реализацией, и область применимости регрессионной проверки фиксированного примера.
rust/demos/ch34-final-evaluation/src/diagram_trace.rs#final-evaluation-trace /// Emits the exact static evidence consumed by the Chapter 34 figure.
pub fn diagram_trace() -> Result<String, FixtureError> {
let evidence = learner_evidence()?;
let report = &evidence.report;
Ok(format!(
"FINAL_EVALUATION_TRACE_V1\n\
REPORT|version={}|partition=test|selected_step={}|selection_criterion=validation-only|gate_openings_before={}|gate_openings_after={}\n\
GATE|selection_test_partition_rejected={}\n\
PROVENANCE|corpus={}|split={}|tokenizer={}|vocabulary={}|context={}|documents={}|windows={}|batches={}|targets={}|target_fingerprint={}\n\
SCORE|model=selected-decoder|fit_partition=train|selected_by=validation|targets={}|total_nll={:.6}|mean_nll={:.6}|perplexity={:.6}\n\
SCORE|model=frozen-bigram|fit_partition=train|selected_by=none|targets={}|total_nll={:.6}|mean_nll={:.6}|perplexity={:.6}\n\
COMPARE|lower_loss=selected-decoder|loss_gap={:.6}|same_targets=true|decoder_lower_on_fixture={}|evidence_scope={}|within_run_selection_isolated={}|fixture_selected_for_ordering={}|independent_generalization_estimate={}|architecture_superiority_evidence={}\n\
PROOF|token_weighted={}|provenance_assertions_match={}|graph_nodes={}|parameters_unchanged={}|gradients_unchanged={}|selection_closed={}\n\
END_FINAL_EVALUATION_TRACE\n",
report.version(),
report.selected_step(),
evidence.gate_openings_before,
report.access_count(),
evidence.selection_test_partition_rejected,
report.provenance().corpus_fingerprint(),
report.provenance().split_fingerprint(),
report.provenance().tokenizer_fingerprint(),
VOCABULARY_SIZE,
report.provenance().context_length(),
report.test_document_ids().join(","),
report.window_count(),
report.batch_count(),
report.target_count(),
report.target_fingerprint(),
report.decoder().target_count(),
report.decoder().total_nll(),
report.decoder().mean_nll(),
report.decoder().perplexity(),
report.bigram().target_count(),
report.bigram().total_nll(),
report.bigram().mean_nll(),
report.bigram().perplexity(),
report.loss_gap(),
report.decoder_has_lower_loss(),
FIXED_FIXTURE_EVIDENCE_SCOPE,
evidence.within_run_selection_isolated,
FIXTURE_SELECTED_FOR_ORDERING,
INDEPENDENT_GENERALIZATION_ESTIMATE,
ARCHITECTURE_SUPERIORITY_EVIDENCE,
evidence.token_weighted,
evidence.provenance_assertions_match,
report.recorded_graphs(),
report.parameters_unchanged(),
report.gradients_unchanged(),
evidence.selection_test_partition_rejected,
))
} Отделите локальную изоляцию от результата фиксированного примера
Проследите путь от обучения и выбора по валидации к одному локальному механизму доступа к тестовой выборке. Оценщик сохраняет 24 упорядоченные пары «вход — цель», а явные пометки указывают, что намеренно выбранный порядок потерь, при котором потери декодера ниже потерь биграммной модели, служит регрессионной проверкой фиксированного примера, а не независимой оценкой способности модели обобщать или доказательством общего превосходства архитектуры.
- Знак эквивалентности: одна и та же последовательность проверенных пар «вход — цель»
- Двойная рамка: меньшие потери на фиксированном примере
- Знак ×: при выборе доступ к тестовой выборке был отклонён
Разделите роли выборок
Нумерованная последовательность закрепляет за каждым этапом одну роль: обучение подгоняет параметры, валидация выбирает состояние, а оценщик может прочитать ID тестовых токенов только после фиксации всех решений.
-
Обучение подгоняет параметры
Может обновлять параметры
-
Валидация выбирает контрольную точку
Может выбирать среди запланированных состояний
-
Зафиксируйте все решения
После этого решения не меняются
-
Откройте один локальный доступ к тестовой выборке
Проверяет и сохраняет пары для итогового отчёта, но не для выбора модели
-
Сохраните неизменяемый отчёт
Проверено реализацией или записано
Сравните модели на одной и той же проверенной последовательности пар «вход — цель»
Декодер отдельно оценивает исходную эпоху без записи графа вычислений, а биграммная модель использует те же 24 проверенные пары «вход — цель» в том же порядке. Двойная рамка отмечает только меньшее среднее значение на фиксированном примере, сохранённом для регрессионной проверки.
| Модель | Заявленная роль при обучении | Заявленная роль при выборе | Одинаковые целевые позиции | Суммарная NLL | Средние потери на тесте |
|---|---|---|---|---|---|
| Выбранный декодер Двойная рамка: меньшие потери на фиксированном примере | Обучение | Валидация | |||
| Зафиксированная биграммная модель | Обучение | Нет |
Отделите заявленные сведения от проверяемых фактов
Строки корпуса, разбиения и токенизатора задаёт вызывающий код; их совпадение показывает только согласованность метаданных. Длину контекста, словарь, тестовые цели, совпадение состояния с моделью и сохранность состояния при оценке без графа реализация проверяет отдельно. Требуемую историю объектов обеспечивает код сборки примера.
Заданные вызывающим кодом идентификаторы совпадают
corpus=ch33-34-synthetic-v1 split=fixed-role-split-v1 tokenizer=literal-u32-v1 provenance_assertions_match=true Выбор уже был завершён
selection_test_partition
selection_test_partition_rejected=true Декодер оценивается отдельно, без записи графа вычислений
Проверено реализацией или записано
graph_nodes=0 parameters_unchanged=true gradients_unchanged=true Одно проверенное представление тестовой эпохи и один отчёт
fnv1a64:dac4bb4d76beeb59 gate_openings_before=0 gate_openings_after=1 report_version=1 Пять нумерованных карточек показывают, почему внутри этого запуска тестовая
выборка не может стать доступной до фиксации решений. В таблице для обеих строк
повторяется , а строка с меньшим значением функции потерь на
фиксированном примере выделена текстовой пометкой и двойной рамкой. Четыре
карточки проверок содержат отклонённую
попытку обратиться к тестовой выборке во время выбора, одно открытие доступа,
ноль записанных узлов графа, неизменные биты состояния, одно проверенное
внутреннее представление, provenance_assertions_match=true и отпечаток
упорядоченных пар «вход — цель» fnv1a64:dac4bb4d76beeb59.
Определите допустимые решения до запуска
- Два документа дают и целевых позиций. Предскажите правильный знаменатель и объясните, почему среднее двух средних по документам неверно.
- Разработчик уменьшает скорость обучения после того, как увидел значение функции потерь на тестовой выборке. Это допустимое действие при итоговой оценке или утечка данных?
- Другая контрольная точка даёт меньшее значение функции потерь на тестовой выборке, чем . Можно ли заменить в отчёте?
- Декодер оценивает перекрывающиеся окна длины два, а биграммная модель — каждый исходный переход ровно один раз. Сопоставимы ли полученные средние значения?
- Сначала измените отображение токенизатора, но повторно используйте прежнюю строку отпечатка и сохраните размеры словаря и контекста. Затем измените только строку отпечатка. Какое изменение API обнаружит до открытия доступа?
- Поместите ID токена в эту тестовую эпоху для словаря размера . Что произойдёт, когда проверка при открытии доступа дойдёт до этого ID?
- Для значений функции потерь и сформулируйте обоснованный вывод о фиксированном примере и объясните, почему из него не следуют ни независимая оценка способности модели обобщать, ни общее превосходство архитектуры.
- Два процесса создают по одному локальному оценщику; каждый получает собственную копию тестовых данных. Какого внешнего механизма не хватает?
Заблуждение: gate_openings_after=1 означает, что во всей истории репозитория
этот результат использовали только один раз. Счётчик относится к одному
экземпляру оценщика в пределах одного запуска. Он не отменяет ни намеренный
выбор учебного примера, ни постоянную регрессионную проверку известного порядка
результатов.
Проверьте ход рассуждений
- ; одинаковый вес получает каждая целевая позиция, а не каждый документ.
- Так результат на тестовой выборке начинает влиять на оптимизацию. Это решение следовало принять по валидационной выборке.
- Нет. Значение уже выбрано по валидации. После выбора другой контрольной точки понадобится новая, ещё не использованная тестовая выборка.
- Нет. Обе модели должны оценивать одни и те же упорядоченные позиции, включая повторы.
- Повторное использование той же строки скрывает первое изменение от проверки заявленных сведений: API не исследует токенизатор. Изменение самой строки создаёт несовпадение заявленных сведений, поэтому доступ не откроется и счётчик останется равным .
- Проверка вернёт ошибку выхода ID за границы словаря.
FinalEvaluatorуже израсходовал своё одноразовое разрешение, поэтому повторный вызов вернётAlreadyEvaluated. - На намеренно выбранном синтетическом примере с обратным циклом значение функции потерь у декодера ниже. Этот порядок полезен как условие регрессии, но не является независимой оценкой способности модели обобщать на ранее не использованных данных и не доказывает общего превосходства архитектуры декодера над биграммной моделью.
- Не хватает контроля доступа к набору данных и общего журнала аудита.
Передайте дальше выбранную модель и соответствующий снимок оптимизатора
К этому этапу у декодера есть состояние модели, выбранное по валидации, и неизменяемый отчёт о регрессионной проверке фиксированного примера на общих целевых позициях. Локальная оценка не может изменить это состояние, но отчёт не является независимой оценкой способности модели обобщать. В главе 35 будет сериализовано состояние, выданное циклом обучения: снимок выбранной модели, зафиксированное одновременно с ним состояние AdamW и их общий номер шага. В файл также войдут токенизатор и конфигурация декодера, необходимые для интерпретации модели, и отдельное состояние генератора для последующего выбора токенов. Итоговый отчёт главы 34 и сведения о происхождении тестовых данных не сериализуются, а генератор для выбора токенов не относится к происхождению данных оценки.
Сериализованное состояние должно позволять точно воспроизвести эту модель и её логиты. При этом нельзя повторно открывать доступ к тестовым данным или превращать итоговый отчёт в новый источник данных для выбора модели.