ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零构建你的第一个微型LLM:架构、训练与生成实践指南

从零构建你的第一个微型LLM:架构、训练与生成实践指南

前阵子有朋友问我,想入门大语言模型,是不是得先买上几张GPU、把几十篇论文啃完再说。我说恰恰相反,与其站在岸边看别人训练大模型,不如先花一个周末,从零开始手把手构建自己的第一个LLM。哪怕它只有几十万参数、生成的句子错漏百出,只要这一整套流程在你手里跑通,你对语言模型的理解会比看十篇综述都更扎实。这篇文章就是干这个用的——不依赖高性能硬件,不堆晦涩公式,把模型架构、训练、生成、评估一条龙拆开讲清楚。标题敢写“收藏版”,是因为里面每个选择我都会解释背后的为什么,每个常见坑我都会提前标出来,你可以把它当作第一次做小模型实验的完整参考手册。

这篇适合谁看?有一点Python基础、对Transformer只有模糊概念、想用最小成本搞清楚语言模型到底是怎么“学会说话”的人。它不适合期待读完之后立刻能训练出接近商业产品级别模型的人——我们不追求大,我们追求的是把一个模型从零到一完整跑通过程中获得的认知。

1. 先想明白:我们要构建的“第一个LLM”到底有多大

1.1 语言模型的本质,是“预测下一个字符”

不管多大的模型,底层的训练目标都极其朴素:给它一段文本作为上下文,让它预测下一个字符(或者更准确的叫法是token,一个token可以是一个字符、一个词或者一段子词)。比如你输入“床前明月”,模型要输出一个概率分布,告诉候选字符里“光”的概率最高。训练时我们拿真实的下一字符作为标准答案,用交叉熵计算预测和答案的差距,再通过反向传播调整模型参数。这个流程不断重复,模型就慢慢学会了上下文里的统计规律。

这看起来简单到有点“笨”,但所有语言模型的能力都是从这条主线上长出来的。GPT系列能写文章、能对话、能编程,本质上都在做同一件事——根据前面的内容预测下一个token。区别只在于数据规模、参数规模和训练技巧。所以当你亲手实现过“预测下一个字符”这个最小闭环之后,后面再看任何大模型的技术文章,都不会有“空中楼阁”的陌生感。

1.2 为什么微型模型是最高效的学习载体

有人可能会问:现在开源大模型一堆,直接拿现成的微调不是更方便吗?直接调用确实方便,但那是“使用模型”,不是“构建模型”。自己从零构建一个微型LLM,你能直接观察到三样东西:数据怎么变成训练样本、参数怎么一步步把Loss压下去、采样温度怎么改变生成风格。

微型模型的价值在于“可控”。几十万参数的小模型,跑一次完整训练只要几分钟到几十分钟,你可以像做实验一样反复调参、看不同超参数下的曲线变化。换成大型模型,一次训练的资源消耗足以让你不敢乱试。就好比学开车,先在模拟器上把油门刹车方向盘玩明白,再上真车才不慌。理解微型模型的每个细节,就是未来理解和驾驭大模型的模拟器。

2. 环境与数据准备:动手前的三件小事

2.1 硬件与运行库:你的电脑可能已经够用

先给个定心丸:训练一个几十万参数、上下文长度128的微缩语言模型,纯CPU都能跑。我自己第一次跑通全程用的就是一台普通办公笔记本,内存8GB,没有独立显卡。训练10万字符级别的语料,大概几分钟能完成一轮基础训练,完全不耽误边训练边刷剧。

软件层面东西不多:

  • Python环境:建议直接用集成环境管理器,避免手动装包把系统搞乱。
  • 自动微分框架:选主流的、支持CPU训练的那一款就行。装库的时候有个小坑——如果你有NVIDIA显卡,提前确认好显卡驱动版本再装对应版本的库,别直接装默认版,否则很容易报CUDA不匹配的错误。没有显卡的直接装CPU版,省心。
  • 数值计算库:配合自动微分框架用的二维数组计算库,默认装最新版即可。

2.2 语料选择与清洗:不要让脏数据毁了第一次实验

第一次做实验,语料尽量选公开领域的、干净的数据,少踩数据清洗的坑。英文世界最经典的选择是莎士比亚全集,内容在公开领域,长度适中,风格鲜明;中文的话可以用公开的古诗文语料,或者其他经过整理的开源纯文本数据集。

拿到原始语料之后,清洗这步千万别跳过。我第一次用中文语料训练时,数据里残留了大量HTML标签和乱码符号,导致生成结果里频繁出现半截标签,看起来非常滑稽。建议至少做三步:

  1. 统一编码为UTF-8,删除所有非文本符号。
  2. 按行或按段落过滤,去掉长度小于10个字符的碎片。
  3. 用正则把连续空白符压缩成单个空格。

清洗完的语料建议切成训练集和验证集两段,比例差不多9:1。验证集的作用后面训练章节会讲,它是你判断模型有没有过拟合的重要窗口。

2.3 分词方案:为什么教学项目首选字符级

常见的分词方案有字符级、词级和子词级(BPE等)。正规的大模型项目基本都走子词级,但对于“从零构建第一个LLM”这个目标,我强烈建议你先用字符级。

原因很直白:字符级的词表大小通常只有几十到一两百,代码简单到可以手写,而且你不需要引入任何外部分词工具。词表小,意味着模型的嵌入层和输出层参数量小,训练速度更快,符合“微型实验”的定位。更重要的是,用字符级你能更清楚地看到模型在学习“音节”“词根”“风格”这些现象,而不必怀疑是不是分词工具替模型做了太多事情。

字符级分词的核心就三步:收集文本里所有独立字符形成词汇表、建立字符到整数ID的映射、把整段文本序列化成ID数组。就这么简单,不涉及任何需要安装的第三方分词库。

3. 模型骨架:从零实现一个极简Transformer

3.1 六个核心组件,先认识一遍

一个极简的Decoder-only Transformer,核心组件可以拆成这么几块:

  • 输入嵌入层(Token Embedding):把每个字符ID映射成一个固定维度的向量。说白了就是给每个字符找一个“数学意义上的管家”,负责把离散ID翻译成模型能计算的连续向量。
  • 位置编码(Position Embedding):注意力机制本身不关心顺序,所以必须额外告诉模型“哪个字符在前、哪个在后”。最简单的方法是用一组可学习的位置向量,学到第几个位置大概长什么样。
  • 多头注意力(Multi-Head Attention):让每个位置都能“回看”前面的位置,并从不同子空间提取关联信息。这是整个Transformer的引擎。
  • 前馈网络(Feed-Forward Network):对每个位置单独做非线性变换,相当于给模型加一组可学习的“思考层”,增强表达能力。
  • 层归一化(LayerNorm)+ 残差连接:训练深层模型时的稳定器。残差让梯度能顺畅回传,归一化保证每层输出尺度不失控。
  • 输出投影层:把最后一层的向量映射回“词表大小”的分数向量(logits),再配合Softmax得到每个字符的预测概率。

以上这些组件,每一个单独拎出来都能写上千字文章,但第一次实验阶段,你只需要理解它们的组合逻辑,不需要逐行默写公式。

3.2 注意力机制:Transformer真正的引擎

注意力机制值得单独说透一点。它的输入是三份东西:Query(查询)、Key(键)、Value(值)。可以这样类比:你在图书馆里找一本书,Query是你想知道的问题,Key是每本书的标签,Value是书本内容。系统先把你手里的问题跟每个标签做匹配,算出相似度分数,再按相似度从高到低加权提取对应的内容。

微型Transformer里的实现会用到因果掩码(Causal Mask)。因为语言模型只能看当前时间步之前的内容,不能偷看未来的字符。实现方式很简单:在计算注意力分数之后,把“未来位置”的分数设置成一个极大的负数,Softmax之后那些位置的权重就变成0。这一步就是Decoder和Encoder最本质的区别之一——Encoder允许双向互相看,而Decoder强制单向。

缩放因子也值得提一嘴。注意力分数除以头维度的平方根,是为了防止向量维度过大时点积结果太大,导致Softmax进入梯度饱和区。这不是调参玄学,是有数学依据的稳定化操作。

3.3 把子层拼成解码器:初始Loss检查是救命的

把上面几块按顺序拼接,一个微型Transformer解码器就成型了:输入ID → 嵌入向量 → 位置编码 → N个Transformer层(每层都是“注意力+残差+归一化+前馈+残差+归一化”)→ 输出投影层 → logits。

拼完之后,在开始正式训练前,一定要先做一件事:打印初始Loss。这里有一个非常实用的经验值:初始Loss应该接近 log(vocab_size)。比如你的词表大小是65,那么 log(65) ≈ 4.17。如果初始Loss是这个数附近,说明前向传播、数据切批、标签构造都基本正确;如果差得离谱,说明某个环节肯定有bug,这时候修比训练半天之后才发现loss不降要划算得多。

为什么初始Loss必然接近这个值?因为模型参数刚随机初始化,它还没有学到任何规律,对每个字符的预测概率基本均匀。均匀分布的交叉熵就是词表大小的自然对数。这个小小的检查点,能帮你把训练前的隐性问题一次性暴露。

4. 训练循环:如何让Loss曲线替你说话

4.1 一套可直接复用的超参数起点

第一次训练微型LLM,超参数不需要搞得很复杂。我经常给新手的起点是一组已知能稳定跑通的配置:

  • 优化器:AdamW,beta1=0.9,beta2=0.95,epsilon=1e-8。
  • 学习率:1e-3到3e-3之间,微型模型可以承受相对高一点的学习率。
  • 权重衰减:0.1,配合AdamW使用。
  • 批大小:64个样本,每个样本是一段定长文本块。
  • 上下文长度:128个字符。字符级模型对序列长度本来就敏感,128是个不错的起步值。
  • 训练步数:先定在2000到5000步之间,根据Loss曲线再动态调整。

这些参数不是我拍脑袋给的,它们背后的逻辑是:模型参数少、数据规模不大,优化器不需要太重的稳定性约束,学习率拿中等偏上的值更容易在有限步数内看到明显收敛。

训练循环本身很标准:每个批次把输入序列丢进模型,算出预测logits,和真实目标序列计算交叉熵损失,反向传播拿到梯度,裁剪一下梯度范数(推荐限制在1.0以内),然后用优化器更新参数。梯度裁剪这一步别省,它能在学习率偏高或遇到异常批次时防止Loss爆炸。

4.2 训练中一定要盯的几个信号

训练期间别把Loss数字当成唯一指标,有几个信号值得同时关注:

  • 初始Loss是否落在理论值附近:前面说过了,先验证再开跑。
  • Loss的下降速度:正常情况前500步会掉得很快,后面慢慢变慢。如果训练了1000步Loss还在原地晃,大概率是学习率太小或数据有问题。
  • 训练Loss与验证Loss的距离:训练Loss持续下降、验证Loss也开始下降但更慢,这是正常;一旦训练Loss还在降但验证Loss开始回升,就是过拟合的警钟。
  • 梯度范数:如果出现NaN或异常大值,先看数据里有没有脏字符,再看学习率是不是太高。

我自己第一次训练时就遇到过“Loss掉到2附近就再也不动”的情况。后来检查发现是验证集和训练集数据切分时用了同一个随机种子,导致两边数据重叠严重。这个问题不看验证Loss根本发现不了,所以强烈建议你养成“边训练边看验证曲线”的习惯。

4.3 微型模型的过拟合问题

微型模型很容易在小型语料上过拟合——因为它参数量虽然小,但语料量更小。我训练10万字语料时,大概两三千步之后验证Loss就开始反弹,训练Loss却还在稳步下降。这个时候模型并不是“学会得更好”,而是在“背诵训练集”,生成出来的文本开始机械重复原文片段。

应对办法其实不多,选一个就够:

  • 提前停止:盯着验证Loss,最低点那个checkpoint就是最佳模型。
  • 增加数据量:语料从5万字扩到20万字,过拟合出现的时间会明显推迟。
  • 增大权重衰减:把weight_decay从0.1调到0.3,一定程度上抑制参数过度膨胀。
  • 做轻量化的丢失增强:对某些层随机移除部分连接,增强泛化。

对于第一次实验,我最推荐先试提前停止,简单直接,不用改任何训练逻辑。训练完保存验证Loss最低点的模型文件,后续推理都用它。

5. 文本生成:把概率变成句子

5.1 自回归生成的基本流程

模型训练好之后,生成文本就是把它当成一个“自动续写机”来用。流程非常直观:

  1. 给模型一个起始提示词,比如“The”,转成ID序列。
  2. 前向计算,拿到下一个字符的概率分布。
  3. 从分布里按照某种策略采样一个字符ID。
  4. 把采样出的字符追加到序列末尾,同时滑动窗口丢掉最前面的老内容,保持长度和训练时一致。
  5. 重复2到4步,直到生成足够长的文本或遇到终止条件。

注意第4步的窗口滑动。训练时上下文长度是固定值,推理时序列也不能无限变长,所以每步都要保证末尾新字符能被“看见”。这个细节第一次写很容易漏,漏了会出现“预测越来越瞎”的情况。

5.2 温度与Top-k:一冷一热两种采样

概率分布出来之后,直接每次都选概率最大的字符,得到的结果会非常机械,而且特别容易陷入重复循环。这时候就需要采样策略来“注入随机性”。

温度(Temperature)是最常用的旋钮。简单说,把预测分数除以一个温度值,再进Softmax。温度小于1,分布变得更尖锐,输出更保守;温度大于1,分布变得更平坦,输出更随机;温度等于1,就是原始分布。我自己常用的组合:

  • 复现式写作:温度0.4到0.7之间,输出稳定且有点变化。
  • 创意式续写:温度0.8到1.2之间,变化更多但有失控风险。
  • 玩梗式随机:温度1.5以上,基本就是乱说,适合娱乐。

Top-k采样是另一道保险:只从概率最高的前k个字符里按归一化后的概率做选择,避免低概率的奇怪字符突然冒出来。经典搭配是k=40。温度负责调节“胆量”,Top-k负责过滤“杂质”,两者配合效果最好。

采样策略效果表现适用场景
贪心解码字面复读机,重复严重调试模型是否工作
温度=0.7相对通顺,偶用新词日常生成基础推荐
温度=1.2 + Top-k=40变化丰富但有翻车概率创意生成、测试模型边界

5.3 让生成结果更稳定的几个小技巧

生成时的重复问题是新手最常遇到的,生成50个字符可能就开始无意义循环。几个亲测有效的办法:

  • 限制上下文窗口:不要把所有历史都喂进去,只保留最后64到128个字符,减轻模型陷入局部循环。
  • 启用重复惩罚:当检测到某个字符已经在最近N个位置里高频出现,直接压低它的采样概率,这是最简单粗暴但有效的方式。
  • 先生成短片段再拼接:不要指望一次生成上千字,每次生成100到200字,手动挑选满意的片段拼接,体验会好很多。

字面上看这些技巧不复杂,但微小型模型尤其容易暴雷,每次生成前想一下“哪段历史可能在误导模型”,基本就能解决大半问题。

6. 评估与改进路线:从“会说话”到“说得像话”

6.1 用困惑度校准你的体感

评估语言模型最经典的指标是困惑度(Perplexity),它的定义是交叉熵损失的指数。困惑度越低,说明模型对验证集的预测越“意外惊喜少”,也就是越拟合数据分布。

训练完一个模型,用验证集算出最终Loss,然后做一次 exp(Loss),就能得到困惑度。字符级模型配合字符集65,困惑度在10到30之间算是“能看出点语言结构”的水平;如果困惑度还在50以上,说明模型基本是在瞎猜。

不过老实说,困惑度只能作为客观参考,它不考虑“语义是否合理”。一个照抄训练集片段的模型可以有很低的困惑度,但生成的文本毫无新鲜感。所以评估一定要过“肉眼”这关。

6.2 生成样本的纵向对比法

我习惯的训练完第一件事,是做一组“纵向对比”——固定同一个起始提示词,分别用不同训练步数的checkpoint生成同样长度的文本,并排放在一起看。

训练500步的模型可能只会拼出几个高频词;训练1500步的模型能拼出看起来像单词的东西;训练3000步的模型开始模仿语料的节奏和风格。这种对比比任何指标都直观,能让你清晰地看到训练过程到底给模型带来了什么。

6.3 最有效的三种升级方向

跑通第一版之后,如果想继续深入,建议按顺序走三条路线:

第一条是换分词方案。从字符级换到BPE或Unigram子词方案,词表从几十跳到几千甚至几万,模型能更高效地利用上下文信息,生成质量通常会有一次质的飞跃。代价是需要引入分词库,以及注意力计算的内存会上升。

第二条是放大模型。参数从几十万提到几百万,同时把上下文长度从128拉到256甚至512,验证集困惑度会明显改善。放大模型的同时记得同步扩大数据量,否则过拟合会提前到来。

第三条是微调路线。预训练完成的模型只是“续写机器”,如果希望它像助手一样回答问题,需要用指令数据做监督微调(SFT)。这一步会用到提示词模板和问答对数据,能直观体会到“基础模型”与“助手模型”之间的差异。

这三条路不是互斥的,可以先后组合。但每走一步都建议保留旧checkpoint,方便对比。大部分人在这个阶段容易犯的毛病是同时改好几个变量,结果出了问题根本定位不到原因。记住:一次只改一个变量,这是所有实验能让你真正学到东西的前提。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进