简介:面向自然语言处理初学者与舆情分析实践者的微博情感分析完整代码包,围绕中文微博文本的正负情感判别展开,可用于课程实验、毕设选题与小型舆情监控原型搭建。压缩包共收录385个文件,以311个Python源码为主体,配合6个CSV数据集与预测结果样例、若干pyd与dll依赖、exe可执行程序、pth模型权重及txt、xml配置文件;解压后约18.16MB。目录按数据、训练、预测环节分布,便于按流程检索。内容覆盖数据清洗、中文分词、停用词过滤、词性标注与情感词典构建,并给出朴素贝叶斯、支持向量机、随机森林及LSTM、Transformer等多条建模路线的实现;读者可对照CSV样本完成训练与测试集预测,理解从原始微博到情感标签的完整链路。已有3407人学习下载,适合想动手复现中文文本分类基线、积累特征工程与模型调参经验的学习者。
1. 数据不缺,缺的是一条能跑通的基线:微博情感分析这套代码到底解决什么问题
上周有人丢给我一个 CSV:两列,一列是微博正文,一列是 0/1 标签,问能不能三天出一版情感分析。这活儿听起来简单——分词、TF-IDF、逻辑回归,三个库就能跑。真正卡人的从来不是模型,是数据:话题符号、@ 昵称、表情占位符、转发链里重复的文案、正负样本比例 7:3,随便哪一条都能让第一版基线翻车。
这份微博情感分析代码包解决的正是「从 0 到能跑」这一段:带标注的数据集、文本清洗脚本、jieba 分词与停用词表、TF-IDF 加传统分类器的基线,以及一条 TextCNN 或预训练模型微调的进阶线,最后收成一个predict(text)的推理入口。适合两类人:要交课程设计或毕设的学生,和手里有业务文本、想快速验证一版情感打标可行性的后端或算法开发。下面按目录结构、数据流水线、模型训练、故障排查、二次开发的顺序拆开讲。
2. 项目骨架与技术选型:从目录结构看一条微博怎么变成标签
拿到压缩包,别急着python train.py。先花十分钟把目录扫一遍,搞清楚每一层在干什么,后面出问题才知道该看哪个文件。这类情感分析工程,结构基本是固定的四段式:数据、预处理、模型、推理入口。差异只在模型层是纯 sklearn 还是混了深度学习。
2.1 目录结构:一个压缩包解开之后应该看到什么
解压后的典型形态如下。如果你的实际目录跟这里略有出入,比如模型目录叫model而不是models,别慌,按文件职责对齐就行,路径差异改一下config.py里的常量即可。
weibo_emotional_analyse/ ├── data/ # 数据集与词表资源 │ ├── train.csv # 主训练集:text + label │ ├── test.csv # 留出集,用来做最终评估 │ ├── stopwords.txt # 中文停用词表 │ ├── user_dict.txt # jieba 自定义词典,补网络新词 │ └── emotion_words.txt # 情感词白名单,防误删 ├── preprocess/ │ ├── clean.py # 正则清洗:URL、@、话题、表情占位符 │ └── tokenizer.py # 分词 + 去停用词 + 词表构建 ├── models/ │ ├── train_ml.py # TF-IDF + 传统分类器基线 │ ├── train_dl.py # TextCNN / 预训练模型微调 │ └── textcnn.py # 网络结构定义 ├── utils/ │ ├── config.py # 路径、超参、随机种子集中管理 │ └── metrics.py # 混淆矩阵、F1、分类报告封装 ├── outputs/ # 训练产物落盘位置 │ ├── tfidf_lr.pkl │ ├── vocab.json │ └── textcnn.pt ├── predict.py # 单条 / 批量推理入口 ├── app.py # 简易 Web 服务,方便演示 └── requirements.txt几个文件值得单独说。utils/config.py是整个工程里最容易被忽视、但改造成本最低收益最高的文件——路径写成相对路径、随机种子固定成常量、超参集中放一处,后面换数据集只动这一个文件。data/emotion_words.txt是很多人不会加的东西,作用后面第 3 章讲分词时会重点说,没有它,你的模型会把「差」「烂」「赞」这类单字情感词直接删掉。
| 文件 | 职责 | 你在二次开发时改它的频率 |
|---|---|---|
preprocess/clean.py | 文本清洗规则 | 高,换数据源必改 |
preprocess/tokenizer.py | 分词与词表 | 高,加新词必改 |
models/train_ml.py | 基线训练 | 中,调参时改 |
models/textcnn.py | 网络结构 | 低,除非换模型 |
utils/config.py | 全局配置 | 高,但每次都是小改 |
predict.py | 推理封装 | 中,接业务时改 |
2.2 技术选型:为什么同时留了 sklearn 和深度学习两条线
这类项目最常见的设计是「双轨制」,不是作者偷懒,是实用主义。传统线是 TF-IDF 加逻辑回归或线性 SVM,几万条数据在普通笔记本上几分钟跑完,F1 通常能到 0.80 上下,做基线、做对比实验、给非算法同事演示都够用。深度学习线是 TextCNN 或中文预训练模型微调,在十万条量级上一般能再涨三到五个点,代价是要显卡、要调参、训练时间从分钟级变成小时级。
分词用 jieba 几乎是默认选择,中文情感分析里没有比它更省事的分词器。它的问题在于对新词不敏感,「破防」「无语子」「绝绝子」这类词在默认词典里没有,会被切碎成无意义的单字,所以工程里都配了user_dict.txt。特征层面,传统线走 TF-IDF 并开ngram_range=(1,2),让「不开心」这种否定搭配成为一个特征;深度线走词表映射加定长截断,超出部分直接砍掉。
Web 层用 Flask 起一个/predict接口,够用、依赖少、十行代码能跑通。如果你后面要接前端页面或者做批量文件上传,也是在这个app.py上长出来的,不需要另起服务框架。
提示:不要一上来就上预训练模型。先用基线把数据跑通,拿到一份可复现的 F1 数字,再去对比深度模型有没有涨。没有基线的涨跌判断,全是玄学。
2.3 环境搭建:三条命令把跑起来的前提补齐
依赖集中在requirements.txt,环境我一般用 conda 单独开一个,避免和本机的其他项目打架。中文文本处理最容易踩的是编码和分词器版本冲突,隔离环境能省掉一半麻烦。
# 1. 建一个干净的 Python 3.8+ 环境,别用系统自带解释器 conda create -n weibo_sa python=3.9 -y conda activate weibo_sa # 2. 装依赖;torch 建议按官网命令选对应 CUDA 版本单独装 pip install -r requirements.txt # 传统线必需:pandas numpy scikit-learn jieba joblib # 深度线额外:torch(或 tensorflow)、tqdm # 3. 验证分词器是否装好,中文能否正常切分 python -c "import jieba; print('/'.join(jieba.lcut('这家店服务态度真的很差')))"第三条命令不是多余的。很多人装完依赖直接跑训练脚本,报一堆看不懂的错,最后发现是 jieba 的词典路径加载失败或者系统 locale 不是 UTF-8。先单独验证分词器,能把「环境问题」和「代码问题」隔离开,这是血泪经验。
requirements.txt里如果没锁版本,装完先pip freeze > requirements.lock.txt存一份,后面换机器复现实验时你会感谢自己。深度学习那条线还要额外确认torch.cuda.is_available()返回 True,否则训练脚本默认跑在 CPU 上,一个 epoch 能跑一下午。
跑之前还有一步:把data/train.csv用文本编辑器打开看一眼前二十行,确认列名是text和label还是content和sentiment。不同来源的语料列名不统一,是新手第一个卡点,而报错信息往往只是KeyError: 'text',看不出所以然。
3. 数据处理流水线:把一条带表情的微博清洗成模型能吃的向量
情感分析的效果天花板,八成由数据决定,两成由模型决定。这一章按数据流动顺序走一遍:读进来、看清楚、洗干青、切开词、变成向量。
3.1 读数据:先看标签分布,别急着训练
任何一份标注语料拿到手,第一件事是统计标签分布和文本长度。正负样本严重失衡时,Accuracy 这个指标就废了——全预测成多数类也能有 0.7 的准确率,看起来很美,实际毫无用处。
import pandas as pd # encoding 先试 utf-8,报 UnicodeDecodeError 再换 gbk,Windows 导出的 CSV 常见 df = pd.read_csv("data/train.csv", encoding="utf-8") print(df.columns.tolist()) # 确认列名,不对就在下面改 print(df["label"].value_counts()) # 绝对数量 print(df["label"].value_counts(normalize=True)) # 比例,超过 7:3 就要考虑加权 # 文本长度分布:决定深度模型的 max_len 该设多少 df["char_len"] = df["text"].astype(str).str.len() print(df["char_len"].describe(percentiles=[0.5, 0.9, 0.99])) print(df["text"].isna().sum()) # 空值条数,清洗前必须知道describe那行的percentiles参数很关键。如果 90 分位是 60 个字、99 分位是 120 个字,那max_len设 128 就够了;设成 512 只会让显存白白翻四倍,padding 部分全是无效计算。isna().sum()也不能省,很多语料里存在文本为空但标签存在的脏行,直接进模型会在 embedding 层报索引越界。
看分布还有一层用途:确认标签体系。有的语料是 0/1 二分类,有的是 0/1/2 三分类(负面/中性/正面),甚至还有 0-4 的五级评分。二分类和三分类的损失函数、评估方式、阈值处理完全不同,这一步不确认清楚,后面所有指标都不可信。
注意:如果正负比例超过 8:2,训练时给分类器加
class_weight="balanced",或者对少数类做欠采样。别指望靠调阈值补救,那只是把问题往后推。
3.2 清洗:@、话题、链接和表情怎么处理
微博文本的噪声是结构性的,不是随机的。URL 对情感没贡献但要占长度;@某人是社交行为;#话题#里的词本身可能带情感倾向(比如#年度最佳服务#);[微笑]这种表情占位符携带的情绪信息量其实很高,删不删要看你后续模型能不能用上。
import re URL_RE = re.compile(r"https?://\S+|www\.\S+") AT_RE = re.compile(r"@[\w\u4e00-\u9fa5\-]{1,30}") TOPIC_RE = re.compile(r"#([^#]{1,40})#") # 话题保留词本身,去掉井号 EMOJI_TAG_RE = re.compile(r"\[[^\[\]]{1,8}\]") # [微笑] [泪] [doge] 这类占位符 REPEAT_RE = re.compile(r"(.)\1{2,}") # 哈哈哈 -> 哈哈,减少特征稀疏 SPACE_RE = re.compile(r"\s+") def clean(text: str) -> str: if not isinstance(text, str): return "" text = URL_RE.sub(" ", text) # 链接整段替换为空格,不留 http 残渣 text = AT_RE.sub(" ", text) # @昵称 对情感判断几乎无贡献 text = TOPIC_RE.sub(r"\1", text) # #服务态度差# -> 服务态度差 text = EMOJI_TAG_RE.sub(" ", text) # 想利用表情信息就改成映射表,见下方说明 text = REPEAT_RE.sub(r"\1\1", text) # 连续重复字符压缩,保留两连表强调 text = SPACE_RE.sub(" ", text) return text.strip()逐个说这几条规则为什么这么写。AT_RE里加了\u4e00-\u9fa5,是因为微博昵称常有中文,只写\w会匹配不完整,留下半截昵称污染文本。TOPIC_RE用r"\1"保留话题词,是因为话题词往往是整条微博的情感焦点,直接删掉会丢掉关键信号。REPEAT_RE压到两个而不是一个,「哈哈」和「哈哈哈哈哈哈」的强度差异还是有一点信息量的。
表情占位符的处理是最容易翻车的一处。全部删掉是最省事的做法,但「[泪]」「[怒]」这类本身是强情感符号。一个常见的折中做法是维护一张小映射表,把高频表情替换成对应的情感词,比如[泪]换成「难过」,[doge]换成「调侃」。这件事没有标准答案,但你在改造时至少要知道自己做了什么选择,而不是无意识地把信息删干净。
3.3 分词与词典:jieba 的默认行为在这类文本上会翻车
中文不像英文有天然空格,必须分词。jieba 的默认词典偏新闻语料,对社交文本里的新词、缩写、谐音处理得不好。工程里配套user_dict.txt就是为了补这块,把领域词加进去,让它们作为一个整体被切出来。
import jieba # 加载自定义词典:每行一个词,可跟词频和词性,如「破防 100 n」 jieba.load_userdict("data/user_dict.txt") # 停用词表:常见的是「的、了、是、在」这类高频虚词 STOPWORDS = set(open("data/stopwords.txt", encoding="utf-8").read().split()) # 情感词白名单:即使出现在停用词表里也强制保留 EMOTION_WORDS = set(open("data/emotion_words.txt", encoding="utf-8").read().split()) def cut(text: str) -> str: words = jieba.lcut(text) kept = [] for w in words: w = w.strip() if not w: continue if w in EMOTION_WORDS: # 情感词优先级最高,先放行 kept.append(w) continue if w in STOPWORDS: # 再去停用词 continue if len(w) == 1: # 单字噪声大,但情感单字要留 continue kept.append(w) return " ".join(kept)这段代码里最关键的不是jieba.lcut,是判断顺序。很多人的写法是先判断len(w) > 1再查停用词,结果「差」「烂」「赞」「坑」这些单字情感词在第一步就被干掉了。情感强度最高的词往往就是单字,删完再训模型,F1 上不去还找不到原因,这类问题是排查成本最高的。
cut返回的是空格拼接的字符串,这是为了直接喂给TfidfVectorizer。如果你走深度模型路线,这里应该返回list,后面再经过词表映射变成 id 序列。同一个函数两种输出格式,改造时记得对齐,别把分词结果和词表 id 搞混。
3.4 特征化:TF-IDF 与词表截断两条路
传统线用 TF-IDF 把文本变成稀疏向量。几个参数直接决定效果上限:max_features控制词表规模,min_df过滤掉只出现一两次的极低频词(多半是噪声或拼写错误),ngram_range=(1,2)让相邻两词的组合也成为特征。
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( token_pattern=r"(?u)\b\w+\b", # 已经用空格切好,按空白+词边界切 ngram_range=(1, 2), # 一元 + 二元,"不开心" 这类否定搭配能被抓住 max_features=100000, # 词表上限,太大显存/内存吃不住 min_df=2, # 出现少于 2 次的词直接丢,多数是噪声 sublinear_tf=True, # 用 1+log(tf) 抑制长文本的词频优势 )sublinear_tf=True这个参数经常被忽略。微博文本长度差异极大,一条 10 字的短评和一条 300 字的长文,词频量级差几十倍,不压缩的话长文本会主导整个特征空间。取对数后这个差距被拉平,短文本的情感信号才不会被淹没。
深度模型那条线不用 TF-IDF,走词表映射:统计训练集词频,取前 N 个词建vocab.json,<pad>固定为 0、<unk>固定为 1,然后把每条文本转成定长 id 序列,不足补 0、超出截断。词表大小一般取 2 万到 5 万,太小会大量落到<unk>,太大则 embedding 层参数暴涨且低频词学不到东西。
4. 模型训练:从朴素贝叶斯基线到 TextCNN 微调
数据洗完,模型层就是常规套路。这一章按「先基线后深度」的顺序走,两条线的代码都给出,重点是每一步的参数为什么这么设、结果不对时该看哪里。
4.1 传统基线:Pipeline 三行训出一个能交差的模型
sklearn 的Pipeline把特征化和分类器串成一条流水线,好处是训练和推理走同一套逻辑,不会出现「训练时用了一套清洗、推理时忘了」的经典事故。
from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # stratify 保证训练/验证集的正负比例一致,小数据集上非常关键 X_train, X_val, y_train, y_val = train_test_split( df["text_cut"], df["label"], test_size=0.2, random_state=42, stratify=df["label"] ) pipe = Pipeline([ ("tfidf", TfidfVectorizer( token_pattern=r"(?u)\b\w+\b", ngram_range=(1, 2), max_features=100000, min_df=2, sublinear_tf=True, )), # C 越大正则越弱,情感分析这种高维稀疏特征上 C 取 1~10 通常比较稳 ("clf", LogisticRegression(C=4.0, max_iter=1000, class_weight="balanced")), ]) pipe.fit(X_train, y_train) print(classification_report(y_val, pipe.predict(X_val), digits=4))stratify=df["label"]这行在样本不平衡时不能省。不分层的话,随机切分有可能让验证集里正样本只剩个位数,评估结果的方差大到没有参考价值。class_weight="balanced"则是让损失函数按类别频率反比加权,少数类的错误被放大,模型不会一味偏向多数类。
逻辑回归之外,线性 SVM(LinearSVC)在这类高维稀疏特征上往往表现相当甚至略好,缺点是它不输出概率,做置信度过滤时不如 LR 方便。朴素贝叶斯(MultinomialNB)训练最快,适合当「能不能跑通」的第一个验证,但效果一般会低几个点。我一般三个都跑一遍,取验证集 F1 最高的那个作为基线。
4.2 深度学习:TextCNN 的输入怎么对齐
TextCNN 结构简单、训练快、在小样本中文情感任务上性价比很高。核心思路是用不同宽度的卷积核去抓 2 到 4 个词的局部搭配,再对整句做最大池化,留下最强的那个响应。
import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim=128, num_classes=2, kernel_sizes=(2, 3, 4), num_filters=128, dropout=0.5): super().__init__() # padding_idx=0 让 <pad> 对应的向量不参与梯度更新,始终为 0 self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) # 三组卷积核宽度分别对应 2/3/4 个词的局部搭配 self.convs = nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, k) for k in kernel_sizes ]) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(num_filters * len(kernel_sizes), num_classes) def forward(self, x): # x: [batch, seq_len] -> [batch, seq_len, embed_dim] -> [batch, embed_dim, seq_len] x = self.embedding(x).transpose(1, 2) # 每个卷积核扫完整句后沿时间维取最大值,留下最显著的局部特征 x = [torch.relu(conv(x)).max(dim=2).values for conv in self.convs] x = self.dropout(torch.cat(x, dim=1)) return self.fc(x)transpose(1, 2)这步是新手最容易漏的。nn.Conv1d要求输入是[batch, channel, length],而 embedding 出来是[batch, length, embed_dim],不转置就会报维度不匹配,报错信息还相当难懂。padding_idx=0也别省,没有它的话 padding 位置会被当成真实词汇参与训练,短文本的表示会被大量无效向量稀释。
kernel_sizes=(2,3,4)是针对中文调的。中文两字词、三字词、四字成语占比最高,卷积核宽度和词长对齐能覆盖大部分局部搭配。如果你的语料里长短语更多,可以加到(2,3,4,5),代价是参数变多、训练变慢。
训练循环里另外三个参数值得盯:max_len决定截断长度,按第 3.1 节的 90 分位设就行;batch_size在显存允许范围内尽量大,32 或 64 是常见起点;学习率用 Adam 的话 1e-3 起步,配合ReduceLROnPlateau在验证集 F1 不涨时衰减。
4.3 评估:准确率骗人,F1 和混淆矩阵才看得到真相
样本不平衡时,accuracy是最没有信息量的指标。真正要看的是每一类的 precision/recall/F1,以及混淆矩阵里错分的方向——是正面被判成负面多,还是负面被判成正面多,这两种情况的业务含义完全不同。
from sklearn.metrics import confusion_matrix, f1_score y_pred = pipe.predict(X_val) print(confusion_matrix(y_val, y_pred)) # 看错分方向 print("macro F1:", f1_score(y_val, y_pred, average="macro")) print("weighted F1:", f1_score(y_val, y_pred, average="weighted"))macro是对每类 F1 做算术平均,少数类的表现和多数类权重相同,能暴露「多数类很好、少数类很烂」的问题。weighted是按类别样本数加权,接近整体感受。两个都打出来对比,如果 macro 明显低于 weighted,说明模型在少数类上塌了,回去加class_weight或者补数据。
混淆矩阵还要看具体数字。如果负面类被大量判成正面,先别急着调模型,回去翻这些样本的原文——大概率是反讽、双重否定,或者你第 3.3 节把关键情感词切碎了。模型只是把数据问题忠实地反映出来,它不会替你修数据。
4.4 推理封装:把模型收成一个 predict(text) 函数
训练完把模型joblib.dump落盘,推理侧封装成一个类,内部串起「清洗 → 分词 → 预测」三步。这一步做得好不好,决定了这份代码是「一个实验脚本」还是「一个能被人调用的模块」。
import joblib from preprocess.clean import clean from preprocess.tokenizer import cut class SentimentPredictor: def __init__(self, model_path="outputs/tfidf_lr.pkl"): self.pipe = joblib.load(model_path) # 加载时确保特征化+分类器是同一个 Pipeline def predict(self, text: str) -> dict: cleaned = cut(clean(text)) if not cleaned: # 清洗后为空说明整条都是噪声,直接兜底,别让模型去猜 return {"label": "unknown", "score": 0.0} proba = self.pipe.predict_proba([cleaned])[0] idx = int(proba.argmax()) return { "label": str(self.pipe.classes_[idx]), "score": round(float(proba[idx]), 4), }两个细节:self.pipe.classes_而不是硬编码 0/1,这样换标签体系时不用改推理代码;清洗后为空时返回unknown而不是让模型硬猜,能避免大量噪声文本被赋予高置信度的错误标签——这种错误在业务侧最难发现。
深度模型的推理封装同构,只是中间多一步「文本转 id 序列」,并且要确保推理时的词表和训练时是同一份vocab.json。词表不一致是深度线上最隐蔽的 bug:不报错,不崩溃,指标就是低,查起来像黑匣子。
5. 踩坑排查:五类让脚本跑不起来或指标虚高的典型故障
这一章是我自己复现这类工程时攒下来的记录,每条按「现象 → 原因 → 解决」写,对着排查能省不少时间。
5.1 环境与文件路径类
第一条:FileNotFoundError: data/stopwords.txt
现象是训练脚本刚启动就退出,报找不到停用词表。原因通常有两个:一是脚本用了相对路径,而你是在上层目录执行python models/train_ml.py,工作目录不是项目根;二是压缩包解压后多套了一层同名目录。解决办法是把路径统一收敛到utils/config.py,用Path(__file__).resolve().parent.parent / "data"拼绝对路径,跟启动目录解耦。
第二条:UnicodeDecodeError: 'utf-8' codec can't decode byte ...
现象是读 CSV 时直接崩。原因是文件本身不是 UTF-8,Windows 上导出的表格默认是 GBK 或 GB18030。解决方式是写个小函数按顺序试探编码:先utf-8-sig(能吃掉 BOM 头),失败再gbk,还失败就gb18030。别手动一个个试,浪费时间还容易改错文件。
5.2 数据与标签类
第三条:验证集准确率 0.95,换一批真实数据直接崩
现象是离线指标漂亮得不像话,上线后效果惨淡。八成是数据泄漏:同一段文案在转发链里出现多次,随机切分把它同时放进了训练集和验证集,模型等于在背答案。解决办法是先按文本内容去重再切分,条件允许的话按用户或按时间做切分,让验证集和训练集在来源上真正隔开。
第四条:模型对「服务太差了」判成正面
现象很具体,明显的负面句被判成正类。回去看分词结果,大概率是「差」这个单字被len(w) > 1那条规则过滤掉了,或者它被误收进了停用词表。解决办法是维护emotion_words.txt白名单,在分词函数里给情感词最高优先级,先放行再判断其他规则。这个改动通常能立竿见影地涨一两个点。
5.3 训练与指标类
第五条:深度模型 loss 不动,预测全是同一个类别
现象是训练几个 epoch 后 loss 几乎不变,推理输出全部是 0。常见原因有三个:标签没有做stratify,训练集里某个类别样本极少;学习率太大导致直接发散;或者 embedding 的padding_idx没设,padding 噪声淹没了信号。排查顺序是先打value_counts确认标签分布,再把学习率降一个数量级试,最后检查模型定义。
提示:遇到「指标异常好」和「指标异常差」两种情况,先怀疑数据处理,再怀疑模型。经验上八成的问题在数据侧。
6. 二次开发:把一次性的实验代码改成能持续迭代的服务
跑通之后真正的活儿才开始。这类工程最常见的改造方向有三个,我按投入产出比排一下:换领域数据、补增量闭环、做多类别细化。
换领域是最直接的。把微博语料换成电商评论或客服对话,模型结构可以一行不改,但三处必须动:user_dict.txt补领域词(比如「发货慢」「客服敷衍」这种固定搭配),clean.py里的正则按新数据源的噪声特点重写,max_len按新语料的长度分布重设。我见过有人直接拿微博模型去判电商评论,F1 掉了十几个点,回头才发现新数据里全是「宝贝」「亲」这种在微博语料里从未出现的词。
| 改造点 | 影响面 | 验证方式 |
|---|---|---|
| 自定义词典 | 分词质量 | 抽 100 条看切分结果 |
| 清洗规则 | 特征有效性 | 对比改造前后同模型 F1 |
| 标签体系(二分类→三分类) | 损失函数 + 评估 | 混淆矩阵看中性类表现 |
| 词表与 max_len | 深度模型输入 | 统计<unk>占比,超过 5% 要扩词表 |
增量闭环是第二个方向,也是最值钱的。在predict里把score低于阈值(比如 0.65)的样本单独落一张表,定期人工复核后回流到训练集,两周迭代一次。这套机制搭起来之后,模型会随着业务数据自己变好,比你手工调参有效得多。落表逻辑建议直接写在predict.py的返回分支里,别另起一个服务,越简单越不会断。
第三个方向是多类别细化。二分类只能告诉你正负,业务方往往想知道「物流」「质量」「服务」分别是什么情绪。做法是在现有流水线上加一层属性分类,或者把标签体系改成「属性-极性」的组合标签。这一步的坑在于标注成本会翻几倍,先拿几百条做小样本验证,确认业务方真的需要再铺开。
验证方法上,我习惯准备一个只有两三百条的小型 smoke set,每条都是人工确认过标签的硬样本,包括反讽、双重否定、纯表情、超短文本。任何一次改造之后先跑这个集合,掉点就回滚。这个习惯是被坑出来的——有一次改完清洗规则,整体 F1 涨了 0.8 个点,结果反讽类的召回从 0.6 掉到 0.2,幸好上线前在 smoke set 上过了一遍。
从那以后我每次动清洗规则或者换词表,都强制先跑一遍这个小集合再谈其他指标,看着慢,实际省下来的返工时间多得多。这份代码包的目录结构和脚本划分已经把该留的口子都留出来了,按上面的顺序改,不太容易走偏。希望帮到你。
本文还有配套的精品资源,点击获取