
1. 为什么多数人会在第一步就放弃对“从零开始”的误解我见过太多人满怀热情地打开 ai-engineering-from-scratch 的学习路线图然后在前两周就彻底泄气。不是因为学不会而是因为把“从零开始”理解成了“先把数学学完”。他们去买了一堆教材从《线性代数》到《概率论与数理统计》甚至有人先从微积分复习起打算把整本《深度学习》教材的前三章证明全部啃透再动手写代码。结果就是复习了一个月代码一行没写热情全烧完了。这里有一个反常识的经验AI工程的学习路径不是“先学完理论再实践”而是“用实践倒逼理论”。你不需要先成为数学博士才能训练模型你需要的是先跑通一个最小闭环然后在使用中把缺的知识补上。说一个我自己带新人的经验。团队里来了一个实习生数学基础一般但动手能力很强。我只给了他一个任务在两周内用 PyTorch 训练一个手写数字识别模型并把它包装成一个 HTTP 接口。他做到了虽然中间很多原理说不清楚但他已经建立了第一个完整的“数据-模型-训练-部署”心智模型。之后他再回头去学反向传播、学学习率公式都是有的放矢——他知道这些概念在解决什么问题。这比那些先打算“学完所有理论再动手”的人快了至少三个月。我还想强调的是“从零开始”指的应该是“从零开始动手做一个完整的AI项目”而不是“从零开始学数学”。数学很重要但它是建筑中的钢筋水泥不需要你亲自烧砖。你可以直接使用深度学习框架封装好的自动求导、梯度下降把注意力放在如何组织数据、如何设计模型结构、如何评估效果上。钢筋水泥的问题等你建到高层发现承重不够时再回来加固完全来得及。所以如果你正在看本文如果你曾经因为“数学不好”“基础不扎实”而迟迟没有开始AI工程现在可以调整心态了。你真正需要的只是以下几样东西一台能跑的电脑或者云服务器、一个 Python 环境、以及一颗准备踩坑的心。2. 第一个选择就决定成败搭建上手环境时的几个取舍环境搭建是很多人放弃的第二个节点。说实话这个环节我见过的问题比模型调参多十倍。用户卡在安装依赖、版本冲突、CUDA 不兼容这些事上会非常挫败。2.1 Conda 环境再懒也建议用我强烈建议你从第一天就使用虚拟环境而不是直接往系统 Python 里装包。很多新人图省事直接在全局环境里pip install torch一旦某个项目需要不同版本的 PyTorch 或 CUDA就会把整个环境搞成一团乱麻。推荐的做法是安装 Miniconda不是 Anaconda因为 Anaconda 自带太多用不到的包然后为每个项目单独创建环境。基本流程是conda create -n ai-starter python3.10 conda activate ai-starter pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118这里有个小细节PyTorch 的版本一定要对应你的 CUDA 版本。你可以先用nvidia-smi查看本机支持的 CUDA 版本再去 PyTorch 官网挑选对应的安装命令。如果显卡驱动太旧不一定要升级驱动直接在官网选择低一点的 CUDA 版本通常也能跑。2.2 GPU 和云端的现实选择很多人在学习阶段就纠结“没有 GPU 怎么办”。我的建议是没有 GPU 完全不影响入门。手写数字识别、简单的图像分类、文本情感分析这些任务用 CPU 跑几分钟到十几分钟就够了。深度学习框架的训练开销并没有你想象得那么大——当然你要有个合理的预期:别拿 CPU 去硬跑大规模 Transformer 从零训练那个级别的项目本就不该出现在入门阶段。如果你的主要环境是公司的台式机或自己的笔记本可以按以下优先级来借用资源公司已有的内网 GPU 服务器。很多人不知道自己的团队里其实有 GPU多问一句就能省下很多事。Kaggle Notebook 或 Google Colab 的免费 GPU。虽然免费额度有限但足以完成入门项目。云厂商的竞价式 GPU 实例。按小时计费比较便宜用完记得销毁避免忘记关机导致额外费用。在这里我要特别提醒一个云服务器常见的坑——数据所在位置和计算所在位置之间的传输延迟。如果数据在 OSS 或 S3 上每次训练都实时从远程拉数据训练速度会变得不可接受。务必要把数据集先同步到离计算资源最近的本地磁盘再用 DataLoader 读取。2.3 直接把运行环境固化下来从第一个项目开始就养成写requirements.txt或environment.yml的习惯。我见过很多朋友换电脑后环境搭建又花了一整天。只要把这个文件保存好在任何新机器上都能用两三条命令还原环境name: ai-starter channels: - defaults dependencies: - python3.10 - pip - pip: - torch2.0.1 - torchvision0.15.2 - numpy1.24.3 - pandas2.0.3 - scikit-learn1.3.0 - tqdm - jupyter这一份文件就是你的“环境保险”。这也是 AI 工程第一步里面最被低估的工程习惯。3. 用最小闭环建立第一个训练流水线从 MNIST 到端到端项目环境搭好之后第一个项目的选择极其关键。我建议你不要找一步到位的语音识别或者推荐系统而是找一个足够小、但在结构和流程上五脏俱全的经典项目MNIST 手写数字识别。有人会说“MNIST 太入门的大家都在跑不够亮眼。”但我的经验是——MNIST 的最大价值不在于难度而在于它能让你在一小时以内跑完一个完整的训练和评估流程同时还能暴露很多基础的工程问题。3.1 数据管线不要直接硬编码下载很多教程会直接写一行datasets.MNIST(root./data, downloadTrue)然后就走下去了。但对一个 AI 工程师来说数据管线的思维要从第一天建立。至少要做两件事把原始数据单独存放不要把数据目录和代码目录混在一起。设置固定的随机种子打散数据时保证可复现。推荐的项目目录结构大概是├── configs/ # 配置文件 │ └── mnist.yaml ├── data/ # 原始数据 │ ├── raw/ │ └── processed/ ├── models/ # 模型定义 │ └── cnn.py ├── src/ # 训练脚本 │ ├── train.py │ └── predict.py ├── tests/ # 单元测试 └── requirements.txt看起来好像没什么大不了的但你只要这样做过一两个项目再回头看到把所有 .py 文件都堆在根目录下、数据散落在桌面上的代码库你就能直接感受到“工程化”这三个字的分量。3.2 训练脚本的关键组件训练脚本的核心逻辑其实很固定你可以把它当作一个模板来记忆。无论以后是用在语义分割还是文本分类上骨架都是一样的加载配置和数据集初始化模型定义损失函数和优化器循环若干个 epoch前向传播计算 loss反向传播优化器更新权重每个 epoch 结束后在验证集上计算指标保存最优模型 checkpoint用 PyTorch 写出来大概是这样import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms # 固定随机种子这对可复现性很重要 torch.manual_seed(42) transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset datasets.MNIST(root./data/raw, trainTrue, downloadTrue, transformtransform) val_dataset datasets.MNIST(root./data/raw, trainFalse, downloadTrue, transformtransform) train_loader DataLoader(train_dataset, batch_size128, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size256, shuffleFalse, num_workers4) class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 32, kernel_size3) self.conv2 nn.Conv2d(32, 64, kernel_size3) self.fc1 nn.Linear(64 * 5 * 5, 128) self.fc2 nn.Linear(128, 10) def forward(self, x): x torch.relu(self.conv1(x)) x torch.max_pool2d(x, 2) x torch.relu(self.conv2(x)) x torch.max_pool2d(x, 2) x x.view(x.size(0), -1) x torch.relu(self.fc1(x)) return self.fc2(x) model SimpleCNN() criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) num_epochs 5 for epoch in range(num_epochs): model.train() running_loss 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() acc 100 * correct / total print(fEpoch [{epoch 1}/{num_epochs}], Loss: {running_loss / len(train_loader):.4f}, Val Acc: {acc:.2f}%) # 保存模型 torch.save(model.state_dict(), ./models/mnist_cnn.pth)这段代码本身没什么特别的但请留意几个局部torch.manual_seed(42)写在所有数据加载之前它保证每次运行得到几乎相同的初始化参数和打乱顺序。如果你不固定很可能第二天再跑同一个代码结果会有些差异。验证阶段必须加model.eval()和torch.no_grad()这会影响 BatchNorm 和 Dropout 的行为也会节省显存和加速推理。num_workers不是越大越好Windows 平台上设 0 最安全Linux 上可以设成 CPU 核心数的一半。很多新手在这里遇到“死锁”或“内存爆炸”就是num_workers设得太高数据子进程频繁拷贝导致的。3.3 第一次训练时遇到最多的三个报错这里直接列我帮别人调试时重复反复遇到的三个问题省得你走弯路问题一张量在 CPU 上模型在 GPU 上或反向。报错通常长成这样Expected all tensors to be on the same device。解决办法是在训练循环里判断一下是否有 GPU然后把images和labels都.to(device)。问题二维度对不上。常见于手写线性层输入维度算错。以 MNIST 为例输入是 28×28经过两个 stride 为 1 的 3x3 卷积和两次 2x2 max pooling 后特征图尺寸是 5×5。如果你用了 5x5 卷积或者步长不一样最后展平进全连接层的特征维度就得重算。不要硬靠脑子算老老实实在某个 forward 里打印x.shape一眼就能看出问题。问题三DataLoader 卡死不动。如果你在 Windows 上把num_workers设成大于 0很可能训练在第一个 epoch 就卡住。最简单的方案是直接设num_workers0如果一定要用多进程需要把代码放到if __name__ __main__:下面。这个坑非常多人都要踩一遍。4. 工程化思维让代码从“能跑”变成“别人能用”很多人训练出了模型自认为已经完成。但从 ai-engineering-from-scratch 的角度来看训练出一个模型只算完成了三分之一剩下三分之二是工程化让代码可复现、可维护、可交给别人使用。4.1 数据版本与可复现性模型和数据的血缘关系很多项目里模型文件存了好几个版本训练脚本也改了一堆但没人说得清哪个模型是用哪份数据训练的。这在团队协作里会引发灾难。我见过一个真实案例模型在验证集上准确率 95%上线后表现却很差。排查了一个星期最后发现训练脚本里有一行代码不小心做了数据清洗把一部分“困难样本”直接过滤掉了。训练时爽了线上真实数据可没有经过同样的清洗流程。解决这个问题不需要上特别复杂的工具做两件事就行每个训练实验都记录下数据版本、代码 commit hash、超参配置。哪怕只是写一个train.log也远远好过什么都没有。把原始数据、中间处理脚本、最终训练脚本、模型权重放到“同一个时间戳文件夹”下保持数据血缘清晰。比如experiments/ ├── exp_20250110_1435/ │ ├── config.yaml │ ├── train.log │ ├── model.pth │ └── data_snapshot.md5如果你后续想尝试更专业的方案可以用 DVC 管理数据、用 MLflow 管理实验追踪。但对个人起步来说养成“每次训练前记下数据 MD5”和“打开一个训练日志”这两个习惯已经能让你的工程素养超过很多调包侠。4.2 把超参和代码分离用配置文件代替硬编码我看过太多人的训练代码是直接写死的learning_rate 0.001 batch_size 128 num_epochs 5这在小项目里很省事但当你要跑十组实验对比学习率时就得手动改代码跑十次效率低且容易改错地方。更好的做法是引入一个简单的配置管理模块。可以先用最朴素的 YAML 文件加argparsemodel: name: SimpleCNN hidden_size: 128 train: learning_rate: 0.001 batch_size: 128 num_epochs: 5 seed: 42 data: dataset: MNIST data_dir: ./data/raw训练入口统一用python train.py --config configs/mnist.yaml读取。这背后的逻辑就是**“代码与配置分离”**。代码负责逻辑配置负责变化。当你以后做复杂项目时要调整的不再是程序本身而是那些“声明式”的配置文件。团队里谁都可以安全地改配置而不会把代码改坏。4.3 单元测试至少要覆盖数据预处理很多搞 AI 的人觉得“写单元测试是软件开发的事和我无关”。但 AI 项目恰恰更容易因为数据预处理的微小 bug 导致整个模型失灵而这类 bug 还特别隐蔽通常不会报错。我建议在每个项目里至少给数据预处理写一个冒烟测试比如确认每个样本的形状符合模型输入预期确认标签范围正确确认标准化后的像素分布大致符合预期这种测试写起来很简单一眼就能看完但能在你改动数据增强策略时迅速发现新问题。你的项目规模越大固定的测试带来的安心感越大。5. 让模型真正创造价值部署为服务的几个实用选择训练好的模型如果只停留在.pth文件里它只能算作业。只有当它被一个接口封装、可以被业务调用时才真正成为工程的一部分。这也是 ai-engineering-from-scratch 中“工程”二字的另一半含义。5.1 最简单的 HTTP 服务方案如果你只是打算做一个 demo 或者内部小工具完全不需要上复杂的部署平台。用 FastAPI 就可以在二十分钟内把 PyTorch 模型包装成服务。import io import torch from torchvision import transforms from PIL import Image from fastapi import FastAPI, UploadFile import torch.nn as nn app FastAPI() # 复现模型结构 class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 32, kernel_size3) self.conv2 nn.Conv2d(32, 64, kernel_size3) self.fc1 nn.Linear(64 * 5 * 5, 128) self.fc2 nn.Linear(128, 10) def forward(self, x): x torch.relu(self.conv1(x)) x torch.max_pool2d(x, 2) x torch.relu(self.conv2(x)) x torch.max_pool2d(x, 2) x x.view(x.size(0), -1) x torch.relu(self.fc1(x)) return self.fc2(x) model SimpleCNN() model.load_state_dict(torch.load(./models/mnist_cnn.pth, map_locationcpu)) model.eval() transform transforms.Compose([ transforms.Grayscale(), transforms.Resize((28, 28)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) app.post(/predict) async def predict(file: UploadFile): image_bytes await file.read() image Image.open(io.BytesIO(image_bytes)) tensor transform(image).unsqueeze(0) with torch.no_grad(): logits model(tensor) pred torch.argmax(logits, dim1).item() return {prediction: pred}这里有一个很容易忽略的细节而且和训练阶段的行事风格完全不同部署时不要再写.cuda()也尽量不要把模型放在 GPU 上推理。小模型用 CPU 已经足够快省去显存和电费还不会遇到“线上没有 GPU”的麻烦。还有一个更隐蔽的问题——训练时的预处理和数据增强部署时必须“原样复刻”。很多模型上线后效果不佳就是因为训练时对图片做了Resize(28, 28)部署时却忘了直接把原图丢进模型。这种“预处理不一致”的错误我在真实项目里看过不止两次。5.2 从单机到容器其实没有想象中复杂当你需要把服务交给运维或部署到服务器时Docker 是在所难免的一步。很多人被 Docker 的几百条命令吓住其实对一个模型服务来说只要写好两个关键点就够选择干净的 PyTorch 基础镜像而不是从零安装。把requirements.txt复制进镜像后执行pip install尽可能利用 Docker 的缓存层。示例的Dockerfile可以简化成FROM pytorch/pytorch:2.0.1-cuda11.8-cudnn8-runtime WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt fastapi uvicorn COPY . . CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]需要注意的坑是基础镜像里自带的 PyTorch 版本可能和你本地不一致。保守的策略是在容器里也跑一个简单冒烟测试确认模型的预测结果和本机一致。5.3 显存管理推理阶段的“内存碎片”问题如果你要部署多个模型到同一张 GPU 上会经常遇到一个诡异现象明明每个模型只占 1GB 显存4 个模型应该只需要 4GB但nvidia-smi显示显存占用却是 6GB。原因是每个模型的框架上下文和 CUDA 缓存有额外开销尤其是不同的模型结构组合时显存会出现碎片化。我的经验是尽量把多个模型放到同一个进程里管理而不是各起一个服务进程。开启 PyTorch 的显存缓存策略torch.cuda.set_per_process_memory_fraction或使用max_split_size_mb控制缓存分配。监控显存占用时需要区分“实际占用”和“缓存占用”用torch.cuda.memory_summary()查看。当然这是进阶内容。如果你只是学习阶段知道这个坑存在就行不必立刻优化。6. 从第一阶段走向真实的AI工程绕不开的实验管理与团队协作当你完成了上面几步你其实已经掌握了“独立完成一个AI项目”的基本盘。但距离真正的 ai-engineering-from-scratch 还差最后一块拼图实验管理和协作习惯。6.1 为什么要写实验记录很多初学者做实验是“灵感驱动”的今天觉得加一个 Dropout 效果好明天觉得换个优化器效果好。最后模型确实调到了一个不错的点但完全说不清关键的是哪一步。我自己的做法是把每一次实验当作一次“假设-验证”的过程。在动手之前先写下一句话我预期什么会变好基于什么原因。然后只改动一个变量。跑完之后记录结果判断是否符合预期。这既是工程效率问题也是科学思维问题。这么做还有个额外好处当你和团队成员沟通时能给出清晰的实验脉络而不是“我试了很多方法最后发现这个最好”。后者会严重拖慢协作效率。6.2 模型评估不能只看“准确率”如果你只用一个指标评估模型非常容易陷入“泛化能力不足但指标好看”的陷阱。尤其类别不均衡的数据集准确率几乎是废物。以 MNIST 为例所有类别都相对均衡准确率还可以但到了真实场景的用户回访、故障检测、异常识别准确率会被多数类完全主导。推荐的评估维度至少有精确率、召回率、F1-score 的宏平均和加权平均混淆矩阵看具体哪些类别互相混淆在“最坏子集”上的表现比如光照较差的图像、文本较长的样本这些指标的计算在 scikit-learn 中只要几行代码但很多人图省事最终把模型的短板全部隐藏起来。6.3 遇到的问题和快速定位思路这里总结一份“先查这些”的清单当你发现模型不收敛或者效果很差的时候不要急着换模型结构很多人上来就换更大的模型结果问题并没有解决先查数据标签是否干净有没有 NaN有没有顺序泄漏很多时候模型效果差的根因都在数据上模型只是背锅侠。查 Loss 曲线训练 loss 是否下降验证 loss 是否在早期就开始回升过拟合如果不降可能是学习率过大或网络初始化有问题。查预处理与模型是否匹配输入尺寸、归一化范围、图像 channel 顺序这些细节足以让模型报废。查随机种子同一个代码多跑几次如果结果差异巨大说明训练过程方差太高需要先降低方差或在多次运行中取平均结果。7. 写在最后保持“从零开始”的心态我见过很多做到算法工程师岗位的人依然在每个项目启动时把自己当作“从零开始”。不会因为以前做过图像分类就轻视一个新的视觉任务的数据特点也不会因为自己熟读 Transformer 就跳过数据分析直接开训。“ai-engineering-from-scratch”这件事真正的意思是哪怕你已经很熟练也永远要从数据和问题本身出发老老实实把基线搭好把训练闭环跑通再一点点增加复杂度。这个思路适用于 MNIST适用于工业质检也适用于大语言模型的应用开发。以我个人的体会来说从零到一的过程里最容易被忽略的不是某个高级技巧而是“把基础闭环走完整”。只要你能独立完成一个“数据加载-模型训练-结果评估-服务化部署”的项目你其实已经跨过了 AI 工程最陡峭的门槛。如果你现在还卡在环境搭建或者第一个 epoch 上别怀疑自己那只是必经之路。按本文的顺序一步步走下来很快你就能拥有自己的第一个端到端项目。