资讯动态

中文文本分类实战:Python+scikit-learn+jieba从基线到上线

发布时间:2026/10/27 11:26:12 来源:尧图企业网站定制
做文本分类这件事我自己前后接过大大小小十几次需求从工单自动派单、评论情感判断到日志归类、举报内容初筛用的都是同一套东西Python scikit-learn jieba。所谓文本分类器说白了就是给一段文字自动打上一个或多个标签让原本要人一条条看完的工作变成机器先过一遍。它解决的核心问题不是AI 有多聪明而是每天几千条文本我能不能先自动分流掉八成只把模棱两可的留给人看。这篇内容适合三类人手里有几百到几万条带标签文本、想快速跑出一个能用版本的开发者刚学完机器学习课程、想找个真实场景把链路走通的同学以及被准确率 95% 但线上没法用坑过一次、想搞清楚中间差在哪的人。我下面写的是从零到上线的一条完整路径包括每处参数为什么这么填、哪里最容易翻车。1. 先把需求想清楚整体设计与技术选型1.1 什么样的场景值得上一个文本分类器不是所有文本处理都值得上分类器。我的判断标准很朴素重复性高、标签体系相对稳定、有人工标注过的历史数据这三条同时满足才动手。举个例子某公司的售后工单一天进来两千多条原来靠三个人分拣到物流、退换、质量、咨询四个队列平均每条停留四十分钟这种就是典型场景——文本短、类别少、历史数据现成随便一个线性模型跑出来 macro-F1 到 0.8 以上就能把人工分拣量砍掉一大半。反过来如果标签每周都在变、或者一条文本同时命中好几个类别且权重不同、或者总共只有几十条样本那先别上模型。前两种情况属于业务规则还没定模型只会把这个不确定性放大第三种情况硬上模型的结局基本是过拟合到死不如先写关键词规则顶一阵同时把标注流程建起来。我见过一个团队在只有 60 条样本的情况下硬训了一个四分类模型测试集准确率 0.9上线一周被投诉到下线——因为那 60 条里有 40 条是同一个类模型实际上只学会了猜最多的那类。还有一个常被忽略的前置条件你得能定义什么算分对了。文本分类里质量投诉和售后投诉的边界不同人标出来能差出一半。所以动手写代码之前我会先拉两百条样本让实际做业务的人标一遍算一下标注者之间的一致率。如果两个人都标不到 0.8 的一致率那说明标签定义本身有歧义这时候该做的是改标签说明不是调模型参数。1.2 为什么我建议先做基线而不是上来就上大模型现在一提文本分类很多人第一反应是拉个预训练模型微调。能跑但我不建议把它当第一步原因有三个都很现实。第一是迭代速度。线性模型 TF-IDF在几万条数据上训练几十秒就出结果你可以一个下午试十几种特征组合微调一个小预训练模型光环境、显存、训练脚本调通就得一天中间任何一个环节出问题你都分不清是数据问题还是代码问题。做基线最大的价值不是效果好而是给你一个可对照的下限——后面不管换什么模型只要没超过这个数你立刻知道是模型的问题而不是数据的问题。第二是可解释性。逻辑回归和线性 SVM 能直接吐出每个词对每个类别的权重业务方问为什么这条被判成质量投诉你能回答因为出现了开裂退货七天这几个词。这个能力在项目早期极其重要它帮你验证模型是不是学到了正确的信号而不是学到了标注者的手滑。我踩过的一个坑就是早期模型准确率特别高一看权重发现它主要靠客服工号这个字段在分类——因为不同工号负责不同业务线标注数据里这个特征完美泄漏了标签。第三是部署成本。一个 joblib 文件几兆到几十兆一台普通机器能扛住每秒几百次推理没有任何外部依赖。基线能解决问题的时候上更重的方案在工程上就是负收益。注意我不是说预训练模型不该用。当基线摸到天花板比如短文本、口语化严重、类别语义高度相近或者数据量涨到十万条以上换模型是合理的。但顺序应该是基线 → 分析误差 → 判断瓶颈 → 换方案不是直接换方案 → 效果不行 → 不知道为啥。1.3 三条常见路线的取舍真正要选的主要就三个模型我把它们放在一起对比这张表是我自己项目里反复用到的判断依据模型适合的样本量训练速度对参数敏感度典型表现我的使用场景多项式朴素贝叶斯几百到几万极快低短文本、类别多时意外地稳冷启动、快速验证标签体系逻辑回归几千到几十万快中C 值综合最稳输出可解释首选基线绝大多数场景直接上线性 SVM几千到几十万中中C 值类别边界清晰时略优二分类、特征维度高时备选朴素贝叶斯我一般只用来做第一天的验证——它假设特征独立这个假设在文本上明显不成立不和满意显然相关但它对短文本、高维稀疏特征的鲁棒性出奇地好而且几乎没有参数要调适合在还没想清楚标签体系的时候先跑个数看看。逻辑回归是我默认的选择原因是它输出的概率可以直接当置信度用我可以设个阈值把低于 0.6 的样本转人工这在业务上非常实用。线性 SVM 在类别边界比较干净的时候能比逻辑回归高出一两个点但它不输出概率要用CalibratedClassifierCV包一层做校准多了道工序所以我一般把它当备选。还有一个经常被忽略的选项如果类别数超过二十个先考虑层次分类。我做过一个内容归类项目一开始是 34 类平铺逻辑回归怎么调 macro-F1 都卡在 0.62后来改成两级——先分 5 个大类每个大类下再分 4 到 8 个子类整体准确率直接抬到 0.78。原因是平铺的时候模型要在 34 个高度相似的类里做区分而两级结构让第一层只处理粗粒度差异难度被拆开了。2. 数据这道坎清洗、分词与标签体系2.1 样本量到底要多少才够这个问题没有标准答案但有一个经验区间可以参考每个类别至少 100 到 200 条有效样本是线性模型能跑出可用效果的底线。如果某个类别只有二三十条那它在测试集里可能就三五条评估结果的波动会大到没有意义——你今天测 0.8换个随机种子明天测 0.6。我一般的做法是先统计各类别样本量画出分布。如果最多的类是最少的类的 10 倍以上就要处理不平衡第 5 章会细讲。如果某个类少于 50 条我会先问业务方这个类能不能合并到相近的类里或者能不能补标一批。硬扛着补样本是最差的选择因为模型学不到足够的变化只会记住那几个具体的词。还有一个估算方法很实用看学习曲线。用learning_curve跑一下横轴是训练样本量纵轴是验证分数。如果曲线在样本量翻倍后还在明显上升说明加数据有用去要数据如果已经平了说明瓶颈在特征或模型加数据白搭。这张图我每次立项都会跑一遍它能把要不要继续投人力标注这个争论直接终结。2.2 中文文本清洗的六件事清洗这一步决定了后面的上限我把它拆成六件必做的事每件都有原因统一编码。读 CSV 一定用encodingutf-8-sig因为从表格软件导出的文件经常带 BOM 头用普通 utf-8 读进来第一列名会变成\ufeff标签然后你在代码里找df[标签]直接 KeyError查半天。这个坑我至少踩过三次。去掉无意义的结构化噪声。链接、邮箱、一长串数字、重复的标点这些对分类几乎没有区分度但会白白占掉词表空间。注意数字不要直接删我一般替换成占位符NUM因为退款 3 天和退款 30 天虽然数字不同但出现数字本身可能是有信号的特征。全角半角统一。中文文本里和100、和()混着来是常态用unicodedata.normalize(NFKC, s)一行搞定。处理大小写和空白。英文缩写、品牌名的大小写不一致很普遍统一转小写。连续空格、不换行空格\u3000、制表符都要压成单个空格否则分词结果里会混进一堆空白 token。剔除超短样本。长度小于 4 个字符的文本比如好的收到1在业务上是无效样本留着只会污染训练集。我一般会统计长度分布把 1% 分位数以下的直接删掉同时记录删了多少——如果删得太多说明数据采集环节有问题。去重。这条最容易被跳过但影响巨大。真实数据里重复样本非常多尤其是从客服系统导出的数据同一个问题被不同人问过几十遍文本几乎一模一样。如果不去重这些重复样本很可能同时落进训练集和测试集你的评估分数会被虚高十几个点。import re import unicodedata def clean_text(s: str) - str: if not isinstance(s, str): return s unicodedata.normalize(NFKC, s) # 全角转半角 s s.lower() s re.sub(rhttps?://\S|www\.\S, , s) # 链接 s re.sub(r\S\S\.\S, , s) # 邮箱 s re.sub(r\d, NUM , s) # 数字替换为占位符 s re.sub(r[^\w\u4e00-\u9fa5], , s) # 保留中文、字母、数字 s re.sub(r\s, , s).strip() return s注意上面最后一行的正则会把标点全部干掉。这一步是有争议的——问号和感叹号在情感分类里是有用的信号这也叫服务和这也叫服务语气完全不同。做情感、意图识别时我会保留。这几个做主题分类时则全删。清洗规则要跟着任务目标走不存在通用模板。2.3 分词与停用词别迷信默认词典中文必须分词这一点没得选。jieba 是最省事的选择但默认词典在垂直领域会翻车。举个例子蓝牙耳机默认词典能切对但降噪豆开放式耳机挂脖式这类词很可能被切碎降噪豆变成降噪豆语义就散了。同类问题在医疗、法律、工业领域更严重。解决办法是加载自定义词典把业务词表塞进去import jieba jieba.load_userdict(user_dict.txt) # 每行一个词可带词频和词性 words jieba.lcut(这款降噪豆的续航有点短) # 加载前[这款, 降噪, 豆, 的, 续航, 有点, 短] # 加载后[这款, 降噪豆, 的, 续航, 有点, 短]自定义词典怎么来两个途径一是让业务方给一份产品名、术语表二是用 TF-IDF 或者jieba.analyse.extract_tags从全量语料里抽高频词人工过一遍把被切碎的组合词挑出来。后者更省力我一般先跑一遍提取几百个词看一遍也就半小时。停用词这块要格外小心。通用的中文停用词表各种哈工大百度版本的列表拿来主义会出问题因为它把不没很太这些删掉了。做情感分类时删掉否定词是灾难性的——好和不好变成同一个特征。做主题分类时删掉这些倒无所谓。我的做法是用通用停用词表打底再把明显有情感或程度含义的词捞回来形成一个项目专属的停用词文件放在代码库里版本管理。还有一个小技巧停用词不要删得太狠。词表越大TF-IDF 的维度越低、训练越快但信息损失也越大。我的经验是停用词表控制在 800 到 1500 个之间比较平衡删掉的应该主要是虚词、语气词、高频无信息动词进行相关问题这类。2.4 标签体系怎么切才不返工这一节是我认为整个项目里最值钱的部分因为它决定了后面所有工作的方向。标签必须互斥且穷尽。互斥是指一条文本只属于一个类多标签任务另说穷尽是指所有可能出现的情况都有归属。听起来是废话但实际数据里其他这个类经常被当成垃圾桶塞进去 30% 的样本然后模型在这个类上表现极差拉低整体指标。我的处理是如果其他超过 15%就说明标签体系没覆盖全回去补类别。粒度要匹配业务动作。分类结果最终是要驱动某个动作的标签粒度应该和这个动作对齐。比如工单分派如果分到退换组和质量组之后两边还要再人工判断一轮那说明粒度太粗如果分到退换-七天无理由退换-质量问题退换-错发漏发但后两个的处理流程完全一样那说明粒度太细。判据就一条分类结果能不能直接触发动作不需要人再看一眼。标注规范要写成文档。每个类别给三到五个正例、两到三个边界例写清楚什么情况算、什么情况不算。这份文档的投入产出比高得离谱我在一个项目里花了半天写完规范后面标注一致率从 0.72 提到 0.89返工量少了三分之二。3. 特征工程TF-IDF 的参数不是随便填的3.1 词袋与 TF-IDF 到底差在哪词袋模型CountVectorizer数的是词出现次数TF-IDF 在次数基础上做了两件事一是用词频做归一化TF二是用一个词在多少篇文档里出现过做惩罚IDF。直白点说词袋认为出现三次比出现一次重要三倍TF-IDF 认为在所有文档里都出现的词不重要。为什么这个区别关键因为文本里最高频的词往往是最没信息量的。的了是出现次数最多词袋会给它们最高的权重模型就被这些噪声带偏了。IDF 正好把它们压下去同时把开裂漏液拒收这种只在少数文档里出现的词权重抬上来。实测下来同一份数据词袋 逻辑回归大概能到 0.75换成 TF-IDF 直接到 0.83其它什么都不改。所以在中文文本分类里我基本不考虑纯词袋除非任务本身就跟词频强相关。3.2 三个必须调的参数TfidfVectorizer参数不少但真正影响结果的只有三个加上一个隐藏的第四个参数含义常用取值调它的理由max_features保留词表大小20000 / 50000 / None控制维度和过拟合数据少时必设ngram_range词组长度(1,1) / (1,2)中文里双字词、三字词信息量大min_df/max_df词的最低/最高文档频率2 / 0.9过滤生僻词和万能词sublinear_tf词频取对数True抑制长文本里词频的线性膨胀max_features怎么定我的做法是先不设上限跑一次看实际词表大小。如果只有三万个词那就别设如果到了五十万说明数据里有大量生僻词、错别字、噪声这时候设 30000 到 50000 是合理的同时回去看看清洗是不是漏了什么。设太小必然丢信息设太大只会变慢不会变好所以这个参数我通常放在网格搜索里但搜索范围只给两三个值。ngram_range从 (1,1) 提到 (1,2)是性价比最高的一步。中文里不满意、七天无理由、没有收到这种搭配单词层面完全看不出含义二元组就不一样了。代价是词表膨胀三到五倍、训练变慢但准确率通常能涨两到四个点。三元组我很少用增益有限而维度爆炸。min_df2的意思是至少在两篇文档里出现过的词才保留。这个默认值很实用它能把只出现一次的错别字、人名、随机字符过滤掉相当于一次轻量降噪。数据量小于一千条时我会设成 1否则词表太稀疏。sublinear_tfTrue是个容易被忽略的开关。它把词频取1 log(tf)好处是长文本里同一个词出现二十次和五十次权重不会差太多。文本长度分布很不均匀时比如既有十个字的短评又有五百字的长评这个开关能明显稳住效果。3.3 中文的词级与字符级词级先分词再进 TF-IDF是默认选择语义清晰、可解释性好。但它有个前提分词得靠谱。当文本里有大量网络用语、错别字、缩写、拼音谐音时分词会切得很碎效果反而下降。字符级analyzerchar_wb按 2 到 4 个字符切片不走分词直接按字符组合做特征。它的优势是完全不受分词质量影响对错别字和网络用语天然鲁棒缺点是维度高、可解释性差。我在短文本、口语化严重的场景里试过字符级 (2,4) 的方案能比词级高出三到五个点。代价是模型文件大了好几倍、推理慢一些但如果你只是内部工具这点成本完全可接受。一个折中方案很实用词级和字符级各训一个把预测概率加权平均。听起来像土办法实测在短文本上很有效而且实现起来就十几行代码。4. 完整实操一份能直接跑的分类器代码4.1 环境与依赖依赖不多但要锁版本否则换台机器很容易出问题pip install scikit-learn1.3.2 pandas2.1.4 jieba0.42.1 joblib1.3.2为什么强调锁版本因为 scikit-learn 在模型序列化上并不保证跨版本兼容用 1.3 训练出的 joblib 文件用 1.4 加载可能会报InconsistentVersionWarning甚至直接失败。这条在单人开发时无所谓一旦要交付给别的团队部署就是必踩的坑。4.2 数据加载与训练集划分数据格式我一般约定成两列text和label。读进来先做三件事——去空、去重、看分布。import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(data.csv, encodingutf-8-sig) df df.dropna(subset[text, label]) df[text] df[text].astype(str).str.strip() df df[df[text].str.len() 4] df df.drop_duplicates(subset[text]).reset_index(dropTrue) print(df[label].value_counts(normalizeTrue).round(3)) X_train, X_test, y_train, y_test train_test_split( df[text], df[label], test_size0.2, random_state42, stratifydf[label] # 关键 )stratify这个参数必须加。不加的话如果某个类只占 3%随机划分很可能训练集里一条都没有或者测试集里只有一条评估结果完全不可信。加了之后能保证训练集和测试集的类别比例一致。random_state42不是玄学是为了让所有实验可复现。我见过不止一次昨天调出来 0.85 今天变成 0.80的争论最后发现只是没固定种子。4.3 用 Pipeline 把预处理串成一条线这一步是我最想强调的。很多人写代码是这样的先对全量数据做 TF-IDF再划分训练测试集然后训练。这个顺序有个致命问题——数据泄漏。向量化时用到了测试集的词频统计IDF 是在全量数据上算的测试集的信息提前泄露进了特征里评估分数会虚高。正确做法是把向量化和分类器放进Pipeline让 sklearn 保证只在训练集上fit测试集只transform。import jieba from sklearn.pipeline import Pipeline from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression STOPWORDS set(open(stopwords.txt, encodingutf-8).read().split()) def preprocess(s: str) - str: # clean_text 来自 2.2 节 s clean_text(s) return .join(w for w in jieba.lcut(s) if w.strip() and w not in STOPWORDS) pipe Pipeline([ (tfidf, TfidfVectorizer( token_patternr(?u)\b\w\b, # 不用默认的 \w\w否则单字被丢 ngram_range(1, 2), min_df2, max_df0.9, sublinear_tfTrue, max_features50000, )), (clf, LogisticRegression( C1.0, max_iter1000, class_weightbalanced, n_jobs-1, )), ]) pipe.fit(X_train.map(preprocess), y_train)这里面有两个细节值得展开。第一个是token_pattern。默认值是r(?u)\b\w\w\b意思是至少两个字符的词单个汉字直接被过滤掉。这对英文没问题对中文问题很大——卡慢贵差这些单字在中文里是完整语义丢掉它们等于丢信息。改成\b\w\b就保留了。我第一次遇到这个问题时排查了两个小时以为是分词器没生效。第二个是token_pattern和自定义tokenizer的冲突。网上很多教程直接写tokenizerjieba.lcut在较新版本的 sklearn 里会触发参数交互的警告甚至行为不一致。我现在的习惯是把分词放在 Pipeline 外面的预处理函数里向量化器只负责按空格切这样行为确定、好调试而且预处理函数可以单独写测试。代价是推理时也得调一遍preprocess这个约束我会明确写进接口文档第 4.6 节会讲一个更彻底的解法。4.4 训练与评估别只看准确率from sklearn.metrics import classification_report, confusion_matrix y_pred pipe.predict(X_test.map(preprocess)) print(classification_report(y_test, y_pred, digits4)) labels sorted(y_test.unique()) cm confusion_matrix(y_test, y_pred, labelslabels) print(pd.DataFrame(cm, indexlabels, columnslabels))classification_report会给每一类的 precision、recall、f1以及 macro 和 weighted 平均。这里有个必须知道的事类别不平衡时accuracy 是废的。假设四个类占比分别是 70%、10%、10%、10%一个把所有样本都预测成第一个类的模型accuracy 也有 0.7但完全没用。这时候要看macro avg的 f1它对每一类一视同仁。confusion_matrix一定要看尤其是它的对角线之外。我在一个项目里遇到整体 macro-F1 0.79看着还行但打开混淆矩阵发现物流破损这一类有 60% 被判成了质量问题。这两类在业务上处理流程完全不同实际损失很大。原因也很好理解——训练数据里破损相关的描述经常同时提到质量标注者自己也没分清楚。后来是把这两个类的定义重新写清楚、补标了三百条边界样本才解决。另外建议额外算一下每类的召回率下限。业务上通常更怕漏判比如投诉升级这类漏了要出事。我会单独看这一类 recall 到没到 0.9不够的话单独调阈值或加规则兜底。4.5 超参数搜索把范围收窄再搜from sklearn.model_selection import GridSearchCV param_grid { tfidf__ngram_range: [(1, 1), (1, 2)], tfidf__min_df: [1, 2, 3], tfidf__max_features: [30000, 50000, None], clf__C: [0.3, 1.0, 3.0], } grid GridSearchCV(pipe, param_grid, cv5, scoringf1_macro, n_jobs-1, verbose1) grid.fit(X_train.map(preprocess), y_train) print(grid.best_params_, grid.best_score_)这里 2×3×3×3 54 种组合5 折交叉验证就是 270 次训练。在几万条数据上这个量级大概跑十几分钟到半小时可以接受。如果组合数上千我会换成RandomizedSearchCV(n_iter40)效果通常差不了多少。scoringf1_macro这个选择很重要。默认的 scoring 是 accuracy前面说过不平衡时它不可靠。用f1_macro会逼着模型在所有类别上都做对代价是可能牺牲头部类的精度——这个取舍你要根据业务判断。如果头部类最重要用f1_weighted如果每一类都不能放弃用f1_macro。搜索完之后一定要用最优参数重新在完整训练集上拟合一次因为GridSearchCV默认refitTrue会自动做这件事但如果你手动传了refitFalse就拿到的是一个没训练的模型。这个坑不常见但很致命报错信息还特别难懂。4.6 模型持久化与线上推理import joblib joblib.dump(pipe, clf_pipeline_v1.joblib) # 线上 pipe joblib.load(clf_pipeline_v1.joblib) text 收到货就发现外壳有裂纹申请换一个 proba pipe.predict_proba([preprocess(text)])[0] label pipe.classes_[proba.argmax()] print(label, round(proba.max(), 4))这里有个容易踩的雷预处理函数不在 Pipeline 里线上必须手动调。一旦有人忘了调模型照常输出结果只是结果全错而且不报错。这种静默失败最难排查。我的解法有两个。一是在Pipeline前面加一个FunctionTransformer把预处理也包进去from sklearn.preprocessing import FunctionTransformer pipe Pipeline([ (prep, FunctionTransformer(preprocess)), (tfidf, TfidfVectorizer(...)), (clf, LogisticRegression(...)), ])这样pipe.predict([原始文本])就能直接用接口干净。但要注意FunctionTransformer里包的是自定义函数joblib 反序列化时需要能 import 到这个函数否则会报AttributeError: module __main__ has no attribute preprocess。所以这个函数必须写在可导入的模块文件里比如preprocess.py不能定义在脚本的__main__里。这个坑我踩过表现形式是本地测试好好的打包成服务就加载失败。第二个解法是把预处理写成独立模块 单元测试。给preprocess写几个断言输入 None 返回空串、输入空串返回空串、输入带链接的文本链接被去掉、输入超长文本不抛异常。这几个测试花十分钟写能省掉后面几小时的线上排查。还有一件事保存模型的时候连词表和版本号一起存。我会在旁边放一个meta.json记录训练时间、样本量、各类别分布、评估指标、sklearn 版本、jieba 版本。模型效果掉了的时候这份元信息是唯一能帮你快速定位是数据变了还是模型变了的东西。5. 常见问题与排查实录5.1 离线 95% 线上 60%问题出在哪这是最高频的抱怨原因就那么几类我按出现频率排一下。第一数据泄漏。向量化在划分前做了、去重没做导致训练测试集有重叠、特征里混进了未来信息比如用处理时长去预测是否会升级投诉——升级之后才会有处理时长。排查方法很简单把测试集里的文本拿去训练集里搜看有没有几乎一模一样的。有的话先解决重复。再看特征把每个特征的实际含义过一遍问一句这个字段在预测时刻真的已知吗。第二线上数据和训练数据分布不一致。训练数据是从历史工单里导的那是人工筛选过的、语言比较规范的线上进来的是用户随手打的错别字、表情、语音转文字的错误一大堆。这种情况的解法是从线上真实流量里抽一批标好混进训练集。我一般会在上线第一周每天抽 200 条人工标注一周后加进去重训效果通常能拉回来一半以上。第三预处理没对齐。离线时preprocess在X_test.map()里调了线上忘了调或者离线用的停用词表是旧版线上打包的是另一版。这类问题的特征是所有类别的预测分布都变得很怪比如全挤到一个类里。解法就是前面说的把预处理放进 Pipeline。第四标签映射错了。模型输出的是 0/1/2/3线上有个映射表这个表如果顺序错了结果会整批错位而且错得很均匀特别有迷惑性。我现在的习惯是让模型直接输出中文标签或者把classes_一起存进 meta.json上线前跑一遍对照。5.2 类别不平衡处理顺序很重要发现最多的类是最少的类的 15 倍怎么办我的处理顺序是固定的先看能不能合并或补样本。业务上能不能把长尾类合并能补标的话补 200 条长尾样本比任何算法手段都有效。再试class_weightbalanced。这是一行参数的事效果通常很明显代价是头部类的 precision 会掉一点。逻辑回归、SVM 都支持这个参数。然后考虑对训练集做重采样。注意只对训练集做测试集必须保持真实分布否则评估结果没意义。过采样复制长尾样本简单但容易过拟合欠采样丢头部样本快但丢信息。我一般用RandomOverSampler配合class_weight双管齐下。最后才考虑调阈值。如果业务只关心长尾类的召回可以把长尾类的判定阈值从 0.5 降到 0.3用 precision 换 recall。这个操作要在验证集上做不能拍脑袋。有一个必须提醒的点不要用准确率来判断不平衡问题解决得好不好。上面那个 70/10/10/10 的例子你把所有样本都预测成头部类accuracy 就是 0.7看着比认真分类但只有 0.65还高。这种自欺欺人在项目汇报里特别常见我见过太多次了。5.3 短文本、新词、错别字短文本少于 10 个字是传统方法的软肋因为可用特征太少。我用过最有效的三招加入字符级 n-gram 特征把用户的上下文信息拼进来比如同一会话的前一条消息如果业务允许把极短文本直接走规则兜底不进模型。新词网络热词、新造词的问题是训练时没见过推理时会被切成碎片或者变成 OOV。解法是定期更新自定义词典并保留一条低置信度转人工的通道。我一般会监控置信度分布如果某个时间段低于 0.5 的样本比例突然上升说明数据分布变了该重训了。错别字是最难办的。字符级特征能缓解一部分质量打成质亮字符级还能匹配到质但根治要靠输入侧的纠错或者干脆在训练数据里主动加入错别字变体做数据增强。我做过一次数据增强把训练集里 10% 的样本随机替换同音字测试集上的鲁棒性提升很明显——代价是干净文本上的精度掉了不到一个点可以接受。5.4 问题速查表把上面这些整理成一张表出问题的时候从上往下对现象最可能的原因快速验证处理线上全线崩全预测成同一类预处理没调 / 标签映射错拿一条已知样本手动跑把预处理包进 Pipeline离线高线上低且差距稳定数据分布不一致对比线上线下的文本长度和词频分布混入线上标注样本重训离线就虚高交叉验证也高数据泄漏 / 重复样本检查划分顺序、做文本去重先划分再 fit去重长尾类 F1 极低类别不平衡看各类样本量class_weight 过采样某个固定词出现就判错特征泄漏看模型权重最高的词删掉泄漏字段补标边界样本推理变慢、内存涨模型文件变大 / 词表膨胀看 joblib 文件大小降 max_features换线性模型加载模型报版本警告sklearn 版本不一致对比训练和部署环境锁版本或重新训练加了新词还是切错自定义词典没生效单独跑一次 jieba.lcut 看结果检查词典路径和加载时机注意这张表里的快速验证一栏我强烈建议你做成自动化脚本放进项目里。最常见的三个检查——去重、划分顺序、预处理一致性——写成五个断言每次提交前跑一遍。这比事后排查省太多时间了。6. 我在实际项目里踩出来的几条取舍先说一个具体的别急着上深度学习。我在一个四分类的工单项目里基线逻辑回归跑到 macro-F1 0.81业务方觉得不够要求上预训练模型。折腾了两周训出来 0.86看着涨了五个点但推理成本翻了二十倍而且每次重训要等半小时迭代速度全没了。最后上线的是逻辑回归加一条规则兜底把含投诉12315等关键词的样本强制转人工实际业务指标反而更好。这件事之后我判断要不要升级模型的依据变成了看混淆矩阵里错的那部分是数据问题还是模型能力问题。如果错的是标签本身有歧义换什么模型都没用只有确认是语义太复杂线性模型学不动才值得上更重的方案。第二个是个小技巧很实用给模型加一个我不确定的出口。predict_proba的最大值低于某个阈值时不输出结果标成待人工处理。这个阈值怎么定在验证集上按置信度排序取准确率达到 0.95 时对应的最低置信度作为阈值。我一般会得到 0.55 到 0.65 之间的值这样能覆盖 85% 左右的样本、保证这部分的准确率在 95% 以上。剩下的 15% 转人工整体效率提升依然巨大但出错率可控。这个设计在业务方那里的接受度远高于全自动但偶尔错。第三个是关于评估的测试集要冻起来。划分出一份测试集之后除了最终验收平时不许碰。调参、看效果一律在验证集或交叉验证上做。我见过一个团队反复在测试集上试了二十多个方案最后报的指标是 0.93实际线上 0.7 都费劲——因为测试集已经被他们看穿了。冻测试集这个纪律比任何调参技巧都重要。最后一个留一份 badcase 库。每次发现判错的样本不管线上线下都存一条标注上错在哪、应该是什么。这份库积累到几百条的时候价值远超你随手加的数据量——因为它集中了模型真正的短板。我现在的习惯是每个版本重训前先看一遍 badcase 库把它们按类别归类往往能直接看出该补哪类数据、该调哪个特征。这件事花的时间不多但几乎每次都能找到改进点。如果非要给这套东西一个入口建议那就是先花半天把数据和标签弄清楚再用三十行代码跑通 Pipeline最后才去想怎么把分数从 0.8 抬到 0.85。顺序反了后面每一步都会加倍痛苦。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑