Skip to main content

机器学习与LLM舆情分析

· 10 min read
Allen
software engineer
此内容根据文章生成,仅用于文章内容的解释与总结

该项目适合作为小型的项目原型,适合教学和练手。最初这个项目的灵感源于我的个人需求,我需要一个工具来查看主流话题,同时又不想下载一堆 APP 来接收推送。

项目开源地址:https://github.com/jiangyangcreate/AI-Practice-Collection/SocialMood

项目目录结构

SocialMood/
├── docs/ # 文档和静态页面目录
│ └── index.html # 数据可视化的静态页面,展示词云图等图表
├── src/ # Python代码主目录
│ ├── _dataclean.py # 数据清洗脚本,负责数据清洗和处理
│ ├── _model.py # 模型脚本,负责情绪分析
│ ├── _pyechart.py # 数据可视化脚本,负责数据可视化
│ ├── _database.py # 数据库操作模块,处理SQLite数据存储
├── README.MD # 项目说明文档,包含项目介绍和使用说明
├── .gitignore # Git忽略文件配置,用于排除不需要版本控制的文件
├── .nojekyll # 用于GitHub Pages的配置文件
├── requirements.txt # Python依赖包列表
├── setup.py # 项目依赖安装脚本,负责安装必要的包和模型
├── run.py # 项目运行脚本,负责运行爬虫和数据处理
├── news.db # SQLite数据库文件,存储新闻数据

使用流程

# 安装依赖
cd python
python setup.py

# 运行数据抓取,生成静态网页
python run.py

依赖安装

所需依赖可以通过 setup.py 下载安装。因为有些模块不是pip就算安装好的。

setup.py
import subprocess
import sys
import nltk


def install_requirements():
print("正在安装依赖...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])

def install_browser():
print("正在安装浏览器...")
subprocess.check_call([sys.executable, "-m", "playwright", "install"])

def download_nltk_data():
print("正在下载NLTK词典文件...")
nltk.download('vader_lexicon')

def download_model():
try:
import ollama
ollama.pull('qwen2.5')
except Exception as e:
print(f"下载模型失败:{e}")

def main():
try:
install_requirements()
install_browser()
download_nltk_data()
download_model()
print("所有安装步骤已完成!")
except subprocess.CalledProcessError as e:
print(f"安装过程中出现错误:{e}")
sys.exit(1)

if __name__ == "__main__":
main()

主要功能

该系统的主要功能包括:

  • 抓取热搜数据:从微博、抖音、B站等平台抓取热搜数据。

这里也可以通过API获取,爬取注意不要变成DDOS攻击。

  • 数据处理:使用 Pandas 进行数据清洗与处理。

使用pandas主要是处理一些文本型的数据,譬如10万要换算为100000。

使用jieba分词用于后续词云图生成,需要剔除一些单字与标点符号。当然,最近b站很喜欢在标题中加空格,所以要先去空格再分词。

有些数据的热度值还没计算出来,可以使用幂律分布的线性回归填补热度缺失值。这里使用指数回归、普通线性回归效果都不好。

幂律分布常见的样子:个人UP主粉丝排名前几名粉丝差距有百万,像指数分布。但是后续排名的up粉丝差距就很小,接近线性分布。如果我随机扣掉一个排名的up主的粉丝数,让你预测,你可以预测多准。我面对的大概就是这样的一个问题。

下面是我的解决方案:

src/_dataclean.py
def estimate_missing_heat(self):
"""估算缺失的热度值"""
features = ["排名"]

target = "处理后热度"

def impute_group(group):
X = group[features]
y = group[target]

# 如果没有缺失值,直接返回原始组
if not y.isnull().any():
return group

# 幂律分布拟合(通过对数变换实现)
X_train = X[y.notnull()]
y_train = y[y.notnull()]

# 将 X_train 转换为浮点数类型
X_train = X_train.astype(float)

# 对X和y进行对数变换,处理可能的零值
log_X_train = np.log(X_train + 1)
log_y_train = np.log(y_train + 1)

lr = LinearRegression()
lr.fit(log_X_train, log_y_train)

# 获取拟合的系数
slope = lr.coef_[0]
intercept = lr.intercept_
# print(f"拟合的幂律方程为: y = {np.exp(intercept):.2f} * x^{slope:.2f}")

# 预测缺失值
X_missing = X[y.isnull()]
if not X_missing.empty:
# 将 X_missing 转换为浮点数
X_missing = X_missing.astype(float)
log_X_missing = np.log(X_missing + 1)
log_predictions = lr.predict(log_X_missing)
predictions = np.exp(log_predictions) - 1 # 反向变换

# 确保预测值不小于0
predictions = np.maximum(predictions, 0)

# 确保预测值符合排名顺序
for i, pred in enumerate(predictions):
rank = X_missing.iloc[i]["排名"]
higher_ranks = y[(X["排名"] < rank) & y.notnull()]
lower_ranks = y[(X["排名"] > rank) & y.notnull()]

if len(higher_ranks) > 0:
pred = min(pred, higher_ranks.min())
if len(lower_ranks) > 0:
pred = max(pred, lower_ranks.max())

predictions[i] = pred

group.loc[y.isnull(), target] = predictions

return group

# 对每个信息来源分别进行缺失值填充
self.df = self.df.groupby("信息来源").apply(impute_group)

# 删除仍然为None的行(如果有的话)
self.df = self.df.dropna(subset=["处理后热度"])
self.df["处理后热度"] = self.df["处理后热度"].astype(int)
  • 情绪分析:监测和分析公众情绪。算出单条标题的情绪数值之后,标准化到 (-1,1) 这个区间之中。最后通过热度与排名计算出对社会的情感影响力。正数数则是积极影响,负数则是负面影响。

如果你的电脑性能还不错,可以使用本地模型作为情绪分析的核心,根据自己的设备选择模型大小。

src/_model.py
class Model:
@classmethod
def get_sentiment_score(cls,text):

prompt = "请仅对用户提供的句子进行情感评分,并返回介于-1到1之间的分数。-1表示非常负面,1表示非常正面,0表示中立。无需提供其他信息或上下文。"

messages = [
{"role": "system", "content": prompt},
{"role": "user", "content": text}
]
response: ChatResponse = chat(model='qwen2.5', messages=messages)
return float(response.message.content)

数据存储

系统使用 sqlite3 保存中间数据,此部分为基本的增删改查,相关代码在 _database.py 中,数据文件为 news.db,包含两张表:

  • news:清洗后的可用数据。
  • raw_html:原始网页数据。

这样可以保证速度的同时,可以保持文件夹的整洁,如果数据量大可以直接平滑的迁移到正式数据库中。

数据可视化

接下来绘制一些图表,包括:

  • 各个平台的情绪红绿图。
  • 公众情绪的涨跌折线图。
  • 每日全网词云图。

通过pyecharts的脚手架,导出为静态网页。这里为了代码直观,封装为了类。

为了保证协调统一,这里将所有图表的绘制都封装到了一个类中,都使用pyecharts。table的绘制使用pyecharts的components的Table类,这个类默认会将超链接转义,查看源代码发现内容有一个参数 escape_data 用于设置是否转义。但是没有被暴露出来,已经提交了PR,如果你的escape_data=False报错,可以自己修改源代码。

src/_pyechart.py
from pyecharts.charts import Bar, Line, Pie, WordCloud, Page
from pyecharts.components import Table
from pyecharts import options as opts
import os
class ChartGenerator:
def create_table(self,data) -> Table:
"""Generate a table."""
table = Table()
headers = data.columns.tolist()
rows = data.values.tolist()
table.add(headers, rows,escape_data=False)
return table

def create_bar_chart(self, data , y_label="Sentiment",title="正负面情绪") -> Bar:
"""Generate a bar chart with positive, negative, and absolute sentiment scores."""

x_data, y_data_positive, y_data_negative, y_data_absolute = zip(*data)
bar = (
Bar()
.add_xaxis(x_data)
.add_yaxis("积极", y_data_positive, stack="stack1", category_gap="50%")
.add_yaxis("消极", y_data_negative, stack="stack2", category_gap="50%")
.add_yaxis("绝对值", y_data_absolute, stack="stack3", category_gap="50%")
.set_global_opts(
title_opts=opts.TitleOpts(title=title, pos_left="center"),
xaxis_opts=opts.AxisOpts(name="来源"),
yaxis_opts=opts.AxisOpts(name="情绪得分"),
legend_opts=opts.LegendOpts(pos_top="10%", pos_right="10%"),
)
)
return bar

def create_line_chart(self,data,y_label="情绪得分",title="绝对情绪变化折线图") -> Line:
"""Generate a line chart."""
x_data, y_data = zip(*data)
line = (
Line()
.add_xaxis(x_data)
.add_yaxis(y_label, y_data, is_smooth=True)
.set_global_opts(
title_opts=opts.TitleOpts(title=title, pos_left="center"),
xaxis_opts=opts.AxisOpts(name="日期"),
legend_opts=opts.LegendOpts(is_show=False)
)
)
return line

def create_pie_chart(self, data_pairs, title="信息来源占比") -> Pie:
"""Generate a pie chart."""
pie = (
Pie()
.add("", data_pairs)
.set_global_opts(
title_opts=opts.TitleOpts(title=title, pos_left="center"),
legend_opts=opts.LegendOpts(pos_bottom="0%") # Move legend to the bottom
)
.set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {d}%"))
)
return pie

def create_wordcloud(self, words, title="词云") -> WordCloud:
"""Generate a word cloud."""
wordcloud = (
WordCloud()
.add("", words, word_size_range=[20, 100],shape="circle")
.set_global_opts(title_opts=opts.TitleOpts(title=title, pos_left="center"))
)
return wordcloud

def render_charts(self, charts, output_file=None):
"""Render multiple charts to a single HTML file."""
if output_file is None:
output_file = os.path.join("docs", "index.html")

page = Page(layout=Page.SimplePageLayout)
page.add(*charts)
page.render(output_file)


代码设置

随着时间的推移,爬虫部分的代码可能需要自己修改。你也可以在main函数中,将debug设为True,这样不会真的爬取,而是调用本地的已爬取的网页。生成后的内容在docs/index.html