ARTICLE DETAIL

资讯详情

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

AI工程从零到上线:文本分类项目完整实战与踩坑指南

AI工程从零到上线:文本分类项目完整实战与踩坑指南 “AI工程从零开始”这句话我见过太多人理解成“从零开始学Python、学PyTorch、学深度学习”。真做过项目的人会告诉你不是这么回事。AI工程的核心不是训练出一个模型是走通一条从“想法”到“数据”再到“模型”最后到“能被人稳定调用”的完整链路。我见过太多卡在半路的案例Notebook里准确率95%一部署就崩本地跑得飞快换个环境全部报错训练Loss降得很好线上效果一塌糊涂。这些问题都是因为只盯着“模型”这一个点没有把“工程”这条线走通。这篇文章我把自己做AI工程项目的完整路线、实操细节、踩坑记录原原本本摊开来讲。适合三类人正在做第一个正式AI项目、不想只停留在跑通Demo的初学者写过不少训练脚本、但没系统梳理过工程化流程的开发者需要在团队里牵头做AI落地、要把想法变成可交付产物的人。1. AI工程到底在解决什么问题1.1 你缺的不是模型是“从模型到服务”的这段路很多技术帖子教你调参、教你刷榜但没人告诉你真实项目里一个模型从“能跑”到“能用”中间隔着一片海。这片海名字叫工程化。我见过一个很典型的场景算法工程师花了两周微调一个模型离线指标涨了三个点老板很高兴说赶紧上线。结果部署时发现模型推理一次要200毫秒线上接口要求100毫秒输入文本里有大量乱码训练数据里根本没见过还有一批用户传的是图片不是文字整个pipeline接不住。这哪是模型问题这是从头到尾的工程问题。研究者的终点是“准确率”工程者的终点是“有人用、用得稳”。这意味着你得同时关心数据质量、评测口径、失败预案、部署环境、监控反馈。模型只是这个系统里的一环不是全部。1.2 为什么“从零开始”的路线最有价值直接拿HuggingFace现成模型微调或者套一个成熟的训练框架当然能很快跑出结果。但我强烈建议你至少完整走一遍“从零开始”的流程——不是从写神经网络开始而是从“自己动手处理数据、定义任务、搭训练循环、写评估脚本、做模型导出、写推理接口”开始。原因很简单框架帮你把路铺好了你看不到路下面的地基。你以为数据加载是天经地义的直到你自己写才发现不同格式的编码、字段缺失、标签分布偏移都是要处理的细节你以为评估就是print一个accuracy直到你遇到一个数据类别严重不平衡才发现准确率毫无意义。我自己带项目时有个判断标准一个工程师能不能在没有框架的情况下用一个最小流程跑通一个真实任务。能说明他理解每个环节在干什么不能说明他只是把框架用熟了。这两者的差距在项目遇到环境依赖冲突、线上数据分布漂移、推理性能瓶颈时会瞬间暴露。1.3 AI工程的能力模型三条线都要硬做AI工程不是只懂算法就行它需要三条线同时在线能力线具体内容对应的常见问题算法线模型选型、训练方法、参数调整、效果优化Loss不降、过拟合、效果平台期数据线数据采集、清洗、标注、增强、评测集设计脏数据、标注不一致、线上线下分布不一致工程线环境管理、代码结构、训练部署流程、推理性能环境冲突、OOM、推理延迟高、模型文件管理混乱新手通常只盯着第一条线踩了坑才发现后面两条才是大头。实际项目里数据线花的时间最多工程线决定模型能不能落地算法线反而是最容易被“现有工具”替代的部分。想清楚这个你的精力分配才不会跑偏。2. 从想法到上线一张可复用的路线图2.1 前置准备不是从“0”开始是从“能跑”开始“从零开始”最容易让人误入的歧途是从装显卡驱动、配CUDA开始。除非你要自己从物理层面部署机房否则千万不要这么干。你的起点应该是一套能跑通的最简环境。我建议的前置条件很朴素一台至少有8G显存的GPU机器没有就用云GPU按小时租Python 3.10conda或者venv做环境隔离。框架层面PyTorch是主力HuggingFace Transformers作为模型和Tokenizer的标准接口。这些就是全部。别在环境上追求“最新最全”你会发现项目后半段比的不是谁环境炫而是谁能稳定复现。有一个我吃过亏的经验新建项目时第一时间把依赖版本固定写进requirements.txt最好再生成一个lock文件。别指望“等以后再整理”等到你踩完所有坑回头整理版本时你已经分不清当时是哪个版本跑出来的结果了。我现在每个项目第一件事就是初始化一个干净的虚拟环境然后每装一个关键包就记录版本后面能少掉一半头发。2.2 把需求翻译成一条可执行的pipeline绝大多数项目失败不是因为技术难度是因为需求没有翻译成工程任务。你拿到的需求往往是“帮我们做一个评论审核系统”“给客服做一个自动分类工具”“做一个知识库问答机器人”。这种描述不能直接开工你要把它拆成pipeline。我习惯拆成七个环节数据采集、数据清洗与标注、评测集设计、基线模型、训练迭代、评估分析与归档、部署与监控。每个环节都要有明确的交付物数据环节交付的是“清洗脚本数据集文件”评测环节交付的是“评测脚本指标定义”部署环节交付的是“推理接口性能报告”。关键原则先定义“什么算做对了”。很多项目在训练开始后才争论评估标准结果发现数据集跟指标不匹配整个流程要返工。正确做法是在动手处理数据之前就先把评测脚本写好拿一个最简单的基线跑通后面的所有优化都以这个评测为准。没有评测规则的优化都是自我感动。2.3 模型选型别一上来就追大模型现在聊AI项目很多人开口就是“要不要上大模型”。我理解这个冲动大模型确实能在很多任务上直接给出不错的效果。但在一个真实工程里模型选型要考虑推理成本、响应速度、硬件资源、可维护性这些往往比“更高一个点的准确率”重要得多。我的选型逻辑分两条路。验证路先用一个中小规模模型比如BERT-base级别的几亿参数模型快速验证数据和pipeline是否靠谱这阶段目标是跑通全流程不是刷指标。上线路如果任务对效果要求极高且资源充裕再考虑更大规模模型如果任务相对垂直且数据量不大中小模型微调已经够用上线成本低得多。实际案例里很多文本分类任务用几百M的模型微调就能达到95%以上准确率完全没必要请一个几百B的大模型来做响应又慢又贵。记住一个观点模型大小跟项目成功概率没有任何线性关系。跟项目成功强相关的是数据是否干净、评测是否合理、pipeline是否稳定、失败预案是否充分。3. 实操实录一个文本分类项目从零到可部署的完整过程这一章我直接用一个“工单分类”项目走一遍全流程。需求很简单客服系统里每天有大量用户提交的工单文本需要自动判断工单属于哪个类别比如“退货退款”“技术支持”“账号问题”“物流查询”等然后转给对应的处理小组。技术选型用BERT中文小模型微调。下面就按真实做事顺序来所有代码都能直接用。3.1 数据先行5条脏数据比50条干净数据值钱很多教程一上来就让你去下载某某公开数据集真实项目里根本没这好事。我建议从零开始搭数据集的第一件事是先人工收集一小批真实数据哪怕只有三五十条。为什么先要少的因为少样本能让你快速看清任务的真实形态。你会发现自己以为的“分类规则”在真实文本面前有多天真用户会写错别字会用口语化表达会一句话里掺杂两个意图会发一个只有半截的句子。这些东西你在臆想的数据集里永远见不到。我自己做文本项目的标准流程是先手工标注50条真实数据不管准确率先把数据的“脾气”摸清楚。下表是这类数据常见的原始形态原始文本人工判断类别我要退了这个东西质量太差了退货退款登录一直提示密码错误怎么办账号问题我的快递卡在运输中三天没动了物流查询这个功能怎么用啊找不到入口技术支持麻烦帮我看看订单号8934到哪了物流查询有优惠券吗我想买那个套装售前咨询注意看最后一条“有优惠券吗”严格说不是被动的售后请求而是售前咨询。你不拿真实数据过一遍根本不会意识到这六个类别之外还藏着第七类。这种认知只有从真实样本里长出来。3.2 数据清洗与划分先定评测规则再动模型拿到原始数据后的第一件事不是训练是清洗和划分。我见过太多人直接拿原始数据开训最后模型被乱码带偏。清洗的核心是“去脏但不破坏语义”。不同项目策略不同这里给出通用思路去掉HTML标签、URL、多余空白符、不可见字符。统一全角半角中文场景特别重要。修正编码乱码比如UTF-8读成GBK的情况。去掉过短和无意义文本但不迷信长度有时候短文本恰好是有效意图。清洗代码很简单难在你要理解每一条规则为什么存在。下面这段函数我几乎每个文本项目都会复用import re def clean_text(text: str) - str: # 去除 HTML 标签 text re.sub(r[^], , text) # 去除 URL text re.sub(rhttp\S|www\.\S, , text) # 全角转半角中文中标点转英文标点统一格式 text text.replace(, ,).replace(。, .).replace(, !).replace(, ?) # 压缩连续空白 text re.sub(r\s, , text).strip() return text清洗完做数据划分。文本分类任务常规做法是训练集70%、验证集15%、测试集15%。来自同一个用户的多条工单要全部划分到同一个集合里防止数据泄漏导致评估结果失真。更重要的是划分必须发生在任何统计观察之前。你要是先看了全量数据再做划分很容易在写代码时不自觉地“记住”测试集特征等于作弊。数据划分为三份各有用途训练集喂给模型学习验证集在训练过程中做早停、调参测试集只在最终评估时使用一次并且必须保证模型从未见过。这个纪律性比任何训练技巧都重要。3.3 基线模型与评估脚本没基线之前一切调参都是瞎调直接上BERT微调之前强烈建议先训练一个简单的基线模型。基线不需要效果好目的是给你一个“参照系”。没有基线你无法判断后面深度模型提升的准确率里有多少是模型结构带来的有多少是数据质量带来的。文本分类最简单有效的基线是TF-IDF 逻辑回归用scikit-learn几行代码就能完成from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline import joblib pipeline Pipeline([ (tfidf, TfidfVectorizer(max_features5000, ngram_range(1, 2))), (clf, LogisticRegression(max_iter1000)) ]) pipeline.fit(train_texts, train_labels) joblib.dump(pipeline, baseline_tfidf_lr.joblib) val_acc pipeline.score(val_texts, val_labels) print(fBaseline Validation Accuracy: {val_acc:.4f})同时你要把真正的评估脚本写出来。光看Accuracy在类别不平衡时会被骗必须看每个类别的精确率、召回率、F1还有混淆矩阵from sklearn.metrics import classification_report, confusion_matrix y_pred pipeline.predict(val_texts) print(classification_report(val_labels, y_pred, target_nameslabel_names)) print(confusion_matrix(val_labels, y_pred))这一步的产出物不是“一个还不错的模型”而是一套评估代码。后面的所有实验都跑同一套脚本结果才能公平对比。我见过有人每次实验都现场写几行验证代码今天看accuracy明天看f1完全是给自己制造混乱。评估脚本固定后你才能真正开始做科学实验。3.4 深度学习训练从数据加载到模型微调的关键细节基线确认后开始切到深度学习模型。这里我用HuggingFace生态记住一个原则能用标准接口解决的事不要自己造轮子。Tokenizer、Dataset、Trainer这些接口比自己手搓稳定太多了。模型结构选择这个任务用bert-base-chinese就够。它的中文处理能力经过大规模预训练验证模型大小在单卡上训练毫无压力。如果你的任务是在英文场景对应换成bert-base-uncased即可。Tokenizer和Dataset加载代码from transformers import AutoTokenizer, AutoModelForSequenceClassification model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained( model_name, num_labelslen(label_names) ) label2id {label: i for i, label in enumerate(label_names)} id2label {i: label for label, i in label2id.items()} def encode(examples): return tokenizer(examples[text], truncationTrue, paddingmax_length, max_length128) from datasets import Dataset train_dataset Dataset.from_dict({text: train_texts, label: [label2id[t] for t in train_labels]}) train_dataset train_dataset.map(encode, batchedTrue)训练参数这里容易踩坑。我用一套自己的默认值起步实测稳定参数值说明learning_rate2e-5BERT微调的标准学习率再大容易破坏预训练权重batch_size168G显存能跑得动再大要看显存余量num_epochs5配合早停防止过拟合max_length128工单文本一般不长够用且省显存weight_decay0.01轻微正则稳定训练训练过程代码我用transformers的Trainer封装训练循环。很多人喜欢手写train loop我理解那种想要掌控感的心态但实际项目中Trainer真的更省心from transformers import Trainer, TrainingArguments training_args TrainingArguments( output_dir./checkpoints, learning_rate2e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs5, weight_decay0.01, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1, logging_dir./logs, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetval_dataset, tokenizertokenizer, ) trainer.train()有个细节值得展开说为什么要用load_best_model_at_endTrue因为深度学习训练过程中验证集效果不是单调上升的。模型可能在第三轮达到最佳在第五轮过拟合了。如果不好好选最佳checkpoint你最后保存的可能是“已经退化”的模型。True的作用是训练结束后自动加载验证指标最好的那一次权重。这个参数救了无数个实验我见过太多人因为没开这个拿着过拟合模型上线后骂效果差。3.5 模型导出与推理接口让模型离开Notebook训练完模型接下来是最容易被忽视但最能区分“Demo”和“产品”的环节让模型能脱离训练环境被调用。第一种方式是保存为Transformers标准格式适合PyTorch生态内部使用model.save_pretrained(./final_model) tokenizer.save_pretrained(./final_model)第二种方式是为了服务化部署导出为ONNX格式。ONNX的好处是推理框架多、跨语言支持好C、Java、Python都能加载并且在CPU上也能有不错的推理速度。用optimum库导出optimum-cli export onnx --model ./final_model ./final_model_onnx导出ONNX后写一个推理脚本加载ONNX模型做预测。ONNX Runtime在CPU上的推理速度实测通常比PyTorch原版快1.5到3倍对于没有GPU推理条件的场景很关键import numpy as np import onnxruntime as ort from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(./final_model_onnx) session ort.InferenceSession(./final_model_onnx/model.onnx) def predict(text: str): inputs tokenizer(text, return_tensorsnp, truncationTrue, paddingmax_length, max_length128) outputs session.run( None, { input_ids: inputs[input_ids], attention_mask: inputs[attention_mask], token_type_ids: inputs[token_type_ids], }, )[0] pred_id int(np.argmax(outputs[0])) return id2label[pred_id] print(predict(我想退货东西坏了))部署层面如果只是内部工具用Flask或FastAPI包一层HTTP接口就够了from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class Item(BaseModel): text: str app.post(/predict) def get_prediction(item: Item): return {label: predict(item.text), confidence: ...}这段就够内部系统用了。如果线上并发高、稳定性要求高再考虑用专门的推理服务框架比如Triton或TorchServe但一开始别追求架构复杂度先把接口跑起来。3.6 一个小小的进阶操作置信度阈值与“拒判”工程里一个很实用的技巧是给模型加一个“我不确定”的出口。现实中模型不是每个输入都能精确分类的。有些工单写得太模糊硬分类等于瞎猜不如直接转人工。做法是看softmax输出概率最高概率大于某个阈值比如0.8才输出分类结果否则输出“不确定需要人工介入”。这个策略对线上效果的提升巨大因为它把“模型做错”转化成了“模型知道自己不知道”从产品角度讲诚实比硬撑重要得多。实现时只需要在Softmax输出后加一个阈值判断几分钟就能做出来但体验完全不一样。这个技巧还可以跟业务指标挂钩比如目标是“减少20%的人工处理量”同时又要求“不能让错误率超过1%”。简单分类做不到加上拒判阈值后模型只处理高置信度样本剩下的给人两个指标都能满足。这是工程思维比单纯堆模型效果更好用的典型案例。4. 现场实录从“能跑”到“能扛”的踩坑记录这一章全是真金白银换来的经验。我不讲漂亮理论只讲我在真实项目里见过、踩过的问题以及排查思路。4.1 五个高频翻车点和速查表现象可能原因快速解法训练Loss不降学习率过大/过小、标签有噪声、数据没归一化先跑几十步看趋势用2e-5起步试一遍检查标签是否错标训练Loss降、验证Loss反弹过拟合加正则、加dropout、早停、减少训练轮数验证集指标好、线上实际效果差评测集与真实分布不一致、数据泄漏检查数据划分是否混入用户在多个集合重新采集线上真实样本做测试推理速度慢模型太大、没有用ONNX/量化、batch太小换小模型、导出ONNX、开动态batch换环境跑不通依赖版本没固定用requirements.txtlock file锁版本用Docker固化环境4.2 排查方法论先看数据再看代码最后怀疑模型遇到问题我的排查顺序永远固定先看数据再看代码最后怀疑模型。为什么因为模型很少突然变笨但数据经常会莫名其妙变脏。举一个实际案例有个项目训练过程中指标突然暴跌团队第一反应是“模型参数出问题了”“学习率调坏了”。我让他们先看训练样本结果发现数据pipeline里有一段新增的代码把所有文本里的数字都替换成了固定占位符导致包含“订单号”“手机号”的样本全部失去区分度。数据错了再好的模型也白搭。具体排查顺序是这四步第一从训练集里随机抽20条样本打印出来肉眼看清洗结果是否合理。第二检查数据划分的种子random seed是否固定不固定会导致每次跑实验数据不一样。第三打印模型输入输出的shape确认维度对齐。第四最后才动模型结构和超参数。这四步走完80%的问题都能定位。4.3 那些pipeline里的小陷阱这里有三个容易忽略的细节值得单独拎出来记第一个是Token对齐。BERT类模型的输入是Token ID如果你的数据里有特殊标记、自定义词汇必须保证训练和测试时用同一套Tokenizer并且padding和truncation策略完全一致。很多人训练时max_length128推理时忘了传默认长度不同结果输入被截断效果凭空掉几个点。第二个是随机种子。深度学习训练里数据加载顺序、权重初始化、dropout都是有随机性的。不固定种子你每次跑的同一套代码结果都不同甚至没法判断是参数生效了还是运气生效了。项目开跑前在代码最顶部固定所有随机源Python、NumPy、PyTorch三处都要设置。第三个是ONNX推理时设备的设备对齐问题。ONNX模型虽然跨框架但如果你用CPU训练导出、推理时也要明确设备。我见过有人在GPU上训练、在CPU上推理没问题的但如果你用了GPU特有的算子CPU上可能不支持这时要留意算子兼容性。规律是能不用自定义算子就不用了省很多麻烦。5. 工具体系与长期沉淀最后一个层面聊的是把一个从零开始的项目变成一份能长期复用、可交底、可复现的资产。这一步是区分“做完一个项目”和“沉淀一套能力”的分野。5.1 从脚本到模块再到服务项目结构怎么组织临时脚本总会随着项目推进变得不可维护。我推荐一套简单的演进路线不要一上来就建复杂的微服务架构先让代码分家再谈扩展。起步阶段train.py、evaluate.py、predict.py三个脚本东西不多但职责要分清。进阶段抽出data_processing.py数据清洗函数、model_utils.py模型加载保存、config.py参数集中管理。成熟阶段变成一个标准的Python包目录长这样project/ ├── data/ │ ├── raw/ │ └── processed/ ├── src/ │ ├── __init__.py │ ├── data.py │ ├── model.py │ └── train.py ├── models/ ├── eval/ │ └── evaluate.py ├── config.yaml └── requirements.txt配置管理用YAML好处是参数和代码分离不用改代码就能调参。每条实验跑完把配置、代码版本、评估结果三个绑定在一起存档。这三个信息缺一个这条实验将来就无法追溯。5.2 评估看板与实验记录的仪式感做AI工程项目一定要有记录实验的习惯不是写日记是记关键信息日期、数据版本、代码commit号、模型结构、关键超参数、训练集验证集指标。就这几列再多不写。为什么要记录因为深度学习实验变量太多不记录的话三天后你看到“验证集指标突然涨了2个点”你根本想不起来是哪个改动带来的。我见过最悲惨的场景是实验跑出了最好效果但没人记得当时用的哪个数据版本、哪个参数组合导致线上复现不了只能从头再调。指标记录不追求花哨一张表格就够了。每个实验结果加一行长期积累后你对自己项目的数据和模型会形成一种直觉——“这个数据分布下这个模型大概能到多少分”。这种直觉全是记录喂出来的。5.3 项目“收尾”的正确姿势项目做完不是把代码往仓库一推就完事。我给自己定了一个“收尾清单”每次项目做完后对一下是否有一键运行的训练脚本和评估脚本是否有一个README说明项目背景、数据格式、模型结构、复现步骤是否导出过可直接部署的模型文件不只是训练权重是否锁定了依赖版本是否记录了“已知问题”和“未解决事项”这份清单看着不起眼但决定了一个项目三个月后还有没有人能接手。真实团队里最常见的悲剧是核心工程师在的时候一切正常人一走项目就黄了。不是技术不行是知识没有沉淀下来。README文档里写清楚“为什么这么做”比写“做了什么”更重要——因为“做了什么”代码里看得到“为什么”只有人记得住。最后说点实在的AI工程这件事我做了这些年最大的体会是它不神秘但确实每一个环节都有它的脾气。数据不是拿来就能用的模型不是跑通就完事的部署不是写个接口就结束的。每一个看起来“不应该有问题”的步骤都藏着足够让你折腾一晚上的细节。但恰恰是这个“从零到一”的过程最值得认真走一遍。你只有亲手处理过脏数据才知道数据清洗规则的每一行代码为什么存在只有亲自排查过训练崩溃才知道随机种子和版本锁定有多重要只有自己导出模型、写推理接口才知道一个模型从Notebook到生产环境之间隔了多少层包装。我的建议是不要追求一步到位不要贪多找一个真实的小任务几十条数据一个中小型模型把整条链路完整走一遍。中途遇到问题不要跳过去每一个坑都是你未来判断力的来源。等你能独立把一个小项目从想法带到上线再回看你会发现那些曾经让你头疼的“工程细节”已经变成了你信手拈来的基本操作。
返回列表