1. 为什么叫"from scratch":这条路线想解决的真正问题
如果你的收藏夹里躺着不少AI相关的学习资料,却一直没有真正动手敲过代码,那你看到 ai-engineering-from-scratch 这个标题时,大概会和我第一次见到同类项目时一样:先心动,再陷入沉思。心动是因为它承诺了一条从零起步的路径,沉思是因为我们都被"收藏即学会"骗过太多次。我也把自己这几年的AI工程学习笔记起成了这个名字,而且确实是把它当成了一种学习方法,不是宣传语。
1.1 一个尴尬的现状:会调库不代表会AI
我见过不少同行,能熟练跑通sklearn的fit/predict,也能拿着transformers跑通问答demo,但一面对真实业务就抓瞎:数据是脏的,标签是不准的,训练完评估之后也不知道下一步该调什么。问题不在智商,而在工具把太多东西藏起来了。框架的调用门槛越低,理解门槛越容易被忽略。你以为自己完成了"训练模型",其实只是把数据塞进了一条连参数含义都说不清的管道里。
from scratch 的价值正在于此:亲手把神经网络里每一层矩阵乘法、每一个梯度写出来,把数据清洗的每个步骤走一遍,把一次失败的实验分析到底。这些东西能在较短时间里把"我大概知道它怎么工作"变成"我能控制它按预期运行"。
1.2 我把"从零到能干活"拆成了四个阶段
- 阶段一:工程底座——Python生态、环境管理、git、实验记录规范。
- 阶段二:模型原理——用numpy手写线性回归、逻辑回归、多层感知机、反向传播。
- 阶段三:完整数据项目——用现成框架完成清洗、训练、评估、调优、部署的完整闭环。
- 阶段四:大模型应用——Prompt工程、RAG、微调选型、评估体系。
每个阶段都设置一个可检验的里程碑:阶段一是能连续三天不翻文档写数据脚本;阶段二是能合上参考资料写出反向传播;阶段三是能独立交付一个带评估报告的项目;阶段四是能用合理的成本解决一个真实业务问题。这样每一阶段都有明确终点,不会学着学着就迷路。
1.3 这条路线适合谁,不适合谁
如果你准备应聘AI相关岗位、想把AI引入工作流,或者纯粹想弄清模型背后到底发生了什么,这套路线很有价值。但如果你只是想要一个能演示的Demo,直接抄官方示例反而更合适,没必要从零走一遍。
这不是谁更聪明的问题,而是投入产出比的差异。from scratch 的学法贵在遇到问题时心里不慌,但确实慢。先把自己的目标想清楚再决定要不要这么学,这本身就是工程师的基础能力。
2. 地基阶段:编程、数学与工具链的取舍
2.1 Python 学到什么程度算够用
很多零基础同学纠结要不要先把Python系统学完再开始AI,我的答案是:边做边学,但有几项基本功必须提前具备。第一,会用pandas读数据、做筛选、分组聚合,能完成常见的数据清洗逻辑。第二,理解numpy的数组维度,知道矩阵乘法的结果形状是怎么来的,这关系到你后面能不能读懂网络结构。第三,能写函数和类,至少可以读懂框架源码的核心部分。第四,会用虚拟环境隔离依赖,conda和venv至少掌握一种。第五,写脚本时习惯处理异常,别让代码一碰脏数据就崩溃。
import pandas as pd df = pd.read_csv("feedback.csv") df["clean_text"] = df["text"].str.strip().str.lower() print(df.groupby("label").size())这段代码看起来简单,但真实业务里大部分时间都花在这些琐碎的数据处理上。数据越脏,基础功的差距越明显。我见过太多人把精力放在选多高级的模型上,结果数据清洗阶段就翻车。
2.2 数学:教材里的大部分内容可以大胆跳过
一提到数学很多人就发怵,但AI工程需要的数学比大学教材少得多。真正高频使用的是三块:线性代数、微积分、概率统计。神经网络代码几乎全是矩阵运算,所以对矩阵乘法、转置、维度匹配要非常敏感,看到形状对不上马上能意识到哪里出了问题。微积分部分重点是链式法则,反向传播本质就是链式法则的工程化实现。概率统计则要理解均值、方差、分布、条件概率和最大似然估计,这些是看懂损失函数和评估指标的基础。
至于更深的数学内容,比如复杂矩阵分解、泛函分析,用到再查完全来得及。学数学的目标不是应付考试,而是看训练曲线时能明白 loss 为什么这样波动、梯度为什么消失、为什么换了个激活函数就有改善。
2.3 实验追踪能力越早建立越好
这个能力几乎是被所有人低估的。我看到太多人调了几十次模型,最后说不清哪次实验用了哪些参数。哪怕项目只有你一个人,也要从第一天养成记录习惯。每次实验至少记录日期、数据版本、模型结构、关键超参数、评估指标和备注。
| 日期 | 模型 | 学习率 | 训练步数 | ACC | F1 | 备注 |
|---|---|---|---|---|---|---|
| 2025-01-15 | baseline-lr | 0.01 | 300 | 0.82 | 0.74 | 未清洗数据 |
| 2025-01-15 | baseline-lr | 0.01 | 300 | 0.88 | 0.81 | 清洗后提升明显 |
配合git管理代码、表格记录实验、模型文件按"日期_模型_数据_参数"命名,这三件事能让你在几周后依然精确回看当时的决策过程。看似多花了时间,实际上是最快的路径。
3. 第一个关键项目:用numpy手写神经网络
3.1 为什么先手写再上框架
现在的框架确实太方便了,输入数据、调用fit、等结果、看指标,整个过程流畅得像魔法。问题是魔法会掩盖问题:模型不收敛时,你分不清是数据问题、学习率问题还是梯度计算问题。手写一遍之后,魔法会变成看得见的算术。你会真切看到每一层的输出是什么量级,反向传播时哪个环节的梯度在消失,从而真正理解框架在背后做了什么。
3.2 一个最小可用的numpy多层感知机
以二分类任务为例,网络结构是"输入层-隐藏层-隐藏层-输出层",激活函数用sigmoid,损失函数用交叉熵。初始化参数时,权重用较小的随机值,避免梯度在小数点后挣扎:
import numpy as np def init_params(layer_sizes): params = {} for i in range(len(layer_sizes) - 1): params[f"W{i}"] = np.random.randn(layer_sizes[i], layer_sizes[i+1]) * 0.1 params[f"b{i}"] = np.zeros((1, layer_sizes[i+1])) return params def sigmoid(z): return 1 / (1 + np.exp(-z)) def forward(X, params, layer_sizes): cache = {"A0": X} for i in range(len(layer_sizes) - 1): Z = cache[f"A{i}"] @ params[f"W{i}"] + params[f"b{i}"] cache[f"Z{i+1}"] = Z cache[f"A{i+1}"] = sigmoid(Z) return cache def compute_loss(y, y_pred): eps = 1e-12 return -np.mean(y * np.log(y_pred + eps) + (1 - y) * np.log(1 - y_pred + eps))反向传播部分我重点说下逻辑:先计算输出层的误差,再从最后一层往前面逐层推。每一层的梯度等于"前一层输出转置乘以当前层的误差",然后误差继续向后传时乘上对应权重矩阵的转置,最后对batch内所有样本取均值。整个过程全是矩阵运算,写完后把形状打印出来逐一验证,比对着公式抄要有效得多:
def backward(X, y, params, cache, layer_sizes): m = X.shape[0] grads = {} n = len(layer_sizes) - 1 y_pred = cache[f"A{n}"] dA = -(y / (y_pred + 1e-12) - (1 - y) / (1 - y_pred + 1e-12)) for i in range(n - 1, -1, -1): Z = cache[f"Z{i+1}"] dZ = dA * sigmoid(Z) * (1 - sigmoid(Z)) grads[f"W{i}"] = cache[f"A{i}"].T @ dZ / m grads[f"b{i}"] = np.sum(dZ, axis=0, keepdims=True) / m if i > 0: dA = dZ @ params[f"W{i}"].T return grads训练循环做的事情只有三件:前向算预测,反向算梯度,更新参数。把这三步写在循环里,一个最小可训练的神经网络就成了:
def train(X, y, layer_sizes, lr=0.1, epochs=500): params = init_params(layer_sizes) history = [] for epoch in range(epochs): cache = forward(X, params, layer_sizes) loss = compute_loss(y, cache[f"A{len(layer_sizes)-1}"]) grads = backward(X, y, params, cache, layer_sizes) for i in range(len(layer_sizes) - 1): params[f"W{i}"] -= lr * grads[f"W{i}"] params[f"b{i}"] -= lr * grads[f"b{i}"] if epoch % 50 == 0: history.append((epoch, loss)) return params, history3.3 手写之后再看框架,会有一种通透感
跑完手写版本之后再打开PyTorch或TensorFlow的官方示例,你会发现自己能对上框架里的每个概念:optimizer.step对应参数更新,loss.backward对应反向传播,model.train对应训练模式切换。最重要的是,当模型训练出现异常时,你至少知道该往哪个方向排查:是学习率太大导致震荡,还是网络太深导致梯度消失,还是数据本身分布有问题。
这不是让你以后都手写模型,而是通过一次从头搭建,把框架藏起来的逻辑补回来。之后真正工作中用框架,效率反而更高。
4. 进入工程世界:数据、评估和调优的完整闭环
4.1 数据质量检查清单
从手写模型进入工程化项目,第一个拦路虎不是模型选型,而是数据质量。我每次接手新数据集都会按下面的清单过一遍,缺一项都可能让后面的工作白做。
- 重复样本:同一文本既出现在训练集又出现在测试集,测试分数虚高,线上表现立刻露馅。
- 标签噪声:标注内部不一致的问题很常见,交给模型之前至少抽检50条,人工核对标签合理性。
- 特征泄漏:预测目标被当成输入特征。典型例子是客户流失预测里把"是否已经流失"当作特征,训练集准确率逼近99%,上线后模型完全失效。
- 分布漂移:训练数据来自渠道A,上线后面对的却是渠道B的样本,输入分布变了,再好的模型也会打折。
数据问题有个共同特点:它们不会在训练时报错,只会在上线后以指标下滑的形式暴露。所以任何模型项目,第一周最值得花时间的一定是数据审查,而不是模型调参。
4.2 评估指标不是越多越好,而是要匹配业务
新手最爱盯着准确率看,但在很多场景里准确率是骗人的。如果样本中99%是负类、1%是正类,一个把所有样本都预测为负类的模型也能拿到99%准确率,而它对业务毫无价值。这时候必须引入精确率和召回率:精确率关心"模型判为正类的结果里有多少是真正类",召回率关心"真正的正类里有多少被找了出来"。
精确率和召回率往往此消彼长,具体怎么取舍要看业务成本:
| 业务场景 | 更看重 | 原因 |
|---|---|---|
| 垃圾邮件过滤 | 精确率 | 误杀正常邮件的代价远高于漏过垃圾邮件 |
| 疾病筛查 | 召回率 | 漏诊的后果比多复查一次更严重 |
| 内容审核 | 两者加权 | 误伤正常内容和放过违规内容都不可接受 |
所以评估这一步不能单独看数字,还要回到业务里问一句:哪种错误更贵?切换评估视角之后,很多"模型很差"的结论可能要改写。
4.3 调优循环里最容易忽略的环节
拿到一个项目,我建议的调优顺序是:先跑通一个最简单的baseline,再做错误分析,然后处理数据和特征,最后才调超参数。很多人一上来就上贝叶斯优化或者网格搜索,结果最优超参数跑在了一个不合理的特征集上,毫无意义。
错误分析这一步特别值得重视。做法很简单:把模型预测错的案例全部打印出来,人工看一遍,找出共性。你会发现大量"模型问题"其实只是数据问题:某些类别样本太少,某些文本标注错误,某些领域的术语模型没见过。把这些问题解决掉,模型效果往往自己就上来了。调超参数则遵循一次只动一个变量的原则,配合实验记录表格,确保每个提升都能定位到具体原因。
5. 大模型应用开发:Prompt、RAG和微调的决策框架
5.1 先写Prompt,不要一开始就想微调
到了大模型应用阶段,很多人容易陷入"动不动就微调"的冲动。我的经验是,绝大多数业务场景先用Prompt工程解决,成本低、迭代快。几个亲测有效的策略:
- 结构化输出:要求模型返回JSON对象,并明确每个字段的含义和格式,便于下游程序直接解析。
- Few-shot示例:与其反复描述你的需求,不如直接给2到3个好例子,模型模仿能力通常比理解长指令更强。
- 分步推理:在允许的场景下,让模型先列出已知条件和推理过程再给结论,复杂任务的正确率会明显提升。
- 控制解码参数:温度设为0附近适合分类、抽取等确定性任务,创意生成再考虑提高温度。
写Prompt和写代码类似,都需要版本管理。同一个需求,你的Prompt v1、v2、v3分别对应什么输出质量,建议记录下来,你就会发现Prompt也是一个可以系统迭代的工程模块。
5.2 RAG落地时最容易被忽视的细节
当任务涉及私有知识或实时信息时,RAG通常优于微调。但RAG不是把文档丢进向量库就结束,落地时有几个细节直接影响效果。
文档切分要按语义结构来,而不是机械按固定长度切。切得太碎,上下文丢失;切得太长,检索噪音变大。一般建议控制在一个语义块内,需要时加少量重叠。检索端要关注"有没有把正确答案找回来",也就是召回率,建议设置一个合理的TopK范围,再用相似度阈值过滤明显不相关的结果。如果对效果要求高,可以在召回之后加一个重排序环节,把相关性最高的几条排到前面,再交给大模型生成回答。
RAG上线前要分别评估检索和生成两个环节:单独看检索命中率,再看答案正确率,否则你很难定位短板到底在哪。
5.3 微调到底什么时候才值得做
我建议按这样的顺序决策:先用Prompt,不够再上RAG,两者都试过仍然不满足,最后才考虑微调。真正适合微调的信号包括:有足够数量的领域标注数据,对输出格式有极强的一致性要求,或者需要固定某种语言风格。微调能带来稳定性和可控性,代价是训练资源和维护成本都明显上升,而且每换一次基础模型都要重做一遍。
| 方案 | 适用场景 | 成本 | 迭代速度 |
|---|---|---|---|
| Prompt | 逻辑清晰、示例明确 | 低 | 快 |
| RAG | 需要外部知识、文档问答 | 中 | 中 |
| 微调 | 领域风格、稳定输出、格式强约束 | 高 | 慢 |
把这三个工具放进同一个决策框架里,应用开发才不会一上来就做出过度设计。
6. 一次完整复盘:中文文本分类从零到部署
6.1 项目需求和方案选型
我拿一个实际做过的客服反馈分类项目作为例子。需求是把用户反馈分成"投诉、咨询、表扬、建议"四类,数据约8000条,来自不同渠道,文本长度从几个字到几百字不等。项目目标不是发论文,而是能稳定上线运行。
当时的方案选型思路是:先做一个TF-IDF加逻辑回归的baseline,把数据流程和评估体系打通。很多时候,这种简单模型已经能解决相当一部分问题,而且部署成本极低。如果效果不达标,再切换到预训练模型做微调,或者调API。有了baseline,后续每个模型到底提升了多少,都有明确参照。
6.2 数据清洗、训练和评估实录
数据处理环节做了四件事:统一小写并去除首尾空格,去掉URL和@符号,把空白字符压缩,统一部分繁体词和常见错别字简写。标签方面做了分布统计,发现"投诉"类明显偏少,于是采用了简单的类别重采样,避免模型直接忽略少数类。
训练阶段,先是baseline跑通,得到一个F1约0.72的成绩。接下来做错误分析,发现大量预测错误来自极短文本——比如只有两个字的内容,人类都难以判断意图,模型自然也很难。于是给短文本增加了一条规则兜底,F1提升到0.78。之后再用预训练的中文模型微调,把F1推到0.88左右。最后的结论是:对这类任务,预训练模型微调确实有用,但baseline和错误分析带来的提升同样不可忽视。
6.3 部署阶段踩过的三个坑
第一个坑是预处理器和服务化代码分开维护。训练时写的清洗函数是一份代码,部署时又重新写了一遍,结果线上输入和训练数据格式不一致,预测效果明显下降。教训就是数据预处理必须复用同一套代码,最好打成公共包。
第二个坑是线上环境没有做输入兜底。用户反馈里偶尔会出现空文本、全部是符号的文本,直接把模型卡死或输出不合法标签。后来在服务入口加了一层输入校验和默认标签兜底。
第三个坑是模型文件管理混乱。迭代了十几个版本之后,依赖和模型文件混在一起,回滚非常痛苦。之后统一用固定的目录结构保存模型版本,并把关联的代码版本、数据版本、评估结果一起归档。这些坑都不是高深的技术问题,但每一个都会真实影响上线的稳定性和排查效率。
7. 绕开我踩过的这些坑,你能快很多
7.1 新手最容易犯的五个错误
第一,跳过baseline直接上大模型。没有参照系,你永远不知道大模型的提升来自能力还是运气。第二,反复使用测试集调参,导致测试指标虚高,上线后原形毕露。正确做法是预留一份坚决不碰的测试集,用验证集做决策。第三,实验不做记录,几周后连自己都说不清哪个参数带来提升。第四,代码和数据处理脚本不进git,改坏了无法回退。第五,只会调API不读报错,模型概念理解停留在黑盒层面,遇到问题无从下手。
这些错误的共同根源是把AI工程单纯理解成"跑模型"。实际上,一个能稳定运行的AI项目,工程习惯和数据意识占了大半权重。
7.2 我现在保持动手节奏的做法
有一件事我坚持了很久,就是每学一个新概念,都逼自己在不看任何参考的情况下,写一个最小实现。学Transformer就写一个简化版注意力,学RAG就手工搭一个最小检索链路。写不出来就回头看资料,再写,直到能独立完成。这个过程很慢,但带来的记忆和理解深度远超看十篇教程。
如果你也决定按 from scratch 的方式走一遍,不用给自己定太大的目标。每周一个最小项目,一篇实验笔记,比收藏一百个教程有用得多。我现在回看踩过的坑,绝大多数都是因为早期急着往前跑,跳过了基本功。慢下来,反而成了最快的路程。