尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从零构建AI工程:文本分类模型训练到推理部署全复盘

发布时间:2026/9/29 18:50:54

资讯中心
01
ARTICLE

从零构建AI工程:文本分类模型训练到推理部署全复盘

从零构建AI工程:文本分类模型训练到推理部署全复盘
元旦假期没闲着把手头上一个叫ai-engineering-from-scratch的项目完整跑通了一遍正好借这个机会把整个过程中的思路、踩坑和关键细节全部沉淀下来。这个标题直译过来是从零开始做AI工程但真正落地的内容比这四个词要厚重得多——它不只是一段段代码的堆叠而是一整套从需求拆解、数据准备、模型训练到推理部署的完整方法论。之所以要把这个项目放在从零开始的视角下做而不是直接调一套封装好的工具链是因为我越来越觉得AI工程这行的核心壁垒不在会不会用某个库而在能不能理解系统每一层的真实行为。你会用框架不等于你能排查问题你能训练模型不等于你能让它稳定上线。这篇博文我会完整复盘这个项目的设计思路、技术选型、实操流程和踩坑记录适合那些想真正吃透AI工程全链路、而不是停留在调用API层面的开发者阅读。不论你是刚入门还是已经有几年开发经验这篇文章都能给你一些可落地的参考。1. 项目整体设计与思路拆解1.1 从零开始拒绝黑盒依赖启动这个项目之前我先做了一个自我审视过去几年写的AI相关代码有多少是我真正能从头解释清楚的模型架构用的是什么、激活函数为什么选这个、学习率调参的边界在哪、推理服务在高并发下会怎样劣化——如果每一项都依赖别人的封装出了问题只能对着文档干瞪眼。所以我给这个项目定了一个近乎苛刻的原则不依赖任何一层说不清内部机制的黑盒所有核心环节必须手写理解、手动掌控。这不是说框架和第三方库不好而是说盲目依赖会让你丧失判断力。举个例子很多人用现成框架训练模型loss不下降就换优化器换完还不行就调学习率最后实在不行就加数据——整个过程完全是在碰运气因为你根本不清楚模型内部哪一层出了问题。从零开始构建的意义就在于你要被迫理解每一层的输入输出、梯度流动方向和数值分布变化这个过程虽然慢但一次跑通后你的排错能力会有质的提升。1.2 技术选型背后的权衡逻辑在做技术选型时我没有一上来就追求最前沿的模型或最复杂的架构而是遵循了一个实际原则用最简单的方案解决核心问题在关键路径上保留可控的复杂度。整个项目我选了Python作为主语言配合PyTorch做深度学习框架服务端用FastAPI做推理接口前端接了一个非常朴素的可视化页面。这套组合听起来平平无奇但恰恰是因为它足够标准我才能在排查问题时把变量控制在最小范围内。每一层选型我都明确记录了理由。比如为什么不直接用TensorFlow不是因为PyTorch比它好而是因为PyTorch的调试体验更接近写普通Python程序这对于从零做工程的人来说太重要了——你能在任意位置打断点、print张量的shape和数值这种透明的调试体验能把心智负担降到最低。再比如为什么用FastAPI而不用Flask因为后续要承担一定的并发推理请求FastAPI原生的异步支持和自动生成API文档的能力省了我不少功夫而且它的性能表现在同类框架中确实有优势。1.3 模块拆分与管线连接方式整个项目的架构我拆成了五个模块数据层、训练层、推理层、服务层和可视化层。数据层负责原始数据的采集、清洗和特征工程训练层负责模型的定义、训练循环的编写和权重管理推理层把训练好的模型封装成可调用的预测函数服务层用FastAPI把推理函数暴露成HTTP接口可视化层则负责把预测结果以图表和仪表盘的形式呈现出来。模块之间我定义了严格的接口规范——数据层的输出格式是统一的DataFrame结构训练层只接受这个结构推理层只依赖模型权重文件和服务配置服务层只转发请求参数。这种模块划分方式一开始会显得繁琐但后来越到工程后期越能感受到它的价值。你可以独立升级数据预处理逻辑而不动其他代码你可以把训练好的模型文件拷贝到其他环境直接加载推理你可以对服务层做压测而不关心底层的模型实现。模块间松耦合、模块内高内聚这条软件工程的老原则在AI项目里同样适用而且比传统软件项目更重要——因为AI模型本身就是数据、代码和权重的复合体一旦耦合过深排查问题的成本会成倍上升。2. 核心细节解析与实操要点2.1 数据准备质量比数量更关键数据源选择与实际采集过程数据清洗的优先级排序数据切分的三种方式对比我在项目里用的数据集是公开的中文新闻分类数据集包含娱乐、体育、财经、科技等八个类别的文本。很多人拿到数据的第一反应就是直接开训这其实是大忌。我花了两天时间做数据清洗优先级依次是去重、去噪、纠错、平衡。去重不只是删完全重复的样本也包括去掉高度相似的长文本——两篇新闻可能来源不同但内容相同这种数据会把训练集的多样性拉低导致模型泛化能力下降。去噪主要是去掉HTML标签、特殊符号和无意义的短文本。纠错针对的是错别字和繁体中文我写了一套规则结合人工抽检来处理。平衡这一步最容易被人忽视。原始数据里体育类的样本是科技类的三倍多如果直接拿去做训练模型会对体育类文本产生明显的偏好这是因为训练过程中它对这类样本见得最多。我用了欠采样和过采样的组合策略对样本多的类别随机抽取一部分对样本少的类别用回译加同义词替换的方式扩充。经过处理后八个类别的样本量都控制在15000条左右这个数量对于验证整个工程链路来说已经足够。数据切分我采用了经典的训练集、验证集、测试集三段式划分比例是811。这里有一个容易被忽视的细节切分前一定要先做shuffle而且shuffle的随机种子要固定。如果不固定种子每次跑出来的实验结论都可能不一样你会分不清两个模型的效果差异到底来自模型本身还是来自数据划分的偶然性。我固定了种子值为42这样后续所有实验都可以复现。2.2 模型设计选择从0手写而非直接调用现成模型的原因模型部分我没有直接加载预训练模型而是从零手写了两套方案做对比一套是TextCNN另一套是BiLSTMAttention。这两套都是深度学习文本分类领域的经典结构虽然算不上最新但对于理解底层原理来说是最好的教材。TextCNN的思路很像用放大镜扫描一整篇文章找关键词。它通过多个不同宽度的卷积核我用的是2、3、4三种宽度在文本序列上滑动相当于分别捕捉2-gram、3-gram和4-gram级别的局部信息然后经过最大池化提取每个特征图里最强烈的信号。这个结构的优势是并行度高、训练速度快、参数量小非常适合作为第一个从零手写的模型。BiLSTMAttention则是另一条路线。BiLSTM用两个方向的LSTM同时读一遍文本捕捉上下文双向依赖信息Attention机制再对每个时间步的隐藏状态做加权求和让模型自己学习哪些位置的词更重要。这套结构更灵活但训练速度明显慢于TextCNN。我在写代码时没有直接用nn.LSTM而是连LSTM的单元内部都自己推导了一遍——输入门、遗忘门、输出门和候选记忆的更新公式逐个验证这一步走完我真的对循环神经网络的理解上了一个台阶。对比这两个模型时我记录了训练时间、收敛速度和验证集准确率三项指标。在同样条件下TextCNN收敛明显更快差不多训练10个epoch就稳定在92%左右的准确率BiLSTMAttention需要更长的训练时间但最终准确率能达到93.5%。这个差距不算大却让我对模型复杂度是否一定带来更好的效果这个问题有了更清醒的认识。2.3 训练调参学习率、Batch Size与正则化的配合训练这块我踩了不少坑最大的感触是调参不是玄学而是对每个参数行为逻辑的掌握。以学习率为例我一开始直接用固定学习率0.01结果loss曲线在训练中段开始剧烈震荡。后来我换成了warmup余弦退火的策略——前几个epoch让学习率从很小的值线性上升到达峰值后再按余弦曲线逐渐衰减到接近零。这样做的好处是训练初期梯度更新幅度温和可以避免模型参数在起始阶段就跑飞训练后期学习率变小能够帮助loss稳定收敛到更优的区域。Batch Size的设定直接影响训练速度和模型效果。我的经验是在单卡显存允许的范围内Batch Size尽量取大一些因为更大的batch相当于用更准确的梯度方向做更新训练过程更稳定但也不能太大否则模型容易陷入尖锐极小值泛化能力反而下降。我在TextCNN模型上用Batch Size 64和128各试了一轮64时验证集准确率92.1%128时又回升到92.3%差别很小但每epoch的训练时间差了将近一倍最终我选了64做默认值。正则化方面我用了三层保护Embedding层做Dropout、模型中间加了Weight Decay、训练过程中配合早停机制。关于早停我要多说一句——很多新手会忽略这个机制导致模型过拟合了都不知道。我的设置是patience设为5个epoch也就是连续5个epoch验证集准确率都没有刷新最好成绩时就停止训练并回滚到历史最优参数。这一步看起来简单实际效果却非常好让我节省了大量不必要的训练时间。3. 实操过程与核心环节实现3.1 从环境搭建到数据管线的落地部署环境我使用的是Ubuntu 22.04 Python 3.10 CUDA 11.8的组合。有个细节值得提醒Python虚拟环境一定要在项目初始化的时候就建好而不是装了一堆包之后再想起来隔离环境。我用venv建了独立的虚拟环境所有依赖通过requirements.txt来管理并在文件头部锁定了关键库的版本号——torch2.0.1、transformers4.35.0这些必须写清楚。很多人不锁定版本号半年后重新拉代码跑就会因为依赖版本不兼容而抓狂这是完全可以通过一个简单的习惯避免的。数据管线的落地我写了一个prepare_data.py脚本完整流程包括四个函数load_raw_data负责从原始路径读取数据clean_text封装了上面提到的清洗逻辑balance_dataset实现了欠采样和过采样的组合策略split_and_save负责做三段切分并把结果以parquet格式落盘。为什么用parquet而不是csv因为parquet压缩率高、读写速度快数据量大时优势尤其明显。这个脚本跑完只需要十几秒得到三个文件train.parquet、val.parquet、test.parquet。从这个环节你就可以体会到一个道理数据处理的质量直接决定模型的天花板你在清洗和切分上多花的时间最后都会在训练效果上体现出来。3.2 手写训练循环从数据处理到反向传播我没有直接用PyTorch的Trainer封装而是手写了完整的训练循环这是我认为整个项目收获最大的部分。训练循环的核心代码虽然不长但每一行都要求你真正理解训练在底层发生了什么# 核心训练循环核心部分 for epoch in range(max_epochs): model.train() total_loss 0.0 for batch_idx, (input_ids, labels) in enumerate(train_loader): input_ids input_ids.to(device) labels labels.to(device) optimizer.zero_grad() logits model(input_ids) loss criterion(logits, labels) loss.backward() # 梯度裁剪防止梯度爆炸的关键操作 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() # 每100个batch打印一次loss便于实时观察训练状态 if (batch_idx 1) % 100 0: avg_loss total_loss / (batch_idx 1) print(fEpoch {epoch1} | Batch {batch_idx1} | Loss {avg_loss:.4f})这里我要重点说两个细节。一是optimizer.zero_grad()的位置——它必须在每次前向传播之前清零梯度否则梯度会跨batch累积。我早期踩过这个坑loss曲线毫无规律地跳动排查了大半天才发现是漏了这一行。二是梯度裁剪——当loss突然变成NaN或者模型输出数值异常时排除数据问题后第一反应就应该是梯度爆炸。我给max_norm设了5.0整个训练过程都没有再出现loss异常跳变。模型训练的过程是一次信息流动的完整演示输入文本先经过Embedding层变成向量经卷积或循环层提取特征再经注意力层加权最终在分类层输出概率分布。损失函数计算出的误差通过反向传播逐层返还优化器根据梯度来调整每一层的权重参数。第一步正向传播计算出当前模型的表现第二步反向传播算出每个参数的调整方向第三步优化器确定具体的调整幅度——这三步不断重复就像一个人反复练习同一份考卷总能越来越熟练。3.3 推理服务从模型文件到可用的HTTP接口模型训练完后我把最优权重保存为best_model.pt这里面包含了模型的参数字典和优化器状态。接下来要做的是把它封装成可供调用的推理服务。推理和训练有一个关键区别训练时你希望模型有随机性Dropout层生效来防止过拟合但推理时你必须把模型切换到eval模式并且关闭梯度计算。我用以下这段代码做了封装def predict(text: str): # 配置加载和预处理 cfg load_config() tokenizer get_tokenizer(cfg) encoded tokenizer.encode(text, max_lengthcfg.max_len, truncationTrue, paddingmax_length) input_tensor torch.tensor([encoded], devicecfg.device) # 加载最优权重并切到推理模式 model TextCNN(cfg).to(cfg.device) model.load_state_dict(torch.load(cfg.model_path, map_locationcfg.device)) model.eval() # 关闭梯度计算 with torch.no_grad(): logits model(input_tensor) prob torch.softmax(logits, dim1) pred_cls torch.argmax(prob, dim1).item() confidence torch.max(prob, dim1).values.item() return {class: class_names[pred_cls], confidence: round(confidence, 4)}在做推理服务时还有一个性能优化的小技巧值得说。很多人每次请求都加载一次模型权重这在并发量上来之后会产生严重的IO瓶颈。我改成在FastAPI启动事件中加载一次权重到内存之后所有请求都共享这同一份模型实例推理耗时从平均650毫秒降到了80毫秒左右。这个优化思路本质上就是把高频且耗资源的初始化操作移到服务进程内只做一次。FastAPI接口部分我用了一个简洁的POST /predict端点接收JSON格式的{text: ...}返回预测类别和置信度分数。测试阶段我用curl验证了接口的正确性又用locust做了简单的并发压测——在100个并发用户下接口的P99延迟维持在300毫秒以内这个表现对于一个基础文本分类服务来说已经在可用范围之内。3.4 可视化层让模型结果看得见一个完整的AI工程项目不能只停留在接口返回JSON数据这个层面因为数据分析师、业务人员甚至你的领导都不会盯着命令行看你模型的输出。所以我在最后加了一个简单的可视化层用Streamlit写了一个轻量级页面用户输入一段文本点击预测按钮就能看到分类结果和置信度条形图。同时我还实现了分类结果分布统计、样本错分矩阵展示两个辅助页面方便从宏观上评估模型表现。这里有一个非常实用的经验可视化层的实现要尽可能薄它只是调用服务层暴露的HTTP接口而不应该直接加载模型。也就是说你在浏览器里点按钮前端请求的是FastAPI的服务接口服务接口再调用推理逻辑推理逻辑再加载模型——整个链路保持了模块间解耦。这样做的好处是可视化层坏了不会影响推理服务推理逻辑要更新也不需要动前端代码。很多小项目最后变成一坨意大利面就是因为每层之间没有清晰的边界。4. 常见问题与排查技巧实录4.1 Loss不降或剧烈震荡的排查思路文本分类项目里我碰到最频繁的问题就是loss不降或者训练到中途开始剧烈震荡。排查这个问题的思路不能是瞎猜而是要按步骤来第一步先检查数值看输入数据里有没有NaN标签有没有错位第二步检查模型输出看看logits的范围是否异常第三步检查梯度看看反向传播之后梯度的范数是否过大或过小。我用一个简单的打印脚本监控了三个指标一旦发现问题就能快速定位到具体环节。如果loss在训练初期完全不动那大概率是学习率设置过小或者模型初始化出了问题。如果loss在训练中段开始震荡多数是学习率过大或者是batch size太小导致梯度方向不稳定。如果loss降了一段后突然变成NaN基本可以锁定是梯度爆炸了梯度裁剪和降低学习率是首选的应对手段。我强烈建议初学者每次训练都把loss曲线画出来不要只看终止值曲线的形状会告诉你大量的信息。4.2 模型过拟合的识别与处理文本分类模型参数量不大但过拟合的风险依然存在。一个非常典型的现象训练集准确率持续上升到了98%但验证集准确率卡在90%就不动了训练集和验证集之间的差距越来越大——这就是过拟合的标准化信号。我处理过拟合按下面这个优先级来先增加Dropout比例从0.3调到0.5如果还不够就减小模型容量——把隐藏层维度从256降到128最后才考虑增加数据量因为数据采集和清洗的成本最高。早停在这里再次发挥作用。我在训练时做了完整的训练曲线记录用matplotlib把训练集和验证集的loss画在同一张图上两条线开始分道扬镳的那个点就是模型开始过拟合的时刻。有了这张图你可以精确地判断到底训练到第几个epoch该停止而不是凭感觉拍脑袋。4.3 推理阶段的性能优化推理阶段最容易忽视的问题是数据预处理成为瓶颈。很多人把精力放在优化模型结构上却忽略了tokenizer和文本编码环节同样消耗大量时间。我最初版本每次推理都要重新初始化tokenizer这部分耗时占了整个推理时长的40%以上。后来我把tokenizer和模型一起放到服务启动时初始化推理耗时立刻下降了一个级别。另一个性能优化点是批量推理。当你有多个文本需要预测时不要一个请求跑一次模型而是把多个文本拼成一个batch一次性跑。PyTorch的GPU推理天然支持batch操作batch size设为32时吞吐量比逐个推理高出将近10倍。这个技巧实测下来非常管用尤其在离线批量预测的场景下能帮你省下大把宝贵时间。GPU内存管理同样值得关注。我用torch.cuda.empty_cache()在每次大batch推理后清理缓存碎片避免长时间运行后显存碎片化导致OOM。如果显存真的不够用还有一个更彻底的办法是启用梯度检查点技术用计算换内存但这就需要在训练阶段就做好规划了。4.4 错误排查速查表经过这个项目的完整调试我把最常遇到的问题整理成了下面的速查表方便大家直接对照排查现象可能原因解决方案Loss一开始就是NaN学习率过大输入包含NaN降低学习率检查数据清洗逻辑Loss中途震荡不降学习率过大batch size太小换warmup余弦退火增大batch size训练集高但验证集低模型过拟合增大Dropout减模型容量早停推理速度极慢模型未切换到eval模式tokenizer重复初始化切换到eval模式把初始化移到启动时并发稍高就崩溃每次请求都重复加载模型服务启动时加载一次请求共享实例显存溢出OOM推理batch过大缓存碎片过多减小batch size定期清理显存缓存预测结果明显偏差训练和推理的预处理逻辑不一致统一tokenizer参数和编码流程4.5 关于从零的一点个人感悟跑完这个项目我最深的感受是AI工程能力不会因为你会调几个现成库而自动产生它来自你亲手把每个环节从底层推演到上层的完整过程。当你能为每个选择给出理由能在工程崩溃时快速定位到具体环节能把训练效果和服务性能都掌控在预期范围内——这种能力才是AI工程的核心竞争力。从项目0.1版本的TextCNN到0.2版本加入BiLSTMAttention双模型对比再到0.3版本的完整服务化部署整个推进过程本身就是一次深度的自我精进。我现在回头看手写一个训练循环的价值远大于运行十次别人写好的框架代码。理解原理不是一句空话它会在你遇到问题的时候转化为实际的排错能力和设计判断力。如果你也想做类似的项目我的建议是不要再纠结到底该学哪个框架、用哪个预训练模型先把手头一个小问题用最朴素的方式完整解决一遍。在解决的过程中你会发现你真正缺的不是知道什么而是做过什么。动手把第一条端到端的AI工程链路跑通比收藏一百篇教程都有用得多。这个项目的代码我已经整理好放在GitHub仓库里了欢迎去提issue交流你踩到的坑。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。