ARTICLE DETAIL

资讯详情

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

从零开始AI工程化:数据、训练、部署、监控全链路实战

从零开始AI工程化:数据、训练、部署、监控全链路实战 2022年我给自己定了一个目标搞一个叫ai-engineering-from-scratch的长期项目从零开始把 AI 应用真正做出来而不是一直停留在看论文、刷榜单、跑通别人代码的阶段。两年前我还是一个只会调库的脚本小子看着 HuggingFace 上几十个模型却连把一个模型部署成稳定的在线服务都没底。现在回头看这个项目让我踩遍了数据、训练、部署、监控、重训全链路能踩的坑也让我真正理解了AI 工程化到底意味着什么。如果你也在 AI 领域入门或者已经能做简单实验、但想把系统放到生产环境这篇博文就是给你写的。我不会从什么是神经网络开始灌输而是讲我这个从零开始项目里最值得复制的方法、工具、路线和真实教训包括怎么选型、怎么设计训练闭环、怎么排查线上事故。很多东西不是教程里有的是踩完坑才知道的。1. 为什么这个项目不叫 machine learning而叫 ai engineering先说清楚一个常见误区很多人觉得我会训练模型就等于AI 工程化了。实际上训练出一个实验阶段的模型在机器学习里叫 prototyping而把一个模型变成稳定、可维护、能持续迭代的产品服务这才是工程问题。ai-engineering-from-scratch这个命名重点在 engineering而不在 ai。我理解的 AI 工程至少包含三层算法层模型结构、损失函数、训练策略、评估方法这是传统机器学习课程最重视的部分。数据层数据采集、清洗、标注、漂移监测、数据版本管理这层几乎没有课程系统讲却决定了模型实际效果的上限。系统层API 封装、部署方式选择、容器化、CI/CD、分布式训练、GPU 资源调度、日志与监控、回滚策略这属于软件工程方法论在 AI 场景的延伸。这个项目真正的起点不是选择一个模型而是把这个金字塔搭起来。我给自己定了几个特别朴素的验收标准一个文本分类小项目从原始数据到线上 API全程不需要手动执行脚本用 pipeline 一键跑通线上服务平均延迟低于 200ms可用性超过 99.9%给模型更新的数据后能自动重训、自动评估评估不达标就拒发新版本线上丢一个模型2 分钟内能定位到原因5 分钟内能回滚到旧版本。这些标准听起来不像学 AI倒更像做后端系统的 SLO。但这才是我想要的只有把工程约束绑在模型上你才会逼自己去理解那些真正决定成败的细节。1.1 为什么大多数人从零开始都失败在工具太多任务太小这个项目一开始也差点夭折。当时我打开了一张工具图谱上面有几十个组件GPU 管理、特征平台、模型注册表、实验跟踪、A/B 测试、自动标注……每个看起来都是必备。我花了一周时间去研究 K8s、Prometheus、MLflow、Kubeflow结果发现自己连训练脚本都还没有。后来我砍掉了 90% 的想法只留下一条最小链路Git 仓库 Python 环境 PyTorch Flask Docker 一个数据库。先把最小能跑通的应用做出来再往上面加复杂度。这是我这个项目学到的第一个工程原则面向问题而不是面向工具。AI 工程的落地是一个逐步加权的过程。从零开始的人先把数据用手工标了一份 5000 条的小数据集把模型用 4 层全连接网络跑通把服务用 Flask 部署在本机就已经超过大多数只会跑 notebook的人了。后面再谈分布式训练、自动重训这些高级玩法才不会空中楼阁。2. 从零开始第一课把地基打实在 Python、Linux 和版本管理上ai-engineering-from-scratch的第一阶段我不是去学模型而是花了两周时间把两样东西练熟一个是 Linux 终端另一个是 Python 的虚拟环境与依赖管理。听起来很基础但至少一半的项目毁在环境依赖上。我在实际项目中见过太多次代码没问题但复现不了的情况最后原因就是 conda 环境与 pip 包版本不一致。所以从第一天起我给这个项目定了一条死规矩所有依赖必须在requirements.txt或 pyproject.toml 里写死版本号并且用同一套方式构建环境。我推荐直接使用 conda 来管理 Python 运行时再配合 pip-tools 来冻结依赖。创建环境时尽量把 Python 版本固定在 3.10 或者 3.11太大的版本跨度很容易让一些底层库的预编译包缺失。conda create -n ai-engineering python3.11 conda activate ai-engineering pip install pip-tools然后写一个 requirements.in声明直接依赖再用 pip-compile 生成 requirements.txt。为什么不用直接 pip install因为依赖项之间会存在隐式传递依赖冲突。生成锁定文件后任何人 clone 我的 repo都只需要执行pip-sync就能得到完全一致的环境。这种可复现意识是 AI 工程和科研实验最大的区别。科研实验里运气好的结果也是结果但工程里系统是长期运行的它要能随时重新训练、重新推理、换服务器迁移如果环境都是碰运气搭出来的根本谈不上维护。2.1 工程化里被严重低估的 Git 技能同样的很多 AI 初学者对 Git 的理解停在commit、push。但是当一个项目涉及多人协作、模型文件、数据文件和代码变化时Git 的运用水平直接影响你开发效率。我在这个项目里只做两块内容也建议你参考把大文件模型 checkpoint、数据集排除在 Git 之外用 Git LFS 或 DVC 管理每个实验分支必须与数据版本、代码版本建立明确映射关系。其中数据版本管理是 AI 工程特有的问题。普通软件工程代码定了行为就定了但 AI 工程里模型的行为由代码 权重 数据共同决定。任何一个变了结果都变了。所以我在仓库里用.gitignore排除 data/ 目录然后引入 DVCData Version Control来对数据集和指标做版本跟踪。DVC 的作用很像 Git但它拿来管理的是大文件或目录。它会生成一个很小的.dvc文件里面存着数据文件的哈希值和存储位置。当你修改了原始数据DVC 会识别出文件内容变化自动生成新版本。这样我可以随时从历史记录里找到用了哪份数据、哪个代码 commit、哪个权重版本训练出的模型。这在排查线上效果下降时简直是救命稻草。后来我意识到所谓 AI 工程化有一半工作是让别人能复现你的结果。Git 管理代码DVC 管理数据MLflow 管理实验指标——这三者合在一起终于能回答为什么昨天效果还好、今天上线就不行这种灵魂拷问。3. 第一个端到端概率题从文本分类服务看最小闭环说一千道一万不如亲手做一个完整闭环。这里我就拿这个项目里训练的第一个能上线的模型举例一个垃圾评论分类器。任务不复杂但麻雀虽小五脏俱全。我坚持用真实场景而非经典数据集是因为真实数据能逼着你处理大量脏活标签噪声、长尾类别、类别不平衡、延迟波动。从这些脏活里学到的恰恰是工程能力的核心。3.1 数据准备与标注比想象中花更多时间的环节项目刚开始我从客服后台导出了 3 万条用户评论全部是文本。我第一想法是直接用现成模型做文本分类但后来发现没有领域标注根本不可靠。于是我从零开始做标注策略。第一步是清洗。HTML 标签、HTML 实体、邮箱、网址统一替换全半角字符转成半角。只用正则做基础清洗就行不需要引入多么复杂的 NLP 库。第二步是采样。因为真实数据里正常评论占绝大多数约 80%垃圾评论很少直接拿原始数据训练会导致模型把所有样本都预测成正常类。我先从里面挑出了正例垃圾类2500 条又随机采样了 2500 条正常评论组成了 5000 条均衡训练集。这是一开始提高基线的最简单方法。第三步是标注。我采用一人标注一人复核的方式我标注完后请一个朋友复核不一致的地方。只标注两个标签避免主观性太强。产出是字段说明text清洗后的评论文本label0 表示正常1 表示垃圾splittrain / val / testsource_id原始数据 ID我把数据分成 train/val/test比例 8:1:1并且保证测试集完全独立。这里有个非常容易踩的坑如果用同一个数据生成训练集和测试集模型会因具体样本记忆而显得效果好但实际上线后会崩溃。数据切分必须按照时间顺序或 ID 哈希来防泄漏。我当时直接用train_test_split但通过设置固定随机种子并确保类别比例一致这才算是个合格的开始。3.2 模型选型与训练从简单模型开始不要一上来就用大模型分类这种任务当时 BERT 系列已经是标配了。但为了真正理解训练链路我第一个模型不是 BERT而是用 TF-IDF 逻辑回归作为 baseline。不要小看这个 baseline在 5000 条小数据上简单的线性模型往往能到 0.89 的 F1而完全不需要 GPU。baseline 的作用是给整个 pipeline 跑通提供一条最便宜的路径。当我从 TF-IDF LR 开始把任务分解成特征化、训练、评估、部署四步之后迁移到 BERT 就只是换模型层而已。BF16 和 GPU 的问题是在我切换到 transformer 之后才出现的。我用的是bert-base-chinese如果是中文场景通过 HuggingFace 的AutoModelForSequenceClassification加载。from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForSequenceClassification.from_pretrained(bert-base-chinese, num_labels2)训练参数我给出一个经验值batch size 32学习率 2e-5epochs 3。因为数据规模很小三个 epoch 足够让验证 loss 收敛再多就容易过拟合。在 5000 条数据上即使单卡 T4也只需要三分钟左右。这里有一个我在真实项目中遇到的训练参数坑max_length别设置太高。文本评论一般不到 128 个 token但我一开始图省事设成了 512导致训练内存占用巨大、速度很慢。实际上直接把 tokenizer 的max_length设成 128足以覆盖 99% 以上的样本。训练完成后我保存了两个产物一个是 PyTorch 的pytorch_model.bin权重一个是「向量化模型」的打包产物。后面部署时发现单独保存模型还不够必须把 tokenizer 一起保存因为推理时需要相同的分词规则。这个看似理所当然的细节我忘了无数次。3.3 把模型封装成可观测的服务你远不需要微服务模型训完下一步就是部署。我最开始看到别人用 Kubernetes 部署自己也学着写一堆 yaml 文件结果本机连个稳定的容器编排都搭不起来。后来我才意识到对于一个单模型服务最简单可靠的方式就是单体 FastAPI 服务 Docker 容器。FastAPI 天然支持异步而且有自动生成的 API 文档。我把推理逻辑封装在/predict接口from fastapi import FastAPI from pydantic import BaseModel class Item(BaseModel): text: str app FastAPI() app.post(/predict) def predict(item: Item): result classifier.predict(item.text) return {label: result[label], probability: result[score]}但为了不让模型占用过多内存加载模型这一步需要优化模型加载很耗时不能在每次请求里做。我把模型加载放在lifespan事件或者模块初始化位置确保只有一个进程共享模型。对了还有一个很重要的设置启动时设置model.eval()并且把torch.no_grad()包在推理里否则模型会保留 dropout 行为推理结果不稳定。线上环境的部署还涉及并发问题。Gunicorn 可以选择多 worker但每个 worker 都会加载一份模型内存倍增。一台 8GB 的机器如果模型 500MB开 4 个 worker 就会显得很吃力。我实际测试下来对于这种推理延迟在几十毫秒的模型开两个 worker 就够了后面的吞吐量可以通过扩容器解决不必在单机死磕。3.4 用 Docker 固定一切未知让打包后不可变部署 AI 模型最大的敌人是本地跑得通服务器跑不起来。从零开始这个项目时我吃了太多系统库缺失、CUDAToolkit 版本不对的亏。所以后来我强制要求所有服务必须走 Docker。一个最小可用的 Dockerfile 是这样的FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD [uvicorn, server:app, --host, 0.0.0.0, --port, 8000]注意镜像最好不要选择体积巨大的 pytorch 官方镜像而是先安装 CPU 版还是 GPU 版要想清楚。如果线上只有 CPU 推理就用 CPU 版的 torch体积直接减少好几 GB如果线上有 GPU就使用带 CUDA runtime 的镜像。我这里因为用到小文本模型CPU 推理也只要 30ms 左右所以初期直接走 CPU 部署省了很多 CUDA 兼容性问题。构建并运行后我能通过docker logs查看日志通过docker ps查看状态。再配一个docker-compose.yml同时拉起 API 服务和监控面板。这一套在单机上可以做小型生产也完全足够支持我这个项目的第一个真实用户我自己使用。4. 工程化铁三角数据版本、实验跟踪与线上监控当第一个服务上线后最难的部分不是上线而是持续迭代。AI 模型是有生命周期的它需要被更新、被评估、被监控还要在效果滑坡时被拉回。这是ai-engineering-from-scratch项目最有价值的一章。4.1 用 MLflow 组织每一次训练实验不记录等于没做我在第二个模型迭代时就后悔了没有记录任何实验配置导致过了两周后我忘记上一次训练用了什么学习率、什么数据版本。后来我把 MLflow 引入了项目。MLflow 是一个开源平台给我提供三个核心能力mlflow.start_run()记录每次训练的指标loss、acc、F1mlflow.log_params()记录超参数mlflow.log_artifact()保存模型文件和指标图。这样每次训练都会生成一个 run里面完整保留了代码状态、依赖环境、参数和产物。我甚至可以在那之后轻松地对比两个 run 的指标选出最好的模型。这里我推荐在训练脚本里把 MLflow 接入函数写成一个装饰器这样可以避免每个训练脚本都重复一段初始化代码。我的项目里做了类似track_experiment的工具类它自动收集代码版本、系统环境、GPU 型号等信息。对于从零开始的项目这些元信息越早收后面的分析成本越低。4.2 模型注册表的引入不是所有模型都要直接上线MLflow 还有一个功能是 Model Registry。它把训练好的模型登记在统一命名空间中并标注阶段Staging、Production、Archived。在生产环境我部署的是 registry 中标记为Production版本的模型然后配合 model version 做灰度。这里的关键决策是先评估后生产。每次新模型训练完我都会计算它在测试集上的 F1、延迟并和当前线上模型的 baseline 进行比较只有满足一定条件才把版本推送到生产。这本身是一个简单的 gate候选模型 F1 必须不低于线上模型候选模型推理延迟低于 100ms在测试机配置下候选模型的数据分布与线上分布无明显漂移。最后一个条件是我后来加的因为它解决了一个大坑数据分布漂移会导致候选模型跑分很漂亮上线后却没效果。稍后我会仔细讲这个案例。4.3 监控不只是看 CPU要盯准确率和输入分布部署完成不是终点而是运营的起点。传统的监控面板看 CPU、内存、请求延迟、错误率就够了但对 AI 服务来说还有一类特有的监控模型相关指标。我建立了三层监控系统层请求量、P95 延迟、错误率、CPU/内存占用。这部分用 Prometheus 抓取指标Grafana 展示。数据层线上输入文本的 Token 长度分布、类别关键词频率、ASCII 字符占比等。一旦这些分布漂移模型可能开始看不懂新到的样本。预测层对模型输出的概率置信度、正类率、预测稳定性做统计分析。比如垃圾评论识别线上预测的正类率突然从 9% 升到 20%可能是流量环境变了也可能模型坏了。监控的作用不是好看而是要能在事故发生时快速缩小排查范围。我把这个经验总结为系统监控告诉你哪里出问题数据监控告诉你为什么模型出问题两者缺一不可。5. 一次真实的线上事故推理延迟飚升根因竟是数据漂移这套体系到底有没有用要用一场事故来检验。项目上线第三个月一个晚上我收到报警系统 P95 延迟从 70ms 涨到了 500ms错误率没有明显变化但服务已经开始大量排队。我当时的排查链路可以说把这个项目里的知识全串起来了。第一步看系统监控。CPU 使用率到达 90% 以上但 GPU如果有是空闲的。这首先说明瓶颈不在模型计算而在上游或者预处理。我立刻打开最近的日志看有没有超长文本进入。第二步看数据分布监控。发现新增评论的平均 token 长度从原来的 60 变为 160而且有大量包含大量空白字符和重复标点的样本。原来是一个新的内容运营渠道加了大量长文本导入导致 tokenizer 处理时间翻倍Transformer 的序列长度也拖慢了整个推理过程。第三步看预测分布。模型的垃圾评论判定概率也变了因为长文本中有大量重复信息segments 长度导致注意力计算范围太大输出概率被挤压到边界。我甚至看到部分正常长文被误判为垃圾。这个问题的解决不是靠改代码而是靠规范上游新渠道的内容接入必须经过长度截断和清洗规则。我加了一个预处理拦截函数当文本 token 长度超过 512 时只截取前 512 个 token。同时给 API 增加请求文本长度上限拒绝异常超长输入。这次事故让我最深的体会是AI 服务是一个输入输出高度依赖分布的活系统。如果你不对数据分布设防那么再好的模型也会被来自渠道侧的异常数据打垮。5.1 从事故回溯重训的正确姿势事故解决完还需要重训模型吗答案是肯定的但重训不是盲目训练。我在这个项目里总结了一套重训触发时机当线上输入的 token 长度分布、句式复杂度等明显飘移时当模型输出的置信度整体下降时当分类任务的个别类别召回率下降超过 5% 时当用户投诉或业务侧反馈增多时。具体到事故场景因为新渠道带来了大量长文本我重新标注了一批长评论样本并入训练集再跑一轮训练。此时 MLflow 会把新 run 与旧 run 并列展示我对比各项指标后确认新模型在长文本类人上召回率提升了 15%且不损害短文本效果。这种有数据支撑的决策过程和那种因为新模型看起来好就上线的野路子完全不同。6. 给同样从零开始的人实操建议、工具清单与长期维护经验写到现在ai-engineering-from-scratch这个项目已经跑了半年多。我最大变化是从我能不能训练出一个模型变成了我能不能保证模型在生产里稳定变好。如果你也准备从零开始我建议你把学习路径设计成这样先跑一个完整的传统机器学习项目用 sklearn、pandas不用深度学习体验特征模型评估部署的完整逻辑。再切换到 PyTorch 做一个小型深度学习项目但要加上模型版本管理、Docker 部署、监控。之后再引入自动重训练和复杂部署架构不要一上来就上 K8s。工具清单我整理成表你可以按需取用阶段工具用途数据管理DVC数据版本控制、存储外部化环境管理conda pip-tools依赖固定、可复现环境模型开发PyTorch HuggingFace模型训练与微调实验跟踪MLflow指标/参数/产物记录、模型注册表服务化FastAPI Uvicorn模型 API 封装容器化Docker Docker Compose环境隔离、一键部署监控Prometheus Grafana系统与业务指标采集与可视CI/CDGitHub Actions训练后自动测试、构建镜像并推送这里面没有一项是特别复杂的黑科技但组合在一起就会产生质变。我还想特别提一下笔记我从这个项目开始给每个模块写一个 README 文档内容包括目标、接口、错误代码和重新训练的命令。这个方法让我在半年后还能轻松接手自己当初的项目。6.1 长期维护这个项目的三个心法最后作为从零开始的过来人我想分享三个让项目持续下去的心法。第一把失败当作数据。每次事故、bug 都记录到一个incidents.md文件里写上时间、现象、根因、处理办法。一年后你会拥有一份珍贵的错误集。这些错误集比任何课程都能让你快速变强。第二永远保留一条手动逃生通道。不管自动化 pipeline 多完善我始终保留了用 shell 脚本手动重跑一遍训练加部署的能力。自动化会出 bug但手动脚本永远是你理解系统的参照物。第三不要追求一步到位。这个项目里我有一半时间是在删掉之前不怎么使用的工具。每隔一到两周我都会问自己这个工具还在实际工作流里吗不在了就删。保持系统精简才愿意继续维护它。昨天我又打开这个项目看到自己一年多前刚开始时写的训练脚本那时候连train_test_split的random_state都不知道怎么固定。但也就是在这样一次一次犯傻、排查、补全之后数据版本的映射、模型的 SLO、漂移的响应机制才一点点长进肌肉里。如果你的目标也是做出一个真正可以被用户使用的 AI 产品我建议你直接复制我这条路线从一个最小的任务开始构建一个最简闭环然后让工程约束逼着你学会那些教科书里没有的东西。AI 工程这条路没有捷径但每踩一个坑你的系统都会更硬实一点。
返回列表