05 · Версия материала 8
Как составлять авторегрессионные пары «вход — цель»
Научитесь строить по каждому закодированному документу пары для предсказания следующего токена, не пересекая границы документов и частей корпуса.
Найдите три пары до запуска программы
Авторегрессионная языковая модель учится предсказывать очередной токен по предыдущим токенам. Для обучения ей нужны пары «вход — цель»: вход задаёт доступный контекст, а цель показывает правильное продолжение в каждой позиции. При этом сама пара не запрещает модели заглянуть вперёд — такое ограничение должно быть заложено в вычислениях декодера.
В главе 4 мы получили для каждого документа отдельную последовательность, обрамлённую BOS и EOS. Возьмём самый простой пример:
позиция 0 1 2 3 4 5
токен 0 41 42 43 44 1
^ ^
BOS EOS
Пусть длина контекста , а шаг . На одну пару требуется четыре токена документа: три образуют вход, а ещё один нужен, чтобы сдвинуть цель. Попробуйте сначала самостоятельно выписать входы и цели для позиций 0, 1 и 2.
В позиции 0 берём токены с индексами от 0 до 3. Первые три становятся входом, последние три — целью:
позиция 0 вход [0, 41, 42]
цель [41, 42, 43]
Затем передвигаем начало на один токен и получаем полный набор:
| Начало окна | Вход | Цель |
|---|---|---|
0 | [0,41,42] | [41,42,43] |
1 | [41,42,43] | [42,43,44] |
2 | [42,43,44] | [43,44,1] |
Каждая строка таблицы — один авторегрессионный пример. В Rust такая пара
представлена структурой CausalWindow.
В последней цели есть EOS, и это правильно: после токена 44 модель должна
предсказать конец документа. Если сдвинуть начало окна в позицию , останется
только [43,44,1]. Для новой пары нужны четыре токена, поэтому четвёртого окна нет.
Дополнять остаток фиктивными токенами мы не будем.
Отсутствие новой пары не означает, что токены 43, 44 и EOS потеряны. Они уже встречаются в полных парах выше; в остатке просто недостаточно токенов, чтобы построить ещё одну пару.
Опишите сдвиг на один токен с помощью срезов
Для одного закодированного документа и допустимой позиции :
Диапазоны в Rust не включают правую границу, поэтому в обоих срезах ровно ID токенов. Цель начинается на одну позицию позже входа: — токен документа, который непосредственно следует за . Окно можно построить тогда и только тогда, когда , то есть начиная с позиции в документе есть все токенов.
Шаг задаёт позиции , где . Если в нашем примере выбрать
, будут рассмотрены позиции 0, 2 и 4. Из первых двух получатся пары. Позиция
1 будет пропущена именно из-за шага, хотя начиная с неё токенов достаточно для
полной пары. В позиции 4 останется [44,1] — для новой пары этого мало.
Параметр одновременно задаёт длину входа, длину цели и наибольшую длину контекста, доступного внутри одной пары. Предсказывая , модель вправе опираться только на ; более поздние элементы входа ей видеть нельзя. Параметр определяет расстояние между соседними окнами. При используется каждое возможное начало, поэтому соседние переходы повторяются в перекрывающихся парах. Чем больше шаг, тем меньше примеров: некоторые полные окна пропускаются, а при между ними могут появиться разрывы.
Обход начинается заново для каждого документа — иначе модель получила бы несуществующий переход между двумя независимыми текстами. По той же причине обучающая, валидационная и тестовая части корпуса хранятся раздельно, а код обучения явно выбирает только документы обучающей части.
Сдвиг определяет правильные цели, но сам по себе не скрывает будущие позиции входа. В рекуррентной модели это обеспечивает последовательная обработка слева направо. Декодеру с самовниманием понадобится явная каузальная маска.
Разберите обозначения
| Символ | Что он означает |
|---|---|
ровно один документ [BOS, токены содержимого, EOS] | |
| положительная длина контекста; вход и цель содержат по ID | |
| положительный шаг между соседними позициями начала окна | |
| позиция ; пара строится, только если начиная с неё в документе есть токенов | |
| входной срез, который начинается в | |
| целевой срез, который начинается в |
Это правило применяется отдельно к каждому документу. Нельзя склеить все
документы одной части корпуса в общую последовательность . Короткий документ
может не дать ни одной пары: тогда все его токены образуют остаток, которого
недостаточно для новой пары.
Даже документ без содержимого кодируется как [BOS, EOS], поэтому при из
него получается корректная пара [BOS] -> [EOS].
Откуда языковая модель берёт целевые токены
В задаче классификации ответ существует отдельно от входных данных. Например, двум последовательностям в демонстрации на Rust человек заранее назначил метки тональности. Эти метки не являются продолжением последовательностей:
rust/demos/ch05-autoregressive-examples/src/lib.rs#hand-labeled-contrast /// One task-specific row whose label had to be supplied separately.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HandLabeledRow<'a> {
/// Fixed input chosen by the example author.
pub input: &'a [u32],
/// Separate sentiment class attached to that input.
pub label: SentimentLabel,
}
/// A human-supplied class for the synthetic sentiment contrast.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SentimentLabel {
/// The example expresses negative sentiment.
Negative,
/// The example expresses positive sentiment.
Positive,
}
impl SentimentLabel {
/// Returns the label as display text.
pub const fn as_str(self) -> &'static str {
match self {
Self::Negative => "negative",
Self::Positive => "positive",
}
}
}
/// Returns two tiny task-specific rows with separately supplied labels.
pub const fn hand_labeled_rows() -> [HandLabeledRow<'static>; 2] {
[
HandLabeledRow {
input: &[41, 42, 43],
label: SentimentLabel::Negative,
},
HandLabeledRow {
input: &[51, 52, 53],
label: SentimentLabel::Positive,
},
]
} При обучении языковой модели цель уже задана порядком токенов в тексте: правильный ответ — токен, который следует за данным контекстом в том же документе. Бенжио и соавторы (2003) представляют вероятность предложения как произведение условных вероятностей и в каждом примере предсказывают одно следующее слово по фиксированному числу предыдущих слов. В нашей главе тот же принцип применяется сразу к позициям: каждому элементу входа соответствует следующий за ним токен того же документа. Целевые токены не нужно размечать вручную — их задаёт исходный порядок в тексте.
В отчёте GPT-2 (2019) сохраняется та же авторегрессионная факторизация, а модель обучается на текстах без специальной разметки под одну прикладную задачу.
Ни статья Бенжио, ни отчёт GPT-2 не задают конкретных правил подготовки данных для нашего курса. Границы документов, фиксированное разбиение 8/2/2, шаг между окнами и отказ дополнять остаток фиктивными токенами — решения этого курса. Эти правила сохраняют ID документов и их принадлежность к части корпуса, позволяют коду обучения явно выбрать только обучающие данные и обеспечивают воспроизводимый подсчёт примеров.
Стройте пары, обходя документы по одному
CausalWindowConfig::new отклоняет нулевую длину контекста и нулевой шаг. Длина
контекста, равная usize::MAX, тоже недопустима, поскольку при вычислении
возникло бы
переполнение. При корректных параметрах конфигурация подсчитывает полные пары и
находит начало возможного остатка:
rust/crates/llm-from-scratch/src/data.rs#causal-window-policy /// Positive sizes that determine which candidate starts may produce pairs.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CausalWindowConfig {
context_length: usize,
stride: usize,
required_source_tokens: usize,
}
impl CausalWindowConfig {
/// Validates the context length and distance between candidate starts.
pub const fn new(
context_length: usize,
stride: usize,
) -> Result<Self, CausalWindowConfigError> {
if context_length == 0 {
return Err(CausalWindowConfigError::ZeroContextLength);
}
if stride == 0 {
return Err(CausalWindowConfigError::ZeroStride);
}
let Some(required_source_tokens) = context_length.checked_add(1) else {
return Err(CausalWindowConfigError::ContextLengthOverflow);
};
Ok(Self {
context_length,
stride,
required_source_tokens,
})
}
/// Returns the number of input IDs and target IDs in every emitted pair.
pub const fn context_length(self) -> usize {
self.context_length
}
/// Returns the distance between consecutive candidate starts.
pub const fn stride(self) -> usize {
self.stride
}
/// Returns the number of source IDs required to emit one shifted pair.
pub const fn required_source_tokens(self) -> usize {
self.required_source_tokens
}
/// Counts complete pairs without adding potentially huge indices.
pub const fn window_count(self, document_length: usize) -> usize {
if document_length < self.required_source_tokens {
0
} else {
(document_length - self.required_source_tokens) / self.stride + 1
}
}
/// Borrows every complete shifted pair selected inside one token slice.
pub const fn windows<'a>(self, tokens: &'a [u32]) -> CausalWindows<'a> {
CausalWindows {
tokens,
config: self,
next_start: Some(0),
remaining: self.window_count(tokens.len()),
}
}
/// Returns the suffix at the first candidate start that cannot fill a pair.
///
/// `None` means that the next candidate start lies at or beyond the end of
/// the document. A returned suffix may overlap earlier complete pairs.
pub fn incomplete_tail<'a>(self, tokens: &'a [u32]) -> Option<IncompleteTail<'a>> {
let window_count = self.window_count(tokens.len());
let start = if window_count == 0 {
0
} else {
(window_count - 1)
.checked_mul(self.stride)?
.checked_add(self.stride)?
};
(start < tokens.len()).then(|| IncompleteTail {
start,
tokens: &tokens[start..],
})
}
}
/// A rejected configuration for causal-example construction.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CausalWindowConfigError {
/// An empty input and target would not teach a next-token relation.
ZeroContextLength,
/// `T + 1` cannot be represented, so the source requirement is undefined.
ContextLengthOverflow,
/// A zero stride would select the same start forever.
ZeroStride,
}
impl fmt::Display for CausalWindowConfigError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::ZeroContextLength => "context length must be positive",
Self::ContextLengthOverflow => {
"context length is too large to require one additional source token"
}
Self::ZeroStride => "stride must be positive",
})
}
}
impl Error for CausalWindowConfigError {} Итератор CausalWindows обходит подходящие позиции по порядку. Он заимствует из
документа входной срез и срез цели, начинающийся на один токен позже. ID токенов
при этом не копируются и не меняются:
rust/crates/llm-from-scratch/src/data.rs#causal-window-iterator /// One complete input/target pair borrowed from a single document.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CausalWindow<'a> {
start: usize,
input: &'a [u32],
target: &'a [u32],
}
impl<'a> CausalWindow<'a> {
/// Returns the input's zero-based position inside its document.
pub const fn start(self) -> usize {
self.start
}
/// Returns exactly `context_length` source token IDs.
pub const fn input(self) -> &'a [u32] {
self.input
}
/// Returns the `T`-token source slice beginning one position after the input.
pub const fn target(self) -> &'a [u32] {
self.target
}
}
/// The first selected suffix that is too short to emit a complete pair.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IncompleteTail<'a> {
start: usize,
tokens: &'a [u32],
}
impl<'a> IncompleteTail<'a> {
/// Returns the candidate position at which the too-short suffix begins.
pub const fn start(self) -> usize {
self.start
}
/// Returns the remaining source IDs; these may overlap earlier pairs.
pub const fn tokens(self) -> &'a [u32] {
self.tokens
}
}
/// A repeatable, exact-size iterator over one document's complete pairs.
#[derive(Clone, Debug)]
pub struct CausalWindows<'a> {
tokens: &'a [u32],
config: CausalWindowConfig,
next_start: Option<usize>,
remaining: usize,
}
impl<'a> Iterator for CausalWindows<'a> {
type Item = CausalWindow<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
let start = self.next_start?;
let input_end = start.checked_add(self.config.context_length)?;
let target_start = start.checked_add(1)?;
let target_end = input_end.checked_add(1)?;
let input = self.tokens.get(start..input_end)?;
let target = self.tokens.get(target_start..target_end)?;
self.remaining -= 1;
self.next_start = if self.remaining == 0 {
None
} else {
start.checked_add(self.config.stride)
};
Some(CausalWindow {
start,
input,
target,
})
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.remaining, Some(self.remaining))
}
}
impl ExactSizeIterator for CausalWindows<'_> {}
impl FusedIterator for CausalWindows<'_> {} По самим ID невозможно понять, где заканчивается один документ и начинается
другой. Поэтому итератору всегда передаётся срез ровно одного EncodedDocument,
а затем для следующего документа создаётся новый итератор.
При подсчёте окон и определении начала остатка каждое сложение проверяется на
переполнение, поэтому код корректно обрабатывает предельные размеры. Метод
incomplete_tail возвращает остаток с позиции следующего окна, если она ещё
находится внутри документа, но полная пара уже не помещается. Если эта позиция
совпала с концом документа или оказалась за ним, результатом будет None.
Токены возвращённого остатка могли встречаться в предыдущих полных парах.
EncodedCorpusPartitions хранит закодированные последовательности вместе с ID
документов и принадлежностью к обучающей, валидационной или тестовой части.
from_partitions кодирует документы по отдельности с помощью токенизатора,
зафиксированного в главе 4, а documents открывает доступ только к явно
выбранной части корпуса. Здесь используются окончательные ID токенизатора, а не
вспомогательное пространство ID, в котором в главе 3 обучались правила слияния.
API не позволяет представить весь корпус в виде одной склеенной ленты:
rust/crates/llm-from-scratch/src/data.rs#partition-encoding /// One owned token sequence with its stable document and partition identities.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EncodedDocument {
id: String,
partition: Partition,
token_ids: Vec<u32>,
}
impl EncodedDocument {
/// Returns the stable source-document identity.
pub fn id(&self) -> &str {
&self.id
}
/// Returns the frozen partition role inherited from the split manifest.
pub const fn partition(&self) -> Partition {
self.partition
}
/// Returns the separately wrapped `[BOS, content..., EOS]` sequence.
pub fn token_ids(&self) -> &[u32] {
&self.token_ids
}
/// Opens a fresh borrowed pair iterator without consuming this document.
pub fn windows(&self, config: CausalWindowConfig) -> CausalWindows<'_> {
config.windows(&self.token_ids)
}
/// Reports the first candidate suffix that cannot fill a complete pair.
pub fn incomplete_tail(&self, config: CausalWindowConfig) -> Option<IncompleteTail<'_>> {
config.incomplete_tail(&self.token_ids)
}
}
/// Encoded documents kept in three disjoint owned collections.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EncodedCorpusPartitions {
train: Vec<EncodedDocument>,
validation: Vec<EncodedDocument>,
test: Vec<EncodedDocument>,
}
impl EncodedCorpusPartitions {
/// Applies one frozen tokenizer independently to every frozen source document.
pub fn from_partitions(partitions: &CorpusPartitions<'_>, tokenizer: &BpeTokenizer) -> Self {
let encode = |partition| {
partitions
.documents(partition)
.iter()
.map(|document| EncodedDocument {
id: document.id().to_owned(),
partition,
token_ids: tokenizer.encode_utf8_document(document.text()),
})
.collect()
};
Self {
train: encode(Partition::Train),
validation: encode(Partition::Validation),
test: encode(Partition::Test),
}
}
/// Returns only the separately encoded documents for one partition.
pub fn documents(&self, partition: Partition) -> &[EncodedDocument] {
match partition {
Partition::Train => &self.train,
Partition::Validation => &self.validation,
Partition::Test => &self.test,
}
}
} На схеме ниже тот же итератор применяется к четырём коротким документам из трёх частей корпуса. Для каждой ленты отдельно вычисляются полные пары и возможный остаток, поэтому ни вход, ни цель не могут пересечь границу.
Исполняемый пример загружает двуязычный корпус и зафиксированный манифест разбиения. Как и в главах 3–4, BPE обучается только на обучающей части; в результате получаются восемь правил слияния. Затем токенизатор фиксируется, а программа проверяет, что после кодирования по-прежнему есть 8 обучающих, 2 валидационных и 2 тестовых документа, и отдельно считает пары в каждой части:
rust/demos/ch05-autoregressive-examples/src/main.rs#chapter-output use ch05_autoregressive_examples::hand_labeled_rows;
use llm_from_scratch::corpus::{Corpus, Partition, SplitManifest};
use llm_from_scratch::data::{CausalWindowConfig, EncodedCorpusPartitions};
use llm_from_scratch::tokenizer::bpe::BpeTokenizer;
use llm_from_scratch::tokenizer::bpe_trainer::BpeTrainer;
const CORPUS_JSON: &str = include_str!("../../../data/tiny-bilingual-corpus.json");
const SPLIT_MANIFEST: &str = include_str!("../../../data/splits.json");
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rows = hand_labeled_rows();
println!(
"task-specific hand-labeled contrast: sentiment rows={}",
rows.len()
);
for (index, row) in rows.iter().enumerate() {
println!(
"labeled row {index}: input={:?} label={}",
row.input,
row.label.as_str()
);
}
let source = [0, 41, 42, 43, 44, 1];
let config = CausalWindowConfig::new(3, 1)?;
println!("source sequence for next-token targets: {source:?}");
println!(
"config: context={} stride={} required={}",
config.context_length(),
config.stride(),
config.required_source_tokens()
);
println!("generated pairs: {}", config.window_count(source.len()));
for window in config.windows(&source) {
println!(
"pair start={} input={:?} target={:?}",
window.start(),
window.input(),
window.target()
);
}
let tail = config
.incomplete_tail(&source)
.expect("worked source has one too-short suffix");
println!(
"next start too short: start={} tokens={:?} required={} emitted=false",
tail.start(),
tail.tokens(),
config.required_source_tokens()
);
let short = [0, 61, 1];
let short_tail = config
.incomplete_tail(&short)
.expect("short document leaves a too-short suffix");
println!(
"short document: {short:?} pairs={} suffix={:?}",
config.window_count(short.len()),
short_tail.tokens()
);
let corpus = Corpus::from_json(CORPUS_JSON)?;
let manifest = SplitManifest::from_json(SPLIT_MANIFEST)?;
let partitions = manifest.partition(&corpus)?;
let training = BpeTrainer::new(8).train(&partitions)?;
let tokenizer = BpeTokenizer::from_training(&training)?;
let encoded = EncodedCorpusPartitions::from_partitions(&partitions, &tokenizer);
println!(
"frozen encoded documents: train={} validation={} test={}",
encoded.documents(Partition::Train).len(),
encoded.documents(Partition::Validation).len(),
encoded.documents(Partition::Test).len()
);
let pair_counts = [Partition::Train, Partition::Validation, Partition::Test].map(|partition| {
encoded
.documents(partition)
.iter()
.map(|document| document.windows(config).count())
.sum::<usize>()
});
println!(
"encoded pairs at context=3 stride=1: train={} validation={} test={}",
pair_counts[0], pair_counts[1], pair_counts[2]
);
println!("chapter 6 handoff: count each adjacent training-document transition once");
Ok(())
} Запустите детерминированные проверки из корня репозитория:
cargo test --workspace --locked
cargo run --quiet --locked -p ch05-autoregressive-examples | diff -u rust/demos/ch05-autoregressive-examples/expected.txt -
Если вывод программы полностью совпадает с зафиксированным результатом, вторая
команда ничего не напечатает. Тесты охватывают сдвиг срезов, недопустимые
параметры, пустой срез токенов, документ без содержимого [BOS, EOS], документ,
в который помещается ровно одно окно, остатки, которых недостаточно для новой
пары, границы документов и частей корпуса, а также точное восстановление байтов
зафиксированным токенизатором.
Сдвиг цели и границы на отдельных лентах
Пары для следующего токена внутри каждого документа
На схеме четыре коротких закодированных документа. В каждой полной паре цель начинается на одну позицию позже входа; ни одна пара не пересекает границы документов и частей корпуса.
- Длина контекста
- 3
- Шаг
- 1
- Токенов нужно для пары
- 4
Что показывает схема
Пара строится только при наличии всех необходимых исходных токенов.
На каждой границе документа или части корпуса построение пар начинается заново: ни одна пара не пересекает границу, а стрелки сдвига не соединяют разные ленты.
В каждой показанной позиции токенов уже не хватает на новую пару, но они могли войти в пары, начавшиеся раньше.
BOS — начало документа EOS — конец документа
Граница документа или части корпуса
Обучающая выборка
-
Документ
train-aТокены документа, включая BOS и EOS
BOS:0, 41, 42, 43, 44, EOS:1-
Пара построена #0 Начало окна: 0 0, 41, 42
41, 42, 43 -
Пара построена #1 Начало окна: 1 41, 42, 43
42, 43, 44 -
Пара построена #2 Начало окна: 2 42, 43, 44
43, 44, 1
Остаток: токенов не хватает
Новой пары нетНачало окна: 3 · Токенов нужно для пары: 4
43, 44, 1 -
-
Документ
train-bТокены документа, включая BOS и EOS
BOS:0, 51, 52, EOS:1-
Пара построена #0 Начало окна: 0 0, 51, 52
51, 52, 1
Остаток: токенов не хватает
Новой пары нетНачало окна: 1 · Токенов нужно для пары: 4
51, 52, 1 -
Граница документа или части корпуса
Валидационная выборка
-
Документ
validation-aТокены документа, включая BOS и EOS
BOS:0, 61, 62, 63, EOS:1-
Пара построена #0 Начало окна: 0 0, 61, 62
61, 62, 63 -
Пара построена #1 Начало окна: 1 61, 62, 63
62, 63, 1
Остаток: токенов не хватает
Новой пары нетНачало окна: 2 · Токенов нужно для пары: 4
62, 63, 1 -
Граница документа или части корпуса
Тестовая выборка
-
Документ
test-aТокены документа, включая BOS и EOS
BOS:0, 71, EOS:1Остаток: токенов не хватает
Новой пары нетНачало окна: 0 · Токенов нужно для пары: 4
0, 71, 1
На схеме показаны четыре небольших документа, а не все двенадцать документов корпуса. Правило построения пар во всех частях одинаково, но параметры модели подбираются только по обучающим примерам. Валидационные и тестовые пары для обучения не используются.
Сопоставьте входную строку со строкой целей под ней: цель сдвинута вправо на один токен. Стрелки не пересекают двойные линии между документами и частями корпуса — на каждой новой ленте обход начинается заново. Пунктиром отмечен остаток, которого не хватает на новую пару. Его токены не удаляются: они могли уже встретиться в окнах, начавшихся раньше.
Решите задачи и проверьте себя
- Перечислите все входы и цели для
[0,41,42,43,44,1]при . - Повторите вычисление при . Из каких позиций получатся пары? Какая пропущенная позиция тоже могла бы дать полную пару? Что останется при следующем начале окна?
- Для
[0,61,1]и определите число пар и остаток, которого недостаточно для новой пары. Почему дополнение фиктивными токенами означало бы переход к другому правилу подготовки целей? - Получится ли при из
[0,1]пара[BOS] -> [EOS]? - Даны два документа:
[0,10,1]и[0,20,1]. Может ли при появиться пара[10,1] -> [1,0]? - При в документе из четырёх токенов помещается ровно одна полная пара. Следует ли после неё возвращать пустой остаток?
- Объясните, как токены из остатка могли уже попасть в предыдущие полные пары.
- Почему в главе 6 нельзя подсчитывать переходы, перебирая все перекрывающиеся пары?
- Запрещает ли сам сдвиг цели модели читать более поздние позиции входа?
Проверьте решения
- Позиции 0, 1 и 2 дают три пары из таблицы. В позиции 3 токенов уже недостаточно для новой пары.
- Пары начинаются в позициях 0 и 2. Позицию 1 пропускает шаг, хотя полное окно там поместилось бы. При следующем начале окна, в позиции 4, остаётся
[44,1], чего мало при . - Пар нет. Остаток начинается в позиции 0 и равен
[0,61,1]; дополнять его не нужно. Фиктивные токены создали бы дополнительные входные позиции, потребовали бы маски и отдельного правила о том, какие цели учитывать. Это уже другая политика подготовки данных. - Из двух токенов получается полная пара
[0] -> [1]. - Нет. Такая пара возникла бы только после ошибочного склеивания документов: последняя цель означала бы переход EOS -> BOS. Срез первого документа заканчивается после EOS, а для
[0,20,1]создаётся новый итератор. - В позиции 0 строится единственная полная пара. Следующее начало окна по заданному шагу — позиция 5; она лежит за концом документа, поэтому
incomplete_tailвозвращаетNone. Несуществующий пустой остаток не создаётся. - Начать новую пару из такого остатка нельзя, но его токены могли войти в окна с более ранними началами. В примере остаток
[43,44,1]уже совпадает с целью пары, которая начинается в позиции 2. - Из-за перекрытия один соседний переход встречается в нескольких парах. В главе 6 каждый исходный документ обучающей части нужно пройти ровно один раз.
- Нет. Сдвиг задаёт правильные цели, но доступ к будущим позициям ограничивает сама модель: при рекуррентной обработке — последовательное обновление состояния слева направо, при самовнимании — каузальная маска.
Чтобы проверить, как один параметр влияет на результат, сначала предскажите новые
позиции, пары и остаток для последовательности из шести токенов. Затем временно
измените только шаг в CausalWindowConfig::new(3, 1) в файле
rust/demos/ch05-autoregressive-examples/src/main.rs: замените 1 на 2 и
выполните cargo run --quiet --locked -p ch05-autoregressive-examples. Можно
провести второй опыт, изменив только длину контекста с 3 на 2. Общее число
пар во всём корпусе тоже станет другим, но вручную его считать не требуется.
Сравните результат короткого примера со своим прогнозом, а затем верните
CausalWindowConfig::new(3, 1), чтобы восстановить исходные настройки
демонстрации.
Распространённое заблуждение: «Остаток, из которого нельзя построить новую пару, выбрасывают». Это неверно. Новое окно там не начинается, но те же токены могли уже войти в полные пары с более ранними началами.
Что пары «вход — цель» дают декодеру
В каждой полной паре задана цель для каждой позиции декодера. В позиции нужно предсказать , используя ; и все следующие элементы входа должны быть скрыты. BOS может служить первым входным токеном, а EOS — последней целью. Ни один из них не является токеном заполнения.
Сдвинутые массивы задают, что именно нужно предсказывать, но сами по себе не скрывают от модели будущие позиции. Рекуррентная модель соблюдает ограничение благодаря последовательному обновлению состояния слева направо. В декодере с самовниманием за это отвечает каузальная маска.
Дальше вместо целочисленных счётчиков появятся тензоры и эмбеддинги, а механизм внимания и обучаемые параметры будут участвовать в вычислении логитов. Связь между входом и целью при этом останется прежней. Но сначала в главе 6 мы построим наглядную биграммную модель. Она пройдёт по исходным закодированным документам обучающей части корпуса, а не по перекрывающимся парам: так каждый соседний переход будет учтён ровно один раз, а валидационные и тестовые данные не повлияют на обученные счётчики.