先说一个很多人会忽略的事实:AI工程,真正难的不是“写模型”那一下,而是把模型从想法变成稳定、可维护、能落地的系统。我见过太多人,论文读了、网课刷了,一到自己动手搭项目就卡住——要么环境装三天,要么训练跑不起来,要么模型在Notebook里好好的,一到线上就崩。这个《ai-engineering-from-scratch》要解决的问题,就是把这条链路上那些没人系统性讲清楚的东西串起来:从环境怎么搭、数据怎么处理,到模型怎么训练、怎么调优,再到怎么部署上线、怎么监控维护,全流程走一遍。
这篇文章,就是按“从零开始做AI工程”的路线,把我自己踩过的坑、验证过的方案、以及一些可以“拿来即用”的参数配置和代码模板,整理成一份实操手册。适合刚入门想完整跑通一个AI项目的学生、想转行AI工程岗的开发者,以及已经在做算法但总觉得工程化能力跟不上的朋友。我会尽量用直白的语言讲清楚每一步“为什么这么做”,而不是只丢给你一堆命令和代码。
1. 整体设计思路:为什么“从零开始”比“从模型开始”更有价值
1.1 核心认知:AI工程项目到底在做什么
很多人以为AI工程就是训练模型,其实那是研究岗的事。工程岗的日常,更像一个“翻译官”加“包工头”:把业务需求翻译成技术方案,再把算法模型变成生产环境里稳定运行的服务。一个完整的AI工程项目,拆开来看大概是这几个环节:
- 需求分析与可行性评估:能用的数据有没有,模型精度要到什么程度,响应时间要求多快。
- 数据工程:采集、清洗、标注、增强、版本管理,这个环节通常占掉60%以上的时间。
- 模型开发:选基座、训练、调参、评估,这步反而是标准化的,有成熟的框架和工具。
- 部署上线:把训练好的模型封装成API服务,做性能优化,处理并发和容灾。
- 监控与迭代:模型上线只是开始,日常要盯数据分布漂移、精度下降、资源水位。
基于这个认知,我建议零基础的路径不是先去啃《深度学习》教材,而是先建立一个“最小完整闭环”:用现成的数据集和预训练模型,做一遍从数据到部署的全流程。跑通一次之后再往回深入细节,效率会高很多。这也是我们整个系列的学习策略。
1.2 技术栈选型:少即是多,稳比新重要
选技术栈有个原则:能用成熟的,不追最新。在AI工程领域,稳定的生态和足够的资料远比酷炫的新特性重要。我的推荐配置如下:
| 模块 | 首选方案 | 备选方案 | 选择理由 |
|---|---|---|---|
| 语言 | Python 3.10+ | 无 | 生态最全,AI领域事实标准 |
| 深度学习框架 | PyTorch 2.x | TensorFlow | 动态图调试方便,社区活跃,部署生态完善 |
| 数据处理 | Pandas + NumPy | Polars | 资料多、文档全,够用且稳定 |
| 训练加速 | 单张NVIDIA显卡(RTX 3060 12G起步) | 云GPU(AutoDL等) | 12G显存能跑大部分开源模型的微调 |
| API服务 | FastAPI | Flask | 自带异步和文档,性能好,上手也快 |
| 容器化 | Docker + Docker Compose | 无 | 环境隔离的标配,团队协作必备 |
| 监控 | Prometheus + Grafana | 无 | 开源监控组合拳,生态成熟 |
这里有一个重要的心态调整:不要一上来就搭Kubernetes集群、搞分布式训练、用Ray做编排。这些是在业务量确实到了那个地步才需要的,初期只会增加认知负担。我见过有人为了一个演示项目,先花了两周搭平台,结果模型还没跑通。先做减法,把核心链路跑通,再按需扩展。
1.3 项目目标设定:如何定义“跑通”和“完成”
“从零开始”最怕的是没有终点,学完这篇学那篇,始终停留在“看”的阶段。给自己定义一个具体的交付目标很重要。我建议第一个项目的验收标准是:
- 一个能用的模型:精度不必刷到SOTA,但要在验证集上达到一个合理基线。
- 一个稳定的服务:别人通过HTTP请求就能调用你的模型,连续跑一周不崩溃。
- 一份清晰的文档:别人按照你的文档,能从零复现整个流程。
比如,第一个项目可以用公开的MNIST手写数字识别走向上点难度,用CIFAR-10或者一个领域的小型数据集,目标就是做一个图像分类服务。模型用现成的ResNet18,微调一两个epoch,后端用FastAPI包一层,前端做一个简单的上传图片就能看到分类结果的页面。别小看这个入门项目,它可以覆盖数据、训练、部署、接口设计、前端交互的全链路,为后续做更复杂的项目打基础。
2. 环境搭建:Python、CUDA和PyTorch的版本迷宫
2.1 搭建步骤:从裸机到能跑训练的最小配置
环境搭建是第一个劝退点,尤其是显卡驱动和CUDA版本不匹配的问题。我现在的操作方式,基本上能一次到位:
第一步,安装Python。建议直接装Miniconda,用它来管理Python版本和虚拟环境,避免把系统Python搞坏。
# 下载Miniconda后执行安装脚本,一路默认即可 bash Miniconda3-latest-Linux-x86_64.sh # 创建一个独立的虚拟环境,避免不同项目依赖冲突 conda create -n ai-engineering python=3.10 conda activate ai-engineering第二步,安装CUDA相关的组件。这里要注意一个点:PyTorch官方安装命令里带的CUDA版本,其实是一个运行时库,不要求你系统里装完整的CUDA Toolkit。也就是说,你只需要保证显卡驱动够新,然后按PyTorch官网给出的命令安装即可,不用自己手动去装CUDA。
# 以cu118(CUDA 11.8)为例,根据自己显卡驱动选择合适的版本 pip install torch==2.1.2 torchvision==0.16.2 torchaudio==2.1.2 --index-url https://download.pytorch.org/whl/cu118第三步,安装基础库。
pip install numpy pandas matplotlib scikit-learn jupyter fastapi uvicorn docker-compose第四步,验证环境。这一步很重要,很多人装完就以为万事大吉,结果一跑就崩。
import torch print(torch.__version__) print(torch.cuda.is_available()) # 测试一下张量能否在GPU上运行 x = torch.rand(3, 3).cuda() print(x)如果输出都是正常的,恭喜你,环境这一关过了。这里必须说明一下,以上是描述一种通用的安装路径,实际上只要保证PyTorch、CUDA运行库和显卡驱动三者兼容就行,不一定要完全照搬命令。
2.2 版本兼容矩阵:一张表说清怎么选
环境问题90%出在版本匹配上。这里有一张我整理好的兼容参考,覆盖了常见的搭配:
| 显卡驱动 | CUDA(系统级) | PyTorch对应版本 | 备注 |
|---|---|---|---|
| 470+ | 11.4 | torch 1.12~1.13 | 老版本,不建议新项目用 |
| 510+ | 11.8 | torch 2.0~2.1 (cu118) | 我目前在用的稳定组合 |
| 525+ | 12.0 | torch 2.1+ (cu121) | 新卡推荐 |
| 535+ | 12.4 | torch 2.4+ (cu124) | 需要更新版本的PyTorch |
选配置的核心思路是:先确定显卡驱动支持的最高CUDA版本,然后倒推选哪个PyTorch版本。驱动怎么查?Linux下执行nvidia-smi,在右上角能看到“CUDA Version: xx.x”,这个数值只表示驱动支持的上限,实际用不用得上看PyTorch。
注意:我之前用过一个技巧,就是压根不装系统级CUDA Toolkit,直接用PyTorch自带的运行时。这样环境中不会出现多个CUDA版本互相干扰的问题,出错了也容易排查。
2.3 避坑清单:我在环境搭建上踩过的雷
环境这块的坑,我基本都踩遍了,总结下来就这几类:
第一,torch.cuda.is_available()返回False但显卡驱动正常。九成原因是装了CPU版本的PyTorch。查一下pip list | grep torch,确认版本号里有没有+cu后缀。如果没有,卸载重装GPU版。
第二,conda源和pip源混用导致依赖混乱。我遇到过conda把numpy升到了2.x,然后一堆库不兼容的情况。建议是:要么坚持用conda,要么坚持用pip,不要混着装。默认情况下我推荐pip,因为PyTorch的wheel包在pip上更新最快。
第三,在Windows上开发、Linux上部署导致路径和依赖不一致。最好的办法是一开始就用Docker,开发环境和部署环境统一。刚开始学习可以不用,但到了项目后期一定要容器化。
3. 数据工程:决定模型上限的关键环节
3.1 数据准备的质量控制:脏数据比你想象的更可怕
很多新手拿到数据就开始训练,结果模型精度上不去,还以为是网络或参数的问题,其实问题出在数据上。我见过一个项目,训练集和验证集有大量重复图片,导致模型在验证集上表现很好,一到真实场景就崩。这就是典型的数据泄漏。
数据质量控制有三个基本检查项,也是我拿到数据后必做的清洗操作:
- 重复检查:用哈希或特征比对挑出重复样本,尤其是跨训练集和验证集的重复。如果发现,一定要去重。
- 标签噪音:随机抽几百条样本人工核对标签,统计错误率。如果错误率超过5%,建议先清洗数据再训练,否则模型学到的就是错误映射。
- 分布检查:用直方图或者t-SNE看看训练集和测试集的分布是否一致。如果差异很大,模型泛化能力会大打折扣。
清洗工具上,Pandas做规则清洗、OpenCV做图像格式统一化、pandas_profiling做数据报告。养成一个习惯:训练前,先跑一遍数据报告,快速发现异常值、缺失值分布和类别不均衡情况。
3.2 数据增强:小数据集也能训练出泛化能力强的模型
数据量不够,那就想办法“造数据”。数据增强不是瞎搞,要遵循一个原则:增强方式不能改变样本的语义。比如做猫狗分类,对图片做水平翻转、小幅旋转、裁剪、颜色抖动都是合理的,但你如果把一张狗的照片通过拉伸变成猫的比例,就会干扰模型学习。
PyTorch的torchvision.transforms或者albumentations库都能做增强。我后来更喜欢albumentations,因为速度快、效果好。举个例子:
import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform = A.Compose([ A.Resize(256, 256), A.RandomCrop(224, 224), A.HorizontalFlip(p=0.5), A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=15, p=0.5), A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1, p=0.5), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2(), ])多提一嘴,增强参数不要一上来就拉满。我记得自己有一次为了压过拟合,把旋转角度设成了45度,结果模型把图片方向当作重要特征来学,泛化能力不升反降。增强策略应该是“适度提高样本多样性”,而不是“把样本变成另一个东西”。两个建议:给每类样本设置不同的增强强度;在训练后期逐步降低增强强度,帮模型收敛。
3.3 数据管道设计:把数据准备做成自动化的流水线
训练时最怕的就是手动改数据。规范的做法是搭一条数据管道,让“原始数据”自动变成“模型可训练的输入”。我习惯用PyTorch的Dataset和DataLoader来组织数据流程:
class ImageDataset(Dataset): def __init__(self, image_paths, labels, transform=None): self.image_paths = image_paths self.labels = labels self.transform = transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image = cv2.imread(self.image_paths[idx]) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) if self.transform: image = self.transform(image=image)["image"] label = self.labels[idx] return image, label注意,一整个项目的数据流程,从这里开始就要有“数据版本管理”的意识。哪怕是最笨的办法——每次清洗数据后备份一个标注了时间的副本,都比没有强。等到项目迭代几次,你就知道数据版本管理有多重要了。
4. 模型训练与调优:从“能跑”到“跑得好”
4.1 模型选型:别从头写网络,也别什么都用Transformer
零基础起步,我强烈建议不要自己设计网络结构,直接用成年的开源模型做迁移学习。计算机视觉任务从ResNet、EfficientNet、ConvNeXt里选;自然语言处理任务从BERT、RoBERTa、DeBERTa里选。选型的依据很简单:
- 任务类型:图像分类用CNN系,文本理解用Transformer系,目标检测用YOLO系或DETR系。
- 数据规模:小数据(万张以内)用ResNet这种经典CNN,大数据(百万级)用ViT或者更大规模的预训练模型。
- 推理速度要求:实时性要求高,就选轻量级模型,比如MobileNet、SqueezeNet,或者做模型蒸馏,用大模型教小模型。
核心道理是:预训练模型已经在大规模数据集上学到了通用特征,你只需要让模型适应你的特定数据分布,这个适应过程叫“微调”。微调的做法一般有两种:一是冻结大部分层,只训练最后几层分类头,适合数据量很小的场景;二是全量微调,所有参数都参与训练,适合数据量比较大且和预训练数据分布差异较大的场景。
4.2 训练参数配置:一份能直接用的推荐设置
给出几个我验证过比较稳的训练参数组合,用的是ResNet18在CIFAR-10这种小数据集上的微调场景,作为基线参考:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Optimizer | AdamW | 比Adam收敛更稳,weight decay效果更好 |
| Learning Rate | 1e-4 | 微调用1e-4稳妥,从头训练可以用1e-3 |
| Batch Size | 64(12G显存) | 显存不够就减到32,配合梯度累积 |
| Scheduler | CosineAnnealingLR | 训练后期学习率衰减,收敛更平滑 |
| Epochs | 30 | 不设太大,配合早停策略 |
| Weight Decay | 1e-4 | 正则化,抑制过拟合 |
| Label Smoothing | 0.1 | 减少模型过于自信,提升泛化 |
训练代码遵循一个固定套路,我直接贴一个训练循环核心片段:
model.train() for batch_idx, (inputs, labels) in enumerate(train_loader): inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() if batch_idx % 50 == 0: print(f"Epoch {epoch} | Batch {batch_idx} | Loss {loss.item():.4f}")这里面有个小细节,一定要养成optimizer.zero_grad()的习惯,在loss.backward()之前清零梯度,否则梯度会累加。
4.3 训练监控:让训练过程“有数可看”
训练不是把代码丢进去干等,一定要监控。至少盯住下面几条曲线:
- 训练损失
train_loss:一个正常下降的趋势,如果震荡大,考虑调低学习率或增大batch size。 - 验证损失
val_loss:如果先降后升,和train_loss拉开,就是过拟合的典型信号。 - 学习率变化:用cosine scheduler时,会是一个平滑衰减的曲线。
- GPU利用率:用
nvidia-smi或者gpustat看,正常应该在70%以上。如果低了,检查dataloader是不是瓶颈,num_workers和pin_memory都调上。
可视化工具用TensorBoard或者Weights & Biases。个人更推荐W&B,信息量更全,还能云端同步,团队协作时很有用。如果用不惯W&B,也可以本地开TensorBoard,关键是要有记录习惯。
4.4 过拟合与欠拟合的应对手段
先给一个判断方法:
- 训练损失和验证损失都很高:欠拟合,模型容量不够或训练不充分。加大模型、加训练数据、增加训练轮次。
- 训练损失低、验证损失高:过拟合,模型记住了训练集细节。加正则(Dropout、Weight Decay)、数据增强、早停、减少模型容量。
我这边的经验是,正则手段里的“早停”容易被忽视。用下面这个套路,能帮关键时候省下大量时间:
best_val_loss = float("inf") patience = 7 counter = 0 for epoch in range(epochs): train_loss = train_one_epoch(...) val_loss = validate(...) if val_loss < best_val_loss: best_val_loss = val_loss counter = 0 torch.save(model.state_dict(), "best_model.pt") else: counter += 1 if counter >= patience: print("Early stopping!") break另外,如果遇到单卡显存不够,我一般先调batch_size减半,再加gradient_accumulation:
# 模拟更大batch size:每4步做一次梯度更新 accumulation_steps = 4 ... loss = loss / accumulation_steps loss.backward() if (step + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()5. 模型部署与上线:把模型变成一个真正的“服务”
5.1 模型导出与格式选型:不止是PyTorch的抗体
训练完的模型是.pt格式,但要让它在服务端高效运行,通常要转成特定格式。这一步很多人容易忽略,但正式部署时比训练还重要。
在主流的工程实践中,选择取决于推理环境和性能要求:
| 格式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| PyTorch (.pt) | 快速原型、测试环境 | 简单,无需转换 | 部署依赖PyTorch运行库,体积大、速度一般 |
| ONNX | 跨平台、跨语言部署 | 生态广,支持多框架导出 | 对某些自定义算子支持不好 |
| TensorRT | NVIDIA GPU上的高吞吐低延迟推理 | 性能极强,能融合算子 | 只支持NVIDIA卡,转换时间长 |
| TorchScript | PyTorch生态内的部署 | 和PyTorch结合紧密 | 灵活性一般,逐渐被其他格式取代 |
如果是做小型项目、内网工具、快速交付,直接用FastAPI加载PyTorch模型就够了。如果是生产环境的高并发场景,就要考虑ONNX配合ONNX Runtime,或者TensorRT的优化方案。
5.2 API服务实现:用FastAPI包一层模型的“壳”
起一个FastAPI服务,把模型封装成一个POST接口,这样任何语言、任何平台都能调用。
from fastapi import FastAPI, UploadFile, File from PIL import Image import io import torch import torchvision.transforms as transforms app = FastAPI() device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") model = torch.load("best_model.pt", map_location=device) model.eval() transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) @app.post("/predict") async def predict(file: UploadFile = File(...)): image_data = await file.read() image = Image.open(io.BytesIO(image_data)).convert("RGB") input_tensor = transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(input_tensor) probs = torch.softmax(outputs, dim=1) pred_class = torch.argmax(probs, dim=1).item() conf = probs[0, pred_class].item() return {"class_id": pred_class, "confidence": conf} # 启动命令:uvicorn main:app --host 0.0.0.0 --port 8000写这个接口有几个注意点:
- 必须以
model.eval()模式推理,把Dropout、BN层切到预测状态,这很关键。 - 用
with torch.no_grad()包裹推理代码,省显存省时间。 - 接口的输入输出要做异常处理,比如文件不是图片、图片格式损坏,要有明确的报错信息。
5.3 容器化与上线流程:让服务在任何机器上都能跑
模型服务化之后,下一步就是容器化。用Docker打包,保证开发环境和生产环境一致。
FROM python:3.10-slim WORKDIR /app RUN pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu RUN pip install fastapi uvicorn pillow COPY main.py . COPY best_model.pt . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]这里有个有意思的优化点。推理服务器不一定需要GPU版本PyTorch,如果模型不大,CPU推理足够了,那Docker基础镜像直接用CPU版的PyTorch,可以把镜像体积从3GB降到1GB左右。GPU部署则要加运行时参数,比如用nvidia-container-toolkit。
Docker环境部署完成后,挂上docker-compose做服务编排,把模型服务、Redis、日志服务等组件串起来,这就是一个小而完整的线上架构了。
6. 常见问题、排查思路与避坑技巧
6.1 训练阶段高频问题的排查清单
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 损失值完全不变 | 学习率太低、梯度消失、数据打乱顺序太固定 | 先用lr=1e-3试跑100步观察曲线,检查梯度范数,确认label有没有问题 |
| 损失变成NaN | 学习率太高、输入数据有NaN、反向传播某些操作数值溢出 | 降低学习率,检查数据清洗流程,模型的前向输出加clip操作 |
| 验证集精度远高于测试集 | 数据泄漏、模型早停时机不对、预处理不一致 | 检查train和val是否交叉重叠,确认验证集和测试集用的是同一条预处理管道 |
| 训练很慢,GPU利用率20% | DataLoader瓶颈、CPU预处理耗时、模型太小 | 调大num_workers,设置pin_memory=True,预处理流程放到GPU上 |
| 显存溢出Out of Memory | Batch size太大、输入图片尺寸太大、模型参数太多 | 调低batch size,开启梯度累积,或者用混合精度torch.cuda.amp |
混合精度这块我再多说一句,现在主流显卡都支持自动混合精度,既能提速又能省显存。在PyTorch里用起来很简单:
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() ... with autocast(): outputs = model(inputs) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()6.2 部署阶段常见问题的排查清单
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 接口返回500且日志无输出 | 模型路径错误、依赖库缺失 | 先以简单的/health接口测试容器是否正常运行,再逐层排查 |
| 模型预测结果和训练时不一致 | 预处理逻辑不一致、模型状态不是eval模式 | 把输入图片打印出来,逐像素对比训练时的预处理;确认model.eval() |
| 并发一高,请求超时 | 模型推理太慢、线程池不够 | 换ONNX或TensorRT优化推理,上异步处理队列,横向扩容 |
| Docker镜像过大,构建很慢 | 基镜像选太重,没做分层缓存 | 用slim镜像,安装包用多阶段构建,模型文件用外部挂载方式引入 |
6.3 从零开始最值得养成的3个工作习惯
第一个习惯是“永远留一个能跑的版本”。我见过不少次,代码改一版就崩一版,最后连原来的基线都没了。养成每次改动前先记录或者commit的习惯,最好连数据和模型一起做好版本管理。
第二个习惯是“记录每一次实验”。用W&B或者一个简单的Excel表格,把每次实验的模型结构、参数、数据集、精度、显存占用记下来。这能让你少走很多弯路,而且跟人沟通时有据可依。
第三个习惯是“先做端到端的最小闭环,再迭代优化”。不管功能多复杂,先拿最简单的模型、最少的数据,把整条链路打通,再逐步增加复杂度。
这点是我带项目时反复强调的:AI工程不是一条道走到黑,而是不断绕开坑、不断小步快跑的过程。很多最后成了的项目,初期看起来都“简陋得不像话”,但胜在链路是通的、数据是准的、迭代是快的。
最后再分享一个我个人的小技巧:每一步都亲自动手做一遍,哪怕照着别人的代码抄一遍再改,都比读完十篇文章管用。这个《ai-engineering-from-scratch》能带你走完一条路,但真正能让你把AI工程变成自己手艺的,还是你亲手敲下去的每一行代码,和踩过每一个坑之后的复盘。