去年开始各路大模型项目越来越多,但大多数人看教程看得再多,也只是停留在"会调 API、会用开源权重"的层面。真正想搞明白 transformer 内部那些张量是怎么流转的,最直接的办法就是把一个模型从零开始训练一遍。前阵子我专门抽了一个下午,用 minimind 项目把一个 64M 参数的小模型从随机初始化开始训了 2 小时,想看看这种"小到不能再小"的模型,到底能训练出什么名堂。这篇文章就把这次实测的过程、数据、踩坑和结论完整记录下来,给打算入门大模型训练、或者手头只有一张消费级显卡的朋友做个参考。
先说结论:64M 参数的模型确实能学会"说话"的基本形状,能续写通顺的中文短句,也能在特定话题下给出像模像样的回答,但它的能力天花板非常明显——多步推理、复杂数学、长上下文基本别指望。它的价值不在于"能用",而在于"能跑通"和"能看懂"。如果你正处于"看了很多原理,但不知道训练代码怎么写、loss 曲线怎么读"的阶段,这个东西就是性价比极高的教学样本。
1. 为什么偏偏是 64M 参数的小模型
1.1 从小模型入手,才能真正看清训练的本质
大家现在动辄讨论 70B、千亿参数,好像不训练个大模型就不算入行。但实际上,参数规模一旦上去,普通人的本地环境连前向传播都跑不起来,更别说观察中间过程。64M 参数这个规模,放在今天的视角看属于"微型模型",但它的结构是完整的:嵌入层、多层 transformer block、注意力头、前馈网络、输出层,一样不少。
这意味着你可以像解剖麻雀一样,把每个组件单独拎出来看。训练过程中嵌入层的梯度长什么样、注意力分数怎么变化、某一层 loss 贡献多少,这些在大模型上根本没法可视化观察的现象,在 64M 模型上都能直接看到。说白了,小模型不是缩水版的大模型,而是一个"结构完整、规模可控"的实验载体。
另外,小模型还有一个潜在优势:它对数据质量和超参数的敏感度特别高。同样的数据和配置,稍微改一下学习率就可能从"能学会"变成"训崩"。这种敏感度在工程上是缺点,在学习上反而是优点,因为它逼着你去理解每个超参数到底在干什么,而不是盲目抄配置.
1.2 2 小时从零训练,硬件门槛到底有多低
说到从零训练,很多人第一反应是"至少要 A100 集群吧"。其实不然,64M 参数这个量级,参数量按 float16 计算,权重占 128MB 左右,加上梯度、优化器状态(AdamW 通常要额外存一阶矩和二阶矩),再算上中间激活值,显存需求在 4GB 上下。我用的是 RTX 3060 12G 这张卡,训练过程中显存占用峰值大概 5GB 多,完全跑得动。如果卡在 8GB 显存,也能通过调小 batch size 塞进去。
时间方面,2 小时是一个相对宽裕的基准。我用 3060 实测,一个 batch 约 0.15 秒,训练 3000 步左右大约 15 分钟就能完成一轮数据遍历。如果算上数据加载、checkpoint 保存、偶尔看一眼 loss 的停顿,两三个小时跑完整个预训练流程完全现实。也就是说,一个下班后的晚上,甚至一个午休时间,就足够把"从零训练"这个流程完整走一遍。
这也引出一个重要的认知:训练大模型的瓶颈往往不是显卡算力,而是数据、代码调试和排障的时间。64M 模型让"试错成本"降到了可以接受的量级,你可以在几个小时里尝试多种配置,这种快速迭代的经验迁移到更大模型上同样有效。
2. 训练前的地基:数据、词表和配置怎么选
2.1 数据从哪里来,又要怎么清洗
在动手写训练代码之前,最花时间的其实是数据准备。minimind 项目本身的定位是"学习用",它推荐使用的数据主要是开源的中文语料切片,规模在几亿到十几亿 token 之间。我自己实测时准备了几千万条中文短文本,覆盖百科、新闻、问答社区这几类,然后统一做了清洗处理。
清洗这一步特别容易被人忽略,但它的重要性不亚于模型结构。我做清洗时遵循三个原则:第一,去掉广告、无意义符号和超长段落;第二,统一标点符号,把英文逗号句号转成中文标点,避免词表碎片化;第三,按质量做简单过滤,比如小于 20 个字符的碎片直接丢弃,连续重复的异常文本也过滤掉。实践证明,垃圾数据会直接体现在 loss 上——如果 loss 在训练中途开始震荡不降,先检查数据是不是混入了大量噪声。
数据量这块,64M 模型不需要动辄几十 T 的数据。实测下来,当数据量在 3 亿 token 左右时,模型在 2 小时内基本能把训练集"吃饱",继续加数据边际收益会下降。更合理的做法是像 minimind 社区里大家常干的那样,先用较小数据量跑通流程,再逐步加数据,对比不同数据规模下的 loss 曲线,这本身就是非常好的实验课题。
2.2 自定义小词表的价值
训练词表是很多人第一步就会忽视的内容。社区里 minimind 有个特点,它自带的 tokenizer 词表非常小,只有几千个 token,而不是常见的几万甚至十几万。很多人不理解为什么要这样,觉得词表越大越"强"。但词表大小直接牵扯到 embedding 层的参数量,64M 的总参数量里,embedding 占的比重很大。词表从 5 万砍到 5 千,能省出大量参数留给 transformer 主体。
当然,小词表也有代价:同一个词可能需要被拆成多个 token,序列变长,信息密度下降。对于 64M 这种"玩具级"规模来说,这个交换是划算的,因为它让我们在有限参数量下,把模型的主要容量用在结构学习上。如果你在类似项目上做实验,可以自己用 BPE 算法训练一个词表,控制 vocab size 在 6000 左右,这个操作本身的代码量不算大,但能让你彻底搞懂 tokenizer 的原理。
2.3 模型结构配置和优化器选择
64M 参数听起来小,但结构上一点都不能含糊。我用的配置是 8 层 transformer、hidden size 512、8 个注意力头,使用 RoPE 位置编码、RMSNorm 归一化和 SwiGLU 激活函数。这套组合基本是现代大模型的最小公倍数,RNN 时代的写法在这里已经完全见不到了。
优化器选的是 AdamW,学习率设为 3e-4,配合线性预热和余弦衰减。batch size 设成 32,序列长度 512。这里有一个新手常犯的错误:盲目照搬大模型的超参数。大模型常用 1e-4 乃至更低的学习率,是因为参数多、梯度噪声大;64M 模型没那么"娇气",学习率可以稍微放开,3e-4 到 5e-4 都是安全区间,再高就容易震荡。这些参数组合起来,总参数量刚好落在 64M 附近,训练起来每一轮迭代的速度也足够快。
3. 正式开训:2 小时里到底发生了什么
3.1 从第一个 batch 到 loss 下降的三个阶段
启动训练之后,前几十步是最有意思的。随机初始化的模型,loss 通常在 10 以上,生成的文本全是乱码。这个阶段对应的是"模型连字母和中文汉字的基本分布都没学会",任何输出看起来都是在胡扯。
随着训练进行到几百步,loss 会快速降到 6 到 7 附近,模型开始学会"高频词优先"这个道理——它知道常见的"的、了、是"这类字出现的频率更高,于是输出里开始出现这些字的堆叠。再继续训练到一两千步,loss 大约能降到 4 到 5,模型开始具备一些粗糙的语法模式,输出能蹦出几个意思相对完整的短语。
最后的阶段比较微妙,loss 的下降速度明显放缓,从 4 到 3 可能需要很长一段时间,肉眼很难看出输出质量的提升。这个时候就需要用评估集而不是直觉来判断模型是否在进步。我自己的经验是,训练 2 小时得到的 64M 模型,loss 大概稳定在 3.2 到 3.5 之间。对比一下,从头写代码用 CPU 跑 2 小时,loss 可能还在 5 以上,所以 GPU 加速的价值在这个阶段体现得淋漓尽致。
3.2 训练过程中真正需要盯的关键指标
很多人训练时只盯 train loss,这是一个不小的误区。train loss 下降只能说明模型在"背"训练数据,不能说明它学到了可泛化的规律。我在训练过程中固定每 200 步跑一次小规模评估集,记录 eval loss。当 eval loss 不再下降甚至回升,而 train loss 还在继续降的时候,就说明模型开始过拟合了。
除了 loss,显存占用和训练吞吐量也要关注。用 nvidia-smi 来看,显存占用稳定就说明 batch size 和序列长度搭配合理;吞吐量则以"每秒处理多少 token"来衡量。我这次用 12G 显存跑,吞吐量在每秒 5 万 token 左右,意味着 2 小时能处理大约 3.6 亿 token,这刚好和上面说的 3 亿 token 数据量对上。如果你打算复现这个实验,先算清楚自己的吞吐量,再来决定训练多少步,比盲目设置"训练 5000 步"要合理得多。
3.3 Checkpoint 保存和断点续训的细节
训练代码写起来容易,但保存和恢复 checkpoint 的细节是实战里最容易被坑的地方。我在这次实验里每 500 步存一次 checkpoint,保存内容包括模型权重、优化器状态、当前步数和学习率。这样做的好处是,如果中途断电或者显存溢出,你可以直接从最近的 checkpoint 恢复,不用从头再来。
恢复训练时最容易出的问题,是 optimizer 状态和模型状态不匹配。举例来说,如果你改了模型结构再加载旧权重,embedding 层尺寸对不上,程序直接报错。还有一个小坑:保存时用的是 fp16,恢复时如果转成 fp32 再训练,loss 和梯度行为会有细微变化,可能影响复现。我的建议是训练和恢复全程保持相同精度,这也是一个"看起来不起眼、实际很折腾"的经验。
另外,训练到后半段我习惯开始做一次小规模对话微调。严格来说,预训练和微调是两套流程,但 64M 模型本身训练成本低,完全可以在同一个项目里预训练 1.5 小时,再用剩余时间做几百步的指令微调,让模型从"会续写"变成"会问答"。这一步对最终效果的提升非常明显,值得尝试。
4. 训练完能干嘛:实测能力边界
4.1 短文本续写:语感有,但内容空洞
2 小时训练完成后,我做的第一个测试是给模型一段开头,让它接着写。输入"今天天气很好,我决定"这样的句子,模型能输出"去公园散步,感受大自然的气息"之类的续写。从语感上看,句子通顺,标点使用得当,动词搭配也没有明显错误,这在 64M 规模下已经算达标了。
但仔细观察就会发现,模型的续写基本停留在"局部流畅、全局无逻辑"的层面。它没有能力记住 3 句话之前提到的地点,经常出现"上午说去公园,下午已经在海边"这种前后矛盾的输出。这是因为模型的记忆容量太有限,无法在超长上下文中维持一致的语义状态。用一句话总结就是:它的世界模型非常稀疏,更多是在做"字符串级别的模式匹配"。
4.2 简单知识问答:能答对简单事实,但经不起追问
我准备了一些简单的中文常识问题,比如"中国的首都是哪里""水的化学式是什么"这类。模型给出的答案在大部分情况下是正确的,说明训练数据里高频出现的事实信息能被 64M 参数记住。这也侧面验证了知识压缩这件事:大模型记忆知识并不需要无限参数量,常见事实用 64M 也足够编码。
但一旦涉及追问,模型就露馅了。比如它知道"水的化学式是 H2O",但你问它"为什么水分子是 V 形结构",它就开始一本正经地胡说八道。追问涉及因果关系和推理链条,这不是记忆能解决的问题。所以在做能力评估时,一定要区分"记忆型问题"和"推理型问题",小模型明显擅长前者,后者几乎无解。
4.3 数学和逻辑能力:基本处于不可用状态
把"23 加 37 等于多少"这种题给模型,它有概率答对,但我测试了 20 道类似的题,正确率只有六成左右。两位数加法都不稳定,更不用说乘法或混合运算。原因很好理解:算术需要模型在内部模拟一套运算过程,而 64M 参数留给"算法学习"的容量几乎没有,它只能靠记忆训练数据里的答案来蒙对一部分。
逻辑推理更不用提。我尝试让它按"如果 A 大于 B,B 大于 C,那么 A 和 C 谁大"这类方式进行推理,模型的输出基本没有规律,甚至会在同一段话里给出自相矛盾的回答。这让我想起深度学习社区的一个经典观点:模型规模决定了它能形成的内部表示复杂度,64M 这个规模不足以支撑多跳推理所需的表示结构。
4.4 用评估集量化一下能力
为了让结论更有说服力,我参照社区里常用的评估方式,做了一个简单的分类任务测试:让模型判断一个句子是正面还是负面情感。测试集 200 条,模型正确率约 72%。这个数字比随机猜测高不少,说明模型确实学到了部分语义信息,但距离能用的水平还很远。
另一个测试是中文分词,让模型给文本按词边界加分隔符,正确率在 65% 左右。这个结果和情感分类基本一致:模型能捕捉到明显的语言特征,但对细微的语言规则理解不足。做完这组测试,我对 64M 模型的能力边界有了清晰的画像——具备基础语言能力,不具备任务深度理解能力。如果你拿它当学习工具,这个画像已经足够有价值。
5. 常见问题与排查技巧实录
5.1 显存不够用了怎么办
我这次用的是 12G 显存,很宽裕,但在调试阶段我把 batch size 开到 128,结果直接 OOM。如果你手里的卡是 8G 甚至 6G,更要注意这个问题。最简单的解法是调小 batch size,比如从 32 降到 8,同时保持梯度累积步数不变,让有效 batch size 保持在合理范围。
还有一个更省显存的技巧是使用梯度检查点,它通过前向传播时丢弃中间激活值、反向传播时重新计算的方式来换取显存。开启后显存占用能降低三成以上,代价是训练时间增加约两到三成。对于 2 小时训练的实验来说,这个时间开销完全可接受。我的实测经验是,显存不够时优先降 batch size,其次开梯度检查点,最后才考虑缩短序列长度,因为序列长度对模型能力的影响最直接。
5.2 Loss 不降或者直接变成 NaN
训练中段 loss 突然变成 NaN 的情况,我这次也遇到了。排查思路按照优先级来:先看学习率是不是太大,64M 模型学习率超过 8e-4 就很容易梯度爆炸;再看损失函数输入有没有出现 log(0),softmax 输出如果全为概率 0,对应的交叉熵就是无穷大;最后查 batch 数据里是不是混入了异常值,比如空白文本或者全是特殊符号的脏数据。
如果是梯度爆炸导致的 NaN,可以在 optimizer 里加上梯度裁剪,把梯度范数限制在 1.0 以内。这个操作在 transformer 训练里几乎是标配,加上之后训练稳定性会明显提升。如果是数据问题,最好的办法是在数据预处理阶段多做过滤,别等训练开始后才发现数据有坑。
5.3 生成结果全是重复词或乱码
训练完成后推理时,最常见的现象是模型反复输出同一个词,比如"的的的的"或"我我我我"。这通常是采样参数的问题,温度设得太低会导致模型反复挑选概率最高的 token,形成死循环。把温度调到 0.7 到 0.9,再配合 top-p 采样,重复现象就能大幅度缓解。
如果生成的内容本身是乱码,那问题大概率出在 tokenizer 上——加载模型时用了错误的词表文件,或者词表大小和模型配置不匹配。排查方法很简单:先把词表文件单独加载并编码一段中文文本,确认分词结果正常,再检查模型配置里的 vocab_size 是否和词表大小一致。这两个数对不上,训练时可能不会报错,但推理时输出必然是乱码。
做完整轮排查后你会发现,绝大多数训练问题都不是玄学,而是配置、数据、数值稳定性三个方面的常见组合。把这些坑一个个避开,你的训练过程就会顺畅很多。
6. 实测之后的一些实在体会
两小时训练结束后,我坐在屏幕前反复翻看那些生成的文本,最大的感受是:64M 参数虽然小,但它是完全"长出来"的,不是被裁剪出来的。它从随机噪声开始,一步步摸索出汉字的组合规律、词语的搭配习惯,这种从无到有的过程远比直接用开源权重更能让人理解"训练"究竟意味着什么。我第一次看到模型输出完整通顺的句子时,那种惊讶程度不亚于当年看到大模型发布。
如果你也想复现这个实验,我的建议是不要照搬某个配置就跑,而是把注意力放在"改动一个变量会带来什么变化"上。先固定数据,调学习率;再固定学习率,换不同的词表大小;甚至可以尝试只训练 30 分钟就停止,看看模型处于什么水平。这种受控实验带来的认知增量,比单纯跑完一个流程要有价值得多。
最后分享一个小技巧:训练过程中,每隔一段时间就手动输入几个测试句,哪怕模型还在训练早期,输出还很烂,也要坚持做。因为你会亲眼看到模型从胡言乱语到说出完整句子的全过程,这种直观感受是任何 loss 曲线和评估指标都无法替代的。模型规模可以小,但训练带来的认知提升一点也不小。