Current: Week 9

0%

Week 9

Week 9 — Tokenizers: from raw text to training batches

Key questionHow can we reliably turn “我喜欢AI,AI也喜欢猫。” into usable integer IDs while keeping tokenizer training, encoding and model learning distinct?

Learning objectives

  • Compare character, word, UTF-8 byte and subword units in terms of V, L, coverage and computational cost.
  • Distinguish code points, UTF-8 bytes, BPE symbols, tokens and token IDs; explain why normalization belongs to the tokenizer protocol.
  • Work through two byte-level BPE rounds: pair counts, tie-breaking, merging and length changes.
  • Distinguish tokenizer training from encoding and bind tokenizer artifacts to model checkpoints.
  • Follow an auditable path: text → tokens → stream [L] → shifted examples [T] → a batch [B,T], with padding only where needed.
  • Construct causal, padding and loss masks separately and explain which errors each prevents.
  • Before Week 10, stop using the independent w09-readable-v1 ID space and return to the incompatible five-token mini-gpt-v1 main path.

145 min estimated reading time

Week progress: 0 of 19 sections (0%)Course progress: 0 of 321 sections (0%)

This week focuses on a reliable data interface, not implementing four production tokenizers. The main model path keeps the five-token vocabulary and course_data.py. Character and BPE examples are separate demonstrations of alternatives. Learn one reliable input path before comparing more complex schemes.

Scroll horizontally to view all columns.

Course data table
Study unitThe question we solve
1: A stable text interfaceFix segmentation, ID order, and unknown-token behavior. Encoding again must not randomly renumber the vocabulary.
2: Construct actual dataBuild T+1-token windows from documents using shared data functions. Split training and validation documents first, then construct windows within each split.
3: Decide what an experiment demonstratesCheck duplicate documents, long shared passages, and valid-target counts. Distinguish a pipeline demonstration from independent validation.
4: Compare alternatives as neededStart with two readable BPE merges, then optionally examine bytes, special tokens, and padding. These are not hidden prerequisites for the five-token model.

Run python week09_data_protocol.py from the examples directory. It produces the five-token inputs/targets accepted by Week 10. The final project explicitly creates a separate character vocabulary instead of silently feeding a different ID mapping into an existing checkpoint.

Alternate reading, hand calculation, and code changes. Units can take several sessions. Existing section numbers remain for links and reference; follow the displayed order rather than jumping around by older numbers.

Week 9 learning goal: a stable shared protocol between text and model

A tokenizer is not the language model. It specifies normalization, segmentation, vocabulary entries, token IDs, and decoding. It converts text into integers before model computation and generated integers into text afterward. The contextual patterns learned by the language model reside in its embedding and Transformer parameters, not in the act of encoding.

Scroll horizontally to view all columns.

Course data table
ConceptMeaning in this chapterExample from the fixed sentence
TokenA discrete unit emitted by a tokenizerIt might be 我, 喜欢, AI, or an individual byte
VocabularyA finite list of allowed tokens and their IDsw09-readable-v1 contains V=11 entries
Token IDAn integer index for an entryIn this snapshot, AI has ID 6
TokenizerThe complete normalization, segmentation, encoding, decoding, and special-token policyMap the fixed sentence into the stream below

Scroll horizontally to view all columns.

The complete ordered vocabulary of w09-readable-v1 (V=11)
IDtokenrole
0<BOS>Document start
1<EOS>Document end
2<PAD>Pad batch sequences to a common length
3<UNK>A lossy unknown-item policy
4我 (I)Content token
5喜欢 (like)Content token
6AIContent token
7Content token
8也 (also)Content token
9猫 (cat)Content token
10Content token
Concept sequence
  1. 我喜欢AI,AI也喜欢猫。
  2. [<BOS>, 我, 喜欢, AI, ,, AI, 也, 喜欢, 猫, 。, <EOS>]
  3. IDs s=[0,4,5,6,7,6,8,5,9,10,1], shape [L]=[11]
  4. decode(skip_special_tokens=True) → 我喜欢AI,AI也喜欢猫。

Scroll horizontally to view all columns.

Interpret IDs only within their own tokenizer artifact; do not mix these mappings
Teaching artifactWhere usedBase units / ID spaceLearning purposeWhere this example ends
character-demoSection 4Nine known Unicode code points, IDs 0..8Understand encode, decode, dtype, and [L]End of Section 4
toy-byte-bpeSections 5–7UTF-8 byte IDs 0..255, then merged IDs 256 and 257Calculate pair counts, merges, frozen encoding, and reconstructionEnd of Section 7
w09-readable-v1Sections 8–18Readable pieces, V=11, IDs 0..10Trace the special-token stream, windows, masks, and batchesEnd of Week 9
mini-gpt-v1From Week 10Five tokens, V=5, IDs 0..4Connect to the canonical MiniGPTAccording to the Weeks 10–12 protocol
encodeA:Unicode text{0,,VA1}L,decodeA:{0,,VA1}Ltext\operatorname{encode}_{A}:\mathrm{Unicode\ text}\to\{0,\ldots,V_A-1\}^{L},\qquad \operatorname{decode}_{A}:\{0,\ldots,V_A-1\}^{L}\to\mathrm{text}

Knowledge check

Why version tokenizer artifacts together with model checkpoints?

1. A token is not necessarily a word: four possible boundaries

The same text, “我喜欢AI,AI也喜欢猫。”, can produce different units under different rules. The word-like row below depends on a specified segmenter. The illustrative subword row happens to look the same, but that is a result of its chosen vocabulary, not the definition of subword tokenization.

Scroll horizontally to view all columns.

Four explicit segmentations of the fixed sentence
schemeemitted unitscount Limmediate benefitimmediate cost
character / code point[我, 喜, 欢, A, I, ,, A, I, 也, 喜, 欢, 猫, 。]13Code-point boundaries do not require a word dictionary喜欢 and AI are split, producing a longer sequence
Word-like (specified segmenter)[我, 喜欢, AI, ,, AI, 也, 喜欢, 猫, 。]9Short and easy to readChinese word boundaries, unseen words, and normalization require additional rules
UTF-8 byteE6 88 91 … 41 49 … E3 80 8231A fixed set of 256 byte symbols covers valid UTF-8 textLongest in this example; a byte fragment may not be independently readable
Subword (illustrative result after additional training)[我, 喜欢, AI, ,, AI, 也, 喜欢, 猫, 。]9Frequent pieces can use fewer tokens; less familiar text can be split into smaller unitsMerges, normalization, and ID assignments belong to the tokenizer version
Lcharacter=13,Lwordlike=9,Lbyte=31,Lsubword demo=9L_{\mathrm{character}}=13,\qquad L_{\mathrm{wordlike}}=9,\qquad L_{\mathrm{byte}}=31,\qquad L_{\mathrm{subword\ demo}}=9

Knowledge check

Does a sentence with thirteen code points necessarily produce thirteen token IDs?

2. Why not use whole words? Coverage versus length

For the fixed Chinese sentence, a word-like tokenizer can keep 喜欢 and AI intact. A byte vocabulary covers its valid UTF-8 encoding but needs thirty-one positions. Subword schemes can start from smaller units and learn frequent adjacent combinations from a corpus.

Scroll horizontally to view all columns.

Course data table
choiceVocabulary / coveragesequence lengthMain trade-off
whole wordMany whole-word entries; missing words need <UNK> or another fallbackCommon words can be represented compactlyA large V or information loss on unknown words
character / byteA smaller base vocabulary; a complete byte base covers valid UTF-8 textThirteen / thirty-one positions for this sentenceMore positions and, for bytes, reduced direct readability
subwordcommon pieces + smaller fallback piecesOften a compromise; nine positions in this illustrationLearned merges and segmentation rules must be saved and fixed for use
self-attention pairwise workO(L2C)\mathrm{self\text{-}attention\ pairwise\ work}\approx O(L^2C)

Knowledge check

What can subwords do that a finite whole-word vocabulary alone cannot?

3. Vocabulary-size trade-offs: V and L both affect the system

The sentence's lengths of thirteen, nine, and thirty-one illustrate different choices; they do not prove nine is optimal. Larger vocabularies can dedicate entries to common pieces, while smaller ones may use more positions. The model still produces V logits at every processed position.

Scroll horizontally to view all columns.

Course data table
design directionvocabulary-facing tablessame text lengthcontext / Attention implication
smaller VFewer embedding rows and output candidatesOften a larger LA fixed T may cover less original text; a longer processed sequence has more position pairs
larger VMore embedding rows and output candidatesFrequent pieces may need fewer positionsMore output-head computation and storage per position
EinRV×C,logitsRB×T×VE_{\mathrm{in}}\in\mathbb{R}^{V\times C},\qquad \mathrm{logits}\in\mathbb{R}^{B\times T\times V}
Nvocab, generic biased untied=VC+(VC+V)=2VC+VN_{\mathrm{vocab,\ generic\ biased\ untied}}=VC+(VC+V)=2VC+V
Nvocab, canonical MiniGPT=VC+VC=2VCN_{\mathrm{vocab,\ canonical\ MiniGPT}}=VC+VC=2VC

Scroll horizontally to view all columns.

Only the two vocabulary-facing tables are counted here, not Transformer blocks. These are illustrative configurations.
exampleinput embedding VCbias-free untied LM head VCtotal 2VC
Teaching dimensions V=11, C=411×4=4411×4=4488 parameters
Illustrative larger dimensions V=50,000, C=76838,400,00038,400,00076,800,000 parameters
B=2, T=4, V=11#logits=BTV=2×4×11=88B=2,\ T=4,\ V=11\quad\Longrightarrow\quad \#\mathrm{logits}=BTV=2\times4\times11=88

V determines the size of vocabulary-facing parameter tables and the number of candidates scored per position. Through segmentation, the tokenizer also changes L, the number of positions needed to represent text.

Knowledge check

With C fixed, which interfaces grow when V doubles?

4. A character tokenizer: first complete the encode/decode loop

For “我喜欢AI,AI也喜欢猫。”, this tokenizer treats each Unicode code point as a token. It is a useful mapping demonstration, not a production recommendation. It does not automatically handle out-of-vocabulary code points, grapheme clusters, or normalization differences.

python
import torch

text = "我喜欢AI,AI也喜欢猫。"
character_tokens = ["我", "喜", "欢", "A", "I", ",", "也", "猫", "。"]
char_to_id = {token: index for index, token in enumerate(character_tokens)}
id_to_char = {index: token for token, index in char_to_id.items()}


def encode_characters(value: str) -> list[int]:
    return [char_to_id[character] for character in value]


def decode_characters(ids: list[int]) -> str:
    return "".join(id_to_char[index] for index in ids)


ids = encode_characters(text)
token_tensor = torch.tensor(ids, dtype=torch.long)

assert ids == [0, 1, 2, 3, 4, 5, 3, 4, 6, 1, 2, 7, 8]
assert tuple(token_tensor.shape) == (13,)
assert decode_characters(ids) == text

char_to_id:known code point{0,,Vchar1},ids:[13]\mathrm{char\_to\_id}:\mathrm{known\ code\ point}\to\{0,\ldots,V_{\mathrm{char}}-1\},\qquad \mathrm{ids}:[13]

Knowledge check

Why implement the character tokenizer first even though it uses thirteen tokens here?

8. Tokenizer Training and Text Encoding Are Different

Scroll horizontally to view all columns.

Course data table
phaseinputchanges artifacts?output
tokenizer trainingtraining corpus documentsYes: choose normalization, the base vocabulary, merges and special IDsversioned artifact A
text encodingone text + frozen ANoIDs in 0..V_A−1
model trainingbatches of frozen-A IDsNo: A stays fixed; model parameters changeupdated neural weights
Concept sequence
  1. train documents
  2. normalization / pre-tokenization policy
  3. BPE counts + ordered merges
  4. frozen tokenizer artifact A
  5. encode train / validation / inference text
  6. token streams
  7. model training
train_tokenizer(train documents)A,A={normalizer,pretokenizer,base vocab,merges,specials}\operatorname{train\_tokenizer}(\mathrm{train\ documents})\to A,\qquad A=\{\mathrm{normalizer,pretokenizer,base\ vocab,merges,specials}\}
encodeA(text)ids{0,,VA1}L,Aafter=Abefore\operatorname{encode}_{A}(\mathrm{text})\to\mathrm{ids}\in\{0,\ldots,V_A-1\}^{L},\qquad A_{\mathrm{after}}=A_{\mathrm{before}}

Below, w09-readable-v1 is a small, concrete teaching artifact—not an unspecified library tokenizer. It uses identity normalization, an explicitly ordered vocabulary and fixed longest-first content routes. An unmatched Unicode code point becomes <UNK>. This routing policy is a teaching convention; it is not claimed to be the result of the preceding two toy BPE merges.

Concept sequence
  1. ① TOKENS freezes the ordered vocabulary and special IDs
  2. ② CONTENT_ROUTES defines longest-first content segmentation
  3. ③ segment_content returns token strings only
  4. ④ encode_content looks up their IDs without adding boundaries
  5. ⑤ encode_document adds BOS and EOS exactly once
  6. ⑥ decode reconstructs text using the same ID table

On your first reading, follow the cursor through the fixed sentence from 0 to len(text). On the second reading, inspect boundary ownership and error handling. This separates the main data flow from the defensive details.

w09_readable_v1.py
TOKENS = (
    "<BOS>",
    "<EOS>",
    "<PAD>",
    "<UNK>",
    "我",
    "喜欢",
    "AI",
    ",",
    "也",
    "猫",
    "。",
)
TOKEN_TO_ID = {token: token_id for token_id, token in enumerate(TOKENS)}
ID_TO_TOKEN = {token_id: token for token, token_id in TOKEN_TO_ID.items()}

BOS_ID = TOKEN_TO_ID["<BOS>"]  # 0
EOS_ID = TOKEN_TO_ID["<EOS>"]  # 1
PAD_ID = TOKEN_TO_ID["<PAD>"]  # 2
UNK_ID = TOKEN_TO_ID["<UNK>"]  # 3

# Deterministic longest-first routing for this small teaching artifact.
# Equal-length routes retain this declared order.
CONTENT_ROUTES = ("喜欢", "AI", "我", ",", "也", "猫", "。")
HIDDEN_ON_DECODE_IDS = {BOS_ID, EOS_ID, PAD_ID}


class ReadableTokenizerV1:
    version = "w09-readable-v1"
    normalization = "identity"
    vocabulary = TOKENS

    def segment_content(self, text: str) -> list[str]:
        pieces: list[str] = []
        cursor = 0
        while cursor < len(text):
            matched = next(
                (
                    piece
                    for piece in CONTENT_ROUTES
                    if text.startswith(piece, cursor)
                ),
                None,
            )
            if matched is None:
                pieces.append("<UNK>")
                cursor += 1
            else:
                pieces.append(matched)
                cursor += len(matched)
        return pieces

    def encode_content(
        self,
        text: str,
        *,
        add_special_tokens: bool = False,
    ) -> list[int]:
        if add_special_tokens:
            raise ValueError("encode_content never adds BOS/EOS")
        return [TOKEN_TO_ID[piece] for piece in self.segment_content(text)]

    def encode_document(
        self,
        text: str,
        *,
        add_special_tokens: bool = True,
    ) -> list[int]:
        if not add_special_tokens:
            raise ValueError("use encode_content when boundaries are unwanted")
        return [
            BOS_ID,
            *self.encode_content(text, add_special_tokens=False),
            EOS_ID,
        ]

    def decode(
        self,
        ids: list[int],
        *,
        skip_special_tokens: bool = True,
    ) -> str:
        pieces: list[str] = []
        for token_id in ids:
            if token_id not in ID_TO_TOKEN:
                raise ValueError(f"token ID out of range: {token_id}")
            if skip_special_tokens and token_id in HIDDEN_ON_DECODE_IDS:
                continue
            pieces.append(ID_TO_TOKEN[token_id])
        return "".join(pieces)


W09_READABLE_V1 = ReadableTokenizerV1()


def encode_content(text: str) -> list[int]:
    return W09_READABLE_V1.encode_content(
        text,
        add_special_tokens=False,
    )


def encode_document(text: str) -> list[int]:
    return W09_READABLE_V1.encode_document(
        text,
        add_special_tokens=True,
    )


RUNNING_TEXT = "我喜欢AI,AI也喜欢猫。"
content_ids = encode_content(RUNNING_TEXT)
document_ids = encode_document(RUNNING_TEXT)

assert content_ids == [4, 5, 6, 7, 6, 8, 5, 9, 10]
assert document_ids == [0, *content_ids, 1]
assert W09_READABLE_V1.decode(
    document_ids,
    skip_special_tokens=True,
) == RUNNING_TEXT

# These calls never mutate vocabulary, routes, or IDs.

Scroll horizontally to view all columns.

Encoding and decoding apply the frozen artifact; neither creates a new vocabulary entry.
fixed inputmethodoutputartifact mutation
我喜欢AI,AI也喜欢猫。segment_content[我,喜欢,AI,,,AI,也,喜欢,猫,。]none
我喜欢AI,AI也喜欢猫。encode_content[4,5,6,7,6,8,5,9,10]none
我喜欢AI,AI也喜欢猫。encode_document[0,4,5,6,7,6,8,5,9,10,1]none
[0,4,5,6,7,6,8,5,9,10,1]decode(skip_special_tokens=True)我喜欢AI,AI也喜欢猫。none

Knowledge check

After freezing the BPE rule (41,49)→256, does encoding the fixed sentence again change that rule?

9. Bind the Tokenizer to the Model: Matching Shapes Are Not Enough

In w09-readable-v1, 喜欢 has ID 5 and AI has ID 6. If another V=11 tokenizer swaps those IDs, input 6 still reads the embedding row trained for AI, but the new system calls it 喜欢. Output entry 6 will also decode to the wrong piece.

Scroll horizontally to view all columns.

The tokenizer artifact and model checkpoint form a compatibility bundle
bundle itemwhy model loading needs it
ordered token list / VocabularyNames the token represented by every embedding row and output-logit entry
ordered BPE mergesDetermines how new text is assembled into vocabulary entries
normalizer + pre-tokenizerDetermines the symbols and boundaries seen before merging
special-token IDs / insertion policyDefines the control IDs for BOS, EOS, PAD and UNK, and when they appear
tokenizer version / content hashSupports an exact compatibility check before loading
model config + weightsDeclares shapes such as V, C and context length, and stores learned parameters
text
w09-readable-v1:
ID 5 -> 喜欢
ID 6 -> AI

incompatible-tokenizer:
ID 5 -> AI
ID 6 -> 喜欢

Both report V=11.
Model input ID 6 is numerically in range, but its learned row and displayed token disagree.

E[input_ids]RB×T×C,ERV×C,logitsRB×T×VE[\mathrm{input\_ids}]\in\mathbb{R}^{B\times T\times C},\qquad E\in\mathbb{R}^{V\times C},\qquad \mathrm{logits}\in\mathbb{R}^{B\times T\times V}

Knowledge check

Why might two V=11 tokenizers be incompatible with the same checkpoint?

12. Turn a Corpus into a Token Tensor: Keep Order and Document Boundaries

For the single document “我喜欢AI,AI也喜欢猫。”, w09-readable-v1 produces [0,4,5,6,7,6,8,5,9,10,1], shape [11]. This is still one ordered stream—not a batch, and not a tensor with a C feature axis.

corpus_to_tensor.py
import torch

from w09_readable_v1 import encode_document

documents = ["我喜欢AI,AI也喜欢猫。"]

# encode_document already inserts exactly one BOS and one EOS per document.
stream: list[int] = []
for document in documents:
    stream.extend(encode_document(document))

stream_tensor = torch.tensor(stream, dtype=torch.long)

assert stream == [0, 4, 5, 6, 7, 6, 8, 5, 9, 10, 1]
assert tuple(stream_tensor.shape) == (11,)
assert stream_tensor.dtype == torch.long

s(d){0,,V1}Ld,s=concat ⁣(s(1),,s(D)){0,,V1}L,L=d=1DLds^{(d)}\in\{0,\ldots,V-1\}^{L_d},\qquad s=\operatorname{concat}\!\left(s^{(1)},\ldots,s^{(D)}\right)\in\{0,\ldots,V-1\}^{L},\qquad L=\sum_{d=1}^{D}L_d

Scroll horizontally to view all columns.

Boundary tokens identify boundaries; whether attention or loss crosses them is a separate data-pipeline decision.
multi-document policyboundary exampleWhich transitions are supervised?trade-off
Concatenate boundary-marked documents…content,<EOS>,<BOS>,next document…Last content token→EOS, and possibly EOS→BOSSimple implementation; includes the selected cross-document control-token transitions
Build windows separately within each documentNo window crosses a document boundaryOnly within-document transitions, including final content→EOSClear boundaries; short tails may leave positions unused
packing + boundary-aware masksPack multiple documents into one rectangular batchUse explicit attention/loss policies to block unwanted cross-document connectionsEfficient use of positions, but more complex to implement and audit

These alternatives do not differ for this chapter's single-document stream. With multiple documents, choose and document a policy. Inserting EOS alone does not block all cross-document information flow.

Knowledge check

What are the shape and dtype immediately after the fixed sentence becomes stream_tensor?

13. Train/Validation Split: Separate Documents Before Building Windows

The single sentence “我喜欢AI,AI也喜欢猫。” is a pipeline example, not a meaningful train/validation experiment. For a real corpus, split documents first, freeze the tokenizer, then build train_stream and val_stream separately. Validation forward passes do not call backward or optimizer.step.

Concept sequence
  1. raw documents
  2. document/group/temporal split
  3. fit tokenizer A on train documents only (when training from scratch)
  4. freeze A
  5. encode train documents → train_stream [L_train]
  6. encode validation documents → val_stream [L_val]
  7. form windows separately inside each split
DtrainDval=,wWtrain:source(w)Dtrain,wWval:source(w)DvalD_{\mathrm{train}}\cap D_{\mathrm{val}}=\varnothing,\qquad \forall w\in W_{\mathrm{train}}:\operatorname{source}(w)\in D_{\mathrm{train}},\qquad \forall w\in W_{\mathrm{val}}:\operatorname{source}(w)\in D_{\mathrm{val}}

This is a provenance guarantee, not value-level deduplication. Independent documents can contain common phrases or boilerplate, so identical ID windows may appear in both splits. If deduplication is required, specify a separate document/group deduplication policy.

Scroll horizontally to view all columns.

Course data table
stagetrain lanevalidation lane
raw datatrain documentsheld-out validation documents
tokenizerfit A here if from scratch, then freezeonly apply frozen A
modelforward + loss + backward + stepeval/no_grad forward + loss only
claimoptimization progressestimate on held-out token sequences

Validation does not participate in backward, but it influences model selection and hyperparameter choices. Repeatedly improving against one validation set makes it less suitable as an untouched final assessment. Formal comparisons can reserve a test split for the end. This course's tiny toy data cannot support strong quality claims.

Knowledge check

Why split by document before concatenating streams?

14. Take One Example from a Stream: Shift Targets One Position Right

The fixed stream s=[0,4,5,6,7,6,8,5,9,10,1] encodes “我喜欢AI,AI也喜欢猫。”. Choose start i=0 and context length T=4. x reads four consecutive IDs; y reads four IDs beginning one position later.

Scroll horizontally to view all columns.

The full one-dimensional w09-readable-v1 stream s, L=11
stream indexIDtoken
00<BOS>
14我 (I)
25喜欢 (like)
36AI
47
56AI
68也 (also)
75喜欢 (like)
89猫 (cat)
910
101<EOS>
text
x = s[0:4] = [0,4,5,6] = [<BOS>, 我, 喜欢, AI]  shape [T]=[4]
y = s[1:5] = [4,5,6,7] = [我, 喜欢, AI, ,]       shape [T]=[4]

Scroll horizontally to view all columns.

Four aligned next-token tasks; a query is not given its target in advance.
position tinput tokentarget next tokencausal model may use
0<BOS>我 (I)[<BOS>]
1我 (I)喜欢 (like)[<BOS>, 我]
2喜欢 (like)AI[<BOS>, 我, 喜欢]
3AI[<BOS>, 我, 喜欢, AI]
x=s[i:i+T],y=s[i+1:i+T+1],x,y:[T]=[4]x=s[i:i+T],\qquad y=s[i+1:i+T+1],\qquad x,y:[T]=[4]
0iLT1,L=11, T=4i{0,1,,6}0\le i\le L-T-1,\qquad L=11,\ T=4\quad\Longrightarrow\quad i\in\{0,1,\ldots,6\}

Knowledge check

In the i=0 example, which target corresponds to input ID 5 (喜欢, “like”)?

15. Build a Batch: Randomize Row Starts, Keep Each Row Consecutive

Python s[a:b] includes position a but excludes b, giving b−a items. A T=4 training sequence needs five consecutive source IDs: the first four are inputs and the last four are targets, overlapping by three. randint also excludes its upper bound. Write down the last valid start before choosing high.

Continue with the w09-readable-v1 stream for the same Chinese sentence. Fix B=2, T=4 and valid starts [0,5]. Randomness chooses where each row begins; within a row, IDs retain their original order.

text
starts = [0,5]

inputs = [
  [0,4,5,6],  # <BOS> 我 喜欢 AI
  [6,8,5,9],  # AI 也 喜欢 猫
]             # [B,T] = [2,4]

targets = [
  [4,5,6,7],  # 我 喜欢 AI ,
  [8,5,9,10], # 也 喜欢 猫 。
]             # [B,T] = [2,4]

Scroll horizontally to view all columns.

Each row needs T+1 source IDs to produce T inputs and T shifted targets.
batch row bstartinput sequencetarget sequence
00<BOS> 我 喜欢 AI我 喜欢 AI ,
15AI 也 喜欢 猫也 喜欢 猫 。
python
import torch


def sample_batch(source: torch.Tensor, batch_size: int, context_length: int):
    if source.ndim != 1:
        raise ValueError("source must have shape [L]")
    if len(source) <= context_length:
        raise ValueError("source needs at least context_length + 1 IDs")

    # torch.randint excludes high, so high=L-T yields starts 0..L-T-1.
    starts = torch.randint(
        low=0,
        high=len(source) - context_length,
        size=(batch_size,),
    )
    inputs = torch.stack(
        [source[i : i + context_length] for i in starts.tolist()]
    )
    targets = torch.stack(
        [source[i + 1 : i + context_length + 1] for i in starts.tolist()]
    )
    return inputs, targets


source = torch.tensor([0, 4, 5, 6, 7, 6, 8, 5, 9, 10, 1])
inputs, targets = sample_batch(source, batch_size=2, context_length=4)
assert inputs.shape == targets.shape == (2, 4)

X[b,:]=s[ib:ib+T],Y[b,:]=s[ib+1:ib+T+1],X,YZB×T=Z2×4X[b,:]=s[i_b:i_b+T],\qquad Y[b,:]=s[i_b+1:i_b+T+1],\qquad X,Y\in\mathbb{Z}^{B\times T}=\mathbb{Z}^{2\times4}
LT=114=7legal starts={0,,6}L-T=11-4=7\quad\Longrightarrow\quad \mathrm{legal\ starts}=\{0,\ldots,6\}

Knowledge check

Why does each row require T+1 source IDs?

10. Special Tokens: Give Boundary Insertion One Owner

In Week 9's readable snapshot, IDs 0..3 are reserved specials and content entries begin at 4. They occupy vocabulary entries visible to the model; typing the literal characters <EOS> does not automatically invoke their control behavior. Display, masking, stopping generation and loss exclusion all require explicit API agreements.

Scroll horizontally to view all columns.

Course data table
IDspecial tokenrole in w09-readable-v1typical handling
0<BOS>Document start / initial contextencode_document adds it once at the beginning
1<EOS>Document endencode_document adds it once at the end; generation may use it as a stop candidate
2<PAD>Fill unequal-length rows to form a rectangleUsually excluded through attention and loss policies
3<UNK>Unknown-token marker for this limited content vocabularyLossy; not inherently required by byte fallback

Scroll horizontally to view all columns.

Course data table
API result for the running Chinese sentencetokensIDs
encode_content(add_special_tokens=False)[我,喜欢,AI,,,AI,也,喜欢,猫,。][4,5,6,7,6,8,5,9,10]
encode_document(add_special_tokens=True)[<BOS>,我,喜欢,AI,,,AI,也,喜欢,猫,。,<EOS>][0,4,5,6,7,6,8,5,9,10,1]
special_tokens_example.py
from w09_readable_v1 import (
    BOS_ID,
    EOS_ID,
    W09_READABLE_V1,
    encode_content,
    encode_document,
)

text = "我喜欢AI,AI也喜欢猫。"
content_ids = encode_content(text)
document_ids = encode_document(text)

assert document_ids == [BOS_ID, *content_ids, EOS_ID]
assert W09_READABLE_V1.decode(
    document_ids,
    skip_special_tokens=True,
) == text

# Wrong: encode_document already owns both boundaries.
# duplicated = [BOS_ID, *encode_document(text), EOS_ID]

0special_id<V,Vw09-readable-v1=110\le \mathrm{special\_id}<V,\qquad V_{\mathrm{w09\text{-}readable\text{-}v1}}=11

Knowledge check

Why does encode_document(text)+[EOS] violate this section's convention?

11. Padding and Attention Masks: Rectangular Does Not Mean Valid

Scroll horizontally to view all columns.

Course data table
ControlQuestion to ask first
causal maskCould this query peek at a real answer to its right?
padding maskWas this slot added only to make the tensor rectangular?
loss maskDoes this prediction have a valid target, and should it count toward loss?

All three controls can coexist. Sharing the word “mask” does not make them interchangeable. Here, −100 means “do not score this target”; it is not in the vocabulary and must never be used for embedding lookup.

Use “我喜欢AI,AI也喜欢猫。” as row 0 and the shorter “我喜欢AI。” as row 1. Both come from w09-readable-v1's encode_document. Call this padded batch width T_pad=11 so it is not confused with the later training-window length T=4.

Scroll horizontally to view all columns.

After right padding, input_ids and attention_mask both have shape [B,T_pad]=[2,11].
rowinput_ids [T_pad=11]attention_mask [T_pad=11]
0: 我喜欢AI,AI也喜欢猫。[0,4,5,6,7,6,8,5,9,10,1][1,1,1,1,1,1,1,1,1,1,1]
1: 我喜欢AI。[0,4,5,6,10,1,2,2,2,2,2][1,1,1,1,1,1,0,0,0,0,0]

Scroll horizontally to view all columns.

Course data table
objectexample valuejob
PAD token ID2Occupies an unused slot in the rectangular tensor
attention_maskreal=1, pad=0Prevents queries from using PAD keys as context; the API also needs a policy for padded queries
causal maskj≤tPrevents queries from reading future keys; does not itself identify PAD
loss ignore index-100Excludes padded targets from cross entropy
Mallowed[b,t,j]=1[jt]1[attention_mask[b,j]=1]M_{\mathrm{allowed}}[b,t,j]=\mathbf{1}[j\le t]\land\mathbf{1}[\mathrm{attention\_mask}[b,j]=1]
input_ids,attention_maskZB×Tpad,Mcausal{0,1}Tpad×Tpad\mathrm{input\_ids},\mathrm{attention\_mask}\in\mathbb{Z}^{B\times T_{\mathrm{pad}}},\qquad M_{\mathrm{causal}}\in\{0,1\}^{T_{\mathrm{pad}}\times T_{\mathrm{pad}}}

Now apply these rules to a shorter next-token batch with T_mask=7. Row 0 uses eight consecutive source IDs, yielding seven input→target pairs. Row 1's entire short document has six IDs: append one PAD to its input, and set both the target after EOS and the target for the PAD slot to -100.

Scroll horizontally to view all columns.

ID 2 is the model's PAD input. -100 is not a vocabulary ID; it is the ignore_index sentinel passed to Cross Entropy here.
rowinput_ids [7]attention_mask [7]next-token targets [7]
0: Prefix of the main sentence[0,4,5,6,7,6,8][1,1,1,1,1,1,1][4,5,6,7,6,8,5]
1: Complete short document[0,4,5,6,10,1,2][1,1,1,1,1,1,0][4,5,6,10,1,-100,-100]

Scroll horizontally to view all columns.

The three controls apply to different things: temporal visibility, valid keys and targets that should be scored.
controlQuestion it answersSpecific effect on row 1
causal maskMay query t read a future key at j>t?t=4 (。) may read only key positions 0..4
padding maskMay a query use an invalid PAD key as context?No query may read the PAD key at position 6
loss mask / ignore_indexShould this position's prediction contribute to loss and its gradients?Targets at positions 5 and 6 are -100 and are excluded from mean loss
padding_and_loss_masks.py
import torch
import torch.nn.functional as F

input_ids = torch.tensor([
    [0, 4, 5, 6, 7, 6, 8],
    [0, 4, 5, 6, 10, 1, 2],
], dtype=torch.long)

attention_mask = torch.tensor([
    [1, 1, 1, 1, 1, 1, 1],
    [1, 1, 1, 1, 1, 1, 0],
], dtype=torch.bool)

targets = torch.tensor([
    [4, 5, 6, 7, 6, 8, 5],
    [4, 5, 6, 10, 1, -100, -100],
], dtype=torch.long)

B, T = input_ids.shape
causal = torch.tril(torch.ones(T, T, dtype=torch.bool))

# allowed[b,t,j]: query t may read key j only when j is not
# in the future and row b says that key j is real rather than PAD.
allowed = causal.unsqueeze(0) & attention_mask[:, None, :]
assert tuple(allowed.shape) == (2, 7, 7)
assert allowed[1, 4].tolist() == [
    True, True, True, True, True, False, False
]

valid_targets = targets.ne(-100)
assert int(valid_targets.sum()) == 12  # row 0: 7, row 1: 5

# Zero logits mean a uniform distribution over V=11 candidates.
# Cross Entropy averages only the 12 valid targets.
logits = torch.zeros(B, T, 11)
loss = F.cross_entropy(
    logits.reshape(B * T, 11),
    targets.reshape(B * T),
    ignore_index=-100,
)
assert torch.isclose(loss, torch.log(torch.tensor(11.0)))

Nscored=7+5=12,L=112(b,t):Yb,t100logp ⁣(Yb,tXb,t)N_{\mathrm{scored}}=7+5=12,\qquad \mathcal{L}=\frac{1}{12}\sum_{(b,t):Y_{b,t}\ne-100}-\log p\!\left(Y_{b,t}\mid X_{b,\le t}\right)

Knowledge check

Why is input_ids.shape=[2,11] insufficient to establish that the padded batch is correct?

6. Unseen Tokens: Decide How to Preserve Information

Suppose a limited-vocabulary variant of w09-readable-v1 omits 猫 (“cat”). It could emit <UNK>, ID 3, but distinct unknown pieces would collapse to one address and 猫 could not be reconstructed exactly. Byte fallback can retain E7 8C AB and later reconstruct the original text.

Scroll horizontally to view all columns.

Course data table
policyOutput when 猫 cannot be looked up directlyround-tripcost / behavior
<UNK>[3]Lossy: reconstructs only the unknown markerShort, but different unknown pieces share one ID
subword / byte fallbackOne or more known smaller-piece IDsCan preserve the original text if normalization and byte handling are losslessk may exceed 1, making the sequence longer
explicit errorProduces no IDsThe caller must handle the failureUseful for strict input policies that forbid automatic replacement
f:text piece{0,,V1},encode(u)={f(u),udom(f)fallback(u){0,,V1}k,udom(f)f:\mathrm{text\ piece}\rightharpoonup\{0,\ldots,V-1\},\qquad \operatorname{encode}(u)=\begin{cases}f(u),&u\in\operatorname{dom}(f)\\[2pt]\mathrm{fallback}(u)\in\{0,\ldots,V-1\}^{k},&u\notin\operatorname{dom}(f)\end{cases}

The actual snapshot includes 猫, so it normally encodes as ID 9. Malformed byte sequences are a separate issue requiring an explicit UTF-8 replacement/error policy; do not confuse them with valid but unseen words.

Knowledge check

Why does byte fallback usually lose less information than replacing 猫 with <UNK>?

7. BPE Intuition: Work Through Two Actual Merges

This toy byte-level BPE has no pre-token boundaries: every adjacent byte pair in the sentence can be counted. The initial table lists pairs occurring at least twice. (9C,E6) crosses from the final byte of 喜 to the first byte of 欢 and occurs in both instances of 喜欢.

Scroll horizontally to view all columns.

Complete initial inventory of repeated pairs; all six counts are 2
initial adjacent byte paircountwhy repeated
(41,49)2Both occurrences of AI
(E5,96)2The first two bytes in each 喜
(96,9C)2The last two bytes in each 喜
(9C,E6)2The code-point boundary 喜→欢 in both occurrences
(E6,AC)2The first two bytes in each 欢
(AC,A2)2The last two bytes in each 欢
countr(a,b)=i=1nr11 ⁣[(zi(r),zi+1(r))=(a,b)]\operatorname{count}_r(a,b)=\sum_{i=1}^{n_r-1}\mathbf{1}\!\left[(z_i^{(r)},z_{i+1}^{(r)})=(a,b)\right]
text
Round 1
all six candidates tie at count 2
lexicographically smallest integer pair = (0x41, 0x49)

(41,49) -> new token 256, displayed [41 49]

... E6 AC A2 [41 49] EF BC 8C [41 49] E4 B9 9F ...
length: 31 - 2 = 29 symbols

Recount after round 1. The two [41 49] occurrences have different neighbors, so no new repeated pair appears. Each of the other five repeated pairs still occurs twice.

Scroll horizontally to view all columns.

Sorted by lexicographic integer-pair order; 0x96 is less than 0x9C, 0xAC, 0xE5 and 0xE6
recount after round 1count
(96,9C)2
(9C,E6)2
(AC,A2)2
(E5,96)2
(E6,AC)2
text
Round 2
all five repeated pairs tie at count 2
lexicographically smallest integer pair = (0x96, 0x9C)

(96,9C) -> new token 257, displayed [96 9C]

喜 at each occurrence is now: E5 [96 9C]
length: 29 - 2 = 27 symbols

total after two rounds: 31 - 2 - 2 = 27

(ar,br)=arg max(a,b)countr(a,b)with lexicographic integer-pair tie break(a_r,b_r)=\operatorname*{arg\,max}_{(a,b)}\operatorname{count}_r(a,b)\quad\text{with lexicographic integer-pair tie break}

The following is runnable code, not pseudocode. It starts from the sentence's 31 bytes, trains two rounds using the stated counts and tie-break, then applies the two frozen merges to the same text. Decoding expands each learned symbol into bytes, joins the entire byte stream and only then decodes UTF-8.

toy_byte_bpe.py
from collections import Counter


def count_pairs(symbols: list[int]) -> Counter:
    return Counter(zip(symbols, symbols[1:]))


def choose_pair(pair_counts: Counter) -> tuple[int, int]:
    highest_count = max(pair_counts.values())
    tied_pairs = [
        pair
        for pair, count in pair_counts.items()
        if count == highest_count
    ]
    return min(tied_pairs)  # lexicographic integer-pair tie break


def merge_non_overlapping(
    symbols: list[int],
    pair: tuple[int, int],
    new_id: int,
) -> tuple[list[int], int]:
    merged: list[int] = []
    replacements = 0
    cursor = 0

    while cursor < len(symbols):
        if (
            cursor + 1 < len(symbols)
            and (symbols[cursor], symbols[cursor + 1]) == pair
        ):
            merged.append(new_id)
            replacements += 1
            cursor += 2
        else:
            merged.append(symbols[cursor])
            cursor += 1

    return merged, replacements


text = "我喜欢AI,AI也喜欢猫。"
training_symbols = list(text.encode("utf-8"))
history: list[tuple[tuple[int, int], int, int, int]] = []

assert len(training_symbols) == 31

for new_id in (256, 257):
    pair_counts = count_pairs(training_symbols)
    chosen_pair = choose_pair(pair_counts)
    before_length = len(training_symbols)
    training_symbols, replacements = merge_non_overlapping(
        training_symbols,
        chosen_pair,
        new_id,
    )
    assert len(training_symbols) == before_length - replacements
    history.append(
        (chosen_pair, new_id, replacements, len(training_symbols))
    )

assert history == [
    ((0x41, 0x49), 256, 2, 29),
    ((0x96, 0x9C), 257, 2, 27),
]


def encode_with_frozen_merges(
    value: str,
    merges: list[tuple[tuple[int, int], int, int, int]],
) -> list[int]:
    symbols = list(value.encode("utf-8"))
    for pair, new_id, _training_replacements, _training_length in merges:
        symbols, _ = merge_non_overlapping(symbols, pair, new_id)
    return symbols


encoded = encode_with_frozen_merges(text, history)
assert encoded == training_symbols
assert len(encoded) == 27

# A learned token stores a byte sequence, so decoding expands first.
token_bytes = {byte_id: bytes([byte_id]) for byte_id in range(256)}
for pair, new_id, _count, _length in history:
    left, right = pair
    token_bytes[new_id] = token_bytes[left] + token_bytes[right]

restored_bytes = b"".join(token_bytes[token_id] for token_id in encoded)
restored_text = restored_bytes.decode("utf-8", errors="strict")
assert restored_text == text

Scroll horizontally to view all columns.

Training determines the merge list; subsequent encoding replays it in fixed order.
stageselected pairreplacementsstream lengthwhat changed
initial bytes31Only base byte IDs 0..255
round 1(41,49)→256229Each occurrence of AI saves one position
round 2(96,9C)→257227The last two bytes of each 喜 save one position
encodingApply the merge creating 256, then the merge creating 257Depends on the inputStill 27 for this sentenceNo retraining or rule changes

Knowledge check

Why does round 2 choose (96,9C), and why is 喜 still not one token?

5. Unicode and UTF-8: Character Identity Is Not Serialized Bytes

Scroll horizontally to view all columns.

Course data table
Unit or notationPlain-language meaningExample
Unicode code pointAn identifier for a code point我 has the code point U+6211
UTF-8 byteStores/transmits text as bytes, each with a value from 0 to 255我 uses the three bytes E6 88 91
0x / hexadecimalA way to write integers; A–F represent 10–150x41=65; it does not mean the 41st token
token IDAn address assigned within a tokenizer's vocabularyThe number has meaning only under that vocabulary's mapping

On a first pass, distinguish character counts, byte counts and token counts. Python's ord, encode and hex can verify U+6211 or 0xE6; mental base conversion is not required. Later set notation and mapping arrows simply abbreviate input and output ranges.

Unicode assigns 我 the code point U+6211. UTF-8 serializes it as E6 88 91. ASCII A and I each use one byte. This sentence has seven Han code points using three bytes each, four ASCII code points using one each, and two full-width punctuation marks using three each: 21+4+6=31 bytes.

Scroll horizontally to view all columns.

All 13 code points of the running sentence and their exact UTF-8 bytes
visible code pointUnicodeUTF-8 hex bytesbyte count
我 (I)U+6211E6 88 913
喜 (first character of 喜欢)U+559CE5 96 9C3
欢 (second character of 喜欢)U+6B22E6 AC A23
AU+0041411
IU+0049491
U+FF0CEF BC 8C3
AU+0041411
IU+0049491
也 (also)U+4E5FE4 B9 9F3
喜 (first character of 喜欢)U+559CE5 96 9C3
欢 (second character of 喜欢)U+6B22E6 AC A23
猫 (cat)U+732BE7 8C AB3
U+3002E3 80 823
python
text = "我喜欢AI,AI也喜欢猫。"
utf8_bytes = text.encode("utf-8")

assert len(text) == 13
assert len(utf8_bytes) == 31
assert utf8_bytes.hex(" ").upper() == (
    "E6 88 91 E5 96 9C E6 AC A2 41 49 EF BC 8C "
    "41 49 E4 B9 9F E5 96 9C E6 AC A2 E7 8C AB E3 80 82"
)

Before encoding bytes, decide on normalization: which Unicode sequences should count as the same input? Text that looks like é can be one composed code point, U+00E9, or e (U+0065) followed by a combining acute accent (U+0301). Identity normalization preserves the distinction; NFC converts the latter to the composed form.

Scroll horizontally to view all columns.

Similar-looking text need not have identical code points or bytes. Normalization is a design choice.
visible textcode points before normalizationUTF-8 bytespolicy result
é(composed)[U+00E9]C3 A9Identity and NFC both preserve the composed form
e + ◌́(decomposed)[U+0065,U+0301]65 CC 81Identity preserves two code points; NFC produces U+00E9
AI(full-width)[U+FF21,U+FF29]EF BC A1 EF BC A9NFKC can convert it to ASCII AI; NFC does not perform this compatibility folding
unicode_normalization.py
import unicodedata

composed = "é"
decomposed = "e\u0301"

assert composed != decomposed
assert [f"U+{ord(ch):04X}" for ch in composed] == ["U+00E9"]
assert [f"U+{ord(ch):04X}" for ch in decomposed] == [
    "U+0065",
    "U+0301",
]
assert composed.encode("utf-8").hex(" ").upper() == "C3 A9"
assert decomposed.encode("utf-8").hex(" ").upper() == "65 CC 81"

# NFC makes these two canonically equivalent spellings identical.
assert unicodedata.normalize("NFC", decomposed) == composed

# NFKC also performs compatibility folding, which is a stronger choice.
assert unicodedata.normalize("NFKC", "AI") == "AI"

bi{0,,255},UTF8(我喜欢AI,AI也喜欢猫。){0,,255}31b_i\in\{0,\ldots,255\},\qquad \mathrm{UTF8}(\text{我喜欢AI,AI也喜欢猫。})\in\{0,\ldots,255\}^{31}
Concept sequence
  1. token IDs
  2. expand learned symbols back to byte symbols
  3. join one byte sequence
  4. UTF-8 decoder with a defined error policy
  5. 我喜欢AI,AI也喜欢猫。

Knowledge check

Why do A and 我 not both occupy one UTF-8 byte?

16. A Tokenizer Defines a Discrete Interface; It Does Not Understand Language

Mapping 猫 to 9 in w09-readable-v1 only identifies its vocabulary address. Predictive patterns such as 猫 frequently appearing with 喜欢 must be learned from token sequences through loss, backpropagation and neural-parameter updates. Neither the number 9 nor the BPE merge contains that knowledge.

Concept sequence
  1. Raw text: 我喜欢AI,AI也喜欢猫。
  2. tokenizer → integer IDs [B,T]
  3. embedding lookup → learned floats [B,T,C]
  4. Transformer → contextual floats [B,T,C]
  5. LM head → logits [B,T,V]
  6. decode selected IDs → text

Scroll horizontally to view all columns.

Course data table
tokenizer responsibilitymodel / training responsibility
normalization and segmentationlearn useful continuous features from examples
ordered Vocabulary and ID lookupcombine left context with Attention / FFN
special-token and fallback policyproduce and update vocabulary logits via loss
decode IDs under fixed artifactmodel probability, behavior, factuality and errors
ids:[B,T]E[]embeddings:[B,T,C]Transformercontext:[B,T,C]LM headlogits:[B,T,V]\mathrm{ids}:[B,T]\xrightarrow{E[\cdot]}\mathrm{embeddings}:[B,T,C]\xrightarrow{\mathrm{Transformer}}\mathrm{context}:[B,T,C]\xrightarrow{\mathrm{LM\ head}}\mathrm{logits}:[B,T,V]

Knowledge check

Where can the system learn that 猫 and 喜欢 often occur together?

17. Nine Things to Understand from Week 9

  1. A token is a tokenizer-defined computational unit, not necessarily a word. Each token normally occupies one model position.
  2. Code points, UTF-8 bytes and tokens are different units. Normalization determines whether certain Unicode differences are folded before encoding.
  3. Subwords trade off vocabulary size against sequence length. A token count belongs to a particular tokenizer.
  4. Encoding maps text to IDs; decoding returns text under the frozen tokenizer's policy.
  5. Tokenizer training constructs artifacts; encoding applies them without changing them.
  6. The tokenizer's ID mapping, normalization and special-token policy must match the model checkpoint.
  7. Causal, padding and loss-ignore masks respectively control future keys, invalid padding keys and targets that should be scored.
  8. Shifting a token stream by one position provides inputs and next-token targets.
  9. A batch stacks consecutive, correctly shifted windows into inputs and targets of shape [B,T].
textids [L]x,y [T]batch [B,T]model logits [B,T,V]\mathrm{text}\to\mathrm{ids}\ [L]\to x,y\ [T]\to\mathrm{batch}\ [B,T]\to\mathrm{model\ logits}\ [B,T,V]

Use AI to check statements 5 and 8. After BPE training saves (41,49)→256, encoding both AI occurrences applies the rule without recounting. In the readable-stream example at i=0, AI is the input at t=3 and its shifted target is ,.

Knowledge check

Use AI in the fixed sentence to explain statements 5 and 8.

18. Week 9 → Week 10: Pass the Same Batch to the Model

The English main path uses FIVE_WORD_TOKENIZER in course_examples/course_data.py: you=0, like=1, AI=2, study=3, we=4. It splits on whitespace and has no BOS/EOS/PAD/UNK. The documents remain “you like AI”, “we like you” and “you study AI”; inputs are [[0,1],[4,1],[0,3]] and targets are [[1,2],[1,0],[3,2]]. Use the English example package so its displayed tokens match this edition.

Scroll horizontally to view all columns.

Course data table
MaterialPurposePassed directly to the main MiniGPT?
Five-token tokenizer + T=2 windowsStep-by-step calculations and mechanism demonstrations in Weeks 6–12Yes. V=5 and the ID meanings remain unchanged.
w09-readable-v1 / character-demo / toy-byte-bpeCompare segmentation, unknown characters, special tokens and byte mergingNo. These are independent experiments; they do not reinterpret existing integers.
Character tokenizer for the final independent-document projectWeek 12 explicitly starts a new experimentReuses the model class, but creates a new vocabulary, configuration and checkpoint explicitly.
python
# Standalone: run from the English course_examples directory.
import torch
from course_data import FIVE_WORD_TOKENIZER, DEMO_DOCUMENTS, make_windows

x, y = make_windows(DEMO_DOCUMENTS, FIVE_WORD_TOKENIZER, block_size=2)
inputs = torch.tensor(x, dtype=torch.long)
targets = torch.tensor(y, dtype=torch.long)
print(inputs.tolist())   # [[0,1],[4,1],[0,3]]
print(targets.tolist())  # [[1,2],[1,0],[3,2]]
# Week 10 connects the model: logits, loss = model(inputs, targets)

The independent tokenization examples still matter: they help you decide when a different scheme is needed. They are not hidden prerequisites for the main code. You do not need an industrial BPE implementation to understand GPT training. Next week keeps these inputs/targets and replaces Bigram's score-row lookup with embeddings, positions, blocks and an output head.

Knowledge check

Should these six target IDs change when replacing Bigram with MiniGPT?