1. 从零开始搞AI工程先别急着写代码我见过太多人一上来就抱着Transformers源码啃或者直接开个GPU实例跑Stable Diffusion结果三天之后连自己项目里哪里是数据、哪里是模型、哪里是推理服务都说不清楚。这个“ai-engineering-from-scratch”的项目标题说白了就是一条从零搭建AI工程能力的学习与实战路径。它不是什么高深理论课而是一套让开发者、算法工程师、甚至刚入门的学生能真正把一个AI想法落地成可运行、可维护、可扩展系统的完整工程方法论。如果你正准备进入AI工程这个方向或者已经在做模型训练但总觉得“差点工程味”那这篇文章就是给你写的。我会从整体架构设计讲起拆解数据、模型、训练、部署、监控这几个核心环节全部用我实际踩过的坑和验证过的方案来填细节。保证你看完能直接照着搭一套自己的AI工程流水线而不是又收藏一堆“史上最全资料”然后吃灰。2. 整体设计思路为什么“从零开始”比“从框架开始”更靠谱2.1 先搞清楚AI工程到底在解决什么问题很多人以为AI工程就是“用PyTorch训个模型然后调个API”。真去做项目就会知道训练只占整个系统很小的一部分。一个完整的AI工程从需求定义、数据采集、数据清洗、特征工程、模型训练、评估调优、模型打包、服务部署、线上监控到迭代更新跨度极大。而“from-scratch”的含义不是让你从零重写深度学习框架而是让你从零理解每个环节存在的必要性。比如“数据清洗”这一步看起来就是把空值删掉、把格式统一一下。但实际业务里我刚做第一个AI项目时光处理用户上传图片里的EXIF信息不一致问题就花了整整两天。如果当初我直接调用现成的数据处理库而不理解图片数据从采集到进入训练集的全链路后期遇到线上推理乱码时根本无从排查。用生活类比来说AI工程就像开餐厅。模型训练只相当于“后厨炒菜”但你要把餐厅运转起来还得管食材采购数据、菜品标准化特征、厨师培训训练、出餐流程推理服务、顾客反馈监控。只盯着一口锅永远开不了真正的餐厅。2.2 方案选型边做边学别想一口吃成胖子我的核心建议是把一个最小闭环跑通再逐步加深度。所谓最小闭环就是“拿一个小数据集 → 跑通训练脚本 → 导出一个模型文件 → 写一个最简单的HTTP接口 → 发起预测请求”。这个闭环麻雀虽小但每一个环节都会逼迫你去接触真实工程问题数据怎么组织模型怎么保存接口怎么定义返回格式怎么设计我实际推荐的学习路径是第一周只搞Python环境、PyTorch或TensorFlow二选一、CUDA或CPU环境配置新手先用CPU别一上来就上GPU。第二周找一个小数据集比如MNIST、FashionMNIST完成数据加载、训练循环、评估循环三件事。第三周用Flask或FastAPI把训练好的模型包成一个HTTP服务本地用curl或Python requests测试。第四周加入模型版本管理、日志记录、参数配置文件让项目结构像“工程”而不是“脚本”。不要一上来就学Docker、Kubernetes、MLflow、Kubeflow全套。那些工具很强大但它们的复杂度会直接淹没一个还没建立起工程直觉的新手。等你的最小闭环稳定跑通再逐个引入每次只引入一个工具理解它解决了什么痛点。2.3 项目结构设计从一开始就按工程规范来我在带新人时最头疼的一件事就是对方把“训练脚本”和“数据预处理代码”全塞在一个文件里。虽然最小闭环阶段可以这么做但我建议从第一天就按下面的目录结构组织项目ai-engineering-from-scratch/ ├── configs/ # 参数配置YAML或JSON │ └── config.yaml ├── data/ # 数据目录原始数据与处理后数据分开 │ ├── raw/ │ └── processed/ ├── src/ # 核心代码 │ ├── data/ │ │ ├── dataset.py # 数据集加载与预处理 │ │ └── transforms.py │ ├── models/ # 模型定义 │ │ └── model.py │ ├── train.py # 训练入口 │ ├── evaluate.py # 评估入口 │ └── inference.py # 推理封装 ├── scripts/ # 辅助脚本 │ └── run_experiment.sh ├── notebooks/ # 探索性分析不用于核心流程 ├── outputs/ # 训练产物模型、日志、指标 │ ├── checkpoints/ │ └── logs/ └── README.md这个结构最大的好处是每个文件都只有一个明确的职责。当你需要调试数据问题时你只盯src/data/当模型效果差时你只需要调configs/里的参数当线上报错时你直接从outputs/logs/查日志。这比在1000行的“全功能脚本”里大海捞针高效得多。3. 核心环节拆解数据、模型、训练、部署与监控的工程化要点3.1 数据工程的常见陷阱与正确处理方式数据是AI工程最容易出问题、也最不值得省时间的部分。我常说一句话模型只是数据的压缩器垃圾进垃圾出。很多人精确地把80%时间花在调模型结构上却忽略数据里几处隐蔽的问题。第一个陷阱是数据泄漏。我在做一个时间序列预测项目时因为先对全量数据做了标准化再切分训练集和测试集导致测试集信息混入了训练集的均值和方差离线评估指标好看得离谱一到线上就全线崩溃。正确的做法是先用训练集单独拟合标准化参数然后应用到验证集和测试集。对于任何涉及全局统计量的预处理必须遵循“训练集内拟合其他数据集仅transform”的原则。第二个陷阱是类别不平衡处理不当。很多新手看到正负样本比例1:99就直接上手训练结果模型收敛后预测永远输出负样本。常用方案有三种对负样本降采样、对正样本过采样、引入加权损失函数。但这三种方案各有利弊降采样会丢失信息过采样容易过拟合加权损失则要调节权重比例。我的建议是先在日志里记录训练集和验证集的类别分布确认问题严重程度再从简单的class weight方案开始试。第三个陷阱是文本或图片数据的预处理不一致。训练时你做了归一化、做了分词、做了数据增强可部署推理时如果忘了做同样处理线上效果会断崖式下跌。解决方式是把预处理逻辑封装成独立的transform函数或Processor类训练和推理统一调用同一份代码而不是在训练脚本里写一遍、在推理服务里又写一遍。3.2 模型定义的关键不是“复杂”而是“可变”模型定义最忌讳把模型结构写死在代码里。做AI工程时你一定会遇到调整网络宽度、深度、dropout比例、激活函数等情况。建议从一开始就使用配置驱动的方式。比如用PyTorch写一个简单的多层感知机我不建议直接写死# 不推荐结构写死 class MLP(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(784, 256) self.relu nn.ReLU() self.fc2 nn.Linear(256, 128) self.fc3 nn.Linear(128, 10)我会推荐这样设计# 推荐结构由配置决定 class MLP(nn.Module): def __init__(self, input_size, hidden_sizes, num_classes, dropout): super().__init__() layers [] prev_size input_size for hidden_size in hidden_sizes: layers.append(nn.Linear(prev_size, hidden_size)) layers.append(nn.ReLU()) layers.append(nn.Dropout(dropout)) prev_size hidden_size layers.append(nn.Linear(prev_size, num_classes)) self.net nn.Sequential(*layers) def forward(self, x): return self.net(x)这样配置里的hidden_sizes: [256, 128]一改模型结构就跟着变不用动代码。模型注册机制也很重要如果你想在多个模型之间快速切换可以用一个简单的字典把模型名映射到类MODEL_REGISTRY {} def register_model(name): def decorator(cls): MODEL_REGISTRY[name] cls return cls return decorator register_model(mlp) class MLP(nn.Module): ...这样在配置里写model_name: mlp训练脚本就能自动创建对应模型。看起来是小事但当你实验十几个模型时这个机制能救你于水火。3.3 训练流程工程化从跑通到可复现训练脚本是所有环节里最容易变成“一坨屎”的地方。初学者最常见的做法是把数据加载、模型创建、优化器设置、学习率调整、日志打印全部堆进一个main()函数看起来一气呵成但每次换数据集、调参数都要全局修改。工程化的训练流程至少包括四件事配置管理所有超参数学习率、batch size、epoch数、优化器、损失函数放在配置文件中训练脚本只读取配置。推荐使用yamlargparse命令行参数可以覆盖默认配置。日志记录用logging模块记录关键信息用tensorboard记录loss、accuracy等曲线。日志里必须包含当前epoch、当前step、学习率、loss值、数据条数等方便事后排查。模型保存不只是保存最后一版模型还要按epoch保存中间检查点。建议保存model.state_dict()、optimizer.state_dict()、当前epoch、当前最佳指标这样即使训练中断也能准确恢复。随机种子固定在代码开头固定random.seed(42)、numpy.random.seed(42)、torch.manual_seed(42)在数据加载器里也设置generator。否则每次实验结果都不一样你也分不清是调参有效还是运气好。一个标准训练循环的骨架大概是这样的for epoch in range(cfg.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() if batch_idx % cfg.log_interval 0: logger.info(fEpoch {epoch} | Batch {batch_idx} | Loss {loss.item():.4f}) # 每个epoch后跑一次验证 val_loss, val_acc evaluate(model, val_loader, criterion, device) logger.info(fEpoch {epoch} | Val Loss {val_loss:.4f} | Val Acc {val_acc:.4f}) # 保存检查点 if val_loss best_loss: best_loss val_loss torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_loss: best_loss, }, os.path.join(cfg.checkpoint_dir, best_model.pt))这个循环不复杂但足够了。再往上加东西比如混合精度、分布式训练、Early Stopping都是在这个骨架上扩展而已。骨架不清晰任何扩展都会变成灾难。3.4 推理服务化模型只是文件接口才是产品训练出来的模型本质就是一个权重文件要让它产生实际价值必须把它封装成服务。我在工程实践中强烈推荐使用FastAPI因为它在性能、类型校验、自动生成API文档方面都做得很好对新手也友好。一个最基础的推理服务长这样from fastapi import FastAPI from pydantic import BaseModel import torch from src.models.model import create_model app FastAPI() # 启动时加载模型 model create_model(mlp, config) model.load_state_dict(torch.load(outputs/checkpoints/best_model.pt, map_locationcpu)) model.eval() class PredictRequest(BaseModel): features: list[float] class PredictResponse(BaseModel): prediction: int probabilities: list[float] app.post(/predict) def predict(req: PredictRequest): tensor torch.tensor(req.features).unsqueeze(0) with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) pred probs.argmax(dim1).item() return PredictResponse(predictionpred, probabilitiesprobs.squeeze(0).tolist())部署时用uvicorn app:app --host 0.0.0.0 --port 8000启动然后用curl测试curl -X POST http://localhost:8000/predict \ -H Content-Type: application/json \ -d {features: [0.1, 0.2, ...]}这里有几个容易被忽略的点。第一推理时一定用torch.no_grad()否则模型会构建计算图白白增加内存消耗。第二推理模式要调model.eval()这会关闭dropout和batch normalization的随机行为。第三输入数据要做和训练时完全一致的预处理这在前面说过一定要复用同一份transform逻辑。我见过太多线上和线下效果不一致的案例根因就是预处理分叉。3.5 监控与迭代不上线不知道模型有多差模型上了线工程才完成一半。线上监控至少要覆盖两件事系统健康度和模型漂移。系统健康度比较好理解接口延迟、错误率、QPS、CPU/内存占用。这些用Prometheus Grafana一套就能搭起来。但模型漂移需要单独说。所谓漂移就是线上数据的分布和训练时的分布逐渐不一致。比如你训练了一个猫狗分类器上线后用户开始上传“猫狗都有的图片”甚至是“卡通猫”图片模型的表现就会下滑但离线指标看不到。最简单的监控方式是在推理日志里记录预测结果的分布。比如每1000条请求统计一下各类别的预测比例如果和训练集的类别比例出现显著偏差说明数据分布变了。更进一步可以保存线上样本定期做人工标注和模型预测做对比计算“预测漂移指标”。在我的实践经验里监控的价值不是“好看的图表”而是“能让你睡得着觉”。没有监控的AI服务就是裸奔。一旦线上效果异常你连是什么时候开始变差、哪些请求变差都不知道更别提优化了。3.6 工具链的引入节奏别让工具绑架你前面我提到不要一上来就用全套工程工具现在具体说说引入节奏。当你手动管理模型文件、每次都手动复制文件给后端同学时你会自然感到需要模型版本管理这时候再引入MLflow或DVC。当你觉得在服务器上手工配环境太痛苦才引入Docker。当你的服务从1个实例变成3个实例手工更新疲于奔命时才引入Kubernetes。对这种“痛感驱动”的学习方式我深有体会。我最早接触Docker时只是跟着教程“把项目容器化”学完就忘因为我不知道它解决了我什么实际问题。后来自己做部署遇到“在我电脑上跑得好好的一到服务器就报错”的问题才真正记住Docker的价值。工具是解决问题的不是为了写进简历的。4. 实操实录用FashionMNIST从零跑通一个最小AI工程4.1 环境准备与依赖安装为了让你有直观参照我把我自己从零搭的一套工程完整记录下来。我用的是Python 3.10、PyTorch 2.0.1、FastAPI 0.104。如果你用CPU训练这个实验跑完一个epoch也就一分钟完全没压力。创建虚拟环境python -m venv ai-env source ai-env/bin/activate pip install torch torchvision fastapi uvicorn pyyaml tensorboard4.2 准备配置文件和目录结构按照前面的目录结构建好之后写一份最简配置# configs/config.yaml data: dataset_name: FashionMNIST batch_size: 64 num_workers: 2 model: name: mlp input_size: 784 hidden_sizes: [256, 128] num_classes: 10 dropout: 0.2 train: epochs: 5 learning_rate: 0.001 log_interval: 100 seed: 42 outputs: checkpoint_dir: outputs/checkpoints log_dir: outputs/logs这里的input_size: 784对应FashionMNIST每张28x28图片展平后的像素数。hidden_sizes表示两个隐含层的神经元数量。dropout设为0.2目的就是缓解过拟合。4.3 数据加载模块实现在src/data/dataset.py中写数据加载代码注意把预处理逻辑抽离import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms def get_transforms(): # 训练与推理共用的预处理注意顺序 return transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) def get_dataloaders(cfg): transform get_transforms() train_set datasets.FashionMNIST( root./data/raw, trainTrue, downloadTrue, transformtransform ) val_set datasets.FashionMNIST( root./data/raw, trainFalse, downloadTrue, transformtransform ) train_loader DataLoader(train_set, batch_sizecfg.data.batch_size, shuffleTrue) val_loader DataLoader(val_set, batch_sizecfg.data.batch_size, shuffleFalse) return train_loader, val_loader这里有个细节FashionMNIST的downloadTrue会从网上下载数据如果你在内网环境或网络不好可以先手动下载并放置到data/raw/FashionMNIST对应目录。另外使用transforms.Normalize((0.5,), (0.5,))会把像素值从[0,1]映射到[-1,1]这是一种常见标准化方式。请牢记推理时也需要对这一模一样的transform。4.4 训练与评估实现训练代码前面已经给了骨架我补充评估函数def evaluate(model, loader, criterion, device): model.eval() total_loss 0.0 correct 0 total 0 with torch.no_grad(): for inputs, targets in loader: inputs, targets inputs.to(device), targets.to(device) outputs model(inputs) loss criterion(outputs, targets) total_loss loss.item() * inputs.size(0) preds outputs.argmax(dim1) correct (preds targets).sum().item() total inputs.size(0) avg_loss total_loss / total accuracy correct / total return avg_loss, accuracyevaluate里必须写model.eval()和torch.no_grad()。我建议你养成固定习惯凡是评估和推理一律出现在这两个条件里。在主训练脚本中解析配置import yaml import argparse def load_config(): parser argparse.ArgumentParser() parser.add_argument(--config, typestr, defaultconfigs/config.yaml) args parser.parse_args() with open(args.config, r) as f: cfg yaml.safe_load(f) return cfg这样你修改配置时不需要动任何代码。4.5 启动训练并观察指标我跑了一次5个epoch的实验大致指标如下Epoch训练Loss验证Loss验证准确率10.6120.44284.1%20.3960.39285.6%30.3460.36386.8%40.3160.36786.9%50.2950.35187.5%观察训练和验证Loss的变化可以判断模型是否欠拟合或过拟合。这里训练Loss持续下降验证Loss也在下降但幅度趋缓说明模型仍有提升空间。如果验证Loss开始上升而训练Loss继续下降那就典型的过拟合信号需要增加dropout、增加数据增强或减少模型容量。4.6 部署推理服务并验证训练完成后用前面的FastAPI代码搭建服务。为了更贴近实际我建议在服务启动时通过配置文件加载模型而不是硬编码模型名。验证时用以下Python脚本import requests # 从测试集中拿一张图片处理成列表后发送 response requests.post( http://localhost:8000/predict, json{features: sample_features} ) print(response.json())我在部署时遇到的第一个问题就是模型加载路径写错。因为直接torch.load(outputs/checkpoints/best_model.pt)用的是相对路径而从服务启动目录不同会导致路径失效。后来我统一把路径做成配置文件项服务启动时用绝对路径或基于项目根目录的路径解析来加载这才彻底解决。4.7 增加一个“一键训练”脚本为了让整个流程更顺畅我加了一个scripts/run_experiment.sh#!/bin/bash set -e # 出错立即退出 cd $(dirname $0)/.. # 训练模型 python src/train.py --config configs/config.yaml # 评估模型 python src/evaluate.py --config configs/config.yaml --checkpoint outputs/checkpoints/best_model.pt之后加上Docker等工具时只需要在Dockerfile里执行这个脚本整个项目就具备了一键复现的能力。工程化不是口号而是这些“少操一点心”的小细节积累。5. 常见问题与避坑手册我踩过的那些“经典”坑5.1 坑一依赖环境混乱跑一次漏一个包现象换了一台机器训练脚本报ModuleNotFoundError挨个补包补到怀疑人生。原因没有在项目初始化时固定依赖版本也没有导出依赖清单。解决在项目根目录执行pip freeze requirements.txt并把这个文件提交进版本库。更严谨的做法是使用Poetry或conda-lock把依赖树锁到具体版本。我个人推荐先在虚拟环境里开发每安装一个新包都确认它对项目的影响。5.2 坑二实验记录全靠脑子结果连自己都糊弄现象明明一周前跑出来验证准确率89%现在同样的代码却只有85%你却不知道中间改了什么参数。原因没有把每次实验的配置、代码版本、数据集版本、结果指标关联起来。解决从第一个实验起就养成“运行前记录配置、运行后追加指标”的习惯。最简单的做法是在outputs/logs/下按时间戳建目录把本次运行的config.yaml复制过去训练结束后再写一个result.txt记录关键指标。这样哪怕没有MLflow你也能回溯每个实验。5.3 坑三模型文件路径硬编码服务一换目录就崩现象本地写torch.load(models/best.pt)跑得好好的一部署到服务器就FileNotFoundError。原因相对路径依赖当前工作目录而不同启动方式命令行、systemd、Docker的工作目录不一样。解决在配置项里配置模型路径并在代码里基于项目根目录做解析import os PROJECT_ROOT os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def get_path(relative_path): return os.path.join(PROJECT_ROOT, relative_path)5.4 坑四忘记固定随机种子实验无法复现现象连续跑两次相同配置准确率差了两个百分点根本没法判断优化器调整是否有效。原因没有固定Python、NumPy、PyTorch各自的随机种子数据加载器内部的随机打乱也没固定。解决在进入任何随机操作之前执行import random import numpy as np import torch def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False注意torch.backends.cudnn.deterministic True会牺牲一些性能但换来了可复现性。做实验时更该关注确定性等确定最终方案后再去掉也不迟。5.5 坑五推理时忘了预处理线上效果掉成渣现象离线评估准确率87%线上实际体验惨不忍睹用户反馈像瞎猜。原因训练脚本里直接调用了transforms.Normalize但推理服务没写这一步。图片输入变成了未经归一化的原始像素。解决永远把预处理封装成模块级别的函数或类例如src/data/transforms.py里的get_transforms()训练脚本和推理服务都从同一函数导入。我再强调一次这是AI工程里最容易被忽略、影响也最大的低级bug。5.6 坑六线程数与batch size设置不当训练慢吞吞现象训练时数据加载非常慢GPU使用率忽高忽低但调高num_workers后反而报错。原因num_workers并非越大越好它是数据加载的进程数。太小则数据来不及喂给训练循环太大则进程切换开销巨大甚至内存爆炸。解决我的经验是num_workers先设为CPU核心数的一半观察训练吞吐再逐步调整。数据量小时num_workers0主进程加载反而更快因为减少进程通信开销。5.7 坑七单一指标评估误导优化方向现象用准确率评估所有模型遇到极度不平衡的数据集时模型把所有样本都判为多数类准确率竟然高达95%实际毫无可用性。原因准确率对类别不平衡数据不敏感。解决根据业务场景补充其他指标。分类任务可以看Precision、Recall、F1-score、AUC-ROC回归任务看MAE、RMSE。特别是做推荐、风控、医学诊断等场景更应该先定义清楚“错判代价”。6. 从最小闭环走向完整AI平台如果你已经按前面内容跑通了一个最小AI工程接下来可以往三个方向扩展。第一个方向是自动化实验管理。把当前“手动记录实验日志”升级为MLflow。MLflow可以自动追踪每个实验的配置、指标、模型产物并提供统一的模型注册中心做版本对比时非常方便。二是模型服务性能优化。当前只是一个单进程的FastAPI服务面对并发请求容易成为瓶颈。可以学习批推理将多个请求拼成一个大batch、使用ONNX Runtime加速、引入消息队列做异步推理。第三个方向是数据版本管理。当你的数据集频繁更新手动维护“哪份数据对应哪个模型”会崩溃。DVC可以像Git管理代码一样管理数据目录让每次实验的数据状态可追踪。但我想强调的是这三个方向都建立在你的最小闭环足够扎实的基础上。我在带过的新人里有太多人过早追求“平台化”结果光搭建MLflow和Kubernetes就花了两三周真实业务模型却没跑通。工具服务于业务而不是业务服务于工具。7. 把“工程”刻进习惯里到最后我想用我个人经验给你提三个小建议。第一个建议把折腾环境踩的坑记录下来。很多坑你不是第一次踩也不会是最后一次。我自己的“踩坑笔记”现在已经有几百条每次遇到类似问题先查笔记节约大量时间。第二个建议把日志当作你的朋友。刚开始写代码时我总觉得打印日志没有意义对生产环境的成功判断全都依赖“没报错”。后来线上出过一次严重事故我才发现连发生时间和影响范围都无法定位。从那以后每个关键节点必须留日志数据加载完成、训练开始、训练结束、模型保存、请求进来、预测返回、异常捕获。日志不是写给别人看的是危难时刻救你命的。第三个建议每周花一小时重构项目结构。代码写完不代表结束随着实验增多你一定会发现前期设计不合理的地方比如某个模块边界模糊、某些常量和参数混杂。每周抽点时间整理保证你的项目始终是“我能快速看懂别人也能接手”的状态。工程能力的提升其实就是在这些细微的地方一次一次把“能用”变成“好用”。从零开始搞AI工程重点不在于模型分数多高而在于你是否把整套流程用工程手段管住了。数据、代码、模型、服务、监控每一个环节都在考验你的全局视野。这个ai-engineering-from-scratch的项目本质上就是帮你把这些环节一个一个打通。跑通一个最小闭环再逐步加东西你会发现自己已经能独立支撑起一个AI项目的全生命周期了。