很多朋友问过我同一个问题:想系统学习AI工程,是不是得先啃完几本经典教材、把Transformer的每一行公式手推一遍,然后才敢碰代码?我过去也觉得应该这样,直到自己真正走完一遍才发现,答案恰恰相反。所谓AI engineering from scratch,这个"from scratch"并不等于从数学原理白手起家,而是从一条能端到端跑通的最小闭环开始,在一次次真实的失败里把知识补回来。
这篇内容把我自己从零搭建模型、训练、评估、部署的经验完整梳理了一遍,适合刚接触AI工程的学习者,也适合那些已经能在notebook里跑通模型、但面对完整项目仍然不知道从哪下手的人。你会看到一条AI工程链路到底由哪些环节构成,每个环节里最容易被低估的风险点是什么,以及怎样用最低的成本完成一次有质量的从零到一。
1. 从零开始之前,先给"AI工程"画一条边界线
1.1 算法研究与AI工程是两种不同的能力
很多入门者最大的痛苦,其实是分不清自己到底要学什么。算法研究员的重心放在模型结构、损失函数、训练技巧上,目标是让某个指标在基准数据集上提升零点几个百分点。而AI工程师的重心完全不同:数据流水线怎么搭、训练怎么复现、评估怎么做才公平、推理耗时怎么压、线上模型怎么监控。两者有交集,但分工差异非常明显。
如果你把"AI engineering from scratch"理解为纯论文复现或手写Transformer,那你走的是研究路线,学习资料和项目形态都会很学术。但如果你想做的是工程落地,那需要的几乎是另一套知识栈:Python工程化、数据处理、训练系统、模型压缩、服务部署和运维监控。我的建议是,动身之前先明确路线,不然很容易被眼花缭乱的学习资料带着跑。
1.2 最小闭环的五个环节
我给"从零开始"定义了一个最小闭环,一共五件事:拿到一份数据、清洗并按规则切分、训练一个小模型、用指标评估、部署成一个简单API。这五件事全部跑通,才算真正迈进AI工程的门。
很多人入门的路径是一上来就微调大语言模型、或者想从零预训练一个属于自己的LLM,觉得这才是"from scratch"。但说实话,那只是从零工程的一半,甚至一半都不到。没有数据工程和部署验证,模型永远只活在Jupyter里,和真正的产品之间还隔着一整条生产线。所以我后面所有章节,都围绕这五件事展开。
2. 环境搭建与项目骨架:把时间花在能复用的地方
2.1 硬件、CUDA和版本:够用就行
AI工程的环境搭建没有想象中复杂,但踩坑率不低。我的通用起点是:NVIDIA显卡 + CUDA + PyTorch + Python虚拟环境。具体选型上,我现在的建议是不要追求最新版本。举个例子,我长期使用的一组组合是Python 3.10、PyTorch 2.1、CUDA 12.1,这个组合跨了几年依然稳定。新版本往往带来更激进的API变动,一些第三方扩展库还没来得及适配,你一升级反而会引入莫名其妙的报错。
硬件方面,如果预算有限,云端GPU按小时租用比买一张卡更理性,尤其是练习阶段。我见过太多人花大几千块买了显卡,最终利用率不到一成。一个更务实的方案是:内存足够的CPU机器用来做数据清洗和预处理,配合按需租用的GPU做训练,两三个月的练习期下来,费用可能比一张显卡的零头还少。
2.2 从一开始就按工程结构组织代码
我见过太多训练脚本长成一个单文件:数据加载、模型定义、训练循环、参数配置全部堆在一起。跑通一次之后,作者自己第二天就不想再看第二眼。工程化的第一步,不是写多优雅的框架,而是把代码切成边界清晰的模块。
我常用的一套项目结构是:
project/ configs/ # yaml配置文件 data/ # 数据下载、清洗、切分脚本 models/ # 模型定义 train.py # 训练入口 evaluate.py # 评估入口 deploy/ # 推理服务相关 tests/ # 基础测试 logs/ # 训练日志与实验记录别小看这个结构,它最大的价值是让你每一步操作都能单独验证。数据脚本跑完可以检查输出,模型模块可以单独实例化,训练入口只负责把各部分串起来。配置文件用YAML而不是直接在代码里写死,是我建议从第一天就养成的习惯。训练任务的可复现性太重要了,而一份配置文件就是你每轮实验的"病历本"。
2.3 复现三件套:版本、日志、随机种子
从第一次正式训练开始,我就强制自己记录三样东西:代码版本、数据版本、超参数。代码用Git管理,数据在进入训练前先计算一次hash并记到实验日志里,超参数统一放进config文件。
随机种子需要固定,这一点新手经常忽略。不固定种子,你会被训练结果的随机波动搞到怀疑人生。我常用的配置是:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)还有一个被忽略的习惯:日志。我希望自己在第一天就坚持用日志库而不是print。print在一次性脚本里够用,但训练跑上几十个小时之后,日志文件里按时间戳排列的持续输出才是真正能帮你定位问题的工具。print会被终端缓冲截断,会淹没在滚动信息的海洋里,而日志系统可以按级别过滤、落盘、追溯。
3. 数据工程:AI项目里被低估的主力
3.1 脏数据如何进入训练集
有一个反常识的事实:在大部分AI工程任务中,数据处理的耗时占项目总工时的60%到70%。我做过一个文本分类项目,模型结构从论文里复现出来只花了半周,清洗数据却用掉三周。很多人觉得这是效率低,但真正进入生产环境后你会发现,脏数据总会以意想不到的方式出现在评估结果里。
我清洗数据时固定会做几件事。第一是去重,不仅要去完全重复的样本,还要做模糊去重,因为相似度极高的样本会把训练集分布拉偏。第二是去空和格式统一,这听起来基础,但当你面对多来源抓取的数据时,格式混乱几乎是必然的。第三是标签与内容的一致性校验,这是最容易被忽视的。当数据集来自多个渠道时,同一语义的样本可能被标成不同标签,不校正的话,模型学到的就是标签噪声。
3.2 分组切分与数据泄漏
训练集、验证集、测试集的切分方式,直接影响你对模型能力的真实判断。很多人切数据时随手shuffle一下再按比例切,这在某些场景没问题,但一旦你的数据带时间属性或存在同源聚集,乱切就会带来数据泄漏。
举个例子,在跨时间的预测任务里,你如果先把所有样本打乱再切分,验证集里就会混进"未来"的数据。模型在验证集上表现很好,因为它在训练时已经见过相近时间片的信息;到了真实世界,面对的时间点都在训练数据之后,性能立刻跳水。这就是典型的泄漏导致的乐观偏差。
我的做法是:先按一个合理的分组维度切分,比如时间、账号、会话,然后再从每个组内部采样组成训练、验证、测试集。这样能保证验证集和测试集扮演真正"没见过世面"的角色,评估结论才可信任。
3.3 tokenizer是模型的第一道加工线
在文本模型的世界里,分词器决定数据进入模型时的形状。不同分词器的词汇覆盖和压缩率差异很大,尤其对中文和代码文本,选择不当会让有效序列长度变得扭曲,进而影响训练效果。
我直接用Hugging Face的tokenizer接口,标准的处理方式是:
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") encoded = tokenizer( text, padding="max_length", truncation=True, max_length=512, return_tensors="pt" )这里有新手容易忽略的两点。第一,padding方向默认在右,训练时影响不大,但推理时某些GPU算子对左边padding的矩阵布局更友好,线上延迟会有可感知的差异。第二,分词器词表必须和模型严格匹配。你换了一个预训练模型却忘记换tokenizer,模型会疯狂报错,甚至更糟——不报错但输出乱码。
数据准备完毕前,我还会看三个维度的统计:词汇覆盖率、标签类别分布、序列长度分布。它们能直接告诉你,模型训练过程中会遇到数据层面的哪些困难。
4. 训练最小闭环:从单机小模型到多卡
4.1 刻意从小模型开始
我的核心建议是:不要一上来就训练大模型。先构建一个小规模的Transformer,在几十万条样本上跑通训练、验证、保存的全流程,再考虑扩大规模。小模型迭代成本低,你可以快速验证数据管线、代码逻辑、评估流程有没有问题。
我见过有人一上来就申请八卡A100跑一个十亿参数模型,结果代码里的数据标签错位在几十万条数据跑完之后才暴露出来,既浪费算力又消耗信心。反过来,小模型能让全流程在半小时内循环一次,这种快速反馈对学习阶段的人来说是极其宝贵的。
4.2 训练参数与损失曲线解读
以文本生成任务为例,一套足够起步的训练参数大概是:
training_args = { "per_device_train_batch_size": 8, "gradient_accumulation_steps": 4, "learning_rate": 5e-5, "num_train_epochs": 3, "warmup_ratio": 0.1, "fp16": True, "logging_steps": 50, "save_steps": 500, }fp16混合精度基本可以无脑开,显存占用和训练速度都会得到明显改善,精度损失在多数任务里可以忽略。梯度累积是为了应对显存不足的招:它把几个小batch的梯度攒起来再更新一次,效果等价于增大了batch size。但要注意,等效batch变大之后学习率一般也要跟着微调,否则收敛速度可能变慢。
训练过程中,损失曲线是最诚实的仪表盘。如果训练loss迟迟不降,先检查学习率是否合适,尝试2e-4或者做一次学习率扫描;如果训练loss在降但验证loss反弹,说明模型开始过拟合,这时可以增加数据量、调大dropout或提前停止;如果loss一开始就离谱地大,大概率是数据标签错位或者tokenizer配置有问题,不要继续等,先停下来修。
4.3 多卡训练:DDP的正确用法
当单卡显存真的不够时,再上分布式。PyTorch的DDP(DistributedDataParallel)是实现数据并行最简单可靠的方式,核心用法是:
import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP dist.init_process_group("nccl") model = DDP(model, device_ids=[local_rank])我调试分布式训练时有个原则:先在单卡上跑通一个极小数据集,确认模型逻辑和训练循环没有问题,再改成DDP。这样能把并行环境的变量和代码逻辑的变量分离开,省掉很多排查时间。
另外,如果你的单卡batch size已经小到1、还想继续增大batch,优先考虑梯度累积而不是直接上更复杂的并行策略。张量并行、流水线并行这些技术虽然强大,但配置复杂度和调试难度是另一个量级,新手阶段除非必要,不建议先啃它们。
5. 评估与迭代:让模型可衡量、可进步
5.1 评估集与指标设计
指标不能只看accuracy或者loss,这是我从几个项目里得来的教训。尤其是在文本生成任务里,验证集困惑度相差10%,换成真实用户的体验可能只是"都还行"和"有一点怪"的差别。反过来,有些模型困惑度非常好看,停下来一问却答非所问。
所以我在每个项目里都会维护一个专门的人工评估集:规模不需要大,500到1000条就够;难度要贴近真实使用场景;每轮实验后,我会随机抽几十条样本自己看一遍,感受模型输出有没有变好。这种人工观感和数值指标互为印证,才能形成完整的评估体系。
评估集的分布一旦确定,就不要轻易改动。这是第二个重要原则。你每改一次评估集,之前所有实验之间的对比都失去了公平性,你会被"我明明改进了,指标怎么反而降了"这类问题反复折磨。
5.2 LoRA微调:从零训练之外的实用路线
从零完整预训练一个大语言模型的成本,绝大多数团队和个人都承受不起。工程实践中,更主流的方式是拿一个开源的基座模型,配合LoRA做参数高效微调。LoRA把更新参数量降到极低比例,显存和时间开销都大幅下降。
LoRA的核心配置是秩r和缩放系数alpha:
from peft import LoraConfig lora_config = LoraConfig( r=8, lora_alpha=16, target_modules=["q_proj", "v_proj"], lora_dropout=0.05, )r值我在项目里试过4到32,总结出的经验是:r太小模型学不进去,最终指标偏低;r太大会失去参数高效的优势,还更容易过拟合。8到16是一个比较稳妥的区间。alpha通常取r的两倍,这个比例在多数任务里表现稳定。
5.3 一次只改一个变量
复盘自己失败的实验记录时,我发现绝大多数"模型越调越差"并不是因为方向错了,而是一轮里同时改了好几个变量,最后根本不知道改动该归功于哪一步。正确的迭代方式是:单次实验只改动一个变量,这轮只动学习率,下轮只换数据量,再下轮只调LoRA的秩。
我在项目里用一个实验对比表记录每一轮的信息:
| 实验编号 | 数据规模 | 训练轮数 | 学习率 | 评估指标 | 备注 |
|---|---|---|---|---|---|
| 001 | 50万 | 3 | 5e-5 | 0.86 | 基线 |
| 002 | 50万 | 3 | 2e-4 | 0.87 | 调学习率 |
| 003 | 100万 | 3 | 5e-5 | 0.88 | 增数据 |
积累五组以上实验记录后,趋势会自己浮现出来。这种有据可循的迭代,比凭感觉调参要可靠得多。
6. 部署与反馈闭环:模型的价值在上线后
6.1 导出、推理服务与量化
模型训练完只是第一步,把它变成能被服务框架加载的格式才是工程的一半。我通常先保存成safetensors格式,由Hugging Face生态直接管理权重。如果推理服务要跑在ONNX Runtime或TensorRT上,就需要提前做模型转换。转换过程经常出现算子不兼容的问题,我的经验是:转换前先跑一遍官方样例确认环境没有问题,再转自己的模型,否则你会分不清错误来自转换工具还是网络结构。
一个最简单的推理API用FastAPI实现,流程无非是加载模型、接收请求、分词、推理、解码、返回。但有一个细节很多人会栽跟头:模型要常驻显存,不能在每次请求时动态加载。服务启动后我会用一个虚拟请求先跑一次预热,让CUDA上下文初始化完毕,否则第一个真实用户会被冷启动拖慢几秒钟甚至几十秒。
量化是让模型跑得更快的重要手段。把FP16权重转成INT8,显存占用接近减半,推理速度提升明显,对大多数任务来说质量损失在可接受范围。我实践下来觉得torch.compile搭配INT8量化是既有稳定性又有收益的组合。需要记住的是:量化收益最明显的场景不是演示用的强机,而是资源紧张的线上部署,这时候它是优先级极高的优化选项。
6.2 监控、数据回流与静默退化
部署之后,监控不能省。我至少会记录三类指标:请求延迟、错误率、输入分布的变化。第三类最隐蔽,也最重要。
生产环境的输入分布和训练分布一旦偏离太远,模型表现会悄悄下滑。这种"静默退化"不会立刻让接口报错,用户也不会每次都吐槽,但业务效果会一点一点变差。应对办法是建立数据回流机制:定期收集线上的真实样本,进入下一轮标注和重训。模型更新的循环一旦转起来,整个AI工程才真正成为一个活系统。
7. 回望从零到一:几条带有痛感的经验
写到最后,我翻出自己过去几年的实验记录,整理出几条最想对正在从零开始的人说的话。
第一条,不要等基础全部扎实再动手。AI工程是一个极度依赖反馈的领域,很多教科书没写的细节只能从踩坑里学会,但踩坑的前提是你已经有一个能跑起来的东西,哪怕它非常简陋。
第二条,算法能力和工程能力都重要,但工程能力更容易被低估。能写好数据管道、能让训练过程复现、能定位线上问题,这些能力不显眼,在关键时刻却最值钱。
第三条,建立自己的实验记录习惯,比看一百篇教程都有效。每一组实验的配置、结果、异常都应该沉淀下来。那些看似无聊的记录,最终会成为你判断力的土壤。
第四条,关于时间预期。从零到独立跑通一条小模型的完整训练链路,快的话两到三周。从零到能可靠地交付一个生产级AI项目,节奏正常的话也需要一年左右。这类能力是磨出来的,不是看出来的。
最后分享一个我至今仍在用的习惯:每隔一段时间,就主动做一个自己没做过的小任务——换个数据集、换个模型结构、换个部署环境。这些刻意制造的"不舒适"是成长最快的时刻,也是这条路上最有意思的部分。