02 · Версия материала 10
Документы корпуса и фиксированное разбиение на выборки
Распределите целые исходные документы по обучающей, валидационной и тестовой выборкам, чтобы обучение, выбор вариантов и итоговый отчёт опирались на предназначенные для них тексты.
Назначьте роли исходным документам до обучения
Токенизатор может учиться объединять части текста, а модель — подбирать веса, которые помогают предсказывать текст. Если оценивать систему только на наблюдениях, использованных для её обучения, результат может зависеть именно от этих наблюдений. Сам по себе он не устанавливает, как система работает с новым текстом. Поэтому ещё до обучения нужно решить, какие исходные тексты будут использованы для обучения, а какие — для других задач.
Корпус — это собрание исходных документов. Исходный документ — целый текст,
начало и конец которого мы сохраняем как границы документа. В нашем корпусе
одна законченная сценка на одном языке составляет один документ. Единственный
файл rust/data/tiny-bilingual-corpus.json содержит двенадцать записей JSON,
поэтому единицей разбиения служит не файл целиком. У каждой записи есть язык,
группа происхождения и текст. ID документа — постоянная метка, по которой
запись указывают при разбиении; сама метка не доказывает независимость её текста
от других текстов.
Выборка — подмножество документов корпуса, которому назначена одна роль: обучающая, валидационная или тестовая. Группа происхождения объединяет документы, для которых известна связь исходного содержания. Здесь английская и русская версии одной сценки входят в общую группу. Их ID и тексты различаются, но перевод в обучающей выборке всё равно может раскрыть содержание парного документа, оставленного для оценки. Поэтому целые документы и известные группы связанных документов нужно оставлять в одной выборке.
В главе 1 использовалось фиксированное соответствие между единицами текста и ID. Теперь мы готовим границу данных для случая, когда это соответствие предстоит получить из текста при обучении. Три роли разделяют обучение, выбор вариантов и итоговый отчёт:
| Роль | Для чего используются документы |
|---|---|
| Обучающая | По ним обучают систему: в главе 3 подсчитывают частоты пар и получают правила слияния при кодировании пар байтов (BPE), в следующих главах обучают веса модели. |
| Валидационная | Для сравнения вариантов, обученных на обучающей выборке, или выбора контрольной точки — состояния модели, сохранённого на определённом шаге обучения. |
| Тестовая | Для отчёта о качестве после завершения обучения и выбора. Если по тестовому результату принять ещё одно решение, он станет частью выбора. |
Валидационная и тестовая выборки — отложенные данные, поскольку они не участвуют в обучении. По ним всё же можно вычислять показатели качества, например величину ошибки. Вычислить такой показатель — не то же самое, что добавить их частоты пар в обучение токенизатора или использовать их для вычисления сигналов обновления весов, называемых градиентами. Валидация влияет на выбранную систему, поэтому после такого выбора её нельзя считать незатронутой. В этой главе мы назначаем роли, но не обучаем токенизатор или модель и не вычисляем показатель качества.
Правило распределения удобно разобрать на отдельном примере из шести документов.
Пусть полный корпус этого примера имеет ID от doc-01 до doc-06. Следующее
распределение не проходит проверку:
train = [doc-01, doc-02]
validation = [doc-03, doc-04]
test = [doc-04, doc-05]
В списках шесть позиций, но лишь пять различных ID. doc-04 занимает позицию
и в валидационной, и в тестовой выборке, а doc-06 нет ни в одной. Поэтому
объединение — все ID, встречающиеся хотя бы в одной выборке, — не содержит
doc-06. Попарные пересечения, то есть ID, общие для двух выборок, выявляют
вторую ошибку: у валидационной и тестовой выборок общий doc-04, тогда как
обучающая не имеет общих ID ни с одной из них. Нужного числа позиций оказалось
недостаточно.
Оставим обучающий и валидационный списки без изменений и исправим тестовый:
train = [doc-01, doc-02]
validation = [doc-03, doc-04]
test = [doc-05, doc-06]
Теперь каждый из шести ID корпуса встречается один раз. Объединение содержит
все ID от doc-01 до doc-06, а все три попарных пересечения пусты. Исправление
обеспечило полное распределение этих шести документов без пересечения выборок.
Оно не доказывает, что тексты не связаны или представляют все виды будущих
текстов. Корпус из двенадцати документов, с которым работает программа далее
в главе, — другой пример.
Запишите полноту и непересечение через множества
Рассмотрим корпус и состав каждой выборки как множества целых документов. Исправленный пример удовлетворяет условию:
Равенство означает, что три множества покрывают весь корпус. Знак объединения
с точкой дополнительно требует, чтобы ни один документ не принадлежал разным
выборкам. Условие с пересечением явно выражает это требование для любых двух
различных обозначений ролей. В ошибочном примере валидационная и тестовая
выборки содержали общий doc-04, поэтому их пересечение не было пустым.
Пропущенный doc-06 нарушал полноту покрытия.
Манифест хранит упорядоченные списки ID, а формула описывает множества. Множество не сохраняет сведения о том, сколько раз ID встречается в списке, и не сохраняет порядок. Если один обучающий ID записать дважды, состав множества обучающих ID не изменится, но это не оправдывает повтор в списке. Обратный порядок списка тоже не меняет состав множества, однако меняет порядок обхода. Программа проверяет эти свойства списков отдельно. Формула также не требует непустых выборок, целостности групп происхождения, неизменных входных байтов или репрезентативности данных.
Значения символов в условии разбиения
| Символ | Значение в этой главе |
|---|---|
| Корпус как множество целых исходных документов. | |
| Обучающие документы, по которым получают правила токенизатора и обучают веса модели. | |
| Валидационные документы для сравнения вариантов, обученных на обучающей выборке, или выбора сохранённого состояния модели. | |
| Тестовые документы для отчёта после завершения обучения и выбора. | |
| Обозначения обучающей, валидационной и тестовой ролей. | |
| Дизъюнктное объединение: множества покрывают корпус, и ни один документ не принадлежит разным выборкам. | |
| Два обозначения ролей, каждое из которых выбирают из , и . | |
| Пересечение множеств: документы, которые входят в оба сравниваемых множества. | |
| Пустое множество, не содержащее ни одного общего документа. | |
| Условие о пересечении действует, когда обозначения двух ролей различны. |
Эти символы описывают состав выборок и их назначение, а не рекомендуемые доли разбиения. Количество текста для каждой роли зависит от объёма корпуса и цели оценивания. Встречающиеся далее восемь/два/два — числа документов, выбранные для небольшого учебного примера. Это не числа байтов или токенов и не универсальная пропорция.
Сохраняйте исходные границы за каждым ID
Разделение обучения и оценки появилось задолго до современных LLM. В статье Стоуна 1974 года изучаются выбор и оценка с помощью перекрёстной проверки. В языковом моделировании важна и единица текста, которая несёт контекст. Мерити с соавторами представили WikiText, используя тексты статей для работы с более дальними зависимостями. Браун с соавторами исследовали пересечения обучающих данных с валидационными и тестовыми наборами в более крупном масштабе. Эти работы обосновывают внимание к оценке и исходному контексту, но не предписывают фиксированное двуязычное разбиение нашего курса.
Существующий пример на Rust показывает, как обозначение отложенной выборки
может скрывать общий исходный контекст. Из единственного предложения
north star glows softly вызов
overlapping_word_windows("north star glows softly", 3) создаёт два окна:
["north", "star", "glows"] и ["star", "glows", "softly"].
Функция shared_words возвращает ["star", "glows"] в порядке слов первого окна:
rust/demos/ch02-corpus-partitions/src/lib.rs#overlapping-excerpts pub fn overlapping_word_windows(text: &str, width: usize) -> Vec<Vec<&str>> {
let words = text.split_whitespace().collect::<Vec<_>>();
if width == 0 || width > words.len() {
return Vec::new();
}
words.windows(width).map(<[&str]>::to_vec).collect()
}
/// Lists tokens shared by two separately named excerpts in left-hand order.
pub fn shared_words<'a>(left: &'a [&'a str], right: &[&str]) -> Vec<&'a str> {
left.iter()
.copied()
.filter(|word| right.contains(word))
.collect()
} Если дать окнам разные имена и поместить одно в обучение, а другое — в валидацию, ID будут различаться, но исходный контекст окажется по обе стороны границы. Причина в том, что оба окна получены из одного предложения. Само по себе общее слово в двух не связанных между собой документах не доказывает такую утечку. Здесь утечка данных означает, что информация, доступная при обучении, пересекает границу, которая должна отделять от него оценку качества.
Сначала распределяйте целые исходные документы и известные группы происхождения. Собирайте статистику для обучения токенизатора только по обучающим документам, зафиксируйте полученные правила и применяйте их отдельно к текстам каждой выборки. Любые последующие токеновые окна должны оставаться внутри исходного документа и сохранять его роль. Переименование фрагментов не заменяет этот порядок. Так предотвращается показанный путь утечки; это не утверждение, что утечки были во всех исторических экспериментах или что любое пересечение улучшает любой показатель.
Изучите существующие проверки документов и разбиения
Существующий загрузчик сохраняет те исходные единицы, о которых шла речь выше.
Его вход — текст JSON в виде &str, поэтому на этой границе уже гарантирован
корректный UTF-8. serde_json::from_str обрабатывает синтаксис JSON и поля
заданных типов. Отдельно Corpus::from_json проверяет метаданные документов
и непустой текст, отклоняет повторяющиеся ID и точные совпадения декодированных
текстов, а также сохраняет порядок исходного массива:
rust/crates/llm-from-scratch/src/corpus.rs#document-loader pub fn from_json(source: &str) -> Result<Self, CorpusError> {
let decoded: Vec<DocumentJson> = serde_json::from_str(source)
.map_err(|error| CorpusError::new(format!("invalid corpus JSON: {error}")))?;
if decoded.is_empty() {
return Err(CorpusError::new("corpus contains no documents"));
}
let mut documents = Vec::with_capacity(decoded.len());
for (index, document) in decoded.into_iter().enumerate() {
let position = index + 1;
for (value, label) in [
(&document.id, "document ID"),
(&document.language, "language"),
(&document.provenance_group, "provenance group"),
] {
if !is_kebab_identifier(value) {
return Err(CorpusError::new(format!(
"corpus document {position} {label} must be lowercase ASCII kebab case"
)));
}
}
if document.text.trim().is_empty() {
return Err(CorpusError::new(format!(
"corpus document {position} text is empty"
)));
}
if documents
.iter()
.any(|existing: &Document| existing.id == document.id)
{
return Err(CorpusError::new(format!(
"duplicate document ID {}",
document.id
)));
}
if documents
.iter()
.any(|existing: &Document| existing.text == document.text)
{
return Err(CorpusError::new(
"duplicate document text would leak identical content",
));
}
documents.push(Document {
id: document.id,
language: document.language,
provenance_group: document.provenance_group,
text: document.text,
});
}
Ok(Self {
documents,
checksum: format!("fnv1a64:{:016x}", fnv1a64(source.as_bytes())),
})
} Манифест разбиения записывает фиксированное распределение по ролям.
Файл rust/data/splits.json в репозитории содержит версию схемы 1, стратегию
fixed-paired-document-holdout-v1 и контрольную сумму fnv1a64:723b071980ae8a22.
SplitManifest::from_json декодирует поля; успешное декодирование JSON само
по себе ничего не говорит о корректности разбиения этого корпуса.
Отдельный метод partition проверяет поддерживаемые схему и стратегию и
сравнивает записанную контрольную сумму с суммой, заново вычисленной по байтам
UTF-8 текста JSON корпуса. Он также требует, чтобы все ID были известны,
ни один ID не занимал больше одной позиции внутри списка или между списками,
весь корпус был покрыт, все выборки были непустыми, внутри каждой сохранялся
порядок корпуса, а объявленные группы происхождения оставались целыми.
Только после успешного завершения проверок метод возвращает представления
документов, содержащие ссылки на исходные данные:
rust/crates/llm-from-scratch/src/corpus.rs#partition-invariants pub fn partition<'a>(&self, corpus: &'a Corpus) -> Result<CorpusPartitions<'a>, CorpusError> {
if self.schema_version != SPLIT_SCHEMA_VERSION {
return Err(CorpusError::new(format!(
"unsupported split schema version {}",
self.schema_version
)));
}
if self.strategy != SPLIT_STRATEGY {
return Err(CorpusError::new(format!(
"unsupported split strategy {}",
self.strategy
)));
}
if self.corpus_checksum != corpus.checksum {
return Err(CorpusError::new(format!(
"corpus checksum mismatch: manifest={}, actual={}",
self.corpus_checksum, corpus.checksum
)));
}
for partition in [Partition::Train, Partition::Validation, Partition::Test] {
if self.ids(partition).is_empty() {
return Err(CorpusError::new(format!(
"{} partition is empty",
partition.label()
)));
}
validate_source_order(corpus, partition, self.ids(partition))?;
}
let mut seen = Vec::new();
for partition in [Partition::Train, Partition::Validation, Partition::Test] {
for id in self.ids(partition) {
if corpus.document(id).is_none() {
return Err(CorpusError::new(format!(
"{} partition contains unknown document {id}",
partition.label()
)));
}
if seen.contains(&id) {
return Err(CorpusError::new(format!(
"document {id} appears in more than one manifest position"
)));
}
seen.push(id);
}
}
if seen.len() != corpus.documents.len() {
let missing = corpus
.documents
.iter()
.find(|document| !seen.iter().any(|id| id.as_str() == document.id))
.map_or("<unknown>", Document::id);
return Err(CorpusError::new(format!(
"manifest does not cover corpus document {missing}"
)));
}
for document in &corpus.documents {
let assigned = self.assignment(document.id()).ok_or_else(|| {
CorpusError::new(format!(
"manifest does not cover corpus document {}",
document.id
))
})?;
if let Some(related) = corpus.documents.iter().find(|candidate| {
candidate.provenance_group == document.provenance_group
&& self.assignment(candidate.id()) != Some(assigned)
}) {
return Err(CorpusError::new(format!(
"provenance group {} is split between {} and {}",
document.provenance_group, document.id, related.id
)));
}
}
let mut partitions = CorpusPartitions {
train: Vec::new(),
validation: Vec::new(),
test: Vec::new(),
};
for document in &corpus.documents {
match self.assignment(document.id()).ok_or_else(|| {
CorpusError::new(format!(
"manifest does not cover corpus document {}",
document.id
))
})? {
Partition::Train => partitions.train.push(document),
Partition::Validation => partitions.validation.push(document),
Partition::Test => partitions.test.push(document),
}
}
Ok(partitions)
} Фиксированный манифест делает распределение воспроизводимым, а сохранение порядка корпуса — воспроизводимыми последующий обход и печатаемые результаты. Ни то ни другое не устанавливает статистическую независимость. Проверка точных совпадений не обнаруживает все почти одинаковые тексты или скопированные отрывки, а известные метки групп не выявляют все нераспознанные связи. Детерминированное разбиение всё ещё может охватывать узкий круг тем или плохо представлять тексты, которые встретятся позже.
64-битное значение FNV-1a служит ограниченной проверкой изменений. Несовпадение указывает на другую контрольную сумму. Совпадение означает лишь, что повторное вычисление дало записанное значение. Возможны коллизии, поэтому совпадение не доказывает тождество байтов, авторство, условия лицензирования или криптографическую подлинность. Происхождение учебного текста отдельно записано в README корпуса.
Программа выводит принятое распределение, пересечение окон и число обучающих документов для следующей главы:
rust/demos/ch02-corpus-partitions/src/main.rs#chapter-output fn main() -> Result<(), Box<dyn std::error::Error>> {
let corpus = Corpus::from_json(CORPUS_JSON)?;
let manifest = SplitManifest::from_json(SPLIT_MANIFEST)?;
let partitions = manifest.partition(&corpus)?;
println!("corpus checksum: {}", corpus.checksum());
println!("documents: {}", corpus.documents().len());
print_partition("train", &partitions, Partition::Train);
print_partition("validation", &partitions, Partition::Validation);
print_partition("test", &partitions, Partition::Test);
println!("complete: yes");
println!("disjoint: yes");
println!("provenance groups intact: yes");
let excerpts = overlapping_word_windows("north star glows softly", 3);
println!("historical excerpt A: {:?}", excerpts[0]);
println!("historical excerpt B: {:?}", excerpts[1]);
println!(
"shared context: {:?}",
shared_words(&excerpts[0], &excerpts[1])
);
println!("safe split unit: whole source document");
println!(
"chapter 3 tokenizer input: train only ({} documents)",
partitions.training_documents().len()
);
println!(
"held out: validation={} test={}",
partitions.documents(Partition::Validation).len(),
partitions.documents(Partition::Test).len()
);
Ok(())
} Эти представления явно выражают, какие данные предназначены для обучения. Они не запрещают всем другим вызывающим программам читать исходный файл и не доказывают независимость текстов у документов с разными именами.
Проследите все двенадцать документов корпуса
Корпус содержит шесть сценок на английском и русском языках. Каждая сценка даёт
два документа в одной группе происхождения pair-*. В обучающую выборку в
порядке корпуса входят пары river-dawn, clock-shop, rain-library и bee-garden:
восемь документов в четырёх группах. Валидационная выборка содержит пару
night-station — два документа одной группы. Тестовая содержит пару winter-window —
также два документа одной группы.
На схеме приведены точные ID, языки и группы всех документов. Три списка — равноправные множества, а не этапы перемещения документа. Каждая карточка представляет целый документ; карточки внутри выборки идут в порядке корпуса:
Двенадцать целых документов в трёх выборках
Равноправные выборки: обучение (8), выбор по валидации (2), тестовый отчёт после выбора (2). Каждый документ — один раз; в выборках — порядок корпуса. Переводы одной группы происхождения вместе.
-
TRОбучающая
Обучение токенизатора и весов модели
Документов 8
- Целый документ
- ID
en-river-dawn- Язык
en- Группа по источнику
pair-river-dawn
- Целый документ
- ID
ru-river-dawn- Язык
ru- Группа по источнику
pair-river-dawn
- Целый документ
- ID
en-clock-shop- Язык
en- Группа по источнику
pair-clock-shop
- Целый документ
- ID
ru-clock-shop- Язык
ru- Группа по источнику
pair-clock-shop
- Целый документ
- ID
en-rain-library- Язык
en- Группа по источнику
pair-rain-library
- Целый документ
- ID
ru-rain-library- Язык
ru- Группа по источнику
pair-rain-library
- Целый документ
- ID
en-bee-garden- Язык
en- Группа по источнику
pair-bee-garden
- Целый документ
- ID
ru-bee-garden- Язык
ru- Группа по источнику
pair-bee-garden
- Целый документ
-
VAВалидация
Сравнение вариантов, обученных на обучающей выборке
Документов 2
- Целый документ
- ID
en-night-station- Язык
en- Группа по источнику
pair-night-station
- Целый документ
- ID
ru-night-station- Язык
ru- Группа по источнику
pair-night-station
- Целый документ
-
TEТестовая
Отчёт после завершения выбора
Документов 2
- Целый документ
- ID
en-winter-window- Язык
en- Группа по источнику
pair-winter-window
- Целый документ
- ID
ru-winter-window- Язык
ru- Группа по источнику
pair-winter-window
- Целый документ
Распределено документов
Повторов ID 0
- Полнота: распределён каждый документ корпуса
- Непересечение: ни один документ не отнесён к разным выборкам
- Каждая объявленная группа происхождения остаётся в одной выборке
Например, у en-night-station язык en, у ru-night-station — ru. Оба имеют
группу pair-night-station и входят в валидационную выборку. Различие языков
не устраняет связь между текстами одной сценки. То же правило пар действует для
pair-river-dawn, pair-clock-shop, pair-rain-library, pair-bee-garden и
pair-winter-window. Каждый ID корпуса относится ровно к одной роли: в обучении
восемь документов, в каждой из отложенных выборок — по два.
Это небольшой общедоступный учебный корпус с оригинальными текстами, который можно изучить целиком. Он позволяет воспроизводить примеры, но не доказывает практическую полезность языковой модели. Повторный запуск с известным тестовым результатом проверяет отсутствие регрессий, а не даёт каждый раз новую независимую оценку способности модели обобщать.
Необязательная проверка примеров и запуск
Результаты примеров уже разобраны выше. Следующие задания необязательны и используют те же фиксированные входные данные.
- Сравните ошибочные и исправленные списки из шести ID. Для каждого случая укажите различные ID и все три попарных пересечения.
- Объясните, почему обучение BPE, выбор сохранённого состояния модели и итоговый отчёт используют разные роли. Уточните, почему валидационные данные нельзя считать незатронутыми.
- Объясните связь исходного содержания у двух окон из слов и у каждой двуязычной пары. Отличите её от обычного совпадения слов в разных текстах.
- Опишите, какая информация теряется, если рассматривать упорядоченный список ID манифеста только как множество.
- По двенадцати карточкам найдите обучающие группы и две группы отложенных данных. Объясните, что именно считают указанные количества.
- Укажите, о чём сообщает совпадение контрольной суммы FNV, и назовите ещё одно ограничение корректного разбиения документов.
- При желании воспроизведите существующие проверки и пример командами ниже. Свяжите успешные проверки и выведенное распределение с границами данных, описанными в этой главе.
Для задания 7 нужны Cargo, Rust 1.93 или новее, а также корпус и манифест из репозитория. Выполните все три команды из корня репозитория:
cargo test --locked -p llm-from-scratch --lib corpus::tests::
cargo test --locked -p ch02-corpus-partitions
cargo run --quiet --locked -p ch02-corpus-partitions | diff -u rust/demos/ch02-corpus-partitions/expected.txt -Первая команда запускает девять библиотечных тестов загрузки документов и
отклонения неверных разбиений. Вторая запускает два библиотечных теста примера
с перекрывающимися окнами. При успешном сравнении diff ничего не выводит
и завершается с кодом ноль. Вывод примера сравнивается с
rust/demos/ch02-corpus-partitions/expected.txt; менять корпус или манифест
не требуется.
Проверенные объяснения и результаты запуска
- В ошибочных списках шесть позиций, но пять различных ID:
doc-04повторяется, аdoc-06отсутствует. Пересечения обучения с валидацией и обучения с тестом пусты; пересечение валидации с тестом содержитdoc-04. После исправления все шесть ID указаны по одному разу, и все три пересечения пусты. - Обучающая выборка даёт частоты для обучения BPE, а позднее — данные для обновления весов модели. По валидационной сравнивают обученные варианты или выбирают сохранённое состояние, поэтому она влияет на выбор. Тестовая даёт итоговый отчёт после фиксации решений; ещё один выбор по её результату превратил бы её в данные для выбора. По отложенным данным разрешено вычислять показатели качества без обучающих частот и градиентов.
- Окна перекрываются внутри одного исходного предложения. Двуязычные документы передают одну сценку на разных языках. Это известные связи исходного содержания, которые не исчезают от нового ID. Общее слово в не связанных между собой документах само по себе не устанавливает ни одной из этих связей.
- Множество не сохраняет повторы ID внутри списка и порядок элементов. Проверяющий метод контролирует оба свойства отдельно, поэтому одного неизменного состава множества для принятия манифеста недостаточно.
- В обучение входят
pair-river-dawn,pair-clock-shop,pair-rain-libraryиpair-bee-garden, в валидацию —pair-night-station, в тест —pair-winter-window. В каждой группе два документа. Восемь, два и два — количества документов, а не байтов или токенов и не универсальная пропорция. - Совпадение сообщает о записанном 64-битном значении, но не гарантирует тождество байтов или подлинность. Другие ограничения — нераспознанные почти одинаковые тексты, смещение выборки и будущее изменение вида оцениваемых текстов.
- Команды тестируют существующие проверки документов и разбиения и пример
с окнами. Если
diffничего не выводит и завершается с нулевым кодом, стандартный вывод этого запуска совпал с файлом из репозитория. Такое совпадение подтверждает воспроизводимость примера, но не измеряет качество модели и не устанавливает независимость исходного содержания.
Передайте BPE только обучающие документы
В главе 3 обучение BPE использует partitions.training_documents() — восемь целых
обучающих документов. Для каждого начинается отдельная последовательность
байтовых токенов, и смежные пары подсчитываются внутри неё. Последний байт одного
документа не становится соседом первого байта следующего. Валидационные и тестовые
данные не добавляют частот пар и правил слияния, получаемых при обучении.
После обучения полученные правила можно зафиксировать и применять к отложенному тексту, не обучаясь на нём. Последующие токеновые окна также должны оставаться внутри исходных документов и сохранять их роли. Так сохраняются обе границы: какой текст участвует в обучении и какой текст относится к одному источнику.
При обучении модели обязанности остаются теми же: обучающая выборка служит для подбора весов, валидационная — для выбора вариантов, тестовая — для отчёта после выбора. В главе 34 этот порядок показан локальным механизмом оценки внутри одного выполнения. Такой механизм не обеспечивает однократный доступ к тестовым данным за пределами одного выполнения: другие программы и будущие запуски могут прочитать их снова. Переданные вызывающей стороной метки происхождения сами по себе не дают независимого подтверждения истории данных. Более позднее сравнение специально подобрано, а его известный результат служит проверке регрессий, не оценке обобщения на нетронутых данных. Введённая здесь граница помогает точно формулировать последующие утверждения, не делая из небольшого фиксированного корпуса основание для более широких выводов.