从零搭建AI工程:评论分类器实战全流程
发布时间:2026/9/29 7:56:56来源:尧图网络
我一开始接触AI工程AI engineering时目标朴素得连自己都想笑能跑通一个项目就够了。可真把项目拉到生产环境跑第一次就被线上评论分类器的乱判打脸了——训练时明明有87%的准确率上线后却把正常评论当成垃圾。排查了一圈问题居然出在一行没有调用的文本清洗函数上。从那时起我决定用最笨的方式自己从头搭一遍AI工程基础链路环境、数据、模型、训练、部署、监控每一步都亲手做不依赖一键库不复制现成模板。于是有了这篇 ai-engineering-from-scratch 的实战记录目的是给那些和我一样——学过一点机器学习知识、但面对真实项目总是无从下手的人提供一条可以照着走的完整路线。这篇文章不要求你懂复杂数学也不需要显卡。我会用一个评论情感分类器作为贯穿案例从两万条原始文本开始逐步构建出一个可以真实调用的API服务。整个过程里我踩过的坑也会原原本本说出来包括那些查了一下午才发现是自己造成的低级错误。1. 为什么“从零开始”比直接调库更有价值——AI工程与传统开发的分水岭1.1 调包侠陷阱能跑通demo和能做工程是两回事很多入门者会陷入一种错觉在notebook里加载一个预训练模型跑通官方示例就觉得已经掌握AI工程了。我曾经也这么想。直到第一次接手一个实际项目才发现传统的“调包”经验在AI工程里根本不通用。传统软件开发里调用一个库函数输入输出是可预期、确定性的。你传两个数字给加法函数它永远返回正确的和。但机器学习系统完全不一样模型的行为是由训练数据决定的数据分布一变模型输出就可能完全不可控。这意味着做AI工程的日常不是“写代码”而是“管理不确定性”。举个真实场景。我线上部署过一个评论分类器模型本身没改一行代码某天突然开始把大量正常评论判断成垃圾。排查到最后原因竟然是线上接收的新评论里出现了之前从未见过的全角标点预处理环节没清洗干净模型在“分布之外”的输入上就瞎了。这种问题没有从数据到部署的全局视角根本不知道从哪查起。1.2 为什么选评论分类器作为从头搭建的第一个项目我最终选定的贯穿案例是一个英文评论情感二分类器输入一段评论文本输出正面还是负面。这个项目看起来不起眼但该踩的坑一个都不少。选它有几个非常现实的理由数据好找公开的评论数据集一抓一大把不需要折腾爬虫标签明确正面负面靠常识就能判断不需要领域专家文本预处理环节丰富编码、清洗、分词、向量化全部覆盖模型可以做得小而精一个两层神经网络就够了CPU就能训练不用为显卡发愁。更重要的是从工程角度看它覆盖了AI工程闭环里的所有关键节点数据的获取与清洗、特征表示、模型训练与评估、部署上线与监控。等于用最低的成本把完整链路走了一遍。1.3 AI工程闭环的四个阶段做AI工程跟做传统项目最大的不同在于它是一个“闭环”而不是一条“直线”。我习惯把它拆成四个阶段数据工程——把原始数据变成机器能吃的干净格式特征与模型——选择合适的方式表示数据设计模型结构训练与评估——迭代优化判断模型是否真的“会了”部署与监控——把模型封装成服务持续观察它在真实环境中的表现。这四个阶段不是依次执行的而是会反复循环。模型上线后表现不好可能回去改数据清洗逻辑监控发现输入分布变了可能又要重新训练。这篇文章的章节顺序就是按这个闭环推进的每一步都会写清楚我是怎么做、为什么这么做、踩了什么坑。2. 环境与项目骨架被大多数人忽略的工程第一步2.1 Python环境我为什么没有用Anaconda很多人起步时图省事直接装Anaconda所有包都塞进base环境用的时候pip install一下。前两个星期很爽三个月后就开始痛苦依赖冲突、环境臃肿、版本不明项目根本没法复现。我现在的习惯是用 pyenv 管理Python版本再用 venv 为每个项目建独立环境。Anaconda也不是不能用但它默认的包管理和base环境策略会把“环境隔离”这件事的约束变得太弱。你试过的包越多环境越脏最后连自己都说不清项目到底依赖了什么。实测下来pyenv加venv这套组合的优点是每个项目从Python版本到第三方库完全隔离删掉一个项目不会污染其他项目切目录自动切环境不会出现“在A项目里import了B项目的包”这种离谱问题部署到服务器时环境构建过程干净透明。pyenv install 3.11.9 pyenv local 3.11.9 python -m venv .venv source .venv/bin/activate pip install --upgrade pip四行命令环境就绪。注意 pyenv local 会在当前目录生成一个 .python-version 文件整个团队只要装了pyenv进入目录自动切换到对应版本这是它能复现的第一步。2.2 项目目录结构从第一天就按工程化组织搭建环境之后紧接着要做的不是写代码而是把项目目录设计好。我看到太多人的AI项目长这样项目文件夹里躺着几个命名混乱的notebook数据散落在Downloads和桌面模型权重随便存了个model_v2_final真的不改了.pkl。我的做法是分四个目录职责分明data/raw/ 原始数据只读不写 data/processed/ 清洗后的数据可复现生成 models/ 模型参数和配置 src/ 所有源代码 tests/ pytest测试这样划分的逻辑是原始数据是不可再生的资产必须保持只读处理后的数据是流水线产物任何时候删掉都能重新生成模型参数是制品可以存档可以替换源代码是整个工程的心脏必须纳入版本管理。不是说要一上来就把工程往大了做而是要在第一天就养成“每个文件该放哪”的习惯。后面前置那些让人崩溃的坑有一半都是因为文件乱放、环境混乱造成的。如果从一开始就按这个结构组织排查问题的范围会小很多。2.3 依赖锁定让两周后的自己能再跑起来环境搭建的最后一步是锁定依赖版本。很多人不重视这一点觉得写个requirements.txt就够了。问题是pip freeze 生成的版本列表只记录了顶层包如果哪天numpy升级了即使你没主动升级任何包新装环境时也可能装上不兼容的版本结果就是项目跑出来的结果和之前不一样而且完全不知道为什么。我的经验是直接用 pip freeze 把当前环境所有包版本固定下来生成的requirements.txt提交到Git里。这虽然粗暴但对于单人项目和中小团队已经足够了。更讲究一点可以用pip-tools维护 pyproject.toml但那是另一个复杂度级别的话题新手先把 pip freeze 版本锁好就成功了一大半。3. 数据管道从两万条原始文本到干净向量的完整过程3.1 读取CSV时最容易忽略的编码问题我的数据是一份两万条左右的公开英文评论CSV每行两列text和label。看起来再简单不过第一行代码就给我上了课。import pandas as pd df pd.read_csv(data/raw/comments.csv, encodingutf-8-sig)注意encoding参数我写的不是常见的utf-8而是utf-8-sig。很多从Excel或者Windows系统导出的CSV文件文件开头会带一个不可见的BOM标记。如果你用utf-8读取第一个字段名会变成“\ufefftext”你按“text”去访问就直接KeyError了。这种错误很气人因为它报错的位置和真正的根因离了十万八千里。读取之后先别急着清洗我习惯做三件例行检查看看每列的空值情况有多少行是空文本看看label的分布正面负面是否严重失衡看看文本长度的粗略分布有没有异常长的广告文或者异常短的废话。这几步能帮你建立对数据的直觉而不是一上来就盲写清洗逻辑。3.2 文本清洗哪些能删哪些必须留文本清洗是数据管道里最容易被低估的一环。很多人上来就是一句 pattern 正则替换掉所有非字母字符结果把“!!!”这种强烈的情感信号也一并删了对情感分类来说这等于主动毁掉一个重要特征。我在这个项目里的清洗规则每条都经过了实测import re def clean_text(text): # 去HTML标签和URL它们跟情感无关 text re.sub(rhttp\S|www\.\S, , text) text re.sub(r[^], , text) # 统一小写降低词表规模 text text.lower() # 保留字母、数字、空格以及感叹号和问号 text re.sub(r[^a-z0-9\s!?], , text) # 合并多余空格 text re.sub(r\s, , text).strip() return text注意正则里保留了感叹号、问号和字母数字其他的符号全部清掉。做过一个对照实验如果把感叹号也删掉验证集准确率掉了大约两个百分点。原因很简单“amazing!!”和“amazing.”表达的情绪强度完全不同。另外我还试过是否该去停用词最终结论是不去。停用词在大多数NLP任务里是噪音但在情感分类里“not good”里那个“not”就是决定性信号你把它删了句子就从否定变肯定了。这个坑不少资料都提过但只有亲手跑一遍对比实验才会真的记住。3.3 分词与词表构建英文文本清洗完后分词就简单了直接按空格切开df[tokens] df[text].apply(lambda x: x.split())中文文本到这里就会比较麻烦需要额外引入分词工具但本文案例是英文评论最简单的空格切分足够。关键是切完之后怎么构建词表。我的做法是统计全部文本的词频只保留出现次数不少于5次的词。这样做的目的是过滤掉低频噪声——比如某条评论里出现一次的人名、拼写错误它们对模型训练不但没有帮助还会增加词表规模让输入向量维度虚高。过滤完之后我这边两万条评论的词表从三万多个词收敛到了一万二左右计算压力小了很多。接着给每个词分配一个固定编号外加两个保留符号UNK未知词和PAD填充。实际实验里“UNK一定要有”这条经验非常关键。因为模型部署后遇到新评论一定会出现训练数据里没见过的词如果你没有UNK机制线上预处理就会直接报错或者产生一个错误的词表下标。3.4 向量化先别急着上TF-IDF和Embedding向量化阶段大多数人第一反应是上TF-IDF或者预训练Embedding。我的建议是从零搭项目的时候先用最简单的方式词袋向量也就是统计每个词在当前文本中出现的次数。def text_to_vector(tokens, vocab, vocab_size): vec np.zeros(vocab_size, dtypenp.float32) for tok in tokens: idx vocab.get(tok, vocab.get(UNK)) vec[idx] 1 return vec这么朴素的表示准确率当然不会惊艳。但它的价值在于所有环节你都能一眼看懂——模型输入、矩阵维度、梯度计算每一步都是透明的。TF-IDF和预训练Embedding确实更强但它们的引入会掩盖很多基础问题。从零开始做工程的第一目标不是刷分数而是建立正确的心智模型词袋向量就是你用来理解模型机制的最短路径。数据的最后一步是把向量和标签组装成模型可用的格式并保存到data/processed目录。这里有个细节处理后的数据文件命名要包含数据集的统计信息比如processed_vocab12300_mincount5.csv这样下次想复现时根据文件名就能知道当时的配置。4. 手写一个两层神经网络并训练评论分类器4.1 模型结构设计与参数初始化数据准备好之后终于到了最核心的部分手写神经网络。我用的是一个经典的两层全连接结构输入层词表大小→ 隐藏层64个神经元→ ReLU激活 → 输出层2个类别→ Softmax。选择两层结构的原因是单层网络本质上是线性模型对评论这种复杂文本几乎无能为力而三层以上的网络对新手来说调参难度会急剧上升一旦模型不收敛你很难判断问题出在代码还是超参数。所以从零开始两层是最优解。再来看参数初始化。这一步很多人会忽略直接全零初始化结果训练很久准确率纹丝不动。问题在于如果所有神经元初始参数相同反向传播时的梯度也会相同神经元之间永远不会产生差异整个隐藏层形同虚设。我用的方法是He初始化也就是针对ReLU激活函数设计的初始化方式def init_params(input_dim, hidden_dim, output_dim): W1 np.random.randn(input_dim, hidden_dim) * np.sqrt(2.0 / input_dim) b1 np.zeros(hidden_dim) W2 np.random.randn(hidden_dim, output_dim) * np.sqrt(2.0 / hidden_dim) b2 np.zeros(output_dim) return W1, b1, W2, b2初始化权重时用标准差缩放控制每一层输出的方差让信号在传播过程中既不会爆炸也不会消失。4.2 前向传播与交叉熵损失前向传播就是把输入数据从第一层传到输出层得到预测概率。具体来说输入向量x先经过第一层线性变换得到z1然后经过ReLU激活函数把负值全部归零得到激活值a1再经过第二层线性变换得到logits最后通过Softmax函数将logits转换成两个类别的概率分布。def forward(x, params): W1, b1, W2, b2 params z1 x W1 b1 # (batch, hidden) a1 np.maximum(0, z1) # ReLU logits a1 W2 b2 # (batch, 2) exp_logits np.exp(logits - np.max(logits, axis1, keepdimsTrue)) probs exp_logits / exp_logits.sum(axis1, keepdimsTrue) return a1, probs代码里有一个小细节计算Softmax时先减去批次内最大的logits这叫数值稳定技巧。如果不做这一步当logits里的数值稍微大一点比如变成100计算np.exp(100)会直接溢出成inf损失函数变成NaN。这是我实际踩过的坑而且是整整查了一个小时才发现的。损失函数选交叉熵公式是 L -log(p_y)其中p_y是模型分配给真实类别的概率。它的好处是如果模型对正确类别非常有信心概率接近1损失接近0如果模型自信满满地预测错了概率接近0损失会变成无穷大给优化器一个极强的修正信号。4.3 反向传播的推导与实现反向传播是整个手写神经网络里最容易写错的部分。但它的核心思想就一句话链式法则——从最终损失出发逐层计算每个参数对损失的影响程度也就是梯度。先讲一个关键结论对于Softmax加交叉熵这个组合损失对logits的导数有一个非常漂亮的简化形式直接就是模型预测概率减去真实标签的one-hot向量dlogits probs - y_onehot这个公式是我当年第一次见到时拍案叫绝的。它说明当预测概率和真实标签完全一致时梯度为零不一致时梯度方向指向真实标签。直观又好记。剩下的梯度计算就是沿用链式法则逐层往回传def backward(x, y_onehot, a1, probs, params): W1, b1, W2, b2 params dlogits probs - y_onehot # (batch, 2) dW2 a1.T dlogits # (hidden, 2) db2 dlogits.sum(axis0) da1 dlogits W2.T # (batch, hidden) dz1 da1 * (a1 0) # ReLU的导数 dW1 x.T dz1 # (input, hidden) db1 dz1.sum(axis0) return dW1, db1, dW2, db2这里最需要小心的是ReLU的梯度。ReLU的导数是输入大于0时导数为1输入小于等于0时导数为0。用代码实现时直接用 (a1 0) 作为掩码乘上去就行因为 a1 是经过ReLU激活后的值如果它是0说明对应神经元没有被激活梯度就应该在这里截断。4.4 梯度检查训练前必须做的一步手写反向传播最恐怖的事情不是推不出公式而是推出来的公式有个符号错了、维度错了却死活看不出来。所以我强烈建议在正式训练之前先做一次梯度检查——用数值方法近似计算梯度和解析梯度对比。数值梯度的原理很简单对每个参数加一点点扰动e然后看损失函数的变化量也就是 (L(xe) - L(x-e)) / 2e。def numerical_grad(f, params, idx, eps1e-5): params_plus params.copy() params_plus[idx] eps params_minus params.copy() params_minus[idx] - eps return (f(params_plus) - f(params_minus)) / (2 * eps)把解析梯度和数值梯度放在一起逐项对比它们的相对误差。如果某个参数的误差大于1e-4基本就可以断定反向传播的相应部分写错了。我第一次做梯度检查时就发现b2的梯度符号反了用了半小时就看出来了而没有梯度检查的话这个问题可能要到训练环节才会以“损失不降”的形式暴露出来到那时候排查成本就高多了。5. 训练流程里的性价比动作损失曲线、过拟合与Mini-Batch5.1 数据集拆分训练、验证、测试各归其位模型代码写对之后下一步是训练。但训练前还有一件重要的事把数据集拆分成三个部分。我这里的比例是60%训练、20%验证、20%测试并且拆完之后先验证一件事确保三份数据之间没有任何重叠。这一步看似多余但后面踩坑里会专门讲这是我最悔恨的教训之一。三个数据集各有职责千万别混用。训练集用来更新模型参数验证集用来调整超参数、判断是否过拟合测试集最后只用一次用来衡量模型在全新数据上的真实表现。最常见的错误是拿测试集来回试不同超参数试到最后测试集已经泄露了信息显示在测试集上的分数虚高上线就露馅。5.2 mini-batch训练与学习率的选择训练时我用的批大小是64也就是每次从训练集里随机抽64条样本计算梯度并更新参数。全部样本过完一遍叫做一个epoch我默认跑了100个epoch。Mini-batch的逻辑也很直观用5个样本更新一次参数太不稳定把两万条全部算一遍再更新又太慢。取一个中间量既能利用矩阵运算的并行加速又能让梯度更新方向带有一定随机性反而有助于跳出局部极小点。训练中最关键的超参数是学习率。我做了几个对照实验结果非常直观学习率训练集表现验证集表现现象观察1.0损失变成NaN不可用梯度爆炸0.1震荡剧烈稳定在50%左右从一开始就在发散边缘0.01平稳下降稳步提升收敛最终约87%0.001下降缓慢几乎不涨学到太慢100个epoch不够0.01是效果与效率的平衡点。这个结论只对这个模型和这份数据有效但“先粗扫一个量级范围再在最优量级附近精调”的方法是可复用的。5.3 过拟合信号与我的干预方式随着epoch增加我注意到一个典型的过拟合模式训练集准确率一路涨到97%验证集却停在87%左右不动了。这说明模型开始“背诵”训练数据而不是“理解”评论内容了。标准的应对手段有三件提前停止、L2正则化、Dropout。我这个项目里用了前两种。提前停止就是监控验证集指标连续几个epoch不涨了就停止训练防止模型在训练集上越背越熟。实现起来非常简单每次epoch结束保存当前验证损失如果验证损失连续5个epoch没有创下新低就停止训练并回滚到历史最优参数。L2正则化是在损失函数里加一项参数平方和的惩罚让权重系数不要变得太大。实现方式是在原始损失上加一个 lambda * (sum(W1 ** 2) sum(W2 ** 2)) / 2。这个惩罚的直观理解是权重太大说明模型过度依赖个别特征而正则化能迫使模型使用更多的特征、更平滑地进行预测。我把lambda设为0.01最终测试集准确率稳定在89%左右F1分数0.86。看上去只比定点87%好了一点但关键不是分数提升而是模型在验证集上的表现变得稳定了不再出现训练集97%、验证集87%这种刺眼的落差。6. 从Notebook到线上API模型序列化、接口封装与最小监控6.1 模型对象的序列化设计不要无脑pickle训练结束后摆在面前的问题是怎么把模型保存下来并且部署到线上使用。最直觉的做法是pickle.dump整个模型对象我做过后来后悔了。因为pickle是Python专用的二进制格式它保存的是整个对象的内存状态。问题是如果你的环境版本变了比如numpy从1.24升到2.0老文件很可能反序列化失败。而且pickle的文件里还可能携带不必要的Python内部信息既不安全也不跨语言。更工程化的做法是只保存模型的权重参数以及重建模型所需的元数据。用我的做法就是三个文件np.savez_compressed(models/model_params.npz, W1W1, b1b1, W2W2, b2b2) with open(models/vocab.json, w, encodingutf-8) as f: json.dump(vocab, f) with open(models/config.json, w, encodingutf-8) as f: json.dump({hidden_dim: 64, vocab_size: len(vocab)}, f)这三个文件各自独立模型参数、词表、配置全都解耦。以后想换更大模型、新增类别、修改词表只需要更换对应文件即可不会互相拉扯。6.2 FastAPI封装推理接口模型加载与推理逻辑我用一个类来封装核心是load方法。这个类的好处在于训练和部署时引用的是同一套预处理逻辑从源头上杜绝了“训练时做了清洗、推理时忘了”这种事故。from fastapi import FastAPI from pydantic import BaseModel class CommentRequest(BaseModel): text: str class CommentResponse(BaseModel): label: int score: float app FastAPI() model CommentClassifier.load(models/) app.post(/predict, response_modelCommentResponse) def predict(req: CommentRequest): label, score model.predict(req.text) return CommentResponse(labellabel, scorescore)接口本身极其简单POST请求携带一段文本进来返回预测标签和置信分数。但有两个工程细节是不可省的。第一个是Pydantic的请求模型FastAPI在接收请求时会自动校验字段类型。如果有人传了个missing text字段接口直接返回400错误不用我们自己写各种if判断。第二个是输入长度限制。线上评论动辄几万字符如果不加保护服务很可能被一个超长文本拖垮。我的做法是超过2000字符的文本只截取前2000个并在日志里记录截断标记。这既保护了服务稳定性又不影响绝大多数正常评论。6.3 最小监控结构化日志模型上线之后最怕的不是出bug而是出了bug你完全不知道。所以从第一天起就要做监控。最实用的最小方案是结构化日志——把预测的关键信息全部写入日志文件。def predict_with_log(text): start time.time() label, score model.predict(text) elapsed (time.time() - start) * 1000 log_data { text_len: len(text), label: label, score: float(score), latency_ms: round(elapsed, 2), truncated: len(text) 2000 } logger.info(json.dumps(log_data)) return label, score日志里记录文本长度、预测标签、置信分数、推理耗时、是否截断。这样一旦线上出现问题我可以直接分析日志是不是突然出现大量超高置信的误判是不是某类文本的耗时暴涨数据全部有迹可循。等流量大了以后还可以把这些结构化日志接入监控系统做实时指标展示但那是后话了先把日志留好是第一步。6.4 用Docker固定运行时环境本地调试通过后部署时我用了Docker。这一步的好处是把运行环境整个固化下来服务器上不用再装Python、不用管numpy版本一个镜像就是一份可复现的环境。我用的Dockerfile是两阶段构建先在一个临时容器里安装依赖再把产物复制到干净的运行时镜像里这样最终镜像体积能小很多。模型文件我建议用volume挂载而不是打进镜像里。原因是模型参数会经常更新如果每次重新训练模型都要重新构建镜像工作流会非常笨重。挂载的话只替换服务器上的模型文件就能完成升级接口代码都不用改。到这里整个项目已经从一个notebook变成一个有接口、有监控、可复现的服务了。但真正让这个项目有价值的反而是那些我反复折腾、差点放弃的时刻。7. 栽过的几个跟头完整排查链路7.1 验证集比测试集好太多原来是数据泄露我在早期版本中发现一个诡异的信号验证集准确率高达94%但去跑测试集只有87%差了7个百分点。按常理验证集和测试集都来自同一个数据分布二者表现不会差这么多。最开始我怀疑过拟合就加了正则化没用。然后怀疑训练集和验证集的分布不一致准备去重新抽样赌气之下我决定先做个简单的检查把训练集和验证集的文本逐一比对。结果发现因为原始CSV里存在重复评论有些相同的句子被随机分到了训练集和验证集两边。模型在训练里见过这些话到了验证集自然“背得”出来准确率虚高。完整的排查链路是先怀疑过拟合——加了正则化指标没有改善再怀疑数据切分逻辑——看拆分代码没发现问题最后打印训练集与验证集的重复数量——发现大量重复确认是数据泄露。修复方法很简单在切分前先按文本去重并把重复数据单独清理掉。修复后验证集准确率下降到和测试集差不多的水平虽然数字不好看了但这份指标才是可信的。这条踩坑经历告诉我任何验证集上的指标第一步都要质疑它是不是真的来自“没见过的数据”。对文本类任务去重不是可选项而是必须项。7.2 推理时忘了调用文本清洗函数第二个坑是把我坑得最惨的模型在本地测试完美部署到线上接口后几乎所有评论都被判成负面。我一度怀疑是模型文件损坏重新训练了两轮也没解决。后来偶然对比了本地调用和线上API接收的同一段文本才发现问题。本地测试时我习惯写了一个 text clean_text(text) 再传给模型。但部署时封装接口我竟然忘了调用clean_text导致线上收到的原始文本直接进入分词和向量化流程。那些带HTML标签、URL、大写字母的文本在词表里匹配不到全部变成UNK模型的输入几乎等于噪音。这个问题的修复只需要一行代码在接口的predict里加上 text clean_text(text)。但它引发的思考是为什么光靠代码审查没发现因为本地和线上走了两套不同的调用路径训练习惯性地把预处理写在notebook里而接口另有入口。从那之后我强制规定所有项目必须有一个统一的预处理模块训练和推理都从同一个模块导入不允许各自维护一份清洗逻辑。7.3 依赖版本漂移导致结果不可复现还有一次项目搁置了两个星期再跑同样的代码、同样的数据训练出来的准确率却比之前低了2%。我没有改任何代码很费解。后来用pip list对比当前环境与requirements.txt发现numpy从1.24.x被升级到了1.26.x——因为我在别的项目里执行过pip install把全局环境里的numpy版本带高了一点。这事的教训就是环境隔离不到位以及版本锁定不严格。解决的方案就是第二章里说的给每个项目独立venv用pip freeze锁定版本。搁置再久只要用requirements.txt重建环境就能完全复现之前的结果。文章写到这里你可以看到很多所谓的“玄学问题”根源都是工程习惯问题。7.4 类别不平衡带来的指标幻觉最后再讲一个我把“准确率”当唯一指标的教训。这个数据集的类别分布不太均衡负面评论大约占六成。模型很快学会了一个偷懒策略全预测负面就能拿到60%的准确率。只看准确率的话会觉得模型还不错。但看分类正类别的召回率和精确率就会发现正面评论几乎全被漏掉了。这个问题的本质是准确率对类别不平衡非常不敏感。正确做法是用混淆矩阵加F1分数来评估综合考虑精确率和召回率。我在训练时加入了类别权重让模型在预测数量较少的类别时误分类代价更高最终把F1从0.71拉到了0.86正负两类的召回率都变得合理。这类问题属于评估指标选错不属于代码bug。但它比代码bug更危险因为它会让模型带着明显的缺陷上线。现在我做任何AI项目第一步就是把正负样本比例、混淆矩阵、F1这些基础指标全部列出来再决定要不要往下走。项目跑通那天我做了件相当没出息的事——把博客评论区那些“博主在吗加我微信”的垃圾评论逐条喂给我自己搭的接口。看到它们大多被判定为垃圾内容概率还不低我是真的有点高兴。这个系统不算聪明可它是我从数据到模型、从代码到服务一路亲手搭起来的每一处都清楚底细。如果你也在从零开始学AI工程我的建议只有一条别急着追新模型新框架先完整地走一遍这个最基础的项目。过程中你会真正明白数据、模型、训练、部署、监控这些事情是怎么拧在一起的。这个体会是看再多教程也换不来的。
网站建设高端定制企业官网