
简介面向自然语言处理与舆情分析学习者这份资源提供了一套完整的基于机器学习的中文情感两极化分析方案聚焦人民日报和微博等渠道的疫情相关话题数据。项目系统对比了情感词典与机器学习两类主流方法重点分析了中文语料情感词典的选取与扩充方式并强调训练语料质量对机器学习模型性能的关键影响适合有Python基础、希望深入理解情感分析流程的学生、研究人员或开发者。资源包共2000个文件总大小约87MB主体包括毕业论文文档、Python项目源码以及大量txt、json、csv等格式的原始文本与中间结果数据同时含有pkl模型文件、html关键词页面、png可视化图表等能够完整还原从数据采集、预处理、特征构建到模型训练与结果展示的工程链路。目前已有433人学习下载可用作课程设计或毕业论文的参考实例也可为其他领域的中文情感分析任务提供可复用实验基础。1. 两极情感分析先把“两极”这两个字掰清楚看到“两极情感分析”很容易理解成“舆论两极化程度检测”也就是算人群态度分裂成对立阵营的强度。但做过文本情感分析的从业者第一反应基本都是另一个意思:把语料分到正、负两个极性类别里做二分类。我拆这个压缩包里的方案时按的是后者:人民日报和微博中与疫情有关的已采集文本通过机器学习训练二分类模型把每条文本判定为正面或负面情绪。人民日报的规范正文和微博的碎片化短评在句式、话题分布、表情符号使用上差异非常大恰好是练习跨来源短文本舆情情感倾向分析的理想场景。就算你真正想要的其实是“群体态度两极分化”的度量把标签从正负两类换成强度五级本文的特征和模型部分照样能用只是评价口径不同。这套流程适合正在做舆情系统设计、写毕业设计或是想从零跑通一个完整机器学习实战项目的读者不需要上大模型Python 常见包就能复现。2. 拿到压缩包先别急着跑模型数据结构与清洗2.1 先看清包里到底有什么目录结构与数据格式这类标题带.zip的资源最常见的交付形式是“数据 代码 说明文档”三件套。我一般拿到压缩包会先建一个新目录把 zip 解压进去然后立刻执行一条tree命令看整体结构而不是双击打开在图形界面里一头扎进去。过程并不复杂但会直接决定后续代码里路径怎么设计、数据集按什么方式读。在项目根目录下执行:mkdir pandemic-sentiment cd pandemic-sentiment unzip ../基于机器学习的人民日报和微博等与疫情有关话题数据两极情感分析.zip -d . find . -maxdepth 3 -type f | sort参数说明-d .表示解压到当前目录find的-maxdepth 3限制只看到第三层目录避免把模型文件、缓存全部刷出来。正常情况下你会看到原始数据文件、代码脚本、分词词典和一份 Markdown 或 PDF 说明文档。如果压缩包里有嵌套的同名文件夹tree结构会很难看建议解压后立刻mv把内层文件夹提升到根目录保持单一规范路径。之后用 pandas 快速读一遍每个 csv 的行数和前几行确认字段名是中文还是英文。这个动作很关键很多源码包里的字段名是拼音缩写比如qlsj代表“评论时间”不对一眼根本猜不出来。import pandas as pd df_people pd.read_csv(data/people_daily.csv, encodingutf-8) df_weibo pd.read_csv(data/weibo.csv, encodingutf-8) print(df_people.shape, df_people.columns.tolist()) print(df_weibo.shape, df_weibo.columns.tolist())逻辑说明这里分开读两个数据源不做预拼接目的是先看每一份的字段设计是否一致。人民日报来源大概率是“日期、版次、标题、正文”微博来源大概率是“发布时间、用户昵称、微博正文、转发数、评论数、点赞数”。参数说明encodingutf-8是最常见的编码但疫情早期一些爬虫脚本在 Windows 下写文件落盘成了gbk读取会直接报UnicodeDecodeError。遇到这种情况把编码换成encodinggbk再试或者用encodingutf-8, errorsignore先忽略坏字符应急。真正决定后续情感分析质量的不是模型而是这份数据里本地文本字段是否完整、时间是否连续。2.2 人民日报正文和微博短文本的处理差异两个数据源必须在清洗阶段就分开关照不能丢进同一个函数里批量处理。人民日报是长文本一段新闻正文动辄几百上千字里面会出现“记者 XX 报道”“新华社电”这类固定的新闻套话微博单条一般不超过 140 字口语缩略、网络新词、表情符号占很大比例。两者拼在一起做特征工程如果不做针对性的文本规约模型很可能会学到“来源”本身的差异而不是情感极性。先做一个基础清洗层把两个来源公用的脏数据操作提出来:import re def clean_text(text: str) - str: if not isinstance(text, str): return # 去掉 URL、 用户、话题 #xx# 里面的 # 号 text re.sub(rhttp\S|www\.\S, , text) text re.sub(r[\w\u4e00-\u9fa5_\-], , text) # 去掉多余空白和不可见字符 text re.sub(r\s, , text) return text.strip()逻辑说明http\S抓 URL 会把 ? 和感叹号一并吞掉所以要先于标点处理后面的用户昵称对情感极性没有贡献直接删除话题标签里的#号会干扰后续分词只去掉井号保留话题词因为“#武汉加油#”本身就是一个强烈的正向信号。微博正文里常见的“分享图片”这种系统后缀也要在数据预处理时过滤否则会产生大量重复无意义样本。人民日报侧的清洗重点是去掉固定结构比如“新华社北京电”“编辑:XXX”这类落款:def clean_people_daily(text: str) - str: text clean_text(text) text re.sub(r[^]*电, , text) text re.sub(r(记者|通讯员)[\u4e00-\u9fa5]{2,3}, , text) text re.sub(r责编[:].*$, , text) return text参数说明[^]*电匹配“新华社北京 6 月 5 日电”这类括号结尾带“电”的新闻电头注意括号必须用中文全角如果数据里是英文括号要同步加一条替换规则。清洗规则宁多勿少因为这个阶段的错误是累加的几个脏词混进语料里后续 TF-IDF 特征就会多出几个高频噪音词模型解释性直接被拖垮。2.3 疫情话题过滤别把无关新闻带进训练集压缩包里给的数据不一定全是疫情文本很多时候采集脚本是按关键词表在涉疫账号或话题页里采集的其中会混入少量非疫情内容。我一般会在清洗后加一道话题过滤器用一组正向关键词做召回再用一组负向关键词做剔除。pos_keywords [新冠, 肺炎, 疫情, 确诊, 隔离, 疫苗, 核酸, 方舱, 防控, 病例] neg_keywords [世界杯, 奥运会, 股市, NBA, 明星, 电影] def is_pandemic_related(text: str) - bool: hit_positive sum(1 for kw in pos_keywords if kw in text) hit_negative sum(1 for kw in neg_keywords if kw in text) return hit_positive 1 and hit_negative 0逻辑说明正向关键词命中一条就算相关负向关键词只要命中就剔除。这里故意把“疫苗接种”和“股市”都保留因为涉疫新闻里会出现“受疫情影响股市波动”“疫苗接种有序推进”这样的样本负向关键词里的“股市”是为了剔除纯粹的财经新闻而不是剔除涉疫财经新闻。如果出现“股市”且“疫情”同时出现hit_negative 0会把这条样本排除掉属于误杀实际项目中我不建议把两个列表设成完全互斥。更好的做法是把负向规则做成“交集判断”:只有当负向词出现且正向词没出现时才剔除。这个细节看似不起眼但直接决定最终数据集规模。疫情初期的语料里“延期”这个词既出现在“考试延期”也出现在“疫情导致复工延迟”靠单一关键词判断很容易翻车统计完分布后人工抽检一下最稳妥。清洗完成后按时间字段排个序看一眼时间跨度是否连续。疫情话题数据经常出现一个月密集、下个月断档的情况。时间空洞会影响后续第 6 章要做的时间轴演化分析所以建议在清洗阶段就把缺失月份记录输出成一个清单方便后面对齐补采。3. 情感标注与特征工程人工先标 500 条再谈模型3.1 标注方案用情感词典还是用人工标注很多入门项目会直接用 SnowNLP 或者大连理工情感本体库的词典给每条文本打分分数大于阈值标正、小于阈值标负。这种做法速度极快但有两个问题:一是词典覆盖不了疫情语料的领域专有词“囤菜”“试剂盒”“流调”这些词在通用情感词典里多数是中性词二是词典打分的输入是词粒度句子里的否定和程度副词组合会产生“不是很满意”这种极性反转词典方法基本无能为力。所以实操中的常见做法是“先小批量人工标注再训分类器”。我一般会从清洗后的语料里按天分层抽样 500 条让两个人分别标正负第三个人仲裁不一致的样本。注意不要在原始数据的 csv 里直接改单独维护一份标注文件原始语料保持只读。import random random.seed(42) sample_days df[publish_date].drop_duplicates().sort_values() per_day max(1, 500 // len(sample_days)) sample_idx df.groupby(publish_date, group_keysFalse).apply( lambda x: x.sample(min(per_day, len(x)), random_state42) ).index df.loc[sample_idx, is_selected] 1 df[df[is_selected] 1][[text, publish_date]].to_csv(annotate/sample.csv, indexFalse)逻辑说明按天分层抽样是为了防止标注样本全挤在某个新闻热点期间导致模型学到的只是那个事件的情绪而不是通用的情感表达。连续两周每天抽 20~30 条得到的标注集比随机抽 500 条更能代表真实分布。group_keysFalse是为了避免apply产生多余的日期分组列。参数说明random_state42只是固定随机种子保证第二次运行产出同样的抽样索引。如果你发现 500 条里正负比例严重失衡比如正样本占了 85%不要急着做欠采样先想想采集策略是不是偏了——疫情早期新闻集中在“防控部署”“物资保障”上正面率偏高是数据本身的特性不是你标注错了。人工标注阶段最常见的坑是不一致率超过 15%这时候直接训模型就是在垃圾标签上拟合先把不一致的样本拉出来讨论统一口径才是正事。3.2 中文分词与 TF-IDF 特征从词频到向量标注完成后的标准流程就是分词加 TF-IDF 向量化。中文文本不像英文天然有空格分隔必须先分词。这里选 jieba 而不是更重的 LAC 或 HanLP主要原因是压缩包面向的是复现场景jieba 安装最简单、词典可扩展跑出来的特征也足够支撑传统机器学习模型。对疫情语料我会在 jieba 里添加两个自定义组成部分领域词典和停用词。领域词典保证“新冠疫苗”“无症状感染者”“流调”这类词不被切开停用词表则要把“我们”“他们”“进行了”这种无情绪词过滤掉。import jieba df[seg] df[text].apply(lambda x: .join(jieba.cut(str(x), cut_allFalse))) from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( max_features5000, ngram_range(1, 2), min_df5, max_df0.8, sublinear_tfTrue ) X vectorizer.fit_transform(df[seg]) y df[label]参数说明ngram_range(1, 2)同时保留单词和相邻双词这样“不是/很好”会被切成“不是”“很好”“不是 很好”三个特征模型有机会捕捉否定结构min_df5过滤掉只在五六条样本里出现过的低频词这些词对分类没有统计意义max_df0.8过滤掉在 80% 以上样本都出现的词防止“疫情”这种高频背景词成为压倒性特征。sublinear_tfTrue是 sklearn 里很值得开的参数它用1 log(tf)代替原始词频削弱长文本的词频优势——人民日报长文里的词频天然比微博短文本大不开这个参数模型容易被来源特征带偏。维度从几千词压缩到 5000 维X是稀疏矩阵不要转成 numpy 数组否则内存直接爆掉。后续模型训练时 sklearn 的接口都支持稀疏输入不需要手动转。特征做完后看一眼最重要的词是哪些用vectorizer.get_feature_names_out()配合模型系数排序即可这一步能及时发现特征里混进了人名地名这类无情绪词。3.3 正负样本不平衡下采样、权重和不看准确率疫情早期语料的正负比例经常是 2:1 甚至 3:1因为主流媒体盖度的内容以正向报道居多“新增确诊”“死亡病例”这类负面新闻在绝对数量上少于“治愈出院”“疫苗研发”。直接用原始分布训练模型会把所有样本都预测成多数类然后准确率还能到 70% 以上看起来很好看实际上什么也没学到。先做一个快速的下采样对比实验:from sklearn.utils import resample df_pos df[df[label] 1] df_neg df[df[label] 0] df_neg_down resample(df_neg, replaceFalse, n_sampleslen(df_pos), random_state42) df_balanced pd.concat([df_pos, df_neg_down]).sample(frac1, random_state42)逻辑说明resample把负样本随机抽到和正样本一样多再用concat拼回并打乱顺序。这样训练集的先验概率变成 50/50分类器不再偷懒。下采样的代价是丢掉了一部分负样本信息如果你的负样本本身只有两千条丢掉一半很可惜这时候更好的方案是给模型加class_weight参数让少数类的错分代价更高。我常用的做法是两种都跑一遍小数据集用下采样规模超过十万条就换class_weight。评价指标上分类任务绝对不能只看 accuracy要同时看精确率、召回率和 F1。疫情负面言论漏报的代价比误报高得多所以我会在验证集上优先看少数类召回率再通过阈值下移把召回率拉上来。这个思路和陈运文在达观数据做舆情项目时强调的“代价敏感学习”是一致的教科书里的机器学习八股不会教你这个但真实项目它就是命门。4. 机器学习模型选型与参数调优从经典模型到折中方案4.1 为什么先试朴素贝叶斯和逻辑回归而不是一上来就上深度模型压缩包里的方案大概率是若干传统机器学习模型的组合而不是预训练语言模型原因很现实:疫情话题数据集的规模通常只有几万条深度学习模型在这种量级下容易过拟合而且可解释性差舆情分析项目经常要向非技术背景的同事解释哪些词贡献了负面得分Bert 的注意力和 MLP 的特征表达没法直接回答这个问题。周志华在《机器学习》里讲分类器对比时反复强调“没有免费午餐定理”——没有哪个模型天然优于其他模型先跑快速简单的模型确定基线再考虑是否往复杂方向走。我在这个项目里的选型顺序是:朴素贝叶斯计算最快适合高维稀疏文本和逻辑回归系数可解释能看特征极性先跑通如果 F1 不够再试 SVM 和 LightGBM。微博短文本叠加 tf-idf 特征后矩阵非常稀疏线性模型往往比树模型效果更好并且训练快得多。这一节代码先把朴素贝叶斯跑起来:from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import cross_val_score mnb MultinomialNB(alpha0.5) scores cross_val_score(mnb, X, y, cv5, scoringf1_macro) print(fMultinomialNB F1{scores.mean():.4f} ± {scores.std():.4f})逻辑说明cross_val_score做五折交叉验证而不是一次性划分训练测试集主要原因是疫情语料在时间上存在自相关某一天的热点话题会同时出现在训练和测试集里单次划分得到的分数会虚高交叉验证至少能反映不同子集上的稳定性。alpha0.5是拉普拉斯平滑系数比默认的alpha1.0更小对高频词的特征概率压制更轻。参数说明如果scores.mean()低于 0.7大概率不是模型问题而是特征问题优先回头检查分词质量、停用词表是否干净、max_features是否足够。我在一个类似的涉疫短文本数据集上遇到的情况是F1 卡在 0.65 上不去最后发现是清洗阶段把“武汉加油”中的感叹号全删了“加油”单独成词后极性强度反而减弱了保留感叹号让 ngram 特征变成“加油”F1 直接上了 0.05。这种经验很玄但文本情感分析就是这样一个符号的取舍经常比模型选择影响更大。4.2 逻辑回归作为主力模型从系数看到模型逻辑朴素贝叶斯跑完之后我一般会立刻换逻辑回归。逻辑回归的预测结果可以等价地拆成特征贡献求和:from sklearn.linear_model import LogisticRegression lr LogisticRegression( max_iter1000, C1.0, class_weightbalanced, solverliblinear ) lr.fit(X_train, y_train)逻辑说明solverliblinear对小数据集和高维稀疏矩阵更稳定默认的lbfgs在这种场景下可能不收敛class_weightbalanced根据样本比例自动给少数类更大的权重等效于前面的手工下采样max_iter1000是给优化器一个充分迭代的预算如果忘了调这个参数sklearn 会报ConvergenceWarning模型拿到的不是最优解。训练完成后立刻做一次“系数解释”:feat_names vectorizer.get_feature_names_out() coef lr.coef_.ravel() top_pos coef.argsort()[-15:][::-1] top_neg coef.argsort()[:15] print(正向词:, [feat_names[i] for i in top_pos]) print(负向词:, [feat_names[i] for i in top_neg])逻辑说明系数大于 0 的特征把输出往正向推小于 0 往负向推。这一步输出的词表要拿给业务方看一眼“确诊”出现在负向词表里不奇怪但如果“稳定”出现在负向词表里基本可以断定语料里有大量“病情稳定”被误标成负面或者分词把“不稳定”切成了“不”和“稳定”。特征解释是文本情感分析项目里的硬性环节它相当于给黑匣子模型做透视不看这一步就上线舆情大屏上永远是莫名其妙的正负比例。4.3 GridSearchCV 调参的三个关键参数逻辑回归需要调的超参数并不多核心就三个正则化强度 C、正则化类型 penalty、类别权重 class_weight。我用GridSearchCV一次跑完避免手动循环试参:from sklearn.model_selection import GridSearchCV param_grid { C: [0.1, 1.0, 10.0], penalty: [l1, l2], class_weight: [None, balanced] } grid GridSearchCV( LogisticRegression(max_iter1000, solverliblinear), param_grid, cv5, scoringf1_macro, n_jobs-1 ) grid.fit(X_train, y_train) print(grid.best_params_, grid.best_score_)逻辑说明三个参数的组合数是3 x 2 x 2 12组乘以五折交叉验证就是 60 次完整训练数据量在十万以内完全可接受。solverliblinear同时支持 l1 和 l2换成其他 solver 有些组合会直接报错。n_jobs-1让多个参数组并行训练能省不少时间。参数说明调参的目标是 F1 最大化而不是准确率最大化所以scoringf1_macro。调完参后要对比调参前后的验证集 F1如果提升不到 1 个百分点就别迷信最优参数——这种情况下模型瓶颈在特征质量建议回到第 3 章重新清洗数据、扩充领域词典。对于疫情文本这种类别分布随热点事件明显波动的场景C 值取大一点模型更自信取小一点泛化更稳两者相差不大的时候我倾向于选更小的 C理由简单线上数据的时间分布和训练集必然有偏移平滑一点更抗造。5. 避坑疫情语料情感分析最容易翻车的五个地方5.1 现象模型把“封城”这个词稳定地预测成负面但 2020 年前它基本是中性词原因情感词的极性不是固定的它随事件语境和时间漂移。“封城”在 2020 年初的微博语境里承载了强烈的恐慌和担忧情绪但到了 2022 年一些地区的常态化防控语境里“封城”的新闻情感色彩已经变化。模型训练时如果只用了某一个时间段的数据学到的词极性就是那个时间段特有的。解决训练集和验证集要按时间切分不能随机切分。我把数据按时间前 80% 做训练、后 20% 做验证这样能暴露出模型在时间漂移下的真实表现。另外要定期用新数据做增量训练不要指望一个模型吃一年。补充一句这种“词义漂移”在舆情系统里比在通用情感分析里严重得多热点事件会让几百个词的极性在几天内集体反转。5.2 现象人民日报和微博混合训练后人民日报的文本几乎全被预测为正面原因两个来源的写作风格差距太大。人民日报正文用词规范、句式完整、情感表达含蓄微博口语化、情绪外露、大量使用感叹号和表情。模型很容易学到“来源风格”这个捷径而不是真正的语义情感——长文本 规范标点被当成正向信号这属于典型的领域偏差。解决来源不平衡时把“来源”作为一个显式特征加入模型或者干脆分来源训练两个模型再做结果融合。我更推荐后者每个来源的清洗策略、特征分布、标签口径本来就不同硬生生拉齐反而是违背数据规律。分模型训练还有一个额外收益——可以分别看两个来源的 F1快速定位是哪一侧的数据拖后腿。5.3 现象人工抽检时发现“不太满意”“不是很好”被模型判成正面原因jieba 分词把“不”和“满意”切成两个词、ngram 窗口又太短时“不 满意”和“满意”在特征空间里同时存在模型按词频加权后“满意”的高频优势盖过了“不”的否定作用。教科书上的 TF-IDF 不会主动告诉你怎么处理否定结构。解决在特征层面加入否定词翻转规则。具体做法是预处理时扫描“不、没、无、莫、非”等否定词如果它后面的三个词里有情感词典命中的词就把这个 ngram 从“不 满意”改写成“不_满意”这种带标记形式。sklearn 里没有现成接口我是用自定义 tokenizer 包住 jieba 实现的。这个操作能直接把 F1 拉回 2~3 个百分点是性价比最高的一项修补。5.4 现象训练时交叉验证 F1 高达 0.9上真实数据后效果崩盘原因微博数据里大量存在“原文转发”模式同一条段子被几百个账号原封不动发出只是用户名不同。随机切分时这些重复文本同时落进训练集和测试集模型实质上是在背答案。压缩包里的方案大概率踩过这个坑。解决先去重再切分。按文本内容做 md5 或直接按文本字段drop_duplicates()保留时间最早的一条其余删除。转发数、评论数等元数据可以另存出来做特征但文本主体一定不能重复。做完去重后验证集 F1 通常会下降 5~10 个点这是真实水平不是模型变差了。5.5 现象两个标注员的一致率只有 70%模型训练后行为很怪原因人工标注的边界不清晰。一条文本到底是“正”还是“负”同一个句子不同人理解不同。最典型的例子是“某医院防护物资短缺”“短缺”是负面但主题是“医院在积极自救”标注员一个按事实极性标负、一个按整体态度标正标准完全没对齐。解决标注阶段先定规则。我的经验是统一按“文本传递出来的整体情绪倾向”标注而不是按事实正负标注。规则确定后先标 50 条做试标算一致率低于 85% 就继续讨论规则高于 85% 再动工。不要吝啬这个时间后面模型的一切性能都受标签质量上限约束。6. 把模型接上时间轴舆情演化验证的进阶技巧模型训练完成后别急着输出一个静态“正负比例”交差。疫情类话题的舆情价值恰恰在“随时间演化”这条线上——新增确诊曲线和负面情绪曲线是否同步、政策发布后 24 小时正面情绪是否抬升这些分析比单个准确率数字有说服力得多。我把预测结果按天聚合画出正负比例的时间序列:df_pred pd.DataFrame({date: df[publish_date], pred: y_pred, prob: proba[:, 1]}) daily df_pred.groupby(date).agg( total(pred, count), pos_ratio(pred, lambda x: (x 1).mean()) ).reset_index() daily[pos_smooth] daily[pos_ratio].rolling(7, min_periods1).mean()逻辑说明rolling(7)做七天滑动平均消除周末发帖量下降和单日热点事件带来的毛刺。pos_ratio是当日正向预测占比这条曲线叠加在每日新增病例曲线旁边可以快速看出情绪的反身性——疫情数据是客观事实但公众情绪往往比事实曲线滞后或超前几天。更进阶的验证手段是错误分析我每个月会随机抽 200 条预测错误的样本统计错误类型分布。连续两个月发现“政府通知类文本预测错误率偏高”说明某些通知句式“暂停”“严禁”“务必”没被模型正确识别为中性或负向这时候再回头补特征工程就非常有针对性。另外可以尝试 SHAP 对单条预测做解释揪出那条“为什么把去世新闻判成正向”的 bUG——这种样本往往是新闻里有“致敬”“缅怀”这类致敬性正面词情感表达和事实主题分离模型被局部词吸引这个问题不靠样本增强很难彻底解决。从人工标注、特征工程到模型部署这套流程走完最大的感触是传统机器学习模型的每一个失误几乎都能溯源到数据处理层真正属于模型本身的问题反而很少。每次看到糟糕的预测结果我的第一反应从来不是换一个更复杂的模型而是去翻清洗和特征代码。希望这套经验能帮你在疫情文本情感分析这条路上少撞几次墙少熬夜调几个无用参数。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。