尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AI工程化实战:从零搭建完整机器学习流水线的关键与坑

AI工程化实战:从零搭建完整机器学习流水线的关键与坑 1. 为什么值得从零搭建一套AI工程链路先说说我自己的经历。我做了几年后端开发转岗到AI工程方向之后接到的第一个正经任务不是训练某个模型而是把别人在Notebook里跑通的算法变成一条能上线、能迭代的工程链路。当时我面前有两条路一条是直接上托管的ML平台把数据、训练、部署全都交给平台省事另一条是从裸机开始自己从零搭一套完整的AI工程流水线。我选了后者也就是后来置顶在仓库里的那个项目ai-engineering-from-scratch。现在回头看这个选择不算轻松但带来的收益远远超出预期。这篇文章就把这条从零搭建的路完整复盘一遍聊聊每个环节为什么要这么设计以及藏在里面的坑。如果你正准备在自己的项目里落地AI能力或者想系统理解AI工程化到底包含哪些事这篇文章应该能帮你省下不少试错时间。1.1 托管平台解决不了的两类问题先给托管平台说句公道话它确实能解决快速跑通的问题。数据上传、自动调参、一键部署这些能力在有现成平台的情况下是实打实的效率提升。但实际用在生产环境里我碰到了两个托管平台很难绕开的障碍。第一个是控制力缺失。模型训练涉及大量的超参数、数据切分方式、随机种子、硬件配置这些在平台里往往被封装成了黑盒。平台想做的是标准化但AI工程恰恰是一个需要大量实验和调优的领域。当同一个数据集的精度波动超过预期时你根本不知道是哪一层出了问题——是平台的数据预处理版本变了还是底层CUDA库被替换了还是调度器把任务分到了不同的节点。这种排查在自建链路里是几个小时能定位的事在平台里可能要提工单等反馈。第二个是数据闭环难打通。AI系统上线之后最重要的不是初始精度而是能不能持续从线上反馈中学习。托管平台通常只负责训练部署这一段线上的请求日志、推理结果、人工反馈这些数据要想回流到训练集里往往需要额外的ETL管道对接。自建链路的好处是从数据采集到训练再到部署整条链路都是自己的代码反馈数据回流只是一个管道配置的问题不会卡在平台边界上。1.2 从零搭建才看得清的工程真相还有一个更虚但更重要的理由只有从零搭一遍你才能真正理解AI工程化里的工程二字指的是什么。拿数据版本管理来说。第一次搭建时我天真地以为数据存在数据库里就够了。直到有次重训模型发现效果比上一版退了两个点查了两天才发现是某张表的某个字段在两周前被上游任务改了生成逻辑而模型训练代码根本不知道这件事。从此我养成了一个习惯任何训练任务必须记录数据集的文件哈希、行数、特征分布快照否则这个训练结果就是不可复现的。这种东西不踩坑是学不会的。再比如实验追踪。头两个月我靠文件夹目录和文件名记录实验结果就是出现了一堆model_final_v3_真的最终版.pth这种命名的文件。改成MLflow之后每次实验的参数、指标、产物自动关联再也不用靠文件名脑补当时改了什么东西。这些都是从零搭建过程中被现实教育出来的经验。下面用一个表格对比一下自建链路和托管平台的实际差别维度自建链路托管平台环境可控性完全可控每个依赖都可以锁定受平台版本策略约束数据闭环全链路自主设计反馈回流灵活依赖平台的数据管道能力成本人力成本高硬件资源自管理使用成本透明启动快可复现性需要自己建立版本规范平台自带部分能力适合场景深度定制、长期迭代、数据敏感快速验证、标准化流程、入门学习我的结论是对于刚接触AI工程的人来说先在托管平台上跑通闭环再自建一套精简链路是最舒服的学习路径。但如果你的目标是把AI工程作为核心竞争力亲手从零搭一遍是绕不开的一课。2. AI工程化全景一条链路的六个关键环节在我把整条链路搭完之前我对AI工程化的理解是模糊的以为就是把模型训练代码写得干净一点再包个接口。真正动手之后才发现一个可用的AI系统远不止这些。它至少包含六个环节每个环节都有自己的目标和产物而且环环相扣。2.1 六个环节各自要回答的问题一条完整的AI工程链路我用六个环节来划分业务定义这个AI系统要解决什么问题成功标准是什么是离线批量分析还是在线实时推理这个环节输出的是一份可量化的指标定义而不是一句提高用户体验这种空话。数据工程原始数据从哪来质量如何保证特征怎么加工训练集和验证集怎么划分才能不走漏数据这个环节决定了整个模型效果的上限。模型实验选什么模型结构用什么损失函数数据预处理和增强怎么做这个环节需要一套高效的实验追踪机制否则就是一团乱麻。训练工程训练多长时间用什么样的学习率策略怎么混合精度来加速显存不够怎么办模型产物怎么保存和注册服务化部署模型怎么从训练产物变成线上接口推理服务怎么处理并发怎么控制延迟怎么平滑上线和回滚监控迭代线上效果怎么度量数据漂移怎么发现反馈数据怎么回流到数据集里形成闭环你可以看到这六个环节里只有中间的两个是大多数机器学习教程会覆盖的其余四个恰恰是工程化的重点。从零搭建的意义就是把这六个环节全部打通而不是只做其中一段。2.2 分层设计与依赖方向的铁律这六个环节在代码层面怎么组织我踩过不少坑。一开始我把所有代码按功能模块堆放结果就是数据管道的代码和训练代码耦合在一起改一个特征要动用三处代码训练脚本里还残留着部署逻辑的痕迹。后来我采用了严格的分层设计参考了后端开发里经典的依赖倒置思想数据层只负责产生标准格式的数据集不感知模型结构模型层只定义模型结构和损失函数不关心数据从哪个文件来训练层负责把数据层的数据喂给模型层完成训练循环和产物保存服务层只加载训练层输出的产物对外提供推理接口。这里最关键的设计约束是依赖方向只能自上而下服务层可以依赖训练层的产物格式训练层可以依赖数据层的输出格式但反过来绝对不行。你永远不会在数据层里看到因为模型需要某格式所以改数据处理的代码。需要这种调整的时候应该去模型层适配而不是倒回去破坏数据层的稳定性。2.3 版本可追溯的工程底线最后一条贯穿所有环节的原则是版本可追溯。代码有版本数据有版本模型有版本三者之间的对应关系也必须能查询。这句话听起来像废话但真正做到位并不容易。我现在的做法是给每一条训练任务生成一个全局ID然后在这个ID下记录三样东西代码的Git commit号、数据集的哈希和行数、以及实验管理平台里对应的Run ID。这样任何一个线上模型出问题我都可以从产物反查到它是由哪一份代码、哪一份数据训练出来的然后决定是回滚代码还是补数据重训。这套机制在项目初期会显得繁琐但一旦线上出过事故你就能体会到它有多值钱。我见过不止一个团队线上模型出了问题后连这个模型是用什么数据训的都答不上来最后只能全部重训。这种代价远比一开始建立版本规范要大得多。3. 环境搭建的三个劝退点根因、排查与固化从零搭AI工程链路第一个劝退点不是代码是环境。这里说的环境不光是pip install而是从硬件驱动到Python依赖再到容器镜像的一整套东西。我在这块花掉的时间不比其他环节少而且绝大多数是重复试错。下面这三个点是我觉得最值得写清楚的。3.1 GPU驱动与CUDA版本的隐形依赖很多第一次做AI项目的人都会在CUDA上卡住。症状很典型torch.cuda.is_available()返回False或者装完PyTorch之后一跑就报CUDA error: no kernel image is available for execution on the device。这个问题的根源是版本矩阵不匹配NVIDIA驱动、CUDA运行时、PyTorch的CUDA编译版本三者必须匹配到兼容的区间。驱动版本太老新的CUDA运行时根本跑不起来PyTorch的wheel编译时用的是什么CUDA版本运行时就会去加载对应的CUDA库和系统里装的不一致就会报错。我的建议是别按最新版本来装而是按驱动版本倒推。先跑nvidia-smi看驱动支持的CUDA版本号然后去PyTorch官网查对应版本的安装命令。比如驱动支持CUDA 12.1那就装torch对应的cu121版本而不是无脑装cu124。这一个小习惯可以帮你躲掉一大半的环境问题。另外一个容易被忽略的点是CUDA环境变量。有时你明明装了匹配的CUDA但程序加载的是另一个路径的版本。排查时先用nvcc --version和python -c import torch; print(torch.version.cuda)对比两边版本是不是一致不一致就在~/.bashrc里把CUDA_HOME和LD_LIBRARY_PATH明确指到同一个路径上。3.2 Python虚拟环境与依赖锁Python依赖在AI项目里是出了名的混乱尤其是深度学习框架的依赖树很深随便一个包更新就能让环境在几个月后装出来的东西跟当初完全不一样。为了杜绝这种问题我从一开始就强制规定所有实验和部署环境必须用独立的Python虚拟环境。具体做法上我用venv作为基础配合pip-tools做依赖管理。requirements.in里只写顶层依赖比如torch、transformers、mlflow这些然后通过pip-compile生成带所有传递依赖精确版本的requirements.txt。这样每次环境重建时装出来的依赖树是完全一致的。这里有个细节要区分训练环境和部署环境。训练环境里可能有notebook、matplotlib、jupyter这些调试用的包部署环境里根本不需要。把它们混在一个requirements.txt里会导致镜像体积膨胀并增加安全风险。我现在的做法是拆成requirements-train.txt和requirements-deploy.txt两个文件训练镜像怎么都好说部署镜像严格控制最小依赖。3.3 用Docker镜像固化环境即代码虚拟环境解决了本机的一致性问题但解决不了换台机器就不一样的问题。把环境固化成Docker镜像是我认为从零搭建链路里性价比最高的一步。我不会直接拿官方的pytorch/pytorch镜像就用而是自己写一版Dockerfile在官方镜像之上做最小修改创建非root用户、设置工作目录、安装项目依赖、复制项目代码。基础镜像只锁一个大版本比如pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime然后再通过requirements-deploy.txt锁死Python依赖。这样两层锁定环境基本不会漂移。在搭建期间我吃过一个闷亏有次训练结果异常排查了半天发现是有人在服务器上手动升级了一个传递依赖的包导致数值结果出现微小的偏差。从那之后我的原则是所有环境的变更都要通过镜像构建来发生禁止在运行中的容器里手动pip install。要改环境就改Dockerfile重新构建旧镜像留着随时可以回滚。4. 训练流水线让实验快、准、可复现环境就绪之后真正进入AI工程的核心地带训练流水线。这条流水线的目标不是训练出一个好模型这么简单而是让训练过程可控、结果可复现、效率可提升。如果只是追求模型精度那在Notebook里也能跑但工程化要做的是把同样的事情变成一条稳定运行的自动化流程。4.1 数据层标准化输入堵住脏数据数据层是整个流水线的地基地基不稳后面所有环节都是空中楼阁。我在第一批实践中就经历过训练集和验证集混入重复样本、特征分布偏移这些经典问题所以现在对数据层有三个硬性要求。第一个是统一的Dataset接口。不管原始数据是CSV、数据库还是对象存储都给训练层暴露一个统一的torch.utils.data.Dataset接口输出标准化的样本字典。这样训练代码完全不感知数据来源后续换数据源只需要重写一个Dataset类。第二个是保证数据切分的隔离性。涉及时间序列数据时绝不能随机切分而是按时间戳切分防止未来信息泄漏。涉及用户ID场景时要按用户维度切分确保同一个用户的数据全部在训练集或全部在验证集。我见过有人随机切分后模型离线指标漂亮得不真实上线直接崩掉原因就是同一个用户的行为数据同时出现在训练集和验证集里模型等于把答案背下来了。第三个是数据校验。在训练启动前跑一组断言检查数据的基本约束字段是否缺失、类别是否完整、标签分布是否合理。早期我懒得加这些检查直到有一次上游字段类型从整数悄悄变成了字符串模型训练直接崩溃。后来我在数据层加了一个validate_dataset()函数每次训练前自动跑几分钟就能发现异常。4.2 训练循环从朴素实现到工程化的演进一个标准的PyTorch训练循环写起来很短但工程化要关心的远不止forward和backward。以实际经验来看有四个点值得重点处理。混合精度训练。在不牺牲精度的情况下显存占用和训练速度都能得到明显改善。使用torch.cuda.amp可以做到半精度前向和反传同时对损失缩放。对于大部分常规模型这块的收益是立竿见影的尤其是显卡显存紧张的时候混合精度往往就是能跑和跑不了的区别。梯度累积。当批量大小受显存限制而实验又想用更大的有效批量时梯度累积是常用手段。实现上就是累积若干步的梯度后再做一次优化器更新。但要注意配合学习率调度器和BatchNorm的行为差异不然反而会把训练搞坏。学习率调度。训练里面轮数固定到100就完事是我认为最常见的训练陋习。工程化的做法是设定早停策略加动态调整学习率后者通常采用Cosine退火或ReduceLROnPlateau。模型训练到后期loss平台期的时候调小学习率往往还能再突破一个量级的精度。可复现性锚点。每次训练开始时把随机种子、数据哈希、配置参数统统记录下来。PyTorch里设置torch.manual_seed只能管住PyTorch自己的随机源DataLoader里的worker_init_fn也要做相应设置否则多进程数据加载的随机性依然无法复现。分享一段我沉淀下来的训练启动代码骨架把上面几个点串起来import mlflow import torch from torch.cuda.amp import GradScaler, autocast def run_training(cfg, train_loader, val_loader, model): # 配置追踪 with mlflow.start_run(run_namecfg.run_name): mlflow.log_params(cfg.__dict__) optimizer torch.optim.AdamW(model.parameters(), lrcfg.lr) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxcfg.epochs) scaler GradScaler(enabledcfg.amp) for epoch in range(cfg.epochs): model.train() for step, batch in enumerate(train_loader): with autocast(enabledcfg.amp): loss model.compute_loss(batch) scaler.scale(loss).backward() # 梯度累积 if (step 1) % cfg.grad_accum_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad() scheduler.step() val_loss evaluate(model, val_loader) mlflow.log_metric(val_loss, val_loss, stepepoch) mlflow.pytorch.log_model(model, model)这个骨架看起来不复杂但它的价值在于把实验管理、精度加速、训练稳定性集中到了一段可复用的流程里而不是每次实验都重新写一遍。4.3 实验追踪与模型注册告别目录文件式管理前两个月我靠目录管理实验吃过不少苦头。第一次意识到必须改变是有一天在服务器上看到三个名字分别是best_model.pth、best_model_v2.pth、best_model_final.pth的文件每个大小都差不多但完全想不起来分别是用什么参数训的。之后我就引入MLflow作为实验追踪和模型注册的统一入口。每次训练开始记录数据和代码版本每个epoch记录指标训练结束把模型产物注册到Model Registry。这样任何一个注册过的模型都能追溯到它的训练配置、数据版本和代码commit。这个环节看起来是多一步操作但在模型数量超过十个以后它能省下的时间是你想象不到的。有一个细节容易被忽略注册模型时必须附带模型输入输出的Schema。训练时顺手把输入特征的字段名、类型、示例样本都记录下来部署时就不需要去翻代码猜测输入格式了。这个习惯在你写出第一个端到端项目时会显得特别有用。5. 部署从训练产物到线上服务的最后一公里模型训练好只是起点真正的考验在于部署。这一节我重点讲三件事模型格式选型、推理服务封装、以及上线策略。每一件都是从实际生产里总结出来的不是教科书上的标准答案。5.1 模型导出为什么不能直接保存整个训练模型很多初学者会把整个模型对象用torch.save(model.state_dict())保存然后部署时再加载回来。在小规模原型验证时这样做没问题但到了生产环节问题就来了部署环境未必有训练时的源码结构。如果你的模型定义用了自定义类或动态图特性部署端加载时一旦类找不到整个服务就起不来了。更好的选择是把模型导出为与代码解耦的格式。PyTorch生态里常用的是TorchScript或ONNX。TorchScript可以把模型结构和权重打包成一个独立的文件部署端只需要torch.jit.load即可不依赖训练时的模型定义代码。ONNX则更进一步换来的是跨框架、跨语言的互操作性。我的建议是优先考虑ONNX。原因很简单在线推理服务一般不用Python而用更轻量或者更高效的语言栈而ONNX Runtime在CPU和GPU上都能提供很好的优化即使是在纯Python环境ONNX Runtime也比直接跑PyTorch模型省内存。实际上已经上线的经验是同一模型从PyTorch动态图切换到ONNX Runtime后推理延迟下降了20%-40%具体数字取决于模型复杂度。模型导出时要注意PyTorch的trace是追踪式的在模型有控制流分支或动态shape时容易出问题。稳妥的做法是导出前先用真实样本跑一遍确认输出一致。5.2 推理服务封装延迟、并发和资源隔离部署一个推理服务最关键的是把模型的输入输出映射成对外API协议。一个模型往往有复杂的预处理过程这些预处理放在客户端还是放在服务端是设计上必须明确的决策。我的原则是所有预处理逻辑都应该在服务端完成客户端只上传原始请求这样任何客户端调用的行为都是一致的不会出现Python客户端能跑通但Go客户端结果不对这类诡异问题。服务端我通常用FastAPI来实现因为它的异步支持对IO密集的推理场景非常友好。整体结构大概是接收请求 - 解析参数 - 调用预处理 - 模型推理 - 后处理 - 返回结果。模型实例放在内存里只加载一次所有请求共享不能每次请求都重新加载模型否则延迟会高出好几个数量级。并发控制也是部署中容易踩坑的点。GPU推理和CPU推理的特性完全不同。GPU喜欢大batch但单请求延迟会因此被batch里最慢的请求拖累CPU推理虽然单次慢但天然可以利用多进程并行。我的经验是给推理服务加一个简单的动态批处理机制短时间内到达的多个请求合并成一个batch喂给模型同时设定最大等待时间避免请求饿死。这个机制能显著提升GPU利用率。5.3 上线策略灰度、回滚和流量切换算法工程师最容易忽视的问题是上线风险。一个模型在离线指标上表现再好也不能保证线上效果一定好。所以我在部署流程里引入了三个机制灰度发布、版本回滚、流量切分。灰度发布不复杂就是在网关层面把一定比例的流量指向新模型服务。最开始5%接着20%如果监控指标正常再逐步放大。这里最关键的是一开始就要想好回滚机制——一旦发现新版本指标下降立刻把流量切回旧版本。流量切分在实现上可以用网关的权重路由或者简单的特征开关。权重路由对用户来说是无感的两个模型都是可服务的状态切流量只是调整权重。这一点在做AB实验时尤其有用。6. 上线之后模型测试、可观测性与迭代闭环模型上线不是终点而是后半场的开始。很多从零开始的AI项目都倒在了这一步模型上线时效果不错但运行一段时间后效果越来越差没人知道为什么也缺少改进的抓手。这一节说说我在监控和迭代上沉淀出来的做法。6.1 模型测试不止是正确性传统软件工程中的单元测试和集成测试在AI项目里同样适用但程度和侧重点要调整。测试对象不只是代码逻辑正确还包括模型行为符合预期。我通常把模型测试分成三层。第一层是输入输出Schema测试确保推理服务对合法输入返回结构正确的响应对非法输入返回友好的错误信息。这层测试在模型更新和代码重构时非常有价值能防止接口向前不兼容。第二层是鲁棒性测试输入轻微扰动比如文本里加点错别字、数值特征加一点噪声看模型输出是否发生不合理的剧烈变化。鲁棒性差的模型上线后很容易被线上噪声数据打崩溃这个测试能提前暴露问题。第三层是影子评估也就是把新模型的推理输出和当前线上模型的输出做比对。离线指标提升不一定代表线上表现更好影子评估是新模型上线前我必做的一步。6.2 可观测性从延迟指标到数据漂移检测推理服务的可观测性比普通Web服务要宽。常规的延迟、QPS、错误率当然要有但AI场景还要多盯几个独特指标。模型漂移是其中最重要的一项。线上数据的分布不是一成不变的用户行为会变、环境会变、数据源也会变。当输入分布和训练分布差异过大时再好的模型也会失效。我通常的做法是按固定时间窗口比如每天记录线上请求的特征分布和训练时的分布做对比用KL散度或PSI来量化偏移程度。超过阈值就告警让工程师决定是否需要介入。推理置信度分布也很值得看。对大多数分类模型来说softmax输出可以近似看成置信度。线上置信度整体下降往往预告着模型对当前数据不确定了这是数据漂移的早期信号比特征分布统计更灵敏。可观测性的落地我用的是Prometheus加Grafana这套组合。推理服务把指标暴露给Prometheus采集Grafana做Dashboard和告警规则。模型漂移这类离线指标则通过定时任务算好后写入Prometheus或专门的存储统一在Grafana里展示。6.3 迭代闭环反馈数据回流与重训机制我见过最多的AI项目停滞模式就是模型上线后没人管直到几个月后业务方反馈效果变差了才想起来看一下。根本原因是缺少一个迭代闭环线上产生的数据没有回流机制也就没有人有动力持续关注模型效果。在自建链路里这个闭环做起来其实很快。推理服务把每一次请求的输入、输出、用户反馈都落到日志存储里定时任务每天把这批日志清洗、标注、合并到训练数据集中形成新版本的数据集。当监控指标触发重训条件时自动化流水线拉起新一轮训练评估通过后就部署上线。这个闭环跑起来之后整个AI系统才算真正成为活的系统——数据驱动模型模型反过来服务数据周而复始地变好。从零搭建这套工程链路花掉我不少个周末但回头看收获很大。比起直接用平台拖拽出来的结果亲手搭出来的链路让我对每一个环节都有了底层的理解。经验就一条一步到位的完美方案不存在先把闭环跑起来再逐步把每一环做扎实这比一开始就追求所有细节都完善要有效得多。
返回列表