ARTICLE DETAIL

资讯详情

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

从零搭建AI工程能力:环境、数据、训练与部署全流程实战

从零搭建AI工程能力:环境、数据、训练与部署全流程实战 1. 从零搭建AI工程能力为什么大多数人卡在第一步就放弃了很多人对AI工程这四个字的理解还停留在调个API、写个提示词的阶段。我刚开始接触这个方向的时候也是这么想的觉得无非就是拿现成的模型接口拼拼凑凑能跑通一个问答机器人就算入门了。但真正动手做一个完整的AI工程项目之后才发现从零到一构建一套可用的AI工程能力涉及的东西远比想象中多——数据处理、模型选型、推理部署、性能调优、效果评估每一个环节都有大量细节需要自己踩一遍才算真正掌握。ai-engineering-from-scratch这个方向之所以值得认真对待是因为它解决的不是怎么用别人的工具而是怎么自己造工具。这两者之间的差距就像会开车和会修车的区别。平时路上跑没问题一旦遇到特殊情况会修车的人才能从容应对。AI工程也是一样当现成的框架满足不了需求、当推理性能成为瓶颈、当数据管道出现诡异bug的时候只有真正理解底层原理的人才能快速定位和解决问题。这篇文章适合几类人看一是刚入行AI方向、想系统建立工程能力的开发者二是有一定算法基础但缺乏工程落地经验的算法工程师三是想从传统后端转AI工程方向的程序员。我会按照一个完整的AI工程项目从零搭建的真实流程来展开把每个环节的核心原理、实操步骤、容易踩的坑都讲清楚。不会只给一堆代码让你复制粘贴而是把为什么这么做讲透这样你遇到新问题的时候才能自己推导出解决方案。整个内容会围绕一条主线假设我们要从零构建一个文本分类服务从环境搭建、数据处理、模型训练、推理优化到最终部署上线把每个环节的关键决策点和技术细节都过一遍。这条主线走完你对AI工程的整体轮廓就会有比较清晰的认识。2. 环境搭建与工具链选型别一上来就装一堆用不上的东西2.1 为什么环境管理是AI工程的第一道坎我见过太多人在环境配置这一步就耗掉两三天最后还没跑通。问题出在哪主要是两个原因一是盲目追求全家桶看到什么工具都装结果版本冲突一大堆二是没有隔离意识所有东西都装在系统Python里装崩了只能重装系统。AI工程和普通后端开发在环境管理上有一个很大的区别AI领域的依赖包体积大、版本敏感度高。PyTorch一个版本升级可能就导致CUDA不兼容numpy一个小版本变动可能就让某个科学计算库报错。所以环境隔离不是可选项是必选项。我的建议是用conda来管理Python环境用pip来装包。conda的好处是它能管理非Python的依赖比如CUDA运行时、cuDNN这些底层库pip管不了这些。具体操作conda create -n ai-eng python3.10 -y conda activate ai-engPython版本选3.10是有讲究的。3.8太老很多新库已经不支持了3.12太新部分AI框架的wheel包还没跟上。3.10是目前兼容性最好的版本主流框架都有对应的预编译包。2.2 核心依赖的安装顺序与版本锁定装包顺序很重要顺序不对会导致依赖解析器选错版本。正确的顺序是先装深度学习框架再装数据处理库最后装工具类库。# 第一步深度学习框架 pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 第二步数据处理与科学计算 pip install numpy1.24.3 pandas2.0.3 scikit-learn1.3.0 # 第三步工具类 pip install tqdm4.66.1 tensorboard2.14.0 pyyaml6.0为什么PyTorch要指定cu118这个index-url因为默认的PyPI源装的是CPU版本不带CUDA支持。如果你有NVIDIA显卡想做GPU加速训练必须从PyTorch官方的CUDA源安装。cu118对应CUDA 11.8这是目前最稳定的CUDA版本之一兼容大多数消费级显卡。注意安装之前先用nvidia-smi确认你的显卡驱动支持的CUDA版本。如果驱动支持的版本低于你要装的CUDA版本需要先升级显卡驱动。版本锁定这件事我强烈建议在项目根目录维护一个requirements.txt把所有依赖的精确版本都写进去。不要用pip freeze直接导出那个会把间接依赖也写进去导致文件臃肿且难以维护。手动维护核心依赖的版本号就够了。2.3 项目目录结构的设计逻辑很多人不重视目录结构觉得能跑就行。但AI工程项目和普通项目不一样它涉及数据、模型、配置、日志、实验记录等多种类型的文件结构乱了后期维护成本极高。我常用的结构是这样的ai-project/ ├── configs/ # 配置文件 │ ├── train.yaml │ └── model.yaml ├── data/ # 数据目录 │ ├── raw/ # 原始数据 │ ├── processed/ # 处理后数据 │ └── external/ # 外部数据 ├── src/ # 源代码 │ ├── data/ # 数据处理模块 │ ├── models/ # 模型定义 │ ├── train/ # 训练逻辑 │ └── inference/ # 推理逻辑 ├── experiments/ # 实验记录 ├── notebooks/ # 探索性分析 ├── tests/ # 测试代码 └── requirements.txt这个结构的关键设计点在于数据和代码完全分离配置和代码分离实验记录独立存放。这样做的好处是你可以随时切换数据集而不用改代码可以复现任何一次实验的完整配置可以在不污染主代码的情况下做探索性分析。experiments/目录特别值得说一下。每次训练都新建一个以时间戳命名的子目录里面存放当次的配置文件副本、模型权重、训练日志、评估结果。这样当你想对比不同超参数的效果时直接翻实验记录就行不用凭记忆回想上次那个学习率设的多少。3. 数据处理管道AI项目里最脏最累但最重要的活3.1 原始数据清洗的常见陷阱在真实项目里你拿到的原始数据大概率是脏的。我做过的一个文本分类项目原始数据有20万条但真正能用的不到15万条。剩下的5万条里有重复的、有标签错误的、有文本为空的、有编码乱码的。清洗的第一步是去重。但去重不是简单的drop_duplicates()就完事了。文本去重需要考虑近似重复的情况比如两条文本只差一个标点符号或者只是顺序不同。我一般用SimHash来做近似去重它能将文本映射成一个64位的指纹通过计算汉明距离来判断相似度。from simhash import Simhash def get_simhash(text): return Simhash(text).value def hamming_distance(hash1, hash2): xor hash1 ^ hash2 return bin(xor).count(1) # 汉明距离小于3认为是重复 threshold 3标签错误的检测比较麻烦没有完全自动化的方法。我的做法是先用模型跑一遍预测把预测结果和原始标签不一致的样本挑出来人工审核。通常能发现2%-5%的标签错误修正之后模型效果会有明显提升。3.2 文本预处理的分词策略选择中文文本和英文文本的预处理策略完全不同。英文天然有空格分隔分词相对简单中文需要专门的分词工具。对于中文文本分类任务我一般用jieba做分词但有几个细节需要注意import jieba def tokenize(text): # 加载自定义词典 jieba.load_userdict(custom_dict.txt) # 精确模式分词 tokens jieba.lcut(text) # 去除停用词和单字 stopwords set(open(stopwords.txt).read().split()) tokens [t for t in tokens if t not in stopwords and len(t) 1] return tokens为什么要加载自定义词典因为通用词典不包含你业务领域的专有名词。比如你做医疗文本分类房颤、心梗这些词如果不加到自定义词典里jieba会把它们拆成单字丢失语义信息。为什么要去除单字因为单个汉字在文本分类任务中往往噪声大于信号。当然这不是绝对的如果你的任务对细粒度语义敏感比如情感分析那单字可能也有价值。这个需要根据具体任务做实验来定。3.3 构建可复用的数据管道数据处理代码最忌讳的是写成一次性的脚本。今天处理这个数据集写一个脚本明天换个数据集又重写一遍。正确的做法是抽象出通用的数据管道。我通常定义一个DataPipeline类把清洗、分词、特征提取、划分数据集这些步骤串起来class DataPipeline: def __init__(self, config): self.config config def clean(self, df): # 去重、去空、修正标签 pass def tokenize(self, df): # 分词、去停用词 pass def split(self, df): # 按比例划分训练/验证/测试集 pass def run(self, raw_path): df pd.read_csv(raw_path) df self.clean(df) df self.tokenize(df) train, val, test self.split(df) return train, val, test这个类的好处是所有处理步骤的参数都从config读取换数据集的时候只需要改配置文件代码不用动。而且每个步骤都可以单独调用方便调试。数据划分有一个容易忽略的点要保证类别分布一致。如果训练集里正负样本比例是1:1测试集里变成了1:3那评估结果就没有参考意义。用sklearn的train_test_split时加上stratify参数就能解决from sklearn.model_selection import train_test_split train, test train_test_split(df, test_size0.2, stratifydf[label], random_state42)4. 模型训练从能跑到跑好的关键细节4.1 基线模型的选择逻辑很多人一上来就想用BERT、GPT这些大模型觉得效果一定好。但实际上在很多文本分类任务上一个调好参数的TextCNN或者BiLSTMAttention就能达到90%以上的效果而且训练速度快、推理延迟低、部署成本小。我的建议是先跑一个简单的基线模型确认数据管道没问题、评估指标合理然后再逐步尝试更复杂的模型。基线模型我一般选TextCNN因为它结构简单、训练快、效果稳定。import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, num_classes, filter_sizes, num_filters): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv2d(1, num_filters, (fs, embed_dim)) for fs in filter_sizes ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(num_filters * len(filter_sizes), num_classes) def forward(self, x): x self.embedding(x) # (batch, seq_len, embed_dim) x x.unsqueeze(1) # (batch, 1, seq_len, embed_dim) x [torch.relu(conv(x)).squeeze(3) for conv in self.convs] x [torch.max_pool1d(c, c.size(2)).squeeze(2) for c in x] x torch.cat(x, dim1) x self.dropout(x) return self.fc(x)filter_sizes一般选[2, 3, 4]对应二元、三元、四元词组特征。num_filters一般选128或256。embed_dim选128或300如果数据量小就选小一点防止过拟合。4.2 训练过程中的监控与早停训练不是跑完固定轮数就完事需要监控验证集上的表现在效果不再提升的时候及时停止。这就是早停Early Stopping机制。class EarlyStopping: def __init__(self, patience5, min_delta1e-4): self.patience patience self.min_delta min_delta self.counter 0 self.best_score None self.early_stop False def __call__(self, val_loss): if self.best_score is None: self.best_score val_loss elif val_loss self.best_score - self.min_delta: self.counter 1 if self.counter self.patience: self.early_stop True else: self.best_score val_loss self.counter 0patience设5的意思是验证集loss连续5轮没有下降就停止训练。min_delta是判断下降的阈值小于这个幅度的下降不算数避免被噪声干扰。除了loss还要监控准确率、F1值这些指标。有时候loss在降但F1不涨说明模型可能在过拟合。这时候需要检查训练集和验证集的指标差距如果训练集F1 0.98、验证集F1 0.75那过拟合已经很严重了需要加正则化或者减模型复杂度。4.3 学习率调度与优化器配置学习率是训练中最重要的超参数没有之一。设大了loss震荡不收敛设小了训练慢到怀疑人生。我的经验值是Adam优化器初始学习率设1e-3到1e-4之间配合余弦退火调度from torch.optim import Adam from torch.optim.lr_scheduler import CosineAnnealingLR optimizer Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_maxnum_epochs, eta_min1e-6)weight_decay是L2正则化系数设1e-4能有效抑制过拟合。eta_min是学习率的下限防止后期学习率降到0导致完全学不动。还有一个技巧是warmup前几个epoch用很小的学习率线性增加到初始值让模型先热身再全力训练。这在Transformer类模型上特别重要因为注意力机制在训练初期很不稳定。def warmup_lr(step, warmup_steps, base_lr): if step warmup_steps: return base_lr * step / warmup_steps return base_lr4.4 处理类别不平衡的实用方案真实数据里类别不平衡是常态。比如一个垃圾文本检测任务正常文本占95%垃圾文本只占5%。如果直接训练模型会倾向于把所有样本都预测为正常准确率看起来有95%但垃圾文本一个都检测不出来。解决方案有三种我按推荐程度排序第一种是加权损失函数。给少数类更高的权重让模型更关注少数类的错误class_weights torch.tensor([1.0, 10.0]).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)权重的计算方式是总样本数除以类别数再除以该类样本数。比如总共有1000个样本2个类别正常类950个垃圾类50个那正常类权重是1000/2/950≈0.53垃圾类权重是1000/2/5010。第二种是过采样少数类。用imblearn库的SMOTE或者简单的随机复制。但随机复制容易导致过拟合SMOTE在小样本上效果也不稳定。第三种是focal loss。它通过降低易分类样本的权重让模型聚焦在难分类样本上。公式是FL -alpha * (1-pt)^gamma * log(pt)gamma一般取2。实际项目中我一般先用加权损失如果效果不够再试focal loss。过采样作为最后手段因为它改变了数据分布可能引入额外偏差。5. 推理优化与部署让模型真正能用的最后一公里5.1 模型导出与格式转换训练好的PyTorch模型不能直接部署到生产环境需要先导出成推理友好的格式。常见的选择有ONNX和TorchScript。ONNX的好处是跨框架、跨平台可以在TensorRT、OpenVINO等多种推理引擎上运行。导出方式import torch.onnx dummy_input torch.randint(0, vocab_size, (1, max_seq_len)).to(device) torch.onnx.export( model, dummy_input, model.onnx, input_names[input_ids], output_names[logits], dynamic_axes{input_ids: {0: batch_size, 1: seq_len}}, opset_version13 )dynamic_axes这个参数很关键它允许输入的长度是可变的。如果不设导出的模型只能接受固定长度的输入实际使用中会很受限。TorchScript的好处是导出简单、不需要额外依赖scripted_model torch.jit.script(model) scripted_model.save(model.pt)但TorchScript对动态控制流的支持不如ONNX好如果模型里有if-else分支或者循环可能导出失败。5.2 推理加速的几种手段模型推理速度直接决定了服务能承载多少并发。加速手段从易到难排列量化是最容易上手的。把FP32的权重转成INT8模型体积缩小4倍推理速度提升2-3倍精度损失通常在1%以内。PyTorch支持动态量化quantized_model torch.quantization.quantize_dynamic( model, {nn.Linear}, dtypetorch.qint8 )算子融合是中等难度的优化。把多个连续的小算子合并成一个大的算子减少kernel launch的开销。TensorRT在这方面做得很好但需要NVIDIA显卡。知识蒸馏是从模型层面优化。用大模型教小模型让小模型达到接近大模型的效果但推理速度快很多。这个需要重新训练成本较高。批处理是最简单也最有效的。单条推理和批量推理的吞吐量差距可能有几十倍。在服务端维护一个请求队列攒够一批再一起推理class BatchInference: def __init__(self, model, max_batch_size32, max_wait0.01): self.model model self.max_batch_size max_batch_size self.max_wait max_wait self.queue [] def predict(self, text): self.queue.append(text) if len(self.queue) self.max_batch_size: return self._flush() # 等待一小段时间看有没有更多请求 time.sleep(self.max_wait) return self._flush()5.3 服务化部署的架构选择部署方式取决于你的场景。如果是内部工具、并发不高用Flask或者FastAPI起一个HTTP服务就够了from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class Request(BaseModel): text: str class Response(BaseModel): label: str confidence: float app.post(/predict, response_modelResponse) async def predict(req: Request): label, conf model.predict(req.text) return Response(labellabel, confidenceconf)如果是高并发场景需要考虑用Triton Inference Server或者自己写一个基于gRPC的服务。Triton的好处是支持多模型、多版本、动态批处理而且对GPU的利用率很高。部署时还有一个容易忽略的点模型版本管理。每次更新模型都要保留旧版本万一新模型出问题可以快速回滚。我一般用日期加序号来命名模型文件比如model_20240115_v2.onnx同时在数据库里记录每个版本的评估指标和上线时间。5.4 线上服务的监控与告警模型上线不是终点而是起点。你需要持续监控服务的各项指标监控项正常范围告警阈值处理方式推理延迟P99100ms200ms检查GPU利用率、请求队列长度错误率0.1%1%检查输入数据格式、模型加载状态QPS根据容量定超过容量80%扩容或限流内存占用80%90%检查是否有内存泄漏预测分布与训练分布一致偏移超过20%检查数据源是否变化预测分布监控特别重要。如果线上请求的预测结果分布突然和训练时差异很大说明输入数据的分布变了模型可能已经不适应当前数据了需要重新训练。我遇到过一次真实案例一个情感分类模型上线两周后准确率从92%掉到78%。排查发现是数据源那边改了一个字段的编码格式导致部分文本变成了乱码模型对乱码文本的预测基本是随机的。加上输入数据的格式校验之后问题就解决了。6. 效果评估与持续迭代AI工程没有一劳永逸6.1 离线评估指标的选取准确率是最直观的指标但在类别不平衡的场景下会严重误导。比如99%的样本是A类模型全预测A也能有99%的准确率但这样的模型毫无价值。更可靠的指标是F1值它综合了精确率和召回率。对于多分类任务用macro-F1每个类别F1的算术平均比micro-F1全局计算的F1更能反映少数类的表现。from sklearn.metrics import classification_report print(classification_report(y_true, y_pred, digits4))这个报告会输出每个类别的精确率、召回率、F1值和支持度。我一般重点看两个东西一是少数类的F1是否可接受二是各类别之间的F1差距是否过大。如果某个类的F1只有0.5而其他类都在0.9以上说明模型在这个类上存在严重问题需要针对性优化。6.2 线上A/B测试的设计离线指标好不代表线上效果好。上线之前一定要做A/B测试用真实流量对比新旧模型的表现。A/B测试的关键是分流要随机且稳定。同一个用户每次请求应该落到同一组否则用户体验会不一致。我一般用用户ID的哈希值对100取模小于50的走A组大于等于50的走B组。def get_group(user_id): return A if hash(user_id) % 100 50 else B测试周期至少一周覆盖完整的工作日和周末因为用户行为在不同时间段可能有差异。样本量要足够大一般每组至少1000个样本才能得到统计显著的结果。6.3 模型迭代的触发条件模型不是训练一次就永远不用管了。以下几种情况需要触发重新训练第一种是数据分布漂移。通过监控线上请求的输入特征分布如果和训练数据分布差异超过阈值就需要用新数据重新训练。第二种是业务需求变化。比如新增了一个类别或者分类标准调整了那模型必须重新训练。第三种是定期更新。即使没有明显的变化也建议每季度用最新数据重新训练一次让模型跟上最新的语言表达习惯。第四种是效果下降。当线上核心指标连续下降超过一定幅度比如F1值下降超过5个百分点就需要排查原因并决定是否重新训练。6.4 构建自动化的训练流水线当模型迭代频率变高之后手动训练就不现实了。需要把整个流程自动化数据拉取、清洗、训练、评估、导出、部署全部串成一条流水线。我一般用Airflow或者Prefect来做流程编排。核心的DAG是这样的拉取新数据 - 数据质量检查 - 数据预处理 - 模型训练 - 离线评估 - 模型导出 - 灰度发布 - 线上监控每个环节都有成功和失败的分支。数据质量检查不通过就告警并终止离线评估不达标就不进入发布环节。灰度发布先放10%的流量观察24小时没问题再全量。这套流水线搭起来之后模型迭代的效率会有质的提升。以前手动跑一次要半天现在全自动跑完只要一个小时而且不会因为人为疏忽漏掉某个步骤。我在实际做AI工程项目的过程中最大的体会是模型算法本身固然重要但真正决定项目成败的往往是工程细节。数据管道是否健壮、训练流程是否可复现、推理服务是否稳定、监控体系是否完善这些脏活累活才是AI工程的核心竞争力。算法可以看论文快速学习但工程经验只能一个项目一个项目地积累。希望这篇内容能帮你在从零构建AI工程能力的路上少走一些弯路。
返回列表