ARTICLE DETAIL

资讯详情

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

从零开始AI工程:数据清洗、模型训练与部署的完整实践

从零开始AI工程:数据清洗、模型训练与部署的完整实践 这个项目标题是我在某次把旧实验目录整个推翻重写时随手敲下来的名字“ai-engineering-from-scratch”。字面意思是“从零开始搞AI工程”但它后面成了我大半年里最值的一次重构。不是说我从零实现Transformer、从零写CUDA而是我把一个靠拼装API和现成库堆起来的模型项目彻底拆成了一条自己可以完全掌控的工程链路——从数据清洗、样本构建、模型训练、指标验证到服务部署每一步都用最直白的方式给自己交代清楚。如果你现在正处在一个“能跑通别人的notebook但自己从头搭一套完整AI项目就手忙脚乱”的状态这个标题对应的思路应该对你有用。我身边很多朋友在这个阶段会特别焦虑模型框架日新月异工具越来越多似乎直接调用就能出结果为什么还要“从零开始”。我只能说框架永远是帮你节省时间的不是替你思考的。当你只知道“调包”而不知道内部数据怎么流动遇到Loss掉不下去、指标不涨、结果复现不了的时候你会发现自己连排查问题的入口都找不到。所以我更想跟你讲清楚这一套从零搭建的AI工程到底是怎么拆解的、有哪些关键环节、以及我实际写代码时踩过的坑。1. 为什么我从零开始搭 AI 工程而不是直接用成熟训练框架1.1 框架解决的是通用问题而你的任务往往是特定的一圈训练框架用下来你会发现一个现象框架越高级离你的具体问题越远。Hugging Face的Transformers把数据管道、模型加载、训练循环都封装得很舒服但你换了任务类型之后经常要跟它的内部设计做对抗。比如我想在训练时做一种比较特殊的数据采样某些样本的出现频率要对训练轮数动态衰减让模型后期少看噪声数据。这个需求写在框架里要翻源码自己写训练循环几行就搞定了。从零搭建的最大价值不是“图省事”而是让自己清楚每一个参数到底在影响什么。框架帮你遮住了复杂度也就遮住了判断力。你失去判断力之后出了问题只能上网提问而别人没有你的数据也没有你的Loss曲线那些提问通常得不到真正有用的答案。我自己选择从零开始还有另一个原因稳定性。框架升级频繁接口说改就改。某个版本跑得好好的训练脚本换了新版本可能直接报错因为内部类名变了。我会把模型训练这一层做成自己可以控制的结构数据流是显式的优化器状态是明确的就连随机种子落在哪里都是自己安排的。这样换环境、换GPU型号、换数据量都不会影响核心逻辑。1.2 从零搭建的另一层价值成本与调试门槛都能降下来很多初学者以为“从零搭建”意味着更高的成本恰恰相反。你只在模型结构层面复用一个可靠的框架比如直接用PyTorch或者某种开源模型仓库但训练、评估、数据这些工程部分自己控制成本是最低的。因为每一层都是透明的一个环节出问题直接定位到那一层而不是让黑箱替你藏问题。举一个实际数字我早期用现成框架训练一个文本分类模型官方文档里的默认配置一启动就占满了显存。我当时以为是我数据太大了来回调batch_size调了三天。后来用自己写的训练循环才发现框架默认把验证集也塞进了同一个批次加上梯度累积内部的中间变量显存翻倍。如果我对工程链路没有掌控力这种问题会消耗我很多时间。自己搭工程还能帮你真正控制成本。大模型训练不是只有GPU费用还有电费、时间、调试成本和试错成本。一个干净的工程链路可以让你用很小的数据集先跑通全部环节确认每次改动的影响后再上完整数据。这个节奏是任何AI项目都应该有的基本素养。2. 动手之前先把AI项目拆成可度量的四个要素2.1 数据组件质量、去重和划分优先级高于一切从零搭建AI工程我第一个要拆解的一定是数据而不是模型。很多项目和面试简历最大的问题就是数据部分一笔带过但真实训练中数据往往决定了模型能力的上限。我之前做文本分类模型结构换了三轮指标纹丝不动最后发现是有大量重复样本被放进了验证集。重复样本既存在于训练集内部也存在于训练集和验证集之间导致模型看起来“很厉害”一换真实数据就露馅。清洗和去重是数据的底座。最基础的操作包括按哈希或MinHash做精确和近似去重文档级别和句子级别都要去过滤掉过短、过长、包含乱码或异常重复符号的样本对文本类数据做统一编码处理去掉隐藏的控制字符做一次全局的标签分布统计如果某些类别样本太少要想好是增采样还是合成样本从确定性的时间、来源、ID等维度做训练集和验证集的分割不要随机打乱后直接切分避免同源数据泄漏到验证集。这些步聚要比调参重要几个数量级。我见过太多团队在数据没洗干净的情况下花大力气换模型尝试那是不折不扣的浪费。数学上可以这么理解模型的容量再大也无法弥补数据分布本身的错误信号。你把标签贴错了模型会“认真”学习你的错误然后给你一个看似合理的错误结果。2.2 模型基线先用简单模型跑通再考虑复杂结构从零搭建项目的时候我的习惯是先建立一个“极不可能出错”的基线。做文本任务时一个词频统计加上逻辑回归就够用做图像任务时一个简单的卷积模型就够用做序列任务时可以先尝试浅层模型。很多人会觉得这样太简单不够“AI”。但你要明白基线模型有三大用处第一验证数据管道的正确性第二拿到一个合理的指标参考线第三帮你分辨后续模型的提升到底是数据带来的还是结构带来的。基线模型还有一个问题帮你顺手解决你真的知道自己的任务在多难吗用简单模型如果已经达到90分你就不需要为了两分牺牲大量资源和时间。用简单模型如果只有30分你也能更早意识到问题可能出在数据或者任务定义上而不是模型不够先进。我曾经带过一个实习生上来就想用大模型微调做意图识别我让他先用TF-IDF加逻辑回归。他花半天跑完准确率到了85%。后来他把数据清洗做了一轮同样逻辑回归直接到90%。这一下就说明数据的作用远大于模型。这个案例后来成了我判断项目健康度的标准动作。2.3 训练目标明确你在优化什么而不是“再训几轮”很多项目从零搭建后最大的问题不是不会写代码而是不知道在优化什么。AI工程不能只有“Loss下降”这一个信号因为过拟合时Loss也会下降。你在项目开始前就要定义清楚离线指标是什么线上业务指标又是什么。分类任务要有精确率、召回率、F1生成任务要有BLEU、ROUGE或人工评估回归任务要有MAE、RMSE大规模语言模型还要考虑困惑度、回答准确率、事实一致性。更好的做法是把指标分成两类一类是训练时就能快速反馈的代理指标另一类是最终衡量效果的终极指标。代理指标不需要跟终极指标完全一致但必须高度相关。比如训练一个生成式摘要模型终极指标是摘要的事实一致性但训练时你不可能每一步跑一次大模型评估那么代理指标可以是ROUGE或者抽取式重叠度。你要保证代理指标提升时终极指标的大方向也是提升的否则就需要停下来重新设计。我见过最浪费时间的问题就是目标不清晰。一个人闷头训练两周然后发现评价指标选错了所有实验都要重来。这个成本比从零搭建工程本身高得多。所以我会在项目启动的第一天就把评估脚本写出来用一个小模型把全部流程跑到能出数字再去增加训练数据量。整个逻辑顺序一定是先有评估再有训练最后调参。3. 核心模块的实现细节以及我在写代码时的真实取舍3.1 目录结构从一开始就为可复现做准备从零搭建的工程目录结构决定了你后面三个月的幸福感。我的标准结构大致是这样ai-engineering-from-scratch/ ├── config/ # 所有实验配置yaml或json均可 │ ├── data_config.yaml │ └── train_config.yaml ├── data/ │ ├── raw/ # 原始数据只读不写 │ ├── processed/ # 清洗后的数据带版本标记 │ └── splits/ # 训练/验证/测试划分 ├── src/ │ ├── data/ │ │ ├── cleaning.py │ │ ├── dedup.py │ │ └── dataset.py │ ├── models/ │ │ ├── base_model.py │ │ └── model_wrapper.py │ ├── train/ │ │ ├── trainer.py │ │ └── optimizer.py │ └── evaluate/ │ └── metrics.py ├── experiments/ │ ├── exp_001/ │ ├── exp_002/ │ └── run_record.md └── scripts/ ├── run_train.sh └── run_eval.sh这个结构的核心思想是数据原始目录永远只读清洗后的数据带版本所有实验配置和运行记录绑定在一起。我经历过的痛苦几乎都来自“只记得代码改过不记得为什么改”。有了实验记录目录之后每跑一次训练就把当时的配置、代码commit编号、数据版本号、训练日志一起存进去。三个月后你回头看还能准确说出某个数字是怎么来的。这一点对AI工程来说比写100个函数都重要。3.2 数据加载与采样容易忽略但对结果影响很大的部分数据加载写得好不好决定了训练效率的上限。很多人直接用现成的DataLoader机械地加载数据我发现很容易出问题的地方有三个随机种子的一致性、顺序保持、以及样本量变化时的稳定性。第一随机种子。我在代码里固定了Python的random、NumPy的random和PyTorch的random种子来源于配置文件。做分布式或多卡训练时还需要为每个进程设置不同但可复现的种子偏移。否则你连一次实验都复现不了。第二顺序保持。对文本任务我通常先对所有样本按ID排序再做一次性shuffle。这样即便某个DataLoader在断点恢复后行为不同你还是能从原始顺序推导出采样结果。很多数据泄漏问题就是因为这里图省事。第三样本量变化。我在训练文本模型时往往会做“动态采样”某些长文档会被切成长度为512的多个片段而原始样本量取决于切割方式。如果这个逻辑不稳定同一份数据在不同代码版本里会展现出不同分布让实验结果没法比较。数据加载模块还有一个容易出错但很少人注意的点样本的批处理要尽量平衡长度。文本、语音、图像处理中都有“形状差异”的问题如果你把长度悬殊的样本塞进同一个批次短样本得到大量填充计算效率大大降低。我会把样本按长度排序后分桶再在每个桶内做随机采样这个技巧在实际训练中能提高20%到30%的吞吐量。3.3 训练循环混合精度、梯度累积与学习率调度训练循环是AI工程的心脏我会把最核心的经验都放在这一节。下面这一段是我经常使用的完整且直白的伪代码结构model.train() optimizer.zero_grad() for batch_idx, batch in enumerate(train_loader): inputs batch[input_ids].to(device) labels batch[labels].to(device) with torch.cuda.amp.autocast(): outputs model(inputs, labelslabels) loss outputs.loss / grad_accumulation_steps loss.backward() if (batch_idx 1) % grad_accumulation_steps 0: optimizer.step() scheduler.step() optimizer.zero_grad()这里有两个细节我要特别解释混合精度和梯度累积。混合精度不是“把模型变成半精度”。它的本质是在计算前向和反向传播时用FP16加速同时在优化器状态里保留FP32的权重复本让它在不牺牲稳定性的前提下大幅降低显存占用、提高吞吐量。用torch.cuda.amp的autocast和GradScaler是简单有效的做法。我见过很多新手直接把模型转成.half()训练结果Loss直接发散这不是混合精度而是误用。梯度累积解决的是“显存不够、batch又不够大”的矛盾。假设你想要的batch是64但显存只能放下16那就可以设置grad_accumulation_steps4每4个batch做一次参数更新。这跟你直接跑batch64的效果并不完全等价因为BN层对应的统计是逐小批次的但绝大多数任务里这个差异可以接受。在Loss这一层做除法归一化也很关键因为PyTorch在反向传播时是累加梯度除非你除以累积步数否则梯度会被放大4倍学习率就要跟着调。学习率调度我基本会固定用一个组合前几步做warmup后面跟一个线性或cosine衰减。设置warmup的原因很实际训练初期模型权重是随机或预训练初始状态梯度的统计量非常不稳定过大的学习率会让参数一下子飞出合理的区域。一般warmup步数占总步数的5%到10%就够了。衰减则是为了让模型在后期能稳定收敛避免在最优区域附近来回震荡。你可以跑一组极端实验来看没有warmup时训练半天指标都上不去加上几百步就能稳定下降我自己在这个问题上吃过亏。检查点保存也要提前设计好。我既保存“最新”的checkpoint也保存“最优验证指标”的checkpoint。两个文件名要区分清楚因为训练后期可能会过拟合只有最优验证指标的checkpoint才有上线价值。保存的字段除了权重还要带上当时的学习率、优化器状态、随机种子和数据版本方便断点续跑和复现。3.4 评估模块和训练分开但要一起设计评估模块是很多从零项目里最后才补的东西但这是错的。我建议在写训练脚本之前就把评估脚本写好哪怕它只有几行。评估的重要性在于它是你判断“训练是否有效”的唯一标准。没有评估的训练不过是在盲目调参。评估有几个容易踩的坑第一评估数据必须和训练数据完全隔离。我刚才提到过数据泄漏不是只有“忘了分割”这一种形式很多人用了全量数据做统计归一化比如计算词表的频率、计算均值和方差这就把验证集的信息泄露进去了。所以凡是需要拟合在数据上的统计量都必须只对训练集计算再应用到验证集和测试集。第二评估过程不能跟训练抢资源。我在代码里把每个epoch结束后的验证逻辑独立成一个函数在单独的进程中调用。训练Loop里直接调用验证函数虽然方便但会让GPU利用率出现周期性的尖峰和谷值还容易把验证阶段的梯度误放进训练流程。最好是在验证时给模型设一个validation模式确保Dropout和BatchNorm的行为正确。第三评估指标要分数据子集看。只给一个平均分远不够我会把样本按来源、长度、类别等维度切分分别计算指标。这能帮你定位模型在哪些地方失效。比如一个意图识别模型整体F1为80%但你看长句类别发现F1只有30%那调参方向就完全不同。宏观率指标往往会掩盖这种关键差异。4. 从跑通到跑好我在试运行阶段踩过的典型坑4.1 Loss 正常下降但验证集指标不动这是我在训练过程中最常遇到的现象也是最让人困惑的一个。表面上看模型在训练集上不断进步Loss曲线的确在往下走但验证集上一看准确率、F1一点变化都没有。这通常有三个原因。第一个原因训练Loss和验证指标对应的目标不完全一致。如果验证指标是准确率训练目标却是交叉熵模型可能为了降低交叉熵而在某几个易混淆类别上增加了预测概率但阈值不变时准确率不会立刻改变。把验证指标换成模型在正确类别上的平均置信度往往就能看到变化。第二个原因模型出现了“早熟”这是我自己起的叫法。模型在早期先用最容易的特征做出基本的判断之后它只是在拟合噪声所以训练Loss继续降低面验证指标不动。这种情况适合用正则化、数据增强、或者更早的早停策略来解决再盲目训下去没有意义。第三个原因评估代码本身有问题。有些人的验证集因为shuffle方式没固定每次都跟其他阶段混合指标抖动巨大根本看不见真实趋势。我的检查方式是把验证集固定多次跑同一个权重看指标是否稳定如果波动超过1%第一嫌疑就是评估逻辑而不是训练逻辑。4.2 显存溢出不等于代码写错新手看到CUDA out of memory通常会以为自己代码写得不行但显存溢出很多时候是合理的工程问题。我的排查顺序是先看是不是批次太大再看是不是输入长度没有padding到统一区间最后看是否有缓存没有被释放。批次太大是最容易解决的把batch_size降下来或者开梯度累积。输入长度的问题容易被忽略很多人给文本按最长样本的99%分位做截断后剩下的极长文本偶尔会撑爆显存。解法是训练时对长度超过上限的样本截断或者把它们放进一个单独的小batch里处理。第三个“缓存没有释放”则和代码逻辑有关比如每步都生成新的计算图却没有用torch.no_grad()做推理或者用了一个会跨epoch保留梯度的动态数据结构。还有一个我实际验证过很有效的手段用torch.cuda.max_memory_allocated()统计峰值显存用量先跑一个batch看基准再逐步增加batch_size。这比你每次报错之后瞎猜要高效得多。只要峰值显存不超过物理显存的一个安全比例比如80%到90%训练一般不会中断。4.3 模型不收敛先检查哪几个地方Loss完全不下降的排查顺序跟很多人想的不一样。我第一个查的是数据不是学习率。你先打印一批样本看看输入和标签对齐没有。文本任务里最常见的问题是把标签错位了一个token或者做了padding之后标签没有做相应的mask。数据没问题的话再查模型输出范围比如二分类任务的logit是否都是同一个值这能说明模型有没有从数据中学到任何信号。接下来查学习率。学习率太大Loss会在一个区间剧烈震荡然后发散学习率太小Loss下降极其缓慢几乎像一条水平线。我通常会在开始大规模训练前跑一个“学习率扫描”从小到大设置几个实验比如1e-5到1e-2之间取几个值每轮只训练几百个batch看哪段区间Loss下降最稳定。还要检查优化器的配置。Adamw和Adam看似接近但weight decay有时候不只是“正则化”这么简单它能影响最终的泛化表现。我把weight decay设置过高时模型训练经常出现Loss下不去的情况。另外不要忘了loss本身的计算逻辑比如一个复杂的机器翻译项目如果计算mask不够严谨padding位置也会产生Loss模型会一边提升有效位置的表现一边降低无效位置的Loss整体指标自然就会混乱。5. 这个项目还可以继续扩展的部分5.1 把训练链路升级成能反复使用的流程从零搭建的最大收获不是某一个实验的结果而是一条能复用的流程。我现在会把这个目录进一步抽象成模板配置管理、数据清洗、训练循环、评估模块各自相对独立换任务时只替换模型和数据组件。这样新项目能在一到两天内跑通整个基线。往这个方向扩展时有两个关键点第一把流程里的“实验记录”做成自动化。每次训练开始自动采集代码commit、数据版本、配置内容和环境依赖到实验目录内。第二把数据版本管理纳入进来。原始数据在一段时间内可能会被补充更新如果不加版本你很难知道某个实验结果到底对应哪一批数据。这个其实在工业数据链路里是必备的自己做小项目时也值得提前设计。后面如果要加入更大规模分布式训练从零搭的那套逻辑也能平滑升级。只需要把数据采样改成分布式采样器把梯度同步改成跨卡通信训练部分的核心结构不需要推翻。这就是框架透明带来的底气。5.2 把训练链路升级成能反复使用的流程模型训练完不是工程的终点最后总要面对推理和服务。我一开始只写了离线评估的脚本后面又补了一个轻量的推理模块。核心思路是在离线和在线推理之间共用同一套数据预处理代码。很多人训练时用一种方式切词、tokenize服务时又用框架默认的预处理方法结果模型分毫未改上线后指标却掉一大截原因几乎必然在这里。部署时我建议先做单机批量推理脚本加上一个最简单的HTTP服务接口验证延迟和吞吐。如果响应时间超标再去考虑模型量化、剪枝或者增加缓存。这些手段都要基于可量化的指标来做判断不要为了炫技而做。这个项目的下一步我打算把混淆矩阵、样本级预测结果、线上反馈日志统一存下来做成一套持续追踪模型表现的闭环。因为AI工程真正的价值不是训练完那一刻模型多准而是它在真实环境里长时间稳定可维护。说到底从零搭建标的从来不是“不用现成库”而是“能讲清楚自己系统里每一个环节的前因后果”。把这条链路走通之后你再回过头去看各种框架会发现它们不再是黑箱而是你可以按需借用的工具。这个感觉跟一个新手时期完全不一样。
返回列表