简介:面向计算机相关专业毕业设计及NLP初学者,这份基于Python的微博情感分析与文本分类系统实现,覆盖了从数据采集、文本预处理、特征工程、机器学习模型训练到评估的完整流程,涵盖朴素贝叶斯、SVM、AdaBoost等经典算法,并引入情感词典进行倾向判断,很适合作为实践项目或毕业设计参考。压缩包共70个文件,主要包括31个Python源码、15个npy参数、4个model模型、13个txt词典/语料,以及1份说明文档(docx),总大小仅6.06MB;源码涉及数据处理、模型训练、测试和可视化等模块,txt与npy等对应情感词典和训练产物,docx可辅助整理算法思路。当前已有6889人学习或下载。资源内含可直接运行的实验代码、训练好的模型参数、中文情感词典和项目文档,既可快速跑通复现结果,也便于替换数据集或调整模型参数开展二次实验,是理解自然语言处理与文本分类的实用材料。
1. 微博情感分析与文本分类:这个 Python 毕设到底在做什么
做过 NLP 类毕业设计的人应该都有同感:微博情感分析这个题目,几乎每年都有人做,但真正能把准确率做到八十分以上、还能把结果讲清楚的项目,不到三成。这个基于 Python 的微博情感分析系统,解决的不是「能不能跑通」的问题,而是「跑通之后能不能写进论文、能不能应付答辩」的问题——它把数据采集、情感标注、文本分类、结果可视化整条链路串在了一起,用朴素贝叶斯和逻辑回归这类可解释模型做分类,训练集和测试集的验证指标、混淆矩阵、ROC 曲线都有,文件结构也适合直接改成自己的毕设。适合的读者很明确:正在选毕设题目的本科生,或者想快速搭一套情感分析 demo 作为简历项目的在职开发者。后面几章我会拆开讲实现细节、关键参数和我在复现过程里踩过的坑。
2. 数据集从哪来:公开语料与微博爬虫两条路
做情感分析的第一步不是写分类器,而是先把数据拿到手。这个项目用的是「公开二手数据 + 微博评论爬取补充」的组合策略,很多人一上来就急着爬全站微博,结果封号又封 IP,最后连训练集都没凑齐。先解决数据问题,后面所有模型才有得玩。
2.1 公开语料选型:为什么不用自建数据集
公开情感分类数据集在中文 NLP 圈子里其实不少,常见的有 ChnSentiCorp(酒店评论)、谭松波老师的酒店评论语料,以及一些开源项目里整理好的微博情感标注集。微博这个场景比较特殊,文本长度短、口语化严重、标点乱用、还有大量表情符号,直接拿酒店评论训练出来的模型效果很一般(我实测过,迁移后会掉 3~5 个点)。这个项目里用的是从 GitHub 开源仓库里整理好的中文微博情感语料,标注维度是「正向 / 负向 / 中性」三分类,数据量在 10 万条上下,文件是 CSV 格式、字段就两列:label和text。
在选数据集的取舍上,我的建议是优先看标注是否干净,而不是看数据量。10 万条里如果有 20% 标注是错的,模型精度天花板就会锁死在 60% 上下,后面再怎么调参都是做无用功。拿到数据先抽样 100 条人工读一遍,看标注一致性是否在九成以上,这一步花一小时,能省后面调模型的五天。
2.2 数据集快速探索:看一眼分布再动手
拿到数据集,不要急着喂进模型。先用 Python 快速统计类别的数量分布、文本长度分布,这一步的目的很简单——搞清楚数据有没有严重的类别不均衡。我一般会写上几十行代码直接输出统计结果,这能让你心里有底,做分类阈值调整的时候有依据。
import pandas as pd df = pd.read_csv("weibo_sentiment.csv", encoding="utf-8") # 类别分布 print(df["label"].value_counts()) # 文本长度分布 df["text_len"] = df["text"].apply(lambda x: len(str(x))) print(df["text_len"].describe())这段代码做的事很简单:用value_counts()看三个类别的样本量差异,再用describe()看文本长度的均值、中位数和上下四分位。实际操作中,如果发现正向和负向有 5 倍以上的差距,优先做的是欠采样或者给少数类加权,绝不能让模型学成「全都预测多数类」的偷懒模式。后文模型参数设置时,我会把类别权重放进逻辑回归的class_weight参数里,这是对应这个数据集结构的常见处理方式。
2.3 需要补充数据时的爬虫方案
某些情况下,公开语料不够用而自行采集微博数据是一个选择。需要说明的是,本节仅讨论在公开数据不足时,基于网页渲染内容进行数据采集的技术方案。自行编写采集程序前,务必确认目标平台的用户协议与数据使用合规要求,且仅将采集结果用于个人学习研究。以微博移动端搜索页面为例,其内容是服务端渲染的,直接请求 HTML 就能拿到嵌入的文本数据,这比抓 Web 接口要稳定得多。
import requests from bs4 import BeautifulSoup def fetch_weibo_comments(keyword: str, pages: int = 5): results = [] for page in range(pages): url = f"https://m.weibo.cn/api/container/getIndex?containerid=100103type=1&q={keyword}&page_type=searchall&page={page}" resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10) if resp.status_code != 200: continue cards = resp.json().get("data", {}).get("cards", []) for card in cards: mblog = card.get("mblog", {}) text = BeautifulSoup(mblog.get("text", ""), "html.parser").get_text() if text: results.append({"text": text, "label": None}) return results这里请求的是微博移动端的搜索接口,返回是 JSON 嵌套结构,每条博文的主体在mblog.text字段,里面有 HTML 标签,用 BeautifulSoup 的get_text()剥离掉。pages参数控制翻页深度,实际使用建议先跑 1 页试通,再扩大范围。这段代码补足的场景是:公开语料里的某些实体(比如某个品牌名、某部电影名)出现频率极低,要用微博搜索来补充相关样本,让模型在特定实体上有更好的识别能力。响应超时和字段缺失是高频问题,要在项目里统一加上异常兜底,没有这一步,爬一晚上数据可能存下来的不到一半。
3. 数据清洗与特征工程:决定准确率上限的关键环节
文本分类性能的 80% 由数据质量决定,这是这行里公认的结论。微博文本噪声极大,URL、@提及用户、表情符号、连续标点混在一起,以至于如果你跳过清洗直接抽特征,模型学到的「规律」大概率是标点和一些功能词的分布,而不是真正的语义差异。这一章要讲的几件小事——分词、去停用词、特征提取——是在为分类器做弹药,做得好不好,直接决定后面所有努力的上限。
3.1 清洗规则和 jieba 分词的参数取舍
微博文本清洗有几条实用规则,按优先级排列:第一,去掉 URL 和@用户名,既可以是正则补全也可以直接用字符串替换;第二,把连续重复的标点压缩,例如连续多个感叹号保留一个;第三,表情符号的处理要慎重,正向表情对情感判断有很大帮助,不能一刀切删除,我会把[哈哈]、[泪]这类文本表情分别归入正负向倾向字段;第四,中文和英文之间加空格没有意义,不用模仿英文 NLP 的处理习惯。清洗完成后所有的文本字段都转成同一标准,这是为了后续分词的一致性。
jieba 是这个项目默认的分词工具,选用它的理由很简单:社区成熟、词典可控、速度够快。分词时要打开两个参数,cut_all=False保证是精确模式而不是全模式,HMM=True让未登录词可以被隐马尔可夫模型识别。另外,自定义词典在微博场景里几乎是刚需,比如「不明觉厉」「蚌埠住了」这类网络词,如果不加进词典,会被拆成单片字,特征维度直接失效。
import re import jieba STOPWORDS = set(line.strip() for line in open("stopwords.txt", encoding="utf-8")) def clean_text(raw: str): text = re.sub(r"https?://\S+|www\.\S+", "", raw) # 去 URL text = re.sub(r"@[\u4e00-\u9fa5a-zA-Z0-9_-]+", "", text) # 去 @用户 text = re.sub(r"(\D)\1{2,}", r"\1", text) # 压缩连续重复字符 return text.strip() def tokenize(text: str): words = jieba.lcut(clean_text(text)) return [w for w in words if w not in STOPWORDS and len(w.strip()) > 0]re.sub(r"(\D)\1{2,}", r"\1", text)这一行是压缩连续重复的非数字字符,比如把「哈哈哈」和「!!!!」压缩成「哈」和「!」,这既是为了降噪,也是在把特征维度收窄,不然「哈哈哈」和「哈哈哈哈哈哈」会被统计成两个不同的词。STOPWORDS从停用词表文件里读入,但要明确一点:微博场景下「不」「没」「太」这些否定词和程度副词不能进停用词表,否则「不太好吃」和「太好吃」会被清洗成同一个向量,这类文本清洗过程中最常见的错误,会让模型准确率在无感知之间掉 5 个百分点以上。
3.2 TF-IDF 特征表示与参数边界
分完词之后要决定用什么方式表示文本。这个项目用的是 TF-IDF 向量化,而不是 Word2Vec 或 BERT,原因是 TF-IDF 的可解释性和实现成本在毕设里都是最优的,而且用 TF-IDF 提取出的特征词表可以直接写进论文的附录里。TfidfVectorizer里要调好的参数是ngram_range=(1, 2)、max_features=50000、min_df=2、sublinear_tf=True。
ngram_range=(1, 2)代表既保留单字词也保留相邻两个词的组合,比如「不好」会被当作一个整体特征,而不是「不」加「好」两个分开的特征,这能在不引入复杂模型的前提下捕捉一部分短语级语义。max_features=50000是限制特征维度,防止词表膨胀后带来维度灾难。min_df=2是说至少在两条文本里出现过的词才被纳入词表,滤掉那些只出现过一次的噪声词汇。sublinear_tf=True将词的词频做对数缩放,抑制高频停用词的冲击。这组参数在不同语料上的微调空间不大,除非你手里的数据量级翻了十倍,否则这组参数大概率不会成为准确率的瓶颈。
from sklearn.feature_extraction.text import TfidfVectorizer df["clean"] = df["text"].apply(lambda s: " ".join(tokenize(s))) vectorizer = TfidfVectorizer(ngram_range=(1, 2), max_features=50000, min_df=2, sublinear_tf=True) X = vectorizer.fit_transform(df["clean"]) y = df["label"]这段代码把分好词的文本做成了稀疏矩阵,X的维度是「样本数 × 特征数」,用于后续训练。y是标签列,注意这里的标签要保证是离散整数形式,后续模型会默认它是类别编号而不是连续数值。在fit_transform之后检查一下X.shape,如果稀疏矩阵里非零元素占比低于 1%,说明数据清洗阶段可能出了问题,先去查分词结果再往下走。
3.3 类别不均衡怎么处理
三分类情感任务里不均衡是常态,中性样本往往明显少于正向和负向。处理方式在代码层面有两处落点:一是数据层面,用train_test_split的stratify参数保证训练集和测试集的类别分布一致;二是模型层面,在逻辑回归里设置class_weight="balanced",让少数类获得更高的错分代价,避免模型为了整体准确率而牺牲小类别。两者配合使用,既要分布一致,又要模型端加权,这一点需要多说一句话:改用三层结构后,我在各个二分类场景中见到很多效果不理想的情况,绝大多数是因为只顾一侧,而不是两侧同时处理。
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y)stratify=y是这里最关键的参数,它让训练集和测试集中三个类别的比例与原始数据集相同。random_state=42固定随机种子,保证复现结果一致。需要注意的是,写完这段代码不要急着跑模型,先打印一下划分后的y_train分布,确认比例没有因为划分而走形。
4. 文本分类模型选型:从朴素贝叶斯到 LinearSVC 的对比
数据准备好了,模型选择这个环节才是抬头见山的地方。这个项目内部同时实现了朴素贝叶斯、逻辑回归、LinearSVC 三种分类器,并且在论文实验章节里做了准确率对比。这个选型逻辑是合理的,因为它覆盖了生成式模型、判别式线性模型、带正则的线性模型三个流派,在答辩时能讲出对比故事,又不至于引入深度模型的黑匣子。
4.1 三个基础模型的原理差异
朴素贝叶斯假设特征之间条件独立,这个假设在文本任务里显然不成立(「不好」里「不」和「好」并非独立),但不影响它在短文本分类上表现尚可,因为微博文本本身就短,特征共现不严重。它的优势是训练极快、小样本下稳定。逻辑回归通过 sigmoid 函数直接建模 P(y|x),不做独立性假设,在 TF-IDF 特征上的表现普遍优于朴素贝叶斯。LinearSVC 本质是带有 L2 正则化的线性分类器,用合页损失函数优化决策边界,它和高维稀疏文本特征配合得很好,经常在短文本分类里拿到最好的准确率。
我的经验是,在这类任务里不需要一上来就上 BERT 或 LSTM,先跑一组线性模型,确定一个较高的基线,再决定要不要上深度模型。在这个数据集上,逻辑回归和 LinearSVC 通常已经能到 80% 上下,足够满足毕设的要求,而且训练耗时在分钟级别。
4.2 训练、评估与保存:模型使用流程
模型训练的过程相对固定,关键是在评估时不要只盯准确率。这篇项目里包含了完整的三分类报告打印、混淆矩阵计算和 ROC 曲线绘制,这些都值得照搬进你自己的项目里。下面这段代码完整地跑了逻辑回归的训练、评估和保存:
from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix import joblib model = LogisticRegression(C=1.0, class_weight="balanced", max_iter=2000, random_state=42) model.fit(X_train, y_train) y_pred = model.predict(X_test) print(classification_report(y_test, y_pred, target_names=["neg", "neu", "pos"])) print(confusion_matrix(y_test, y_pred)) joblib.dump(model, "model/logistic_model.pkl") joblib.dump(vectorizer, "model/tfidf_vectorizer.pkl")C=1.0是正则化强度的倒数,C 越小正则约束越强,当文本特征维度高、噪声多的时候,C 适当调小能抑制过拟合。但 C 的值不宜过度调小,经验值是不要低于 0.3,过小会让模型把所有非零特征都压向零,准确率反而骤降。max_iter=2000是为了防止梯度下降不收敛跳过警告,如果你观察到了不收敛,不用急着增大迭代次数,先检查特征矩阵中是否有 NaN。分类报告里除了准确率更需要看宏平均 F1,因为准确率在类别不均衡时很容易欺骗人。做完评估把模型和向量化器一起保存,后面 Flask 部署时都要加载,这一步漏掉的话后续 Web 系统没法独立运行。
4.3 LinearSVC 的调参边界与避免踩坑
LinearSVC 作为一个可选基线模型,在这个项目里也有实现。它需要关注的关键参数是loss和class_weight。loss有hinge(SVM 原版)和squared_hinge(平方合页损失)两个可选值,squared_hinge收敛更快,很多比赛方案默认选它。另一个容易踩坑的地方是,LinearSVC的class_weight支持balanced,但要注意SVC(带核函数的版本)在文本高维稀疏场景下很慢,不推荐在 TF-IDF 特征上使用核函数,这一点建议直接换成LinearSVC实践。
from sklearn.svm import LinearSVC svm_model = LinearSVC(loss="squared_hinge", C=0.8, class_weight="balanced", random_state=42) svm_model.fit(X_train, y_train)当数据量在万级、特征维度在万级时,LinearSVC训练时间通常不会超过两分钟,它的模型文件也比基于神经网络的方式小很多,适合轻量化部署。调参上主要动C,在 0.5 到 1.5 之间做网格搜索,同时用准确率和宏平均 F1 两个指标来选参,单看一个指标容易翻车。
5. 常见问题排查:数据、调参与部署环节的翻车记录
这一章把我在实际跑这个项目时遇到的三个高频问题做一个完整复盘,每个问题都按「现象 → 原因 → 解决」来拆。这些都是我在处理各类中文文本分类任务中反复出现的典型问题,如果你复现的过程里也遇到类似情况,能少走弯路。
5.1 问题一:准确率只有 50%,和论文写的不一样
现象:用自己的数据跑完,准确率在五成左右徘徊,跟数据集作者声称的七成以上差距很大。原因:数据集划分时没有做stratify,测试集里某一类样本占绝对多数,模型只把多数类预测对了,少数类全错。另一种可能是文本清洗时把否定词和程度副词放进了停用词表,导致「不好」变成「」,特征信号被整段抹掉。解决:回到train_test_split加stratify=y,同时检查停用词表里是否有「不、没、太、很、极」这类词,把它们从停用词列表里移除,重新跑训练。
5.2 问题二:TF-IDF 稀疏矩阵溢出内存
现象:fit_transform之后,内存占用直接飙升到数 GB,程序被 OOM 杀掉。原因:max_features没有设置或设置过大,词表维度膨胀到了百万级甚至千万级,稀疏矩阵的非零元素数量失控。解决:检查TfidfVectorizer的max_features是否在合理区间(5 万到 10 万之间),同时把min_df提到 3 或 5,进一步滤除低频噪声词。我在处理百万级数据时也更倾向用max_features=100000加min_df=5,在不显著掉精度的前提下把内存占比压得很低。
5.3 问题三:模型预测时把所有样本都判为负向
现象:上线后随便输几句正面的句子,模型一律给负向。原因:训练数据里负向样本占比超过 70%,模型学会的策略就是「全都猜负向」,整体准确率看起来不低,但实际毫无可用性。解决:对训练集做类别平衡,用class_weight="balanced"让少数类在损失函数里获得更高的权重,同时用混淆矩阵观察每个类别的召回率而非只看整体准确率。看到混淆矩阵某一整行都是零,就要回去看训练集的类别分布了。
5.4 问题四:打分结果和直觉明显不符
现象:一条明显正向的评论,模型判定为中性或负向,检查代码找不到明显问题。原因:训练语料的领域和实际预测的领域不一致。比如用酒店评论训练的模型去判断数码产品吐槽,词汇分布差异太大,泛化性能自然下降。解决:面向目标场景补充数据。常见做法是用前文提到的微博爬虫代码去定向补充该领域相关的语料,重新标注并加入训练集,再重新训练。另外一个细节是文本中表情符号的丢失,比如把「[泪]」直接删除会削弱负向信号,建议在清洗规则里把这类符号替换成「负面情绪」之类的标记词。
5.5 问题五:转换后的模型文件巨大,Web 启动慢
现象:模型文件一到加载阶段就要几十秒,页面久久没有响应。原因:TF-IDF 词表和稀疏矩阵堆叠出来之后,单模型文件超过 300MB,这是维度设置过高导致的常见结果。解决:先检查max_features是不是设得过大,把 50 万降到 5 万;再者,joblib.dump默认压缩是关闭的,加compress=3参数能有效减小文件体积。这两步操作通常能把模型文件压到 100MB 以下,加载时间也相应缩短到秒级。
6. 把模型封装成可用的系统:Flask 快速部署与一个验证技巧
模型训练好,憋在 Jupyter Notebook 里没法给人看,这时候需要一套简单的 Web 界面,让输入一句话能返回情感标签和置信度。这个项目选择 Flask 是可复现性较好的方案,代码结构轻,部署时不依赖重型框架,作为一个毕设演示完全够用。并且 Flask 的整个路由逻辑可以直接展示在后端答辩中,好讲解。
6.1 Flask 接口设计与两个要点
Flask 应用的核心是「路由接收文本 → 清洗 → 向量化 → 模型预测 → 返回 JSON」。需要注意的点是第一,模型和向量化器在模块加载时初始化一次,不要在predict函数里重复加载,否则每次请求都重读文件会导致接口极慢。第二,将预测的标签和最大概率一起返回给前端,方便展示和调试,这不是可选项而是很好的答辩素材。
from flask import Flask, request, jsonify import joblib app = Flask(__name__) model = joblib.load("model/logistic_model.pkl") vectorizer = joblib.load("model/tfidf_vectorizer.pkl") @app.route("/predict", methods=["POST"]) def predict(): data = request.get_json(force=True) text = data.get("text", "") clean = " ".join(tokenize(text)) vec = vectorizer.transform([clean]) label = model.predict(vec)[0] proba = model.predict_proba(vec)[0] return jsonify({"label": str(label), "prob": {str(k): float(v) for k, v in dict(zip(model.classes_, proba)).items()}}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5001, debug=False)这段代码的逻辑很直白:构建一个 POST 接口/predict,请求体里带text字段,拿到文本后先走清洗和分词,再用保存好的向量化器做转换,模型预测后同时返回标签和概率分布,前端就可以直接展示「这个句子的情感是正向,置信度 91%」。改用更开放的host="0.0.0.0"是为了在局域网内能让手机做功能演示,但这个设置生产部署时要注意访问控制问题。测试这个接口可以直接用 curl 命令下面的形式:
curl -X POST http://127.0.0.1:5001/predict \ -H "Content-Type: application/json" \ -d '{"text": "今天天气真好,出去玩很开心!"}'如果返回的label符合直觉且prob中对应标签的概率不是勉强超过 33%(三分类随机线),接口就可以视为正常。如果概率非常接近随机水平,需要回头检查训练标签是否被顺序搞乱,一个常见的隐藏翻车点是y标签的类别编码在训练和预测两次加载时发生了顺序变化。
6.2 前端展示与交互逻辑的轻量实现
前端不必花哨,一个输入框、一个按钮、一个结果展示区域就够用。用原生 JavaScript 发送 POST 请求到后端接口,然后动态渲染预测标签和概率值。这里唯一值得提醒的是,如果后端端口和前端文件不是同域部署,需要处理 CORS,简单解法给 Flask 应用挂上flask_cors的扩展,或者把前端页面直接放进 Flask 的templates目录里由同一个服务渲染,这样就不存在跨域问题了。
async function predict() { const text = document.getElementById("input_text").value; const resp = await fetch("/predict", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: text }) }); const data = await resp.json(); document.getElementById("result").innerText = "情感倾向:" + data.label + ",置信度:" + JSON.stringify(data.prob); }这段 JavaScript 放在 Flask 提供的静态页面里,由后端渲染输出,请求路径用相对路径/predict,不走跨域。实际使用时注意输入框要做长度限制,微博文本最长 140 字,前端maxlength设成 500 足够,否则过长文本会拖慢 TF-IDF 转换时间。
6.3 部署到本机的完整验证流程
模型被封装成 Web 服务后,最后要做的就是系统性验证。这里有一个我养成的习惯:每次改造完部署路径,都会强制走一遍「三位一体」验证流程,先读入新的原始语料,再走联想流程,最后核对输出结果与原有模型的输出做差异比对,确保版本升级不会引入回归性问题。具体做法是准备好 20 条测试句子,覆盖正向、负向、中性、带表情文本、否定句式这五类典型场景,事先写好期望结果,然后逐个请求接口比对实际输出与期望值。这个验证矩阵最好写成一个测试脚本,因为后续无论是改清洗规则还是换模型参数,回归测试都能自动执行并快速暴露问题。
把模型部署成 Web 服务、用验证脚本固定回归基准,是这套系统从「能跑」变成「可用」的最后一步。从那以后,我每次改完数据处理流程或替换模型版本,都不会跳过回归验证,这样做让我在一次次修改清洗规则时不用反复试错,省去了大量返工时间。希望这篇拆解能帮你把这个系统的结构吃透,祝踩坑顺利、早日跑通。
本文还有配套的精品资源,点击获取