ARTICLE DETAIL

资讯详情

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

从零搭建AI工程能力:数据、实验、服务与评估的完整指南

从零搭建AI工程能力:数据、实验、服务与评估的完整指南 从零搭建AI工程能力这件事我前前后后折腾过好几轮。最早的时候我也觉得搞AI嘛会调个API、能跑通一个demo就算入门了。后来真正在项目里扛过事才发现从能跑到能稳定跑、能被人用、能持续迭代之间隔着一条巨大的鸿沟。这条鸿沟里填的不是模型本身而是工程能力——数据怎么管、实验怎么追踪、服务怎么部署、成本怎么控、效果怎么评估。ai-engineering-from-scratch这个标题说的就是从零开始把这套工程能力搭起来而不是从零训练一个大模型。它适合那些已经会写Python、懂一点机器学习、但一到真实项目就手忙脚乱的人也适合想从算法岗往工程岗转的朋友。下面我把自己踩过的路、绕过的弯、以及最后沉淀下来的一套可复现的做法完整地摊开讲一遍。1. 先搞清楚AI工程到底在工程什么很多人一上来就去学分布式训练、学推理加速方向就偏了。AI工程的核心不是把模型搞得多牛而是让一个AI能力可靠地、可维护地、可度量地服务于真实场景。我把它拆成四块数据工程、实验工程、服务工程、评估工程。这四块缺一块系统就是瘸的。1.1 数据工程别让脏数据毁掉整个链路我见过太多项目模型代码写得漂漂亮亮结果上线后效果崩了一查是训练数据和线上数据分布不一致。数据工程要解决的是数据从哪来、怎么清洗、怎么版本化、怎么保证训练和推理用的是同一套处理逻辑。最容易被忽略的是训练-推理一致性。你在训练时用pandas做了一堆特征变换推理时用另一套Java代码重写了一遍两边逻辑稍微对不上效果就掉。我的做法是把特征处理逻辑抽成一个独立的、可序列化的模块训练和推理都调它。比如用Python写一个FeatureTransformer类训练时fit完把参数存成json推理时加载同一个json。import json import numpy as np class FeatureTransformer: def __init__(self): self.mean None self.std None def fit(self, X): self.mean np.mean(X, axis0).tolist() self.std np.std(X, axis0).tolist() self.std [s if s 1e-8 else 1.0 for s in self.std] def transform(self, X): return (X - np.array(self.mean)) / np.array(self.std) def save(self, path): with open(path, w) as f: json.dump({mean: self.mean, std: self.std}, f) def load(self, path): with open(path, r) as f: params json.load(f) self.mean params[mean] self.std params[std]这段代码看着简单但它解决的是大问题。训练完把save出来的json和模型文件一起打包推理服务加载同一个json一致性就有保障了。数据版本化我推荐用DVC或者直接拿对象存储加命名规范别用data_final_v2_真的最终版.csv这种命名那是灾难。1.2 实验工程没有追踪的实验等于没做我早期做实验全靠Excel记跑了几十组参数之后完全记不清哪组对应哪个结果。后来强制自己上MLflow哪怕本地起一个也行。每次跑实验自动记录参数、指标、模型文件、甚至git commit hash。这样回头看的时候任何一个结果都能追溯到当时的代码和数据状态。实验追踪的关键不是工具而是纪律。你得规定自己任何一次跑实验不管多小都必须记录。我见过有人觉得我就调个学习率不用记结果三天后想复现最佳结果发现忘了当时学习率是多少。MLflow的用法很简单import mlflow mlflow.set_experiment(my-first-ai-project) with mlflow.start_run(): mlflow.log_param(learning_rate, 0.001) mlflow.log_param(batch_size, 32) mlflow.log_metric(val_accuracy, 0.923) mlflow.log_artifact(model.pkl)跑完在UI里就能看到所有实验的对比。这一步的投入产出比极高强烈建议从第一个实验就开始用。1.3 服务工程模型不是拿来供着的是拿来用的模型训练完躺在硬盘里没有任何价值得把它变成别人能调用的服务。这里最朴素的方案是FastAPI包一层但要注意几个坑并发、超时、批处理、版本管理。我最早用Flask写推理服务单线程QPS一高就排队。后来换FastAPI加uvicorn多worker情况好很多。但真正的坑在于模型加载时机。如果你在每个请求里都加载一次模型那性能会惨不忍睹。正确做法是在服务启动时加载一次全局复用。from fastapi import FastAPI import joblib app FastAPI() model None app.on_event(startup) def load_model(): global model model joblib.load(model.pkl) app.post(/predict) def predict(features: list): import numpy as np X np.array(features).reshape(1, -1) pred model.predict(X) return {prediction: pred.tolist()}另外服务一定要有版本号。/v1/predict和/v2/predict可以并存这样新模型上线时老客户端不受影响。这个设计在真实项目里能救命。1.4 评估工程离线指标好看不代表线上好用这是最容易被低估的一块。离线AUC 0.95上线后业务方说没感觉。为什么因为离线评估用的数据集和线上真实分布有偏差或者离线指标和业务指标根本不是一回事。我的经验是离线评估只能做粗筛真正的判断要靠线上A/B测试。但A/B测试成本高所以中间要加一层影子模式——新模型和旧模型同时跑新模型的结果只记录不生效对比一段时间后再决定是否切换。这样风险可控又能拿到真实反馈。2. 从零搭建的最小可行工程骨架说了这么多理念落到实操上一个最小可行的AI工程骨架应该长什么样我按自己的习惯给一个目录结构你可以直接抄。2.1 目录结构设计ai-project/ ├── data/ │ ├── raw/ # 原始数据只读 │ ├── processed/ # 清洗后数据 │ └── features/ # 特征文件 ├── src/ │ ├── data/ # 数据处理脚本 │ ├── features/ # 特征工程 │ ├── models/ # 模型定义与训练 │ ├── evaluation/ # 评估逻辑 │ └── serving/ # 推理服务 ├── experiments/ # 实验记录 ├── configs/ # 配置文件 ├── tests/ # 测试 ├── requirements.txt └── README.md这个结构的好处是职责清晰。数据处理的人只动data/和src/data/建模的人只动src/models/服务的人只动src/serving/。多人协作时不会互相踩脚。2.2 配置管理别把参数写死在代码里我见过太多代码里硬编码learning_rate0.001、batch_size32。改一个参数要翻遍整个文件。正确做法是用配置文件YAML或JSON都行。# configs/train_config.yaml data: train_path: data/processed/train.csv val_path: data/processed/val.csv feature_cols: [f1, f2, f3] model: type: xgboost params: learning_rate: 0.05 max_depth: 6 n_estimators: 200 training: seed: 42 output_dir: experiments/exp_001代码里读配置import yaml def load_config(path): with open(path, r) as f: return yaml.safe_load(f) config load_config(configs/train_config.yaml)这样做的好处是换一组参数不用改代码实验可复现配置本身也能进版本控制。2.3 随机种子可复现性的第一道防线AI工程里最让人抓狂的就是同样的代码跑两次结果不一样。这通常是随机种子没固定。Python的random、numpy的np.random、深度学习框架的随机数生成器都要固定。import random import numpy as np def set_seed(seed42): random.seed(seed) np.random.seed(seed) # 如果用PyTorch # import torch # torch.manual_seed(seed) # torch.cuda.manual_seed_all(seed)注意即使固定了种子某些GPU操作仍然可能有微小差异这是硬件层面的不确定性接受它就好。但至少CPU上的实验应该是完全可复现的。3. 模型训练之外的脏活累活真正做过项目的人都知道训练模型可能只占20%的时间剩下80%都在处理各种工程琐事。这一章专门讲那些没人教但必须会的脏活。3.1 数据校验在训练之前拦住脏数据我踩过最大的坑是一次训练数据里混入了大量空值模型训练没报错但效果奇差。后来我强制在数据加载后加一层校验。import pandas as pd def validate_data(df, required_cols, max_null_ratio0.1): # 检查必需列 missing set(required_cols) - set(df.columns) if missing: raise ValueError(f缺少列: {missing}) # 检查空值比例 for col in required_cols: null_ratio df[col].isnull().mean() if null_ratio max_null_ratio: raise ValueError(f列 {col} 空值比例 {null_ratio:.2%} 超过阈值) # 检查数据量 if len(df) 100: raise ValueError(f数据量过少: {len(df)}) return True这个校验函数看着简单但它能在训练开始前就发现问题省下大量等待训练的时间。校验失败直接抛异常不要静默处理。3.2 训练日志出问题时能查训练过程中的日志要记全每个epoch的loss、指标、学习率、耗时。我习惯用Python的logging模块同时输出到控制台和文件。import logging def setup_logger(log_file): logger logging.getLogger(train) logger.setLevel(logging.INFO) formatter logging.Formatter( %(asctime)s - %(levelname)s - %(message)s ) fh logging.FileHandler(log_file) fh.setFormatter(formatter) logger.addHandler(fh) ch logging.StreamHandler() ch.setFormatter(formatter) logger.addHandler(ch) return logger日志里要包含时间戳这样出问题时能对齐其他系统的日志。另外日志文件要按实验分目录存放别全堆在一个文件里。3.3 模型持久化存什么、怎么存训练完的模型不能只存一个权重文件。我的做法是存一个完整的模型包模型权重文件特征处理器的参数前面说的json训练配置yaml评估指标json训练时的git commit hashimport json import os import subprocess def save_model_package(model, transformer, config, metrics, output_dir): os.makedirs(output_dir, exist_okTrue) # 存模型 import joblib joblib.dump(model, os.path.join(output_dir, model.pkl)) # 存特征处理器 transformer.save(os.path.join(output_dir, transformer.json)) # 存配置 import yaml with open(os.path.join(output_dir, config.yaml), w) as f: yaml.dump(config, f) # 存指标 with open(os.path.join(output_dir, metrics.json), w) as f: json.dump(metrics, f) # 存git hash try: git_hash subprocess.check_output( [git, rev-parse, HEAD] ).decode().strip() except Exception: git_hash unknown with open(os.path.join(output_dir, git_hash.txt), w) as f: f.write(git_hash)这样任何一个模型包都是自包含的拿到就能复现推理结果。这个习惯在团队协作和线上问题排查时价值巨大。4. 把模型变成服务的完整路径模型训练好只是开始让它变成别人能用的服务才是终点。这一章讲从模型文件到线上服务的完整路径。4.1 推理服务的性能优化前面给了FastAPI的基础版本但真实场景下要考虑更多。首先是批处理如果请求量大逐个推理浪费算力可以攒一批一起推。import asyncio from fastapi import FastAPI import numpy as np import joblib app FastAPI() model None batch [] batch_lock asyncio.Lock() BATCH_SIZE 32 BATCH_TIMEOUT 0.05 # 50ms app.on_event(startup) def load_model(): global model model joblib.load(model.pkl) async def process_batch(): global batch async with batch_lock: if not batch: return current_batch batch batch [] X np.array([item[features] for item in current_batch]) preds model.predict(X) for item, pred in zip(current_batch, preds): item[future].set_result(pred.tolist()) app.post(/predict) async def predict(features: list): loop asyncio.get_event_loop() future loop.create_future() async with batch_lock: batch.append({features: features, future: future}) if len(batch) BATCH_SIZE: asyncio.create_task(process_batch()) else: asyncio.create_task(delayed_process()) return {prediction: await future} async def delayed_process(): await asyncio.sleep(BATCH_TIMEOUT) await process_batch()这段代码实现了动态批处理请求来了先攒着攒够32个或者等50ms就一起推。这样能显著提升吞吐量。但要注意批处理会增加单请求延迟需要根据业务场景权衡。4.2 健康检查与优雅关闭线上服务必须有健康检查接口让负载均衡器知道这个实例还活着。app.get(/health) def health(): if model is None: return {status: unhealthy, reason: model not loaded} return {status: healthy}优雅关闭也很重要。服务收到停止信号时要先把正在处理的请求处理完再退出。FastAPI配合uvicorn可以这样import signal import sys app.on_event(shutdown) def shutdown_event(): # 等待正在处理的请求完成 # 实际项目中可以用信号量控制 pass4.3 监控指标服务上线后看什么服务上线不是终点得盯着几个关键指标QPS、延迟P50/P95/P99、错误率、模型输入分布。前三个是通用服务指标最后一个是AI服务特有的。模型输入分布监控特别重要。如果线上输入的特征分布和训练时差异很大模型效果会崩。我习惯在推理服务里记录每个特征的均值和方差定期和训练时的统计对比。import numpy as np class InputMonitor: def __init__(self, window_size1000): self.window_size window_size self.buffer [] def record(self, features): self.buffer.append(features) if len(self.buffer) self.window_size: self.buffer.pop(0) def get_stats(self): if not self.buffer: return None arr np.array(self.buffer) return { mean: arr.mean(axis0).tolist(), std: arr.std(axis0).tolist(), count: len(self.buffer) }定期把这个stats和训练时的统计对比偏差超过阈值就告警。这个机制帮我提前发现过好几次数据管道的问题。5. 评估体系怎么知道模型是真的好模型好不好不能只看一个准确率。这一章讲怎么建立一套靠谱的评估体系。5.1 离线评估的陷阱离线评估最大的陷阱是数据泄漏。比如你用未来数据预测过去或者特征里混入了标签信息。我见过一个项目离线AUC 0.99上线后完全没用一查发现特征里有个字段是是否已转化这明显是标签泄漏。避免数据泄漏的方法严格按时间划分训练集和测试集特征工程只在训练集上fit然后transform测试集。另外要仔细审查每个特征问自己这个特征在预测时刻真的能拿到吗。5.2 线上评估影子模式与A/B测试影子模式前面提过这里给个具体做法。线上请求同时发给新旧两个模型旧模型的结果返回给用户新模型的结果只记录。def predict_with_shadow(features): # 旧模型结果返回给用户 old_pred old_model.predict([features])[0] # 新模型结果只记录 try: new_pred new_model.predict([features])[0] log_shadow_result(features, old_pred, new_pred) except Exception as e: log_shadow_error(e) return old_pred影子模式跑一段时间后对比新旧模型的预测差异和实际效果。如果新模型明显更好再切A/B测试。A/B测试要注意样本量够大、实验周期够长别跑一天就下结论。5.3 业务指标与技术指标的映射技术指标AUC、F1和业务指标点击率、转化率之间往往不是线性关系。我习惯做一个映射表把技术指标的变化翻译成业务语言。技术指标变化业务影响经验值AUC 0.01相对提升1%转化率约0.3%准确率 2%绝对提升人工审核量减少约15%召回率 5%绝对提升漏检率下降约20%这个映射表要根据自己业务的历史数据来建不能照搬。有了它和业务方沟通时就有共同语言了。6. 那些让我熬夜的坑与填坑方法这一章不讲理论只讲我真实踩过的坑和最后怎么解决的。这些经验在官方文档里找不到。6.1 内存泄漏服务跑几天就OOM有一次线上服务跑三天就内存溢出重启。排查了半天发现是全局变量里缓存了所有请求的数据越积越多。AI服务里很容易犯这个错比如把每次请求的特征都append到一个全局list里做监控忘了清理。解决方法用固定大小的环形缓冲区或者定期清理。前面InputMonitor里用window_size限制就是这个道理。另外Python的gc模块可以手动触发垃圾回收但不建议频繁调用会拖慢性能。6.2 数值不稳定推理结果偶尔出现NaN模型推理偶尔返回NaN概率很低但确实存在。排查发现是输入特征里有极端值经过某些变换后溢出。解决方法是在特征处理时加裁剪。def safe_transform(x, lower-1e6, upper1e6): x np.clip(x, lower, upper) return x另外推理服务里要对输出做校验发现NaN就返回默认值或错误码别把NaN传给下游。6.3 版本不一致训练和推理的库版本不同训练时用sklearn 1.0推理环境是sklearn 0.24模型加载直接报错。这个坑很隐蔽因为本地测试时环境是一样的上线才暴露。解决方法用requirements.txt锁定版本训练和推理用同一个基础镜像。如果做不到至少在模型包里记录训练时的库版本推理服务启动时校验。import sklearn import json REQUIRED_SKLEARN 1.0.2 def check_version(): if sklearn.__version__ ! REQUIRED_SKLEARN: raise RuntimeError( fsklearn版本不匹配: 需要{REQUIRED_SKLEARN}, f当前{sklearn.__version__} )6.4 并发下的模型状态污染有些模型在推理时会修改内部状态比如某些在线学习模型多线程并发时会互相干扰。解决方法要么给每个线程独立的模型副本要么加锁。加锁简单但会降低并发副本占用内存但并发好。我一般选副本因为AI服务通常内存够用。import threading class ModelPool: def __init__(self, model_path, pool_size4): self.models [ joblib.load(model_path) for _ in range(pool_size) ] self.lock threading.Lock() self.index 0 def get_model(self): with self.lock: model self.models[self.index] self.index (self.index 1) % len(self.models) return model这个模型池的思路是准备多个模型副本每次请求轮询取一个。这样既避免了状态污染又不需要加锁等待。7. 从能跑到能扛工程化的进阶方向当你把上面这些基础都跑通之后可以往几个方向进阶。这些不是必须的但能让你的系统更稳、更快、更省。7.1 特征存储让特征复用成为可能当项目多了之后你会发现很多特征在不同项目里重复计算。特征存储Feature Store就是解决这个问题的。它把特征的计算逻辑、存储、服务统一管理起来。开源方案有Feast云厂商也有对应产品。特征存储的核心价值是训练-推理一致性和特征复用。但引入它也有成本小项目没必要上。我的建议是当你有三个以上项目需要共享特征时再考虑。7.2 模型注册与灰度发布模型注册中心Model Registry管理模型的所有版本记录每个版本的状态训练中、已评估、已上线、已下线。MLflow的Model Registry就能做这个。灰度发布是模型上线的标准做法新模型先接1%流量观察指标正常后逐步扩大到10%、50%、100%。任何一步指标异常就自动回滚。这套机制能极大降低上线风险。7.3 成本优化推理成本可能比训练还高很多人只关注训练成本忽略了推理成本。一个模型上线后每天推理几百万次成本可能远超训练。优化方向有几个模型量化把float32换成int8模型体积减半推理速度提升精度损失通常很小。模型蒸馏用大模型教小模型小模型推理快、成本低。缓存对相同输入缓存结果适合输入重复率高的场景。批处理前面讲过的动态批处理提升吞吐量。我做过一个项目通过量化和批处理推理成本降了60%精度只掉了0.3%。这个投入产出比非常划算。7.4 自动化流水线从手动到自动最开始我都是手动跑脚本拉数据、训练、评估、部署。后来发现太容易出错就搭了自动化流水线。每次代码合并到主分支自动触发数据校验、训练、评估、如果指标达标就自动部署到测试环境。工具上小项目用GitHub Actions或GitLab CI就够了大项目可以用Airflow或Kubeflow。关键不是工具而是把流程固化下来减少人为操作。# .github/workflows/train.yml 示例 name: Train and Evaluate on: push: branches: [main] jobs: train: runs-on: ubuntu-latest steps: - uses: actions/checkoutv2 - name: Setup Python uses: actions/setup-pythonv2 with: python-version: 3.9 - name: Install deps run: pip install -r requirements.txt - name: Validate data run: python src/data/validate.py - name: Train run: python src/models/train.py --config configs/train_config.yaml - name: Evaluate run: python src/evaluation/evaluate.py这个流水线跑通之后我基本不用手动干预训练了省下大量时间。8. 给不同阶段的人的一些实在建议最后这部分我想按不同阶段给点具体建议不是空话是我自己走过之后觉得最有用的。如果你刚开始接触AI工程别贪多。先把数据校验、随机种子、实验追踪这三件事做好。这三件事投入最小收益最大。很多人跳过这三步直接去搞分布式训练结果基础不牢后面全是坑。如果你已经能跑通完整流程但系统不稳定重点看服务工程那一章。健康检查、优雅关闭、监控指标、批处理这几个加上去稳定性会有质的提升。我自己的服务从三天一崩到稳定跑几个月就是靠这些。如果你在带团队重点建规范和流水线。代码规范、实验记录规范、上线流程规范这些比技术本身更重要。一个团队如果没有规范每个人一套做法协作成本会高到无法承受。如果你在控制成本先做量化和批处理这两个见效最快。然后再考虑模型蒸馏和缓存。成本优化是个持续过程别指望一次做完。还有一点很重要别过度设计。我见过有人给一个日请求量几百的服务上了Kubernetes加服务网格运维复杂度远超收益。工程化的程度要匹配业务规模小步快跑按需演进。你今天觉得用不上的东西可能明天就需要了但也可能永远用不上。先解决眼前的问题别为想象中的问题提前买单。这套东西我前后迭代了两年多中间推翻重来过好几次。现在回头看最大的心得是AI工程没有银弹就是把一件件小事做扎实。数据校验、日志、版本管理、监控这些看着不起眼的东西才是系统能扛住真实流量的根本。模型再牛工程跟不上也就是个demo。反过来模型一般但工程扎实系统反而能稳定创造价值。这个道理我是在踩了无数坑之后才真正明白的。
返回列表