拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

AI工程从零开始:数据、训练到部署的全链路实战拆解

AI工程从零开始:数据、训练到部署的全链路实战拆解

一提到ai-engineering-from-scratch,很多人都觉得是个悖论:AI工程这么大一个盘子,怎么可能从零开始?我见过太多人一上来就抱着大模型API猛写,结果项目换一个场景就崩;也有人啃了三个月数学,还没跑通一个完整的模型。说句掏心窝的话,从零开始不是指你把CPU到Transformer全部手搓一遍,而是把这条链路上的每个环节——数据处理、模型训练、评估、部署、监控——都用最“笨”但最扎实的方式走通一遍。这条路我走过,踩过的坑能填满一个游泳池,这篇就按我实际操作的路线,把整个ai-engineering-from-scratch的工程完整拆开给你看。

这篇文章适合两类人:一是刚入门AI、想建立完整工程概念而不是只会在笔记本里跑demo的开发者;二是已经在业务里频繁调用AI能力,却总觉得隔着一层纱、想亲手掌控整个链路的工程师。我会从环境搭建讲到模型上线,再到踩坑排查,所有步骤都是我自己验证过的,可以直接照着复现。

1. 内容整体设计与思路拆解

1.1 从零开始的核心不是“造轮子”,而是“通链路”

很多人理解from-scratch就是不用框架、不调现成库,硬怼底层数学。这个方向如果你是为了发论文、写内核,那另说;但作为工程实践,我劝你千万别这么干。真正的from-scratch,是把你原本当成黑盒的环节全部打开,亲手摸一遍。

我当初带队做一个内部文档智能分类项目,团队里有人擅长调API,有人擅长搞模型,但项目一上线就暴露问题了:数据标注格式不统一,训练出来的模型在测试集上分数漂亮,到生产环境里被真实数据一冲就露馅;模型文件怎么打包、服务怎么开、QPS怎么压,全是一笔糊涂账。那时候我们才意识到,缺的不是某一个AI能力,而是一整套工程化的方法。

所以我在设计这条学习路线的时候,定了一个原则:每个环节都可以用成熟工具,但每个环节背后的原理必须亲手验证一遍。比如你用PyTorch训练模型可以,但你要能解释清楚一个batch的数据在GPU里怎么流转;你用FastAPI部署模型可以,但你要理解为什么需要预处理对齐。这种“工具成熟、原理透明”的方式,才是工程视角的from-scratch。

1.2 整条链路的模块划分与依赖关系

我习惯把AI工程拆成六个模块:环境底座、数据处理、模型构建、训练调优、部署上线、监控迭代。每个模块之间是严格依赖的关系,跳步必翻车。

  • 环境底座:Python版本管理、虚拟环境、GPU驱动与CUDA匹配。这块虽然无聊,但它是后面所有环节的地基,我见过太多人死在装环境这一步。
  • 数据处理:采集、清洗、标注、特征工程。AI工程里流传一句话:garbage in, garbage out,数据质量直接决定模型上限。
  • 模型构建:从传统机器学习到深度学习的过渡,重点是理解模型输入输出的约束。
  • 训练调优:损失函数、优化器、学习率、正则化,这是整个工程里最吃经验的部分。
  • 部署上线:把训练好的模型包装成服务,处理延迟、吞吐和资源占用问题。
  • 监控迭代:上线只是开始,数据漂移和模型衰减才是持续要面对的敌人。

这个依赖关系在设计项目的时候就必须想清楚,否则做到一半会发现前面某个环节的疏漏让整个项目返工。

2. 环境底座:很多人第一步就翻车

2.1 Python版本和虚拟环境为什么要较真

Python版本这件事,我踩过最惨的一次坑是项目代码在本地跑得好好的,一上服务器就报错,查了半天发现是服务器上Python 3.6不支持某个语法特性。从此以后我所有项目都强制用Python 3.10以上,并且在项目根目录放一个.python-version文件。

虚拟环境的选择也是门学问。我个人推荐用uv,它比传统的virtualenv和conda都快得多。当初我在一台新机器上创建环境加装依赖,用conda花了将近半小时,换成uv之后五分钟搞定。安装依赖的时候有一个细节:一定要先把requirements.txt或pyproject.toml写好,锁住版本,不要直接pip install一堆包让系统自动解析,否则依赖冲突能让你怀疑人生。

# 创建一个干净的Python 3.11环境 uv venv .venv --python 3.11 # 激活环境(Windows下执行 .venv\Scripts\activate) source .venv/bin/activate # 安装核心依赖,版本锁定是关键 uv pip install torch==2.2.2 pandas==2.2.1 scikit-learn==1.4.2 fastapi==0.111.0

提示:如果你要用GPU训练,装PyTorch之前一定要先确认CUDA版本。nvidia-smi显示的CUDA版本是驱动支持的,不代表PyTorch能用;装错组合的后果是模型训练时提示CUDA不可用,但实际上你驱动明明装好了。

2.2 GPU环境配置的匹配逻辑

GPU环境配置是新手最容易放弃的地方,但这套逻辑搞明白了也就那回事。你只要记住一个核心规则:CUDA Toolkit版本 >= PyTorch要求的CUDA版本,且驱动版本 >= CUDA Toolkit要求的驱动版本。三者构成一个匹配链,任何一个环节不满足,就会出现奇怪的报错。

实操中我建议直接用torch.cuda.is_available()来做最终验证,这个方法返回True就代表一切正常。如果返回False,不要瞎猜,按顺序检查:先看驱动版本是否够新,再确认PyTorch是CUDA版而非CPU版,最后检查环境变量CUDA_HOME是否指向正确的路径。

# 在Python中验证GPU环境 import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))

很多人在这一步折腾几天都没搞定,后来发现只是装成了CPU版本的PyTorch,白忙活一场。所以安装之前去PyTorch官网用版本选择器生成对应的安装命令,比直接在pip里找依赖靠谱得多。

3. 数据处理:AI工程里最容易被低估的硬功夫

3.1 数据采集与清洗的基本操作规程

我见过太多所谓的AI项目,80%的时间不是在调模型,而是在和脏数据搏斗。数据清洗不是简单把空值删掉就完事,你要先理解数据的分布和业务含义。

以我当时做的文档分类项目为例,原始数据来自多个业务部门,格式五花八门:有的用Word,有的是PDF扫描件,还有直接从数据库导出的文本文件。我们的清洗流程分三步:第一步统一格式,把PDF和Word全部转成纯文本;第二步去重和去噪,删掉重复文档和明显的模板噪音;第三步是质量抽检,随机抽5%的数据人工查看,确保清洗规则没有误伤。

这里有一个很多人忽略的点:清洗规则的代码一定要单独保存并做版本管理,不要写在自己笔记本的临时脚本里。因为后续模型效果不好时你可能要回溯数据,如果清洗逻辑不可复现,整个项目就失去了可追溯性。

3.2 特征工程的实践要点与维度思考

特征工程听起来很玄,本质上就是“怎么把原始数据变成模型更容易学习的形态”。文本数据要做分词、去停用词、向量化;表格数据要做缺失值填充、类别特征编码、数值特征归一化。

我自己的经验是,不要一上来就上BERT、GPT这些大模型做表征。先做一套基础特征跑一个简单模型作为baseline,比如TF-IDF加逻辑回归,效果能打60分;然后再逐步加上更复杂的特征或者换更强的模型。这样做的好处是你能直观感受到每一步带来的增量,而不是一次性堆很多技术上去,出了问题都不知道是哪个环节的锅。

特征工程还有一个隐秘的坑:训练集和测试集的特征分布不一致。最常见的就是归一化的时候用了全量数据的均值和方差,导致测试集信息泄露。正确的做法是只在训练集上计算均值和方差,然后把它保存下来,用同一套参数去变换验证集和测试集。sklearn里就是先fit再transform,很多人喜欢省事直接fit_transform,这在有验证集的时候会出事。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline # 正确的做法:在训练集上fit,然后用同一套参数transform验证集 vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2)) X_train_vec = vectorizer.fit_transform(X_train) X_val_vec = vectorizer.transform(X_val) # 注意这里是transform,不是fit_transform model = LogisticRegression(max_iter=1000) model.fit(X_train_vec, y_train)

4. 模型构建与训练调优的实战要点

4.1 从一个简单模型开始建立baseline

我每次带新人做AI项目,第一道指令永远是:三天之内跑出一个最简单的baseline模型。不要想着一步到位上深度学习,先用逻辑回归、随机森林这些传统机器学习模型把完整的流程跑通。

这个baseline的价值太大了。第一,它帮你验证了数据管道是通的,从原始数据到模型输入这一路没有坏链;第二,它给你的模型性能定了一个“地板价”,后面不管上什么复杂模型,都要和这个地板价比,如果提升不到20%以上,就得回头审视数据的价值;第三,它的训练时间以秒计,可以快速迭代,而深度学习模型一训就是几十分钟,出了问题排查效率太低。

拿我那个文档分类项目为例,TF-IDF加逻辑回归的baseline在测试集上做到了81%的准确率,后面换成BERT微调之后提升到87%。如果没有baseline做锚点,团队成员很可能会陷入盲目调参的泥潭,因为大家根本不知道什么样的指标才算好。

4.2 训练循环里的关键要素:数据加载与损失设计

进入深度学习环节之后,编码模型的代码反而越来越模板化,真正的差异体现在数据加载和损失设计上。PyTorch的Dataset和DataLoader是两条必须亲手写一遍的代码,不要偷懒直接用别人的工具类。

from torch.utils.data import Dataset, DataLoader import torch class TextDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len=128): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text = self.texts[idx] label = self.labels[idx] encoding = self.tokenizer( text, max_length=self.max_len, padding='max_length', truncation=True, return_tensors='pt' ) return { 'input_ids': encoding['input_ids'].squeeze(), 'attention_mask': encoding['attention_mask'].squeeze(), 'label': torch.tensor(label, dtype=torch.long) } # DataLoader的设置有几个参数很关键:batch_size、shuffle、num_workers train_loader = DataLoader( train_dataset, batch_size=16, shuffle=True, num_workers=4, # 多进程加载数据,加速明显 pin_memory=True # 加速GPU训练 )

损失函数的设计决定了模型优化的方向。分类任务用交叉熵没有争议,但如果是多标签或者样本不平衡,就要考虑加权损失或者Focal Loss。我见过一个团队做风险识别,负样本占比只有5%,直接用默认的交叉熵训练,模型学出来的几乎全是“预测为负样本”,准确率堪忧。后来改用CrossEntropyLoss(weight=class_weights),把少数类的权重拉高,效果立竿见影。

4.3 训练调优的实践经验:学习率、过拟合与早停

训练调优是整个AI工程里最吃经验、最没法在书里教的部分。我自己总结了几个实战中验证有效的套路。

学习率的设置:优先用带预热和衰减的warmup策略,或者直接用AdamW配合线性衰减。我习惯初始学习率设置在2e-5到5e-5之间(对transformer类模型而言),CNN类模型可以用更大的学习率如1e-3。这里有个技巧:先跑两三个epoch观察损失下降曲线,如果损失震荡严重,说明学习率大了;如果下降缓慢,说明学习率小了。

过拟合的识别和应对:训练损失持续下降、验证损失到了某个点开始回升,这就是典型的过拟合信号。应对手段优先级从高到低:增加数据量(往往是最有效的)、加正则化(dropout、weight decay)、早停(early stopping)。早停机制我强烈建议写进训练脚本里,监控验证集上的指标,连续N个epoch不提升就自动停止,省时省力还防过拟合。

best_loss = float('inf') early_stop_counter = 0 patience = 3 # 连续3个epoch验证损失不下降就停 for epoch in range(num_epochs): train_loss = train_one_epoch(...) val_loss = evaluate(...) if val_loss < best_loss: best_loss = val_loss early_stop_counter = 0 torch.save(model.state_dict(), 'best_model.pt') else: early_stop_counter += 1 if early_stop_counter >= patience: print(f'Early stopping at epoch {epoch}') break

4.4 评估指标的选择:别被准确率骗了

分类任务里只盯着准确率是最容易翻车的事。如果你的测试集两类样本比例是9:1,哪怕模型把所有样本都预测成多数类,准确率也有90%,但这项模型毫无用处。

我的习惯是根据业务场景设计评估矩阵:二分类问题至少同时看准确率、精确率、召回率和F1;多分类问题加一个混淆矩阵可视化;回归问题用MAE和RMSE结合看。如果你做的是搜索排序类任务,还得加上NDCG这些排序指标。

拿我那个文档分类项目举例,我们定的业务目标是“关键文档不遗漏”,这个目标对应的是召回率优先;而在另一个人工审核场景,目标是“减少误报”,对应的是精确率优先。同一个模型,同一个数据集,只是评估指标不同,优化的方向可能完全不同,这就是评估指标设计要从业务场景倒推的原因。

5. 部署上线:从模型到服务的最后一公里

5.1 模型序列化与格式选择的坑

训练好的模型要上线,第一步是把它保存成合适的格式。PyTorch的torch.save默认保存的是模型权重和参数的dict,这个格式对部署来说有几个痛点:它依赖Python环境,跨语言调用不方便;它没有打包模型结构信息,推理时需要额外加载模型类定义。

工程化部署我更推荐把模型转换为ONNX格式。ONNX是一个开放的神经网络交换格式,它把模型的计算图完整地保存下来,推理时不需要再导入PyTorch,只需要ONNX Runtime就能跑,而且推理速度通常比PyTorch的原生推理快,尤其在CPU上部署的时候。

# PyTorch模型转ONNX的关键步骤 import torch import torch.onnx model = MyTrainedModel() checkpoint = torch.load('best_model.pt', map_location='cpu') model.load_state_dict(checkpoint) model.eval() dummy_input = torch.randn(1, 128) # 符合模型输入维度的示例张量 torch.onnx.export( model, dummy_input, 'model.onnx', opset_version=13, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}} )

转换ONNX有一个坑:如果模型里用了自定义层或者比较冷门的op,转换的时候可能报错。解决办法是先在测试集上跑一遍转换前后的推理对比,确保输出一致再部署,不要默认转换成功就万事大吉。量化是一个可选优化项,如果你的对端推理对延迟敏感,可以考虑ONNX Runtime的静态量化,但在做之前一定要实测精度下降幅度,不是所有模型都适合量化。

5.2 用FastAPI搭建推理服务的关键细节

部署服务框架我只推荐FastAPI,理由很简单:自动生成API文档、异步支持、性能足够好、代码极简。但推理服务不是把模型装进去就完事,真正的工程细节在请求处理和预处理对齐上。

推理服务最容易被忽视的是输入数据的预处理对齐。训练时你对原始文本做了分词、截断、编码,推理时你要把同一套逻辑完整复现。我习惯的做法是把预处理逻辑封装成独立模块,在训练和推理两端复用同一份代码,坚决不搞两套实现。文本类模型尤其要注意tokenizer的版本一致性,训练时用的是AutoTokenizer,部署时也必须用同一个,两个版本的分词结果可能完全不同,模型推理效果就崩了。

from fastapi import FastAPI from pydantic import BaseModel import onnxruntime as ort app = FastAPI(title="Text Classifier Service") session = ort.InferenceSession('model.onnx', providers=['CPUExecutionProvider']) class TextRequest(BaseModel): text: str @app.post("/predict") def predict(request: TextRequest): # 关键:预处理必须和训练时完全一致 input_ids = preprocess_text(request.text) logits = session.run( None, {'input': input_ids.astype(np.int64)} )[0] pred = int(np.argmax(logits)) return {"label": pred, "confidence": float(np.max(logits))}

batch推理是提升吞吐量的另一个大杀器。单条推理的延迟是固定的,但如果你把多条请求攒起来组成一个batch,在GPU上推理的额外耗时会被均摊。工程实现上可以用队列做请求缓冲,或者用asyncio并发控制,把并发的请求凑成batch再喂给模型。我实测在同样的硬件上,batch尺寸从1调到8,吞吐量提升了接近4倍,代价是延迟略微增加,这个方法非常值得用在生产环境。

5.3 容器化与部署架构的工程考量

模型服务不能只在自己的机器上跑,部署到服务器时容器化是最稳妥的选择。一个基础镜像加上Python环境、模型文件和推理代码,用Docker打包之后,在哪个环境跑都一样。这里我给你一个可以直接用的Dockerfile例子。

FROM python:3.11-slim WORKDIR /app # 先安装依赖,利用Docker缓存加快构建 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制代码和模型文件 COPY app/ ./app/ COPY model.onnx ./model.onnx COPY tokenizer/ ./tokenizer/ EXPOSE 8080 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080", "--workers", "2"]

注意:容器内跑ONNX Runtime时,CPU的threads参数默认会使用所有核心。如果同时起了多个worker进程,核心争抢反而会让性能下降,建议设置ort.set_default_logger_severity(3)的同时,合理控制进程数和线程数,不要默认拉满。

容器部署还有一个容易被忽略的问题:模型文件的镜像体积。一个BERT模型加tokenizer轻松超过400MB,镜像构建和拉取都会变慢。更轻量的做法是把模型文件放到对象存储,容器启动时再拉取;或者用挂在外部存储上的方式加载模型,这样模型更新时不需要重新构建镜像。

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

6.1 训练阶段的特征问题速查表

我在带项目过程中整理过一个训练阶段问题速查表,把高频翻车点都列在里面,这里直接分享给你。

症状可能原因排查办法
训练损失不下降学习率太小 / 数据预处理错误逐步调大学习率,检查输入数据
训练损失变NaN学习率太大 / 数据里有异常值降低学习率,检查数据分布
训练损失下降但验证指标不变过拟合 / 评估指标代码有bug加正则化,核对评估代码
验证损失先降后升典型过拟合早停,加dropout,减容量
不同框架结果差异大随机种子未固定 / 数据顺序不同设置随机种子,统一数据shuffle规则

随机种子这件事特别提一句:AI训练不是完全可复现的,不设置随机种子,你跑两次训练得到的结果可能差一个百分点。团队协作时,所有人训练前一定要固定好随机种子,否则每次复现别人的结果都是一场噩梦。

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False

6.2 部署阶段的延迟与稳定性问题

上线之后最头疼的问题无非两种:延迟上去了,或者服务直接崩了。延迟问题我建议先做性能剖析,不要凭感觉优化。先用一个简单的压测脚本打一下服务的QPS和延迟分布,看看瓶颈是在CPU计算、数据加载还是序列化上。

模型推理耗时占了服务端到端延迟的大头之后,优化方向通常是三个:模型量化(FP16或者INT8)、模型蒸馏(用大模型蒸馏出小模型)、批量推理。三个方向按成本和效果排序,量化成本最低,但精度有损失;蒸馏成本最高,但效果最好。

服务崩溃的问题通常来自输入数据格式异常。真实世界的请求不会像测试集那么乖巧,字段缺失、类型不对、超长文本这些情况都要在服务里进行防御性处理。我用Pydantic的请求模型做了严格类型校验,并且加了统一的异常处理中间件,任何未知异常都返回结构化错误信息而不是崩溃栈。

6.3 一套可以复用的AI工程检查清单

最后,我把自己从零开始做AI工程时沉淀下来的检查清单分享给你,每个新项目开始之前我都按这个清单过一遍,能少踩很多坑。

  • 数据层面:检查是否有数据泄露。这必须放在第一位,验证集和训练集的数据不能有任何重叠,尤其是文档类数据,同名文件去重要彻底。
  • 特征层面:检查缺失值处理是否只在训练集上拟合。一句话,凡是会用到数据统计量的操作,一律在训练集上fit,在其他集上transform。
  • 模型层面:检查损失函数是否和评估指标对齐,模型的输出层设计是否符合任务类型。
  • 部署层面:检查推理侧的预处理是否和训练侧完全一致,自定义tokenizer和vocab文件是否被完整带上。
  • 监控层面:上线前就想清楚漂移监控要观察哪些特征和指标,建立预测分布和真实分布的基线数据。

这套清单看起来平平无奇,但每一个条目背后都是我一次次上线事故熬出来的教训。AI工程没有灵光一现的捷径,老老实实把每个环节打通吃透,比什么技巧都管用。

我个人的体会是,从零开始做AI工程这件事,最大的回报不是某一个模型的精度暴涨,而是你终于对整条链路有了掌控感:数据出问题了你知道去哪查,模型效果不行你知道从哪个维度调,线上崩了你心里有优先排查的路径。这种掌控感,是靠一次一次踩坑、一步一步验证换来的,也是任何速成课都给不了的东西。

返回列表