
做机器学习项目做到第四个我越发觉得很多教程把精力都花在模型结构上却忽略了真正决定项目成败的环节——数据怎么来、标签怎么定、特征怎么做、评估怎么看。这个“疫情期间网民情绪识别”项目恰好能把这几块完整地串一遍而且素材非常好找2020年前后疫情相关的公开评论、微博文本、新闻跟帖量大、情绪强烈、类别典型用来练手情感分析再合适不过。这篇是系列第四篇的前半部分我会把重点放在“从零到一搭起一条可用的情绪分类流水线”上包括任务定义、数据方案、中文文本预处理、特征提取以及传统机器学习模型的基线效果。适合刚学完Python和sklearn基础、想完整走一遍NLP分类流程的读者也适合正在为课程设计或竞赛找实战题目的同学。看完你至少能回答三个问题情绪识别到底在识别什么中文文本怎么做成特征一套基线模型到底该跑到什么程度才算合格。1. 项目背景与整体技术路线1.1 为什么选“疫情期间网民情绪”这个切入点从技术角度看情绪识别Sentiment Analysis / Emotion Classification是文本分类里最经典的任务之一。它的输入是一段文本输出是这段文本携带的情绪类别比如正向、负向、中性或者更细的愤怒、恐惧、悲伤、高兴等。疫情期间的网民评论天然具备几个优势话题集中大家都围绕疫情相关的事情发言情绪强度高恐惧、焦虑、感动、愤怒都有大量真实样本文本长度适中微博和评论区的表达比长文更适合做短文本分类。从学习角度看这个任务覆盖了NLP项目最常见的完整链路采集数据、设计标注规范、文本清洗、分词、特征工程、模型训练、指标评估。任何一个环节偷懒最终结果都会诚实地反馈在指标上。比如分词词表选得不好模型看到的特征就是一堆碎块标签体系定义得含糊评估时就会出现“模型觉得是正向、人工觉得是负向”的扯皮。这些坑只有亲手做一遍才会真正记住。还有一个现实原因这个项目不需要花一分钱买数据。网上有大量公开的疫情评论语料也可以自己写爬虫去社交平台抓公开文本再配合人工标注就能得到一份几百到几千条的小规模数据集。对小项目来说质量可控的人工标注往往比盲目堆几百万条噪声数据更管用。1.2 三条技术路线的对比与取舍情感分析做到今天大致有三条路线。第一条是词典法维护一张带情感极性的词表统计文本里正负情感词的命中数量谁多就判给谁。优点是简单、可解释缺点是对复杂表达无能为力“绝了”这种词在不同上下文里完全相反。第二条是传统机器学习把文本转成向量再用逻辑回归、朴素贝叶斯、SVM等算法做分类。这是当前项目最合适的起点技术栈要求低训练快结果稳定而且能从特征权重里看出模型到底学了什么。第三条是深度学习从Word2Vec、TextCNN到BERT、大语言模型微调效果通常更好但代价是环境配置、显存、训练时间和调参难度成倍上升。我在这类入门项目里的建议很直接先用传统机器学习跑通基线拿到一套能用的指标和错误分析样本再决定要不要往深度学习升级。直接上手BERT的人经常遇到一个问题模型换了好几个效果却差不多回头一看是数据集本身标注噪声太大——这属于用高射炮打蚊子还打偏了。1.3 本篇的边界与目标标题里有个“一”说明这个项目本身是分阶段的。我给它划的边界是这样的第一阶段本篇完成数据采集方案、标注规范、文本预处理、TF-IDF特征以及逻辑回归和朴素贝叶斯的基线模型目标是得到一个F1值在0.7以上的可跑通分类器。第二阶段可以做Word2Vec特征、深度学习模型和错误分析把指标往上推。为什么要把“一”的终点定在传统机器学习基线上因为所有后续工作——不管是调参、加特征还是换模型——都需要一个对照物。没有基线你根本无法判断BERT带来的提升到底是模型的功劳还是数据划分方式变了导致的假象。先跑通一个简单的、解释清楚的模型是整个项目往后走的锚点。2. 数据获取与标注方案设计2.1 数据来源与清洗思路疫情期间的情绪语料常见来源有三类微博公开评论、新闻客户端评论区、知乎等社区的回答与评论。对新手来说最简单的是直接用公开的疫情评论数据集比如搜“疫情 微博 情感 数据集”能找到不少整理好的CSV文件。更稳妥的做法是自己爬取公开页面但要注意频率不要过高并且只保留公开信息避免涉及个人隐私。我用的是混搭方案先用公开数据集里的3000条做主训练数据再自己补爬了500条近期的评论做补充验证。清洗这一步容易被低估。原始文本里有大量与情绪无关的噪声URL链接、用户名、转发标记“转发微博”、表情符号、乱码字符等。我习惯的处理顺序是先统一全半角再用正则去掉URL和用户接着过滤掉长度小于两个字的文本最后去重。这里有个细节表情符号不要急着全删。比如“哈哈”和“哈哈哈哈哈”表达的情绪浓度不同但对分类模型来说重复的字本身就是一种特征粗暴删除可能丢失信息。清洗的具体代码用一段Python就能完成核心是这样import re def clean_text(text: str) - str: # 统一全半角 text text.replace(\u3000, ) # 去URL text re.sub(rhttp\S, , text) # 去用户 text re.sub(r\w, , text) # 去多余空白 text re.sub(r\s, , text).strip() return text2.2 标注体系三分类还是五分类标注体系的设定直接影响后续所有工作这是整个项目里最需要想清楚的一步。常见选择有两种三分类正向、负向、中性和五分类愤怒、恐惧、悲伤、高兴、中性。三分类简单、标注一致率高、模型好训练适合第一版跑通五分类信息更丰富但标注成本成倍上升不同人对“愤怒”和“悲伤”的边界本身就存在主观分歧。我做这个项目时选了三分类原因很现实项目初期最需要的是“能对齐的标准”。如果连标注的人都经常吵架模型学到的东西必然是一团浆糊。三分类也不是没有技巧关键要把判定规则写到标注手册里例如“包含明显负面情绪词且无反转语气”判负向“包含正面词但存在反讽如‘真是谢谢了’”需要单独标注人员协商后再定。这个步骤经常被忽略但我想强调标注规范不是一个文档问题而是数据质量问题。我踩过的坑是刚开始没有写规范三个人各标各的最后一致性检验Kappa系数只有0.4数据基本废掉。后来重新定义了规则做了一轮预标注和讨论才把一致性拉到0.7以上。2.3 构造带标签数据集的实际操作有了规范就可以进行批量标注。小规模数据集推荐用最简单的办法Excel或在线表格一条文本一行后面加一列“标签”标注人员在正向、负向、中性里选。3000条文本三个人分工一人1000条再互相抽100条做交叉验证大概两天能完成。这里推荐一个小技巧标注时把文本随机打乱不要让标注人员连续看到同一个话题的文本否则容易产生“锚定效应”后面几条会不自觉地参照前面几条的判断。另外每条数据最好有第三列“是否确定”如果标注人员对某条特别没把握就标记为不确定后续讨论或者直接丢弃不要硬着头皮给标签。数据准备好之后用pandas读进来大致长这样import pandas as pd df pd.read_csv(covid_sentiment.csv) print(df.head()) # 输出示例: # text label # 0 武汉加油医护人员辛苦了 正向 # 1 口罩又买不到了感觉好焦虑 负向 # 2 今天新增病例有所下降 中性最终我用的是2830条有效数据其中正向870条、负向1140条、中性820条。类别不太均衡负向略多这是由当时的舆论环境决定的——人在有压力的时候更愿意表达负面情绪这在后续处理时需要留意。3. 文本预处理与特征工程3.1 中文分词的坑与选型中文NLP的第一道坎就是分词。英文单词天然有空格分隔中文没有所以要把句子切成有意义的词。我用的是结巴分词原因很务实免费、开箱即用、社区活跃对短文本的切分效果足以支撑这个项目。结巴分词有精确模式、全模式和搜索引擎模式做分类特征时用精确模式就够了。关键参数是用户词典疫情相关文本里有一堆专有名词“卫健委”“火神山”“核酸检测”“无症状感染者”如果这些词被拆成单字或错误组合特征质量会明显下降。解决方法是把高频领域词加入用户词典import jieba user_words [卫健委, 火神山, 雷神山, 核酸检测, 无症状感染者, 健康码, 方舱医院] for w in user_words: jieba.add_word(w)分词之后还有一个容易被忽略的步骤去停用词。但停用词表不能无脑套通用版。比如“不是”里的“不”如果被当成停用词删掉句子的情绪就完全变了“太棒了”的“太”虽然是个副词但程度词对情绪判断有用。我的做法是只用通用停用词表里的纯虚词部分比如“的”“了”“吗”“呢”同时保留否定词和程度词。3.2 TF-IDF特征与Word2Vec特征怎么选文本转成数字特征传统机器学习里最常用的是词袋模型和TF-IDF。词袋模型统计词频简单但有个问题常见词比如“疫情”“感染”几乎出现在每条文本里对区分情绪毫无帮助却占据了大量维度。TF-IDF的改进在于它会降低常见词的权重提高某些文档里频繁出现但整体罕见的词的权重比如“恐慌”“致敬”这些词对分类的判别力更强。用sklearn的TfidfVectorizer可以很方便地完成需要注意几个参数from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( max_features5000, # 控制特征维度防止维度爆炸 ngram_range(1, 2), # 看单个词也看相邻双词组合比如“不 害怕” min_df2, # 至少在2条文本中出现过滤只出现一次的词 max_df0.8 # 在80%以上的文本都出现的词大概率是停用词级别的噪声 ) X vectorizer.fit_transform(df[text])Word2Vec的思路不同它先把每个词映射到一个稠密向量再对句子里的所有词向量做平均得到一个句向量。理论上它比TF-IDF更能捕捉语义相似性比如“害怕”和“恐惧”在向量空间里距离很近但TF-IDF会把它们当成两个完全独立的维度。不过我在这类小数据集上实测Word2Vec平均池化带来的提升往往有限而且调参更麻烦所以第一版建议直接用TF-IDF简单高效后面再考虑替换。3.3 从原始文本到特征矩阵的完整代码把前面的步骤串起来整体流程是这样# 完整预处理特征提取流程 def build_features(df): # 1. 清洗 df[clean_text] df[text].apply(clean_text) # 2. 分词并用空格连接 df[seg_text] df[clean_text].apply( lambda x: .join(jieba.cut(x)) ) # 3. 构建TF-IDF特征 vectorizer TfidfVectorizer( max_features5000, ngram_range(1, 2), min_df2, max_df0.8 ) X vectorizer.fit_transform(df[seg_text]) y df[label].map({正向: 0, 中性: 1, 负向: 2}) return X, y, vectorizer这里有一个非常关键的细节fit_transform只能用在训练集上验证集和测试集必须用同一个已经训练好的vectorizer做transform不能重新fit。原因很简单如果测试集参与特征构建信息就从“未来”泄漏到了“过去”评估结果会虚高。我在项目里见过太多人在这里翻车用同一个变量名覆盖了vectorizer导致后续所有结果都不可信。一个补充心得做完特征之后一定打印一下特征的形状和几个典型特征词确认它符合预期print(X.shape) # (2830, 5000) feature_names vectorizer.get_feature_names_out() print(feature_names[:20]) # [不可, 不好, 不足, 严重, 中国, 为主, 乐观, 人们, 令人, 付出, ...]看到特征里有“不可”“不好”“严重”这种负面词也有“中国”“人们”这种偏客观叙述的词说明特征构建基本正常。如果看到的是一堆无关字符或单字那就要回头检查清洗和分词步骤。4. 基线模型训练与评估4.1 逻辑回归与朴素贝叶斯的角色分工传统机器学习里做文本分类绕不开逻辑回归和朴素贝叶斯。这俩一个偏判别式一个偏生成式各有各的性格。朴素贝叶斯假设特征之间相互独立这在文本场景里显然不成立——因为“肺炎”和“确诊”经常同时出现并不独立。但这个假设在文本分类里意外地好用尤其在小数据集上它能很快收敛到不错的基准效果。逻辑回归则通过梯度下降优化损失函数能学习到特征之间的权重组合对特征重叠、共线性的鲁棒性更好而且能直接输出每个特征词对分类结果的贡献方向。如果你只打算跑一个模型我推荐逻辑回归。原因有三训练速度快几百条数据几秒钟就能收敛可解释性好feature_importance可以直接拿来做错误分析在短文本分类上通常比朴素贝叶斯稳定。但建议两个都跑一遍互为对照时间成本极低。4.2 训练/验证集划分与交叉验证划分数据集的标准做法是训练集、验证集、测试集三份。训练集用来学参数验证集用来调超参和早停测试集在最后评估时再碰。小项目里常用7:1.5:1.5或者8:1:1但一定要加上stratify参数按标签比例做分层采样防止某个类别在验证集中消失。我习惯先切出测试集再对剩余数据做交叉验证from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 对训练集做5折交叉验证评估模型稳定性 from sklearn.model_selection import cross_val_score from sklearn.linear_model import LogisticRegression lr LogisticRegression(max_iter1000) scores cross_val_score(lr, X_train, y_train, cv5, scoringf1_macro) print(scores.mean(), scores.std()) # 输出示例: 0.763 0.014为什么用交叉验证而不是单次划分因为单次划分受随机种子影响太大可能这次切得巧F1到0.79下次切得差直接掉到0.71你根本不知道是模型问题还是数据划分问题。交叉验证用多轮划分的平均值说话更稳定。4.3 评估指标准确率之外要看什么情感分类任务里最坑的陷阱是只看准确率。如果数据分布是负向占40%、正向占31%、中性占29%那么一个“全都预测负向”的垃圾模型也能拿到40%的准确率看起来好像“还行”实际上一点用都没有。我在这类多分类任务里主要看三个指标宏平均精确率、宏平均召回率、宏平均F1。宏平均的意思是对每个类别分别计算指标再取平均这样少数类不会被多数类淹没。sklearn里一行就能输出from sklearn.metrics import classification_report lr.fit(X_train, y_train) y_pred lr.predict(X_test) print(classification_report(y_test, y_pred, target_names[正向, 中性, 负向])) # 输出示例: # precision recall f1-score support # 正向 0.76 0.72 0.74 174 # 中性 0.65 0.60 0.62 164 # 负向 0.79 0.84 0.81 228 # macro avg 0.73 0.72 0.72 566F1值0.72左右作为第一版基线我认为是合格的。但细看就会发现中性类的精确率和召回率都明显低于另外两类这说明模型倾向于把中性文本误判为正负向——这是短文本情绪识别里非常典型的问题因为人在表达中性情绪时用词确实更模糊。这个信息比只看一个总分价值大得多它直接告诉你下一步优化应该往哪个方向发力。5. 常见问题与排查技巧实录5.1 典型问题速查表我把这个项目里实际遇到的高频问题整理成了一个表基本覆盖了新手会踩的大多数坑。现象可能原因解决方案模型预测结果全是同一个类别数据标注不均衡或特征提取时fit_transform跑错了对象检查类别分布确保用分层划分确认特征只在训练集上fit分词结果里专有名词被切碎用户词典没生效或jieba版本过旧手动添加领域词检查add_word是否在切分前调用训练和验证时间加起来不到10秒但准确率很低特征维度太少或文本本身噪声太大调整max_features、ngram_range先打印特征词看合理性中性的F1明显低于正负向中性文本本身语义模糊样本量不足补充中性样本或者考虑优化标注规范把“中性”定义为“没有明显情绪倾向”换随机种子后指标波动大于0.05数据量太小或测试集划分不合理使用交叉验证取平均值或增加数据量预测时直接报错说特征数量不匹配测试集的vectorizer重新fit了必须复用训练时的vectorizer做transform5.2 断言的隐性问题模型“学偏了”印象最深的一次是模型在训练集上F1接近0.9测试集却只有0.6明显过拟合。我逐个检查特征权重发现排名靠前的特征里居然有“网课”“开学”“封校”这类词——模型学到的不再是情绪本身而是把“提到某个特定话题”直接映射成了某种情绪标签。这在NLP任务里叫“伪相关”或“数据集偏置”。解决办法有两个方向。一是做特征筛选去掉那些和任务无关的话题词但这在文本分类里很难彻底实现因为话题词和情绪词的边界很模糊。二是增加数据多样性引入不同时间、不同平台的文本让模型不能只靠“提到某词”来判断。对这个项目来说更实际的做法是把数据按发布时间分桶保证训练集和测试集来自不同时间段这样能验证模型是否具备定标外的泛化能力而不只是记住某个时期的话题。5.3 对中文文本分类的几条独家心得做完整套流程有几条经验我想单独拿出来说因为它们不看一次错误分析是不会写在教科书里的。第一保存向量化器。训练完模型后一定要把vectorizer和训练好的模型通过joblib也单独存一份后面做预测接口时直接加载否则你写了一个predict函数结果发现新的输入无法转换成向量卡在特征数量不匹配上。第二用bigram而不是单纯unigram做短文本分类。疫情期间的评论里“不 严重”和“不 乐观”是双词组合单独看“严重”和“乐观”都是负向词但加上“不”之后整个句子的极性完全反转。ngram_range(1,2)本质上就是让模型能看到这种反转信号这个参数在这个项目里的收益非常明显。第三别迷信“更多数据一定更好”。我的第一版数据集有5000条但里面混了大量广告和无意义文本清洗完之后只剩2800多条反而F1提升了0.05。数据量增加但质量下降的时候模型学到的是噪声不是模式。这年头做NLP项目花时间看原始文本、抽100条做人工复核比盲目加数据重要得多。第四关于代码环境的坑也提一句。头歌这类在线平台上跑机器学习作业最容易遇到的问题是中文显示乱码和分词库未安装。我的建议是写代码前先print一下当前环境的jieba版本和默认编码不要等到特征矩阵建好才发现分词根本没用上。这个项目做到这一步已经有一份能跑通、可解释、指标讲得清楚的情绪分类基线了。下一步再想提升可以从两个方向入手一是把TF-IDF换成Word2Vec或BERT的句向量二是对误分类样本做一轮细致的错误分析找到模型的系统性盲区。我自己的习惯是先做错误分析再做模型升级因为很多情况下数据集本身的问题比模型能力更值得优先解决。