"ai-engineering-from-scratch"这个名字最早出现在我笔记里,还是三年前。当时我在一家互联网公司做后端,每天看着算法团队的PPT——"训练损失下降""模型效果提升"——心里很清楚自己在AI面前就是个门外汉。真正促使我开始动手的是一次需求评审会,产品经理问我:"这个AI功能大概多久能上线?"我答不上来,因为我对整个训练链路毫无概念。于是我从零开始,花了大约八个月时间走完整条路:从补数学基础、写Python脚本,到用numpy手写线性回归,再到从零实现一个几十万参数的小型语言模型,最后把它做成一个API服务部署上线。
这篇文章就是那条路线图的完整复盘。我会把每一阶段的核心任务、关键选择、踩坑记录和时间规划都摊开来讲。不管你当前是完全没写过代码的转行者,还是和我当年一样有编程背景但没接触过AI的后端工程师,这条"from scratch"路线都能直接用。
1. 为什么"from scratch"路线值得走
1.1 AI工程不是"调包",而是一条完整链路
我见过太多人,一上来就装好 transformers 库,跑通一个情感分析 demo,然后觉得自己已经入行了。等真遇到实际项目就全懵了:数据为什么这么脏?模型线上效果为什么和线下差那么多?显存爆了怎么办?接口延迟太高怎么优化?
这些问题恰恰是 AI 工程的核心。AI 工程不只是一个算法问题,它是一条从数据到模型的完整链路:
- 数据工程:采集、清洗、标注、特征构造、数据版本管理。
- 模型工程:模型选型、架构设计、训练策略、超参调优。
- 推理工程:模型压缩、量化、部署、推理加速、弹性伸缩。
- 评估工程:离线评估、线上测试、效果监控、数据回流。
任何一个环节掉链子,整个系统都跑不稳。而"from scratch"的意义,就是让你有机会亲手把这条链路上的每一个环节都走一遍,而不是永远停留在"别人做好了什么,我拿过来用"的层面。
1.2 "从零开始"不是回到原始社会,而是重建心智模型
有人一听"from scratch"就害怕,以为要把整个数学体系重学一遍,甚至连深度学习框架都不能用,一切从寄存器写起。这完全是误解。
我理解的"scratch"有三层含义:
- 理论从零:不跳过必要的算法原理。你要能解释梯度下降为什么能让 loss 持续下降,而不是只会调用 optimizer.step()。
- 实现从零:核心算法和模型架构至少亲手实现一遍,哪怕用 PyTorch 这种框架,也要清楚每一层到底在算什么。不是为了造轮子,是为了建立正确的直觉。
- 经验从零:不从现成模板和示例项目出发,而是从需求出发,自己设计数据流程和训练方案。
一个很贴切的生活类比是学吉他。你可以照着网上的六线谱一首一首弹,三个月能弹好几首歌,但换一首新曲子还是不会。可如果你先花时间搞明白:吉他为什么是六根弦、和弦是怎么构成的、变调夹的原理是什么——之后再学新曲子,速度完全不一样。前者是"会弹",后者是"懂音乐"。AI 工程也是一样的。
1.3 这条路适合谁?先看看这几类人
先说结论:适合所有愿意慢下来打底子的人。
如果你是完全没有编程经验的转行者,我建议在起步阶段花一个周末玩一玩 Scratch 少儿编程工具,别觉得幼稚。它的积木式逻辑——顺序、循环、条件判断——几乎把编程思维的骨架都摆在你眼前了。我见过好几个转行朋友,就是靠 Scratch 建立了"程序是由一条条指令组合而成"的直觉,再学 Python 完全不费劲。如果有 iPad,从 Scratch Jr 这个更简化的幼儿版开始也行,同样是先建立结构化思维。
如果你有后端或前端经验,编程基础已经有了,直接从数学和机器学习开始就好。我这条路线里的实操对照可以帮你省掉大量试错。
不适合谁呢?不适合只想三天内调出一个大模型 demo、发个朋友圈交差的人。那种需求直接调用 API 就好,真的不用读这篇文章。
2. 核心路线全拆解:四阶路线从零到可用
2.1 第一阶:编程与数学地基(1-2个月)
这一阶段的目标不是成为数学专家,而是建立两个能力:能写代码完成数据处理,能看懂算法公式里的核心符号。
编程方面,Python 是绝对主线。基础部分需要掌握:变量与数据类型、列表/字典/集合、循环与条件、函数定义与调用、类与面向对象、文件读写,以及常用的数据处理库。不要求精通,但要求能独立写一个"读取 CSV 文件并统计某字段分布"的小脚本。
数学方面,很多人是被数学劝退的。这里给你一颗定心丸:AI 工程日常用到最多的数学,其实只有三个模块。
- 线性代数:矩阵乘法、矩阵形状变换、内积。深度学习里所有数据都是向量和矩阵,理解"批量处理其实就是矩阵运算"是第一步。
- 微积分:导数与梯度。反向传播的核心就是链式法则,你只需要理解"梯度是损失函数在山坡上的指向"就够了。
- 概率统计:常见概率分布、期望、条件概率、最大似然估计。语言模型生成一个词,本质上就是从一个概率分布里做采样。
实操建议:不要单独啃数学课本。拿 numpy 手写一遍线性回归,包括梯度下降,你会发现自己在写代码的过程中把线性代数和微积分都补上了,比背三百页公式效率高十倍。
2.2 第二阶:机器学习与深度学习原理(2-3个月)
第一阶搞定后,你已经能写代码、能看懂基本公式了,接下来进入机器学习。
机器学习的世界看起来模型很多:线性回归、逻辑回归、决策树、随机森林、SVM、KMeans……但它们的骨架其实是一致的,就是四个要素:数据、模型、损失函数、优化算法。你给模型喂数据,模型输出预测,损失函数衡量预测和真实标签的差距,优化算法(最常见的是梯度下降)调整模型参数让差距变小。所有模型,万变不离其宗。
这个阶段我强烈建议你手写关键算法,而不是直接调库。比如:
- 用 numpy 实现逻辑回归,在一个简单二分类数据集上完成训练和评估。
- 用 numpy 实现一个两层神经网络,手写反向传播,理解每一行计算对应链式法则的哪一步。
- 对每个算法,都画一遍"loss 随迭代次数变化"的曲线,亲眼看它降下来。
深度学习本质上就是"更多层 + 更大数据 + 更强算力"的神经网络。要掌握的核心概念其实不多:反向传播、自动微分框架、卷积与 Transformer 的基本思路。只要把两层神经网络的反向传播手写懂了,再看 Transformer 的梯度流,心理上完全不难,只是计算图的复杂度高了而已。
2.3 第三阶:从零构建大语言模型(3-4个月)
这是整条路线里最硬核、也最有成就感的一段。我的核心参考是《Build a Large Language Model from Scratch》这本书,它几乎就是为"from scratch"路线量身定做的实战手册:从准备训练数据开始,手把手带你写 BPE 分词器、搭建 Transformer 架构、实现预训练循环、做指令微调,最后还能把模型打包成一个可交互的对话应用。
如果你想做 AI 工程落地,而不是纯算法研究,第三阶可以这样拆:
第一,数据准备。找一个小规模文本语料,比如一部公版英文小说或若干篇文章合集。先做一个字符级或子词级分词器。数据决定了模型的上限,这一步绝对不能省。
第二,模型架构。从零搭建一个 mini 版 GPT:embedding 层把 token 变成向量,多头自注意力让每个 token"看到"上下文,前馈网络做非线性变换,每一层再接残差连接和 LayerNorm。这些模块加起来也就一两百行代码,但每一行都有明确的数学含义。
第三,训练。先在小的语料上把"语言建模"这个目标跑通——预测下一个 token。然后再做指令微调,让模型学会回答指令问题。想更进一步可以了解 RLHF,但小模型阶段不是必需的。
第四,进阶方向。当前的热词是 build a reasoning model from scratch,也就是从零构建推理模型。大模型在数学推理、逻辑推理上的能力是工程落地的关键瓶颈,在小模型范围内尝试推理能力的构建,是一个性价比极高的方向。
2.4 第四阶:工程化落地与部署(1-2个月)
模型训练出来只是第一步,真正体现"工程"二字的是落地。
从工程视角看,你要把训练好的模型包装成线上服务。最简单的方案是用 FastAPI 包一个 HTTP 接口,让外部系统能发起推理请求。之后再逐步补全:用 Docker 做容器化部署、通过批处理和流式处理优化吞吐、用量化把模型从 FP16 压到 INT8 甚至 INT4 以降低显存占用、用 vLLM 之类的推理框架提升并发能力。
同时,必须建立评估与监控体系。离线评估要关注准确率、召回率、困惑度等指标;线上要监控推理延迟、请求量、错误率,并且设计数据回流机制——线上用户输入经过脱敏后进入下一次训练集,形成"数据-模型-反馈-再训练"的闭环。
这里送大家一个经验:工程化升级要一步一个脚印,不要一开始就上全家桶。先把一个最简单的接口跑通,哪怕直接用 requests 发请求调模型也行,然后在保证可用的前提下逐项加东西。工程的首要原则永远是"先能跑,再跑得快,最后跑得稳"。
3. 实操记录:从零训练一个小型GPT模型
3.1 项目定位与硬件准备
理论路线看完了,下面用一个小项目,展示"从零开始"具体意味着什么。
我的练习项目是这样定位的:用一小段真实文本,大约两万多个 token 的规模,训练一个参数量只有几十万的小型 GPT 模型。训练完成后,它能生成一些语法通顺、但内容不太有逻辑的短句。可当你亲眼看到模型"学会了"生成语言,那种成就感足够支撑你继续学下去。
这个项目的硬件门槛极低。如果只用 CPU 也能跑,只是慢一些;我用的是一张 4GB 显存的旧显卡,完全够用。这也说明"from scratch"路线并不烧钱,真正贵的是你的时间和耐心。
3.2 数据准备:分词与数据集构建
第一步是准备语料。我把一本公版英文小说下载下来,去掉目录、标题等噪声,只保留正文,清洗后大概两万多 token。
接下来是分词。最简方案是字符级分词,把每个字符当成一个 token。优点是简单直观,缺点是序列太长、训练效率低。更实用的是子词级分词,最常见的算法是 BPE:从单字符统计开始,迭代地把高频共现的字符对合并成新 token,直到达到目标词表大小。
下面给出 BPE 训练流程的示意代码,生产级实现建议直接参考 tiktoken 一类的成熟库:
# 简单的BPE训练流程示意 # 输入:文本列表,目标词表大小 # 1. 统计每个字符的频率,得到初始词表 # 2. 循环: # a. 统计所有相邻token对的共现频率 # b. 找到频率最高的一对,合并为一个新token # c. 更新词表 # 3. 重复直到达到目标词表大小有了分词器后,还要把语料切成长度为 block_size 的样本,构造训练集。每个样本里,"前 n 个 token"对应"第 n+1 个 token",语言模型的学习任务就是这么定义的。
3.3 模型架构:手写Transformer核心组件
这是整个项目最核心的部分。我建议把它拆成四个子模块逐一实现。
第一个是 Token Embedding 层。每个 token 对应一个固定维度的向量,Embedding 矩阵的形状是 [vocab_size, n_embd],随机初始化后交给模型在训练中自己学习。
第二个是位置编码。Transformer 本身没有顺序感,所以要给模型提供 token 的前后关系。简单有效的做法是给每个位置也学一个向量,加到 token 的 embedding 上。
第三个是多头自注意力,它是 Transformer 的心跳。核心思想是:对序列里的每个 token,通过查询(Q)、键(K)、值(V)三个向量计算"我应该多关注序列里哪些位置"。
import torch import torch.nn as nn import torch.nn.functional as F class Head(nn.Module): def __init__(self, head_size, n_embd, block_size, dropout=0.1): super().__init__() self.key = nn.Linear(n_embd, head_size, bias=False) self.query = nn.Linear(n_embd, head_size, bias=False) self.value = nn.Linear(n_embd, head_size, bias=False) self.dropout = nn.Dropout(dropout) # 因果掩码矩阵:保证每个token只能看到自己及之前的token self.register_buffer("tril", torch.tril(torch.ones(block_size, block_size))) def forward(self, x): B, T, C = x.shape k = self.key(x) # (B, T, head_size) q = self.query(x) # (B, T, head_size) # 计算注意力分数,缩放因子取 head_size 的平方根 wei = q @ k.transpose(-2, -1) * (k.shape[-1] ** -0.5) # (B, T, T) wei = wei.masked_fill(self.tril[:T, :T] == 0, float("-inf")) wei = F.softmax(wei, dim=-1) wei = self.dropout(wei) v = self.value(x) # (B, T, head_size) out = wei @ v # (B, T, head_size) return out第四个是前馈网络加残差与层归一化。注意力机制让 token 之间交换信息,但每个 token 自身的"思考"需要前馈网络完成:一个两层的 MLP,激活函数常用 ReLU 或 GELU。残差连接保证深层网络的梯度顺畅回流,LayerNorm 让每一层的数值分布保持稳定。
把这四个模块组合起来就是标准的 Transformer Block。多个 Block 叠加,再接一个输出头,把最后一层 hidden state 映射到词表大小的 logits,一个可训练的 GPT 模型就完成了。
3.4 训练循环与推理生成
训练的本质,是让模型的输出 logits 与目标 token 的交叉熵损失最小。优化器选 AdamW,它对新手很友好,不太需要精细调学习率。
for step in range(max_steps): x, y = get_batch() # x是输入序列,y是右移一位的下一个token logits = model(x) loss = F.cross_entropy(logits.view(-1, logits.size(-1)), y.view(-1)) optimizer.zero_grad() loss.backward() optimizer.step() if step % 500 == 0: print(f"step {step}, loss {loss.item():.4f}")推理生成时,核心是"自回归":把当前序列输入模型,预测下一个 token,再把这个 token 接回序列尾部,继续预测下下个,直到达到目标长度。为了让生成结果更丰富,一般不直接取概率最高的 token,而是按概率采样,用 temperature 控制随机程度。
def generate(model, idx, max_new_tokens, context_length, temperature=1.0): for _ in range(max_new_tokens): idx_cond = idx[:, -context_length:] logits = model(idx_cond) logits = logits[:, -1, :] / temperature probs = F.softmax(logits, dim=-1) idx_next = torch.multinomial(probs, num_samples=1) idx = torch.cat((idx, idx_next), dim=1) return idx训练几万步之后,这个几十万参数的小模型就能生成出像模像样的短句。你会亲眼看到 loss 从 5.5 左右一路降到 1.5 附近的完整过程,这比看任何教程都更能建立对语言模型的直觉。
3.5 跑通之后的几点心得
跑完这个小项目,有几个体会非常深刻。
一是"模型的本质是压缩"。模型学会的其实是训练数据里的统计规律,数据里有什么,模型才会什么。这让我养成了先看数据、先分析数据分布的习惯。
二是超参数的影响非常直观。学习率设到 3e-3 以上,loss 很容易发散发散;设到 1e-4 以下,收敛慢得让人着急。从零调一遍之后,你对"学习率为什么是工程调优第一参数"会有肌肉记忆。
三是用 TensorBoard 之类的工具记录 loss 曲线非常必要。如果没有曲线,你根本分不清模型是在正常训练,还是在原地打转。
4. 常见问题与排查技巧实录
4.1 学习阶段最容易踩的五个坑
先讲学习阶段。
第一个坑是数学恐惧症。很多人在线性代数上耗了一个月,觉得矩阵乘法都没整明白,不配学 AI。我的建议:先接受"我不需要知道每件事的完整证明",只要能用直觉理解矩阵乘法是"批量计算的一种形式",就可以继续走。用到什么数学,补什么数学,你会发现自己比想象中学得快。
第二个坑是跳跃式学习。跳过机器学习直接学 Transformer,跳过原理直接用大模型 API,短时间看是快了两步,长期看是把自己卡在"永远只会调用、不会创造"的天花板下。
第三个坑是只抄不看。很多人 clone 别人的实现,跑通之后沾沾自喜。跑通 demo 大概只占整个学习的 10%,剩下 90% 在于:你能不能不看代码自己重新实现一遍?能不能回答"这里为什么用 LayerNorm 而不是 BatchNorm"?
第四个坑是完美主义。想把所有细节都理解透了再动手写代码,结果是三个月过去,笔记一堆,命令行一行没敲。先跑起来,再逐步理解,是自学者最该有的节奏。
第五个坑是资料贪多。今天收藏这个课程,明天买那本书,资源库越攒越大,动手时间越来越少。挑一两个核心资源,跟到底,比囤一百个文件夹有用得多。
4.2 训练与部署中的高频问题速查
训练过程中,问题几乎是一路踩过来的。这里把最高频的问题整理成一张速查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| loss 下降很慢或不动 | 学习率太小、数据未预处理、样本太少 | 画数据分布检查噪声;尝试把学习率调大 5-10 倍观察 |
| loss 突然变成 NaN | 学习率过大、数值溢出、数据里有 NaN | 调小学习率;检查预处理是否产生除零;加梯度裁剪 |
| 训练集 loss 低但验证集高 | 过拟合 | 增加数据、加 dropout、早停、减小模型规模 |
| 生成文本全是重复词 | 模型太小、训练不充分、temperature 太低 | 延长训练;调高 temperature;尝试 top-k/top-p 采样 |
| 显存不足 | batch size 太大、序列太长 | 减小 batch size 或 block_size;用梯度累积 |
| 在线服务延迟高 | 模型参数量大、未量化、并发处理弱 | 用 INT8/INT4 量化;批量处理请求;考虑 vLLM 等推理框架 |
排查思路的核心是"控制变量"。一次只改一个参数,并且记录每次实验的超参和结果。很多新手一上来同时改学习率、batch size、模型层数,出了问题根本没法定位。我自己的做法是:每次实验只动一个旋钮,用表格记录,像做物理实验一样。
另外一个常被忽略的坑是随机种子。同样的代码跑两次结果不一样,是正常的——如果你不设置种子,PyTorch 的随机初始化每次都会不同。复现实验时一定要固定 seed,同时记录依赖库的版本号。
4.3 资源清单与时间规划
最后给出一份亲测有效的资源清单。
- 书籍:《Build a Large Language Model from Scratch》是从零构建大模型的最佳实践参考;配合《深度学习》(花书)作为原理查漏补缺。
- 课程:吴恩达的机器学习公开课,虽然是十多年前的老课,但概念清晰度至今无人能比。
- 工具:PyTorch 官方教程里的 60 分钟入门篇;HuggingFace 文档用于工程化阶段参考。
- 启蒙:完全零基础的话,先用 Scratch 少儿编程工具花一个周末建立编程思维,再回头学 Python。
时间规划上,第一阶 1-2 个月,第二阶 2-3 个月,第三阶 3-4 个月,第四阶 1-2 个月,总时长大约 7-10 个月。如果全职学习,可以压缩到 4-6 个月;如果每天只有两小时业余时间,按 10 个月规划更现实。
这里再强调一句:不要在一个知识点上死磕超过三天。遇到实在绕不过去的,先跳过,等整条链路通了之后回头再看,很多难点会豁然开朗。
个人经验,这类"从零开始"的项目,最难的不是技术本身,而是在没有外部压力时保持稳定的输出节奏。我自己的办法是:每周强迫自己产出一个"可以看到的东西",代码也好、笔记也好、训练曲线截图也好。这些东西既是进度记录,也是下一周继续下去的动力。哪怕某周只写了一个几十行的脚本,也远好过一周什么都没动。
最后再分享一个小技巧:在电脑桌面放一个专门的文件夹,按日期命名每次实验,比如"2026-03-15-lr-3e-4-block8"。三个月后回头翻这个文件夹,你看到的是一份活生生的成长地图。这种可视化的进步感,比任何学习计划表都管用。