简介这是一份面向自然语言处理与舆情分析方向学习者、研究者的疫情情感分析完整项目围绕2020年疫情期间人民日报与微博等平台话题数据实现情感极性的两分类分析。资源整合了毕业论文文档、Python项目源码与多格式实验数据共2000个文件压缩包约87.47MB核心文件类型包括txt文本语料、json/html抓取数据、csv数据表、pkl模型文件及py脚本目录结构便于按流程复现。项目对比了基于情感词典与基于机器学习两类主流方法涉及知网Hownet、台湾大学NTUSD情感词典及哈工大同义词词林扩充并讨论了训练语料质量对模型性能的影响适合需要完整参考实验设计与代码实现的读者。目前已有433人学习下载可用于课程设计、毕业设计或舆情分析入门实践。1. 疫情话题两极情感分析一个zip串起数据、源码与论文的完整路径2020年3月前后微博话题关键词和人民日报页面上每天滚动着大量与疫情相关的词条公众情绪在「致敬」「加油」与「物资」「封城」之间快速摆动。针对这类舆论数据做情感分析难点从来不是爬虫能不能抓到页面而是数据到手之后怎么把它清理成模型能吃的结构化样本。这份基于机器学习的人民日报与微博疫情话题两极情感分析资源包正好把这条路完整走了一遍里面有毕业论文、Python源码还有2020-02-23到2020-03-14连续多天的keywords.html关键词快照覆盖了话题热度的原始记录。适合两类人一类是在做舆情分析或文本分类方向毕业设计的学生另一类是想看清「数据解析、词典基线、机器学习建模如何串成一个完整流程」的从业者。整个链路不依赖GPU一台笔记本就能从头跑到尾。2. keywords.html到结构化语料read_html解析、字段重建与弱标注2.1 数据快照的文件结构与时间序列价值拿到zip解压后第一眼看到的是毕业论文文档、Python源码文件以及一组以日期命名的HTML文件。这批keywords.html按日期排列包括2020-02-23、2020-02-26、2020-03-01、2020-03-05、2020-03-06、2020-03-08、2020-03-09、2020-03-11、2020-03-14一共9个快照。每个文件保存的是当天的关键词榜单页面里面至少包含关键词文本、热度数值和排名位置这几类信息具体字段以headless浏览器或requests拿到的页面结构为准。这种「先存页面快照、再统一解析」的组织方式在真实舆情项目里很常见。好处是爬取和解析解耦页面结构变了只需要改解析层不需要重新抓数据。日期连续性好也是这批数据的一个优点9个快照横跨三周既能看单日关键词分布也能看话题热度的演变趋势。如果之后想扩展时间序列分析只需要在同目录下追加新的快照文件解析脚本不用改动。需要提醒的是这些HTML是原始页面状态的留存不是清洗好的CSV。所以字段名大概率是页面里的中文表头比如「关键词」「热度」「排名」之类也可能嵌套在多个表格中。解析之前先打开一个文件看结构比直接写死列名更稳妥。2.2 批量解析keywords.html的pandas写法解析HTML表格常见做法是用pandas.read_html它底层依赖lxml或BeautifulSoup能自动识别页面里的table标签并转成DataFrame。对这批快照来说最省事的做法就是写一个批量读取函数把9个文件全部读进来并按日期拼接形成一张带时间字段的大表。import pandas as pd from pathlib import Path def load_keyword_snapshots(data_dir): all_frames [] for path in sorted(Path(data_dir).glob(*keywords.html)): try: dfs pd.read_html(str(path), encodingutf-8) except ValueError: dfs pd.read_html(str(path), encodinggbk) df dfs[0] df[date] path.stem[:10] all_frames.append(df) return pd.concat(all_frames, ignore_indexTrue) snapshot_df load_keyword_snapshots(./data) print(snapshot_df.head())read_html返回的是一个DataFrame列表因为一个页面里可能有多个表格。这里取第一个表格作为主数据如果页面结构里有多个tab后续可以按需修改索引。encoding部分做了一层兜底优先按utf-8解析报错就换gbk这是因为部分历史页面用的是GBK编码直接指定utf-8会抛UnicodeDecodeError。date字段从文件名前10位截取刚好是YYYY-MM-DD的格式后续做时间维度聚合会很方便。解析完先不急着建模打印几行确认列名和数据类型。如果发现整列都是NaN说明选错了表需要改成dfs[1]或者用match参数做一个正则。比如页面里同时存在「热门话题」和「猜你想搜」两张表时可以写成pd.read_html(str(path), match热门)来精准定位。2.3 从关键词到训练语料字段设计、去重与弱标注关键词快照只是原始榜单距离「可以用来训练情感分类器」的语料还有几步。首先是字段设计把来源区分出来同一批快照里可能包含微博热门话题区和人民日报页面的关键词需要在DataFrame里增加一列source值为weibo或people_daily。其次要按「关键词来源」去重因为同一个词在同一天可能出现在多个榜单位置。最后是构造训练样本我一般会把每一行关键词当作一条短文本配上热度作为权重参考情感标签则是我们要预测的目标变量。snapshot_df[source] snapshot_df.apply( lambda row: people_daily if people in str(row.get(url, )).lower() else weibo, axis1 ) snapshot_df snapshot_df.drop_duplicates(subset[keyword, date, source])关于「两极」的定义这份资源采用的思路是二分类也就是把情感分为负面和非负面。实际标注时先要用词典打一个初版标签词典法得分超过阈值的自动归入负面或非负面绝对值很低的样本再人工抽检。这种方式叫弱标注它不追求每条样本都完美而是用低成本先把大规模语料粗标一遍再用机器学习模型去拟合这个分布。def weak_label(keyword, pos_words, neg_words, threshold1): score sum(1 for w in pos_words if w in keyword) - sum(1 for w in neg_words if w in keyword) if score threshold: return 1 if score -threshold: return 0 return -1 snapshot_df[label] snapshot_df[keyword].apply( lambda kw: weak_label(kw, pos_words, neg_words, threshold1) )label为-1的样本先不进入训练集留到人工抽检环节再决定。这个设计的价值在于把「标注成本」和「模型天花板」之间的平衡点讲清楚了词典法只能提供一个不太差的起点真正让精度上去的是后续机器学习模型对错误标签的容忍能力。3. 词典基线到朴素贝叶斯疫情文本二分类的训练与参数选择3.1 先搭词典基线计算原理与两个现成中文情感词典情感分析的主流做法分两类一类是词典法一类是机器学习。词典法的核心假设是「情感可以通过词汇的情感倾向累加得到」实现很直接——把文本切词后统计正面词和负面词的命中数正面减负面就是文本的情感得分。英文语料里有大量现成词典但中文情感词典资源相对有限常见的就是知网Hownet和台湾大学NTUSD哈尔滨工业大学开源的《同义词词林》则常被用来扩充同义情感词。如果先用词典法在疫情语料上跑一版让每个关键词都得到一个情感得分就会发现两个明显问题。第一是语义翻转处理不了「不乐观」「很难过」这类词组里「乐观」「难过」的情感倾向被否定词反转词典法按字面累加就会误判。第二是领域词缺失「封城」「隔离」在通用情感词典里几乎没有收录但这些词在当时语境下带有明显的负面情绪指向词典法对它们完全无感。对比维度词典匹配法机器学习法是否需要标注语料不需要直接查词典需要一定量的标注样本语义翻转处理弱需额外加否定词规则相对更强靠上下文特征领域新词覆盖差依赖词典收录中等取决于训练语料质量跨场景迁移好词典通用性强差训练语料针对性不足时性能严重下滑项目摘要里的一句话点得很透词典匹配会因语义表达的丰富性产生很大误差而机器学习无需深入到语法层面。所以词典法在这个资源里的定位是基线用来生成弱标签和评估机器学习模型的下限而不是最终交付物。3.2 特征工程jieba分词、停用词与TF-IDF参数机器学习方法把文本变成向量才能喂给分类器。中文文本没有天然空格分隔第一步是分词常见做法是用jieba。分词之后是停用词过滤但疫情语料里这一步要特别小心通用停用词表会把「疫情」「确诊」「防控」这类句子骨干词也滤掉后文避坑章会专门展开。这里先给出一版可运行的特征构建代码方向是TF-IDF向量化它在短文本上比单纯的词频统计更稳。import jieba from sklearn.feature_extraction.text import TfidfVectorizer def segment(text): return .join(jieba.lcut(text)) corpus snapshot_df[keyword].astype(str).tolist() seg_corpus [segment(t) for t in corpus] vectorizer TfidfVectorizer( max_features5000, ngram_range(1, 2), min_df2, max_df0.8 ) X vectorizer.fit_transform(seg_corpus)TfidfVectorizer的参数值得逐个解释清楚。max_features5000限制特征数量只保留词频最高的5000个特征防止维度爆炸疫情短文本语料里5000通常够用ngram_range设为(1,2)是为了同时保留单词和双词组合「加油」在单个词层面是中性的但作为双词组合后如果语料里常以「武汉加油」形式出现模型就能捕捉到这种搭配信号。min_df2表示至少在2条样本里出现过才保留特征滤掉只出现一次的噪音词。max_df0.8表示在超过80%的样本里都出现的词会被剔除这类词通常是「的」「了」「是」对分类几乎没有判别力。分词和向量化是流水线的前两站代码跑通后检查一下X的形状行数应该和语料条数一致列数等于特征维度。如果行数对不上多半是分词后产生了空字符串可能是关键词列存在空值用astype(str)只能兜底为字符串nan这类样本最好在向量化前直接丢弃。3.3 用MultinomialNB训练二分类器并评估在短文本二分类场景里我一般首选朴素贝叶斯而不是SVM或逻辑回归。原因是贝叶斯模型对高维稀疏矩阵的适应性好训练速度快在样本量只有几千条时不容易过拟合而且输出的是概率值方便后面调决策阈值。这份资源里的语料规模撑不起深度学习模型朴素贝叶斯是性价比最高的起点。from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report, confusion_matrix X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) model MultinomialNB(alpha1.0, fit_priorTrue) model.fit(X_train, y_train) y_pred model.predict(X_test) print(classification_report(y_test, y_pred)) print(confusion_matrix(y_test, y_pred))train_test_split里有两个容易被忽略的参数。stratifyy表示按标签比例分层抽样疫情语料中负面样本比例如果到了70%以上不做分层的话测试集里可能出现极端分布random_state42固定随机种子保证每次跑出来的结果可复现这在写毕业论文对比实验时非常重要否则每次切分出的训练集不同实验数据不可比。MultinomialNB的两个参数也说明一下alpha1.0是拉普拉斯平滑系数防止某个特征在训练集中没出现导致概率为0调小到0.1会增强模型对训练集的拟合但也更容易过拟合fit_priorTrue表示从训练数据中学习先验概率也就是各类别样本占比对分布不均衡的语料来说保留这个设定比假设类别均匀更合理。评估时重点看三列precision、recall、f1-score。如果负面类的recall偏低说明模型把不少负面样本猜成了正面这种情况下优先调整决策阈值而不是换模型把model.predict_proba(X_test)的负类概率阈值从默认的0.5往下调通常调到0.4就能明显改善召回。4. 疫情语料避坑实录五个影响情感判定结果的常见问题4.1 read_html翻车空表、多表与编码问题现象是同一个解析脚本在部分日期文件上跑得正常换到另外几个文件就报ValueError: No tables found或者返回的DataFrame里全是NaN。第一次遇到时我还以为是文件下载不完整对比了文件大小发现都差不多后来逐个打开HTML才确认问题出在页面结构不统一上有的页面关键词在第一个table里有的页面第一个table是页面头部导航真正的数据在第二个table还有个别文件是GBK编码在解析层直接把之前的错误暴露出来了。原因在于直接用dfs[0]假定第一个表格就是目标表但强调一下历史页面快照最怕结构改版和编码不一致。解决方法是两步走一是编码层面用try/except在utf-8和gbk之间兜底二是表格选择用match参数指定表头关键词比如pd.read_html(str(path), match关键词)让pandas自己去匹配包含「关键词」文本的表而不是无脑取第一个。从那以后我解析任何HTML快照都先打印len(dfs)看看页面里有几张表再决定取哪个索引。4.2 停用词表误杀领域词疫情相关词被过滤的后果现象是训练出来的模型准确率看着还行但看混淆矩阵发现「确诊」「疑似」「封城」这些词几乎没发挥作用负面类的recall明显偏低。查特征矩阵时发现这些词压根不在TfidfVectorizer的vocabulary里。原因是接了一个通用中文停用词表里面包含「确诊」「疑似」这类词的历史来源——最早这些词在通用语料里大多出现在医疗类文本中被某些停用词表当作领域词一并滤掉了。但在疫情短文本情感分析里这些词恰恰是强情感信号。解决方法是把领域词加回白名单先加载通用停用词表再从表里移除疫情相关的核心词最后合并自定义停用词。stop_words set() with open(cn_stopwords.txt, encodingutf-8) as f: stop_words set(f.read().splitlines()) domain_whitelist {确诊, 疑似, 封城, 隔离, 物资, 驰援, 致敬, 加油} stop_words stop_words - domain_whitelist vectorizer TfidfVectorizer( # 直接传入set对sklearn无效需要转为列表 stop_wordslist(stop_words) )stop_words参数只接收列表或字符串传set会直接报错这是经常翻车的一个细节。覆盖场景其实是通用的任何垂直领域的文本分析任务都不要无脑套通用停用词表先跑一版特征矩阵把词频最高但你觉得和业务相关的词拉出来核对一遍。4.3 来源分布失衡微博噪声与媒体文本的样本比例问题现象是模型在测试集上F1值不错但把预测结果按来源拆开看人民日报关键词的负面判定准确率远低于微博关键词。原因在于数据拼接时没有控制来源比例微博话题关键词数量远多于人民日报页面关键词模型从微博样本里学到的是口语化、情绪化的表达规律碰到人民日报偏书面化的用词就失灵。解决方法是按来源做分层处理而不是混在一起训练。先分别统计两个来源的样本量和正负占比如果比例悬殊一种做法是单独为媒体文本建一个子模型另一种是训练时给少数类来源的样本加权Sklearn里可以通过class_weight或sample_weight实现。我当时用的是折中方案在训练前对人民日报来源的样本做上采样复制若干份直到接近微博样本量的一半模型表现立刻好了不少。要注意的上采样过头会导致过拟合倍增不要超过原来的4倍。4.4 中性样本被硬塞进二分类的副作用现象是语料里不少关键词本身没有明显情感倾向比如「疫苗研发」「新闻发布会」「辟谣」弱标注阶段得分接近0但为了凑二分类样本量把它们强行归入非负面类。结果模型在正面类上虚标严重准确率还可以召回一塌糊涂。原因在于「非负面」和「正面」在语义上不是一回事疫苗研发的新闻确实不负面但也谈不上「积极」把它标为正面会让模型的决策边界被扯歪。解决方法是控制中性样本的占比弱标注时把阈值提高到2让得分绝对值小于2的样本全部从训练集剔除只保留情感倾向明确的词条。实际操作中我还发现一个副产物剔除中性样本后负负类和正正类的准确率都提升了因为决策边界不再被「不太负也不太正」的样本拉扯。4.5 词典匹配的漏判语义翻转与领域新词的盲区现象是词典基线在验证集上的F1只有0.7左右翻看错误样本发现两类问题最集中一类是「不乐观」「难以平静」这种带否定词或程度副词的组合词典只命中「乐观」「平静」按正面计分方向完全反了另一类是「气溶胶传播」「密接排查」这类当时新出现的疫情词汇词典里查无此词情感得分永远为0。原因不难理解词典法本质是词层面的匹配既不做句法分析也不识别语义关系。解决方法是给基线加一条否定词处理规则识别文本中出现在情感词前一个位置范围内的「不」「难」「无」「未」出现则把情感得分取反。这个规则粗糙但能把一部分语义翻转的误差找回来。更重要的启示是词典法分低不代表思路错它更适合当特征生成器——把词典得分作为一列特征喂给机器学习模型模型就能学到「什么时候该信词典、什么时候该忽略」。这个融合技巧放在最后一章展开。5. 让二分类更可靠交叉验证、混淆矩阵与词典特征融合5.1 交叉验证比单次切分更稳单次train_test_split的结果有很强的偶然性换一个random_stateF1可能波动三五个百分点。我后来习惯用cross_val_score做4折交叉验证看5次得分的均值和标准差均值代表模型的真实水平标准差代表稳定性。标准差超过0.05就要警惕说明某些折里正负样本分布和整体不一致这时候优先回到语料层面检查而不是调模型参数。from sklearn.model_selection import cross_val_score scores cross_val_score(model, X, y, cv4, scoringf1) print(F1 mean: {:.3f}, std: {:.3f}.format(scores.mean(), scores.std()))5.2 把词典得分拼进TF-IDF特征矩阵把词典情感得分、关键词长度、热度值这些人工特征拼到向量化矩阵后面让模型在词频特征之外多一个全局视角。拼特征的技巧是处理稀疏矩阵和稠密矩阵的拼接用scipy.sparse.hstack保证输出还是稀疏格式否则内存会翻倍。import numpy as np from scipy.sparse import hstack, csr_matrix lex_scores np.array([weak_label(kw, pos_words, neg_words) for kw in corpus]) hot_values snapshot_df[hot].fillna(0).values vstack hstack([X, csr_matrix(lex_scores.reshape(-1, 1)), csr_matrix(hot_values.reshape(-1, 1))])融合特征后交叉验证的F1在我的实验里涨了2到4个百分点代价几乎为零属于性价比最高的优化手段。5.3 混淆矩阵决定调优方向最后看一眼混淆矩阵的落点。如果负面样本大量被预测为非负面说明模型对负面信号的捕捉不足优先补充负面语料或调整阈值如果非负面样本大量被预测为负面说明负面先验太强考虑调低fit_prior。这套实证习惯是踩了不少坑换来的。从那以后我拿到任何情感分析的新数据第一件事就是先看来源分布和标签分布再谈训练模型这个习惯帮我避开了好几次不必要的返工。希望帮到你。本文还有配套的精品资源点击获取