从零开始构建AI工程:情感分类服务全流程实战
发布时间:2026/10/1 17:13:21来源:尧图网络
1. “从零开始”的真正含义AI工程不是搭乐高积木很多朋友在后台问我同样一个问题“我想入行AI工程该怎么开始”我通常的回答是先别急着冲去学最新的框架也别一头扎进大模型的API堆里。AI工程这件事越是“从零开始”越能走远。很多人觉得现在有现成的模型、现成的工具链搭一个AI服务就像搭乐高——拿几个积木拼一拼就好。但真实项目里你一定会遇到数据乱七八糟、模型训不出来、服务上线就崩、效果说不清道不明这些形形色色的问题。没有底层能力你连问题出在哪里都判断不了。我梳理“ai-engineering-from-scratch”这个思路不是要让你从数学定理手推每一个公式而是希望你在掌握工程化实践的同时真正理解AI系统的每个环节为什么这么设计。任何AI工程都可以拆成五层数据层、模型层、训练层、部署层、运维层。很多人只盯着模型层觉得换个SOTA模型就能解决一切。但实际经验告诉我一个系统80%的问题都出在数据清洗、特征处理、训练配置和推理链路这些“不起眼”的地方。从零开始还有个隐藏价值就是逼着你理解工程方案的取舍逻辑。举个例子为什么用PyTorch而不是TensorFlow为什么用Docker而不是直接在宿主机跑为什么模型导出成ONNX这些问题在教程里通常只会给结论但你在实际搭建一次完整流程后就会明白每个选择背后都是权衡性能、生态、调试友好度、团队协作成本。本文我会用一个完整的小项目——从零构建并部署一个文本情感分类服务——把AI工程的必要环节走一遍适合那些有一定Python基础、但还没完整做过AI项目的朋友参考。1.1 为什么我坚持从零搭建一次全流程先说一个挺反直觉的经验直接复现开源项目并不能让你学会AI工程。因为开源项目已经把坑都填平了你看到的是漂亮的最终态。你复制下来能跑但一旦换个场景、换份数据立刻手足无措。从零搭建一遍最大的收获不是那个模型而是过程中积累起来的调试直觉。比如我在第一次做情感分析时训练Loss一直在0.69附近震荡怎么调学习率都没用。后来才发现是标签映射写反了——0和1对调了。这种错误如果你只跑别人写好的代码可能一辈子都遇不到。从零开始的另一个理由是可控性。生产环境里你不可能依赖某个黑盒模型而不去管它的行为边界。当用户问“为什么这个评论被误判为负面”时你得能追踪到是数据问题、模型容量问题还是阈值设置问题。这种追踪能力必须建立在亲手搭建过完整链路的基础上。我带的工程师里凡是耐着性子从数据处理写到部署的人后续处理线上问题的速度通常比别人快一倍。1.2 一条能落地的学习路线网上的学习路线五花八门但多数不是太理论就是太碎片。我建议按“端到端最小项目”来驱动学习像我下面分享的这个情感分类服务就是一个不错的起点。整体路线可以这样安排第一周掌握Python数据结构、文件与异常处理、面向对象基础了解基本的Git操作。第二周学习Pandas做数据清洗、NumPy做数组运算掌握训练集/验证集/测试集的划分原则。第三周入门PyTorch弄清楚张量、自动求导、数据集与数据加载器的关系。第四周实现一个简单的文本分类模型体验完整训练循环。第五周用FastAPI把模型封装成接口学习请求与响应处理。第六周用Docker容器化服务尝试用Docker Compose编排。第七周做压力测试和日志监控记录模型表现和系统性能。这条路线不会让你成为算法专家但它能让你具备“独立交付一个AI服务”的能力。我见过太多人学了一堆Transformer理论却连一个简单的接口都写不利索。AI工程的门槛恰恰在于“能落地”这三个字。2. 从零搭建AI工程环境、数据与第一个基线模型现在进入实操环节。我们以“酒店评论情感分类”为例目标很简单输入一段评论文本输出“正向”或“负向”标签。选择这个任务的原因是数据容易获取、评估指标直观、也能完整覆盖AI工程的核心环节。我不会用复杂的大模型而是从逻辑回归开始再到简单神经网络这样你能直观感受模型表达能力的变化。2.1 环境搭建与可复现性AI工程的第一步不是写模型而是构造一个干净、可复现的实验环境。很多入门者习惯全局安装包结果不同项目互相依赖冲突最后只能重装系统——我踩过这个坑太痛了。现在我的标准做法是“每个项目一个虚拟环境”用conda或venv都可以。项目结构也建议一开始就规范化sentiment_service/ ├── data/ # 原始数据与处理后数据 ├── models/ # 保存模型权重与训练日志 ├── src/ # 核心代码 │ ├── data.py # 数据加载与预处理 │ ├── train.py # 训练脚本 │ ├── predict.py # 推理封装 │ └── api.py # FastAPI接口 ├── requirements.txt # 依赖清单 ├── Dockerfile └── README.md养成从一开始就用这种结构的习惯后面切任何项目都会顺很多。requirements.txt 要固定版本号不要写“pandas1.0”这种宽松约束。AI库的版本升级经常不兼容比如PyTorch 1.x和2.x在API上就有不少差异。可复现性差是所有AI项目的隐形杀手。我自己就吃过一次亏本地模型在训练时F1是0.85换到服务器后变成0.80最后发现是不同机器上的scikit-learn版本不同导致文本特征向量生成逻辑变了。所以条件允许的话把环境依赖用pip freeze锁定甚至直接用Docker镜像来固化环境。2.2 数据处理比模型更值得花时间很多教程直接跳过数据用已经分好词的干净文本开跑。真实世界里数据脏到你怀疑人生。我们这次的项目虽然用公开数据集也要走完整的数据处理流程。以“酒店评论情感分类”为例样本里会有空行、重复内容、HTML标签、表情符号、大小写不统一等问题。我的建议是写一个clean_review函数按顺序处理去掉HTML标签和多余空白。统一转为小写英文场景这么做中文场景则不需要。处理表情符号某些场景下表情是强特征可以直接映射成“POS”或“NEG”占位词。去除无意义的超高频词如英文的stopwords但保留否定词并做处理比如“not good”要能识别为负面。接着划分数据集训练集70%、验证集15%、测试集15%。注意划分时要做分层抽样保证正负样本比例在每份数据里都是一致的。这一步经常被忽略但直接影响模型评估的可信度。验证集用来调参、早停测试集只在最终评估时碰一次。这是红线——如果你反复用测试集调参测试集就变成了验证集最终评估的分数就是自欺欺人。2.3 建立第一个基线先用简单方法跑通不要第一次就上Transformer。先做一个最简单的基线模型比如逻辑回归这能帮你快速验证整个链路是否通畅。我们这边给一个基于CountVectorizer加LogisticRegression的示例from sklearn.feature_extraction.text import CountVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline pipeline Pipeline([ (vect, CountVectorizer(ngram_range(1, 2), max_features20000)), (clf, LogisticRegression(max_iter1000, C1.0)) ]) pipeline.fit(X_train, y_train)注意我在这里用了ngram_range(1, 2)意思是把单个词和两个词的组合都作为特征。为什么因为“not good”如果你只取单个词“good”会被误判为正面。加入bigram之后“not good”作为一个整体特征就能表达负面倾向。max_features20000限制特征维度防止矩阵太大导致内存膨胀或过拟合。你可能会问C参数怎么选一般可以从1.0开始然后用验证集做网格搜索比如在0.1、1、10之间比较。但其实刚开始跑通链路比调优更重要所以不要纠结参数。这个基线模型跑完后记录准确率、F1分数。我的经验是情感分类任务上逻辑回归的F1通常能到0.85左右虽然不够惊艳但作为基准意义重大。接下来我们做深度学习模型时就知道所谓“新模型更高”到底高了多少值不值得为它付出训练时间。3. 训练、评估与优化让模型真正可用基线跑通之后我们开始构建一个端到端的神经网络分类器。这里我会手写训练循环而不是直接用Trainer库目的是让你看清每个步骤背后发生了什么。用框架确实方便但如果你不理解内部逻辑遇到问题时会束手无策。3.1 搭建一个简洁的文本分类网络我们用一个简单的两层结构Embedding层把词索引映射成稠密向量然后接一个LSTM或GRU提取序列特征最后接全连接层输出二分类logits。对刚入门的朋友我建议用GRU而不是LSTM因为GRU参数更少、训练更快在文本分类这种任务上效果差不多。import torch.nn as nn class ReviewClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim128, hidden_dim64, num_layers2): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.gru nn.GRU(embedding_dim, hidden_dim, num_layersnum_layers, batch_firstTrue, dropout0.3, bidirectionalTrue) self.fc nn.Linear(hidden_dim * 2, 1) def forward(self, x): emb self.embedding(x) # shape: (batch, seq_len, emb_dim) out, _ self.gru(emb) # out: (batch, seq_len, hidden*2) # 取最后一个时间步的输出 out out[:, -1, :] logit self.fc(out).squeeze(-1) return logit有几个细节必须解释。padding_idx0表示索引为0的词向量会保持为零向量不会在训练中更新。所有样本在同一个batch里需要padding到相同长度。补零的位置计算损失时要通过mask屏蔽掉否则模型会在无效padding上学习。bidirectionalTrue意味着模型同时看正向和反向的上下文对评论这类整体语义任务有帮助但计算量也会翻倍。dropout0.3是防止过拟合的常见选择放在GRU层之间和输出之前。3.2 训练循环从Loss震荡到收敛训练一个神经网络的完整代码网上一搜一大把但很多人不知道每个组件的选择逻辑。我这里给一个精简但完整的训练循环并解释关键点from torch.utils.data import DataLoader, TensorDataset import torch.optim as optim def train_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0 for batch in dataloader: input_ids batch[input_ids].to(device) labels batch[labels].to(device) optimizer.zero_grad() logits model(input_ids) loss criterion(logits, labels.float()) loss.backward() # 梯度裁剪防止RNN梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() return total_loss / len(dataloader)优化器我会选择AdamW而不是普通Adam。AdamW把权重衰减和梯度更新解耦能有效降低过拟合尤其是Transformer时代几乎成了默认选择。初始学习率设为2e-4左右如果你用普通SGD可能要0.01或更高但收敛慢很多。损失函数用BCEWithLogitsLoss原因是我们输出的是logits而不是概率这个损失函数内部会先做sigmoid再计算二分类交叉熵数值稳定性更好。一个常见的训练教训是Loss居高不下比如一直在0.69附近。0.69这个数字非常经典因为它是二分类随机猜测的交叉熵值-ln(0.5)约等于0.693。如果你发现在0.69附近纹丝不动首先检查数据和标签对不对齐。我在调试时就遇到过因为DataLoader的shuffle开启后样本和标签错位模型学不到任何规律。另一个做法是先用一小批数据比如32条过拟合如果这个小批数据Loss能降下去说明模型和数据链路没问题如果连小批都学不动那问题多半出在预处理或模型结构上。3.3 评估指标与阈值调优准确率在情感分类这种类别相对均衡的场景里还凑合但如果你做的是垃圾邮件识别类别不平衡就麻烦了。正样本只占1%你把所有邮件都判为“正常”就能有99%的准确率这显然没有意义。所以一定要看F1、Precision、Recall。对情感分类业务来说通常更看重召回率——用户发了一条差评如果系统漏掉会导致严重公关问题而误判少数好评成差评影响相对小一些。业务不同指标偏好就不同没有哪个F1是万能最优的。训练完成后你可以调整预测阈值。默认是0.5但如果你希望更保守比如只有置信度高于0.7才返回“负向”低于0.3返回“正向”中间则交给人工那么你可以通过验证集上的Precision-Recall曲线选择合适的阈值。这个操作实现起来很简单但很多人忽略最终导致模型在线上表现和离线指标差距很大。4. 部署与交付从模型到AI服务的最后一公里训练完模型只算完成了一半。AI工程的价值在于被使用而“被使用”就意味着要部署、服务化、监控。这步做不好前面所有的工作都白费。我见过很多数据科学家训练出漂亮的模型但交付给工程团队后双方互相甩锅。所以从零开始的项目一定要把部署也走一遍。4.1 模型保存与加载的工程规范模型训练结束需要把模型参数、词表、预处理配置一起保存下来。很多人的坏习惯是只保存state_dict换环境时忘记恢复词表或预处理参数服务完全跑不起来。我的建议是保存一个完整的“模型包”import torch artifact { model_state: model.state_dict(), vocab: vocab, config: { embedding_dim: 128, hidden_dim: 64, num_layers: 2 } } torch.save(artifact, models/review_classifier.pt)同时记录模型版本、训练时间、最终验证F1。生产环境里模型需要做版本管理我用一个简单的命名规则review_classifier_v1.0_f10.871.pt。以后更新模型时凭文件名就能知道大概情况。加载模型时要确保运行环境与训练时的依赖版本一致否则可能出现权重加载错误或者推理结果不同的情况。4.2 用FastAPI发布一个REST服务FastAPI是我目前最喜欢的服务化框架。内置参数校验和OpenAPI文档写起来非常轻量。下面的代码就是一个完整的推理APIfrom fastapi import FastAPI from pydantic import BaseModel app FastAPI() model, vocab, config load_model(models/review_classifier_v1.0.pt) class ReviewRequest(BaseModel): text: str class ReviewResponse(BaseModel): label: str confidence: float app.post(/predict, response_modelReviewResponse) def predict(request: ReviewRequest): # 注意这里要复用训练时的预处理函数 input_ids text_to_tensor(request.text, vocab, max_len128) with torch.no_grad(): logit model(input_ids.unsqueeze(0)) prob torch.sigmoid(logit).item() label positive if prob 0.5 else negative return ReviewResponse(labellabel, confidenceprob if labelpositive else 1-prob)这里面有两个容易踩坑的点。一是text_to_tensor必须和训练时的preprocess保持一致比如同样的长度截断策略、同样的特殊token。二是torch.no_grad()不能少否则会额外构建计算图白白占用显存。注意到我没有在每次请求时重新加载模型——模型在启动时加载到内存之后一直驻留这点对响应速度至关重要。我在第一次部署时犯过一个错误每收到一个请求就torch.load一次模型结果单次请求耗时从20毫秒变成了800毫秒而且并发一高直接OOM。这是在本地跑通之后压测才暴露的问题。所以务必养成“模型全局加载”的习惯。4.3 容器化与并发优化接下来用Docker把服务封装。Docker的价值在于“本地和线上一致”。我用一个最小化的DockerfileFROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, src.api:app, --host, 0.0.0.0, --port, 8000]注意我选择的是python:3.9-slim而不是完整镜像因为镜像越小部署越快攻击面也越小。在推理时CPU上跑这个简单GRU模型完全够用无需GPU。生产环境建议用gunicorn之前先实测线程和进程模型。FastAPI本身是异步框架但PyTorch推理是同步阻塞的。并发请求多的时候可以用多进程或增加单进程线程数。我的经验是对于这种小模型设置--workers 2、单个uvicorn内开4个线程可以比较平滑地支撑每秒几十个请求。调优前先做压测用locust或wrk都可以不要猜。4.4 模型监控上线只是开始模型上线后必须做监控否则哪一天数据分布变了模型表现会直线下降而你毫不知情。至少要看三部分系统指标请求延迟、错误率、CPU/内存业务指标调用量、正负样本分布模型指标预测的置信度分布、抽样人工复核的准确率。我习惯给每个预测结果打上模型版本号日志里记录原始文本、预测标签、置信度。这样一旦线上出现问题可以回溯到具体某一条文本对比是新数据问题还是模型版本退化。这种从工程链路里找问题的能力才是AI工程的核心竞争力。5. 常见问题与排查技巧实录那些前人踩过的坑最后这部分我把过去实际踩过、帮别人排查过的一些典型问题整理成了一份速查表。这些问题不一定都出现在本次项目中但大概率会出现在你之后的AI工程实践中。症状可能原因排查方法解决方案训练Loss不降数据标签错位、学习率过大或过小先在小批量数据上过拟合打印前几个batch的输入和标签修正预处理逻辑用学习率预热验证集F1高线上效果差数据泄漏或训练-测试分布不一致检查实验时是否使用了未来信息对比线上样本与训练集文本重新划分数据增加数据增强推理速度极慢每次请求都加载模型检查代码中是否存在重复加载请求函数外做模型加载增加缓存多个请求同时失败进程/线程模型配置不当观察错误并压测查看是否有内存爆炸调整workers数量加大内存限制容器无法启动依赖没安装全或路径不对本地docker run看日志检查Dockerfile和挂载路径模型效果每天下降数据漂移跟踪线上数据分布画置信度直方图定期重训练加入数据质量告警除了表格里的硬问题还有几个软习惯也很值钱。第一个习惯每次实验都要记录配置。我见过一个同学调参调了一周最后终于把F1从0.85提到0.87但问他用的是什么学习率、什么随机种子他完全说不出来。这种结果无法复用等于没做。我坚持用一个简单的CSV表格记录每次实验的数据版本、模型结构、超参数、训练时间、验证指标。坚持一段时间你会发现自己对参数的感觉越来越准。第二个习惯不要急着上大模型。很多问题用简单的baseline就能解决大模型带来的提升可能只有0.1%但推理成本增加10倍。在进行AI工程时“够用就行”是成本意识下个项目里多出来的预算可以用在其他地方。第三个习惯代码里写注释要写“为什么”不要写“是什么”。因为代码是写给未来的自己看的。比如“这里用双GRU是因为消融实验显示比单层提升2%”就比“这里是双GRU”有价值得多。回到这个“ai-engineering-from-scratch”项目我最后的建议是别追求一次把所有环节做到完美。第一遍先跑通整个链路哪怕结果差强人意。第二遍再逐环节优化。这个项目本身就是这样一个完整闭环从零开始最终交付一个可调用的AI服务——这个过程真正训练的是你对AI系统全貌的把控力和调试直觉。现在AI工具越来越多好像什么都可以一站搞定但越是这样底层理解越值钱。你亲手搭过一遍才知道哪里可能是薄弱点出了问题才知道从哪里下手。这就是我认为“from scratch”最大的意义。
网站建设高端定制企业官网