尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Tokenization 与数据形状全解析:train-llm-from-scratch 如何把文本变成可训练张量

Tokenization 与数据形状全解析:train-llm-from-scratch 如何把文本变成可训练张量 Tokenization 与数据形状全解析train-llm-from-scratch 如何把文本变成可训练张量【免费下载链接】train-llm-from-scratchA straightforward method for training your LLM, from downloading data to generating text.项目地址: https://gitcode.com/GitHub_Trending/tr/train-llm-from-scratch本篇技术指南以仓库文档 docs/foundations/tokenization.md 为核心脉络系统讲解 train-llm-from-scratch 中文本 → token id → 训练张量的完整数据链路从 tiktokenr50k_base子词分词器选型到预训练的长 token 流、SFT 的 loss 掩码、偏好学习的 chosen/rejected 对齐、以及 RL 可验证奖励的 prompt 形状。读完本文你将掌握每种训练阶段的数据形状设计动机、对应源码实现位置以及四类最典型的形状 bug的排查思路。核心认知Transformer 看到的只有整数 token id无论读入的是百科词条、GitHub 代码还是用户对话模型本身从不接触字符或单词。文本必须首先被切分成整数 token id才能进入张量运算。用仓库原文档的话说A tokenizer is the boundary between language and tensors——分词器是语言与张量之间的边界。因此理解 train-llm-from-scratch 的训练代码第一步不是看 Transformer 模块而是搞清楚数据在进入 src/models/transformer.py 之前经历了怎样的形状变换。这也正是 docs/foundations/README.md 把 Tokenization 列为六篇基础文档之首、并指向学习路径第 1 步的原因。上图展示的是本仓库 tokenization 的完整流水线原始文本经r50k_base编码为整数 id → 追加|endoftext|id 50256→ 切成context_length 1的固定窗口 → 拆分为x window[:-1]与y window[1:]。分词器选型复用 r50k_base而不是自训分词器本仓库没有训练自己的分词器而是直接复用 OpenAI 的r50k_base通过tiktoken库加载。三个关键数字项目值说明词表大小50304模型 embedding 层与输出 logits 的维度见 configs/base.json 中的vocab_sizeend-of-text 标记\|endoftext\|id50256r50k_base唯一的真正特殊 token同时充当文档分隔符、轮次终止符与生成停止符聊天角色标记纯文本分词器没有自定义聊天 token因此\|user\|、\|assistant\|以普通多 token 字符串形式出现由模型在 SFT 中像普通文本一样学习为什么词表是 50304 而不是 50257r50k_base本身只有 50257 个 id0–50256而配置里写的是 50304。从源码结构看这是padding 到 64 的倍数50257 → 50304。这一做法让 embedding 矩阵的维度对齐硬件友好的对齐边界许多 GPU kernel 对 64 的倍数更高效。代价是模型可能偶尔输出 50257–50303 这些填充 id因此 src/post_training/chat_template.py 中的decode函数做了防御性处理——丢弃 EOT 以及任何id 50256的 token否则一个未充分训练的模型吐出的 padding id 会直接让解码崩溃。为什么需要子词分词subword tokenization这是本仓库文档强调的设计权衡词级词表word-level无法在不爆炸式膨胀的情况下覆盖稀有名字、拼写错误、代码标识符、URL 和新造词——每出现一个新词就要新增一个 id。字符级词表character-level什么都能编码但同样的文本会被拉长数倍序列变长意味着上下文窗口能装下的语义量变少训练成本上升。子词分词subword折中方案——高频词整体占一个 token稀有词被拆成可复用的子词片段。原始的 BPEByte Pair Encoding思想很简单从最小的单元出发反复合并出现频率最高的相邻 pair最终得到一整套固定大小的可复用片段词表。这也是现代分词器包括r50k_base的共同基础。r50k_base名称中的50k即约 5 万词的词表规模其 BPE 原始思想可追溯至 Neural Machine Translation of Rare Words with Subword Units 一文docs/foundations/README.md 中有完整参考文献列表。预训练数据形状一条加 EOT 分隔的长 token 流预训练阶段的输入不是一个个独立样本而是一整条扁平的 token 数组[ [d_1, \text{EOT}, d_2, \text{EOT}, \ldots, d_N, \text{EOT}] ]即每篇文档 tokenize 后立刻追加一个 EOT文档之间无缝衔接。EOT 在这里承担文档边界的语义——模型需要学会在 EOT 处停止当前文档的输出。生成这条流的脚本scripts/prepare_pretrain_data.py 负责从 Pilepile-uncopyrighted数据源流式下载.jsonl.zst压缩分片、解压、批量 tokenize 并写入 HDF5for ids in enc.encode_ordinary_batch(docs): buf.extend(ids) buf.append(EOT_ID) if len(buf) WRITE_CHUNK: flush()其中几个值得注意的实现细节EOT_ID 50256即|endoftext|WRITE_CHUNK 8_000_000约 800 万 token 一批 flush 到 HDF5避免像早期版本那样每篇文档 resize 数据集造成的性能瓶颈ENC_BATCH 1024每 1024 篇文档做一次批量编码encode_ordinary_batch批量编码比逐篇编码快得多数据集以i4int32存储HDF5 dataset 为可扩展maxshape(None,)。实际用法脚本 docstring 中的示例# 验证集 PYTHONPATH. python scripts/prepare_pretrain_data.py --split val --out /ephemeral/data/pile_dev.h5 # 一个训练分片 PYTHONPATH. python scripts/prepare_pretrain_data.py --split train --num_shards 1 --out /ephemeral/data/pile_train.h5主要参数--splittrain/val、--num_shards使用的训练分片数、--raw_dir原始压缩文件缓存目录默认/ephemeral/data/pile_raw、--out输出 HDF5 路径、--max_tokens可选达到该 token 数后提前停止便于小规模调试。训练时如何切窗口context_length 1 与一步移位token 流准备好之后训练加载器 data_loader/data_loader.py 从 HDF5 中随机切取长度为context_length 1的窗口前context_length个 token 作为输入x后移一位的context_length个 token 作为目标y[ x [t_0, t_1, \ldots, t_{T-1}] ][ y [t_1, t_2, \ldots, t_T] ]这一位移位就是整个预测下一个 token任务的全部。加载器中对应的核心逻辑是n_examples (dataset_size - 1) // context_length random_indices example_idxs[counter:counterbatch_size] * context_length random_samples torch.tensor(np.array([dataset[idx:idxcontext_length1] for idx in random_indices])) xb random_samples[:, :context_length].to(device) # 输入 yb random_samples[:, 1:context_length1].to(device) # 目标右移一位注意n_examples (dataset_size - 1) // context_length中的-1因为每个窗口需要context_length 1个 token最后一个 token 无法作为窗口起点所以可用窗口数要减一。窗口起点取example_idxs * context_length保证窗口互不重叠每个 epoch 结束时重新 shuffle 索引实现数据随机化。SFT 数据形状tokens 加一张 loss_maskSFT 样本是对话。我们的目标是让模型学会生成 assistant 的回答而不是背下 user 的提问。因此 SFT 数据包含两个严格对齐的数组tokenstoken id 序列loss_maskassistant 补全位置为1prompt 位置为0。纯文本聊天模板由于r50k_base没有自定义聊天 token模板用普通文本标记角色来自 src/post_training/chat_template.py 的 docstring|user| {question}|endoftext||assistant| {answer}|endoftext|仓库实际支持的标记还包括SYSTEM_HEADER |system|\n以及为 RLVR/数学推理准备的纯文本结构标记think.../think与answer.../answer这些同样不是特殊 token只是 assistant 消息体的内容奖励解析与数据生成共用chat_template.py中导出的THINK_OPEN/THINK_CLOSE/ANSWER_OPEN/ANSWER_CLOSE常量作为单一事实来源。loss_mask 的对齐实现核心实现在src/post_training/chat_template.py的encode_chatheader_ids _encode_ordinary(_header_for(role)) ids.extend(header_ids) mask.extend([0] * len(header_ids)) content_ids _encode_ordinary(m[content]) is_completion role assistant ids.extend(content_ids) mask.extend([1 if is_completion else 0] * len(content_ids)) ids.append(EOT_ID) mask.append(1 if is_completion else 0)要点角色头|user|、|assistant|一律 mask0模型不应被训练去产出角色标记assistant 内容及其终止 EOT mask1模型要学习在正确位置停止当add_generation_promptTrue时对话以|assistant|\n结尾、无补全此时返回的 mask 全为 0——这正是 rollout/推理时使用的 prompt 形态src/post_training/inference.py 与 RL 阶段共用encode_prompt是对encode_chat(..., add_generation_promptTrue)的封装直接返回可以开始生成的 prompt token id。掩码与 token id 的对应关系文档原表SpanExampleMaskuser marker\|user\|0user questionWhat is 22?0assistant marker\|assistant\|0assistant answeranswer4/answer1assistant EOT\|endoftext\|1mask 如何进入损失函数src/post_training/sft.py 中的sft_loss是只比预训练多一张 mask的精确体现logits logits[:, :-1, :] targets tokens[:, 1:] mask loss_mask[:, 1:].to(logits.dtype) V logits.size(-1) ce F.cross_entropy(logits.reshape(-1, V).float(), targets.reshape(-1).long(), reductionnone) ce ce.view(targets.shape) * mask return ce.sum() / mask.sum().clamp(min1.0)同样是从位置 t 预测 t1的移位与预训练完全一致唯一区别是按loss_mask加权求和、再除以 mask 总和mask.sum().clamp(min1.0)保证空 mask 时不会除零。SFT 数据的生产与打包scripts/prepare_sft_data.py 从真实公开指令数据alpaca、dolly、gsm8k构造对话其中 GSM8K 会被重排成think推理/thinkanswer答案/answer结构让模型学习 RL 验证器会奖励的推理格式。之后用sft_loss同文件的pack_examples把 EOT 终止的样本拼接后切成context_length定长行输出两个对齐的 HDF5 数据集tokens与loss_mask均为(N, context_length)。对应的加载器 data_loader/sft_dataset.py 按 rank 分片遍历数据以支持 DDP并可选infiniteFalse做单遍评估。偏好数据形状prompt、chosen、rejected 三元组偏好学习reward model、DPO使用成对数据{prompt: ..., chosen: ..., rejected: ...}chosen 与 rejected 必须共享同一个 prompt。这一点至关重要DPO 和奖励建模比较的是回答质量而不是题目难度——如果两侧 prompt 不同模型完全可以靠这道题更难来区分学不到任何偏好信号。对每个 batch加载器构造两条 tokenize 后的序列[ \text{chosen ids} \text{chat}(prompt, chosen) ][ \text{rejected ids} \text{chat}(prompt, rejected) ]实现细节右侧 padding 真实长度追踪data_loader/preference_dataset.py 的实现有三个关键点两侧 pad 到同一长度chosen 和 rejected 用同一个L max(len)右侧补齐chosen 用EOT_ID填充、mask 用 0 填充使两侧可以共享一次 forward右 padding 是安全的文件 docstring 解释了原因——模型注意力是因果的最后一个真实 token 永远不会注意到它后面的 padding token且 response mask 会把 padding 位置在损失中清零记录真实序列长度chosen_len/rejected_len奖励模型需要读取最后一个真实 token的 hidden state 作为标量奖励而不是读 padding token 的位置。最后一点在 src/post_training/utils.py 的gather_last中落地def gather_last(values: torch.Tensor, seq_lengths: torch.Tensor) - torch.Tensor: idx (seq_lengths - 1).clamp(min0).long() return values[torch.arange(values.size(0), devicevalues.device), idx]这对应 InstructGPT 从序列模型读取标量奖励的惯例。配套的掩码约简工具还有masked_mean/masked_mean_per_rowDPO/RL 阶段对 token 级信号按 mask 求平均。偏好数据由 scripts/prepare_preference_data.py 从 Anthropic/hh-rlhf 与 HuggingFaceH4/ultrafeedback_binarized 生成输出preferences.jsonl训练与preferences_test.jsonl留出测试集用于评估奖励模型偏好准确率。两种数据源都经过清洗HH-RLHF 用rfind(\n\nAssistant:)切出最终回答ultrafeedback 取最后一轮消息内容并跳过 chosen rejected 的无效样本。RL prompt 形状prompt 加可验证的 gold 答案PPO 与 GRPO 需要生成后即可打分的 prompt——训练信号不能依赖人工标注或已学习的奖励模型而要靠验证器{prompt: Jan has 3 apples..., gold: 12}流程是模型生成回答 → 验证器从生成文本中提取最终答案→ 与gold比对。这种奖励被称为verifiable reward可验证奖励因为训练信号由规则/确定性验证给出无需额外标注。仓库中 src/post_training/rewards/ 目录parsing.py、verifiers.py即负责从生成文本中解析answer.../answer块并做字符串/数值比对这正是 docs/07_grpo.md 所述 RLVR 路径的数据前提。常见形状 bug 排查表数据形状的错误是看起来能跑、结果全错的重灾区。文档给出了一张实战性极强的对照表BugSymptomPrevention目标未移位模型学会了复制当前 token始终用tokens[:, 1:]预测tokens[:, :-1]SFT 损失包含 prompt token模型把容量浪费在预测用户输入上使用loss_mask且只对 mask 位置求平均缺少 EOT模型学不会何时停止文档与 assistant 消息后都要带 EOT偏好数据 prompt 不一致奖励/DPO 在比较不同的任务统一为共享 prompt chosen/rejected 回答padding 被当成奖励位置奖励模型在无意义的 pad hidden state 上训练记录seq_lengths用gather_last取最后一个真实 token逐一对照源码移位问题对应 data_loader/data_loader.py 的xb random_samples[:, :context_length]/yb random_samples[:, 1:context_length1]mask 问题对应 src/post_training/sft.py 的掩码平均EOT 问题对应 src/post_training/chat_template.py 中每个轮次强制追加EOT_IDpadding 位置问题对应 src/post_training/utils.py 的gather_last。这五类问题贯穿预训练到 RL 全链路是排查训练异常的第一张检查清单。下一步从 id 到向量文本一旦 tokenize 完成模型需要把整数 id 变成向量。这一步由 src/models/transformer.py 中的 token embedding 与位置 embedding 完成随后进入 docs/foundations/attention.md 讲解的因果自注意力核心运算。继续阅读下一篇基础文档 Decoder-Only Transformer即可把本文的数据形状与模型前向传播衔接起来随后沿 docs/foundations/README.md 的学习路径进入注意力、目标函数、优化系统与生成采样再回到 预训练、SFT、奖励模型、DPO、PPO 与 GRPO 的实战页面。【免费下载链接】train-llm-from-scratchA straightforward method for training your LLM, from downloading data to generating text.项目地址: https://gitcode.com/GitHub_Trending/tr/train-llm-from-scratch创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表