很多人一上来就对着大模型 API 调个 prompt觉得自己已经会 AI 工程了。等真正要上线一个服务面对并发、延迟、成本、模型版本管理、数据回流这些事的时候才会发现那层窗户纸有多厚。我整理这套ai-engineering-from-scratch就是想把这层窗户纸捅破记录一条从零开始、不靠调包侠式速成、真正理解 AI 工程化全流程的路子。这个项目不是什么高屋建瓴的宏大框架它更像一张我亲自走过的地图。从 Python 工程基础、机器学习核心概念到深度学习框架的选择、模型训练与评估的完整闭环再到 MLOps 里的部署、监控、CI/CD每一块都用实际可跑的代码来驱动。适合谁看一种是已经开始写模型代码但没接触过工程化的人另一种是负责系统集成但对 AI 内部一头雾水的后端工程师。你不需要数学博士背景但看完之后你会对AI 应用到底是怎么从笔记本 Jupyter 走向生产环境这件事有画面感。1. 这个项目的整体拆解AI 工程不是堆模型而是搭系统先给 AI 工程画个像。很多人以为 AI 工程 训练模型 调参其实那只是最核心却也最小的一块拼图。真正放到生产环境里的 AI 系统是一个完整的闭环数据管道负责喂料特征工程负责提炼模型负责推理服务层负责对外响应监控负责感知模型是否退化或数据是否漂移反馈回路负责把线上新数据带回训练集。这套东西拆开看都不算难合在一起就是一门工程。从零开始学最大的问题不是知识点太多而是容易一上来就被全栈全景图吓住。我拆解这套内容的第一原则是削减噪音锚定主线。机器学习、深度学习、强化学习、NLP、CV、LLM……当前 AI 领域的概念多到能让人一周换一个方向。但仔细想基础主干就那么几条数据怎么处理、模型怎么架构、损失怎么设计、训练怎么稳定、推理怎么加速、上线怎么服务。抓住这些任何具体方向的模型对你来说都是新瓶旧酒。第二原则是重理解更重复现。我不怕代码写得啰嗦就怕只贴高层接口。from transformers import pipeline一行调用谁都会写但模型内部到底发生了什么、为什么输入要 padding 和 mask、batch 和 epoch 的 trade-off 在哪——这些背后的细节才是工程取舍的根基。所以这套项目里所有核心算法我尽量从张量操作级别开始写哪怕效率低一点也要让肌肉记忆和认知模型同步建立。第三原则是按生产环境的真实流程编排内容。不是按算法分类学来组织章节课而是按一条项目的生命周期走需求定义 - 数据处理 - 基线模型 - 训练调优 - 评估分析 - 部署上线 - 监控迭代。这个设计的好处是学完每一章你都能看到一个真实系统里对应环节应该长什么样而不是一堆孤立的知识孤岛。1.1 核心需求解析谁需要从零开始学 AI 工程从我收到的反馈看关注这条路线的人主要分三类。第一类是计算机相关专业的学生课程里接触过 Python 机器学习的皮毛但没参与过实际项目毕业设计可能是调库实现一个分类器。他们的痛点是要从会调库走向会设计。第二类是中后台或全栈工程师公司业务开始引入模型能力他们需要不只是接入 API还需要理解模型服务的性能特征、成本构成和部署限制。第三类是业务侧的半个技术人比如产品经理想搞明白 AI 系统在工程上为什么慢、为什么贵、为什么改一个模型要那么久。这三类人的共同需求其实不是某一个具体算法而是一条完整的、带路径依赖的学习线索。比如我到底应该先学 PyTorch 还是先学部署先学传统机器学习还是直接跳深度学习这些都是我在项目的 FAQ 里专门回答的问题。我的答案是以工程链路为主线哪里卡住了就补哪里的基础。不要花三个月系统啃完概率论再动手而应该是写代码时遇到某个分布概念花半天搞懂它然后继续往前走。1.2 方案选型思考为什么选 Python PyTorch Docker 这套主流组合技术选型上我几乎毫无悬念地选了 Python 作为主语言PyTorch 作为深度学习主力框架Docker 作为环境封装手段。解释一下为什么。Python 在 AI 领域的统治地位短期不会动摇原因不完全是语言本身优雅而是生态的雪球效应。NumPy、Pandas、scikit-learn、PyTorch、Hugging Face所有你想得到的工具都优先支持 Python。作为工程博主我不想教读者用冷门技术流标新立异因为工程项目的真实目标是别人能维护、能接手、能持续跑。PyTorch 相比 TensorFlow在研究和工程之间的平衡更舒适。它的动态图机制让调试和原型验证变得特别顺手打印中间张量、改一行网络结构、在训练循环里断点检查这种所见即所得的体验对新手极其友好。TensorFlow 当然也很好但它的 API 版本变迁太多初学者很容易在旧教程的坑里迷失。PyTorch 从 1.0 到现在核心接口的稳定性做得相当好。Docker 这一步很多人前期会以为我又不搞运维学它干嘛。但实际上AI 环境依赖是出了名的玄学——CUDA 版本、cuDNN 版本、torch 版本、Python 版本任何一个错位都会让在我机器上明明能跑变成行业笑话。用 Docker 把环境固化成镜像是对自己耐心的保护也是团队协作的基本礼貌。2. 核心细节与实操要点环境、数据与模型三件套从零起步最先要过的三关是环境搭建、数据组织和模型结构理解。这三关不过后面所有花哨的内容都是空中楼阁。2.1 环境搭建别小看 Python 虚拟环境这第一步我见过太多人入坑 AI 是因为环境崩溃而非算法太难。Python 的包管理默认是全局安装不同项目依赖不同版本的 NumPy、torch 时全局环境很快会变成一场灾难。所以第一步一定要做虚拟环境。Python 3.3 之后自带venv但实际用下来我更推荐conda或poetry。conda的优势是对科学计算包的预编译处理远好于 pip尤其在 Windows 上装torch这类重型包时conda 自动解决 CUDA 依赖的体验会让人少掉不少头发。conda create -n ai-eng python3.10 conda activate ai-eng conda install pytorch torchvision torchaudio pytorch-cuda11.8 -c pytorch -c nvidia这里有个细节值得拎出来说CUDA 版本选择。如果直接pip install torch装的是 CPU 版本训练速度会慢得让人怀疑人生。我的建议是查看 PyTorch 官网的安装向导根据自己的 GPU 驱动版本选择合适的 CUDA 编译版本而不是无脑装最新。装完之后马上验证 GPU 是否可用import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))我实测下来的经验是环境搭建环节至少坑掉新手一周时间不算夸张。所以我把这些验证脚本写进了项目的第一个脚本文件里就是为了让读者每次换新机器都能快速确认环境健康。2.2 数据组织与预处理数据决定上限模型只是逼近这个上限机器学习界有句老话Garbage in, garbage out。数据的质量直接决定模型的上限之后的所有努力都只是在逼近这个上限而已。但从工程视角看光数据质量好还不够还需要数据流动顺畅。我这一章讲的不是 Pandas 清洗技巧而是数据链路的整体设计。首先是数据版本化。训练集、验证集、测试集一旦产生就不能随意改动。你训练了一个模型记录指标时一定要能追溯到当时使用的数据版本。这一步很多小团队会忽略直到调参调得怀疑人生才发现不是模型退步了而是训练集悄悄变了。工程上轻量级方案是给每个数据集文件打上哈希值或者在数据目录上使用 Git LFS 或 DVC 这类工具。其次是预处理的一致性。这一步是深度学习最容易踩的坑训练时做了归一化、做了 tokenization验证和推理时却忘了做同样的处理。必须把预处理逻辑封装成可复用的函数或类训练、验证、推理都调用同一套代码。更好的做法是把预处理逻辑写进 dataset 类的__getitem__方法里这样不管数据从哪个入口进入都会经过同样的处理管线。我在代码里专门演示了这个模式强烈建议读者形成这个肌肉记忆。2.3 模型核心要义从全连接到卷积到 Transformer 的逻辑脉络模型部分我不建议一上来就啃 Transformer 的论文原文而是按结构演进的历史顺序去理解。先看全连接网络理解反向传播和梯度下降的基本流程。然后加一点卷积的概念理解空间局部性和权重共享为什么能让图像任务从全连接的鸡肋中解放出来。再到序列问题理解 RNN 的梯度消失困境以及注意力机制如何用全局相关性的加权求和替代逐步传递的隐状态。每一步不只是看结构图还要看代码实现。全连接网络的实现最简洁几十行就能写清楚它的前向传播、损失计算、反向传播逻辑是理解所有后续复杂模型的骨架。我在项目中提供了一个用 NumPy 从零实现单隐层网络的代码不依赖任何深度学习框架。这个练习的价值在于它强迫你关注张量的形状变化和梯度流动——这是我见过理解深度学习最快的方式。之后引入 PyTorch 的nn.Module抽象时你会发现框架帮你做的事情本质上就是把这些张量操作和梯度传播封装起来。你对底层有画面上层就是工具的语法糖而已。当模型来到 Transformer 这一层工程上的复杂度就增加了。多头注意力、位置编码、LayerNorm、残差连接每个组件都有它的角色缺一个训练效果就会打折。在这个项目里我不会回避这些概念但也不会陷入纯学术推导。重点讲清楚这套结构在工程上为什么如此高效并行计算取代了 RNN 的串行依赖自注意力让长距离依赖不再是奢望而残差连接和 LayerNorm 则让深层网络训练稳定。3. 实操过程与核心环节实现跑通一个真正的训练闭环理论讲得再好不如跑通一个完整流程。这套项目的核心实操主线是训练一个图像分类模型并让它跑在一个生产风格的服务里。数据我选了 CIFAR-10原因简单够小、够经典、训练一个 baseline 在当前硬件上也就是几十分钟的事不会让小白在等待中失去耐心。3.1 数据管道搭建让数据按需流动起来PyTorch 里数据加载有一套约定俗成的接口Dataset负责定义如何从原始数据中取一个样本DataLoader负责定义如何批量加载这些样本。为什么要分层因为工程上两者优化方向不同。Dataset关注逻辑要不要做数据增强、要不要做归一化巡检逻辑和变换逻辑分得清清楚楚。DataLoader关注机制乱序、并行预取、批量大小。class CIFAR10Dataset(Dataset): def __init__(self, data, labels, transformNone): self.data data.transpose((0, 2, 3, 1)) # (N, C, H, W) - (N, H, W, C) self.labels labels self.transform transform def __len__(self): return len(self.labels) def __getitem__(self, idx): image self.data[idx] label self.labels[idx] if self.transform: image self.transform(image) return image, label注意这里我做了通道维度的转换。CIFAR-10 原始数据格式是(N, C, H, W)但很多图像处理库期望的是(H, W, C)这种细节就是那些报错半天找不到原因的典型来源。实操中transforms.ToTensor()会把 PIL 图像从[0,255]转到[0,1]的张量transforms.Normalize((0.5,), (0.5,))则先把像素归一化到[-1,1]。这套组合拳跑通之后你自然就知道下次换个数据集该怎么写了。3.2 训练循环那些框架帮你隐藏的细节新手最容易把训练过程理解成model.fit()一行命令。实际上即便用 PyTorch 这种高层抽象训练循环的几个关键点依然需要亲手设计模型设备位置、梯度清零时机、优化器 step 的位置、loss 的累积方式、学习率调度策略。我见过不少跑出 NaN loss 的案例八成都是梯度清零位置写错了。for epoch in range(num_epochs): model.train() total_loss 0.0 for batch_idx, (inputs, targets) in enumerate(train_loader): inputs, targets inputs.to(device), targets.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, targets) loss.backward() optimizer.step() total_loss loss.item() * inputs.size(0) avg_train_loss total_loss / len(train_loader.dataset)这段代码里的几个细节值得展开。model.train()和model.eval()切换的是 BatchNorm 和 Dropout 的行为漏了的话训练指标和验证指标会呈现出极其诡异的差异。optimizer.zero_grad()必须在每次反向传播前清空梯度否则梯度是累加的。loss.item()是把一个标量张量从计算图中脱离出来变成 Python 数值这样我们记录指标时不会让梯度图不断膨胀导致内存爆掉。验证环节有一个强制要求用torch.no_grad()包住整个推理过程。这个上下文管理器会关闭自动梯度追踪不只是省内存更是在语义上明确我此刻不做反向传播。在框架层面loss.backward()是通过计算图反向传导的推理场景根本没有这一步所以没必要保留中间变量的梯度记录。3.3 模型定义以工程视角重构经典结构在代码结构上我不会把模型写成一坨nn.Sequential堆到底。可维护性的起点是模块化拆分。以我项目里的 ConvNet 为例我仿照 VGG 的思路搭了一个只有 5 层卷积的小网络特征提取部分被组织为 conv_block分类头部分由两层全连接构成。这样写的好处是当你想换 Block 结构时只改一个构建函数而不是毁掉整个网络定义。def conv_block(in_channels, out_channels, poolTrue): layers [ nn.Conv2d(in_channels, out_channels, kernel_size3, padding1), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), ] if pool: layers.append(nn.MaxPool2d(2)) return nn.Sequential(*layers)这里 BatchNorm 放在卷积之后、激活函数之前是一种主流排列。inplaceTrue 是为了省内存ReLU 的输出不会保留原始输入这样前向传播时内存占用能降不少。MaxPool2d 的作用是把空间维度减半强制让下一层看到更大的感受野同时降低计算量。这些设计决策都不是玄学你会在训练曲线里直观看到它们的作用。3.4 训练指挥与结果判读准确率之外还要看什么CIFAR-10 上我跑了一个约 15 轮的基线模型Softmax 分类准确率到 70% 左右就稳定了。对新手来说70% 已经足够用来检查训练流程是否健康但在项目中我还想传递一个更重要的思维方式训练曲线比单个指标更有诊断价值。loss 曲线如果训练和验证之间的 gap 越来越大说明模型在过拟合如果两个 loss 都不下降问题可能出在学习率或者模型容量上如果验证 loss 出现剧烈震荡可能要怀疑数据批次之间差异过大或者学习率偏大。所以我在代码里会输出每一轮的训练 loss、验证 loss 和验证准确率并且存为一个历史记录数组。同时用 matplotlib 画出来。这不是什么高深操作但它真的能帮你从盯着测试集数字紧张变成看曲线判断病根在哪的老手。实操到后面你会发现调参的乐趣有很大一部分来自曲线形态的解读。4. 从训练到生产构建模型服务的完整工程能力训练出了一个能用的模型权重文件只是万里长征第一步。模型若只活在 notebook 里那它产生的价值为零。到了这一阶段我要把工程能力往生产环境推进持久化模型、封装推理服务、用容器固化环境。4.1 模型的持久化与版本管理PyTorch 的torch.save(model.state_dict(), path)保存的是模型参数而非完整模型类。这个细微差别很多刚接触的人会踩坑换一个文件重新定义模型类会导致加载失败。工程上的最佳实践是只保存 state_dict同时在代码仓库里保留模型定义文件两者严格配套管理。加载时先实例化一个结构完全相同的模型再load_state_dict并model.eval()切换到推理模式。模型文件本身也应当版本化。我在项目里给模型文件命名时带上时间和准确率信息例如model_cifar10_acc71.3_epoch15.pt这样任何时刻回溯都能清楚知道这个文件出自哪一轮训练。更进一步可以把这个文件路径记录在一个experiments.csv表格里连同训练参数和最终指标一起存档。这其实就是轻量级实验管理的雏形团队协作时尤其有用。4.2 推理服务封装把模型包装成可调用的 HTTP 接口模型放到生产环境的核心模式是把它封装成内部引擎外面包一层 API 接口。框架我选用 FastAPI原因很直白自动生成 API 文档、天然异步支持、类型校验由 pydantic 兜底对工程化落地非常友好。推理服务的基本结构分成三块启动时加载模型、请求时预处理数据、响应时返回结果。app FastAPI() model None class PredictRequest(BaseModel): image_base64: str class PredictResponse(BaseModel): label: int prob: float app.on_event(startup) def load_model(): global model model CIFAR10Model() model.load_state_dict(torch.load(MODEL_PATH)) model.eval() app.post(/predict) def predict(req: PredictRequest): image decode_base64_to_array(req.image_base64) tensor apply_preprocess(image) with torch.no_grad(): logits model(tensor.unsqueeze(0)) prob torch.softmax(logits, dim1).max().item() label logits.argmax(dim1).item() return PredictResponse(labellabel, probprob)这段代码里有两个实战细节。模型加载放在 startup 钩子里是为了避免第一次请求时才加载模型带来的 latency spike。推理时torch.no_grad()与.eval()双保险一个管语义一个管性能。输入输出我都声明了明确的 model 结构这样 FastAPI 会自动完成参数校验和错误返回咱不用手写一堆if not req.image_base64之类的防御代码。4.3 容器化部署让服务可移植、可回滚、可扩展生产环境不是只有一台永远不关机的开发机。为了让服务能在任何机器上一键运行必须用 Docker 把环境、代码、模型全部固化成镜像。我在项目里提供了完整的 Dockerfile基础镜像选择pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime因为基础镜像已经包含了 PyTorch 和 CUDA 运行时镜像体积比从 Ubuntu 一层层装环境小得多。FROM pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app ./app COPY models ./models EXPOSE 8000 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]最容易被忽视的是.dockerignore文件。如果你不排除.git目录、开发缓存、测试数据这三件套会把镜像撑大到几个 G 甚至十几个 G。我实际踩过的坑是__pycache__和.ipynb_checkpoints被带进镜像导致容器里出现一些莫名其妙的缓存文件后来排查半天才发现来源。这个经验特别适合写进自己的部署 checklist 里。5. 避坑指南与常见问题把我在路上踩过的坑告诉你学 AI 工程最怕的不是难题而是那些没人告诉你但一定会遇到的小问题。下面这些坑几乎每个学生或转行工程师都会至少踩一个。5.1 设备张量混乱CPU 和 GPU 的隐性战争最常见的一个报错是Expected all tensors to be on the same device。模型已经在 GPU 上但输入数据还在 CPU 上。光看报错信息往往一头雾水因为涉及的不只是模型和数据还有 target 标签。我在训练循环开始前就统一把inputs和targets传到同一设备这必须成为日常习惯。5.2 归一化不一致训练与推理结果天差地别这是最隐蔽的坑。训练时你用了Normalize(mean, std)但推理时忘了处理就是那种一个模型在测试集上天衣无缝、在线上完全失控的经典现场。解决思路也很简单把预处理逻辑做成一个全局可复用的函数训练和推理只允许走同一个入口。我在项目中刻意设计了build_transform()并在训练脚本和 API 服务里都引用它就是示范这个统一入口的写法。5.3 数据泄露评估结果虚高的隐秘原因举个具体例子做分类任务时如果全量数据先做归一化计算出全局均值和方差再划分训练集和测试集测试集其实已经被污染了。正确顺序永远是把数据先切分再在各数据集内部独立计算统计数据。很多竞赛和老项目里都存在这类问题指标漂亮得夸张上线上线就现原形。这也是为什么我会在项目里专门强调在划分数据之后再定义预处理流程。5.4 训练超参数动不动崩到 NaN 的日常loss 变成 NaN排除数据里有缺失值之后多数情况是学习率设置太大。尤其用 Adam 时初始学习率超过 1e-3 就很容易在深层网络上爆炸。解决思路是先用一个很小的学习率比如 1e-4做一个 sanity run确认梯度方向没问题再逐步调大。我项目里还加入了梯度裁剪作为保险torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)注意梯度裁剪只在梯度爆炸成为问题时才需要。正常训练别把它当作默认配置过强的裁剪会干扰训练动态。6. 我把这套项目做成开源仓库后的三点心得走在 ai-engineering-from-scratch 这条路上的实践让我对如何学和如何教这两件事有了与当初完全不同的理解。写这套项目之前我以为阻碍新人的主要是数理基础真正跑下来之后我发现天堑往往是工程习惯的缺失。第一点心得是代码结构就是思考结构。当一个新手能把 dataset、model、train、eval、inference 这几个模块干净地拆开并串起来他对 AI 系统的理解就已经超过了大半只会在 notebook 里写流水账的人。模块边界清晰意味着你脑子里对这些环节的边界也清晰。我给所有章节代码都配了循序渐进的重构步骤从能跑通到能维护每一步都告诉你为什么要调整结构。第二点心得是没有所谓看完就会了的事。我在项目里刻意把一个模型反复训练了三遍第一遍为了理解数据流第二遍为了解剖训练循环第三遍为了部署上线。整个过程下来你才会真正建立训练一个模型到交付一个模型的肌肉记忆。有人问我不看这些一步一步做、只看代码可以吗可以但你得到的只是半张地图另一半被藏在了亲历的过程里。第三点心得是这个项目可以继续生长。当基础路径跑通之后往里加 MLOps 工具链、加模型量化、加 A/B 测试框架会变得非常顺理成章。我后续打算补充的是一套针对线上推理服务的性能调优实战以及 LLM 应用开发中的工程控制手段。这个仓库与其说是一个课程不如说是一张由真实经验铺出来的索引地图。愿每一个从零开始的人都能找到自己那条不用从头踩坑的捷径。