从零训练一个小模型-nanoGPT 模型训练 (一)数据预处理

张开发
2026/4/20 1:50:29 15 分钟阅读

分享文章

从零训练一个小模型-nanoGPT 模型训练 (一)数据预处理
最近在学习模型训练实际上在大模型训练上我并没有深厚的背景通过视频课程和b站上的一些分享开始入门。由于我非神经网络这些相关的专业所以想把自己学习的过程和经验总结记录下来一方面自己可以巩固总结知识另一方面对于“外行”如何入手把这些经验给到有需要的人。这篇文章用较容易理解的方式去开始去训练一个小模型。我们通过一个项目来直接进入模型训练了解GPT模型的核心逻辑这个是github上的项目地址https://github.com/karpathy/nanoGPT.git这个项目是很简洁、高效的是学习 Transformer 和 GPT 原理的最佳实践项目之一。代码量极少相比于大型工业级框架nanoGPT的代码量非常少几百行核心代码去除了复杂的工程封装保留了 GPT 模型最核心的逻辑。正在学习大语言模型LLM这是一个非常好的切入点能让你看到模型背后的代码是如何运作的。项目给了三个运行方式对于侧重点也是有所不同。train_shakespeare_char从随机参数开始没有加载现成 GPT-2用的是字符级数据表示所以它只是“从零训练一个小模型”。train_gpt2把模型结构设成 GPT-2 风格然后从零开始训练。它不是加载 OpenAI 已经训好的GPT-2而是自己重新训一个。所以它叫复现 GPT-2从零训练 GPT-2 风格模型不是微调。finetune_shakespeare 是微调先把现成 GPT-2 XL 的参数加载进来再拿 Shakespeare 数据继续训练。符合“微调”的定义。如果目标是学训练流程 - 看 train_shakespeare_char学怎么从零训大语言模型 - 看 train_gpt2学怎么在现成模型上继续训练 - 看 finetune_shakespeare我这次要学习训练流程可以直接在cpu上运行我会根据自己实践和理解分为几个部分来总结让大家也有个对模型逻辑性、结构性的认识。一、从目录理解项目可以把这个 nanoGPT 项目先粗分成 5 块——data/准备训练数据——config/配置文件不同训练/评估场景的参数模板——根目录几个 .py真正执行训练、采样、建模、配置解析、性能测试——assets/文档图片资源一般前端静态资源文件图片、音频、视频、字体——.ipynb实验/分析笔记不是主流程——train.py主训练脚本。这是项目最重要的入口。它负责读取 train.bin / val.bin、按 batch 取数据、初始化模型、训练循环、评估 loss、保存 checkpoint你可以把它理解成整个训练流程的总控。——model.pyGPT 模型定义。它负责定义token embedding、position embedding、Transformer block、self-attention、MLP最后的输出层、forward 逻辑从预训练 GPT-2 加载参数、你可以把它理解成“模型长什么样”——sample.py推理 / 采样脚本。它负责加载你训练好的 checkpoint、或直接加载 GPT-2、输入一个 prompt、让模型继续生成文本比如训练完 Shakespeare 后用它生成新台词。你可以把它理解成拿训练好的模型出来说话开始使用模型——configurator.py简易配置加载器。它负责读取 config/*.py、读取命令行里的 --batch_size32 这种参数、覆盖 train.py 或 sample.py 里的默认值你可以把它理解成“把配置文件和命令行参数注入到脚本里”——bench.py性能测试脚本。它不是正式训练入口而是用来测一轮前向/反向大概多快、GPU 利用情况如何、数据读取和模型计算速度如何你可以把它理解成“压测 / 跑分脚本”。如果你是刚入门建议按这个顺序看这几个文件data/shakespeare_char/prepare.pyconfig/train_shakespeare_char.pytrain.pysample.pymodel.py我们第一篇先看如何准备数据data/这个文件夹二、准备数据/数据预处理 data文件夹data文件夹下有几个文件在训练之前先跑 prepare.py文件它在做的是数据预处理还没有训练神经网络。把人能读的英文文本预处理成模型能高效读取的数字数据。模型本质上不会直接理解英文字符它只能处理数字。所以要先做这件事读入原始文本 input.txt、统计里面出现过哪些字符、给每个字符分配一个编号、把整篇文本从“字符串”变成“数字串”、保存成训练时方便高速读取的格式、这一步可以叫数据预处理。1. 读入原始文本读入原始剧本文本就是把 input.txt 整个读进来得到一个超长字符串比如概念上像这样First Citizen:\nBefore we proceed any further, hear me speak.\n...注意这里不是“按句子”读也不是“按单词”读而是整个文件当成一个长字符串。如果你把它拆成一个个字符就会变成F, i, r, s, t, 空格, C, i, t, i, z, e, n, :, 换行, B, e, f, o, r, e, ...这就是“字符序列”。2. 建立“字符 - 整数”的对照模型不能直接吃 F、i、空格、\n所以先要做编号。假设文本里只出现这几个字符a, b, c, 空格那就可能编号成 - 0a - 1b - 2c - 3在你的代码里stoi string to integer字符转数字itos integer to string数字转字符也就是stoi[a] 1itos[1] a为什么要两张表训练前把文本转成数字用 stoi训练后把模型输出的数字转回字符用itos为什么是 65 个字符因为这份 tiny Shakespeare 文本里总共只出现了 65 种不同字符。比如包括大写字母 A-Z小写字母 a-z空格换行标点符号如 , . : ; ? !少量别的符号代码里也打印了这个结果length of dataset in characters: 1,115,393all the unique characters:!$,-.3:;?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzvocab size: 65train has 1,003,853 tokensval has 111,540 tokens这里的 vocab size: 65在这个字符级任务里其实就是这个模型只需要学会处理 65 种可能出现的字符。把整本书变成数字串比如原文是hi!假设编号是! - 0h - 1i - 2那编码后就变成[1, 2, 0]这个过程就是encode(hi!) - [1, 2, 0]大模型处理文本的底层逻辑这个过程想象成把人类语言翻译成机器密码更偏向encoding编码阶段文本 到token ID离散整数EncodingTokenizationNumericalization (转ID)Special Tokens (特殊标记)1Tokenization分词切分“把长句子切成小块”这是第一步。模型不能一口吃成个胖子它需要把连续的文本流切分成有意义的单元Token。动作根据规则如空格、标点或 BPE 算法将字符串切开。输入原始字符串Hi!输出字符串列表[Hi, !]关键点这一步还没有变成数字只是把文本打散了。对于中文可能是[你, 好]对于英文生僻词可能是[Un, believable]。2Numericalization数值化查表转 ID“给每个小块发身份证号”计算机只认识数字。分好词后我们需要去模型的词汇表Vocabulary里查找每个 Token 对应的唯一编号。动作在字典中查找索引。输入字符串列表[Hi, !]输出整数列表[101, 502]假设 Hi 的 ID 是 101! 是 502关键点这就是你之前看到的[1, 2, 0]的来源。这一步将语义符号变成了数学符号。3Special Tokens特殊标记添加控制指令模型不仅需要内容还需要知道“哪里是开头”、“哪里是结尾”或者“这是两个句子中的哪一个”。这就好比写信要有“尊敬的”和“此致敬礼”。动作在数字序列的首尾或中间插入特定的控制 ID。输入整数列表[101, 502]输出100可能是[CLS]开始标记告诉模型从这里开始读102可能是[SEP]结束标记告诉模型这句话读完了关键点如果不加这些模型可能不知道句子在哪里结束或者无法区分这是用户说的话还是 AI 说的话在对话模型中尤为重要。步骤名称数据形态变化说明原始输入TextHi!人类看的文本第一步Tokenization[Hi, !]切分成 Token第二步Numericalization[50257, 30]查表变成 ID第三步Special Tokens[101, 50257, 30, 102]加上 [CLS] 和 [SEP]最终结果Encodingtensor([101, 50257, 30, 102])变成张量送入 GPU 计算所以Encoding 是为了让模型能“读懂并处理”文本而 Tokenization 只是其中负责“切分”的一环。整本 input.txt 也是同理只不过会变成一个非常长的整数列表像这样[18, 47, 56, 57, 58, 1, 15, 47, 58, 47, 62,43, 52, 10, 0, ...]这里每个数字对应一个字符。注意这里说“token”在这个项目的 shakespeare_char 数据集中token 就是一个字符。所以一个字母 一个 token一个空格 一个 token一个换行 一个 token一个标点 一个 token这和很多大模型常见的“token 是词片段”不一样。字符级切法是T o b e所以一共 5 个 token。但很多大模型不是这么切的很多大模型会把文本切成“词片段”而不是单字符。比如playing可能不是 7 个字符 token而可能被切成play ing这就叫 subword token也就是“词片段 token”。所以同一段文本在 shakespeare_char 里token 是字符在 GPT 这类常见 tokenizer 里token 往往是词片段有两层概念文本层面你看到的是英文句子模型层面模型看到的是 token 序列再进一步是 token 对应的整数 id而你这个项目里刚好采用的是最简单的一种字符级 tokenization 也就是 先把文本拆成一个个字符再给每个字符编号。一个非常直观的对比文本Hello, world!如果按字符级H | e | l | l | o | , | 空格 | w | o | r | l | d | !这就是 13 个 token。如果按词片段级可能会像这样Hello | , | world | !那就只有 4 个 token。3. 切成训练集/ 验证集意思是前 90% 文本作为训练集 train_data后 10% 文本作为验证集 val_data为什么要这样因为你不能只看模型“背会了训练内容没有”还要看它对没参与训练的数据表现怎么样。验证集就是用来检查模型有没有真正学到规律而不是死记硬背。你可以粗略理解成train.bin拿来学习val.bin拿来考试4. 要保存成 train.bin / val.bin这是很多新手最容易卡住的点。可能会想既然已经有 input.txt训练时直接读文本不行吗理论上可以但效率差。因为训练会反复做下面的事情从数据里截取一小段连续内容把字符转成数字送进模型如果每次训练一个 batch 都重新从文本解析字符会很慢。所以预处理脚本提前把它变成纯数字并直接保存到二进制文件里train_ids.tofile(...)val_ids.tofile(...)这样后面训练时可以直接高速读取整数不用反复做字符串处理。5. meta.pkl 的作用train.bin / val.bin 里只存了一串数字比如[18, 47, 56, 57, ...]但如果没有那张“对照表”你就不知道18 是哪个字符47 是哪个字符所以还要额外保存 meta.pkl.pkl就是Python 专用的数据或模型存档文件二进制里面有vocab_sizestoiitos这三个可以理解成“这个字符级数据集的说明书 字典”。vocab_size意思是总共有多少种不同的字符。比如你的 Shakespeare 数据里出现过大写字母、小写字母、空格、换行、标点把所有不重复字符收集起来一共有 65 个所以vocab_size 65它告诉模型你最终要预测的候选字符总共就这 65 种。stoistoi string to integer意思是把字符映射成整数编号的字典。例如可能像这样stoi { : 0,!: 1,A: 10,a: 35,}作用是把原始文本编码成数字模型训练前先把字符转成 token id比如stoi[a]# 可能得到 35itositos integer to string意思是把整数编号映射回字符的字典。例如itos {0: ,1: !,10: A,35: a,}作用是把模型输出的数字还原回字符生成文本时把 token id 解码成人能读的内容比如itos[35]# 得到 a三者之间的关系你可以把它们看成一套完整配套工具vocab_size字典里总共有多少个字符stoi查“这个字符编号是多少”itos查“这个编号对应什么字符”这相当于把“字典”也一起存起来了。这样以后你才能把新文本编码成数字把模型生成的数字再解码回字符。6. 总结简单理解数据预处理流程原始 Shakespeare 文本就像一本英文书模型看不懂字母。于是你先做了一套“机器版翻译”把每个字符编上号把整本书翻译成数字把数字存好训练时直接喂数字给模型训练完再把模型吐出的数字翻译回字符。所以这个预处理脚本本质上是在做把“人类可读文本”变成“神经网络可训练数据”。总结prepare.py 做的不是“让模型理解莎士比亚”而是先把莎士比亚文本变成一种统一、紧凑、可快速读取的数字格式这样后面的 train.py 才能高效地训练“预测下一个字符”的模型。三、后面的 train.py 如何用这些文件后面的 train.py 会用内存映射memmap直接读这些文件批量取连续片段。意思是训练时不会一次性把全部数据都完整塞进内存而是像“按需读取”。你可以想象 train.bin 是一条很长很长的数字带子[18, 47, 56, 57, 58, 1, 15, 47, 58, 47, 62,...]训练时模型不会每次看全书而是随机抽一小段例如长度是 8输入 x[18, 47, 56, 57, 58, 1, 15, 47]目标 y[47, 56, 57, 58, 1, 15, 47, 58]可以把 x 和 y 理解成x题目y标准答案在语言模型里更准确一点x模型当前能看到的输入序列y这个输入序列对应的“下一个 token”真实答案也就是说模型看到前面的字符去预测“下一个字符”是什么。这是语言模型最核心的训练方式。block_size 决定“每条样本有多长”batch_size 决定“一次并行喂多少条样本”block_size 的意思更像是模型最多能看多长的上下文。不是说每次必须刚好输入这么长。[47] 可以预测下一个token[47, 58] 可以预测下一个 token[18, 47, 56, 57] 也可以预测下一个 token只要长度不超过 block_size原则上都可以。为什么训练时总是切成 block_size因为训练时要走 GPU 批量计算固定长度最方便。比如代码里会取batch_size 条样本每条样本长度都是 block_size这样张量形状整齐计算高效。所以训练代码里看到的是x.shape (batch_size, block_size)y.shape (batch_size, block_size)这是工程实现上的方便不是概念上只能这么做。为什么训练必须用连续片段因为训练目标是让模型学会真实文本中的规律比如哪些字符常常连在一起、哪些单词后面常出现什么、句子通常怎么延续、这些规律都建立在邻接关系上也就是“谁跟在谁后面”。如果你把上下文打乱了模型就学不到真正的语言结构。即是连续切片而非随机散点。好啦我们这次总结了一下数据集这块内容下篇继续讲配置项训练模型都需要配置什么内容。

更多文章