nanoGPT中文语料训练全流程复盘:从数据准备到过拟合判断
发布时间:2026/10/21 22:26:56 锦皓数字建站

2026年4月7日周日我照例打开那台租来的服务器准备跑一轮nanoGPT训练。没想到这一跑把之前攒的一堆疑问全带出来了数据集准备、参数设定、loss曲线的解读、过拟合的边界判断还有那些藏在训练日志里不容易被注意到的小坑。这篇不是教程就是一次完整训练过程的复盘记录把我当天实际怎么操作、怎么调整、怎么判断的细节都写下来给同样在折腾生成模型的朋友做个参考。我用的数据集不是莎士比亚全集也不是维基百科而是自己准备的一份中文语料大概25MB不到20万条短文本覆盖了一些产品描述、技术FAQ和短小评论。之所以选这个规模是因为nanoGPT本身定位就是“小而能跑”如果一上来就用大语料反而不容易看清模型训练的各个阶段变化。小语料更容易出现可观察的过拟合现象更适合用来理解训练机制。1. 为什么选nanoGPT跑这轮实验先说动机。我之前用现成的API调过几次文本生成效果不错但心里一直没底这些模型内部到底怎么一步步学习的为什么同样的超参数在大模型上有效、在小模型上就不一定为什么有人建议先训到loss拐点再调学习率这些靠调API是搞不明白的必须自己把一条完整的训练链路跑一遍才能建立直觉。nanoGPT正好是这个场景最合适的实验载体。它把GPT的训练流程压缩到几千行代码以内核心模块包括数据切分、tokenization、attention计算、训练循环和采样生成每一段都能单独读、单独改。相比自己从零写一个TransformernanoGPT省掉了大量工程化的部分保留的是真正值得理解的东西。项目作者在说明里强调了它的定位用于教育目的和快速实验而不是生产部署。这一点很重要意味着训练速度、并行优化、分布式支持都被特意简化了跑起来不需要复杂的集群配置一张普通GPU就能完成。对我来说这种“接近底层但可运行”的特性最合适既能看到每个参数如何流动又有即时反馈。还有一个实际考量是复现成本。很多大模型的配置动辄几十亿参数即便能下载权重重新训练一轮也是天文数字。nanoGPT最小的示例配置只有不到1000万参数几分钟就能跑完一轮实验这意味着我可以大胆做超参数对比不用担心时间成本。本次训练定的目标是验证三件事在规模较小的中文语料上nanoGPT默认配置是否仍然能学到有意义的语言模式训练过程中loss的下降曲线呈现什么形态什么时候开始出现明显的收益递减生成的文本质量和过拟合程度如何平衡训练多少步能得到相对自然的结果。带着这三个问题我开始了当天的准备工作。2. 数据准备与预处理环节的实际操作2.1 数据来源与清洗训练记录里最容易被略过的数据准备阶段在当天占了将近一个小时。原始语料是从某个内部知识库导出的纯文本格式混乱有大量超链接、表格残留、重复段落直接拿去训练会浪费大量token在学习噪声上。我用Python脚本做了三件事去掉无关符号和URL、合并断行文本、按一定长度过滤短句。最后留下的数量大约17.2万条总字符数约2600万。清洗脚本需要特别注意编码问题有些文本源会用GBK而不是UTF-8在读取阶段就需要统一转换否则会在切分时报出奇怪的索引错误。with open(raw_corpus.txt, r, encodingutf-8, errorsignore) as f: lines f.readlines()第一版脚本跑完之后我抽查了300条清洗后的文本发现还有不少日期格式不对齐的情况比如“2026/4/7”和“2026年4月7日”混用。为了减少分词后的碎片化影响我又加了一个格式统一的正则替换规则把常见日期格式转成同一写法。这种细节看似不影响训练能否跑起来但会影响模型对数字和日期表达的连贯性尤其是后续要做文本生成时日期混乱会非常明显。2.2 tokenizer的选择与踩坑nanoGPT默认使用字符级tokenizer也就是把每个汉字当作一个token。对于一个2600万字符的语料来说字符级tokenizer的词汇表很小只有几千个不同的字符训练速度会很快但生成结果的语义连贯性通常不如子词级或词级tokenizer。我考虑过直接引入BPE tokenizer但权衡后还是先用字符级跑一轮baseline。这样后续做对比实验时可以先确认模型结构和训练参数本身没问题再换tokenizer观察效果。字符级模型学到的更像“字形组合规律”而BPE模型能学到“词义组合规律”两个结果放在一起看可以更清楚地区分模型能力的瓶颈到底在哪。实践中有个重要的坑如果用openai的tiktoken做BPE编码数据切分后的训练样本格式必须和nanoGPT默认的字符编码保持一致否则会出现输入输出维度对不上的问题。第2.1节里我们清洗过的数据在字符级模式下直接写入train.bin和val.bin每两三个小时训练一次也不需要重新处理数据。但换BPE时就要重新生成bin文件并且val集的切分比例建议沿用默认的0.9/0.1太小的验证集会放大loss波动的视觉幅度影响我们对收敛状态的判断。2.3 数据切分与二进制化nanoGPT数据链路的核心是把纯文本变成二进制token序列。之所以用二进制格式是为了在训练循环中减少CPU和GPU之间的数据搬运量直接用内存映射读取。字符级tokenizer下token就是字符在词汇表中的索引存储时用uint16就够了文件也不会太大。我用官方提供的prepare.py改了一个版本输出两个文件train.bin约2300万token和val.bin约260万token。在训练脚本中nanoGPT通过内存映射加载数据每次迭代随机截取一段block_size长度的序列作为一条样本这个机制使得模型在单个epoch内能看到大量随机窗口相当于隐式做了数据增强。数据切分这里还有个容易出错的地方如果语料总长度小于train.bin所需的取样范围代码会在边界截断时报错表现为“IndexError: index out of range for tensor shape”。我当时第一次跑就遇到了排查后发现是语料清洗后的行尾符残留导致总字符数偏少。加上尾部补全逻辑后解决将超过边界的部分用wrap-around的方式补零。3. 训练参数设计与loss曲线观察3.1 模型配置与训练环境当天用的服务器配置不算高一张大约16GB显存的消费级GPUCPU为8核内存32GB。因为nanoGPT对显存的占用比较温和16GB完全够跑1200万参数级别的模型而且还能把batch size稍微放大一些。我选的模型配置如下表所示参数名称数值说明n_layer6Transformer层数n_head6注意力头数n_embd384嵌入维度block_size256单次输入的最大token长度batch_size32单次迭代的样本数learning_rate3e-4初始学习率max_iters10000最大训练迭代步数grad_accum_steps4每更新一次权重累计的batch数这个配置大致相当于一个极小的GPT-2参数量在600万到1000万之间浮点计算量在单卡上完全可接受。block_size选择256而不是默认的1024是因为语料里很多文本本身就不长过长的上下文窗口会浪费训练数据且增加显存开销。对于中文短文本场景256通常已经够用。学习率3e-4是nanoGPT在小型配置下的常用值。这里有一个关键认知学习率并不是越大越好。过大的学习率会在训练早期把梯度推入难以收敛的区域导致loss值在高位震荡过小的学习率则让漫长训练的大部分时间都花在“低效率的缓慢下降”上。我在之前跑过一次lr1e-3的配置loss很明显在1500步之后开始反复横跳最后又调回3e-4才稳定下来。3.2 训练过程中的实时观察训练启动后我打开了两个终端一个跑训练脚本另一个用nvidia-smi -l监控显存和GPU利用率。nanoGPT默认每100步打印一次训练loss和验证loss这个间隔对小语料来说稍微稀疏了一点我把它改成每25步打印一次以便更早发现异常。前200步的loss下降非常快从最初的约4.2下降到了1.8左右。这个阶段对应的是模型在快速学习最简单的词频规律和常见搭配基本上就是“记住”了哪些字符经常一起出现。到400步时loss降到1.3附近下降速度开始放缓可以明显看到曲线坡度变平。这是常见的“收益递减”特征模型已经掌握了大部分显式的统计规律接下来要学的则是更微妙的语义组合约束。我把关键节点整理成下表迭代步数train lossval loss观察状态254.164.12模型刚开始学习2001.851.91快速下降期5001.321.47速度开始放缓10001.051.22出现明显val高于train20000.881.10开始出现过拟合迹象50000.570.98过拟合加深100000.341.02附近验证loss几乎不再改善从第1000步开始val loss稳定高于train loss且差值不断拉大这是模型在训练集上过度拟合的经典信号。到了第5000步train loss已经低于0.6而val loss还停在1.0附近虽然生成文本看起来更通顺但泛化能力并没有同步提升。关于loss曲线的解读一个容易误解的点是训练loss低并不代表生成质量好。对于语言模型来说loss是交叉熵损失它反映的是模型对下一个token预测的不确定性。loss越低说明模型对语料的统计规律掌握得越紧密但同时也可能意味着它记住了太多训练集中的偶然细节。所以判断训练是否成功一定要同时看val loss和实际生成的文本两者相互印证。3.3 学习率调度策略的调整记录nanoGPT默认提供warmup和余弦退火两种学习率调整方式。warmup阶段让学习率从小到大逐步上升目的是避免一开始就用大学习率把权重冲乱余弦退火则让学习率在训练中后期周期性下降帮助模型在损失表面的低洼区域精细收敛。我这次训练中设置warmup_iters1000lr_decay_iters10000min_lr1e-5。在1000步之后实际学习率会从3e-4沿余弦曲线下降到最低值1e-5。这个策略在小模型上非常有效训练后期val loss的波动明显减小。如果读者用的是自己的数据我建议将lr_decay_iters设置为和max_iters相同这样不会出现训练还没跑完学习率就已经降到最低的情况。另一个会被忽略的参数是eval_interval默认值500而eval_iters是200表示每500步用200个batch去估算验证loss。对于小语料可以适当降低eval_interval到100方便更快观察验证集表现。4. 训练过程中的异常排查与修复4.1 显存不足与batch_size调整第一次启动时我按习惯把batch_size设成了64结果运行到第2个iteration就报了CUDA out of memory错误。nanoGPT在分配attention中间变量时显存占用量大致和batch_size × block_size × n_embd成正比block_size越大占用量越吓人。把batch_size降到32、grad_accum_steps设为4之后显存占用从接近15GB降到约9GB问题解决。这里要说明的是grad_accum_steps是“梯度累积”效果等价于更大的batch size但实际显存占用不增加只是单步的训练时间略长。我后来试过batch_size16 grad_accum_steps8的组合发现效果类似但训练速度更慢不值得。4.2 中文语料下的loss不降问题在中文语料的第一次训练尝试里我遇到了loss卡在3.5附近不动的情况大约300步都没有明显变化。检查之后发现是语料中的标点符号和换行符太多模型找不到稳定的预测目标。清洗之后loss在200步内就顺利降到了2.0以下。这提醒我一个规律nanoGPT的训练效果高度依赖数据的“可预测性”。如果语料中包含了大量随机符号、短文本碎片、无意义的重复模型会花大量容量去拟合噪声造成val loss长期偏高。中文文本最好保留完整标点因为标点在字符级模型中也是token会影响上下文建模。4.3 生成阶段的采样温度与top-k设置训练完成后我用了三种采样参数对比生成结果温度top_k生成效果描述0.540通顺但略显保守重复词多0.8200流畅度和多样性平衡较好1.2无内容变化大但出现语法断裂最合适的组合是温度0.8、top_k200。温度太低会让模型反复输出训练集中出现频率最高的句子温度太高则会让模型失去连贯性把不相关的词强行拼在一起。top_k参数的作用是限制每一步采样只从概率最高的k个token中选防止模型挑选那些概率很低但会造成语义断裂的词汇。我在当天生成了一些测试句子比如输入“这款产品的主要特点是”模型能输出“续航能力强、体积小巧、适合户外使用……”这样通顺的后续。虽然句子偶尔会出现重复或逻辑跳跃但作为一个小模型的表现已经超出预期。5. 从这轮训练中得到的几个明确结论跑完这轮nanoGPT最直接的收获不是生成的文本有多惊艳而是建立了一套“训练—观察—调整—验证”的闭环判断方法。有几个结论值得单独拿出来说。第一val loss的走势是判断模型泛化能力的核心指标而不是train loss。训练loss会一路下降哪怕模型已经开始严重过拟合所以只看训练loss等于把实验做成了自欺欺人。我在5000步之后的train loss已经在0.57附近val loss还停在1.02左右如果当时只盯着train loss说不定会误以为模型还在继续变好。第二小语料训练不宜跑太多步。我在max_iters10000的配置下val loss在5000步之后几乎不再改善但train loss仍在下降。这说明模型把更多容量花在了记忆训练集的偶然特征上。对于2500万token规模的数据5000到8000步之间已经接近实际效果的甜点区。第三字符级tokenizer是理解训练机制的入门方案但不是生成质量的最优解。后续如果想提升效果我计划换成BPE或句子片段tokenizer对比两者的训练速度和生成质量差异。对于中文BPE需要选择合适的词表大小比如8000或16000词表太大会增加嵌入层参数太小则会把常用词拆得太碎。第四训练日志本身就是最好的实验笔记。我把当天所有命令行输出保存到了logs目录下包括每次变更参数前的git diff记录以及生成文本的样例这样后续再复现或调整时不用靠记忆。这个习惯强烈建议保持很多看起来“玄学”的问题回查日志后往往能找到具体的诱因。另外有一个小技巧训练进行中如果想中断再继续训练可以直接修改脚本中的init_from参数为“resume”指定输出目录中的ckpt.pt权重文件。这个过程不影响已训练的进度是一个很实用的功能。当晚最后我又用温度0.7、top_k150跑了一批生成样本效果比之前稳定不少。输入“请注意使用本产品前需要”模型输出了一句完整的注意事项“请确保设备已充满电并保持接口干燥否则可能导致接触不良。”句子结构基本正确内容也符合训练语料中产品描述的常见表达。作为一个训练了不到两个小时的1000万参数小模型这个效果我已经比较满意。下一步我准备做两组对照实验一组换BPE tokenizer一组把block_size从256增加到512重点看val loss和生成连贯性的变化。到时候再写一份完整的对比记录作为这个系列的后续内容。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。