如果你点进这篇博文大概率是跟我一样有过这样的困惑AI 领域看起来热火朝天网上教程一搜一大把但真让自己从零上手做点什么又觉得全是“api调用”和“别人写好的轮子”。我去年做了一个名为 “ai-engineering-from-scratch” 的项目初衷很简单——不靠现成框架的“黑盒”把 AI 工程从原理到落地完整走一遍。用大白话说就是从手推矩阵乘法开始一直做到能自己训练一个微型语言模型再把模型部署成真正可用的服务。这篇文章就是我对整个项目从设计到踩坑的完整复盘。这个项目不是一本“入门书”而是一条“动手路线”。它适合三类人一是想转行做 AI 工程但觉得基础不牢的开发二是正在学校学机器学习、却只会用 sklearn 调包的学生三是已经会用 PyTorch 训练模型但遇到 loss 不降、显存爆掉这类问题就抓瞎的实践者。如果你能接受“不看答案写代码”这种略微自虐的学习方式那这个项目应该能帮你把那些零散的知识点真正串起来。1. 项目概述与目标拆解做任何项目之前先想清楚“为什么”和“做到什么程度”否则很容易在资料海里迷失。这个项目的第一步不是装环境而是把自己的目标写得非常具体。1.1 为什么选择“从零开始”这条路线我见过太多人用 PyTorch 写了半年模型却连nn.Linear在背后到底做了什么矩阵运算都说不清楚。这不怪他们因为现代框架把一切都封装得太漂亮了。但这种“封装式学习”有个致命问题遇到超出教程范围的需求你根本不知道从哪儿改起。“从零开始”的核心思路是“限定范围内不用现成高级封装”。我对自己定的规则是神经网络的前向传播、反向传播、Transformer 的 Attention 计算都不能直接调用 PyTorch 的nn.Transformer或model.backward()蒙混过关。基础训练循环可以借助框架但核心组件必须自己实现或者按源码逐行理解。这个过程非常痛苦尤其是手写反向传播的时候但熬过去之后你再看任何开源模型的代码都会有一种“这不过就是把我知道的原理工程化了一下”的感觉。从零开始并不等于“不用任何工具”。我仍然用 Python仍然用 NumPy 做矩阵运算只是把框架的“自动挡”换成“手动挡”。就像学开车老司机都说手动挡开好了自动挡闭着眼睛都能开。AI 工程也是同理核心机制理解了换个语言、换个框架只是语法层面的平移。1.2 项目的目标边界与最终产物项目没有好高骛远地去“复现 GPT-4”那是超大公司和顶级实验室的玩法。我的目标设定为三个递进阶段每个阶段都有明确的交付物阶段一用 NumPy 从零实现一个两层神经网络在公开数据集上完成分类任务准确率达到 92% 以上。阶段二从零实现一个微型 Transformer 模型参数量约 3000 万在开源短文本语料上完成训练使其能生成语法基本通顺、主题相关的短句。阶段三把阶段二的模型封装成推理服务提供 HTTP 接口支持流式输出并完成基础的性能压测。这三个阶段分别对应 AI 工程里的“原理层”“模型层”和“工程层”。很多人只做前两层第三层直接交给云服务商。但如果你想把 AI 工程作为职业方向模型落地的过程——也就是“从训练脚本到产品化”这一段——恰恰是最稀缺的经验。我给自己定的时间预算是三个月业余时间加两个完整的周末最后实际花了一百二十天左右比你想象的长是因为中间有大量时间耗在排查问题上这一点后面详聊。2. 整体路线设计与知识地图拿到一个宏大的主题第一件事永远是“切分”。AI 工程从零起步如果只看到“数学、Python、机器学习、深度学习、大模型”这些关键词很容易陷入焦虑。我最后把整个知识体系抽象成一张四层地图每层都有明确的学习材料和动手任务。2.1 四层知识架构我习惯把 AI 工程类比成盖房子。底层地基是数学承重墙是经典机器学习预制板是深度学习组件软装才是大模型微调与部署。很多人一上来就学 Transformer属于“软装先行的毛坯房”天花板看起来很高但一脚踩空就摔得很惨。第一层是数学基础。坦白讲不用等数学“完全准备好”再动手那会等到天荒地老。我真正高频用到的数学知识其实就三块线性代数矩阵乘法、转置、特征分解、概率统计分布、期望、方差、极大似然、微积分偏导、链式法则。我的做法是“用到什么补什么”比如实现反向传播之前花一个下午把链式法则从一维推到多维改写 Attention 之前补一下 softmax 的雅可比矩阵。这比从头刷一本数学教材高效得多。第二层是经典机器学习。线性回归、逻辑回归、决策树、SVM、聚类这些算法看起来老土但它们是理解损失函数、梯度下降、过拟合这些核心概念的“语料库”。我要求自己用 NumPy 把线性回归和逻辑回归各手写一遍没有调任何机器学习库。这一步帮我建立了“每个算法背后都是一组参数在优化”的直觉后面看神经网络的损失函数时完全不会觉得陌生。第三层是深度学习基础。从感知机到多层感知机MLP再到卷积神经网络CNN、循环神经网络RNN、注意力机制Attention每一个模块我都会做两件事第一自己推导前向和反向的公式第二用最小实现跑通一个玩具任务。到这一步你已经具备阅读大模型源码的基础了。第四层是大模型与工程化。Transformer 架构、预训练、指令微调、RLHF、推理优化、模型服务。这层内容更新极快我不建议死记任何论文结论而是抓住两条主线模型是怎么训练的以及模型是怎么被用起来的。我的项目在第二阶段到第三阶段之间把这两条主线完整走了一遍后面会具体拆解。2.2 技能栈与工具选型知识架构搭好后需要确定工具链。我的选型原则是“社区主流、稳定、资料多”不追新。最终确定的技能栈如下层类具体选择选择理由编程语言Python 3.10生态最全AI 工程事实标准数值计算NumPy手写算法的基础矩阵运算标准库深度学习框架PyTorch 2.x动态图友好调试方便生产部署也成熟数据处理pandas、datasets、transformers前两者处理常规数据后者直接加载开源语料实验管理wandb或本地 TensorBoard记录 loss、梯度、参数分布省心模型部署FastAPI Docker轻量、文档完善、适合快速上线这里特别说一下 PyTorch 和 TensorFlow 的选择。我不是说 TensorFlow 不好而是 PyTorch 的“命令式调试”风格对从零构建路线更友好。你可以在模型 forward 过程中随时 print 中间张量的形状和值这对理解内部机制至关重要。另外 Hugging Face 生态默认基于 PyTorch用它能减少很多“格式转换”的额外工作量。工具不在多在于每个都能发挥它的位置价值。比如我没有用任何 AutoML 工具因为在“从零开始”的项目里自动调参反而会剥夺你理解超参数的机会。当你手动调了几十次学习率之后你才会真正理解所谓“学习率从 3e-4 降到 1e-5”这种经验值背后的直觉。3. 核心环节实操从零训练一个微型语言模型这个项目最“硬核”的部分在第二阶段自己动手把 GPT 那样的模型当然是缩小版训练出来。我原以为最难的是 Attention 公式推导真正做下来才发现最难的是数据准备和训练稳定性的把控。下面我把这条路径拆成四个可以直接照做的环节。3.1 数据准备与预处理训练语言模型的第一步是获得“高质量、足量”的文本。我这里强调两个词缺一不可。首先是“足量”。大模型领域有一个粗略的经验法则训练 token 量至少应该是模型参数量的 20 到 100 倍。我的模型是 3000 万参数因此至少需要 6 亿到 30 亿 token。这听起来很吓人但其实几个 GB 的普通文本就够几十亿 token 了。我最初用的是开源中文语料库清洗后拿到约 1.2 GB 纯文本大概折算 5 亿到 8 亿 token对一个小模型来说算“勉强及格”。其次是“高质量”。原始语料里有大量 HTML 标签、乱码、重复片段、无意义符号。我写了一个清洗管道按顺序做了几件事去除 HTML 标签和 URL这部分用正则表达式即可。统一换行符把\r\n统一成\n。按长度过滤丢弃过短小于 50 字和过长大于 1000 字的段落。过短的多是表单碎片过长的多是版权书籍全集都会干扰训练。做“重复惩罚”很多语料会反复出现同样的段落我对连续重复 3 次以上的片段直接删除。清洗之后文本量少了 30%但训练出来的模型明显更“清醒”不会像复读机一样循环输出同一段话。接下来是分词。我当时面临一个选择用现成的tokenizers库训练一个 BPEByte Pair Encoding词表还是自己手写一个分词器。既然是“from scratch”项目我选择用tokenizers库训练 BPE 模型但完全理解了 BPE 的合并逻辑——先统计字符频率再迭代合并最高频的相邻符号对。这个过程可以用一个生活化类比理解就像把“人工智能”这四个字先切成单字然后发现“人工”频繁一起出现就把它们绑成一个词再发现“人工智能”整串频繁出现又把三个片段绑成一个整体。词表越大语义单元越大但太大也不好会让嵌入矩阵异常庞大。我最终定了 8000 的词汇量对小语料比较合适。3.2 实现微型 Transformer 的核心组件到这里模型架构正式登场。我不会把全部代码贴出来那会变成源码解析文章但会把最核心的两个组件——掩码多头注意力和位置编码——用“原理加关键代码片段”的方式讲透。先说整体参数配置。我的微型 Transformer 配置如下层数 6 层、隐藏维度 512、注意力头数 8、前馈网络隐藏维度 2048、词表 8000、最大序列长度 256。这个配置的参数量大概在 2900 万左右计算公式可以拆解如下词嵌入矩阵vocab × hidden 8000 × 512约 410 万参数。每层注意力Q、K、V 三个投影矩阵各为 hidden × hidden输出投影也是 hidden × hidden合计 4 × 512 × 512约 105 万参数。每层前馈网络两个线性层分别为 hidden × 4hidden 和 4hidden × hidden合计 2 × 512 × 2048约 210 万参数。每层还有两个 LayerNorm参数很少可忽略。所以每层约 315 万参数6 层约 1890 万加嵌入 410 万再加最后的输出投影与嵌入权重共享如果共享则不再增加加起来约 2300 万到 2900 万。我为什么强调参数量计算因为这直接决定显存消耗和训练时间。以 2900 万参数为例混合精度训练时模型权重、梯度、优化器状态加中间激活值一张 12 GB 显存的显卡勉强能跑起 batch size 8。如果配置翻倍到 1 亿参数显存直接爆掉。这就是我反复说的不动手算一次你永远不会对“模型多大才叫大”有概念。注意力机制的实现我直接手写了scaled_dot_product_attention的 PyTorch 版本但特意没有用F.scaled_dot_product_attention这个封装。核心代码逻辑并不复杂def scaled_dot_product_attention(q, k, v, maskNone): d_k q.size(-1) scores torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k) if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) attn_weights torch.softmax(scores, dim-1) output torch.matmul(attn_weights, v) return output, attn_weights这段代码里有个细节非常值得讲为什么要除以sqrt(d_k)。如果不缩放当维度 d_k 变大时点积结果会变得很大经过 softmax 之后梯度会进入饱和区导致训练难以收敛。除以缩放因子本质上是把注意力得分的方差拉回一个合理的范围。这个小小的sqrt(d_k)是 Transformer 原论文提出的关键改进也是“知其所以然”的典型例子。位置编码我用的是可学习的绝对位置嵌入而非原版的正弦余弦编码。对小型模型来说两者效果差异不大但可学习嵌入实现更简单而且可以在训练中自动调整。这里暴露了一个“从零实现”的陷阱你很容易忘记加位置编码然后发现模型训练出来的结果跟词袋模型差不多——因为注意力机制是“位置无关”的你把“我爱你”和“你爱我”当成同一个意思了。这个坑我踩过后来在输入 embedding 后直接加上位置 embedding才恢复正常。3.3 训练循环、损失函数与评估训练一个语言模型核心目标就是让模型预测下一个 token 的概率分布尽可能接近真实的下一个 token。这个目标被转化为交叉熵损失函数。你训练时做的一步步操作本质上就是在说“模型认为下一个词是‘你’的概率是 0.01但正确答案是‘我’所以要加大梯度惩罚”。训练循环的结构跟经典机器学习几乎一样从数据集中采样一个 batch 的文本序列。把序列喂给模型得到每个位置的预测 logits。计算logits与真实target序列右移一位之间的交叉熵损失。调用loss.backward()计算梯度。用优化器AdamW更新参数。周期性记录 loss、步数、学习率、token 数。训练初期loss 大概在 8 到 9 左右因为模型基本在“瞎猜”8000 个词。训练到 20 万步以后loss 能降到 3 左右。这时候模型生成的句子已经出现了明显的语法结构比如“他在学校里学习数学和物理”这种主谓宾完整的句子。评估语言模型最常用的指标是困惑度Perplexity缩写 PPL它和交叉熵损失的关系是PPL exp(loss)。当 loss 从 8 降到 3PPL 就将近下降了 150 倍说明模型对下一个词的预测能力有了质的飞跃。训练过程中的“温度”参数也很重要。推理时模型的输出是 8000 个词的概率分布如果每次取概率最高的词贪心解码生成的句子会非常死板像复读机。如果引入温度缩放温度高于 1 会让分布更平滑、更多样化温度低于 1 会让分布更尖锐、更保守。我在测试生成效果时用温度 0.8既能保持通顺又有一点小惊喜比如模型偶尔会自发组合出“夜色中的城市像一条发光的河流”这种颇具画面感的句子。4. 工程化落地与工具链细节模型在笔记本里能生成小句但“能跑”和“能用”之间隔着一道工程化的鸿沟。项目第三阶段就是把这个小模型变成真正的服务。这一部分的经验和深度往往是教程里缺失的。4.1 环境准备与依赖版本AI 工程最让人头疼的问题之一就是环境复现。我自己的“血泪史”是训练完模型三个月后再打开项目发现 PyTorch 已经从 1.13 升到了 2.1某些 API 行为变了代码直接跑不起来。从那以后我养成了两个习惯用虚拟环境锁版本以及把关键依赖完整记录在requirements.txt。我的环境组合是Python 3.10 torch2.0.1 transformers4.35.0 datasets2.14.6 tokenizers0.15.0 wandb0.16.0 fastapi0.104.1 uvicorn0.24.0为什么没有用最新版因为训练大模型这件事稳定性比新鲜度重要。新版本常常会改一些底层行为比如 PyTorch 2.1 就把某些算子的默认实现改了可能导致训练结果与之前的实验不可比。做 AI 工程我强烈建议把“可复现”放在第一位再考虑“用新功能”。另外如果你有 NVIDIA 显卡CUDA 版本一定要与 PyTorch 对应否则装完 torch 后会发现它默认用 CPU 跑速度慢到怀疑人生。4.2 实验管理、随机种子与可复现性AI 实验天然带有随机性如果不做控制你会发现“昨天跑出来的 loss 今天复现不了”。我一开始没太在意随机种子直到有次训练到一半发现 loss 曲线莫名其妙地波动很大排查半天才发现是数据加载时没有固定 shuffle 的随机种子。解决方法是把随机性参数统一管理起来def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)在训练脚本开头调用set_seed()后同一份数据、同一个配置两次训练的前几步 loss 完全一致。这不是强迫症这对后续做消融实验比如比较“加位置编码”和“不加位置编码”的差异至关重要。如果没有固定种子你根本无法判断效果差是“改动导致的”还是“随机波动导致的”。实验管理我用了 wandb。它的核心价值不是“炫酷图表”而是每一次实验的超参数、代码版本、loss 曲线、生成样例都被自动记录下来。当训练了五十多次之后我可以通过表格对比发现“学习率 1e-4 比 3e-4 收敛得更慢但最终更稳”“batch size 16 比 8 的 final loss 低了 0.2”。这种“循证式调参”比看几篇博客然后盲调靠谱得多。4.3 从训练到推理部署模型训练完成后保存的是model.state_dict()权重文件。把模型变成服务其实就两步加载权重封装接口。我用 FastAPI 写了一个极简的推理服务核心接口就一个POST /generate接收 prompt 和生成参数流式返回生成文本。这里有一个大多数人第一次都会踩的坑训练时模型用的是“训练模式”model.train()其中 Dropout 和 LayerNorm 的行为都面向训练推理时必须切换成model.eval()并且用torch.no_grad()关闭梯度计算。否则每次推理结果都会不一样速度还会慢一大截。这背后的原因是 Dropout 在训练时随机丢弃神经元在推理时必须关闭否则模型输出就是“随机的”。部署的另一个关键细节是“自动混合精度与推理精度”。训练时我用torch.cuda.amp做了混合精度权重可能是 float16 和 float32 混合的推理时建议统一转成 float16显存占用直接减半速度提升也很明显。但要注意float16 在某些 CPU 上不支持需要回退到 float32。我没用 Docker 的时候模型在本机跑得好好的一放到服务器就“无法连接”。后来学乖了写了一个干净的Dockerfile基于pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime镜像把模型权重和代码一起打进去在服务器上用同一镜像启动容器一次成功。这就是我前面说的“可复现性”在工程领域的延续环境版本相同行为才可能相同。5. 常见问题与排查实录这一部分是从零开始项目里真正拉开差距的地方。训练代码谁都能写出来但遇到问题能不能快速定位决定了你是“会用工具”还是“懂原理”。我把整个项目里最典型的四类问题整理成了一份排查速查表每个都搭配了实操处理方法。5.1 训练不收敛先看数据再看学习率我训练时遇到的第一类大坑是 loss 忽高忽低完全不知道模型在学什么。排查思路分为三步先看数据把训练脚本里每个 batch 的输入打印出来肉眼检查是否出现“标签错位”。我犯过一个低级错误把 target 设置成了序列左移一位而不是右移一位模型相当于在“预测前一个词”自然永远学不好。再看 loss如果 loss 一开始就在 9 附近抖动但迟迟不下降极大概率是学习率太小或者梯度消失。我习惯在前 500 步用一个较小的“warmup”范围快速试探比如从 1e-5 到 1e-3观察 loss 的最初下降速度。如果完全不动就把学习率调大两个数量级再试。最后看梯度用clip_grad_norm_限制梯度范数到 1.0。大模型的梯度范数偶尔会涨到几十甚至上百这是梯度爆炸的前兆。加了梯度裁剪之后训练稳定性会立竿见影。5.2 显存不足与训练速度慢我的显卡是 12 GB 显存的中端卡一开始跑 3000 万参数的模型batch size 设为 32直接报CUDA out of memory。解决方案不是换显卡而是做梯度累积。原理很简单显卡一次只能算 8 个样本但 8 个样本的梯度太小、更新不稳于是我把 4 组 8 个样本的结果累加后再做一次参数更新等效于 batch size 32。梯度累积还有一个附带好处可以动态调整总 batch size而无需占用更多显存。如果你追求更高效训练还可以开混合精度用torch.cuda.amp把前向计算改为 float16显存占用几乎减半。我在项目中同时用了这两招训练速度提升了近 70%这也印证了“工程优化不是靠玄学而是靠组合策略”。5.3 推理结果越来越差模型被“带偏”了训练完成后的模型在线服务一开始效果不错但跑了几百次请求后输出开始出现重复甚至胡言乱语。排查后发现两个问题一是“重复惩罚系数”没有调好。生成时如果 p 值核采样设得太大模型倾向于高频重复输出“安全”的内容。办法是把top_p从 0.95 降到 0.85同时把温度下调到 0.7重复现象明显减少。二是显存不够导致推理进程被杀但服务没有自动恢复。这不是“算法问题”纯粹是工程容灾问题解决方案是给服务加监控和自动重启或者把模型加载到 CPU 上再用torch.no_grad()做批量预计算减轻 GPU 压力。为了让你能快速排查我把常见问题汇总成下边这张表症状可能原因快速排查与修复loss 不降数据错位、学习率过小、梯度异常打印样本检查数据对齐用 warmup 区间试探学习率开启梯度裁剪显存不足batch size 过大、没有混合精度梯度累积开启自动混合精度降低序列长度生成重复温度/Top-p 参数不合适温度降到 0.7~0.8Top-p 降到 0.85推理速度慢未关闭梯度、没有用 eval 模式model.eval()加torch.no_grad()权重转 float16服务偶发崩溃显存压力、容器无自动重启加监控Docker 加restart: unless-stopped6. 个人经验与进阶建议项目做到这里“从零构建语言模型”的技术闭环已经打通但我的真实体会是这个项目最大的收获反而不是模型本身的性能而是建立了一种“遇到问题敢拆解”的信心。以前遇到大模型相关的报错我第一反应是上网搜别人有没有踩过同样的坑现在我会先尝试自己定位先看数据再看模型结构再查梯度最后才怀疑框架和硬件。这个排查顺序虽然不能保证每次都最快但每次解决之后我对整个系统运转机制的理解就更深一层。如果让我给准备开跑类似项目的人一个建议我会说把“最小可运行版本”作为第一个里程碑不要指望一步到位。很多人一开始就想着把数据清洗做得完美、把模型结构调得最优结果折腾了两周还在准备阶段。正确的做法是先用 10 万条数据、一个 10 层的微型模型跑通全流程——哪怕生成结果很蠢只要“数据→训练→评估→推理”这条链路是通的后面所有的优化都只是迭代。这就是工程上的“垂直切片”思想先打通主干再填充细节。这个项目后续还能怎么扩展我看到近期很热的“build a reasoning model from scratch”方向本质上就是在我的第二层和第三层之间加入推理能力的强化训练。你可以尝试在现有小模型上加入思维链数据微调或者引入部分强化学习技术来提升模型的多步推理能力。另一个值得做的是把工程化部分往生产环境推进一步加一个简单的检索增强RAG模块让模型在处理开放域问题时能引用外部知识这会让你从“模型训练”自然过渡到“AI 应用开发”。最后再分享一条我在踩过几次坑后悟到的经验不要总想着用最新的模型结构、最前沿的技术点。AI 工程里真正考验人的往往是最基础的东西——数据是否干净、版本是否可控、梯度是否稳定、服务是否健壮。把这些“笨功夫”做扎实了再去追热点你会发现那些所谓的新技术其实都是你已经熟悉的知识体系的一种新组合。这就是我把这个项目起名为 “ai-engineering-from-scratch” 的全部意义地基打牢了往上盖什么楼都只是时间问题。