Current: Week 9

0%

Week 9

Week 9 - Tokenizer:从原始文字到训练 Batch

Key question怎样把“我喜欢AI,AI也喜欢猫。”稳定地变成模型可用的整数 IDs,同时不混淆 tokenizer training、encoding 与模型学习?

Learning objectives

  • 比较 character、word、UTF-8 byte 与 subword 四种单位对 V、L、覆盖能力和计算成本的影响。
  • 区分 code point、UTF-8 byte、BPE symbol、token 与 token ID,并说明 normalization 为什么属于 tokenizer 协议。
  • 手算 byte-level BPE 的两轮 pair count、tie break、merge 与长度变化。
  • 区分 tokenizer training 与 encoding,并把 tokenizer artifacts 与 model checkpoint 绑定。
  • 沿 text → tokens → stream [L] → shifted examples [T] → padded batch [B,T] 完成一条可审计的数据路径。
  • 分别构造 causal mask、padding mask 与 loss mask,解释它们各自阻止哪一种错误。
  • 在 Week 10 前明确结束 w09-readable-v1,并切换到不兼容的 mini-gpt-v1。

145 min estimated reading time

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

本周重点不是实现四套 tokenizer,而是让数据可靠地进入已经学过的模型。主线沿用五词表与 course_data.py;字符和 BPE 是解释替代方案的独立实验。先学会一个可靠入口,再比较更复杂方案。

Scroll horizontally to view all columns.

Course data table
学习单元本次解决的问题
一:稳定的文字接口固定分词规则、ID 顺序和未知词行为;重复编码不能重新随机编号。
二:真的构造数据同一数据函数从文档构造 T+1 窗口;先分训练/验证,再各自切窗。
三:判断实验是否可信检查文档重复、长片段重叠和有效目标数,分清流程演示与独立验证。
四:按需要比较方案先可读字符 BPE 的两轮合并,再选读字节、特殊 token 和 padding;不要求这些成为五词模型的前置。

运行 python week09_data_protocol.py;它直接产生 Week 10 能接收的五词 inputs/targets。最终项目会明确新建字符词表,不会把另一套 ID 悄悄送进已有 checkpoint。

建议阅读、手算、改代码交替进行,每个单元可拆成几次完成。章节编号保留用于旧链接和回查;按页面从上到下的新顺序学习,不需要按旧编号来回跳转。

Week 9 核心目标:让文字与模型共享一份稳定协议

Tokenizer 不是语言模型。它规定 normalization、怎样切分、Vocabulary 中有哪些 token、每个 token 对应哪个 ID,以及 IDs 怎样 decode。它在模型前把文字变成整数,也在生成后把整数还原为文字;真正可学习的 contextual patterns 位于 embedding 与 Transformer 参数中。

Scroll horizontally to view all columns.

Course data table
概念本章含义固定句中的例子
TokenTokenizer 发出的一个离散计算单位可以是 我、喜欢、AI,也可能是某个 byte
Vocabulary允许 token 与 ID 的有限清单w09-readable-v1 有 V=11 个 entries
Token ID某个 entry 的整数地址在该快照中 AI 的地址是 6
Tokenizernormalization、split、encode、decode 与 special-token policy 的整体协议将固定句映射成下方 stream

Scroll horizontally to view all columns.

w09-readable-v1 的完整 ordered Vocabulary(V=11)
IDtokenrole
0<BOS>文档开始
1<EOS>文档结束
2<PAD>补齐 batch 长度
3<UNK>一种有损的未知项策略
4内容 token
5喜欢内容 token
6AI内容 token
7内容 token
8内容 token
9内容 token
10内容 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.

同一个数字只能在自己的 artifact 内解释;本章不会把这几套 IDs 混在一起
教学 artifact使用位置基本单位 / ID space学习目的何时结束
character-demo第 4 节9 个已知 code points,IDs 0..8看清 encode、decode、dtype 与 [L]第 4 节结束
toy-byte-bpe第 5–7 节UTF-8 bytes 0..255,再建立 256、257手算 pair count、merge、冻结与还原第 7 节结束
w09-readable-v1第 8–18 节可读 pieces,V=11,IDs 0..10走完 special stream、window、mask 与 batchWeek 9 结束
mini-gpt-v1Week 10 起五个 tokens,V=5,IDs 0..4连接 canonical MiniGPT按 Week 10–12 协议
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

为什么 tokenizer artifacts 必须与 model checkpoint 一起版本化?

1. Token 不等于 Word:同一句话可以有四种边界

对“我喜欢AI,AI也喜欢猫。”,同一 raw text 在不同规则下可以发出不同数量的单位。下面的 word-like row 依赖指定 segmenter;subword row 恰好看起来一样,只是这个教学词表训练后的结果,并非 subword 的定义。

Scroll horizontally to view all columns.

同一固定句的四种明确切分
schemeemitted unitscount Limmediate benefitimmediate cost
character / code point[我, 喜, 欢, A, I, ,, A, I, 也, 喜, 欢, 猫, 。]13这些 code points 无需 word dictionary喜欢 与 AI 都被拆开,sequence 较长
word-like(指定 segmenter)[我, 喜欢, AI, ,, AI, 也, 喜欢, 猫, 。]9短而且容易阅读中文边界、未见词和规范差异需要额外规则
UTF-8 byteE6 88 91 … 41 49 … E3 80 8231固定 256 个 base symbols 可覆盖任意有效 UTF-8 文本最长;单个 byte fragment 不一定可独立阅读
subword(进一步训练后的示意)[我, 喜欢, AI, ,, AI, 也, 喜欢, 猫, 。]9常见片段短,少见文本仍可拆成更小单位merge、normalization 与 ID 都属于该 tokenizer 版本
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

13 个 Unicode code points 是否证明所有 tokenizer 都会产生 13 个 IDs?

2. 为什么不简单按 Word 切:覆盖与长度的两难

在固定句“我喜欢AI,AI也喜欢猫。”中,word-like tokenizer 可把 喜欢 与 AI 保持为整体;byte base 则一定能表达这些有效 UTF-8 bytes,但需要 31 个 positions。Subword 从覆盖能力较强的小单位出发,再把训练语料中的常见相邻片段合并。

Scroll horizontally to view all columns.

Course data table
choiceVocabulary / coveragesequence length主要代价
whole word需要很多完整词;缺项时常依赖 <UNK> 或额外 fallback常见词通常较短V 大或 unknown information loss
character / bytebase V 较小;byte base 可覆盖有效 UTF-8固定句为 13 / 31更多 positions 与更弱的人类可读性
subwordcommon pieces + smaller fallback pieces通常介于两端;本示意为 9需要冻结训练出的 merges 与 routing rules
self-attention pairwise workO(L2C)\mathrm{self\text{-}attention\ pairwise\ work}\approx O(L^2C)

Knowledge check

Subword 相比有限 whole-word Vocabulary 解决了什么问题?

3. Vocabulary Size 的 Trade-off:V 与 L 会同时改变系统

固定句的 13、9、31 positions 只显示方向,不证明“9 最优”。较大的 V 可以为常见片段分配单独 entry,较小的 V 则常要用更多 positions 表达同一文字。模型仍会在每个 position 输出 V 个 logits。

Scroll horizontally to view all columns.

Course data table
design directionvocabulary-facing tablessame text lengthcontext / Attention implication
smaller V较少 rows 与 output candidates往往更大 L固定 T 能覆盖的原文可能更少,位置对更多
larger V较多 rows 与 output candidates常见片段往往更短每一步的 LM-head computation / memory 增大
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.

这里只计算两张 vocabulary-facing weight tables;不包含 Transformer blocks,也不声称这是某个真实模型配置
exampleinput embedding VCbias-free untied LM head VCtotal 2VC
本章教学尺寸 V=11, C=411×4=4411×4=4488 parameters
示意规模 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 的双重作用:它一方面决定 embedding / LM-head 的表有多宽,另一方面决定每个 position 要比较多少个 next-token candidates;而 tokenizer 通过改变 L,又会改变要处理多少个 positions。

Knowledge check

C 固定且 V 翻倍,哪两个常见 interfaces 会变大?

4. Character Tokenizer:先看清 Encode / Decode 闭环

对“我喜欢AI,AI也喜欢猫。”,这个简化 tokenizer 把每个 Unicode code point 当作 token。它适合检查闭环,却不等于 production 最佳方案,也不能自动处理 Vocabulary 外 code point、grapheme cluster 或 normalization 差异。

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

为什么明知本句会有 13 个 tokens,仍值得先实现 character tokenizer?

8. Tokenizer Training 与 Text Encoding 不同

Scroll horizontally to view all columns.

Course data table
phaseinputchanges artifacts?output
tokenizer trainingtraining corpus documents是:选择 normalization、base vocab、merges、special IDsversioned artifact A
text encodingone text + frozen AIDs in 0..V_A−1
model trainingbatches of frozen-A IDs否:A 不变;model parameters 改变updated 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}}

下面把 w09-readable-v1 写成一个最小、具体的 teaching artifact,而不是调用未配置的通用 library tokenizer。它使用 identity normalization、显式 ordered Vocabulary,以及固定的 longest-first content routes;无法匹配的一个 Unicode code point 映射为 <UNK>。这个 routing 是教学约定,不声称由前面两轮 toy BPE 直接产生。

Concept sequence
  1. ① TOKENS 冻结 ordered Vocabulary 与 special IDs
  2. ② CONTENT_ROUTES 规定最长优先的内容切分
  3. ③ segment_content 只返回 token strings
  4. ④ encode_content 把 strings lookup 成 IDs,不添加边界
  5. ⑤ encode_document 恰好添加一次 BOS 与 EOS
  6. ⑥ decode 按同一 ID table 重建文字

第一次阅读下面代码时,只追踪固定句从 cursor=0 到 cursor=len(text) 的移动;第二次再检查 special-token ownership 与 error branches。这样可以先看懂数据流,再看防御性细节。

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 与 decoding 都只应用 frozen artifact,不产生新 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

固定 BPE rule (41,49)→256 后,再 encode 固定句会改变这条 rule 吗?

9. Tokenizer 与 Model 必须绑定:Shape 相同仍可能完全错位

w09-readable-v1 把“我喜欢AI,AI也喜欢猫。”中的 喜欢 编为 5、AI 编为 6。若另一个同样 V=11 的 tokenizer 把两者交换,输入 6 会读取原本为 AI 训练的 embedding row,却被新系统解释成 喜欢;输出 column 6 也会 decode 成错误 piece。

Scroll horizontally to view all columns.

Tokenizer artifact 与 model checkpoint 是一个兼容性 bundle
bundle itemwhy model loading needs it
ordered token list / Vocabulary定义每个 embedding row 与 LM-head column 的名称
ordered BPE merges决定新文字如何组合成 Vocabulary entries
normalizer + pre-tokenizer决定 merge 前看到的 symbols 与 boundaries
special-token IDs / insertion policy定义 BOS、EOS、PAD、UNK 的控制地址与何时出现
tokenizer version / content hash在加载前做精确 compatibility check
model config + weights声明 V、C、context 等 shape,并保存已学习参数
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

为什么两个 V=11 的 tokenizer 仍可能不能共享同一个 checkpoint?

12. 把 Corpus 变成 Token Tensor:保留顺序与文档边界

只有一份固定文档“我喜欢AI,AI也喜欢猫。”时,w09-readable-v1 直接得到 [0,4,5,6,7,6,8,5,9,10,1],shape [11]。它仍是一条 chronological stream,不是 batch,也没有 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.

边界 token 让模型看见边界,但是否允许跨边界 Attention 或 loss 仍是独立的数据管线决策
multi-document policyboundary example会训练哪种 transitiontrade-off
连续串接 boundary tokens…内容,<EOS>,<BOS>,下一篇…内容末尾→EOS,也可能包含 EOS→BOS实现简单;需要接受控制 token 间的跨文档 transition
每篇文档内单独切 windowswindow 不跨 document只监督同一文档内与内容末尾→EOS语义边界清楚;短尾部可能浪费 positions
packing + boundary-aware masks一张矩形 batch 放多篇文档用 attention/loss policy 阻断不需要的跨文档连接利用率高,但实现与审计更复杂

本章单文档 stream 不会触发上述差异。扩展到多文档时,必须在数据规范中选定一种 policy;不能只插入 EOS 就声称所有跨文档信息流都已经被阻断。

Knowledge check

固定句刚变为 stream_tensor 时 shape 与 dtype 是什么?

13. Train / Validation Split:先隔离 Documents,再切 Windows

单句“我喜欢AI,AI也喜欢猫。”只用于 pipeline 手算,不能冒充有意义的 train/validation experiment。真实 corpus 应先分 documents,再冻结 tokenizer,最后分别建立 train_stream 与 val_stream;validation forward 不调用 backward 或 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}}

这是 provenance guarantee,不是 value-level deduplication:两份独立 documents 可能都包含常见短语或 boilerplate,因此完全相同的 ID window values 可以自然地分别出现在 train 与 validation。若任务还要求去重,必须另外声明 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 虽不参与 backward,却会通过选模型、调超参数影响你的决策。若反复针对同一 validation 改进,再把它当完全没见过的最终测验,会过度乐观。正式比较可另留 test split,最后才使用;本课三句玩具数据不足以支持这种质量结论。

Knowledge check

为什么应在 stream concatenation 之前按 documents 分割?

14. 从 Token Stream 取一个 Example:Target 必须右移一位

固定 stream s=[0,4,5,6,7,6,8,5,9,10,1] 来自“我喜欢AI,AI也喜欢猫。”。选择 start i=0 与 context length T=4;x 读四个连续 IDs,y 从下一个位置读同样四个 IDs。

Scroll horizontally to view all columns.

w09-readable-v1 的完整一维 stream s,L=11
stream indexIDtoken
00<BOS>
14
25喜欢
36AI
47
56AI
68
75喜欢
89
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.

四个 aligned next-token tasks;target 不是提前提供给同一 query
position tinput tokentarget next tokencausal model may use
0<BOS>[<BOS>]
1喜欢[<BOS>, 我]
2喜欢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

在 i=0 的 example 中,input ID 5(喜欢)对应哪个 target?

15. 构造一个 Batch:随机选择 Rows,Rows 内保持连续

Python s[a:b] 包含位置 a,不包含 b,所以恰有 b−a 项。T=4 的一道训练序列需要连续五个源 ID:前四个给 input,后四个给 target,两段重叠三项。随机数的 high 也不包含上界;把最后合法起点先写出来,再决定 randint 的 high。

继续只用“我喜欢AI,AI也喜欢猫。”的 w09-readable-v1 stream。固定 B=2、T=4,并选择 legal starts [0,5];随机性只选择 row 从哪里开始,每个 row 内仍按原 stream 顺序前进。

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.

每行需要 T+1 个 source IDs,才能得到 T 个 inputs 与 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

为什么每个 row 要从 source 读取 T+1 个 IDs?

10. Special Tokens:由谁添加边界必须只有一个答案

在 Week 9 的可读快照中,0..3 是 reserved specials,内容 entries 从 4 开始。它们是 model-visible Vocabulary entries,不是输入中看见字符 <EOS> 就自动产生的魔法。是否显示、mask、停止生成或忽略 loss 都需要调用方与模型 API 明确约定。

Scroll horizontally to view all columns.

Course data table
IDspecial tokenrole in w09-readable-v1typical handling
0<BOS>文档起点 / 初始 contextencode_document 在最前添加一次
1<EOS>文档终点encode_document 在最后添加一次;generation 可把它当 stop candidate
2<PAD>把不等长 rows 补成 rectangle通常在 attention / loss 中 mask
3<UNK>word-only fallback 的 unknown marker有损;不是 byte fallback 的必需品

Scroll horizontally to view all columns.

Course data table
API result for 我喜欢AI,AI也喜欢猫。tokensIDs
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

为什么 encode_document(text)+[EOS] 会破坏本节约定?

11. Padding 与 Attention Mask:矩形 Shape 不等于有效数据

Scroll horizontally to view all columns.

Course data table
控制项先问一句话
causal mask这个位置能否偷看后面的真实答案?
padding mask这个位置是不是为了凑矩形而填进去的?
loss mask这一次预测有没有有效答案,是否要计分?

三个控制可以同时存在,不能因名字里都有 mask 就交换用法。−100 只是本例“这题不计分”的记号,不属于词表,绝不能拿去查询 embedding。

保留主句“我喜欢AI,AI也喜欢猫。”作为 row 0,并用较短的“我喜欢AI。”作为 row 1 的 padding illustration。两者均由 w09-readable-v1 的 encode_document 产生;本表临时把 batch width 记作 T_pad=11,避免与后面 training-window T=4 混淆。

Scroll horizontally to view all columns.

右侧 padding 后 input_ids 与 attention_mask 均为 [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 ID2占住矩形 tensor 的 unused slot
attention_maskreal=1, pad=0禁止 query 把 PAD key 当作上下文;具体 API 还会处理 padded queries
causal maskj≤t禁止任何 query 读取 future key,不判断 PAD
loss ignore index-100让 padded target 不计入 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}}}

接下来把规则落到一个较短的 T_mask=7 next-token batch。Row 0 从主句取连续八个 source IDs,形成七组 input→target;Row 1 的完整短文档只有六个 IDs,所以 input 补一个 PAD,同时把“EOS 后没有本文件内 next token”和 PAD 对应的 targets 都设为 -100。

Scroll horizontally to view all columns.

ID 2 是模型输入中的 PAD;-100 不是 Vocabulary ID,而是本例交给 Cross Entropy 的 ignore_index sentinel
rowinput_ids [7]attention_mask [7]next-token targets [7]
0: 主句 prefix[0,4,5,6,7,6,8][1,1,1,1,1,1,1][4,5,6,7,6,8,5]
1: 完整短文档[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.

三个控制作用于不同对象:可见的时间方向、有效的 key,以及需要计分的 target
control它回答的问题row 1 的具体效果
causal maskquery t 能否读取 future key j>t?t=4(。)只能读取 key positions 0..4
padding maskquery 能否把无效 PAD key 当作上下文?所有 query 都不能读取 position 6 的 PAD key
loss mask / ignore_index这个 position 的预测要不要计分并产生 gradient?positions 5、6 的 targets=-100,不参与平均 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

为什么 input_ids.shape=[2,11] 仍不足以说明这个 padded batch 正确?

6. 未见 Token:先决定信息如何保留

假设 w09-readable-v1 的 word-only variant 忘了把固定句中的 猫 放进 Vocabulary:它可以输出 <UNK> 的 ID 3,但所有未知片段都会坍缩到同一地址,无法 exact decode 猫。byte-capable fallback 则能保留 E7 8C AB,随后仍可重建原文。

Scroll horizontally to view all columns.

Course data table
policy猫 不可 direct lookup 时的 outputround-tripcost / behavior
<UNK>[3]有损:只能还原为 unknown marker短,但不同未知片段共享一个 ID
subword / byte fallback一个或多个已知 smaller-piece IDs若 normalization/byte policy lossless,可保留原始文字k 可能大于 1,sequence 变长
explicit error不产生 IDs调用方必须处理失败适合不允许自动 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}

固定快照实际包含 猫,所以正常 encoding 是 ID 9。malformed byte sequence 是另一类问题:它需要 UTF-8 replacement/error policy,不能与“有效但未见的词”混为一谈。

Knowledge check

为什么 byte fallback 通常比把 猫 替换成 <UNK> 更少丢失信息?

7. BPE 的核心直觉:真的手算两轮 Merge

这是一个无 pre-token boundaries 的 toy byte-level BPE training:固定句的每对相邻 bytes 都可参与计数。只列频率至少为 2 的初始 pairs;(9C,E6) 跨越 喜 的最后 byte 与 欢 的第一 byte,并在两次 喜欢 中出现。

Scroll horizontally to view all columns.

完整 initial repeated-pair inventory;六组 count 都是 2
initial adjacent byte paircountwhy repeated
(41,49)2两次 AI
(E5,96)2两个 喜 的前两 bytes
(96,9C)2两个 喜 的后两 bytes
(9C,E6)2两次 喜→欢 的 code-point boundary
(E6,AC)2两个 欢 的前两 bytes
(AC,A2)2两个 欢 的后两 bytes
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

Round 1 后必须重新计数。两个 [41 49] 的邻居分别不同,因此没有产生新的 repeated adjacent pair;其余五个 repeated pairs 仍各出现两次。

Scroll horizontally to view all columns.

按 integer-pair lexicographic order 排列;0x96 小于 0x9C、0xAC、0xE5、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}

下面的程序不是伪代码:它从固定句的 31 个 bytes 开始,按本节声明的 count 与 tie rule 训练两轮,再把这两条 frozen merges 应用于同一句文字。最后先展开每个 learned symbol 的 bytes,拼接完整 byte stream,再统一做 UTF-8 decode。

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.

训练决定 merge list;之后的 encoding 只按固定顺序重放它
stageselected pairreplacementsstream lengthwhat changed
initial bytes31只有 base byte IDs 0..255
round 1(41,49)→256229两次 AI 各缩短一个 position
round 2(96,9C)→257227两次 喜 的后两个 bytes 各缩短一个 position
encoding按 256 再 257 的顺序应用由输入决定本句仍为 27不重新训练、不改变 rules

Knowledge check

为什么 Round 2 选择 (96,9C),而且此时 喜 仍不是一个 token?

5. Unicode 与 UTF-8:字符身份不等于序列化 Bytes

Scroll horizontally to view all columns.

Course data table
单位或记号人话说明例子
Unicode code point给一个字符身份编号“我”的编号写作 U+6211
UTF-8 byte把字符存储/传输成每项 0–255 的字节“我”是三个字节 E6 88 91
0x / 十六进制一种写整数的方法;A–F 表示 10–150x41=65,不是第 41 个 token
token ID分词器自己分配的词表地址具体数值只在那份词表内有意义

第一遍会区分“字符数、字节数、token 数”就够了。U+6211、0xE6 等可交给 Python ord、encode、hex 验证;不要求心算进制转换。后面的集合、映射箭头也只是把输入范围和输出范围写短。

Unicode 给“我”分配 U+6211;UTF-8 将这个 code point 序列化为 E6 88 91。ASCII 的 A 与 I 各占一个 byte,而本句中的七个 Han code points 和两个全角标点各占三个 bytes,因此总计 21+4+6=31。

Scroll horizontally to view all columns.

“我喜欢AI,AI也喜欢猫。”的全部 13 个 code points 与精确 UTF-8 bytes
visible code pointUnicodeUTF-8 hex bytesbyte count
U+6211E6 88 913
U+559CE5 96 9C3
U+6B22E6 AC A23
AU+0041411
IU+0049491
U+FF0CEF BC 8C3
AU+0041411
IU+0049491
U+4E5FE4 B9 9F3
U+559CE5 96 9C3
U+6B22E6 AC A23
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"
)

在变成 bytes 之前,还必须回答 normalization:哪些 Unicode 序列应被当作同一种输入?例如屏幕上都像 é 的文字,可以由一个 composed code point U+00E9 表示,也可以由 e(U+0065)加 combining acute accent(U+0301)表示;identity policy 会保留差异,NFC 会把后一种规范化成前一种。

Scroll horizontally to view all columns.

看起来接近的文字不保证拥有相同 code points 或 bytes;采用哪种 normalization 是设计选择
visible textcode points before normalizationUTF-8 bytespolicy result
é(composed)[U+00E9]C3 A9identity 与 NFC 都保留 composed form
e + ◌́(decomposed)[U+0065,U+0301]65 CC 81identity 保留两点;NFC 变为 U+00E9
AI(full-width)[U+FF21,U+FF29]EF BC A1 EF BC A9NFKC 可变为 ASCII AI;NFC 不做这项兼容折叠
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

为什么 A 与 我 不是各占一个 UTF-8 byte?

16. Tokenizer 不负责理解语言:它只定义离散接口

w09-readable-v1 把固定句中的 猫 映射为 9,只表示 ordered Vocabulary 的第 9 个地址。任何“猫常与喜欢共同出现”的 predictive pattern,都要由 token sequences、loss、backprop 与 neural parameters 学习;数字 9 和 BPE merge 本身不含这条知识。

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

系统可以在哪里学到 猫 与 喜欢 经常共同出现?

17. Week 9 最应该理解的 9 件事

  1. Token 是 tokenizer-defined compute unit,不一定是 word;一个 token 通常占一个 model position。
  2. Code point、UTF-8 byte 与 token 是不同单位;normalization 决定编码前是否折叠某些 Unicode 差异。
  3. Subword 在 Vocabulary size 与 sequence length 之间折中;token count 属于具体 tokenizer。
  4. Encode 把 text 映射到 IDs;decode 按 frozen tokenizer policy 返回 text。
  5. Tokenizer training 构建 artifacts;encoding 只应用它们,不改变它们。
  6. Tokenizer ID mapping、normalization 与 special-token policy 必须匹配 model checkpoint。
  7. Causal mask、padding mask 与 loss ignore mask 分别控制未来 keys、PAD keys 与需要计分的 targets。
  8. Token stream 通过右移一位提供 inputs 与 next-token targets。
  9. Batch 把连续、已右移的 windows stack 为 inputs,targets:[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]

用 AI 自查第 5 与第 8 条:BPE training 学到 (41,49)→256 后,encoding 两次出现的 AI 只应用该 rule,不重新计数;在 i=0 的可读 stream example 中,AI 是 t=3 的 input,右移 target 是 ,。

Knowledge check

请用固定句中的 AI 解释第 5 与第 8 条。

18. Week 9 → Week 10:把同一批输入直接交给模型

主线统一使用 course_examples/course_data.py 中的 FIVE_WORD_TOKENIZER:我=0、喜欢=1、AI=2、学习=3、猫=4。按空格切分,无 BOS/EOS/PAD/UNK。原文仍是“我 喜欢 AI”“猫 喜欢 我”“我 学习 AI”;输入 [[0,1],[4,1],[0,3]],目标 [[1,2],[1,0],[3,2]]。

Scroll horizontally to view all columns.

Course data table
材料用途是否直接给主线 MiniGPT
五词 tokenizer + T=2 窗口Week 6–12 的逐步计算与机制演示是。V=5,ID 含义一直不变。
w09-readable-v1 / character-demo / toy-byte-bpe比较切词、未知字符、特殊 token 与字节合并否。它们是独立实验,不重解释已有整数。
最终独立文档项目的字符 tokenizerWeek 12 明确开启一个新实验使用同一模型类,但显式新建词表、配置和 checkpoint。
python
# 可独立运行:在 course_examples 目录中执行。
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 接入:logits, loss = model(inputs, targets)

本章的独立切词示例没有消失:它们帮助你判断何时需要新方案,但不是主线代码的隐含前置。你不必实现工业 BPE,才能看懂 GPT 训练。下一周保留这些 inputs/targets,只把 Bigram 的一行查分换成 embedding、位置、多个 block 和输出层。

Knowledge check

从 Bigram 换成 MiniGPT 后,这六个 target ID 应该改变吗?