这篇不是什么学院派教程,也不是那种"三天精通AI"的速成指南。标题是"ai-engineering-from-scratch"——很多朋友看到这个词的第一反应是"我要从零手写一个神经网络"。我最初也是这样理解的,还花了两周把MLP、CNN、RNN挨个用NumPy手撸了一遍,成就感确实很足。但现在回头看,那段投入对"做AI工程"这件事帮助非常有限。
真正卡住新人的,从来都不是反向传播公式怎么推导,而是这个问题:模型在notebook里跑通了,然后呢?从零开始做AI工程,意味着你要亲手把一个想法变成一条稳定、可复现、能上线、能维护的数据流水线。这跟在Kaggle上刷一个高分kernel完全是两回事。这篇内容适合那些刚入行、被"跑通了一个模型但不知道下一步"卡住的人,也适合想系统化自己项目流程的开发者。我会按我实际走过的路径,把关键环节一个个拆开讲清楚。
1. "从零开始"的分水岭:AI工程不是在notebook里把模型跑通
1.1 为什么"from scratch"不是从写神经网络开始
先把最常见的误解掰开。标题里的"from scratch",真正含义不是"不用框架、纯手写",而是在没有现成模板的情况下,亲手构建一套完整的AI生产链路。你完全可以放心使用成熟的深度学习框架和预训练模型,这不算偷懒,而是工程常识——你不会因为超市里有面粉,就坚持自己种小麦。
我自己的经历就是最好的例子。第一阶段我沉迷于手写各种模型结构,MLP、CNN、RNN都自己实现了一遍,当时觉得特别牛。但等到真要做一个实际项目时,发现自己连"如何正确处理一份带缺失值的csv文件并送入模型"都要想半天。后来我把思路彻底掉转:先搭一整条能跑的管线,再把管线里的每个环节做扎实。这个调整才是真正从"写模型"走向"做工程"的开始。
1.2 工程视角下的AI系统到底由什么组成
为了避免各说各话,我把"AI工程"拆成五个模块,这五个模块构成了一个生产环境AI系统的完整骨架:
- 数据层:源数据接入、清洗、标注、特征工程、数据版本管理。这是整个系统里最耗时、最容易被低估的环节。
- 训练层:实验管理、超参数配置、模型训练、模型注册。核心不是训练本身,而是可复现性。
- 评估层:离线指标计算、合理的测试集切分、上线前的效果摸底。
- 部署层:模型转服务、接口封装、批处理任务、资源调度。模型文件本身不能直接产生价值,它必须跑在某个运行时里对外提供服务。
- 监控层:线上指标、数据漂移检测、效果衰减预警、回滚机制。
我第一次看到这个拆分时,第一反应是"这也太多了吧"。但后来发现,这正是工程和Notebook实验最大的差别。Notebook只需要前三个模块里的一小部分,而"from scratch"的完整路径,需要你把每一层都走通。
| 层级 | 常用工具选型 | 前期可以如何简化 |
|---|---|---|
| 数据层 | DVC / LakeFS / 自定义版本目录 | 个人项目直接在目录名上加日期版本 |
| 训练层 | MLflow / W&B / 自定义json配置 | 前期用一个config.py就够 |
| 评估层 | sklearn metrics / 自定义评估脚本 | 别省,但可以保持很轻量 |
| 部署层 | FastAPI / Triton / TensorFlow Serving | 先从一个FastAPI服务起步 |
| 监控层 | Prometheus / Grafana / 自定义心跳日志 | 初期打结构化日志+定期人工检查 |
1.3 从零开始的最短路径规划
既然要"from scratch",最怕战线拉太长。我给自己规划的是三个里程碑,后来带新人也是这么给的:
- 两周内跑通第一版端到端项目:数据能流进、模型能训练、结果能通过API返回。哪怕整体准确率只有六成,哪怕服务只在你自己电脑上跑,先让整条链路动起来。
- 第二个月做一次工程化回炉:把第一版里写死的路径、不可复现的操作全部配置化,补上完整日志和评估脚本。
- 第三个月再上一个有难度的项目:重点不是模型复杂度,而是在这条产线里加入评测、监控、回滚这些生产要素。
这套路径背后的逻辑很简单:先建立全链路的心智模型,再优化每个局部。如果你一上来就钻进某个点,比如抠某个模型结构,很容易在局部花掉三个月,最后发现连部署都不会。
2. 环境搭建:先把地基打扎实
很多人觉得环境搭建没什么好讲的,装个PyTorch开跑就行。但"from scratch"最大的特点是没人帮你兜底,环境上的每个坑都得自己定位。我前两个月至少有三分之一的时间耗在环境问题上,所以这块值得单独交代清楚。
2.1 硬件选型:CPU也能起步,但预算要花在刀刃上
如果没有GPU,我并不建议上来就买一张很贵的显卡。起步阶段的CPU训练完全够你把管线走通——模型小、数据量小,慢一点反而是好事,因为每一步都慢,你会被迫认真设计数据加载方式、batch size和内存使用,这些恰恰是后面做工程的基本功。
等真要上正经模型,比如中等规模的Transformer,再考虑买卡或租卡。选型时不要只看显存,要看"算力+显存"的组合。如果你主要做主流CV/NLP任务的微调,24GB左右的显存是相对舒服的起步档——很多预训练模型微调时batch size太小会影响效果。显存不够时,你要被迫引入gradient accumulation这类操作,复杂度立刻上来了。
2.2 依赖管理:conda、Docker与CUDA的坑
我在第一版项目里,直接在全局环境里pip install,结果某个包升级之后,之前跑通的脚本突然报错。那次之后我强制自己遵守两条纪律:
- 每个项目一个独立的conda环境,环境导出为environment.yml放在仓库里。
- 安装任何包之前先锁定版本,绝不随意升级已有依赖。
如果走多人协作或部署方向,最终还是得切到Docker。Docker的意义不只是隔离环境,而是让"在我机器上能跑"变成"在任何机器上都能跑"。我后来把所有训练和部署都容器化,整个复现过程一下子干净了——docker build一次,哪里都是同一份环境。
再补一个CUDA相关的实用技巧:不要装最新版CUDA。去查你用的深度学习框架官方测试过的版本组合,PyTorch官网每个版本的安装命令里都写清楚了对应的CUDA版本,直接用它给的命令走,远比去NVIDIA官网手动下载再配置省心得多。
2.3 复现性:固定随机种子与依赖锁定
"from scratch"的隐藏含义是:三个月后,你能闭着眼睛把今天的实验重新跑出来。为此要强制做两件事:
- 固定所有影响结果的随机源。Python的random、NumPy、PyTorch,在训练脚本开头统一seed。
- 锁定依赖版本。environment.yml里不仅要写包名,还要精确到版本号。启动参数、数据切分方式也要一并记录。
我的实践是:每个实验的配置自动以json形式保存到模型输出目录,训练完的目录里既有权重文件、又有配置文件和数据切分记录。这套做完,即使项目换人接手,也不会出现"不知道这模型怎么训出来的"的窘境。
3. 挑一个"小到不可能失败"的项目,走通全链路
环境搭好后,最大的诱惑是直接上一个看起来酷炫的大项目。我强烈建议压住这个冲动,先选一个能在三到五天内完成训练和部署的端到端小项目。这里的"小"指的是模型和数据规模,而不是工程链路的长度。
3.1 项目选择的三个准则
我最常推荐的两类入门项目是"文本情感分类"和"图像二分类"。选择准则有三条:
- 数据容易获得且规模可控:用开源数据集,几千到几万个样本就够。
- 输出容易理解:二分类或多分类的标签,评估和可视化都直接。
- 能自然延伸到工程环节:有数据、有训练、有推理,天然适合包成API。
我当时做的是垃圾邮件分类。数据量不大,但清洗环节天然包含文本预处理、标签分布不平衡这些典型问题,足够练手。我在心里把场景设定成:假设我是一家小公司的唯一工程师,要尽快给客服部门提供一个判断"这条消息是否可能是垃圾"的接口。
3.2 数据管线的第一个版本:能跑通比优雅更重要
第一版数据管线我根本没有设计复杂架构,就是三件事:
- 写一个
load_data()函数读入原始数据。 - 写一个
preprocess()函数完成清洗和基础处理。 - 把结果存成训练脚本能直接读的格式,Parquet最优,CSV也行。
关键点在于每一步都留痕。预处理时把清洗前后的输入各打印几条出来,并统计一下标签分布。这些看起来很笨的输出,恰恰能帮你及时发现数据管道哪里出了问题。我的经验是,很多"模型效果差"的情况,根因根本不在模型,而是数据管道在某个环节悄悄对错了标签。
更重要的是:预处理逻辑从第一版就要固化成独立函数,不要散落在notebook的各个单元格里。我见过太多人后来部署时"预处理在训练和推理两边不一样",就是因为早期没养成这个习惯。文本里的编码、大小写、标点、换行符,这些不起眼的东西会在第一个项目里反复折磨你。建议在预处理函数里统一做大小写和空白归一化,并显式处理缺失值。
3.3 训练脚本的工程化改造:配置化与日志化
第一版训练脚本我是直接把数据路径、超参数、模型结构全写死在文件里。这样也不是不能跑,但实验一多,每个结果对应的是"当时手改的哪段代码",完全说不清楚。于是第二版我做了一次改造,这是从"跑通实验"走向"做工程"的标志性步骤:
- 把超参数和数据路径全部挪到一个配置文件里,可以是yaml或py文件。
- 训练脚本只负责读配置、加载数据、训练、保存模型,不出现任何硬编码路径。
- 每次实验自动生成带时间戳的输出目录,所有日志、checkpoint、配置副本都落在里面。
训练脚本的核心结构大致是这样:
# train.py 简化示意 import yaml, os, torch from datetime import datetime with open("config.yaml") as f: cfg = yaml.safe_load(f) exp_dir = f"runs/{datetime.now():%Y%m%d_%H%M%S}" os.makedirs(exp_dir, exist_ok=True) with open(f"{exp_dir}/config.yaml", "w") as f: yaml.safe_dump(cfg, f) # 之后所有逻辑都从cfg读取参数:数据路径、模型结构、batch size、学习率……这里要特别强调:训练过程必须有日志,而且是带时间戳、带等级的日志。不是print,是用Python标准库logging,同时写到文件和控制台。为什么强调这个?因为训练一跑就是几个小时,你不可能一直盯着。日志是事后排查的唯一线索。我至少有五次是靠翻日志文件发现数据处理存在顺序错误,而不是靠肉眼看代码。
3.4 模型保存与版本管理
训练结束后,模型保存不只是torch.save(model.state_dict())这么简单。我会把三样东西放在同一个目录,目录名就是实验ID:
- 权重文件,比如model.pt或.pth。
- 配置文件,config.yaml。
- 评估指标汇总,metrics.json。
有了这套结构,回滚和对比实验只需要看目录列表。实验多了之后再考虑MLflow这类工具,早期手动维护目录完全够用。
模型文件的二进制特性决定了它不适合直接进Git。我的方案是:代码进Git,模型和实验输出放在单独的目录或模型仓库里管理。否则仓库会越来越臃肿,而且为了回滚代码去下载一个大模型文件,体验极差。
4. 部署与服务化:从脚本到API的惊险一跃
对很多只做过notebook实验的人来说,部署是整个"from scratch"过程中最陌生、也最打击自信心的环节。我第一次部署时的想法是:"不就是把预测函数暴露成接口吗?"真做起来才发现,里面全是细节。
4.1 同步推理 vs 异步推理:先别急着上队列
第一版部署,建议直接上同步接口。用户发一个请求,服务调模型推理,返回预测结果。这最直观,也最容易定位问题。等确认模型效果不错、用户量上来了,再考虑异步:任务提交后立刻返回任务ID,后台消费队列,完成后通过轮询或回调拿结果。
之所以单独讲这个,是因为很多教程一上来就教人用Celery做异步任务队列,看着专业,但对刚起步的AI服务来说纯属过度设计。同步接口代码量少,bug容易复现。直到你遇到"单次推理超过几十秒"或"请求量明显打满服务"的场景,再引入Redis Streams或RabbitMQ这类消息队列不迟。
4.2 用FastAPI包一层:第一个AI后端服务
部署环节我拆成三个阶段,照着做就能少踩坑。
阶段一,把模型加载和推理逻辑抽成独立模块。比如model.py里有load_model()和predict(text)两个函数。要求是:推理模块不能依赖训练脚本里的任何全局变量。因为训练时有GPU、有大内存、有交互环境,这些在部署时都可能没有。
阶段二,用FastAPI写一个很薄的服务层。一个可直接参考的极简示例:
# app.py 简化示意 from fastapi import FastAPI from pydantic import BaseModel from model import load_model, predict app = FastAPI() model = load_model() # 启动时加载一次,不要在请求里加载 class RequestBody(BaseModel): text: str class ResponseBody(BaseModel): label: int probability: float @app.post("/predict", response_model=ResponseBody) def predict_endpoint(body: RequestBody): label, prob = predict(model, body.text) return ResponseBody(label=label, probability=prob)这里最值得划重点的:模型在服务启动时加载一次,而不是每次请求都加载。新手最容易犯的错就是load_model()放在请求处理函数里,并发一上来内存直接爆掉。模型加载是重操作,必须放在启动阶段。
阶段三,加健康检查接口。不要小看/health,它在接监控、做负载均衡、排查线上问题时是必需的基础设施。我会让它返回服务版本和模型加载状态,这样线上出问题时能快速确认"当前跑的是哪个模型"。
4.3 性能优化:缓存、批处理、量化的正确顺序
服务跑通后,优化顺序我建议是:先缓存,再批处理,最后量化。
缓存是性价比最高的优化。如果业务上有重复请求,常见于内部工具或客服场景,用一个LRU缓存命中相同输入,能省掉大量无效推理。批处理是把多个请求攒起来一起推理,利用框架的向量化加速,这个通常要配合异步接口。量化是在模型权重上做文章,比如从float32降到float16甚至int8,推理延迟和模型体积都会下降,但指标可能轻微退化。
做任何优化之前,先记录当前基线的延迟和吞吐。我用的方式很简单:在线程里跑100次请求,统计P50和P95延迟,每次优化完再跑同样的脚本对比。
4.4 容器化部署的实操笔记
最后把服务容器化。Dockerfile的写法我踩过不少坑,下面是一个能用的基础结构:
# Dockerfile 简化示意 FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]三个注意事项:
- 基础镜像选slim版本,体积能差好几倍。如果模型依赖GPU和CUDA运行库,要用带对应CUDA版本的基础镜像。
- 先拷贝requirements并安装依赖,再拷贝业务代码。这样做能利用Docker的层缓存,每次改代码后不用重新pip install。
COPY . .之前一定要写.dockerignore,把.git、runs、data这类大目录排除,否则镜像构建又慢又大。
容器化之后,部署目标就很随意了:云主机、容器服务、内网服务器都可以。第一步先docker run映射端口跑起来,再挂一层Nginx或云负载均衡,就能对外提供稳定服务了。
5. 评测和监控:AI工程里最容易被低估的环节
如果只是在notebook里看个热闹,评测和监控完全不需要。但做ai-engineering,模型上线后的质量保障才是工程的核心。这部分我花的时间一点不比训练少,但带来的安心感是无价的。
5.1 离线评测:换个切分方式,accuracy会骗你
第一版项目我用了最简单的"全数据随机切分训练/测试",测试集accuracy看着有95%。后来我按时间顺序切分数据,准确率直接掉到80%。原因很明确:模型见过的样本和未来真实出现的样本分布有差异,随机切分无形中制造了"窥视未来"的假象。
这个教训让我养成了一套最基础但完整的评测流程:
- 至少做三次不同随机种子的划分,看平均值和方差,不要只看单次结果。
- 额外加一个按时间切分的验证集,模拟真实未知数据。
- 对每个类别分别计算precision、recall、F1,而不是只看总体accuracy。
- 把预测错误的具体样本打印出来人工扫一眼,这是发现数据标注问题最高效的手段。
本质思路是:评测集的构造方式,决定了它能否代表线上真实数据。评测不是走流程,而是为了尽早暴露模型缺陷。
5.2 训练与推理不一致:那个最隐蔽的坑
模型部署后,一堆训练时想象不到的问题会冒出来。最典型的就是"训练推理不一致":训练时输入是清洗干净的文本,推理时用户送进来的全是带表情、带HTML标签、带聊天气泡的原始文本。如果推理端的预处理和训练端没对齐,性能会凭空掉好几个点。
我的做法很朴素:预处理逻辑抽成同一个函数,训练和推理共用,绝不允许训练脚本一份处理逻辑、API里又写一份。再给几个典型预处理案例写单元测试,每次发布前跑一遍。这个步骤救过我很多次。
5.3 线上监控:没有标签时怎么判断模型有没有变差
模型上线后,监控的核心问题是"模型效果会不会悄悄变差"。难点在于线上拿不到真实标签,没法直接算accuracy,只能用间接指标捕捉异常:
- 请求量和推理延迟:最基础的技术健康度指标,出问题最先暴露在这里。
- 预测标签分布:如果业务没有本质变化,但某个类别的比例从10%突然变成40%,大概率是数据漂移或模型异常。
- 输入特征分布:定期比较输入长度、数值范围等特征的分布变化。
实施上,早期不需要上重型监控平台。我是在每次推理时输出一行结构化日志,包含时间戳、输入长度、预测结果类别,定期做统计分析。等到服务多了,再考虑Prometheus和Grafana。先有日志,再谈监控。
6. 复盘:如果让我重新从零开始,我会怎么做
前面这些是我走完一遍"from scratch"之后回头看,确认值得走的路。最后一节,专门说几件我在实际操作中最有体会、但最容易被忽略的事。
6.1 新手最容易走偏的三个地方
第一,沉迷调模型结构,忽略数据质量。我前两个月就陷在这个状态里:模型从MLP换CNN再换Transformer,效果始终不理想,最后发现是数据管道里一个字段错位。先查数据,再动模型,这个顺序不能反。
第二,什么都想自动化,结果被工具折腾得寸步难行。MLflow、Kubeflow、Airflow都很好,但对一个刚起步的项目来说,维护成本可能超过收益。我的建议是:同一件事手工做三遍以上、确实痛苦了,再去找工具。工具的引入必须伴随明确痛点,而不是为了简历好看。
第三,不做实验记录。这个最隐形,代价也最大。你以为记住了参数,一周后就忘了。从零开始做工程,实验记录要从第一天做起,哪怕只是一个Markdown表:日期、模型、数据版本、超参数、主要结论。这是后续一切回归和对比的基础。
6.2 给当初自己的一张实操清单
如果让我列一张纸给刚入行的自己,我会写:
- 先花一天时间把所有工具链跑通,不追求理解,只求能跑。
- 用三天做最简端到端项目,哪怕结果很烂,关键是让数据从文件一路流到API返回。
- 时刻把配置、数据版本、日志记录挂在心上,前期多花一分钟,后期省一小时。
- 评测不是跑一个accuracy,要拆开看每个类别的表现和错误样本。
- 推理服务里模型只加载一次,预处理逻辑训练和推理必须共用。
- 不要一上来就上重型调度和编排工具,先手动,再逐步工具化。
这份清单基本覆盖了我认为"from scratch"最核心的落地动作。
6.3 什么时候该停下来补理论
经常有人问我:要不要先把深度学习理论学扎实再动手?我的答案是:两者并行,但动手的优先级更高。理论学习的价值在于遇到问题时知道往哪个方向找答案,但如果你从没亲手部署过一个模型,很多理论问题根本不会遇到。
我比较认可的节奏是:先跑通一个端到端项目,有了整体认知后再针对性补数学和模型原理。比如当你发现loss震荡到想放弃时,再回头看learning rate、batch size和优化器的文档,感觉会完全不一样。那条弯路并非白走——它让纸面上的概念有了落点。
如果你坚持读到这里,其实已经具备继续深入的基础:你知道了怎么把一个想法变成能跑、能交付、能维护的AI系统,你理解配置、数据和代码要管理起来,你懂得评测和监控必不可少。下一阶段无非是把某一块做得更深,或者去挑战更大规模、更多模态的项目。从零开始这件事,最难从来不是技术本身,而是熬过第一个"什么都还不成形"的阶段。只要按最小闭环一点点推进,那个阶段会比想象中短得多。