ARTICLE DETAIL

资讯详情

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

AI工程实践:从零搭建可靠可迭代的机器学习基础设施

AI工程实践:从零搭建可靠可迭代的机器学习基础设施 开篇为什么我要把“AI工程”当成一个独立课题来做做算法的人大概率都有过这种经历模型在Jupyter Notebook里跑得风生水起精度指标漂亮得能发论文但一说到“上线”两个字整个人就萎了。数据怎么接特征怎么存模型怎么更新服务挂了怎么恢复这些问题和模型本身无关但它们决定了一个AI项目到底能不能成为产品。我大概在一年多前开始系统性地整理自己的“AI工程”方法论慢慢沉淀出了一个叫“ai-engineering-from-scratch”的东西。这里的核心不是“AI”而是“engineering”——它关注的是如何用工程手段把模型能力稳定、可靠、可迭代地交付出去。如果你和我一样是从算法背景转向工程落地或者你想从零开始搭一套属于自己团队的AI基础设施这篇文章应该能帮你少走很多弯路。我会按我自己搭这套体系的顺序来讲从架构设计到数据管线从训练实验到上线运维每一层都结合我实际踩过的坑来说。1. 先从根上说清楚AI工程和算法研究到底差在哪1.1 从“能跑通”到“能上线”隔着一整条工程链很多工程师刚接触AI项目时默认把精力放在“模型选型”和“调参”上这其实是把AI工程误当成了算法研究。算法研究的终点是“效果达标”而AI工程的终点是“稳定运行”。差在哪我举个实际例子你训练了一个文本分类模型离线测试准确率92%很满意。但上线后发现线上数据的分布和训练集不完全一样准确率掉到80%以下怎么办再比如这个模型跑在GPU上延迟是50毫秒但你的业务接口要求20毫秒以内怎么优化模型要每周更新一次但旧的预测结果已经存在了客户的数据库里如何保证版本可追溯这些问题不解决模型再准也白搭。所以我搭建“ai-engineering-from-scratch”的第一原则就是把模型的整个生命周期都纳入工程设计范围而不是只盯着训练那一刻。数据采集、特征工程、训练实验、模型评估、部署上线、监控告警、版本回滚每个环节都要有明确的规范和工具支撑。有人问我一个人或者小团队做这件事是不是太耗费精力我的回答是如果不做后面返工的代价更大。我从一开始就把整套体系拆成了五个层次基础设施层、数据层、模型层、部署层、应用层。每一层都有独立的职责边界彼此通过标准接口交互。这样做的好处是今天你用一个开源模型做原型明天换成商业API影响的可能只是模型层其他层完全不用动。1.2 先给自己画一张路线图别急着装环境很多人学AI工程的第一步是“装一堆工具”——先装个Docker再装个Kubernetes再配个MLflow然后发现什么都不会用又灰溜溜地删掉。我个人的建议相反先画架构图再写代码。你只需要在一张纸上回答这么几个问题你的数据从哪来实时还是离线结构化还是非结构化训练和推理的资源是共用还是分离模型的预测结果要提供给哪些下游系统通过API还是消息队列模型要多久更新一次更新失败有没有降级预案有没有预算用成熟的云服务还是必须全部自建拿我自己来说当时团队条件有限没有专门的ML平台所以我的选择是用开源组件自建轻量级基础设施。数据存S3兼容的对象存储训练环境用Docker封装实验跟踪用MLflow服务部署用FastAPI加容器编排监控用Prometheus加Grafana。这套组合的好处是每一环都换得掉哪个组件不满意可以单独替换不会把整套体系绑死。后来我验证了这个判断一开始我用了某一个重量级的机器学习编排平台配置复杂到让人怀疑人生。后来简化成现在的方案反而跑得更顺。说白了工程化的本质不是工具堆砌而是用最简单可靠的方案满足当前需求同时保留演进空间。2. 基础设施层先把地基打牢后面才不会三天两头塌2.1 环境一致性告别“在我机器上跑得好好的”AI项目里流传度最高的甩锅话就是“在我机器上跑得好好的”。这句话背后的问题是环境不一致。训练依赖的CUDA版本、Python库、系统库推理环境的差异哪怕只是一个小版本不同都能让模型表现天差地别。我采取的方案是Docker容器化但这里有一个很多教程没讲透的细节训练环境和推理环境的镜像必须分开维护。训练环境往往需要更多的编译依赖、调试工具而推理环境讲究精简、快速启动、安全。比如我做文本分类服务时训练镜像用的是Python 3.10加PyTorch 2.0加CUDA 11.8外加一堆数据处理库。推理镜像则只保留跑模型必需的那几个包甚至把基础镜像换成更轻量的发行版镜像体积从2个多G压到不到800M。这样推理服务冷启动时间从十几秒降到三秒左右成本节省非常明显。注意容器虽然能解决大部分环境问题但GPU驱动是宿主机层面的容器没法帮你统一。所以我在所有训练机的宿主机上都锁定了同一个GPU驱动版本并在部署文档里把这个作为硬性前置条件写清楚避免底层不一致。另外依赖锁定也是个容易被忽略的细节。不要用requirements.txt里写“大于某个版本”的方式这等于允许环境漂移。我用的是Poetry的poetry.lock文件锁定每一个依赖包的精确版本同时记录哈希值确保任何人在任何时间拉下来都能复现一模一样的训练环境。实测下来这个习惯帮我减少了很多“同为的bug”“同样是跑同一个脚本结果不一样”的诡异问题。2.2 GPU资源管理单机多卡和多机协同的取舍本身只是做一两个模型的小团队一开始没必要上Kubernetes更没必要搞多机分布式训练。原因很简单分布式训练的复杂度不是线性增加的而是指数级增加。网络通信、数据分发、容错恢复任何一个环节出问题排查成本都极高。我的建议是单机多卡先跑通多机协同按需再上。单机多卡用PyTorch自带的torchrun就能搞定。例如在两块A100上做数据并行训练核心命令并不复杂torchrun --nproc_per_node2 train.py --config configs/bert_base.yaml关键是模型里的代码要做少量适配。把模型和数据加载都封装好再通过DistributedDataParallel做同步。很多开源库其实已经封装好了这些逻辑你只需要理解它的运行机制不用自己从头实现。如果后续确实需要多机训练我也建议借助现成的框架比如DeepSpeed或Horovod来处理而不是自己去写通信逻辑。在AI工程里能站到巨人肩膀上的就别自己去造轮子省下来的时间可以花在更值得投入的数据和业务理解上。还有一点关于GPU利用率的经验很多人训练时发现GPU利用率忽高忽低排查了很久发现是数据加载瓶颈。解决办法很朴素多开几个DataLoader的工作进程设置num_workers8同时打开pin_memoryTrue。这块如果本身是在CPU向GPU传输数据这两个参数的组合基本能解决大半的利用率问题。2.3 对象存储与文件组织规范数据存放这块我强烈建议用对象存储而不是直接散落在各台机器的本地磁盘。本地方案的问题在于机器挂掉数据丢、扩容困难、多机共享麻烦。对象存储S3或兼容方案可以用一套接口统一管理原始数据、中间结果、模型产物和实验日志。这里我踩过一个很典型的坑刚开始没有规范文件路径每个人按自己的习惯存放数据结果不同实验之间经常搞混有的实验A的数据被实验B覆盖了跑了几天白跑。后来我定了一套简单但严格的约定s3://ml-bucket/ ├── datasets/ │ ├── raw/ # 原始数据只读 │ └── processed/ # 清洗后的数据按日期分目录 ├── models/ │ ├── checkpoints/ # 训练中间产物 │ └── released/ # 上线版本带版本号和哈希 ├── experiments/ │ └── {experiment_name}/ │ ├── config.yaml │ ├── metrics.json │ └── artifacts/这个结构看似普通但它解决了三个关键问题原始数据不可变、中间目录按时间隔离、模型产物可追溯。任何一个新同学加入看到这个目录结构就能快速理解整个项目的数据流走向。3. 数据管线数据质量不过关谈什么模型效果3.1 数据版本化像管代码一样管数据做AI工程的人对代码有天然的版本意识但对数据就随意得多。这是个大坑。模型效果可复现的前提是数据和代码都可复现如果你改了数据但没记录版本出了问题根本不知道是哪份数据导致的。我在这个项目里引入了DVC做数据版本管理。它和Git配合得很好Git管代码DVC管大数据文件。DVC的核心机制是把大文件的元数据哈希、地址记录在Git里实际数据放到对象存储这样既保留了版本历史又不用把几十G的数据塞进Git仓库。举个实际使用的场景我需要处理一批用户行为日志原始文件每天落一个目录约20G。我把它纳入DVC管理后每次做数据清洗都生成一个新的版本并打上标签。这样我能随时回溯到“上个月那份经过清洗的数据”不会在实验过程中丢失某个关键数据版本。有一点要提醒数据版本化不能只记录文件的哈希还要把生产数据的元数据字段说明、统计分布、来源一并记录下来。我习惯在每个数据集目录下放一个schema.yaml描述每个字段的类型、含义、取值范围。这一步在数据量小的时候看不出价值但一旦团队扩张或时间久了它就是救命文档。3.2 清洗、标注与增强的流水线设计很多从零做AI工程的人不知道数据清洗应该做到什么程度。我提供一个判断标准清洗的目标是让数据能被下游训练代码稳定消费而不是把数据“洗干净”到失去真实分布。过度清洗会让模型在线上遇到脏数据时表现得很差。我当前的数据处理管线是按“分层处理”的方式组织的第一层格式校验校验字段完整性、类型正确性、缺失值比例是否在阈值内。这一层的作用是防止“垃圾进、垃圾出”。第二层业务清洗去掉对建模没有意义的记录。比如用户协议明确要求不分析的数据、明显的机器人行为记录等。第三层特征计算把原始字段转化为模型可用的特征向量。每一层都做成独立的Python脚本或函数允许单独执行和单独测试。这样最大的好处是出了问题能快速定位是哪一层的问题不用从头到尾跑一遍。标注这一块如果做的是监督学习标注质量比标注数量重要得多。我自己试过给文本分类任务标注数据时如果三个标注员的一致性不到80%训练出来的模型效果会非常差。所以我推荐至少要有标注规范文档加双人标注抽检的机制样本量小时宁可多花时间做校验。数据增强则要根据业务场景慎重使用。CV领域常见的翻转、裁剪等增强在工业场景里不一定适用。比如在质检场景把缺陷样本翻转可能完全改变物理含义。**增强手段必须基于领域知识判断而不是盲目使用。3.3 在线特征与离线特征的一致性一个经常被忽视的暗坑这一块是我个人觉得最“工程化”的部分也是很多算法背景的同学转型时最容易翻车的地方。离线训练时你用于训练的特征和线上推理时模型拿到的特征必须保证计算逻辑完全一致否则上线后效果会受到很大的折损。举个实际例子假设你有一个特征叫“用户过去7天的点击率”离线训练时你用Spark算了一份完整的历史数据。上线时如果在API层用Python现算你会发现因为数据边界、时间窗口、去重逻辑的细微差异算出来的特征值和离线版本对不上模型输入偏移加上特征分布偏移加上线上真实分布与训练分布不同最终效果掉多少你真的不知道。我建议的解决方案是把特征计算方法打包成一个统一模块离线在线共用同一套代码。也就是所谓的“训练-服务一致性”。如果条件允许可以用特征平台或特征存储来统一管理。小团队没有这个预算的话至少要做到把特征计算逻辑抽成独立的库训练脚本和推理服务都依赖这个库用同一份代码计算特征。这样虽然不能做到100%一致但至少可以减少一大部分人为不一致的问题。我在实际项目里就吃过这个亏。当时做推荐排序模型离线AUC达到0.78上线后实时效果却比基线差。排查了两天才发现是线上调用了一个旧版的tokenization库特征编码的vocab不一致导致输入特征整体偏移。从那以后所有特征相关的代码我都要求必须做单元测试并且专门写了一个“特征一致性校验”的脚本定期抽样对比离线特征和线上特征的分布。4. 模型训练与评估让每一次实验都“有据可循”4.1 实验跟踪不要靠“记事本”记录调参历史“我记得上次跑这个模型效果挺好的但参数是什么来着”——这句话我听过太多遍也踩过太多次坑。做AI工程必须有一个实验跟踪系统把所有训练运行的配置、指标、产物都自动记录下来。我选的是MLflow因为它在开源方案里算是最成熟、最不折腾的。核心用法极其简单在训练脚本里启动一个mlflow.start_run()把超参数通过mlflow.log_param()记录把每个epoch的loss、accuracy用mlflow.log_metric()记录最后把模型文件通过mlflow.log_artifact()保存。只需要加十几行代码整个实验过程就“可视化”了。但工具只是一半另一半是实验命名规范和标签体系。我用这样的命名方式{project}__{model_type}__{dataset_version}__{purpose}例如news_cls__bert_base__v20240601__warm_start_test。命名写清楚是为了方便搜索标签则用来标记“这是不是候选版本”“基于哪份数据跑的”这类状态信息。实验跟踪还有一个容易被忽视的功能模型注册。MLflow的Model Registry允许我登记模型版本标注“Staging”预发布、“Production”生产状态。我从一开始就坚持只有登记在Model Registry里的模型才有权部署上线。这条纪律保证了线上模型永远可查、可回滚。4.2 评估体系一个指标打天下是远远不够的很多算法博客喜欢展示“Accuracy提升到99%”但到工程落地阶段光看一个指标很容易翻车。举个实际例子在电商搜索场景下推荐准确率99%可能没有任何意义因为在长尾商品上准确率这个词基本没用。我自己的评估体系分三层第一层模型原生指标比如loss、AUC、F1、BLEU这些用来快速判断训练是否收敛。第二层业务指标比如CTR预估的LogLoss、推荐场景的RecallK、搜索场景的MRR。这些反映模型在具体业务场景中的效果。第三层稳健性指标包括模型在不同人群、不同时间段上的性能差异模型对噪声输入的容忍度等。其中第三层最容易被忽略但对工程落地極为重要。我做过一个内容审核模型整体准确率很高但后来发现对某一种方言文本的误判率异常高就是因为没有在评估时做分群分析。具体的操作建议是评估集要单独构建绝不和训练集混在一起并且要把评估集数据按业务维度切分。比如按时间切分一份按用户来源切分一份甚至构造一些带噪声的对抗样本。然后每次训练结束都生成一份标准化的评测报告自动同步给团队同学。这样每个版本的效果都一目了然不会出现“上线后发现某个维度特别差”的尴尬。另外请记住评估集本身也需要版本管理和数据集一样纳入DVC。因为如果评估集一直在变不同实验之间就没法横向比较了。4.3 可复现性配置文件比代码更重要很多人以为可复现性就是“记下随机种子”。这远不够。真正的可复现性要求任何一次实验结果都能用一条命令从原始数据重新跑一遍得到相同或可接受范围内一致的结果。我让每个实验目录下都保留三个东西固定的代码版本Git commit hash固定的数据版本DVC的哈希值完整的配置文件config.yaml训练脚本的入口统一是python train.py --config experiments/xxx/config.yamlconfig.yaml里包含数据路径、模型结构参数、学习率、batch size、优化器参数、随机种子、训练轮数等一切会影响结果的信息。这样做的价值在于两个月之后同学问起这个实验是怎么跑的你可以直接把实验目录丢给他而不是靠大脑回忆。有一个参数需要特别提醒随机种子要设但不要以为设了种子就能完全复现。GPU计算本身存在非确定性特别是使用CuDNN的某些算子时。如果项目确实对复现精度要求极高可能需要锁定到具体的CuDNN算法但那样会很影响性能。一般业务场景设定种子加数据版本锁定已经足够对比实验效果了。5. 部署、监控与迭代闭环上线才是真正的开始5.1 在线推理服务用FastAPI做轻量级模型服务模型训练完之后怎么供外部系统调用我采用的是FastAPI加Uvicorn这套组合在“简单”“性能”“生态”三者之间是比较平衡的选择。一个最小可用的推理服务大概长这样from fastapi import FastAPI from pydantic import BaseModel import torch app FastAPI() model None class PredictRequest(BaseModel): text: str app.on_event(startup) def load_model(): global model model torch.load(models/released/v3/model.pt, map_locationcpu) app.post(/predict) def predict(req: PredictRequest): output model.predict(req.text) return {result: output}注意几点细节模型启动时加载一次而不是每次请求都加载否则延迟会打爆如果吞吐量要求高可以设置preload和批量推理来把多个请求拼成一个batch输入GPU。再用Gunicorn启动多个Uvicorn worker进程配合--worker-class uvicorn.workers.UvicornWorker比单进程能扛的并发量高一个量级。服务接口设计上我坚持两个原则所有请求和响应都做结构化验证用Pydantic模型所有错误都返回统一的错误格式。千行万行业务系统对接时接口协议清楚是最基础的要求。5.2 监控告警没有监控的模型早晚会背锅模型和传统软件有个最大的区别传统软件的bug通常是“确定性”的逻辑错了就是错了模型的错误却是“分布性”的可能在某个时间点、某类数据上突然表现不好。没有监控你根本无从知晓。我的监控指标分成两套第一套是系统指标推理延迟、QPS、错误率、显存占用。这些用Prometheus加Grafana做指标采集和可视化设置阈值告警比如P99延迟超过500毫秒就告警。第二套是模型指标预测结果的分布变化、特征输入分布的漂移情况、预测置信度随时间的变化。比如一个二分类模型的预测概率均值原本在0.3左右某天开始突然飙升到0.7这说明线上数据分布可能变了模型输入分布已经偏移需要重新评估或更新。数据漂移检测我建议用简单的PSIPopulation Stability Index方法实现在样本量不大的情况下能快速发现分布偏移。计算伪代码大致是def calculate_psi(expected, actual): # 计算两个分布之间的PSI值 # PSI大于0.25表示明显偏移 ...告警方式是钉钉/邮件/企业微信等常见通道。重要的是告警一定要分级不要设置“一有风吹草动就全员通知”否则大家习惯了告警就会忽略真正的风险。我的做法是P0级别服务不可用立即调用值班电话P1级别模型指标异常每半小时汇总一次P2级别数据趋势缓慢变化每天日报展示。5.3 模型更新与回滚机制上线了不代表万事大吉模型上线了你的工作才刚完成一半剩下的一半是怎么持续迭代而不出事。我的做法是“金丝雀发布”新模型先在线上切一小部分流量比如5%实时对比新老模型的业务指标观察一段时间后再逐步放量。如果指标下降一键切回旧版本。这套策略其实不需要复杂的框架用Nginx或API网关的权重路由就能实现关键是流量切分和回滚脚本要提前演练好不要到了发布当天才开始实验。模型回滚有一个容易忽视的细节回滚不只是切换模型文件还需要考虑特征计算的兼容性。如果新模型使用了新的特征逻辑而旧模型依赖的是旧特征回滚时特征服务也需要一起回滚否则新旧版本会发生“错位”。我在实践中用的是“版本对”的概念——每次发布都会把模型文件、特征配置、推理代码打包成一个整体标记为一个发布单元。回滚时一口气回滚整个单元而不是只换模型。模型迭代的频率同样需要设计更新太频繁会消耗大量工程资源更新太慢会导致线上效果逐渐劣化。我一般用数据漂移指标驱动更新决策没有漂移就不更新漂移超过阈值才触发重训和发布流程。这种“按需发布”的策略比固定周期更新要高效得多。6. 常见问题与排查技巧实录那些文档里不会告诉你的坑6.1 环境问题“我在本地跑得好好的”怎么破这是我从零搭这套体系时遇到最频繁的抱怨。一个典型的场景是训练脚本在开发机上正常跑一放到训练服务器上就报错不是No module named xxx就是CUDA error: no kernel image is available。排查思路基本三步走先对比两边的Docker镜像是否一致包括基础镜像tag、依赖锁文件是否更新再检查GPU驱动和CUDA Runtime是否匹配。很多容器里的CUDA是软链接底层驱动版本不对就会出现“no kernel image”的错误最后检查数据路径是否可访问尤其是涉及到挂载盘或对象存储时权限问题经常被忽略我建议在CI阶段就加一道“环境一致性”的检查拉取最新代码和锁文件构建镜像跑一个冒烟测试。冒烟测试通过才算候选版本。6.2 训练走了半天GPU利用率只有30%这个现象很常见我之前排查过好几次。大部分原因集中在数据加载环节数据读取太慢、预处理太耗时、CPU和GPU之间的数据传输没有做异步预取。最简单的优化手段是用DataLoader的num_workers参数把数据加载放到子进程打开pin_memory直接锁页内存传输到GPU用prefetch_factor设置预取的批次数如果数据本身太大优先搞成缓存格式或者做样本采样不要每次都从对象存储拉取全集。再一个检查点数据增强是否在CPU上处理了太多逻辑有时候瓶颈不是读取而是增强计算本身这种可以在GPU上做增强。6.3 模型上线效果不如离线测试问题出在哪儿这个问题的排查是最考验工程能力的也是被问我最多的问题。一般按下面的顺序排查首先看特征一致性线上特征计算逻辑和离线是同一套代码吗这一步检查用什么库、什么版本、什么字典。其次看数据分布线上给到模型的样本和训练集的分布差异有多大可以用PSI快速判断。再看推理服务的行为比如有没有做和前处理不同的tokenization文本截断长度是否一致模型输入的格式是否被f-string转换过等。最后才考虑代码bug。很多时候问题出在“pipeline的某个环节偷偷改了数据格式”这种问题只有靠日志和测试才能发现。我的建议是每次上线前强制跑一遍“一致性测试套件”把离线特征、预测结果、线上请求结果放在一起对比任何不一致都亮红灯不允许上线。这几十分钟的检查省的是上线后几天排查的时间。6.4 模型版本越来越乱团队协作出现混乱版本混乱的根源是没有统一的管理规范。如果没有强制工具和流程每个人都有自己的“v12_final_真的最终版”。规范其实不难定难的是坚持执行模型文件只准通过MLflow注册后发布不允许手工拷贝发布记录统一写在发布文档里和代码版本、数据版本、配置版本对应绑定所有临时实验都在独立的分支或标签下进行禁止在主线上试参数只要坚持两周团队就会养成习惯。我个人体会是工程规范不是限制自由而是把自由留给真正需要创造力的地方。模型调参、算法创新这些地方要有灵活性但版本管理、发布流程这种地方必须严格。7. 最后分享一点真实感悟搭建“ai-engineering-from-scratch”这件事与其说是一个技术项目不如说是一次思维方式的转变。一开始我总觉得“AI工程”就是把一堆开源工具拼起来后来才发现真正难的不是工具而是设计出适合自己团队规模和数据形态的工作流程。工具可以换、框架可以换但流程要是乱的换什么工具都一样。我个人体会最深的一条建议从最小可行链路开始不要追求一步到位。哪怕只有一条数据、一个模型、一个API先把这条链路完整跑通再逐步加监控、加版本管理、加自动化测试。一个跑通的简单系统比一个设计复杂但落不了地的完美系统有价值得多。这这套体系还有很大的延展空间。比如我现在在做的还只是定时训练和按需发布下一步可以引入更自动化的更新触发机制以及更细粒度的模型性能分析。如果以后有机会我也可以把我在“特征存储方案选型”上的对比实验整理出来分享。如果你也在从零搭自己的AI工程体系或者在数据管线和模型部署上有什么问题欢迎一起交流。
返回列表