从零训练中文语言模型:手写Transformer与预训练微调全流程

发布时间:2026/10/4 21:42:02

从零训练中文语言模型:手写Transformer与预训练微调全流程 把“AI engineering from scratch”当口号的人很多真正从零手搓过一遍的人比例很低。我去年完整走过一遍自己清洗数据、从零训练分词器、手写Transformer核心模块、把小模型喂到收敛、再做推理能力微调。整个过程如果用商业眼光衡量确实不划算但从工程角度它值回所有踩坑成本。这篇就聊聊我整套项目从设计到落地的全流程适合那些不满足于“调一堆API拼个demo”、想真正理解AI系统底层机制的工程师和研究者。从Hugging Face上拉个开源模型来做微调和从一堆纯文本/代码语料开始亲手构建一个可用的语言模型技术视野完全不一样。前者是“使用技术”后者才是“engineering from scratch”。1. 内容整体设计与思路拆解1.1 为什么非要“从零训练”而不是继续叠API现在的技术环境下大多数人接触AI工程的第一站是API调用、RAG、Agent编排这类上层能力。这些当然有价值但它们把最核心的问题——数据、模型结构、训练目标、损失函数——全部黑盒化了。当上层框架出现奇怪行为时你很难定位是检索的问题、提示词的问题还是模型本身能力边界的问题。我决定做这个项目的出发点很直接想把模型训练的每一个环节变成自己能控制的东西。从零训练一个模型意味着数据配比、词表设计、模型结构、训练策略、推理增强全部由我决定任何一个环节出问题都可以定位到具体的代码和配置。这种“手术刀式的可控性”是直接用开源模型微调无法获得的体验。这个项目确立的目标很保守做一个参数量在1亿左右的中文语言模型具备基础的文章续写能力并通过推理数据微调让它具备简单的分步思考能力。不追求对GPT级别产品的突破就为了把从数据到模型的每个环节至少深入走一遍。1.2 技术路线选型小规模、全链路、可复现在技术路线选择上我对比了三条路路线优势劣势微调开源大模型如Qwen、LLaMA效果好、速度快底层机制不透明分词/架构被锁定从零训练小模型本项目全链路可控、理解最深效果有限需要自己解决所有细节蒸馏开源模型行为逼近大模型依赖教师模型数据构造复杂三条路里我选了从零训练小模型。核心原因是“可控”两个字。词表是我自己训练出来的模型结构是逐层手写的训练脚本里的每一个参数调整都清楚知道在改变什么。哪怕是作为一次学习型项目这个回报也比训练出一个性能更强的模型高得多。硬件方面我当时用一张RTX 4090 24GB跑完全程显存其实还有富余。实测1亿参数模型在batch size 20、序列长度512的情况下显存占用大约17到18GB这个配置对个人开发者非常友好。2. 核心细节解析与实操要点2.1 数据从哪来怎么处理才能喂给模型模型的能力上限基本由训练数据决定。这句话听起来中庸但它是这个项目里最真实的约束。我用的是开源中文语料和公开的数学/代码数据混在一起做预训练。数据处理有几个必须过的关卡第一是去重。文本里大量重复会直接压低模型输出的多样性训练时还会导致loss假性收敛。我用MinHash做了一轮去重把重复率从原始的约15%压到了3%以下。这一步在数据量不大的情况下比再增加几百万条低质量数据更有效。第二是过滤低质量内容。我按长度、特殊字符占比、语言类型三个维度写了过滤脚本。长度太短的比如纯HTML标签、特殊字符占比超标的比如乱码和符号密集文本、判断为非中文/英文的大段内容全部去掉。质量过滤的逻辑很简单宁可数据少不能数据脏。第三是配比。预训练数据里我放了约85%的中文通用文本、10%的代码、5%的数学和逻辑类文本。这个比例的目的是让模型在语言建模的基础上拥有一定代码和逻辑推理能力为后续reasoning数据的微调打基础。清洗完之后剩余可用于训练的有效文本量大约是4.6GB凑出来约25亿个token的原始语料。对1亿参数的小模型这个量已经绰绰有余——小模型喂太多数据反而会因为容量不足而欠拟合。2.2 tokenizer为什么要自己训不直接用现成的直接用开源模型的tokenizer省事但它隐含着一个问题词表是别人按照他自己的语料分布设计的。我的语料和训练目标跟他不完全一样直接在别人的词表上训练遇上分词颗粒度不匹配的文本效率会偏低模型的表达自由度也会被限制。我选择用BPEByte Pair Encoding算法自己训练一个词表。BPE的原理不复杂从单个字符开始反复统计并合并语料中出现频率最高的相邻字符对直到词表大小达到预设目标。实操时我用的是Hugging Face的tokenizers库但去掉默认的下载流程完全走本地训练from tokenizers import Tokenizer, models, trainers, pre_tokenizers, decoders, processors # 初始化一个空tokenizer使用BPE模型 tokenizer Tokenizer(models.BPE()) tokenizer.pre_tokenizer pre_tokenizers.ByteLevel(add_prefix_spaceFalse) tokenizer.decoder decoders.ByteLevel() # 设置训练器和参数 trainer trainers.BpeTrainer( vocab_size16384, min_frequency2, special_tokens[pad, s, /s, unk], show_progressTrue, ) # 用预处理好的文本文件训练tokenizer files [data/pretrain_01.txt, data/pretrain_02.txt] tokenizer.train(files, trainer) # 保存 tokenizer.save(tokenizer.json)有两个参数需要特别注意。vocab_size16384是我权衡后的选择词表太小会导致单字拆得很碎序列变长、训练效率低词表太大又会让embedding矩阵膨胀小模型根本学不好。16K对中文英文代码混合的1亿参数模型是够用的。min_frequency2意味着至少出现两次的合并才有意义低于这个频次的合并规则会引入大量噪声。一个常见的坑是ByteLevel预分词会把空格也视为普通字符进行合并所以中文文本里如果既有全角空格又有半角空格要提前统一归一化。我在清洗数据时把所有全角空格替换成普通空格并把多个连续空格压缩成一个否则训练出来的词表里会多出一堆空格相关的冗余合并规则。3. 实操过程与核心环节实现3.1 手写Transformer核心模块参数怎么定才合理模型架构我采用了标准的decoder-only Transformer加上三条关键设计Pre-Norm层归一化放在残差之前、Rotary Position Embedding旋转位置编码、GELU激活函数。这三样是现代小模型训练的“标准安全配置”Pre-Norm让深层网络训练更稳定RoPE在无需学习位置参数的同时外推性好GELU的平滑梯度比ReLU更容易收敛。1亿参数的模型我用了一张配置表来锁定所有结构参数参数项数值说明层数 L8小模型不需要太深8层足够表达注意力头数 H8每头维度64共512维模型维度 d_model512嵌入层和隐层尺寸FFN中间维度20484倍d_modelGELU后接线性层词表大小16384tokenizer训练时已确定最大序列长度512训练时统一截断到512token总参数量约95M主要分布在embedding和两层FFN95M的参数量里embedding矩阵本身占掉16384×512×2约1680万个参数包括输入输出共享或否定的部分。我使用了权重绑定tie weights让输出层的权重和输入embedding共享显著减少了参数量。核心Transformer Block的代码不复杂实现在我手写时最关键的是缩放逻辑。注意力分数除以sqrt(head_dim)这步不能省它的作用是防止点积结果过大导致softmax进入饱和区。还有个细节因果掩码要加上-inf而不是0确保每个位置只能看到当前位置及以前的token。import torch import torch.nn as nn import torch.nn.functional as F class CausalSelfAttention(nn.Module): def __init__(self, d_model, n_heads): super().__init__() self.n_heads n_heads self.head_dim d_model // n_heads self.qkv nn.Linear(d_model, 3 * d_model, biasFalse) self.out_proj nn.Linear(d_model, d_model, biasFalse) def forward(self, x): B, T, C x.shape qkv self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim) q, k, v qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2] attn q k.transpose(-2, -1) / (self.head_dim ** 0.5) mask torch.tril(torch.ones(T, T, devicex.device)).view(1, 1, T, T) attn attn.masked_fill(mask 0, float(-inf)) attn F.softmax(attn, dim-1) out attn v out out.transpose(1, 2).reshape(B, T, C) return self.out_proj(out)3.2 训练循环学习率、batch和梯度累积的配合训练部分我实现了一个标准的脚本核心参数如下优化器AdamWlr 3e-4weight_decay 0.1学习率调度warmup 2000步然后余弦退火到1e-5每步batch20个样本每个样本512个token等效每步10240 token梯度累积显存有余所以没开累积直接全batch更新总步数约80000步warmup这步很关键。模型在冷启动阶段参数是随机的如果直接上大学习率梯度幅值波动很大容易把loss推到很高的状态之后要花很多额外轮次才能收回来。warmup在前2000步让学习率从0平滑升到3e-4本质上是在“预跑”阶段用低学习率把模型参数稳定到合理区域内。余弦退火的作用是在训练后期慢慢降低学习率让loss在小范围内精细收敛。实际观察下来最后2万步的loss下降速度虽然慢但模型生成的文本质量提升非常明显尤其是句子的连贯性。训练耗时方面我在4090上实测大约是每秒2.4步折算下来一小时能处理约880万token。8万步跑完大概需要9个多小时但我中间断点续跑了几次实际跨了三天左右。每2000步保存一次checkpoint每次保存大约380MB。这里要提醒一句一定每2000步至少存一个checkpoint。我在训练到约6万步时显卡驱动一度不稳定导致训练中断幸好有checkpoint否则从头再来代价太高。训练到8万步时训练loss降到了约2.1验证集loss约2.4。从生成效果看模型已经能写出逻辑基本通顺的中文段落但长文本后半段偶发跑题——这是小模型容量有限导致的正常现象并不是训练失败的信号。4. 训练过程中的关键观测与调优4.1 损失曲线究竟该怎么看刚开始训练时很多人心态会被loss曲线吓到。我的实际记录是第0步loss约10.8第500步降到7.5第2000步降到5.2第10000步降到3.8。如果单看前几百步会以为这模型废了其实是正常的“随机初始化—快速压缩冗余信息”阶段。真正要看的是曲线是否平滑下降、有没有突然的尖峰。我在训练中遇到过两次loss突然跳到前一步的2到3倍高排查后结论是一致的训练数据里混进了几段损坏的文本主要是截断不完整的Unicode字符和超长重复字符序列。修复方式是写了个规则把所有连续相同字符超过50个的文本段剔除。数据处理阶段多花一小时训练阶段就可以少踩三次坑。验证集loss和训练loss的差距是我主要监控的过拟合指标。1亿参数模型在约25亿token的数据上训练8万步远没有达到过拟合区间。两个loss的gap始终保持在0.2到0.4左右说明模型容量不够用了而不是记住了数据。4.2 显存优化和混合精度训练要注意什么初始版本我直接用FP32训练显存占用居高不下1亿参数的模型一度被推到20GB以上。后来我改成混合精度训练也就是模型参数和优化器状态保持FP32前向和反向计算用FP16显存占用降到了约17GB。混合精度最经典的问题是loss变成NaN。原因通常是梯度幅值在反向传播中超过了FP16的表示上限。解决措施是开scaler.scale(loss)梯度缩放同时在每次更新前把梯度clip到max_norm1.0。如果还不稳定小于目标参数的层换成FP32计算。如果你想在更小的显卡上跑有两个方向一是把序列长度从512降到256模型效果会略降但显存压力小一半二是用梯度累积比如accumulation_steps4用时间换显存。我个人实测1亿参数在16GB显存上配合梯度累积是完全可以跑的。5. 让模型学会“思考”reasoning 微调实战5.1 思维链数据从哪来预训练完成后模型能续写文本但并不具备“分步推理”能力。所谓reasoning在工程上就是让模型在给出最终答案之前先生成一段推理过程。让模型学会这种结构最直接的方式是用思维链数据做监督微调。数据来源我用了公开的数学和逻辑数据集如GSM8K、MATH的一部分以及我自己构造的一批“问题-逐步推理-答案”示例。数据规模不需要大30000条左右即可。微调数据集规模过大反而可能让模型在推理任务上过拟合降低通用能力。思维链示例的数据格式如下{ instruction: 小明有5个苹果他给了小红2个又从商店买了3个。小明现在有几个苹果, reasoning: 小明原有5个苹果。给小红2个后5减2等于3个。又买3个后3加3等于6个。所以小明现在有6个苹果。, answer: 6个苹果 }注意整个流程和典型的对话微调模板一致训练时把instruction、reasoning、answer拼接成一段完整文本只是在reasoning和answer中间加一个特殊分隔符方便推理时截断。5.2 SFT微调后如何部署和验证推理能力微调用的是和预训练相同的优化器配置但学习率降低了10倍设为3e-5步数只需要3000步。这是在现有模型基础上做“精细修整”学习率过高会破坏预训练学到的语言能力连基本的文本连贯性都会退化。微调完成后我在几道数学题上做了验证。以“一个长方形长8厘米宽5厘米求面积”为例模型一开始直接输出“40平方厘米”而不给过程这是预训练模型的典型行为。SFT之后模型会先输出“长方形的面积等于长乘以宽。长是8厘米宽是5厘米。所以面积等于8乘以5等于40平方厘米”再输出答案。这个变化就是训练推理结构的直接体现。推理部署上我用的是最朴素的generate循环from transformers import AutoModelForCausalLM, AutoTokenizer model AutoModelForCausalLM.from_pretrained(./checkpoint-sft-3000) tokenizer AutoTokenizer.from_pretrained(./tokenizer) prompt 一个三角形底边长为6厘米高为4厘米求面积。 inputs tokenizer(prompt, return_tensorspt) outputs model.generate( inputs.input_ids, max_new_tokens256, temperature0.7, top_p0.9, repetition_penalty1.1, ) print(tokenizer.decode(outputs[0], skip_special_tokensTrue))temperature0.7和top_p0.9是给推理留一点随机性避免多次生成完全一致的结果repetition_penalty1.1是为了抑制小模型常见的重复输出问题。小模型生成长文本时特别容易出现“翻来覆去同一句话”的情况这个惩罚系数是实测下来最有效的缓解手段。6. 常见问题与排查技巧实录6.1 高频问题速查表现象可能原因解决方案loss不降或下降极慢数据未清洗干净/学习率太低检查数据质量、提高学习率或检查warmuploss突然变成NaN混合精度梯度溢出开启梯度缩放、梯度裁剪到1.0生成文本大量重复小模型容量不足或温度太低提高temperature或使用repetition_penalty模型输出乱码tokenizer词表或解码器不匹配检查tokenizer训练时是否用了同样的pre_tokenizer显存OOMbatch或序列长度过大减小batch、缩短序列或开梯度累积训练loss过低但验证集效果差数据泄露或验证集与训练集重复用MinHash跨集合去重保存的checkpoint无法加载参数名称不匹配或torch版本不一致保存时同时存state_dict和完整模型结构6.2 避坑经验先小后大、盯紧中间结果这项目我最大的教训是“一上来就想跑大模型”。第一次我把目标定在3亿参数结果数据准备、训练时长、调试周期全部拉长最终因为显存和耐心双双不足而搁置。第二次我把目标砍到1亿参数才真正跑通了全流程。另一个经验是“先做烟雾测试”。所谓烟雾测试就是用极小数据量比如一万条跑一个几十步的训练确认loss在下降、checkpoint能保存、推理能加载、生成是中文而不是乱码。在烟雾测试通过之前不要启动正式训练。这一步能省下大量无效等待。还有一个容易忽视的细节不同版本的PyTorch对Transformer某些算子的实现有差异同一份检查点文件在不同版本之间可能出兼容问题。我在实验中锁定了一套环境组合写入requirements.txttorch2.1.2、transformers4.37.2、tokenizers0.15.1、datasets2.16.1。环境越固定可复现性越高。最后再说几句跑完这套项目之后我的最大感触是代码里真正难的不是某个单独环节而是所有环节咬合在一起时的拧巴感。数据形状多一维、注意力掩码少一维、tokenizer词表和模型embedding维度对不上、训练脚本和推理脚本的sequence长度不一致——这些才是从零构建AI工程最真实的日常。如果有人想复现这条路我给三个建议。第一参数先往小做跑通全流程比做出“能用的模型”优先级高得多。第二每次训练启动前把训练配置打印出来和你的tokenizer词表大小、数据量级做一次人工核对避免低级配置错。第三保存中间checkpoint不是技术洁癖是对自己时间的尊重。我到现在还留着第一次训练时打印的loss曲线截图那是一条充满毛刺但一路向下的曲线。对一个从零开始的项目来说这大概就是最好的回报了。
延伸阅读

更多相关文章

2026/10/4 21:42:02

MIPI LP RX硬件设计实战:从信号完整性到FPGA实现

1. 项目概述:MIPI LP RX到底在解决什么问题?MIPI LP RX——这个缩写组合乍看像一串技术代号,实则直指一个高频、高痛、高门槛的硬件接口工程现场:低功耗(Low-Power)模式下的MIPI接收端(Receiver…

2026/10/4 21:37:02

从零开始AI工程:数据、训练、部署到持续迭代的实战指南

最近在复盘自己过去几年做的AI项目,突然想起最初看到“AI engineering from scratch”这个题目时的情景:当时我以为AI工程就是训练一个准确率很高的模型,结果被现实狠狠上了一课——模型跑通只是万里长征第一步,从数据收集到上线监…

2026/10/4 22:42:05

ANORD-DCT-DWT协同音频水印:嵌入稳在听觉盲区

简介:本资源是一套基于MATLAB实现的音频水印嵌入与提取完整方案,面向数字媒体安全、信息隐藏方向的本科生、研究生及算法工程师,解决在二值图像中隐秘嵌入并高保真还原音频信号的技术问题。项目融合单稳态DCT频域编码、离散小波变换&#xff…

2026/10/4 22:42:05

多芯插件机制深度解析:SGLang 在昆仑芯上的部署与调优实践

跑大模型推理服务这几年,我最大的感受是:真正卡脖子的往往不是模型本身,而是框架对底层芯片的适配。同一套代码,换个加速卡可能就要改一大堆算子、重新编译、再调半天Batch策略。最近团队在把 SGLang 部署到昆仑芯(Kun…

2026/10/4 22:42:05

开源商业化论坛观察:从商业赋能到全球共生的实操指南

1. 论坛定位与议程设计思路拆解1.1 “商业赋能”与“全球共生”在议程里的真义COSCon’25把“商业赋能,全球共生”这个主题放在开源全球商业化论坛上,说实话,第一次看到这个标题时,我脑子里浮现的是过去五年开源圈里反复争论的一个…

2026/10/4 22:37:05

JavaScript性能优化完全指南

上周四凌晨,我们的仪表盘系统在客户数据量突破50万条时突然崩溃。监控显示主线程被一个简单的数据过滤函数阻塞了整整12秒——而同样的操作在测试环境下仅耗时200ms。你猜问题出在哪儿?不是网络请求,不是数据库查询,而是那段看似无…

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑