1. 项目概述与核心设计思路
做中文文本情感分析这个系统,其实源于一个很实际的需求:某业务线每天产生大量用户反馈文本,需要按“正向、负向、中性”快速归类,人工看不过来,而且标注口径不稳定。于是我就着手搭了一套以中文短文本为输入、情感极性为输出的分析服务。这个系统最终是一个轻量级文本情感分类引擎,支持批量离线分析和在线 API 调用,核心价值在于把“人读文本判断情绪”这件事,变成可量化、可复用、可接入业务流程的标准化模块。
市面上的情感分析方案并不少,但真正落到中文场景时,分词差异、口语化表达、否定句式、网络新词这些坑要么没人讲清楚,要么只在论文里一笔带过。这篇博文就是把我从数据准备、模型对比、服务封装到线上排障的完整经验记录下来,适合正在做 NLP 分类项目、被中文文本预处理折磨,或者想从传统机器学习方案平滑过渡到预训练模型的开发者参考。
先说一下整体设计思路。最开始摆在我面前的有三条路线:直接用现成云端 API、用开源预训练模型做微调、自己从传统机器学习方案开始搭。云端 API 确实省事,但批量数据处理成本不低,而且数据要过第三方服务,在业务侧有合规顾虑,所以我直接放弃了。开源预训练模型效果最好,但早期 GPU 资源不稳定,而且团队对部署链路还不熟悉,一口吃成胖子容易翻车。最终我选择了一个折中方案:先用传统机器学习方法把整个数据链路和服务框架跑通,再把模型逐步替换成更强壮的深度学习方案。这个决策在后面帮了大忙——基础架构稳定之后,换模型只是换一个推理函数的事。
系统结构上划分为五个模块:数据预处理、特征表示、模型训练、服务封装、监控反馈。这五个模块各司其职,数据预处理负责把原始文本统一成干净格式,特征表示负责把文本变成模型能读的数字,模型训练负责产出可量化的分类器,服务封装负责对外提供 HTTP 接口,监控反馈负责追踪线上表现并回灌新样本。每个模块之间通过约定好的数据格式衔接,接口边界清晰,这样即使后续要换模型架构,也不会牵一发而动全身。
1.1 核心需求解析
拆解这个项目的核心需求,可以从“输入、输出、约束条件”三个维度来看。输入是中文文本,来源包括用户评论、工单描述、售后反馈,文本长度差异极大,有的只有“好用”两个字,有的能写上千字的小作文。输出是情感极性标签,我这边定义了三类:positive、negative、neutral。为什么不只分正负两类?因为实际数据里有大量纯事实描述型文本,比如“我的订单是昨天下午三点送达的”,这句既没有表扬也没有抱怨,硬归到正或负都会引入噪声,必须给模型一个“中性”的出口。
约束条件才是真正影响设计的关键。第一是延迟要求:在线接口的 p99 响应时间不能超过 300ms,这意味着不能用过于复杂的模型链路。第二是资源限制:初期只有一台 CPU 服务器,GPU 是后来才申请到的,因此前期方案必须能在纯 CPU 环境下跑出可接受的效果。第三是数据隐私:原始语料不能出内网,所以一切训练和服务都必须本地化部署。
这些约束直接决定了技术选型的方向。传统机器学习方案(比如 TF-IDF 加线性分类器)在 CPU 上推理速度极快,单个样本通常只需要几毫秒到十几毫秒,虽然没有深度学习模型的语义理解能力强,但作为基线系统完全够用。等到业务验证了情感分析确实能产生价值,再逐步引入更重的模型也不迟。
1.2 技术选型背后的取舍
技术选型上,我把候选方案分成了三个梯队。第一梯队是 TF-IDF 或词频特征配合朴素贝叶斯、逻辑回归这类线性模型,优势是训练快、解释性强、CPU 友好,缺点是对语义理解浅,遇到“这个产品我不是不喜欢”这种双重否定句基本会判断错。第二梯队是 Word2Vec 或 FastText 词向量配合 LSTM、BiLSTM 这类循环神经网络,能捕捉一定的上下文信息,但训练时间较长,调参复杂,而且词向量质量依赖大规模语料。第三梯队是 BERT 及其变体(比如中文 RoBERTa、MacBERT),通过预训练加微调的方式大幅提升语义理解能力,但显存占用高、推理延迟大、部署成本高。
我的选择策略是分层递进:先实现第一梯队保证系统可用,然后尝试第二梯队积累深度学习经验,最后在资源允许时升级到第三梯队。最终线上同时保留两套模型:一套轻量级模型处理短文本和高并发请求,一套重型模型处理复杂语义和长文本,用文本长度和置信度做路由。
这里有个值得强调的认知:模型选型不是越复杂越好,而是要匹配场景。如果业务数据主要是“好评”“差评”这种短文本,TF-IDF 加逻辑回归已经能达到 90% 以上的准确率,没有必要非得用 BERT。反而是数据质量、标签一致性对最终效果的影响更大——这个问题在后面踩坑部分会详细展开。
2. 数据准备与中文文本预处理全流程
情感分析本质上是一个监督学习任务,数据质量直接决定模型上限。我见过太多项目在模型结构上投入大量精力,却忽视了数据清洗和标注一致性,结果模型怎么调都到不了理想效果。所以在讲模型之前,先花大篇幅讲清楚数据链路是怎么搭的。
2.1 数据采集与标注方案的落地
数据采集阶段,我主要从三个渠道获取原始语料:业务系统历史工单、公开评论数据集、爬取的商品评论(模拟数据,仅用于内部实验)。历史工单最贴近真实业务场景,但噪声也最大,包含大量称呼语、时间描述、无意义字符;公开数据集干净但领域偏移明显,需要评估是否适合当前业务;爬取评论可以快速扩充样本量,但涉及合规问题,必须谨慎。
最终我的数据混合策略是:业务历史数据作为主体(约 60%),公开数据集作为补充(约 30%),剩下 10% 用规则生成的反事实样本(比如把“很好”替换成“不好”构造负例)来填充长尾表达。这里有个容易忽略的问题:直接混合不同来源的数据时,要注意标签口径是否一致。比如公开数据集里可能把“一般般”标为中性,但业务团队认为“一般般”已经偏负向了,这种口径冲突必须在预处理阶段就统一。
标注方案上,我没有直接动用大量人力,而是采用了“规则预标注 + 人工修正”的流水线。先用一个基于情感词典的简单规则跑一遍全量数据,给每条数据打一个初标,然后标注人员只修正规则判错的样本。这样做的好处是大幅降低了人工标注的工作量,同时让标注人员把精力集中在模型容易混淆的边界样本上,等于变相做了一个难例挖掘。
标注质量的控制同样不能放松。我让两位标注员独立标注同一批 2000 条样本,计算标注一致性,也就是 Cohen's Kappa 系数。第一轮只有 0.72,说明大家对中性样本的边界认知差异很大。于是我们重新定义了标注规范:明确“中性”必须满足无明确情感倾向、仅陈述事实两个条件,并补充了几十个典型例句。第二轮标注一致性提升到了 0.85,这个水平对情感分类任务已经完全够用了。
提示:标注一致性低不是标注人员不认真,而往往是标注规范定义得不够清晰。与其反复培训,不如把模糊样本收集起来,在标注规范里补充具体示例,这样迭代效率更高。
2.2 中文分词与文本清洗的常见坑点
中文文本处理相比英文最大的差异在于分词。英文单词天然以空格分隔,中文必须依赖分词工具,而且分词边界直接影响后续特征表示的质量。我对比了几款主流分词工具后,最终选择了结巴分词加自定义词典的方案,原因是它轻量易集成,自定义词典机制方便扩展领域词。
分词选型完了,真正麻烦的是词典沉淀。业务文本里充满了人名、产品名、编号、特殊符号,比如“XX型号的电池用了三天就鼓包了”,如果不把“XX型号”加入自定义词典,会被拆成“XX”“型号”两个词,丢失关键信息。我的做法是周期性从业务数据里挖掘高频片段,人工审核后并入词典,同时记录词典版本号,让分词结果可复现。
文本清洗环节,我的经验是清洗规则宁少勿多,每加一条规则都要在小样本上验证是否会产生副作用。举个例子:有人习惯把所有数字都替换成占位符,但在“3天就坏了”这种句子里,数字“3”恰恰是表达负面情绪的关键信息,直接替换反而丢失了语义。我的清洗原则是:删除不可见字符和乱码,统一全角半角,过滤超长无意义重复字符,其余内容尽量保留。
需要特别小心的一步是停用词处理。停用词表的选择对传统机器学习方法影响极大,但深度学习方法其实不太在意。我在 TF-IDF 方案中维护了一份精简停用词表,只包含“的、了、是、在”这类无明显情感倾向的虚词。像“太、极、非常”这类程度副词绝不能进停用词表,因为它们是情感强度的关键信号。分词之后,我还会做一次词性筛选,在传统方案中主要保留形容词、动词、副词和名词,过滤纯虚词,把特征维度降下来。
2.3 标签体系与样本平衡策略
情感分类的标签体系看起来简单,但定义清楚边界非常关键。我最终确定的是三分类:positive、negative、neutral。在标注工具里还额外增加了一个“垃圾”标签,用来收集那些无法归类、无意义或乱码的文本。这些“垃圾”样本不参与训练,但会进入一个专门的过滤规则库,线上遇到同类文本可以直接拦截。
样本类别不均衡是个绕不开的问题。在真实业务数据里,正向评论天然占多数,大约 60%,负向约 25%,中性只有 15%。如果不做处理,模型会倾向把所有样本都预测为多数类。我最初训练出来的模型准确率有 78%,表面看还行,但看混淆矩阵发现负向样本的召回率只有 40%,基本等于没识别出负面反馈,这在业务上完全不可接受。
解决不均衡问题,我试过几种方法,后面在常见问题部分详细说。这里先提一个原则:优先用数据层面方法(重采样、加权),而不是轻易改模型结构。因为数据层面的问题,最好在数据层面解决,改模型结构往往引入更多不确定因素。
3. 模型训练:从传统方法到预训练模型的三级跳
模型训练是整个系统的核心环节,也是我踩坑最多的部分。我用同一样本集训练了三个不同量级的模型,分别代表三种技术路线,这样既能横向对比效果,也能在线上遇到资源瓶颈时有降级方案可选。
3.1 方案一:TF-IDF 特征与逻辑回归的快速基线
第一个落地的方案是 TF-IDF 加逻辑回归。这个组合可以说是文本分类的入门标配,训练速度快,推理开销小,模型可解释性强。TF-IDF 的核心思想是:一个词在某篇文档中出现频率越高,同时在所有文档中出现频率越低,则这个词对这篇文档的代表性越强。逻辑回归的优点是输出的概率值可以直接拿来当置信度,方便后续做阈值调整。
实现上我用到了 scikit-learn 的 TfidfVectorizer。参数选择上,我设置了 max_features=5000,也就是只保留 TF-IDF 值最高的 5000 个特征,这样能控制特征维度,避免维度爆炸;ngram_range=(1,2) 表示既保留单字词也保留相邻的两个词组合,这样可以捕捉“不好”“太差”“很满意”这类短语级信息。这个参数在实验中被证明比单纯用单个词特征提升了约 3 个百分点的 F1 值。
代码实现非常简洁:
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline model = Pipeline([ ('tfidf', TfidfVectorizer(max_features=5000, ngram_range=(1, 2))), ('clf', LogisticRegression(C=2.0, max_iter=1000)) ]) model.fit(train_texts, train_labels)这里有一个容易踩的坑:TF-IDF 必须只在训练集上拟合,然后用拟合好的向量化器转换测试集和线上数据。如果不小心在全量数据上拟合了 TF-IDF,会造成特征分布泄露,得到虚高的测试集效果,上线后就现原形。
逻辑回归的正则化强度 C 也需要调。我一开始用默认的 C=1.0,验证集 F1 在 0.84 左右,把 C 调到 2.0 后 F1 提升到了 0.86。C 值越大,正则化越弱,模型对训练集的拟合程度越高,但过大容易过拟合。这个参数和特征维度密切相关,需要结合数据量一起调。
3.2 方案二:Word2Vec 词向量与 BiLSTM 的序列建模
第二套方案是 Word2Vec 加 BiLSTM,这是深度学习方法里相对经典的一套组合。Word2Vec 负责把每个词映射成稠密向量,BiLSTM 负责从前后两个方向捕捉句子的上下文信息。相比 TF-IDF 的稀疏表示,词向量是稠密表示,能够编码一定的语义相似性,比如“高兴”和“开心”在向量空间里距离会比较近。
这一阶段我换了模型库,使用 TensorFlow/Keras 来构建。训练 Word2Vec 时我用了从业务数据和公开语料混合训练的领域词向量,维度设置 200,窗口大小 5,最小词频 2。这里有一个心得:词向量的训练语料最好和业务领域匹配。如果用完全通用的百科语料训练词向量,像“鼓包”“掉漆”“续航崩”这类领域词汇的向量表示就不会太好。
BiLSTM 模型结构上,我用了 Embedding 层将词索引映射为词向量,后面接一个 BiLSTM 层,隐藏单元数 128,再通过全局池化后接全连接层和 Softmax 输出。训练参数上使用 Adam 优化器,学习率 0.001,Batch Size 64,早停法当验证集 loss 连续 3 轮不下降时停止。
这段模型的训练过程相对顺利,但暴露了一个问题:训练时间明显变长。CPU 上训练一个 epoch 需要将近半个小时,后来换到 GPU 上才缩短到五分钟以内。线上推理速度也没有传统方案那么快,单条文本约需 30-50ms,还在可接受范围内。
3.3 方案三:预训练语言模型的微调实践
第三套方案是当前效果最好的方案:预训练语言模型微调。我用的是中文 RoBERTa 模型,它在 BERT 的基础上做了更充分的预训练,中文任务上通常表现更好。核心思想很简单:模型已经在海量中文语料上学会了通用的语言表示,我们只需要在它的基础上,用标注数据做一个小规模微调,让它适配情感分类这个具体任务。
我有意识地没有在一开始就直接上预训练模型,是因为当时对部署链路还不熟,GPU 资源也不稳定。事实证明,先跑通传统方案、再迁移到预训练模型的路径非常明智——数据预处理、评估体系、服务框架都是通用的,换成预训练模型时只需要替换训练和推理模块。
微调的核心代码基于 HuggingFace Transformers 实现:
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-roberta-wwm-ext") model = AutoModelForSequenceClassification.from_pretrained( "hfl/chinese-roberta-wwm-ext", num_labels=3 ) training_args = TrainingArguments( output_dir="./checkpoints", num_train_epochs=3, per_device_train_batch_size=16, per_device_eval_batch_size=64, learning_rate=2e-5, evaluation_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, metric_for_best_model="f1", )微调的参数选择比较讲究。学习率设成 2e-5 而不是常见的 1e-4,因为预训练模型的权重已经比较成熟,学习率太大会把预训练学到的东西冲掉,俗称“灾难性遗忘”。训练轮数设 3 轮,配合早停,防止在训练集上过拟合。批大小直接受限于 GPU 显存,我用的显卡显存 16G,单卡批大小只能开到 16,再大就会显存溢出。
注意:预训练模型的序列长度上限通常是 512 token,超过这个长度的文本需要截断或分段。中文一个字大概对应一个 token,所以 512 token 大概能覆盖 400-500 字的内容。对于超长文本,我采取的策略是截取开头和结尾各 200 字拼接,因为情感倾向往往在开头和结尾表达得最明显。
4. 服务封装与工程化部署实践
训练出模型只完成了系统的一半,另一半是如何把模型变成一个稳定、高效、可接入业务的服务。模型训练阶段大家都能做得差不多,但工程化部署阶段才是真正拉开差距的地方。
4.1 API 设计与响应结构定义
我最终选择 FastAPI 来构建在线推理服务,主要看中它几个特点:异步支持好、自动生成 API 文档、Pydantic 模型校验大大减少了参数异常的排查成本。接口设计上,我提供了两个核心端点:单条预测和批量预测。批量预测使用数组传参,一次性处理最多 100 条文本,内部用并发池加速。
响应结构的设计也花了不少心思。最初的版本只返回标签和置信度,但业务方后续提出了新需求:希望知道模型判断的依据。于是我在响应里增加了 top3 标签及其置信度分布,这样即使主标签判断错了,业务方也能从置信度分布里判断出模型“纠结”的情况,从而决定是置信采用还是走人工审核。
FastAPI 接口的核心代码:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class PredictRequest(BaseModel): texts: list[str] threshold: float = 0.5 class PredictResponse(BaseModel): results: list[dict] @app.post("/predict", response_model=PredictResponse) async def predict(req: PredictRequest): results = [] for text in req.texts: label, confidences = inference(text) results.append({ "text": text, "label": label, "confidence": confidences[label], "all_confidences": confidences }) return {"results": results}接口实现不难,真正要注意的是并发控制。模型推理虽然是 CPU 操作,但并发过高时会出现资源争抢,导致响应时间飙升。我给模型推理过程加了一个信号量,限制同时推理的请求数,其余请求排队等待。这样虽然压测时峰值吞吐没有变化,但 p99 延迟从 800ms 降到了 280ms,稳定性反而大幅提升。
4.2 线上稳定性:长文本、乱码与异常输入处理
线上系统遇到的问题永远比测试集里遇到的问题更多样。我遇到的第一类问题是超长文本。业务系统里的工单最长有几千字,RoBERTa 模型最长输入只有 512 token,直接截断会丢失尾部信息。我的解决方案是:先按分句符把文本切成片段,取前 3 段和后 3 段拼接,这样尽量保留开头和结尾的语义。这个方法在人工抽检中效果不错,8 成以上的文本在截断后仍然保留了核心情感信息。
第二类问题是乱码和非常规字符。线上传入的文本有时包含奇怪的不可见字符、重复标点、全角半角混用。我的清洗函数做了几件事:用正则去除控制字符、把全角字符转成半角、连续三个以上的重复标点压缩成一个。但这里有个反直觉的地方:连续重复表情符号其实有情感强化作用,比如“哈哈哈哈哈哈”和“??????”,不能简单压缩掉。我的策略是只压缩重复标点,保留重复汉字和重复表情符号。
第三类问题是空文本和无效文本。有些请求传入的是空字符串或只有空格,模型会输出一些莫名其妙的预测结果。我在入口处加了一个检查:去除空白后如果长度少于 2 个字符,直接返回 neutral 并且 confidence 设为 0,同时打上无效标记。这样把异常拦截在模型之前,既节省算力也避免误判。
4.3 模型热加载与版本管理策略
线上系统不能每次更新模型就重启服务,所以模型热加载机制是必须的。我的实现方案是模型文件和配置文件分离:训练完成后把模型权重、标签映射、词典版本、预处理参数打包成一个版本目录,目录名带上版本号和时间戳。服务启动时加载指定版本的模型,同时启动一个后台线程定期检查是否有新版本目录出现,一旦发现就尝试加载,成功后自动切换流量。
这个机制在实施过程中踩过几个坑。最典型的是新旧模型交替期间的兼容性问题——新模型可能使用了不同的标签映射或不同的预处理规则。为了防止切换时出现低级错误,我在版本目录里加了一个 manifest.json,记录模型类型、标签列表、清洗规则版本、词典版本,加载时先校验这些元信息是否与当前服务配置一致,不一致直接拒绝加载。
版本管理上,我还做了一个灰度策略:先让新模型处理 10% 的线上请求,对比新旧模型的预测一致性和业务反馈,确认无误后逐渐放量到 100%。这个灰度过程靠一个配置开关控制,不需要改代码。模型不是换了就更好,必须用线上数据验证,这是我这几年做机器学习运维最深的体会。
5. 评估指标、实验对比与模型效果解读
模型做出来了,效果到底怎么样不能靠感觉,要用统一的评估体系来衡量。我在这个项目里建立了一套多维度评估机制,覆盖模型效果、推理性能、工程稳定性三个层面。
5.1 核心评估指标的选择与理解
文本分类任务最常用的指标是准确率、精确率、召回率和 F1 值。准确率是整体判断正确的比例,但类别不均衡时参考价值有限。精确率是预测为某类别的样本中实际为该类别的比例,衡量的是“模型说这个是负面,到底有多准”;召回率是实际为某类别的样本中被正确找出来的比例,衡量的是“真实负面被漏掉了多少”。F1 值是两者的调和平均,综合反映模型的精确和召回能力。
在实际业务中,不同类别对精确率和召回率的侧重不同。负面样本的召回率比精确率更重要——漏掉一个用户的不满,比误判一个中性用户为不满要严重得多。中性样本则更看重精确率——如果大量实际有情感倾向的文本被判断成中性,说明模型在做“和稀泥”。所以我最终采用 macro F1 作为主要优化目标,同时单独监控负面类别的召回率作为辅助指标。
混淆矩阵是我每次评估必看的工具。它让我直观看到哪两个类别之间最容易混淆。我训练的第一个版本模型里,明显的错误模式是中性被预测为正向的比例偏高。检查后发现标注规范变动导致部分历史样本标签混乱,修正后模型效果立刻提升了两个百分点。如果没有混淆矩阵,只盯着总准确率很难发现这个问题。
5.2 三套模型的实测对比与线上表现
我用同一份测试集(2000 条人工标注样本)对三套模型做了横向对比。测试集和训练集严格隔离,且测试集是在模型训练完成之后才参与评估,确保结果可靠。
| 指标 | TF-IDF + 逻辑回归 | Word2Vec + BiLSTM | RoBERTa 微调 |
|---|---|---|---|
| 准确率 | 0.86 | 0.88 | 0.93 |
| 加权 F1 | 0.85 | 0.87 | 0.92 |
| 负面召回率 | 0.74 | 0.79 | 0.88 |
| 单条推理耗时 (CPU) | 5ms | 35ms | 400ms |
| 单条推理耗时 (GPU) | 3ms | 8ms | 35ms |
| 模型体积 | 20MB | 80MB | 400MB |
从结果看,RoBERTa 微调在各项指标上是全面最优的,尤其负面召回率达到了 0.88,对比逻辑回归的 0.74 提升明显。这个提升的意义在业务层面很大——相当于每 100 条真实负面反馈里,能多找回来 14 条。但也有一个显著代价:纯 CPU 推理需要 400ms,没办法满足线上 300ms 的延迟要求。
所以线上的最终部署策略是双模型路由:短文本(小于 64 字)和简单句式走逻辑回归模型,长文本和低置信度样本走 RoBERTa 模型,再由置信度阈值决定是否进入人工审核流程。这套混合策略在实际运行中平均响应时间 120ms 左右,整体准确率比纯逻辑回归高约 4 个百分点,同时避免了 RoBERTa 的高延迟瓶颈。
5.3 特征重要性与模型可解释性分析
传统模型的一个优势是可解释性强。逻辑回归的系数直接对应每个特征词对预测结果的贡献权重。我在项目交付时专门给业务方写了一份特征分析报告,列举了正向贡献最大的词和负向贡献最大的词。正向词集中在“满意、不错、好用、贴心、快捷”,负向词集中在“差、慢、坏、失望、投诉”,这符合直觉,也让业务方对模型的判断逻辑有了信心。
但也有几个词很有意思。“但是”这个词在负向类别里的权重很高,不过它的实际作用更像一个转折标记——“好用但是太贵了”情感是偏负的。这说明模型学到的不只是单个词的情感极性,还隐式捕捉了一些句法模式。类似的还有“竟然”“居然”这类词,它们往往出现在惊讶语境中,情感极性随上下文变化。
对 RoBERTa 这种深度模型来说,可解释性就弱多了。虽然有一些工具可以做注意力可视化,但在实际业务中意义有限,更多是作为 Debug 工具使用。我的做法是抽取出模型预测错误但置信度很高的样本,逐个分析错误模式,反推数据层和预处理层的问题。这个方法虽然土,但非常有效——很多时候模型的系统性错误,根源不在模型结构,而在于训练数据的偏见。
6. 常见问题与排查技巧实录
这个部分记录的每一个问题,都是我在开发或上线阶段真实踩过的坑。这些问题在教科书和官方文档里很少被讲透,但实际工作中几乎绕不开。
6.1 类别不均衡问题的系统解决方案
前面提到了数据不均衡的现象,这里展开说解决方案。我尝试的第一个方法是过采样,也就是把少类别的样本复制多份,让训练集里各类别数量接近。这个方法简单直接,但缺点明显:复制样本导致模型对少数类产生过拟合,泛化性能并没有真正提升。我在验证集上测试发现,过采样后的负面召回率确实提升了,但正向的精确率下降了很多,整体 F1 反而略降。
第二个方法是类别加权,也就是在计算 loss 时给少数类样本更高的权重。scikit-learn 的逻辑回归支持 class_weight 参数,PyTorch 里可以手动给 CrossEntropyLoss 传入 weight。这个方案比过采样好,因为它不会改变样本数量,只是调整了模型对少数类的关注程度。我将权重设为与类别频率成反比,负面样本的召回率提升了 6 个百分点,同时正向样本的精确率只下降了 1 个百分点,整体收益为正。
第三个方法是把问题从三分类降为二分类再合并。具体做法是分别训练“正/非正”“负/非负”两个二分类器,再把结果合并。这套方法的优点是更灵活,可以让业务方分别调节两个分类器的阈值,比如宁愿多误判一些中性为负面,也不能漏掉真实负面。缺点是训练和推理成本翻倍。最终我没有在线上使用这个方案,但在数据极度不均衡的场景它确实值得考虑。
提示:处理不均衡问题时,先确认你的业务更在意哪一类误差。如果更在意漏报(负面没识别出来),就优先提升召回率;如果更在意误报(把正常内容当成负面),就优先提升精确率。没有免费的午餐,关键看业务代价。
6.2 线上表现不如测试集的原因排查
模型在测试集上表现很好,上线后却频频出问题,这种情况相信很多人都遇到过。我第一次上线时就遇到了线上准确率比测试集低 8 个百分点的诡异现象。排查之后发现,问题的根源不是模型变差了,而是线上数据的分布和训练时不一样。
具体来说有两个主要原因。第一个是文本来源偏移:测试集是人工从历史工单里抽样标注的,分布比较均匀;线上请求却混杂了大量短文本、重复文本和无效内容,比如用户连续点击导致的重复提交。这些噪声样本在训练集里很少出现,模型面对它们时只能瞎猜。我增加的规则拦截层把这类无意义请求在进入模型前就过滤掉,线上效果立刻回升了 3 个百分点。
第二个原因是标签口径漂移。业务团队在系统上线后半年的标注规范发生了变化,比如说以前“一般”标为中性,后来改为标为负向。模型的标签分布还是老规范,自然和新数据对不上。解决方法是建立定期的标注一致性校验流程:每个月抽取一批最新业务数据,用当前模型预测,再请标注人员人工复核,对比模型预测与人工标注的一致性。一旦发现一致性持续下降,就启动新版本模型的重新训练。
6.3 推理性能优化与资源占用控制
推理性能优化是一个反复迭代的过程。最初我用纯 Python 写了一个同步推理循环,单条请求处理时间还过得去,但并发一上来就开始排队。第一次优化是把预处理中的正则表达式预编译好,并且把清洗函数里的字符串操作尽量向量化。这个改动不大,但减少了约 30% 的单条处理耗时。
第二次优化是使用 ONNX Runtime 替代原生 PyTorch 推理。把 RoBERTa 模型导出为 ONNX 格式后,CPU 推理速度提升了两倍多,从 400ms 降到了 180ms 左右。这个提升非常可观,基本解决了 CPU 环境下的延迟问题。导出过程也没有想象中复杂,只需要安装相应依赖库,做一次模型转换验证即可。不过需要注意,转换过程中有时会出现算子兼容性问题,这时需要检查模型里是否有特殊层不受 ONNX 支持。
第三次优化是批量推理。当 batch size 从 1 调到 32 时,平均单条处理时间进一步降低。在线接口虽然不能像离线任务那样自由调整 batch size,但可以在服务层做一个简单的动态聚合:收集 50ms 窗口内的所有请求,一并送入模型推理。这个技巧在流量平稳时效果明显,能显著提升 GPU 利用率。当然这个方法会增加第一个请求的等待时间,不适合严格的低延迟场景。
6.4 误判兜底与人工审核闭环
无论模型效果多好,总会出现误判。关键是误判之后怎么办,而不是幻想模型不会犯错。我在系统设计里加了一个人工审核兜底链路:当模型预测的置信度低于阈值,或预测标签为负面但置信度在某个区间时,该条记录自动进入人工审核队列。业务方每天只需要处理几十条模型“拿不准”的样本,就能显著提升整体准确率。
置信度阈值的选取需要结合业务容忍度来定。我把阈值调到 0.6 后,需要人工审核的样本占比约 8%,系统整体准确率从 88% 提升到了 93%。如果阈值调到 0.8,人工审核占比降到 3%,但整体准确率提升有限,因为模型高置信度的样本本来就比较可靠。实际运营中我使用的是动态阈值:白天人工充沛时用低阈值,深夜人工少时用高阈值。
人工审核的结果不是白白浪费的。所有审核过的样本都会回流到训练数据中,形成“预测-审核-回流-重训”的闭环。随着系统运行时间增长,模型在边界样本上的表现会越来越好。这个闭环是系统效果持续提升的根本原因,我在设计初期就把它作为核心机制来搭建,现在看来这个决策非常正确。
7. 从情感三分类到更多方向的实用扩展
三分类情感分析上线并稳定运行之后,业务方自然会提出更多需求。这个项目其实留下了很好的扩展空间,我这里聊聊几个已经在尝试或验证过的扩展方向,每个方向都有真实的业务场景驱动。
7.1 细粒度情绪识别:从正负到喜怒哀乐
三分类只能告诉我们是好还是坏,但“坏”也分很多种:“愤怒”和“失望”的负面反馈,处理方式完全不同。愤怒的用户可能需要紧急安抚,失望的用户可能需要了解产品哪里没达到预期。所以细粒度情绪识别是自然扩展方向。
我需要先澄清的是,这个扩展不是简单地把三分类改成多分类就完事。情绪之间不是互斥的,一条文本可以同时包含“满意”和“失望”,比如“东西不错,但物流太慢”。所以细粒度情绪识别更适合建模为多标签分类问题,让模型同时输出多个情绪标签。将三分类模型升级为多标签模型,网络结构只需要把最后的 Softmax 换成多个 Sigmoid,训练时就为每个标签分别计算二分类交叉熵损失。
数据标注是最大的瓶颈。多标签标注比单标签标注难得多,标注员需要识别一个句子里的所有情绪,一致性更难保证。我的做法是先让规则系统给每条候选文本打标签,再由标注员修正,重点标注那些包含情绪转折的样本。目前这个方向的验证结果显示,识别准确率比三分类略低,但信息量大幅增加,业务方的接受度很高。
7.2 属性级情感分析:从句子到对象的精细化拆解
属性级情感分析是另一个重要扩展方向。用户说“屏幕很好,电池太差”,句子整体情感是混合的,简单的三分类会丢失关键信息。如果业务需要知道用户到底在夸什么、骂什么,就需要把情感和具体属性关联起来。这个方向在电商评价、产品反馈场景里价值极大。
实现上通常的做法是:先通过实体识别或属性词匹配找到评价对象,再判断每个对象对应的情感倾向。这个任务可以拆解为两个模型串行处理:第一个模型负责抽取候选属性(屏幕、电池、充电速度等),第二个模型负责判断每个属性对应的情感极性。我暂时用规则加词典的方式做属性抽取,准确率在常见属性词上还可以,但遇到新出现的产品部件名称就无法覆盖,后续计划接入序列标注模型来替换。
属性级情感分析对训练数据的标注提出了更高要求。标注人员需要同时标注属性词的位置和情感倾向,成本大约是普通三分类标注的三倍。但这个方向对业务决策的价值也是三分类的很多倍,因为它能直接告诉产品团队:到底是哪个部件、哪个环节出了问题。
7.3 增量学习与半监督技术的实际应用
最后聊一下增量学习。业务在变化,模型需要持续更新,但每次都重新训练全量数据成本太高,而且旧数据里的知识容易遗忘。我的方案是每周固定时间做一次增量训练:用上周积累的新标注数据和人工审核回流数据,配合旧数据的一部分作为回放,微调当前模型。这个方法在 Bert 模型上效果不错,既能吸收新知识,又不会剧烈改变已有能力。
回放比例的设置很关键。回放比例太低,模型在新数据上过拟合,旧数据上的表现大幅下降;回放比例太高,增量训练的时间成本接近重新训练。经过几次实验,我将回放比例定为旧数据的 30%,在保持旧数据 F1 下降不超过 1 个百分点的情况下,新数据 F1 提升了约 4 个百分点。这个平衡点会随数据变化而波动,每个项目都需要单独测。
半监督学习也在小范围实验过。对未标注的线上数据用模型打伪标签,挑选置信度大于 0.95 的样本加入训练集,能小幅提升模型效果。但伪标签的风险是放大模型的既有偏见——如果模型系统性偏向把某些类型文本判断为负向,这些错误会被伪标签固化。所以我把伪标签样本的权重设得比人工标注样本低,同时限定伪标签样本不超过训练集的 20%,作为增益手段而不是主力。
回看整个项目,最有价值的不是某个模型达到了多高的准确率,而是我把“数据怎么来、怎么标、怎么清洗、模型怎么选、怎么部署、怎么迭代”这条完整链路都走通了。这个链路本身是复用的,换一个领域、换一种文本类型,只需要调整数据标注规范和预处理规则,其他环节都可以平滑迁移。如果你也打算构建文本分析类系统,我建议不要急着追求最前沿的模型,而是先把数据基础设施和评估体系建好。地基稳了,上面盖什么都稳。