ARTICLE DETAIL

资讯详情

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

AI工程化从零搭建:数据版本控制、实验追踪与模型部署实战

AI工程化从零搭建:数据版本控制、实验追踪与模型部署实战 1. 从零搭建AI工程能力这个项目到底在解决什么问题第一次看到ai-engineering-from-scratch这个标题我脑子里蹦出来的第一个念头是又一个“从入门到放弃”的教程仓库但翻了一圈之后发现它想做的事情其实比“教你调包”要硬核得多——它试图回答一个被很多人忽略的问题当一个AI模型离开Jupyter Notebook进入真实的生产环境时中间到底缺了哪些东西这个项目本质上是一套面向开发者的AI工程化实践路线图。它不是教你如何训练一个SOTA模型而是教你如何把一个能跑的demo变成一套能扛住流量、能持续迭代、能被团队协作维护的系统。适合谁看如果你已经会用PyTorch或TensorFlow跑通几个demo但一到部署、监控、版本管理就抓瞎那这个项目就是为你准备的。如果你是完全零基础的小白也不用慌它从最基础的环境搭建讲起只是节奏会比较快需要你边看边动手。我之所以对这个标题感兴趣是因为过去几年我见过太多团队在AI工程化上踩坑。模型在实验室里准确率95%上线之后掉到70%都不到数据管道今天能跑明天就崩模型版本管理靠文件名区分最后谁也说不清线上跑的是哪个版本。这些问题不是算法问题是工程问题。而ai-engineering-from-scratch这个项目恰恰是冲着这些工程问题去的。它的核心价值在于把AI工程化拆解成可学习、可复现、可落地的模块从数据版本控制、实验追踪、模型打包、服务部署到监控告警、CI/CD流水线每一个环节都有对应的工具选型和实操方案。接下来我会按照这个项目的逻辑主线把每个环节的核心思路、工具选型理由、实操要点和我自己踩过的坑逐一拆开来讲。2. 核心模块拆解与工具选型逻辑2.1 为什么AI工程化不能直接套用传统软件工程很多人第一反应是AI工程不就是软件工程加个模型吗用Git管代码用Docker打包用K8s部署不就完了我一开始也这么想直到被现实反复教育。传统软件工程的核心假设是代码即逻辑逻辑确定则行为确定。你写一个排序函数输入相同输出永远相同。但AI系统不一样它的行为由三部分共同决定代码、数据、模型权重。代码可以版本控制数据呢模型权重呢这三个东西任意一个变了系统行为就变了。更麻烦的是数据是持续变化的模型是需要重新训练的这就导致AI系统的“版本”概念比传统软件复杂得多。ai-engineering-from-scratch在开篇就强调了这一点AI工程化的第一原则是“可复现”。什么叫可复现给定一个时间点你能精确还原出当时线上跑的是哪份代码、哪份数据、哪个模型权重、哪套配置参数。听起来简单做起来极难。我见过一个团队模型效果突然下降排查了三天才发现是上游数据源某个字段的格式悄悄变了而他们没有任何数据版本记录只能靠翻聊天记录去猜。所以这个项目的工具选型逻辑始终围绕“可复现”这个核心目标展开。下面这张表是我根据项目内容整理的对比了传统软件工程和AI工程化在关键环节上的差异维度传统软件工程AI工程化版本控制对象代码代码 数据 模型权重 配置测试重点功能正确性功能正确性 数据质量 模型效果部署单元服务二进制/容器服务 模型文件 特征管道监控重点延迟、错误率、资源上述全部 数据漂移 模型衰减迭代周期按需发布数据更新触发 模型重训触发这张表不是学术分类是我在实际项目中总结出来的。每次我觉得“这次应该没问题了”就会有一个新的维度冒出来打脸。比如数据漂移传统软件根本不存在这个概念但在AI系统里它是导致线上效果衰减的头号杀手。2.2 数据版本控制DVC为什么比Git LFS更合适项目里第一个实操模块是数据版本控制。很多人第一反应是用Git LFS毕竟Git用熟了。但ai-engineering-from-scratch明确推荐了DVCData Version Control理由很实在Git LFS管的是大文件DVC管的是数据管道。这两者的区别在哪Git LFS本质上还是Git它只是把大文件存在别的地方版本历史还是Git那套。但数据版本控制的需求不止于此你需要知道这份数据是从哪个源、经过哪些处理步骤、用什么参数生成的。DVC的做法是它不直接存数据而是存一个.dvc文件里面记录了数据的哈希值和生成它的命令。你dvc repro一下它就能根据依赖关系重新生成数据。我实测下来的感受是DVC的学习曲线比Git LFS陡但一旦跑通收益巨大。举个例子你有一个数据清洗脚本处理100万条数据要跑2小时。某天你改了清洗逻辑想对比新旧数据训练出来的模型效果。用Git LFS你得手动存两份数据占双倍空间。用DVC你只需要切换一下版本它自动判断哪些步骤需要重跑哪些可以复用缓存。我试过一个场景改了最后一步的过滤阈值DVC只重跑了最后一步前面90%的耗时都省了。注意DVC的缓存目录默认在项目根目录的.dvc/cache这个目录会随着版本增多越来越大。建议在项目初期就配置好远程存储比如S3兼容的对象存储把缓存推上去本地只保留当前版本。我踩过的坑是本地磁盘被缓存撑爆清理的时候不小心把正在用的缓存删了导致整个管道重新跑了一遍。2.3 实验追踪MLflow的定位与轻量替代方案实验追踪是AI工程化里最容易被低估的环节。很多人觉得我拿个Excel记一下不就行了我一开始也这么干直到实验数量超过50个Excel彻底失控。ai-engineering-from-scratch推荐的是MLflow但同时也提到了Weights Biases和Neptune作为备选。MLflow的核心优势是开源、可自托管、与框架无关。它主要解决四个问题参数记录、指标记录、模型存储、模型注册。你可以把它理解成一个“实验的Git”每次跑实验它自动记录你用了什么超参数、跑了什么指标、产出了什么模型。最实用的是它的模型注册功能你可以给模型打上“Staging”“Production”这样的标签部署的时候直接按标签拉取不用再靠文件名去猜。但MLflow也不是没有缺点。它的UI比较朴素多人协作时的权限管理也比较弱。如果你的团队规模在5人以下MLflow足够用。如果超过10人或者需要更精细的权限控制和更漂亮的可视化可以考虑Weights Biases。不过WB是商业产品免费版有额度限制。我个人的建议是先用MLflow跑通流程等真正遇到瓶颈了再考虑迁移。过早引入商业工具反而会增加学习成本和迁移成本。这里有一个我实际用过的MLflow最小配置可以直接抄import mlflow import mlflow.sklearn mlflow.set_tracking_uri(http://localhost:5000) mlflow.set_experiment(my-first-experiment) with mlflow.start_run(): mlflow.log_param(learning_rate, 0.01) mlflow.log_param(max_depth, 5) # 训练代码... mlflow.log_metric(accuracy, 0.95) mlflow.log_metric(f1_score, 0.93) mlflow.sklearn.log_model(model, model)跑完mlflow ui打开浏览器就能看到所有实验的对比。我特别喜欢它的平行坐标图能直观看出哪些参数组合效果最好。2.4 模型打包与服务化为什么选FastAPI而不是Flask模型训练完下一步是把它变成API。项目里对比了Flask、FastAPI和TorchServe最终推荐FastAPI。理由有三点原生异步支持、自动生成API文档、Pydantic数据校验。Flask是同步框架虽然也能用但在高并发场景下性能不如FastAPI。TorchServe是专门为PyTorch模型设计的功能很全但如果你用的不是PyTorch或者模型逻辑比较复杂比如需要预处理和后处理TorchServe的定制成本反而更高。FastAPI则是一个通用Web框架你可以把模型推理逻辑当成普通的业务逻辑来写灵活度最高。我实测过一个场景同一个模型分别用Flask和FastAPI部署在100并发下的表现。FastAPI的P99延迟比Flask低了约30%而且CPU占用更稳定。当然这个数据不是绝对的跟模型复杂度和硬件都有关系。但FastAPI的异步特性在处理IO密集型任务比如调用外部特征服务时优势明显。项目里给出的FastAPI最小示例是这样的from fastapi import FastAPI from pydantic import BaseModel import joblib app FastAPI() model joblib.load(model.pkl) class PredictRequest(BaseModel): features: list[float] class PredictResponse(BaseModel): prediction: float model_version: str app.post(/predict, response_modelPredictResponse) async def predict(request: PredictRequest): prediction model.predict([request.features])[0] return PredictResponse(predictionprediction, model_versionv1.0)这个示例虽然简单但包含了几个关键设计请求体校验、响应体结构化、模型版本标识。特别是模型版本标识很多人会忽略但它在排查问题时极其重要。线上出问题了你第一件事就是确认当前跑的是哪个版本。3. 实操流程从零搭建一条完整的AI工程流水线3.1 环境准备与项目初始化项目的第一步是环境准备。ai-engineering-from-scratch推荐用Conda管理Python环境用Poetry管理依赖。我一开始觉得这俩功能重叠后来发现分工不同Conda管的是Python版本和系统级依赖比如CUDAPoetry管的是项目级的Python包依赖。两者配合使用能避免很多“在我机器上能跑”的问题。具体操作步骤安装Miniconda比完整版Anaconda轻量没有预装一堆用不上的包创建环境conda create -n ai-eng python3.10激活环境conda activate ai-eng安装Poetrypip install poetry初始化项目poetry init按提示填写项目信息添加依赖poetry add fastapi uvicorn scikit-learn mlflow dvc这里有个细节Poetry默认会创建虚拟环境但如果你已经在Conda环境里它会复用当前环境。我建议在poetry config里设置virtualenvs.create false避免环境嵌套带来的混乱。项目结构建议这样组织ai-engineering-from-scratch/ ├── data/ │ ├── raw/ │ └── processed/ ├── src/ │ ├── data/ │ │ └── make_dataset.py │ ├── features/ │ │ └── build_features.py │ ├── models/ │ │ ├── train.py │ │ └── predict.py │ └── api/ │ └── main.py ├── tests/ ├── dvc.yaml ├── pyproject.toml └── README.md这个结构不是随便定的。data/目录被DVC管理src/目录被Git管理dvc.yaml定义数据管道pyproject.toml定义依赖。每个目录的职责清晰新人接手时能快速定位。3.2 数据管道搭建DVC实操详解数据管道是AI工程化的地基。ai-engineering-from-scratch用DVC来定义管道核心是dvc.yaml文件。我拿一个实际项目举例假设你要做一个房价预测模型数据管道分三步下载数据、清洗数据、生成特征。dvc.yaml这样写stages: download: cmd: python src/data/download.py deps: - src/data/download.py outs: - data/raw/housing.csv clean: cmd: python src/data/clean.py deps: - src/data/clean.py - data/raw/housing.csv outs: - data/processed/housing_clean.csv features: cmd: python src/features/build.py deps: - src/features/build.py - data/processed/housing_clean.csv outs: - data/processed/housing_features.csv这个配置的关键在于deps和outs的声明。DVC会根据这些声明自动构建依赖图。当你运行dvc repro时它会检查每个阶段的依赖是否变化只重跑受影响的部分。我实测过一个场景改了clean.py里的一个过滤条件DVC只重跑了clean和features两个阶段download阶段直接复用缓存省了十几分钟。实操心得dvc repro默认只重跑变化的部分但如果你改了dvc.yaml本身比如加了新阶段需要先运行dvc stage add或者手动更新依赖图。我踩过的坑是改了dvc.yaml之后直接dvc repro结果DVC没识别到新阶段白跑了一遍。3.3 模型训练与实验追踪集成数据管道跑通之后下一步是训练模型并记录实验。项目里把MLflow集成到了训练脚本中每次训练自动记录参数、指标和模型。我按照这个思路写了一个训练脚本核心逻辑如下import mlflow import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, r2_score mlflow.set_tracking_uri(http://localhost:5000) mlflow.set_experiment(housing-price) df pd.read_csv(data/processed/housing_features.csv) X df.drop(price, axis1) y df[price] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) with mlflow.start_run(): n_estimators 100 max_depth 10 mlflow.log_param(n_estimators, n_estimators) mlflow.log_param(max_depth, max_depth) model RandomForestRegressor(n_estimatorsn_estimators, max_depthmax_depth) model.fit(X_train, y_train) y_pred model.predict(X_test) mse mean_squared_error(y_test, y_pred) r2 r2_score(y_test, y_pred) mlflow.log_metric(mse, mse) mlflow.log_metric(r2, r2) mlflow.sklearn.log_model(model, model)这个脚本跑一次MLflow就会记录一次实验。跑个十几次之后打开MLflow UI你能直观看到不同参数组合的效果对比。我特别喜欢它的“Compare”功能可以勾选多个实验并排看参数和指标差异。这里有个细节MLflow的模型存储路径默认是本地文件系统。如果你在容器里跑训练容器一销毁模型就没了。建议在mlflow.set_tracking_uri里配置远程存储或者用mlflow.sklearn.log_model的registered_model_name参数把模型注册到模型仓库。3.4 API服务开发与模型加载模型训练好之后下一步是把它变成API。项目里用FastAPI搭建了一个简单的预测服务。我按照这个思路结合自己的经验写了一个更完整的版本from fastapi import FastAPI, HTTPException from pydantic import BaseModel import mlflow import numpy as np app FastAPI(titleHousing Price Predictor) model None model_version unknown app.on_event(startup) async def load_model(): global model, model_version model_uri models:/housing-price/Production model mlflow.sklearn.load_model(model_uri) model_version model_uri.split(/)[-1] class PredictRequest(BaseModel): features: list[float] class PredictResponse(BaseModel): prediction: float model_version: str app.post(/predict, response_modelPredictResponse) async def predict(request: PredictRequest): if model is None: raise HTTPException(status_code503, detailModel not loaded) try: features np.array(request.features).reshape(1, -1) prediction model.predict(features)[0] return PredictResponse(predictionfloat(prediction), model_versionmodel_version) except Exception as e: raise HTTPException(status_code400, detailstr(e)) app.get(/health) async def health(): return {status: ok, model_version: model_version}这个版本比项目里的示例多了几个关键设计启动时加载模型、健康检查接口、异常处理。启动时加载模型是为了避免每次请求都去拉模型健康检查接口是为了配合K8s的存活探针异常处理是为了避免模型报错时直接把堆栈暴露给客户端。注意app.on_event(startup)在FastAPI的新版本里已经被标记为废弃推荐用lifespan事件。不过目前还能用如果你的FastAPI版本比较新建议改成lifespan写法。3.5 容器化与部署配置API写好了下一步是打包成容器。项目里给出了一个Dockerfile模板我根据自己的经验做了优化FROM python:3.10-slim WORKDIR /app RUN apt-get update apt-get install -y --no-install-recommends \ build-essential \ rm -rf /var/lib/apt/lists/* COPY pyproject.toml poetry.lock ./ RUN pip install poetry \ poetry config virtualenvs.create false \ poetry install --no-dev --no-interaction --no-ansi COPY src/ ./src/ COPY models/ ./models/ EXPOSE 8000 CMD [uvicorn, src.api.main:app, --host, 0.0.0.0, --port, 8000]这个Dockerfile有几个优化点用slim基础镜像减小体积、分层复制利用缓存、只安装生产依赖。我实测下来这个镜像大概300MB左右比直接用完整版Python镜像小了将近一半。部署的时候项目推荐用K8s但如果你只是小规模使用Docker Compose也够用。我个人的建议是先用Docker Compose跑通等真正需要弹性伸缩了再上K8s。过早引入K8s运维成本会吃掉你大部分精力。4. 常见问题与排查技巧实录4.1 数据管道常见报错与解决DVC用起来很爽但报错的时候也挺让人头疼。我整理了几个高频问题和解决方法报错信息原因解决方法ERROR: failed to reproduce xxx: missing dependencies依赖文件不存在或路径写错检查dvc.yaml里的deps路径确保文件存在ERROR: output xxx is already tracked by DVC输出文件已经被DVC管理先dvc remove再重新dvc addERROR: unexpected error - [Errno 2] No such file or directory缓存目录被清理或远程存储未配置运行dvc pull拉取缓存WARNING: cache xxx not found本地缓存缺失运行dvc fetch或dvc pull我踩过最坑的一个问题是在dvc.yaml里写了相对路径但在不同目录下运行dvc repro相对路径的基准目录不一样导致找不到文件。建议统一在项目根目录运行DVC命令路径也统一用相对于根目录的写法。4.2 MLflow实验记录丢失的排查思路MLflow用久了偶尔会遇到实验记录丢失的情况。我遇到过两次一次是本地文件系统满了一次是数据库连接断了。排查思路如下先看MLflow服务是否正常curl http://localhost:5000/health再看后端存储是否可写如果是文件存储检查磁盘空间如果是数据库检查连接最后看实验是否被误删MLflow的删除是软删除可以通过API恢复实操心得MLflow的默认文件存储路径是./mlruns这个目录会随着实验增多越来越大。建议定期清理不再需要的实验或者配置数据库后端。我现在的做法是每个季度归档一次旧实验把mlruns目录打包存到对象存储本地只保留最近三个月的。4.3 模型服务性能瓶颈的定位方法API上线之后最怕的就是性能瓶颈。我总结了一套定位方法按顺序排查先看模型推理耗时在代码里加时间戳记录model.predict的耗时。如果推理本身就慢那优化方向是模型压缩或换更快的模型。再看预处理耗时特征工程往往比推理还耗时。如果预处理慢考虑把预处理逻辑移到训练管道里线上只做轻量转换。最后看网络和序列化耗时请求体太大、JSON序列化慢都会影响延迟。可以用orjson替代标准库的json序列化速度能快好几倍。我实测过一个场景一个模型推理只要10ms但API响应要200ms。排查后发现是特征预处理里有一个循环每次请求都要遍历一个列表做归一化。把归一化参数提前算好存下来响应时间直接降到30ms。4.4 模型版本管理与回滚策略模型版本管理是AI工程化里最容易出事的环节。我见过太多团队靠文件名区分版本最后谁也说不清线上跑的是哪个。ai-engineering-from-scratch推荐用MLflow的模型注册功能给模型打标签部署时按标签拉取。我的做法是每次训练完把模型注册到MLflow打上Staging标签。经过验证后再改成Production标签。部署服务启动时从Production标签拉取模型。回滚的时候只需要把旧版本的标签改回Production重启服务即可。这个流程的关键是标签切换要快服务重启要快。如果服务启动要几分钟回滚就失去了意义。所以我在设计API服务时会把模型加载做成懒加载启动时只加载模型元数据第一次请求时才真正加载模型权重。这样服务重启只要几秒钟回滚速度大大提升。5. 我在这条路上踩过的坑和总结的经验5.1 不要过早追求“完美架构”我刚开始做AI工程化的时候总想一步到位DVC、MLflow、FastAPI、K8s、Prometheus全套上齐。结果花了两个月搭架子真正训练模型的时间不到一周。后来我学乖了先用最简方案跑通全流程再逐步替换瓶颈环节。最简方案是什么数据用文件夹管理实验用Excel记录模型用pickle保存服务用Flask写个接口部署用nohup跑在服务器上。这套方案虽然土但能让你在一天之内跑通“数据到服务”的全流程。跑通之后你才知道瓶颈在哪再针对性地引入工具。5.2 监控比训练更重要很多人把精力全花在训练上觉得模型效果好就万事大吉。但线上系统出问题往往不是模型本身的问题而是数据的问题。我经历过一次线上事故模型效果突然下降排查后发现是上游数据源某个字段的编码格式变了导致特征提取出错。如果当时有数据质量监控这个问题在数据进入管道时就能被发现。所以我现在做AI工程化监控的优先级高于训练。至少要监控三个东西输入数据的分布、模型输出的分布、推理延迟。输入数据分布变了说明上游有问题输出分布变了说明模型可能失效了推理延迟变了说明系统有瓶颈。5.3 文档和测试是给自己留的后路AI项目的人员流动往往比传统软件项目更频繁。一个人走了他训练的模型、搭的管道、写的脚本如果没文档没测试接手的人基本要从头再来。我现在的习惯是每个数据管道脚本都要有对应的测试每个模型都要有模型卡片每个API都要有接口文档。模型卡片是什么就是一份说明文档记录这个模型是用什么数据训练的、效果指标是多少、适用场景是什么、有什么已知缺陷。听起来很形式主义但真正出问题的时候这份文档能帮你快速定位原因。我见过一个团队模型效果下降后排查了两天最后发现是训练数据里混入了测试集的数据导致指标虚高。如果当时有模型卡片记录数据来源这个问题五分钟就能发现。5.4 小步快跑持续迭代AI工程化不是一次性的项目而是一个持续迭代的过程。不要想着一次把所有事情做完而是每次解决一个痛点每次改进一个环节。今天把数据版本控制加上明天把实验追踪加上后天把监控加上。每次改动都小风险可控出了问题也容易回滚。我现在的节奏是每两周做一次工程化改进每次只改一个环节。改完之后跑一周确认稳定了再改下一个。这样虽然慢但稳。AI工程化最怕的就是大跃进一次性改太多出了问题根本不知道是哪个改动导致的。最后分享一个我最近在用的技巧给每个模型训练任务加一个“数据指纹”。具体做法是在训练脚本里计算训练数据的哈希值把这个哈希值记录到MLflow的参数里。这样当模型效果出问题时你可以快速对比当前数据和训练数据的哈希值判断是不是数据变了。这个技巧帮我省了好几次排查时间强烈推荐你试试。
返回列表