ARTICLE DETAIL

资讯详情

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

从零搭建AI工程体系:数据、训练、服务全链路实操指南

从零搭建AI工程体系:数据、训练、服务全链路实操指南 1. 从零搭建AI工程体系为什么我劝你别一上来就调包ai-engineering-from-scratch这个标题第一次看到的时候我愣了一下。不是因为陌生恰恰相反是因为太熟悉了——过去两年我见过太多人抱着从零开始学AI工程的念头冲进来结果三天后就开始复制粘贴别人的训练脚本两周后连自己模型为什么收敛都说不清楚。这个标题背后藏着的其实是一个被严重低估的问题AI工程的门槛从来不在调包而在于你有没有真正理解数据、模型、训练、部署这条链路上每一个环节的因果关系。我自己是从传统后端转过来的最早做推荐系统那会儿连梯度下降都推不利索全靠sklearn的fit和predict混日子。后来被一个线上事故逼着啃了三个月底层才发现之前写的所谓AI工程代码本质上就是在黑盒外面套了一层壳。所以当我看到ai-engineering-from-scratch这个方向时第一反应不是又一个教程而是终于有人愿意把这条脏活累活的路径讲清楚了。这篇文章想做的事情很具体把AI工程从零搭建的完整路径拆开告诉你每一步为什么这么做、不这么做会踩什么坑、以及在实际操作中哪些环节最容易翻车。适合谁看如果你已经会写Python、懂一点线性代数和概率论但面对一个真实的AI项目不知道从哪下手那这篇内容就是给你准备的。如果你已经是老手也可以看看我在数据版本管理、训练可复现性、推理性能优化这几个环节的实操记录说不定能帮你省下几个通宵。核心关键词ai-engineering-from-scratch我会在全文反复提到但不会硬塞。我更想让你读完之后的感受是原来从零搭一套AI工程体系是这么一回事。2. 整体设计思路为什么我不建议你从模型开始2.1 先搞清楚AI工程和算法研究的边界很多人把AI工程和算法研究混为一谈这是第一个大坑。算法研究的目标是刷高指标AI工程的目标是让一个模型在真实业务场景里稳定、可维护、可迭代地跑起来。这两个目标的差异决定了你从零搭建时的优先级完全不同。我见过一个团队花两个月调出了一个在公开数据集上F1值0.92的模型结果上线第一天就崩了——因为线上数据的分布和训练集差了十万八千里而且他们没有做任何数据监控。这就是典型的研究思维做工程。从零搭建AI工程体系第一件事不是选模型而是定义清楚你的输入输出契约、数据流转路径、以及失败时的降级策略。具体来说我会把整个体系分成五层数据层、特征层、训练层、服务层、监控层。每一层都有独立的职责和接口层与层之间通过明确的契约通信。这样做的好处是当你的模型需要从XGBoost换成Transformer时只需要改训练层和服务层的适配代码数据层和特征层几乎不用动。2.2 技术选型的核心逻辑可控性优先于先进性从零搭建的时候最容易犯的错就是追新。今天看到某个新框架出来了赶紧换明天看到某个新工具火了马上接。结果搭了半年体系没成型技术债倒是欠了一堆。我的选型原则很简单在满足性能要求的前提下优先选你能完全掌控的组件。举个例子模型服务这块你可以用现成的推理框架也可以自己用FastAPI写一个。如果你的QPS要求不高比如每秒几十次我强烈建议自己写。为什么因为自己写的服务每一行代码你都知道在干什么出问题的时候能快速定位。用现成框架一旦遇到版本兼容或者性能瓶颈排查成本会高得离谱。再比如数据版本管理。很多人一上来就上DVC或者LakeFS但如果你只是一个人做项目数据量在几十GB以内用Git LFS加一套命名规范就足够了。工具越简单你越能把精力放在真正重要的事情上——理解数据和模型的行为。2.3 从零搭建的四个阶段划分我把整个搭建过程分成四个阶段每个阶段有明确的交付物和验收标准阶段核心目标交付物验收标准第一阶段跑通最小闭环一个能训练、能推理的脚本在样例数据上端到端跑通第二阶段建立可复现流程数据版本、配置管理、实验记录同一配置能复现同一结果第三阶段服务化与性能优化推理服务、批处理、缓存满足延迟和吞吐要求第四阶段监控与迭代机制数据漂移检测、模型重训流程能自动发现并响应异常这个划分的好处是你不需要一次性把所有东西都搭好。每个阶段结束后你都有一个能用的东西而不是一堆半成品。我见过太多人想一步到位结果三个月后连一个能跑的demo都没有。提示第一阶段千万不要追求完美。你的目标是在最短时间内让数据流过整个链路哪怕模型用的是逻辑回归哪怕服务用的是Flask自带的开发服务器。先跑通再优化。3. 核心细节解析数据、训练、服务三个环节的实操要点3.1 数据层从零搭建时最容易被忽视的环节数据层是AI工程的地基但也是最容易被跳过的一环。很多人拿到数据就直接往模型里塞结果训练集和测试集有重叠、类别极度不平衡、特征泄漏这些问题一个接一个。从零搭建数据层我建议你至少做四件事第一建立数据字典。每一个字段的含义、类型、取值范围、缺失率、更新频率都要记录清楚。这个字典不是给别人看的是给你自己三个月后看的。我吃过亏一个项目隔了两个月回来改完全不记得某个字段的0和1代表什么翻代码翻了半天。第二做数据质量检查。至少包括缺失值比例、异常值检测、类别分布、时间分布。这些检查要写成脚本每次数据更新都跑一遍。我一般用pandas-profiling生成一份报告然后针对关键字段写断言。第三划分数据集要讲究策略。如果是时序数据绝对不能随机划分必须按时间切分。如果是分类任务要保证训练集和测试集的类别分布一致。我通常会用分层抽样并且保留一个独立的验证集用于调参。第四数据版本管理。哪怕你只用Git LFS也要给每次数据更新打上标签。我的做法是数据文件用日期加哈希命名比如train_20240115_a3f2c1.parquet然后在配置文件中引用这个文件名。这样任何时候都能追溯到用的是哪份数据。# 一个简单的数据质量检查脚本示例 import pandas as pd def check_data_quality(df, target_col): report {} report[missing_rate] df.isnull().mean().to_dict() report[target_distribution] df[target_col].value_counts(normalizeTrue).to_dict() report[numeric_summary] df.describe().to_dict() # 检查类别不平衡 target_dist df[target_col].value_counts(normalizeTrue) if target_dist.min() 0.05: print(f警告类别不平衡最小类别占比 {target_dist.min():.2%}) return report3.2 训练层可复现性是底线不是加分项训练层最核心的要求只有一个可复现。同一份数据、同一份配置、同一个随机种子必须得到同一个模型。做不到这一点后面所有的实验对比都是空中楼阁。从零搭建训练层你需要控制三个东西随机种子、配置管理、实验记录。随机种子这块Python的random、numpy的random、框架的random比如PyTorch的torch.manual_seed都要设置。如果用了GPU还要设置CUDA的种子。我一般会写一个set_seed函数在训练脚本开头调用。配置管理我推荐用YAML文件把所有超参数、数据路径、模型结构都写进去。不要用argparse传一堆参数那样实验记录很难管理。YAML文件加上Git的版本控制就能保证任何时候都能回到某个实验的配置。实验记录这块小规模可以用CSV或者SQLite大规模可以用MLflow或者Weights Biases。我个人的习惯是每个实验一个文件夹里面放配置文件、训练日志、模型权重、评估结果。文件夹命名用实验名_日期_哈希的格式。# config.yaml 示例 data: train_path: data/train_20240115_a3f2c1.parquet valid_path: data/valid_20240115_a3f2c1.parquet target_col: label model: type: xgboost params: max_depth: 6 learning_rate: 0.05 n_estimators: 500 training: seed: 42 batch_size: 1024 epochs: 50 early_stopping_rounds: 10注意配置文件里不要写绝对路径。用相对于项目根目录的路径这样换一台机器也能跑。3.3 服务层推理性能优化的三个关键点服务层是从零搭建时最能体现工程能力的地方。很多人模型训得好好的一上线就崩问题基本都出在服务层。第一个关键点是批处理。单条推理的效率极低尤其是深度学习模型。如果你的业务场景允许微批比如每10毫秒攒一批请求一定要做批处理。我实测下来批大小从1提到32吞吐量能提升10倍以上延迟只增加几毫秒。第二个关键点是缓存。很多请求的特征是重复的尤其是推荐和搜索场景。在服务层加一层LRU缓存命中率往往能到30%以上。缓存key用特征的哈希值注意要设置合理的过期时间。第三个关键点是降级策略。模型服务不可能永远不出问题当模型推理超时或者返回异常时要有兜底方案。最简单的降级是返回一个默认结果比如热门商品好一点的降级是切换到备用模型比如轻量级的LRM。# 一个带批处理和缓存的推理服务示例 from fastapi import FastAPI from functools import lru_cache import numpy as np app FastAPI() lru_cache(maxsize10000) def cached_predict(feature_hash): # 实际推理逻辑 pass app.post(/predict) async def predict(request: dict): features request[features] feature_hash hash(tuple(features)) # 先查缓存 cached cached_predict(feature_hash) if cached is not None: return {result: cached, source: cache} # 批处理逻辑简化示意 result model.predict(np.array([features])) return {result: result.tolist(), source: model}4. 完整实操流程从空文件夹到可服务的AI系统4.1 项目结构初始化与依赖管理从零开始的第一步是建立一个清晰的项目结构。我用了很多年的结构是这样的ai-project/ ├── configs/ # 配置文件 ├── data/ # 数据目录不纳入Git ├── src/ │ ├── data/ # 数据处理代码 │ ├── features/ # 特征工程代码 │ ├── models/ # 模型定义 │ ├── training/ # 训练脚本 │ └── serving/ # 服务代码 ├── experiments/ # 实验记录 ├── tests/ # 测试代码 ├── requirements.txt └── README.md依赖管理我推荐用requirements.txt加虚拟环境。不要用全局Python环境否则不同项目的依赖会打架。虚拟环境用venv就够了简单直接。如果团队协作可以考虑poetry但一个人做项目没必要。# 初始化项目 mkdir ai-project cd ai-project python -m venv venv source venv/bin/activate # Windows用 venv\Scripts\activate pip install pandas numpy scikit-learn xgboost fastapi uvicorn pyyaml pip freeze requirements.txt4.2 数据处理流水线的搭建数据处理流水线的核心目标是从原始数据到模型输入每一步都可追溯、可复现。我一般会把流水线拆成三个脚本prepare_data.py、build_features.py、split_data.py。prepare_data.py负责读取原始数据、做基础清洗去重、处理缺失值、类型转换。这个脚本的输出是一份干净的中间数据存成parquet格式。build_features.py负责特征工程。所有的特征变换逻辑都写在这里包括归一化、编码、交叉特征。注意归一化的参数均值和方差必须从训练集计算然后应用到验证集和测试集否则就是特征泄漏。split_data.py负责划分数据集。时序数据按时间切非时序数据用分层抽样。划分结果要保存成独立的文件并且记录划分的随机种子。# build_features.py 的核心逻辑 import pandas as pd from sklearn.preprocessing import StandardScaler import joblib def build_features(train_df, valid_df, test_df, numeric_cols): scaler StandardScaler() # 只在训练集上fit train_df[numeric_cols] scaler.fit_transform(train_df[numeric_cols]) valid_df[numeric_cols] scaler.transform(valid_df[numeric_cols]) test_df[numeric_cols] scaler.transform(test_df[numeric_cols]) # 保存scaler供服务层使用 joblib.dump(scaler, experiments/scaler.pkl) return train_df, valid_df, test_df提示特征工程的代码一定要和服务层的特征处理代码保持一致。我的做法是把特征处理逻辑封装成一个类训练和服务都调用同一个类避免线上线下不一致。4.3 模型训练与超参数搜索的实操记录模型训练这块我的建议是先跑一个基线模型再考虑复杂模型。基线模型用逻辑回归或者决策树目的是验证数据流水线没问题、评估指标计算正确。基线跑通之后再上XGBoost或者深度学习模型。超参数搜索我一般用Optuna比GridSearchCV灵活支持早停和剪枝。搜索空间不要设太大先粗后细。比如学习率先搜[0.01, 0.1, 0.3]找到大致范围后再细化。import optuna import xgboost as xgb from sklearn.metrics import roc_auc_score def objective(trial): params { max_depth: trial.suggest_int(max_depth, 3, 10), learning_rate: trial.suggest_float(learning_rate, 0.01, 0.3, logTrue), n_estimators: trial.suggest_int(n_estimators, 100, 1000), subsample: trial.suggest_float(subsample, 0.6, 1.0), } model xgb.XGBClassifier(**params, early_stopping_rounds10) model.fit(X_train, y_train, eval_set[(X_valid, y_valid)], verboseFalse) preds model.predict_proba(X_valid)[:, 1] return roc_auc_score(y_valid, preds) study optuna.create_study(directionmaximize) study.optimize(objective, n_trials50) print(f最佳参数: {study.best_params}) print(f最佳AUC: {study.best_value:.4f})实测下来50次试验通常能找到接近最优的参数组合。如果数据量特别大可以先用10%的采样数据搜参数再用全量数据训练最终模型。4.4 推理服务的部署与压测推理服务部署这块我用FastAPI加Uvicorn的组合最多。FastAPI的异步特性适合IO密集型的场景Uvicorn的worker数量根据CPU核数来定一般是核数加一。压测用Locust或者wrk。我一般会测三个指标P50延迟、P99延迟、QPS。P99延迟比平均延迟重要得多因为用户体验是由长尾决定的。# 启动服务 uvicorn src.serving.main:app --host 0.0.0.0 --port 8000 --workers 4 # 用wrk压测 wrk -t4 -c100 -d30s http://localhost:8000/predict压测的时候要注意请求的数据要尽量模拟真实分布。我见过有人用全零的特征压测结果QPS高得离谱上线后直接崩了。正确的做法是从验证集里随机采样一批真实特征作为压测输入。5. 常见问题与排查技巧实录5.1 训练不收敛或指标异常的排查思路训练出问题是最常见的我整理了一个排查顺序基本能覆盖80%的情况现象可能原因排查方法loss不下降学习率过大或过小尝试1e-2到1e-5的学习率loss震荡batch size太小增大batch size或减小学习率训练集指标高验证集低过拟合加正则化、早停、更多数据训练集和验证集都低欠拟合增加模型复杂度、更多特征指标突然变差数据问题检查数据分布是否变化我踩过最坑的一次是模型训练集AUC 0.95验证集AUC 0.52。排查了半天发现是特征里有一个字段在验证集里全是缺失值。所以每次训练前都要检查训练集和验证集的特征分布这个习惯帮我省了很多时间。5.2 线上线下效果不一致的定位方法线上线下不一致是AI工程里最头疼的问题之一。原因通常有三个特征不一致、数据分布不一致、模型版本不一致。定位方法很简单在服务层记录每次推理的输入特征和输出结果然后离线用同样的特征跑一遍模型对比结果。如果离线结果和线上不一致那就是服务层的特征处理有问题如果一致但业务指标差那就是数据分布的问题。我一般会在服务层加一个采样日志把1%的请求特征和结果存下来。这个日志不仅能用于排查问题还能作为后续模型重训的数据来源。5.3 性能瓶颈的快速定位技巧服务层性能瓶颈的定位我一般用分层计时的方法。在请求处理的每个关键节点打上时间戳然后统计各阶段的耗时占比。import time def predict_with_timing(features): timings {} t0 time.time() features preprocess(features) timings[preprocess] time.time() - t0 t0 time.time() result model.predict(features) timings[inference] time.time() - t0 t0 time.time() result postprocess(result) timings[postprocess] time.time() - t0 return result, timings实测下来大部分性能问题都出在预处理阶段尤其是特征查询和拼接。如果预处理耗时占比超过50%就要考虑加缓存或者预计算了。注意不要过早优化。先用分层计时找到瓶颈再针对性优化。我见过有人一上来就把模型量化了结果发现瓶颈根本不在推理而在数据库查询。5.4 模型更新与回滚的机制设计模型更新不能直接覆盖线上模型必须有一套灰度发布和回滚机制。我的做法是新模型先跑影子模式也就是接收线上流量但不返回结果对比新老模型的输出差异。差异在可接受范围内再切5%的流量做A/B测试。A/B测试指标正向再逐步放大流量。回滚机制要简单可靠。我一般会保留最近三个版本的模型文件服务层通过配置切换版本。一旦发现问题改配置重启服务就能回滚整个过程不超过一分钟。# 模型版本管理示例 import os class ModelManager: def __init__(self, model_dir): self.model_dir model_dir self.current_version None self.model None def load(self, version): path os.path.join(self.model_dir, fmodel_{version}.pkl) self.model joblib.load(path) self.current_version version def rollback(self): versions sorted(os.listdir(self.model_dir)) if len(versions) 2: previous versions[-2] self.load(previous)这套机制看起来简单但关键时刻能救命。我有一次上线新模型后发现某个重要类别的召回率掉了20%靠回滚机制在五分钟内恢复了服务然后慢慢排查问题。6. 从零搭建AI工程体系的个人体会做AI工程这些年我最大的体会是从零搭建的价值不在于你用了多先进的技术而在于你对每一个环节的理解深度。调包谁都会但知道为什么调这个包、什么时候不该调这个包才是工程能力的体现。ai-engineering-from-scratch这个方向我建议你至少完整走一遍。不要跳过数据层不要跳过服务层不要觉得监控层可有可无。每一个环节你亲手搭过一遍后面用现成工具的时候才知道工具帮你做了什么、没帮你做什么。最后分享一个小技巧每次搭建新项目的时候我都会先写一个README.md把整个体系的架构图画出来把每个模块的职责写清楚。这个README不是给别人看的是给我自己看的。写不清楚的地方往往就是我没想清楚的地方。这个习惯帮我避免了很多返工。如果你也在从零搭建自己的AI工程体系欢迎交流踩坑经验。这个领域没有银弹只有一个个具体的、琐碎的、但必须解决的问题。
返回列表