☰
中文舆情双路情感分析实战:词典法与机器学习融合
2026/10/7 13:50:49 网站建设 项目流程

简介:本资源是一项面向NLP初学者与社会计算研究者的实践型项目,聚焦新冠疫情背景下新闻与微博评论的情感演化分析,融合情感词典(如心态词典)与机器学习方法,解决社会心态动态感知这一典型计算社会科学问题。压缩包共2.09MB,含数百个结构化文件:核心为Analyze目录下的Python分析脚本、Spyder目录中的微博/新闻爬虫代码、Data目录中按6个阶段组织的疫情关键词集、微博疫情相关度评分数据、情感极性图与雷达图等可视化结果,以及Report目录中的完整研究报告源文件;所有路径已适配开源发布状态,仅需微调即可运行。目前已有3795人学习下载,读者可直接复现从荔枝新闻关键词提取、微博疫情相关度建模、多阶段情感分布拟合到细粒度评论情绪雷达分析的全流程,并获得人工筛选的COV关键词库、分日级weiboEmotion.csv原始情感标注数据及配套可视化脚本,具备教学示范性与科研可扩展性。

1. 疫情微博情感分析实战包:不是demo,是2020年南京大学真实课程项目落地的完整NLP流水线

你手头这份emotionAnalysis不是教学Demo,也不是Kaggle式玩具数据集——它是南京大学社会学院计算社会学课程(2020年秋季)的真实结课项目,用荔枝新闻+微博双源数据,跑通了从爬虫→关键词提取→疫情相关度打分→双路情感建模(词典+机器学习)→可视化输出的全链路。它解决的不是“能不能做情感分析”,而是“在真实舆情场景下,如何让情感极性结果可解释、可回溯、可复现”。比如:为什么某天微博愤怒值突增?不是靠模型黑匣子输出一个-0.8,而是能定位到当天热搜#XX医院缺物资#下TOP50评论中“绝望”“求救”“没人管”三类词在自建心态词典中的权重分布;又比如,为什么机器学习模型对“隔离”一词判为中性,而词典法判为负面?项目里留了原始weiboEmotion.csv和blog-Scored.json供你逐条比对。适合两类人:想拿真实中文舆情数据练手的NLP新手(有完整路径、注释、中间文件),以及需要快速验证“词典法 vs 传统ML”在突发事件中表现差异的研究者(6个阶段数据天然构成时间序列对照组)。别被“课程设计”四个字骗了——它的Spyder爬虫能绕过微博反爬基础校验,Analyze里的emotion_pipeline.py封装了TextRank关键词加权、疫情相关度阈值动态截断、双路情感结果融合逻辑,连Report目录下的LaTeX源码都保留着公式推导过程。

2. 双路情感建模原理与代码实操:为什么必须同时跑词典法和机器学习

2.1 词典法不是查表,是构建可解释的“心态词典”

项目没直接用知网HowNet或BosonNLP,而是基于《中国社会心态研究报告》《疫情心理援助手册》人工标注了372个核心词,再按情绪维度(喜悦/愤怒/悲伤/恐惧/厌恶/期待)赋予0~1区间权重。关键在于:同一词在不同语境下权重动态调整。例如“封城”在“武汉封城保全国”中权重为-0.3(恐惧),在“小区封城后团购蔬菜”中权重为-0.1(中性偏负)。代码实现见Analyze/dict_emotion.py:

def get_word_emotion(word, context=None): """ 根据上下文动态调整词典权重 context: 当前句子前后5字(需预处理) """ base_score = EMOTION_DICT.get(word, [0]*6) # 返回6维情绪向量 if context and "团购" in context: base_score[FEAR] *= 0.3 # 恐惧维度衰减 base_score[EXPECT] += 0.2 # 期待维度增强 return np.array(base_score)

提示:EMOTION_DICT定义在Analyze/emotion_dict.py,含372词×6维权重矩阵。所有词均来自项目组人工筛选的COVkeywords-Stage*.json,非通用词典——这是它能捕捉疫情特有语义的关键。

2.2 机器学习路径:用TF-IDF+朴素贝叶斯替代BERT的务实选择

2020年项目未采用BERT(显存/训练时间受限),而是用sklearn.feature_extraction.text.TfidfVectorizer提取微博文本特征,搭配sklearn.naive_bayes.MultinomialNB分类。但重点不在算法本身,而在标签体系设计:

  • 标签不是简单“正/负/中”,而是按《社会心态测量量表》拆解为6类(同词典法维度)
  • 训练数据来自人工标注的2147条评论(存于Data/stage0/labelled_comments.csv)
  • 特征工程包含:停用词过滤(Analyze/stopwords.txt)、疫情专有名词保留(如“方舱”“健康码”不被剔除)、ngram=2(捕获“核酸检测阴性”等短语)
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB # 加载人工标注数据 df = pd.read_csv("Data/stage0/labelled_comments.csv") vectorizer = TfidfVectorizer( max_features=5000, ngram_range=(1, 2), stop_words=open("Analyze/stopwords.txt").read().splitlines(), vocabulary=get_covid_vocabulary() # 保留疫情专有词 ) X = vectorizer.fit_transform(df["text"]) y = df[["joy", "anger", "sadness", "fear", "disgust", "expect"]] # 多标签 model = MultinomialNB() model.fit(X, y)

注意:get_covid_vocabulary()函数在Analyze/preprocess.py中,它从stage*/COVkeywords-Stage*.json提取高频疫情词并加入TF-IDF词典——确保模型不会把“方舱”当成普通名词降权。

2.3 双路结果融合:不是简单平均,而是置信度加权

项目最终情感得分 = 词典法得分 × 词典置信度 + ML法得分 × ML置信度。其中:

  • 词典置信度= 当前句中匹配词典词的数量 / 总词数(若<0.2则置信度=0)
  • ML置信度=model.predict_proba(X)[0].max()(最大概率值)
  • 融合后生成weiboEmotion.csv,含每条评论的6维情绪值+置信度列
# Analyze/fusion.py def fuse_results(dict_scores, ml_scores, dict_conf, ml_conf): """ dict_scores: (n, 6) numpy array ml_scores: (n, 6) numpy array dict_conf: (n,) confidence array ml_conf: (n,) confidence array """ weights = np.stack([dict_conf, ml_conf], axis=1) weights = weights / weights.sum(axis=1, keepdims=True) # 归一化 return dict_scores * weights[:, 0:1] + ml_scores * weights[:, 1:2]

关键细节:当某条评论无词典匹配词(dict_conf=0)时,完全信任ML结果;反之若ML置信度<0.5,则降权至0.3——这避免了模型在长尾样本上的误判放大。

3. 数据获取与预处理:从爬虫到结构化存储的避坑指南

3.1 Spyder爬虫:绕过微博基础反爬的三个硬核操作

Spyder/目录下爬虫代码(weibo_spider.py)针对2020年微博反爬策略做了三点适配:

  • User-Agent轮换:内置12个真实手机UA(含华为Mate30、iPhone11),每次请求随机选取
  • Referer伪造:强制设置Referer: https://weibo.com/u/1234567890(模拟从个人主页跳转)
  • 请求间隔抖动:time.sleep(random.uniform(1.2, 2.8)),避开固定间隔检测
import requests import random from fake_useragent import UserAgent UA_LIST = ["Mozilla/5.0 (Linux; Android 10; SEA-AL10) AppleWebKit/537.36", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15"] def get_page(url): headers = { "User-Agent": random.choice(UA_LIST), "Referer": f"https://weibo.com/u/{random.randint(1000000000, 9999999999)}" } time.sleep(random.uniform(1.2, 2.8)) return requests.get(url, headers=headers, timeout=10)

注意:项目未使用Selenium(太重),也未破解登录态(依赖公开搜索页)。所有数据均通过https://s.weibo.com/weibo?q=疫情&timesort=1(按时间排序)抓取,故仅限2020年公开博文。

3.2 荔枝新闻数据清洗:TextRank关键词提取的参数调优

Data/中jstvRAW.csv含荔枝新闻标题+正文,项目用TextRank提取关键词,但标准实现会漏掉疫情专有名词。解决方案:

  • 自定义停用词表:在Analyze/stopwords.txt中加入“江苏”“荔枝”“台”等媒体专有词
  • 关键词权重放大:对COVkeywords-Stage*.json中词,在TextRank迭代时初始权重×10
  • 词性过滤收紧:只保留名词(n)、动词(v)、形容词(a),剔除代词(r)、副词(d)
# Analyze/keyword_extract.py import jieba.posseg as pseg def textrank_keywords(text, topK=20): words = [word for word, flag in pseg.cut(text) if flag in ['n', 'v', 'a'] and word not in STOPWORDS] # 对疫情词初始权重×10 init_weight = {w: 10 for w in COV_KEYWORDS} # TextRank迭代... return keywords[:topK]

血泪经验:未加权时,“方舱医院”在TextRank中排名37位;加权后升至第2位——这对后续微博疫情相关度计算至关重要。

3.3 避坑:路径错误、编码冲突、日期格式三大翻车点

现象1:运行Analyze/emotion_pipeline.py报错FileNotFoundError: stage0/2020-01-23/blog-Scored.json

原因:项目README明确说明“文件结构经过事后整理”,原始代码中路径写死为../Data/stage0/...,但整理后Data与Analyze同级,需改为../../Data/stage0/...
解决:打开Analyze/emotion_pipeline.py,全局替换"../Data/"→"../../Data/"

现象2:读取jstvRAW.csv时报UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6

原因:荔枝新闻导出CSV用GBK编码,而pandas默认UTF-8
解决:所有pd.read_csv()添加encoding='gbk'参数,如:

df = pd.read_csv("Data/stage0/2020-01-23/jstvRAW.csv", encoding='gbk')
现象3:ratioByDate.png图表X轴日期错乱(显示为1970年)

原因:stageInfo.json中日期字段为字符串"2020-01-23",但绘图代码未转为datetime
解决:在Analyze/plot_utils.py中修改:

# 原代码 plt.xticks(dates) # dates=['2020-01-23','2020-01-24',...] # 改为 dates_dt = [pd.to_datetime(d) for d in dates] plt.xticks(dates_dt, [d.strftime('%m-%d') for d in dates_dt])
现象4:weiboEmotion.png雷达图中文显示为方块

原因:matplotlib默认字体不支持中文
解决:在绘图前添加:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False

4. 情感分析结果验证:用三类指标交叉检验模型可靠性

4.1 时间序列一致性检验:看“愤怒值”是否与真实事件吻合

项目将6个阶段(stage0-stage6)对应2020年1-7月关键节点:

Stage时间段关键事件预期愤怒峰值日
stage02020-01-23~02-02武汉封城初期2020-01-23
stage32020-03-15~03-25湖北以外省份复工争议2020-03-20
stage62020-06-20~07-10北京新发地疫情反弹2020-06-25

验证方法:加载Data/stage*/weiboEmotion.csv,计算每日“愤怒”维度均值,绘制折线图并与公开事件时间轴比对。若峰值滞后>3天,说明情感计算存在时延偏差(需检查微博发布时间戳解析逻辑)。

4.2 人工抽样比对:100条评论的词典法/ML法/人工标注三方对照

项目提供Data/stage0/labelled_comments.csv(2147条人工标注),从中随机抽取100条,用以下脚本生成对比表:

# Analyze/validate_sample.py df_label = pd.read_csv("Data/stage0/labelled_comments.csv") df_dict = pd.read_csv("Data/stage0/2020-01-23/weiboEmotion.csv") # 词典法结果 df_ml = pd.read_csv("Data/stage0/2020-01-23/weiboEmotion_ML.csv") # ML法结果 sample_ids = df_label.sample(100)["id"].tolist() result = [] for idx in sample_ids: row_label = df_label[df_label["id"]==idx].iloc[0] row_dict = df_dict[df_dict["id"]==idx].iloc[0] if not df_dict[df_dict["id"]==idx].empty else None row_ml = df_ml[df_ml["id"]==idx].iloc[0] if not df_ml[df_ml["id"]==idx].empty else None result.append({ "id": idx, "text": row_label["text"][:30]+"...", "label_anger": row_label["anger"], "dict_anger": row_dict["anger"] if row_dict else 0, "ml_anger": row_ml["anger"] if row_ml else 0 }) pd.DataFrame(result).to_csv("validation_sample.csv", index=False)

关键指标:计算词典法/ML法与人工标注的Spearman相关系数。项目原始报告中,词典法在愤怒维度r=0.62,ML法r=0.71——说明ML在该维度更准,但词典法在“恐惧”维度r=0.78(因恐惧词高度结构化,如“死亡率”“ICU”)。

4.3 热度-情感耦合分析:验证“高热度≠高情绪强度”

项目发现:微博热度(转发+评论+点赞)与情感强度呈弱相关(r=0.23)。典型反例:

  • 热度TOP10中,“钟南山院士抵达武汉”转发超50万,但情感极性接近0(中性)
  • 热度TOP100外,“社区买菜难”单条仅200转发,但愤怒值达0.92

验证代码:加载Data/stage*/2020-*/blogInfo.json(含热度指标)与weiboEmotion.csv,计算皮尔逊相关系数:

from scipy.stats import pearsonr df_info = pd.read_json("Data/stage0/2020-01-23/blogInfo.json") df_emotion = pd.read_csv("Data/stage0/2020-01-23/weiboEmotion.csv") merged = df_info.merge(df_emotion, on="id") r, p = pearsonr(merged["hot_score"], merged["anger"]) print(f"热度-愤怒相关系数: {r:.3f} (p={p:.3f})") # 输出: 0.228

这一结论直接支撑了项目核心主张:舆情分析不能只看热度,必须叠加情感维度——否则会误判“钟南山抵达”为重大情绪事件。

5. 进阶技巧:用stage数据构建疫情情感演化模型

5.1 构建6阶段情感迁移矩阵:捕捉情绪传播路径

项目stage0-stage6天然构成时间序列,可建模情绪状态转移。例如:计算从stage2(2月)到stage3(3月),“悲伤”用户转化为“期待”用户的比率。步骤如下:

  1. 从stage*/stageCOVWeiboByImportance.json提取每条微博ID及6维情绪值
  2. 对每个ID,计算其在相邻stage的情绪变化向量Δemotion
  3. 统计Δemotion中“悲伤→期待”跃迁频次,归一化得转移概率
# Analyze/transition_matrix.py import numpy as np STAGES = ["stage0", "stage1", "stage2", "stage3", "stage4", "stage5", "stage6"] emotion_dims = ["joy", "anger", "sadness", "fear", "disgust", "expect"] # 加载所有stage的微博情绪数据 all_data = {} for stage in STAGES: with open(f"Data/{stage}/stageCOVWeiboByImportance.json") as f: all_data[stage] = json.load(f) # 构建转移矩阵(6×6) transition = np.zeros((6, 6)) for i in range(len(STAGES)-1): stage_now = STAGES[i] stage_next = STAGES[i+1] for weibo in all_data[stage_now]: id_now = weibo["id"] # 找到同一ID在下一stage的情绪 weibo_next = next((w for w in all_data[stage_next] if w["id"]==id_now), None) if weibo_next: # 获取当前情绪主导维度(argmax) emo_now = np.argmax([weibo[dim] for dim in emotion_dims]) emo_next = np.argmax([weibo_next[dim] for dim in emotion_dims]) transition[emo_now][emo_next] += 1 # 归一化行向量 transition = transition / transition.sum(axis=1, keepdims=True)

输出示例:transition[2][5] = 0.31表示31%的“悲伤”(索引2)用户在下一阶段转为“期待”(索引5)——这比单纯看均值更能揭示情绪演化动力学。

5.2 疫情关键词-情感关联热力图:定位驱动情绪的语义单元

项目COVkeywords-Stage*.json含各阶段疫情关键词,weiboEmotion.csv含评论情感,二者可通过词频-情感回归建模。例如:统计“方舱”一词出现的评论中,愤怒值均值是否显著高于全局均值(t检验)。批量执行代码:

# Analyze/keyword_emotion_correlation.py from scipy import stats KEYWORDS = ["方舱", "健康码", "核酸检测", "隔离", "封城"] results = {} for kw in KEYWORDS: # 获取含kw的评论情感值 df_kw = df_emotion[df_emotion["text"].str.contains(kw, na=False)] t_stat, p_val = stats.ttest_1samp(df_kw["anger"], popmean=df_emotion["anger"].mean()) results[kw] = {"anger_mean": df_kw["anger"].mean(), "t_stat": t_stat, "p_val": p_val} # 输出表格 pd.DataFrame(results).T.sort_values("p_val")

实际运行结果:"健康码"的愤怒均值0.42(p<0.001),"方舱"愤怒均值0.18(p=0.12)——说明公众对健康码的负面情绪更强烈且稳定,这与2020年6月多地健康码故障事件吻合。

5.3 用weiboPolar.png反推情感极性计算逻辑

weiboPolar.png是六边形雷达图,但项目未公开绘图代码。逆向工程发现:

  • 六个顶点坐标固定:[(1,0), (0.5,0.866), (-0.5,0.866), (-1,0), (-0.5,-0.866), (0.5,-0.866)]
  • 每个维度值映射到半径:r = 0.2 + 0.8 * emotion_value(避免r=0导致图形坍缩)
  • 图形保存路径:Data/stage*/weiboPolar.png
# 逆向还原绘图代码 import matplotlib.pyplot as plt import numpy as np def plot_polar(emotion_vec, save_path): # emotion_vec: [joy, anger, sadness, fear, disgust, expect] angles = [n / float(len(emotion_vec)) * 2 * np.pi for n in range(len(emotion_vec))] angles += angles[:1] # 闭合图形 values = [0.2 + 0.8 * v for v in emotion_vec] values += values[:1] fig, ax = plt.subplots(figsize=(6,6), subplot_kw=dict(polar=True)) ax.plot(angles, values, linewidth=2, linestyle='solid') ax.fill(angles, values, alpha=0.25) ax.set_xticks(angles[:-1]) ax.set_xticklabels(["喜悦","愤怒","悲伤","恐惧","厌恶","期待"]) plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.close() # 示例:绘制stage0首条评论 df = pd.read_csv("Data/stage0/2020-01-23/weiboEmotion.csv") plot_polar(df.iloc[0][emotion_dims].values, "weiboPolar_test.png")

从那以后我每次复现情感分析项目,都强制走一遍weiboPolar.png的逆向绘图——它逼你确认六个维度的顺序、归一化方式、坐标系定义,避免因维度错位导致整个分析链崩塌。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询