基于BERT+LSTM的微博情感分析系统:从爬虫到可视化全链路实战
简介这是一套面向Python初学者与自然语言处理爱好者的微博情感分析完整源码围绕数据爬取、清洗、建模与可视化四个环节展开可用于课程设计、毕业项目或情感分析入门实践。压缩包共73个文件约517KB以20个py源码文件为核心辅以15个pyc编译文件、5个vue与4个js前端文件、7个svg与3个png图表资源以及json、xml等配置与说明文档前后端结构清晰。系统采用Scrapy框架抓取微博推文通过自定义脚本完成繁简转换、URL去除与表情包处理再以BERT与LSTM混合模型进行情感分类并绘制柱状图展示各情感标签下的模型表现。前端基于Vue搭建后端提供预测与训练脚本目录划分明确便于按模块阅读与二次开发。目前已有113人学习适合希望掌握从数据采集到模型评估全流程的读者参考借鉴。1. 微博情感分析系统拆包从爬虫到 BERTLSTM 的完整链路微博每天产生的文本量以亿计想从中判断用户情绪走向靠人工看根本不现实。这份基于 Python 的微博情感分析系统把「爬数据 → 洗数据 → 跑模型 → 出图表」串成了一条能跑通的链路前端用 Vue 做展示后端用 Flask 起服务模型部分结合了 BERT 和 LSTM。它不是那种只丢一个 notebook 的玩具项目而是带了 Scrapy 爬虫、数据清洗脚本、训练器、预测器和可视化页面的完整工程。适合谁正在做课程设计、想找一个能改能跑的情感分析项目、或者需要一套微博文本处理流水线做二次开发的人。下面我按实际拆包的顺序把每个模块的作用、怎么跑起来、参数在哪改、容易翻车的地方一条条讲清楚。2. 环境搭建与依赖安装Python 3.7 到 Node 的版本对齐2.1 后端 Python 环境与 requirements.txt这个项目的后端依赖集中在back-end/requirements.txt里从目录结构看用到了 Scrapy、PyTorch或 TensorFlow取决于MyModel.py的实现、transformers、pandas、numpy 等。Python 版本建议 3.7 或 3.8因为__pycache__里出现了cpython-37.pyc说明作者是在 3.7 下跑的。如果你用 3.10 以上部分老版本依赖可能装不上。# 创建虚拟环境避免污染全局包 python -m venv venv # Windows 激活 venv\Scripts\activate # Linux/Mac 激活 source venv/bin/activate # 安装后端依赖 cd back-end pip install -r requirements.txt逻辑说明虚拟环境是必须的因为情感分析项目通常会锁死 transformers 和 torch 的版本全局安装容易和别的项目冲突。requirements.txt里如果没写版本号pip 会拉最新版这时候 BERT 模型加载可能报错。我一般会先pip install -r requirements.txt如果报错再手动降级transformers4.6.0和torch1.8.0这类组合。参数说明requirements.txt里通常包含scrapy、flask、flask-cors、torch、transformers、pandas、numpy、jieba、opencc-python-reimplemented繁简转换。如果缺opencc清洗脚本里的繁简转换会直接抛异常。2.2 前端 Vue 环境与 npm 依赖前端在webapps目录下有package.json、vue.config.js、babel.config.js是标准的 Vue CLI 项目。需要 Node.js 14 以上npm 6 以上。# 进入前端目录 cd webapps # 安装依赖如果 npm 慢可以换 cnpm 或 yarn npm install # 启动开发服务 npm run serve逻辑说明npm install会读取package.json里的依赖包括 vue、vue-router、axios、echarts 等。npm run serve启动的是开发服务器默认端口 8080。如果后端接口不在同一个端口需要在vue.config.js里配代理或者在前端代码里写死后端地址。参数说明vue.config.js里通常有devServer.proxy配置把/api转发到http://localhost:5000。如果后端跑在别的端口改这里。package.json里的scripts.serve对应vue-cli-service serve如果报vue-cli-service not found说明node_modules没装全删掉重装。提示前端和后端要同时跑起来才能看到完整效果。只跑前端会看到页面但没数据只跑后端只能用接口测试。3. Scrapy 爬虫与数据清洗从微博页面到干净文本3.1 爬虫模块结构与运行方式back-end/spiders目录下有single_tweet.py、user.py、tweet.py、common.py说明爬虫按功能拆分了单条微博、用户信息、推文列表、公共方法。run_spider.py是启动入口settings.py是 Scrapy 配置。# 在 back-end 目录下运行爬虫 cd back-end python run_spider.py逻辑说明run_spider.py里一般用CrawlerProcess或cmdline.execute启动指定的 spider。settings.py里会配DOWNLOAD_DELAY、CONCURRENT_REQUESTS、USER_AGENT、ROBOTSTXT_OBEY等。微博对爬虫有频率限制DOWNLOAD_DELAY设太小会被封 IP。参数说明DOWNLOAD_DELAY建议 2 到 3 秒CONCURRENT_REQUESTS设 1 到 2ROBOTSTXT_OBEY设为False否则很多页面爬不到。USER_AGENT要换成真实的浏览器 UA不然返回的是登录页。middlewares.py里可能配了随机 UA 或代理中间件但代理这块我不建议新手碰容易出问题。3.2 数据清洗脚本 CleanData.py 的处理逻辑CleanData.py是清洗核心从文件名和摘要看它做了繁简转换、去 URL、表情包转换。微博文本里混着大量噪声某人、#话题#、http://t.cn/xxx、[微笑]这类表情占位符、繁体字、重复标点。# CleanData.py 的典型清洗流程根据项目结构推断 import re import opencc def clean_text(text): # 繁简转换微博上港澳台用户会发繁体 converter opencc.OpenCC(t2s) text converter.convert(text) # 去掉 URL text re.sub(rhttp[s]?://\S, , text) # 去掉 用户 text re.sub(r[\w\u4e00-\u9fa5], , text) # 去掉 #话题# text re.sub(r#.*?#, , text) # 表情占位符转成文字或去掉 text re.sub(r\[.*?\], , text) # 去掉多余空格和换行 text re.sub(r\s, , text).strip() return text逻辑说明繁简转换放在第一步因为后面的正则对繁体字匹配可能不准。去 URL 用http[s]?://\S能覆盖大部分链接。去和#是为了减少噪声但如果你要做用户关系分析不能去。表情占位符[微笑]这种如果直接去掉会丢失情感信息更好的做法是映射成「开心」「难过」这类词但项目里可能只是简单删除。参数说明opencc.OpenCC(t2s)是繁转简如果要简转繁用s2t。正则里的\u4e00-\u9fa5是中文字符范围。re.sub的替换为空字符串就是删除。清洗后的文本存到哪看pipelines.py通常会存成 CSV 或 JSON也可能直接入库。注意清洗脚本跑之前先备份原始数据。我有次把和#全删了后来想做话题分析只能重新爬。4. BERTLSTM 模型训练与预测MyModel、Trainer、Predictor 三件套4.1 MyModel.py 里的混合模型结构models/MyModel.py定义了模型结构MyDataSet.py定义数据集Trainer.py是训练器Test.py是测试入口Predictor.py是预测器。BERTLSTM 的典型做法是BERT 输出每个 token 的向量取[CLS]或最后一层 hidden state送进 LSTM 做序列建模最后接全连接分类。# MyModel.py 的典型结构根据项目文件推断 import torch import torch.nn as nn from transformers import BertModel class BertLstmModel(nn.Module): def __init__(self, bert_path, hidden_size256, num_layers2, num_classes3): super().__init__() self.bert BertModel.from_pretrained(bert_path) self.lstm nn.LSTM( input_size768, # BERT base 的 hidden size hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, bidirectionalTrue ) self.classifier nn.Linear(hidden_size * 2, num_classes) self.dropout nn.Dropout(0.3) def forward(self, input_ids, attention_mask): outputs self.bert(input_idsinput_ids, attention_maskattention_mask) sequence_output outputs.last_hidden_state # [batch, seq_len, 768] lstm_out, _ self.lstm(sequence_output) # 取最后一个时间步 last_hidden lstm_out[:, -1, :] logits self.classifier(self.dropout(last_hidden)) return logits逻辑说明BERT 负责提取上下文语义LSTM 负责捕捉序列依赖。双向 LSTM 的输出维度是hidden_size * 2因为前向和后向拼接。num_classes3对应积极、中性、消极三类。如果标签是 2 类或 5 类改这个参数。参数说明bert_path可以是bert-base-chinese或本地路径。hidden_size一般 128 到 256太大容易过拟合。num_layers1 到 2 层够用再深训练慢且收益小。dropout0.3 是常规值数据量小可以调到 0.5。4.2 Trainer.py 训练流程与关键参数Trainer.py负责数据加载、模型训练、验证、保存。MyDataSet.py把清洗后的文本转成 BERT 的输入格式input_ids、attention_mask、token_type_ids。# Trainer.py 的训练循环核心根据项目结构推断 from torch.utils.data import DataLoader from transformers import AdamW, get_linear_schedule_with_warmup def train(model, train_dataset, val_dataset, epochs5, batch_size16, lr2e-5): train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) val_loader DataLoader(val_dataset, batch_sizebatch_size) optimizer AdamW(model.parameters(), lrlr) total_steps len(train_loader) * epochs scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(total_steps * 0.1), num_training_stepstotal_steps ) for epoch in range(epochs): model.train() for batch in train_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) outputs model(input_ids, attention_mask) loss nn.CrossEntropyLoss()(outputs, labels) loss.backward() optimizer.step() scheduler.step() optimizer.zero_grad() # 验证集评估 evaluate(model, val_loader)逻辑说明AdamW是 BERT 微调的标配优化器lr2e-5是常用学习率太大容易破坏预训练权重。warmup让学习率从 0 线性升到设定值避免初期震荡。batch_size16是显存和效果的折中显存小就降到 8。参数说明epochs一般 3 到 5再多会过拟合。lr范围 1e-5 到 5e-52e-5 最稳。max_length在MyDataSet.py里设微博文本短128 够用长文本可以 256。device用cuda如果有 GPUCPU 训练会非常慢。4.3 Predictor.py 预测与 app.py 接口对接Predictor.py加载训练好的模型对单条或批量文本做预测。app.py是 Flask 入口提供 HTTP 接口给前端调用。# Predictor.py 的预测逻辑根据项目结构推断 class Predictor: def __init__(self, model_path, bert_path): self.model BertLstmModel(bert_path) self.model.load_state_dict(torch.load(model_path)) self.model.eval() self.tokenizer BertTokenizer.from_pretrained(bert_path) def predict(self, text): inputs self.tokenizer(text, return_tensorspt, max_length128, truncationTrue, paddingTrue) with torch.no_grad(): logits self.model(inputs[input_ids], inputs[attention_mask]) pred torch.argmax(logits, dim1).item() return pred逻辑说明model.eval()关闭 dropout 和 batch norm 的训练行为。torch.no_grad()减少显存占用。truncationTrue和paddingTrue保证输入长度一致。参数说明model_path是训练时保存的.pt或.pth文件路径。bert_path要和训练时一致否则词表对不上。max_length要和训练时一致。# app.py 的 Flask 接口根据项目结构推断 from flask import Flask, request, jsonify from flask_cors import CORS app Flask(__name__) CORS(app) # 允许前端跨域调用 predictor Predictor(model.pth, bert-base-chinese) app.route(/predict, methods[POST]) def predict(): text request.json.get(text, ) result predictor.predict(text) return jsonify({sentiment: result}) if __name__ __main__: app.run(host0.0.0.0, port5000)逻辑说明CORS(app)解决前端跨域问题不加的话浏览器会拦截请求。host0.0.0.0让局域网内其他机器也能访问。port5000是 Flask 默认端口前端代理要对应。参数说明request.json.get(text)从前端 POST 的 JSON 里取文本。返回的sentiment是 0、1、2 这样的类别编号前端需要映射成「积极」「中性」「消极」。5. 避坑与常见问题排查血泪经验五条5.1 爬虫被封 IP返回 403 或登录页现象跑爬虫几分钟后所有请求返回 403或者内容变成微博登录页。原因DOWNLOAD_DELAY太小CONCURRENT_REQUESTS太高或者USER_AGENT是默认的 Scrapy UA被微博识别为爬虫。解决把DOWNLOAD_DELAY调到 3 秒以上CONCURRENT_REQUESTS设为 1USER_AGENT换成真实浏览器 UA。如果还被封加COOKIES_ENABLED True并手动填 Cookie。别用免费代理稳定性极差。5.2 BERT 模型加载报错提示找不到 config.json现象运行Trainer.py或Predictor.py时报OSError: Cant load config for bert-base-chinese。原因bert_path写的是在线模型名但网络不通或者本地路径不对。解决提前把bert-base-chinese下载到本地bert_path改成绝对路径。下载方式transformers的BertModel.from_pretrained(bert-base-chinese, cache_dir./bert_cache)跑一次后缓存就有了。5.3 前端 npm install 卡住或报 node-sass 错误现象npm install卡在node-sass或sass-loader或者报 Python 版本不兼容。原因node-sass需要编译依赖 Python 2.7 和 node-gyp新环境经常装不上。解决把package.json里的node-sass换成sassdart-sass或者用npm install --sass-binary-sitehttps://npm.taobao.org/mirrors/node-sass指定镜像。Node 版本降到 14 或 16 更稳。5.4 训练 loss 不下降准确率卡在 33%现象训练几个 epochloss 一直在 1.1 左右准确率等于随机猜。原因学习率太大BERT 权重被破坏或者标签没对齐MyDataSet.py里 label 映射错了。解决学习率降到 1e-5加 warmup。检查MyDataSet.py里label2id的映射确保 0、1、2 对应正确的情感类别。数据量太少也会这样至少几千条起步。5.5 前端页面空白控制台报跨域错误现象npm run serve后页面能打开但数据加载失败控制台报Access-Control-Allow-Origin。原因前端 8080 端口后端 5000 端口浏览器同源策略拦截。解决后端app.py里加CORS(app)或者vue.config.js里配devServer.proxy把/api转发到http://localhost:5000。两种选一种别同时用。6. 模型表现可视化与进阶调参技巧模型在各标签下的表现.svg这个文件是训练完成后生成的柱状图展示模型在积极、中性、消极三个标签上的准确率、召回率、F1 值。AvgUtils.py应该是计算这些指标的工具类。想自己重新生成跑Test.py就行。# Test.py 里生成评估指标的典型逻辑 from sklearn.metrics import classification_report, confusion_matrix import matplotlib.pyplot as plt def evaluate_model(model, test_loader): all_preds, all_labels [], [] model.eval() with torch.no_grad(): for batch in test_loader: outputs model(batch[input_ids], batch[attention_mask]) preds torch.argmax(outputs, dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(batch[labels].cpu().numpy()) # 打印分类报告 print(classification_report(all_labels, all_preds, target_names[消极, 中性, 积极])) # 画柱状图 report classification_report(all_labels, all_preds, output_dictTrue) labels [消极, 中性, 积极] f1_scores [report[str(i)][f1-score] for i in range(3)] plt.bar(labels, f1_scores, color[#e74c3c, #95a5a6, #2ecc71]) plt.ylabel(F1 Score) plt.title(模型在各标签下的表现) plt.savefig(模型在各标签下的表现.svg)逻辑说明classification_report输出每个类别的 precision、recall、f1-score。output_dictTrue让结果变成字典方便提取。柱状图用不同颜色区分情感极性红色消极、灰色中性、绿色积极。参数说明target_names要和label2id的顺序一致。plt.savefig保存成 SVG 矢量图放大不糊。如果中文显示方块加plt.rcParams[font.sans-serif] [SimHei]。进阶调参方面我一般会做这几件事第一用grid search试lr和batch_size的组合lr在 1e-5 到 5e-5 之间batch_size在 8 到 32 之间。第二加early stopping验证集 loss 连续 2 个 epoch 不降就停省时间。第三数据增强把中性文本通过同义词替换扩充缓解类别不平衡。第四换roberta-base-chinese或chinese-bert-wwm-ext通常比bert-base-chinese高 1 到 2 个点。从那以后我每次跑情感分析项目都先把requirements.txt里的版本号锁死再备份一份原始数据清洗脚本单独跑一遍看输出。模型训练前一定先跑 1 个 epoch 确认 loss 在降不然白等几小时。希望帮到你。本文还有配套的精品资源点击获取