尧图精选

从零搭建AI工程能力:手写训练循环与推理服务实战

🕒 发布时间:2026/10/1 3:57:02 📁 来源:尧图网络
1. 从零搭建AI工程能力为什么我劝你别一上来就调包这两年AI应用开发的门槛肉眼可见地在降低随便拉个框架、调个API就能跑出一个能对话的Demo。但我带过不少新人也面试过不少号称“做过AI项目”的候选人发现一个很普遍的问题模型能跑起来但一问到“为什么这么设计”“数据怎么流转”“推理延迟卡在哪”基本就答不上来了。这就是典型的“会调包不懂工程”。ai-engineering-from-scratch这个项目标题核心讲的其实就是一件事抛开那些封装好的高级框架从最底层的环节开始把AI工程能力一块一块搭起来。它适合谁适合那些已经会用Python、了解一点机器学习概念但总觉得自己的项目“浮在表面”的开发者也适合想从传统后端、数据分析转AI工程方向的朋友。这篇文章我会把整个从零搭建的思路、关键环节、实操细节和踩坑经验全部摊开讲你跟着走一遍至少能搞清楚一个AI系统从数据到上线到底经历了什么。我先把核心关键词摆出来AI工程、从零搭建、数据处理、模型训练、推理服务、工程化落地。这几个词会贯穿全文也是这个项目最核心的价值所在。2. 整体设计思路为什么我要把“调包”这条路先堵死2.1 先搞清楚“AI工程”和“调包”的本质区别很多人把AI工程等同于“用现成框架跑模型”这其实是个很大的误解。调包解决的是“快速验证想法”的问题而AI工程解决的是“让想法稳定、高效、可维护地跑在生产环境”的问题。这两者的差距就像你在家煮碗面和开一家面馆的区别——前者只要面能熟就行后者要考虑出餐速度、食材损耗、口味一致性、高峰期排队、卫生标准等等。ai-engineering-from-scratch这个项目的设计初衷就是强迫你走一遍“开面馆”的全流程。具体来说它要求你从原始数据开始自己写数据清洗逻辑、自己实现特征工程、自己搭训练循环、自己做模型评估、自己写推理服务、自己做性能监控。每一步都不允许直接调高级API必须理解底层在干什么。我举个例子你就明白了。假设你要做一个文本分类任务调包的做法是from transformers import pipeline然后三行代码搞定。但从零搭建的做法是你得先搞清楚文本怎么转成token、token怎么映射成向量、向量怎么经过注意力机制、注意力输出怎么接分类头、损失函数怎么选、梯度怎么回传、学习率怎么调度。这一套走下来你对模型的理解会完全不一样。2.2 技术选型的核心考量可控性优先于便利性在这个项目的技术选型上我的原则是可控性优先于便利性。什么意思就是宁可多写点代码也要保证每个环节都是透明的、可调试的、可替换的。具体到工具层面我的建议是这样的环节推荐方案为什么不直接用高级封装数据处理原生Python NumPy/Pandas高级框架的数据管道黑盒太多出问题难排查模型定义PyTorch原生张量操作不用高层API强制理解每一层的输入输出形状训练循环手写训练循环理解梯度累积、混合精度、学习率调度的实际作用推理服务FastAPI 原生模型加载避免推理框架的隐式优化掩盖性能瓶颈监控自己打日志 Prometheus搞清楚到底该监控哪些指标这个选型逻辑背后的思考是你只有先知道“手动挡”怎么开才能理解“自动挡”什么时候会出问题。我见过太多人用高级框架跑通了Demo结果一上生产就各种报错连错误信息都看不懂就是因为中间层数太多了。2.3 项目模块的拆解逻辑按数据流向来组织整个项目我建议按数据流向拆成五个核心模块而不是按技术栈来拆。为什么因为AI工程本质上是一个数据加工流水线按数据流向组织你能清楚地看到每一步的输入输出是什么出了问题该去哪个环节找。这五个模块分别是数据采集与清洗模块负责把原始数据变成干净可用的结构化数据特征工程与数据集构建模块负责把清洗后的数据转成模型能吃的格式模型定义与训练模块负责定义网络结构、损失函数、优化器并完成训练评估与调优模块负责用合理的指标评估模型并做超参数调优推理服务与监控模块负责把训练好的模型部署成可调用的服务并监控运行状态每个模块之间通过明确的接口通信比如数据模块输出的是标准格式的Dataset对象训练模块只依赖这个接口不关心数据具体怎么来的。这样做的好处是你可以单独替换任何一个模块而不影响其他部分。提示不要一上来就追求“全流程自动化”。先把每个模块单独跑通手动串联起来确认每一步的输出都符合预期再考虑用脚本或工作流引擎把它们串起来。我见过太多人一开始就搞复杂的编排结果某个环节出问题排查了一整天。3. 核心细节解析数据、模型、服务三个关键环节的实操要点3.1 数据清洗别急着上模型先把数据里的“脏东西”清干净数据清洗是整个项目里最枯燥但最重要的环节。我个人的经验是一个AI项目80%的时间花在数据上而数据清洗又占了其中的60%。很多人模型效果不好第一反应是调参、换模型其实问题往往出在数据里。从零搭建的数据清洗流程我建议按这个顺序来第一步数据探查。拿到原始数据后先别急着写清洗代码用Pandas做一轮基础探查。看什么看每个字段的类型、缺失率、唯一值数量、分布情况。我通常会写一个explore_data函数输出一份数据质量报告包括import pandas as pd def explore_data(df): report pd.DataFrame({ dtype: df.dtypes, missing_rate: df.isnull().mean(), nunique: df.nunique(), sample_values: [df[col].dropna().head(3).tolist() for col in df.columns] }) return report这份报告能帮你快速定位问题字段。比如某个字段缺失率超过80%那基本可以考虑直接丢掉某个字段唯一值只有一个那说明它没有区分度也可以丢掉。第二步缺失值处理。缺失值处理没有万能方案得看字段含义。数值型字段可以用均值、中位数填充但要注意如果缺失不是随机的填充反而会引入偏差。类别型字段可以填“未知”或众数。我的原则是能删就删不能删再填填的时候一定要记录填充逻辑方便后续追溯。第三步异常值处理。异常值不一定是错误数据有可能是真实但罕见的样本。我通常用IQR方法先标记出异常值然后人工抽查一部分判断是保留还是剔除。对于文本数据异常值可能是超长文本、乱码、特殊字符这些需要单独处理。第四步去重与标准化。重复数据会严重影响模型训练尤其是如果重复样本集中在某个类别会导致模型过拟合。标准化包括统一大小写、去除首尾空格、统一日期格式、统一单位等。注意数据清洗的每一步都要保留原始数据的副本并且记录清洗前后的样本数量变化。我踩过的坑是清洗完发现样本少了一半但不知道是哪一步删多了只能从头再来。3.2 特征工程把“原材料”加工成模型能消化的“营养餐”特征工程的核心目标是把原始数据转换成模型容易学习的数值向量。这一步做得好不好直接决定模型效果的上限。我经常跟团队里的人说模型选型决定效果的下限特征工程决定效果的上限。对于不同类型的特征处理方式完全不同数值型特征最常见的是做标准化Z-Score或归一化Min-Max。标准化适合分布接近正态的数据归一化适合有明确边界的数据。如果数据分布偏斜严重可以先做对数变换。我通常会写一个NumericalFeatureProcessor类把拟合和转换分开避免数据泄露。class NumericalFeatureProcessor: def __init__(self, methodstandard): self.method method self.mean_ None self.std_ None self.min_ None self.max_ None def fit(self, X): if self.method standard: self.mean_ X.mean(axis0) self.std_ X.std(axis0) elif self.method minmax: self.min_ X.min(axis0) self.max_ X.max(axis0) return self def transform(self, X): if self.method standard: return (X - self.mean_) / (self.std_ 1e-8) elif self.method minmax: return (X - self.min_) / (self.max_ - self.min_ 1e-8)类别型特征低基数的类别比如性别、省份可以用One-Hot编码高基数的类别比如用户ID、商品ID建议用Target Encoding或Embedding。One-Hot编码要注意维度爆炸问题如果类别超过50个就要考虑其他方案了。文本特征从零搭建的话我建议先实现一个简单的词袋模型或TF-IDF理解文本向量化的基本原理。然后再过渡到词嵌入Word2Vec、GloVe最后再考虑预训练语言模型。这个渐进过程能帮你理解为什么后来的模型效果更好。时间特征时间字段可以拆成年、月、日、星期、小时、是否节假日等。对于时序预测任务还需要构造滞后特征lag features和滑动窗口统计量。3.3 模型定义与训练手写训练循环到底能学到什么手写训练循环是ai-engineering-from-scratch这个项目最核心的环节之一。很多人用model.fit()用惯了根本不知道训练过程中发生了什么。手写一遍之后你会对以下概念有完全不同的理解梯度累积当显存不够时可以通过累积多个小批次的梯度再更新一次参数等效于增大批次大小。手写循环时你需要自己控制optimizer.zero_grad()的调用时机。混合精度训练用torch.cuda.amp可以加速训练并减少显存占用但需要自己管理scaler的缩放和反缩放。学习率调度什么时候用余弦退火、什么时候用阶梯下降、什么时候用warmup这些策略的效果差异很大手写一遍你才能直观感受到。早停与模型保存根据验证集指标决定什么时候停止训练并保存最佳模型。这个逻辑看似简单但实际写的时候要考虑指标是越大越好还是越小越好、 patience设多少合适、保存时要不要保存优化器状态等。我通常会写一个Trainer类来封装这些逻辑class Trainer: def __init__(self, model, optimizer, scheduler, criterion, device): self.model model self.optimizer optimizer self.scheduler scheduler self.criterion criterion self.device device self.best_metric float(inf) self.patience_counter 0 def train_epoch(self, dataloader): self.model.train() total_loss 0 for batch in dataloader: inputs, targets batch inputs, targets inputs.to(self.device), targets.to(self.device) self.optimizer.zero_grad() outputs self.model(inputs) loss self.criterion(outputs, targets) loss.backward() self.optimizer.step() total_loss loss.item() return total_loss / len(dataloader) def validate(self, dataloader): self.model.eval() total_loss 0 with torch.no_grad(): for batch in dataloader: inputs, targets batch inputs, targets inputs.to(self.device), targets.to(self.device) outputs self.model(inputs) loss self.criterion(outputs, targets) total_loss loss.item() return total_loss / len(dataloader)实操心得手写训练循环时一定要在第一个epoch结束后打印出损失值确认损失在下降。如果损失不降反升大概率是学习率太大或者数据标签有问题。我遇到过好几次损失爆炸的情况最后发现是数据里混入了NaN值。4. 实操过程从零到一搭建一个完整的AI工程流水线4.1 环境准备与项目结构初始化动手之前先把环境搭好。我建议用conda创建一个独立环境避免依赖冲突conda create -n ai-eng python3.10 conda activate ai-eng pip install torch numpy pandas scikit-learn fastapi uvicorn prometheus-client项目结构我习惯这样组织ai-engineering-from-scratch/ ├── data/ │ ├── raw/ # 原始数据 │ ├── processed/ # 清洗后的数据 │ └── features/ # 特征工程后的数据 ├── src/ │ ├── data/ # 数据处理模块 │ ├── features/ # 特征工程模块 │ ├── models/ # 模型定义 │ ├── training/ # 训练逻辑 │ ├── evaluation/ # 评估逻辑 │ └── serving/ # 推理服务 ├── configs/ # 配置文件 ├── notebooks/ # 探索性分析 ├── tests/ # 单元测试 └── requirements.txt这个结构的好处是职责清晰每个模块独立可测。我特别建议把configs单独拿出来因为AI项目有大量超参数硬编码在代码里后期维护会很痛苦。4.2 数据管道的搭建与调试数据管道我建议分成三个阶段extract、transform、load。每个阶段单独写函数方便调试和复用。def extract_data(source_path): 从原始数据源读取数据 df pd.read_csv(source_path) print(f原始数据形状: {df.shape}) return df def transform_data(df): 清洗和转换数据 # 1. 去除完全重复的行 df df.drop_duplicates() # 2. 处理缺失值 for col in df.columns: if df[col].dtype in [float64, int64]: df[col] df[col].fillna(df[col].median()) else: df[col] df[col].fillna(unknown) # 3. 去除异常值以数值列为例 numeric_cols df.select_dtypes(include[float64, int64]).columns for col in numeric_cols: q1, q3 df[col].quantile([0.25, 0.75]) iqr q3 - q1 lower, upper q1 - 1.5 * iqr, q3 1.5 * iqr df df[(df[col] lower) (df[col] upper)] print(f清洗后数据形状: {df.shape}) return df def load_data(df, output_path): 保存处理后的数据 df.to_parquet(output_path, indexFalse) print(f数据已保存至: {output_path})调试数据管道时我习惯在每个阶段结束后打印数据形状和关键统计量。这样一旦发现异常能快速定位是哪个阶段出的问题。4.3 模型训练与验证的完整流程训练流程我建议分成四个步骤数据加载、模型初始化、训练循环、模型保存。数据加载用PyTorch的Dataset和DataLoader注意shuffle只在训练集开启验证集和测试集不要打乱。from torch.utils.data import Dataset, DataLoader class CustomDataset(Dataset): def __init__(self, features, labels): self.features torch.FloatTensor(features) self.labels torch.LongTensor(labels) def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.features[idx], self.labels[idx] train_dataset CustomDataset(X_train, y_train) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue)模型初始化权重初始化很重要不好的初始化会导致梯度消失或爆炸。我通常用Xavier初始化或Kaiming初始化。训练循环前面已经给了Trainer类的框架这里补充一下学习率调度的用法from torch.optim.lr_scheduler import CosineAnnealingLR optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler CosineAnnealingLR(optimizer, T_max50)模型保存我习惯保存两个文件一个是模型权重一个是训练配置。这样后续复现时不会因为忘了超参数而抓瞎。4.4 推理服务的封装与性能优化推理服务用FastAPI封装核心是把模型加载和请求处理分开from fastapi import FastAPI import torch app FastAPI() model None app.on_event(startup) def load_model(): global model model torch.load(model.pth, map_locationcpu) model.eval() app.post(/predict) def predict(request: dict): features torch.FloatTensor(request[features]) with torch.no_grad(): output model(features) return {prediction: output.argmax().item()}性能优化方面我建议关注三个点批处理把多个请求合并成一个批次推理、缓存对重复请求缓存结果、异步用async def处理IO密集型操作。实测下来批处理对吞吐量的提升最明显通常能提升3到5倍。注意推理服务上线前一定要做压力测试用locust或wrk模拟并发请求观察响应时间和错误率。我遇到过模型在本地跑得好好的一上线就超时最后发现是批处理逻辑在低并发时反而增加了延迟。5. 常见问题与排查技巧实录5.1 训练过程中的典型问题与解决方案训练过程中最常见的问题我整理成了下面这张表方便你快速排查问题现象可能原因排查方法解决方案损失不下降学习率太小、数据标签错误、模型太简单检查标签分布、打印梯度范数调大学习率、检查数据、增加模型容量损失震荡严重学习率太大、批次太小观察损失曲线调小学习率、增大批次验证集损失上升过拟合对比训练集和验证集指标加正则化、Dropout、早停梯度为NaN学习率太大、数据有NaN打印每层梯度调小学习率、检查数据显存不足批次太大、模型太大查看显存占用减小批次、用梯度累积、混合精度我重点说一下“损失不下降”这个问题。很多人第一反应是调学习率但其实更常见的原因是数据标签有问题。比如标签和特征错位了、标签编码错了、数据里混入了大量噪声。我的排查顺序是先看数据再看模型最后看超参数。5.2 推理服务的性能瓶颈排查推理服务的性能问题通常出在三个地方模型加载、请求处理、资源竞争。模型加载慢如果模型文件很大加载时间可能达到几十秒。解决方案是用torch.jit或onnx做模型序列化加载速度能提升好几倍。请求处理慢单条推理如果超过100ms就要考虑优化了。优化方向包括减小模型、量化、剪枝、用更高效的推理后端。资源竞争多个请求同时到达时如果模型不支持并发就会排队。解决方案是用批处理或者启动多个worker进程。我踩过的一个坑是推理服务用了全局锁导致所有请求串行处理QPS低得可怜。后来改成批处理加异步QPS直接翻了10倍。5.3 数据泄露与评估偏差的隐蔽陷阱数据泄露是AI工程里最隐蔽的坑之一。什么叫数据泄露就是训练时用到了预测时拿不到的信息。比如标准化时用了全量数据的均值和方差而不是只用训练集的特征工程时用了未来信息比如用明天的数据预测今天数据划分时没有按时间划分导致训练集和测试集有重叠这些问题的共同特点是离线评估指标很好上线后效果一塌糊涂。排查方法是仔细检查每一步的数据处理逻辑确认没有用到测试集的信息。我通常会在代码里加一个检查确保训练集和测试集的索引没有交集。评估偏差的另一个来源是评估指标选择不当。比如类别不平衡时用准确率评估会高估模型效果。这时候应该用F1、AUC等指标。我建议在评估模块里同时输出多个指标综合判断。6. 工程化落地的经验总结与扩展方向6.1 从脚本到服务代码组织与配置管理从零搭建的项目一开始往往是几个脚本拼起来的。但要真正落地必须做工程化改造。我的经验是分三步走第一步模块化。把数据处理、特征工程、模型训练、推理服务拆成独立的模块每个模块有明确的输入输出接口。第二步配置化。把所有超参数、路径、开关都抽到配置文件里用YAML或JSON管理。这样切换环境时不用改代码。第三步自动化。用Makefile或Shell脚本把常用命令封装起来比如make train、make serve、make test。减少手动操作降低出错概率。配置管理我推荐用hydra或pydantic前者适合复杂配置后者适合配置校验。我自己的项目里用pydantic定义配置类启动时自动校验避免配置写错导致运行时才报错。6.2 监控与日志上线后怎么知道模型有没有“变坏”模型上线不是终点而是起点。上线后你需要持续监控模型的运行状态和效果。我建议监控三类指标系统指标CPU、内存、GPU利用率、请求延迟、QPS、错误率。这些指标能告诉你服务是否健康。模型指标预测分布、置信度分布、特征分布。这些指标能告诉你模型是否遇到了数据漂移。业务指标点击率、转化率、用户反馈。这些指标能告诉你模型是否真的产生了价值。日志方面我建议用结构化日志JSON格式方便后续用ELK或Loki做聚合分析。每条日志至少包含时间戳、请求ID、输入摘要、输出摘要、耗时、是否出错。实操心得监控告警的阈值不要设得太敏感否则会被大量误报淹没。我通常先观察一周的正常波动范围再根据P99值设定告警阈值。6.3 后续可以继续深挖的几个方向这个项目跑通之后你可以往几个方向继续深挖方向一模型压缩与加速。学习量化、剪枝、知识蒸馏把模型做小做快适合部署到边缘设备。方向二分布式训练。当数据量和模型规模上来之后单卡训练不够用了需要学习数据并行、模型并行、流水线并行。方向三AutoML。把特征工程、模型选择、超参数调优自动化减少人工干预。方向四MLOps。把训练、评估、部署、监控串成完整的流水线实现持续训练和持续部署。我个人觉得从零搭建最大的价值不是让你成为某个框架的专家而是让你建立起对AI系统的全局认知。你知道每个环节在干什么、为什么这么干、出了问题该去哪里找。这种能力比会调几个API要值钱得多。最后分享一个我自己的习惯每做完一个项目我都会写一份“复盘文档”记录哪些地方走了弯路、哪些参数是拍脑袋定的、哪些代码是临时凑合的。这份文档在下一个项目里往往能帮我省下大量时间。AI工程这条路踩坑是常态但踩过的坑要变成经验而不是重复踩。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →