news 2026/10/3 6:00:53

从零构建AI工程能力:数据、特征、训练与推理全链路实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建AI工程能力:数据、特征、训练与推理全链路实战

1. 从零搭建AI工程能力:这个项目到底在解决什么问题

第一次看到ai-engineering-from-scratch这个标题,我脑子里蹦出来的第一个念头是:又一个教人调包的教程?但仔细琢磨了一下“from scratch”这个限定词,再结合这两年带团队、面候选人的实际感受,我意识到它瞄准的其实是一个很具体的痛点——市面上大量所谓“AI入门”内容,本质上是在教你怎么用现成框架拼装,而不是让你理解一个AI系统从数据到上线到底经历了什么。

我自己在工业界做AI系统落地差不多十年,从早期用传统机器学习做风控,到后来做推荐、做多模态检索,踩过的坑基本都集中在“工程”这两个字上。模型结构本身反而是最不容易出问题的部分,真正让人熬夜的是数据管道断了、特征对不齐、推理延迟抖动、线上指标和离线对不上。所以当我看到这个标题时,第一反应是:如果它真的能做到“from scratch”,那它应该覆盖的是数据工程、训练工程、推理工程、评估工程这一整条链路,而不是只讲模型。

这个项目适合谁?我认为有三类人值得认真看。第一类是刚入行、只会调库的算法工程师,他们需要补上“模型之外”的那一半知识;第二类是从后端或数据方向转过来的工程师,他们有工程底子但缺AI系统的全局观;第三类是想自己动手做一个小型AI产品的人,比如做一个文本分类服务或者图像检索demo,他们需要一条能跑通的最小闭环路径。这篇文章我会按照我自己理解一个AI工程项目的方式,把这条路径拆开讲清楚,包括每一步为什么这么做、参数怎么定、坑在哪里。

2. 整体设计思路:为什么“从零”比“调包”更难也更有价值

2.1 从零构建的核心逻辑:把黑盒拆成可验证的环节

很多人对“from scratch”有误解,以为是要手写反向传播、手写卷积核。其实在工程语境下,from scratch 的核心含义是不依赖高度封装的端到端框架,而是把每个环节单独实现或至少单独验证。举个例子,你可以用 PyTorch 定义模型,但数据加载、特征处理、训练循环、评估指标、推理服务这些部分,你要能说清楚每一步的输入输出是什么、边界条件在哪里。

我之所以强调这一点,是因为在实际工作中,一个AI系统出问题,90%的情况不是模型结构错了,而是某个环节的数据分布变了、某个预处理步骤在训练和推理时不一致、某个指标的计算方式有歧义。如果你一直用高度封装的pipeline,这些问题会被掩盖,直到线上炸了才暴露。从零构建的价值就在于,每个环节都是显式的、可单独测试的。

2.2 技术选型的取舍:为什么我建议从轻量级工具链入手

在工具选型上,我的建议很明确:训练侧用 PyTorch,数据处理用 pandas + numpy 起步,服务侧用 FastAPI,实验管理先用文件系统加命名规范。为什么不推荐一上来就上 Spark、TFX、MLflow 这些重型工具?因为学习阶段最重要的是缩短反馈循环。你用 pandas 处理几万条数据,几秒钟就能看到结果;你用 Spark 处理同样的数据,光环境配置和任务提交就要几分钟,调试成本完全不一样。

这里有个我自己的经验:在数据量小于100万条、特征维度小于1000维的情况下,单机 pandas + numpy 完全够用。我做过一个文本分类项目,80万条样本、5000维稀疏特征,用 pandas 做特征工程加上 PyTorch 训练,单机16核CPU加一张消费级显卡,整个流程跑下来不到20分钟。这个效率对于学习和原型验证来说绰绰有余。等你真的遇到单机扛不住的数据量,再迁移到分布式框架,那时候你对数据流的理解已经足够深,迁移只是换API的事。

2.3 项目模块划分:一条最小但完整的AI工程链路

我把这条链路拆成五个模块,每个模块都有明确的输入输出和验收标准:

模块输入输出验收标准
数据准备原始文件/数据库清洗后的结构化数据无缺失值、类型正确、分布合理
特征工程结构化数据数值化特征矩阵训练/推理一致、无泄漏
模型训练特征矩阵+标签模型权重+训练日志损失收敛、验证集指标达标
评估验证模型+测试集评估报告指标可复现、切片分析完整
推理服务模型+请求预测结果延迟达标、错误可追踪

这张表看起来简单,但每个环节都有大量细节。比如“训练/推理一致”这一条,我见过太多项目栽在这里:训练时用了某个归一化参数,推理时忘了加载;训练时对缺失值填了均值,推理时直接报错。这些问题的根源都是没有把特征工程当成一个独立的、可序列化的组件来对待。

3. 核心细节解析:数据、特征与训练中的关键决策

3.1 数据清洗:先做“体检”再动手

拿到一份原始数据,我的习惯是先做三件事:看形状、看类型、看分布。看形状就是df.shape,知道有多少行多少列;看类型就是df.dtypes,确认数值列是不是被读成了字符串;看分布就是df.describe()加上对类别列的value_counts()。这三步做完,基本能发现80%的数据问题。

举个实际例子。我之前处理一份用户行为日志,timestamp列读进来是字符串,格式是2024-01-15 08:30:00。如果直接丢给模型肯定不行,需要转成数值特征。我的做法是拆成四个特征:小时、星期几、是否周末、距今天数。为什么这么拆?因为小时反映日内模式,星期几反映周内模式,是否周末是二值强特征,距今天数反映时效性。这四个特征加起来,比直接用一个时间戳数值效果好得多,因为树模型对原始时间戳的数值大小没有语义理解。

注意:时间特征拆分时一定要确认时区。我踩过一次坑,训练数据用的是UTC,推理时服务器本地时间是东八区,导致小时特征整体偏移8,线上效果直接掉了一截。后来统一在数据入口处强制转UTC,问题才解决。

3.2 特征工程:训练和推理必须共用同一套代码

这是我最想强调的一点。很多教程把特征工程写在训练脚本里,推理时重新写一遍,这是灾难的根源。正确的做法是把特征处理逻辑封装成一个类或一组函数,训练和推理都调用同一份代码。

具体怎么做?我通常定义一个FeatureProcessor类,包含fit和transform两个方法。fit在训练集上计算统计量(均值、方差、类别编码映射等),transform用这些统计量做转换。训练时先fit再transform,推理时只transform,并且把fit得到的统计量保存成文件,推理服务启动时加载。

class FeatureProcessor: def __init__(self): self.mean = None self.std = None self.category_map = {} def fit(self, df): self.mean = df['age'].mean() self.std = df['age'].std() for col in ['city', 'gender']: self.category_map[col] = {v: i for i, v in enumerate(df[col].unique())} def transform(self, df): df = df.copy() df['age'] = (df['age'] - self.mean) / self.std for col in ['city', 'gender']: df[col] = df[col].map(self.category_map[col]).fillna(-1) return df

这段代码看起来简单,但它解决了一个大问题:推理时的数据转换和训练时完全一致。保存mean、std、category_map这些统计量,本质上就是把训练数据的“状态”固化下来,推理时复现这个状态。

3.3 训练循环:别急着上高级技巧,先把基础打牢

训练循环的核心就四步:前向传播、计算损失、反向传播、更新参数。但就是这四步,有很多细节决定成败。

批次大小的选择:我一般从32或64开始试。太小了梯度噪声大,太大了显存吃紧且泛化可能变差。有个经验公式可以参考:批次大小在32到256之间,学习率相应地在1e-4到1e-2之间调整。如果批次翻倍,学习率也可以适当翻倍,这是线性缩放规则的大致思路。

学习率调度:我习惯用余弦退火或者带热重启的余弦退火。为什么?因为固定学习率很难同时兼顾初期快速下降和后期精细收敛。余弦退火让学习率从初始值平滑降到接近零,后期模型能在局部最小值附近稳定下来。实测下来,同样的模型和數據,余弦退火比固定学习率在验证集上通常能高0.5到1个点。

早停策略:验证集损失连续N个epoch不下降就停。N一般取5到10。这个策略帮我省了大量训练时间,也避免了过拟合。但要注意,早停的“不下降”最好用平滑后的指标,比如移动平均,避免被单次波动误导。

3.4 评估验证:离线指标好不代表线上好

评估环节最容易犯的错误是只看一个总体指标。比如分类任务只看准确率,如果类别不平衡,准确率会严重误导。我通常会看四个东西:混淆矩阵、每个类别的精确率和召回率、AUC、以及按关键维度切片的指标。

切片分析特别重要。举个例子,一个推荐模型整体AUC是0.78,看起来不错。但按用户活跃度切片后发现,高活跃用户AUC是0.82,低活跃用户只有0.65。这说明模型对低活跃用户预测能力差,而低活跃用户恰恰是拉新的重点。如果不做切片,这个问题根本发现不了。

实操心得:切片维度不要太多,选3到5个业务上最关心的维度就够了。每个切片样本量不能太少,少于1000条的切片指标波动太大,参考价值有限。

4. 实操过程:从原始数据到可调用服务的完整实现

4.1 环境准备与依赖管理

我强烈建议用 conda 或 venv 创建独立环境,依赖写进requirements.txt。核心依赖就几个:numpy、pandas、scikit-learn、torch、fastapi、uvicorn、joblib。版本尽量固定,比如torch==2.1.0,避免不同机器上行为不一致。

python -m venv venv source venv/bin/activate pip install numpy pandas scikit-learn torch fastapi uvicorn joblib pip freeze > requirements.txt

这一步看起来简单,但我见过太多人因为环境问题浪费半天。固定版本号能避免“在我机器上能跑”的经典问题。

4.2 数据加载与清洗的实操步骤

假设我们有一份CSV文件raw_data.csv,包含用户ID、年龄、城市、性别、历史行为次数、是否转化六个字段。目标是根据前五个字段预测“是否转化”。

第一步,加载并检查:

import pandas as pd df = pd.read_csv('raw_data.csv') print(df.shape) print(df.dtypes) print(df.isnull().sum()) print(df['是否转化'].value_counts())

第二步,处理缺失值。年龄缺失用中位数填充,城市和性别缺失用“未知”填充。为什么年龄用中位数而不是均值?因为年龄分布通常有偏,中位数更稳健。

第三步,处理异常值。年龄小于0或大于120的置为缺失再填充,历史行为次数为负的置为0。

df.loc[(df['年龄'] < 0) | (df['年龄'] > 120), '年龄'] = None df['年龄'].fillna(df['年龄'].median(), inplace=True) df['城市'].fillna('未知', inplace=True) df['性别'].fillna('未知', inplace=True) df.loc[df['历史行为次数'] < 0, '历史行为次数'] = 0

4.3 特征处理与数据集划分

特征处理按前面说的FeatureProcessor来做。划分数据集时,我习惯用train_test_split,测试集占20%,随机种子固定为42,保证可复现。

from sklearn.model_selection import train_test_split train_df, test_df = train_test_split(df, test_size=0.2, random_state=42, stratify=df['是否转化']) processor = FeatureProcessor() processor.fit(train_df) train_X = processor.transform(train_df.drop(columns=['是否转化', '用户ID'])) test_X = processor.transform(test_df.drop(columns=['是否转化', '用户ID'])) train_y = train_df['是否转化'].values test_y = test_df['是否转化'].values

注意stratify参数,它保证训练集和测试集的类别比例一致。如果类别不平衡,这个参数很重要。

4.4 模型定义与训练循环实现

模型用一个简单的多层感知机,输入维度等于特征数,隐藏层128和64,输出层2类。

import torch import torch.nn as nn class MLP(nn.Module): def __init__(self, input_dim): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 2) ) def forward(self, x): return self.net(x)

训练循环:

model = MLP(train_X.shape[1]) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) criterion = nn.CrossEntropyLoss() train_tensor = torch.tensor(train_X.values, dtype=torch.float32) train_label = torch.tensor(train_y, dtype=torch.long) dataset = torch.utils.data.TensorDataset(train_tensor, train_label) loader = torch.utils.data.DataLoader(dataset, batch_size=64, shuffle=True) for epoch in range(50): model.train() total_loss = 0 for batch_x, batch_y in loader: optimizer.zero_grad() output = model(batch_x) loss = criterion(output, batch_y) loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() print(f'Epoch {epoch}, Loss: {total_loss / len(loader):.4f}')

这里有几个细节:Dropout(0.3)防止过拟合,CosineAnnealingLR让学习率平滑下降,shuffle=True打乱批次顺序。这些看起来是小事,但组合起来对最终效果影响很大。

4.5 模型保存与推理服务封装

训练完成后保存模型权重和特征处理器:

torch.save(model.state_dict(), 'model.pt') import joblib joblib.dump(processor, 'processor.pkl')

推理服务用 FastAPI:

from fastapi import FastAPI import joblib import torch import numpy as np app = FastAPI() model = MLP(input_dim=5) model.load_state_dict(torch.load('model.pt')) model.eval() processor = joblib.load('processor.pkl') @app.post('/predict') def predict(data: dict): df = pd.DataFrame([data]) X = processor.transform(df) tensor = torch.tensor(X.values, dtype=torch.float32) with torch.no_grad(): output = model(tensor) prob = torch.softmax(output, dim=1)[0][1].item() return {'probability': prob, 'label': int(prob > 0.5)}

启动命令:uvicorn main:app --host 0.0.0.0 --port 8000。这样你就有了一个可调用的预测接口。

5. 常见问题与排查技巧实录

5.1 训练不收敛或损失震荡

这是最常见的问题。排查顺序我一般是:先看学习率,再看数据,最后看模型。学习率太大导致震荡,太小导致收敛慢。我通常先用1e-3试,如果损失震荡就降到1e-4,如果下降太慢就升到1e-2。数据方面,检查特征是否归一化、标签是否有问题。模型方面,检查层数是否过深、激活函数是否合适。

有个容易被忽略的点:输入特征的尺度差异过大。比如年龄是0到100,历史行为次数是0到10000,如果不做标准化,梯度会被大数值特征主导。我的做法是对所有数值特征做标准化,对类别特征做嵌入或独热编码。

5.2 过拟合:训练集好测试集差

过拟合的信号很明确:训练损失持续下降,验证损失先降后升。应对手段按优先级排序:增加数据量 > 加正则化 > 减小模型 > 早停。增加数据量最有效但成本最高;加正则化包括Dropout、权重衰减、数据增强;减小模型就是减少层数或隐藏单元;早停是最省事的兜底方案。

我自己的习惯是同时用Dropout和权重衰减,Dropout率0.2到0.5之间,权重衰减1e-4到1e-2之间。这两个参数需要根据验证集表现微调。

5.3 推理延迟过高

如果单次推理超过100毫秒,对于实时服务来说就偏高了。优化方向有几个:模型量化、批处理、ONNX导出、减少特征计算。模型量化把float32转成int8,速度能提升2到4倍,精度损失通常在1个点以内。批处理是把多个请求攒在一起推理,吞吐量能大幅提升,但单次延迟会增加。ONNX导出配合ONNX Runtime,推理速度通常比原生PyTorch快20%到50%。

避坑技巧:量化后的模型一定要在验证集上重新评估,我遇到过量化后AUC掉3个点的情况,原因是某些层的数值范围太窄,int8表示精度不够。这时候可以只量化部分层,或者用动态量化代替静态量化。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
损失为NaN学习率过大/数据有异常值打印每步损失和输入降低学习率、裁剪梯度、清洗数据
验证指标远低于训练过拟合对比训练/验证损失曲线加Dropout、早停、增加数据
推理结果与训练不一致特征处理不一致对比训练和推理的中间输出统一特征处理代码
服务启动报错依赖版本冲突检查requirements.txt固定版本、重建环境
预测概率全为0.5模型未加载或输入全零检查模型权重和输入重新加载模型、检查特征

6. 从原型到可用:我踩过的坑和总结的经验

6.1 数据泄漏:最隐蔽也最致命

数据泄漏是指训练时用到了推理时拿不到的信息。最常见的场景是用未来数据预测过去。比如做用户流失预测,特征里包含了“最近一次登录距今天数”,但如果这个特征是在标签时间点之后计算的,就泄漏了。排查方法是:对每个特征问一句“这个特征在预测时刻真的能拿到吗?”如果答案是否定的,就必须去掉。

我踩过一次严重的泄漏坑:做商品推荐时,特征里包含了商品的“总销量”,但这个销量是包含预测时间点之后的。结果离线AUC高达0.95,上线后直接掉到0.6。后来把销量改成“截至预测时间点的历史销量”,离线AUC降到0.78,但线上一致了。这个教训让我明白,离线指标虚高不一定是好事,可能是泄漏的信号。

6.2 版本管理:模型、数据、代码要一起管

AI项目和传统软件项目最大的区别是,模型效果依赖于数据、代码、超参数三者的组合。只管理代码版本是不够的。我的做法是:每次训练生成一个实验ID,把代码commit hash、数据版本、超参数配置、模型权重、评估结果都关联到这个ID上。这样任何时候都能复现某个模型。

具体实现可以很简单:建一个experiments目录,每个实验一个子目录,里面放config.json、metrics.json、model.pt。不需要上MLflow也能做到基本可追溯。

6.3 监控:上线只是开始

模型上线后,数据分布会变,模型效果会衰减。必须监控三个东西:输入特征分布、预测结果分布、业务指标。输入特征分布偏移检测可以用PSI或KL散度,预测结果分布看均值方差变化,业务指标就是点击率、转化率这些。

我一般设置这样的告警规则:PSI大于0.2触发警告,大于0.5触发严重告警;预测均值连续三天偏移超过10%触发告警。这些阈值不是绝对的,要根据业务容忍度调整。

6.4 最后分享几个小技巧

第一个技巧:训练前先跑一个极小数据集。取100条数据,跑几个epoch,确认整个流程能跑通、损失能下降。这能提前发现大部分代码错误,比在全量数据上跑半天才发现问题高效得多。

第二个技巧:把随机种子固定住。torch.manual_seed(42)、np.random.seed(42)、random.seed(42)三件套。这样每次实验结果可复现,排查问题时不会因为随机性干扰判断。

第三个技巧:保存最佳模型而不是最后一个模型。训练过程中验证集指标最好的那个epoch的权重才是你要的。我通常在每个epoch结束后判断验证指标是否提升,提升就保存,不提升就跳过。

第四个技巧:推理服务加一个健康检查接口。/health返回模型加载状态和版本号,方便运维排查。这个接口不涉及预测逻辑,只是确认服务活着、模型加载成功。

这条从零构建AI工程能力的路径,我自己走过很多遍,每次都能发现新的细节。最开始觉得麻烦,但当你真正遇到线上问题、需要快速定位的时候,你会发现这些“麻烦”的步骤恰恰是救命的。数据要可追溯、特征要一致、模型要可复现、服务要可监控,这四句话是我这些年做AI工程最深的体会。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 6:00:37

用Vue3和CodeMirror 6自研公式编辑器:从选型到实现全攻略

做了几年后台管理系统&#xff0c;最让我头疼的需求之一就是"表单里让用户填一条计算规则"。你说它是代码吧&#xff0c;用户不认&#xff1b;你说它是纯文本吧&#xff0c;业务方又不满意&#xff0c;说没有提示、写错了也不知道。直到我尝试用 Vue3 加 CodeMirror …

作者头像 李华
网站建设 2026/10/3 6:00:19

用Dify构建hindsight:对抗后视偏差的AI复盘助手

1. hindsight这个词&#xff0c;我想把它变成一款AI应用如果说这两年我听到最多的一个英文单词&#xff0c;除了hallucination之外&#xff0c;大概就是hindsight了。hindsight直译过来是"后见之明"&#xff0c;就是我们常说的"事后诸葛亮"。但这个词在心理…

作者头像 李华
网站建设 2026/10/3 6:00:19

Univer 在线表格引擎实战:Canvas 渲染与 Node.js 协同开发指南

1. 从“univer”这个标题说起&#xff1a;它到底是什么&#xff0c;能解决什么问题第一次看到“univer”这个词&#xff0c;很多人会以为是“universe”的缩写&#xff0c;或者某个新出的前端框架。实际上&#xff0c;Univer 是一个开源的在线电子表格与文档协作引擎&#xff0…

作者头像 李华
网站建设 2026/10/3 5:59:47

STM32实战开发:从嵌入式系统到硬件控制的全流程解析

做嵌入式这行&#xff0c;不管你是准备毕业设计还是刚进公司接手 MCU 项目&#xff0c;STM32 几乎都是躲不开的一道坎。它说到底是嵌入式系统里的一个具体芯片系列&#xff0c;但真正用起来&#xff0c;你会发现难点从来不在芯片本身&#xff0c;而在怎么把外设控制、通信协议和…

作者头像 李华
网站建设 2026/10/3 5:59:27

AI编程助手稳定输出秘籍:superpowers指令集与AGENTS.md实战指南

先说结论&#xff1a;如果你已经在用 Codex CLI 这类 AI 编程助手&#xff0c;但总觉得它“时而聪明、时而智障”&#xff0c;大多数问题出在你没有给它一套稳定的工作方法。superpowers 这个开源工具&#xff0c;做的事情就是把这套“让 AI 稳定变强”的方法论&#xff0c;封装…

作者头像 李华
网站建设 2026/10/3 5:59:27

AI编程助手Skills完全指南:从安装到自定义实战

最近在几个技术群里聊AI编程工具&#xff0c;我发现大家问得最多的已经不是“怎么配API”或者“用哪个模型”&#xff0c;而是“你装了什么skills”。从Claude Code到Codex再到OpenCode&#xff0c;这些命令行AI助手的生态里突然冒出一层叫skills的东西&#xff0c;有越来越多的…

作者头像 李华