ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI工程化从零到一:环境搭建、数据治理到模型部署的全链路实践

AI工程化从零到一:环境搭建、数据治理到模型部署的全链路实践 先说一句大实话AI工程化跟训练模型是两码事。很多人以为会用PyTorch调个参、能跑通一份开源代码就算懂AI工程了。等我把第一个稍微像样的模型推进生产环境时才意识到差距有多大。那一年我几乎是靠踩坑攒经验从环境搭建一路踩到模型上线踩通了这条从零开始的路。这篇文章就是把这套过程里最有用的东西沉淀下来给准备动手做AI工程、或者正在被别人说“你这也叫工程化”的人一份能直接参考的路线图。我梳理了六个阶段每个阶段都有我当时踩过的坑、最终留下来的方案以及为什么这样选而不是那样选的逻辑。全文偏实践适合刚入门的AI工程师、后端想转AI的开发者还有那些模型已经能跑、但不知道怎么把它变成产品的团队。1. 先拆掉一个误区工程化不等于训练出模型我刚入行时在团队里做过一个耐用品表面缺陷检测的项目模型在测试集上准确率98.5%当时觉得自己老厉害了。结果一部署就翻车线上处理一张图片要2.8秒并发一高直接超时检测成功率掉到74%。后来复盘才明白我所谓“AI能力”只覆盖了全流程里的一小段真正的工程化是让模型在真实环境里稳定、高效、可维护地跑起来。1.1 工程师和炼丹师的分水岭模型训练解决问题是“我能不能做到”AI工程化解决的是“在特定成本、延迟、可靠性约束下能不能持续做到”。我见过太多项目死在半路不是因为模型精度不够而是死在下面这些问题上训练环境没问题生产环境一换CUDA版本模型直接跑不起来。标注数据散落在每个人的电脑里没有版本、没有标注规范训练集不可复现。模型效果全靠“拍脑袋加正则”调出来没有可量化的评估标准换一个阈值就全乱套。上线靠手工拷贝文件没有灰度、没有回滚、没有监控出了问题只能全员上线救火。换句话说训练模型是科学实验AI工程是软件工程、数据工程和机器学习交叉的那部分。你可以在Kaggle比赛里拿第一名却不一定会把一个千分之一的坏样本过滤模块做好。这篇博文讲的就是后者——让模型作为系统的一部分可靠运转。1.2 我最终沉淀下来的AI工程全链路我经过这几个项目之后把AI工程拆成六个环节后面章节就按这个链路来写环节核心问题常见失败原因环境与依赖代码能跑环境复现未锁定版本未容器化数据规范数据可信版本可溯标注混乱无校验模型选择能力与成本匹配盲目上大模型忽略baseline评估体系效果可量化可回归只看单一指标没有case分析部署推理延迟、吞吐、稳定性未做推理优化并发测试缺失监控迭代线上效果可观测无数据回流无漂移检测这六个环节写出来很轻巧每一步做扎实都费功夫。拿我自己经历来说环境复现一项就折腾了快一周后面每一步都沉淀了不少工具、命令和代码。下面逐一细说。2. 环境搭建我为什么从裸pip切换到Poetry加Docker双锁定第一节说的环境问题是我到新团队后首先撞上的坑。当时同事给了我一个满是print日志的训练脚本外加一份requirements.txt里面全是numpy1.20这种放养式写法。我在自己机器上装依赖装了一个下午不是这个冲突就是那个版本不对最后能跑了结果一跑loss函数又报错。2.1 裸pip的真实现状Python环境混乱是工程化的第一只拦路虎。pip install默认装到全局多项目一多版本冲突几乎是必然。我知道有virtualenv但真正麻烦的是即便用了虚拟环境如果requirements.txt里写的是1.20这样的范围约束下次安装解析出来的版本很可能和上次不是同一个锁等于没锁。而且训练环境还有一个特殊矛盾GPU依赖。PyTorch或者TensorFlow对CUDA版本有强绑定你装完torch后系统一升级CUDA模型推理可能直接行为异常。这个坑看起来小排查起来真的想砸电脑。2.2 锁版本锁到依赖树的方案后来我统一改成这样用Poetry管理依赖和虚拟环境锁定所有顶层依赖和传递依赖的具体版本用uv配合锁文件提升依赖解析速度训练和推理都用Docker封装Dockerfile里再固化CUDA基础镜像版本。pyproject.toml里我习惯这么写[tool.poetry.dependencies] python 3.10,3.12 torch 2.1.2 transformers 4.36.2 numpy 1.26.3这还不够Poetry真正厉害的poetry.lock它会把每一次依赖解析的结果完整保存。团队的另一个同学拉下仓库后我要求他执行poetry install --no-root如果他想update某个包禁止直接改版本号必须走poetry add packageversion这个流程确保锁文件同步更新。这样队伍里任何一个人安装出来的环境都一致。2.3 镜像里的隐雷基础镜像版本不对等于白锁锁完依赖还没完。有一回我们模型在开发机精度正常推到Kubernetes集群上变乱码查了一个多小时发现是基础镜像里的libgomp版本太老并发加载OpenMP算子时出了问题。这事以后我对Docker镜像的版本固定变得病态敏感。FROM nvidia/cuda:12.2.0-cudnn8-runtime-ubuntu22.04 RUN apt-get update apt-get install -y --no-install-recommends \ python3.11 python3-pip \ rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --frombuilder /root/.cache/poetry /root/.cache/poetry COPY pyproject.toml poetry.lock ./ RUN pip install poetry poetry config virtualenvs.create false poetry install --no-dev COPY src ./src这里面的关键点是基础镜像标签里的12.2.0-cudnn8-runtime-ubuntu22.04完整复制了CUDA和CUDNN的大版本而poetry.lock锁住了Python依赖。两层一起固定才能保证从开发机到生产线的环境几乎无差异。经验上我还想提醒一点别在Dockerfile里装完整版CUDA toolkitruntime镜像就够了。很多模型跑不起来是因为装了新版本CUDA改变了运行时目录结构反而破坏了原镜像的配套关系。3. 数据链路从一堆标注文件到可审计的训练集环境问题解决后接下来最影响模型天花板的是数据。我做过几个项目后发现多数做AI的人对数据管理的态度很随意Excel表格存标签、微信传来传去、训练前临时写个脚本清一下。这样出来的模型就是炼丹练出来什么全看运气。3.1 数据治理的四个基本动作我后来在团队里推了一套数据规范核心是这四件事原始数据只读不改原文件标注结果有版本每个训练集版本对应明确的标注规范文档数据进入训练集前经过schema校验和统计校验异常数据直接拦截每条样本可追溯能从训练集batch反查回原始图片和标注提交人。这四点做完以后数据问题就从“全凭口口相传”变成了系统可控的状态。3.2 Schema校验不想让脏数据悄悄流进训练集就卡这关以我们缺陷检测项目为例标注格式是COCO JSON跑出来的结果。所谓schema校验就是检查每个标注文件是不是符合我们预先定义的字段结构、类型和取值范围。from pydantic import BaseModel, Field, validator from typing import List class Annotation(BaseModel): image_id: int bbox: List[float] Field(..., min_items4, max_items4) category_id: int Field(..., ge0, le5) area: float Field(..., gt0) validator(bbox) def _check_bbox(cls, v): if v[2] 0 or v[3] 0: raise ValueError(bbox宽高必须大于0) return v class COCODataset(BaseModel): images: List[dict] annotations: List[Annotation] categories: List[dict]你可能会觉得“这有什么难的”但现实中我见过太多bbox坐标出现负数、category_id超出范围、甚至图片路径指向不存在文件的情况。这些如果不在入口卡掉训练时轻则loss异常重则让模型学到错误特征而且你还很难发现。3.3 用DVC给数据加版本和代码一起流转说了校验再说版本。我用DVC管理数据文件做法是把数据集目录纳入DVC跟踪数据本身存到S3或本地MinIO仓库里只保留描述文件。dvc remote add myremote s3://my-ai-bucket/dvc-store dvc add data/raw_images dvc add data/annotations/train.json dvc push这以后每次数据发生变化dvc.lock会记录对应的文件哈希和历史版本。我把DVC版本和Git commit对齐训练脚本里直接指定dvc checkout 版本。这样整个训练过程你用的原始数据、标注数据、代码版本就是完全一致的三元组。有一次线上模型判定出问题我拿训练集的DVC版本反查发现某个批次标注的类别标准和交付标准不一致。如果没有这层版本回溯这种问题基本没法定位最后只能重新训练。4. 模型选型与训练策略先让baseline跑起来再谈精调模型选型是整个AI工程化里最容易上头的一环。刚接触大模型的时候大家都想用最前沿、参数最大的模型好像这样才显得有水平。但工程化的做事方式不是这样。4.1 无脑上大模型之前先问自己四个问题项目启动时我习惯逼自己回答这四个问题延迟要求是多少100毫秒还是2秒能接受推理成本谁出每千次调用的预算大概多少硬件资源是什么手头是A100还是只有一台GPU服务器效果天花板在当前数据质量下能达到多少问完之后绝大多数场景得到的结论是一个轻量模型加上高质量数据已经能满足70%以上的业务需求。盲目上大模型只会让后续的推理优化和成本控制成为恶梦。4.2 一个拿来就用的选型方法论我现在的做法是三步走第一步用最朴素的算法建立baseline。比如做文本分类先用TF-IDF加逻辑回归拿到一个可以接受的准确率 第二步换成一个中等体量的开源预训练模型比如BERT-base或ResNet50这类跟baseline对比收益 第三步只有当效果确实不够时再考虑更大模型、蒸馏或再加数据。三步走的好处是每一步都能量化“多花的成本换来多少效果”决策有理有据跟谁汇报都不心虚。4.3 训练这块我想为“老掉牙”的打法说句话很多人一上来就全参数微调。其实在数据量几千到几万条的情况下我更推荐冻结大部分层、只调后面几层或者用LoRA这类参数高效微调方法。这样训练快、显存占用小效果往往不会比全量微调差太多。拿我做过的一个文本意图识别项目举例同样是BERT-base全量微调在验证集上准确率93.7%用LoRA微调是93.2%几乎持平。但训练消耗差出不少。如果你的业务对精度要求没那么苛刻省下的资源够你再跑好几个实验。再强调一点训练时一定要固定随机种子并且把数据切分的随机因子一起固定。我吃过一次亏两次训练因为shuffle随机性不同模型效果上下波动了1.5个百分点让我误以为是代码改出了问题白排查了两天。5. 建立评估闭环从单一指标到可回归的评测体系模型训练完了工程师最容易干的一件事情是看准确率挺高直接上。但真实业务里准确率这个数字会骗人。缺陷检测项目里我的模型整体准确率98.5%一到线上就露馅就是因为坏样本比例太低全部预测为好样本准确率都能到95%以上指标根本反映不了真实问题。5.1 离线评估的正确姿势我现在的离线评估至少包含三组指标全局指标准确率、精确率、召回率、F1按业务场景分开统计分桶指标按图片尺寸、亮度、纹理复杂度、类别等维度拆分找出模型在哪个子集上明显偏弱错误样本集把预测错的case全部导出来人工过一遍看是标注错了、特征太少还是模型真的判断不了。分桶指标特别有用。比如在我们质检项目里整体F1看着没问题但按缺陷类型一拆细长划痕的召回率只有51%远低于整体水平。如果没有分桶分析这种系统性短板根本不会被发现。5.2 建回归测试集你会有安全感我要求每个项目都要维护一个独立的回归集大概一两千条覆盖正常样本和所有已知的极端case。每次改网络结构、调超参数、换数据版本都先在这个回归集上跑全量评估如果核心指标下降超过一定阈值比如0.5%就直接拦下不进入上线流程。这个回归集作用极大。它保证你后续的每一次优化都是向前走的而不是东补一枪西补一枪。我发现不少团队在做模型迭代时因为缺少回归测试今天修好A类错误明天又把B类能力弄坏了然后团队就陷入了改bug的循环里。5.3 上线前最后一道关线上环境预演光有离线评估还不够。我吃过一次亏离线评估指标漂亮但模型在真实业务数据上的分布和训练集差很多一上线效果就崩。所以我现在上线前一定会做线上shadow mode让模型对线上真实流量做推理只记录结果、不返回给用户。等积累一两天数据后把模型在真实流量上的输出标签抽样人工评估如果效果符合预期才灰度上线。这一步相当于在真实环境和模拟环境之间加了一道缓冲成本低、收益高强烈推荐。6. 部署与推理优化把模型变成真正能扛流量的服务到了这一步离“AI工程化”就剩最后一公里了。不要小看这一公里不少模型是死在部署这一步的——吞吐上不去、延迟忽高忽低、GPU显存爆掉、重启恢复不了。6.1 推理优化的三板斧第一板斧模型格式转换。PyTorch训练完的模型直接部署当然可以但性能不是最优。通常先转成ONNX格式再按需转成TensorRT或者OpenVINO的中间表示。以我们的场景为例同样的batch sizePyTorch原生推理一张图大约20毫秒转换成ONNX后15毫秒再转TensorRT跑到9毫秒翻倍的效果还是很明显的。第二板斧动态batch。网上很多部署教程都建议batch size调大以提升GPU利用率但真实业务请求到达时间是不可控的。如果每个请求都等攒够batch再一起处理延迟会明显上升。我的做法是为推理服务配置动态batch窗口比如每50毫秒攒一批能攒多少算多少到了时间窗口就送GPU推理。代码逻辑大概是这样class DynamicBatcher: def __init__(self, max_batch16, max_wait_ms50): self.queue [] self.max_batch max_batch self.max_wait_ms max_wait_ms async def submit(self, item): self.queue.append(item) if len(self.queue) self.max_batch: batch self.queue self.queue [] return await self._infer(batch)第三板斧并发与资源预估。上线前我会做压力测试观察P99延迟和GPU显存占用。一个简单的经验法则是如果P99延迟在排队情况下超过目标延迟的3倍说明实例数或batch配置需要调整不是单靠调代码能解决的。6.2 服务化应该选什么GPU推理服务我比较常用的是Triton Inference Server原生支持动态batch、并发模型加载和多种后端格式一套服务能同时管多个模型不用每个模型单独写一个web服务。CPU场景下如果模型中等规模直接用FastAPI加ONNX Runtime也可以。不过在写FastAPI推理接口时要注意ONNX Runtime的session是线程安全的千万不要每次请求都重新创建一个session否则性能直接被打穿。我通常的做法是进程启动时初始化一个全局session用锁或线程池控制并发import onnxruntime as ort from fastapi import FastAPI from concurrent.futures import ThreadPoolExecutor app FastAPI() session ort.InferenceSession(model.onnx, providers[CUDAExecutionProvider]) pool ThreadPoolExecutor(max_workers4) def infer(bytes_input): result session.run(None, {input: bytes_input})[0] return result app.post(/predict) async def predict(data: bytes): return await pool.submit(infer, data)6.3 监控指标不要只看CPU模型服务上线后我至少会盯这几类监控请求量、P50/P95/P99延迟、GPU显存占用率、GPU利用率、推理错误率以及输入数据分布是否发生漂移。这里额外提一下数据漂移这是AI工程里很多人忽略的部分。模型周边环境一变输入分布跟训练集差距越来越大模型效果就会滑坡。我现在的做法是把线上输入的特征抽象成一组统计特征比如文本长度分布、图像亮度均值等每隔一段时间跟训练集特征对比一下用PSI或者KS统计量判断漂移程度。超过阈值就自动告警再决定是否要补充数据、重新训练。def calculate_psi(expected, actual, bins10): expected_pct np.histogram(expected, binsbins, densityTrue)[0] actual_pct np.histogram(actual, binsbins, densityTrue)[0] psi np.sum((actual_pct - expected_pct) * np.log((actual_pct 1e-6) / (expected_pct 1e-6))) return psiPSI值超过0.2时是时候认真审查一下数据分布到底发生了什么。7. 模型热更新与灰度发布配合这一节是我特别想补充的。传统后端服务更新代码很简单停机重发、重启完事。AI服务不一样模型文件动辄几百兆重新加载要花时间推理中的流量要平滑切到新版本稍不注意就出现线上断流或新旧模型混用的问题。7.1 模型版本管理与热加载我现在用的方案很简单模型文件全部通过配置中心的路径变量引用服务启动时读取一个版本号不用改代码只要更新配置服务就能自动拉取新模型文件并加载。做法是服务内部定期检查模型目录下的版本文件发现版本号变化就后台加载新模型加载完成后再原子的切换推理引用。这期间旧的请求继续使用旧模型切完后新请求走新模型。核心代码长这样import threading import glob class ModelManager: def __init__(self, model_dir): self.model_dir model_dir self.current_version None self.session None self.lock threading.Lock() self._check_version_loop() def _check_version_loop(self): def run(): while True: version_path glob.glob(f{self.model_dir}/version.txt) if version_path: version open(version_path[0]).read().strip() if version ! self.current_version: new_session ort.InferenceSession(...) with self.lock: self.session new_session self.current_version version time.sleep(30) threading.Thread(targetrun, daemonTrue).start() def predict(self, data): with self.lock: session self.session return session.run(None, {input: data})[0]7.2 灰度发布的评判标准以前上线模型是“三二一全切”现在我是分阶段跑先切5%流量观察一天看错误率和P99延迟没问题再切到20%、50%最后全量。每阶段停留时间取决于业务重要度和监控警报情况。这里要说一个容易被忽视的点灰度期间必须同时监控新旧两个版本的推理效果至少要对比它们的业务指标比如点击率、处理成功率。如果只看了延迟和错误率很可能旧模型效果更好你却浑然不觉。我们做质检项目时还额外加了一条规则新旧版本在灰度期的关键业务指标差超过1个百分点就要回滚。制定了明确的量化标准执行的时候才不会犹豫。8. 几个伴随整个工程化过程的通用经验把这个全链路跑透彻以后还有几件事是跨项目通用的单拿出来说。8.1 实验记录比想象中重要得多AI项目有大量组合数据版本、模型结构、超参数、seed、训练环境任何一个变量不同都有可能影响结果。如果不记录实验结果就很难对比。我现在习惯每个实验一个目录里面包含config.yaml、日志、指标文件和模型权重命名规则是项目_模型_数据版本_日期。data_version: v20240801 model: bert-base-chinese lr: 2e-5 batch_size: 32 seed: 42 framework: transformers 4.36.2 eval_f1: 0.932这份配置直接塞进模型目录导出做实验报告时顺手就能出表。也方便后续如果要复现直接照着配置重新跑。8.2 自动化能省的力气绝不手工省模型训练结束后的重复劳动很多导出模型、转换格式、生成评估报告、打包上传。我后来写了一套Shell和Python脚本把这些步骤压成一条命令。这个习惯救了我很多次尤其是到了晚上上线前手工操作最容易出错。8.3 关于成本说一点我的反思最开始做AI工程我感觉凡事都要上最新的卡、最大的模型、最复杂的框架。真正经历过几个项目之后我的体会完全变了项目上线以后日常迭代里的瓶颈往往不是模型精度而是数据质量、链路稳定性和可观测性。把基础工程打好比追最新的模型更重要。这套从零到一的路线确实没什么捷径但把每个环节做扎实以后后续每一步都会越走越顺。
返回列表