简介面向计算机相关专业毕业设计及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酒店评论、谭松波老师的酒店评论语料以及一些开源项目里整理好的微博情感标注集。微博这个场景比较特殊文本长度短、口语化严重、标点乱用、还有大量表情符号直接拿酒店评论训练出来的模型效果很一般我实测过迁移后会掉 35 个点。这个项目里用的是从 GitHub 开源仓库里整理好的中文微博情感语料标注维度是「正向 / 负向 / 中性」三分类数据量在 10 万条上下文件是 CSV 格式、字段就两列label和text。在选数据集的取舍上我的建议是优先看标注是否干净而不是看数据量。10 万条里如果有 20% 标注是错的模型精度天花板就会锁死在 60% 上下后面再怎么调参都是做无用功。拿到数据先抽样 100 条人工读一遍看标注一致性是否在九成以上这一步花一小时能省后面调模型的五天。2.2 数据集快速探索看一眼分布再动手拿到数据集不要急着喂进模型。先用 Python 快速统计类别的数量分布、文本长度分布这一步的目的很简单——搞清楚数据有没有严重的类别不均衡。我一般会写上几十行代码直接输出统计结果这能让你心里有底做分类阈值调整的时候有依据。import pandas as pd df pd.read_csv(weibo_sentiment.csv, encodingutf-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 fhttps://m.weibo.cn/api/container/getIndex?containerid100103type1q{keyword}page_typesearchallpage{page} resp requests.get(url, headers{User-Agent: Mozilla/5.0}, timeout10) 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_allFalse保证是精确模式而不是全模式HMMTrue让未登录词可以被隐马尔可夫模型识别。另外自定义词典在微博场景里几乎是刚需比如「不明觉厉」「蚌埠住了」这类网络词如果不加进词典会被拆成单片字特征维度直接失效。import re import jieba STOPWORDS set(line.strip() for line in open(stopwords.txt, encodingutf-8)) def clean_text(raw: str): text re.sub(rhttps?://\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_features50000、min_df2、sublinear_tfTrue。ngram_range(1, 2)代表既保留单字词也保留相邻两个词的组合比如「不好」会被当作一个整体特征而不是「不」加「好」两个分开的特征这能在不引入复杂模型的前提下捕捉一部分短语级语义。max_features50000是限制特征维度防止词表膨胀后带来维度灾难。min_df2是说至少在两条文本里出现过的词才被纳入词表滤掉那些只出现过一次的噪声词汇。sublinear_tfTrue将词的词频做对数缩放抑制高频停用词的冲击。这组参数在不同语料上的微调空间不大除非你手里的数据量级翻了十倍否则这组参数大概率不会成为准确率的瓶颈。from sklearn.feature_extraction.text import TfidfVectorizer df[clean] df[text].apply(lambda s: .join(tokenize(s))) vectorizer TfidfVectorizer(ngram_range(1, 2), max_features50000, min_df2, sublinear_tfTrue) X vectorizer.fit_transform(df[clean]) y df[label]这段代码把分好词的文本做成了稀疏矩阵X的维度是「样本数 × 特征数」用于后续训练。y是标签列注意这里的标签要保证是离散整数形式后续模型会默认它是类别编号而不是连续数值。在fit_transform之后检查一下X.shape如果稀疏矩阵里非零元素占比低于 1%说明数据清洗阶段可能出了问题先去查分词结果再往下走。3.3 类别不均衡怎么处理三分类情感任务里不均衡是常态中性样本往往明显少于正向和负向。处理方式在代码层面有两处落点一是数据层面用train_test_split的stratify参数保证训练集和测试集的类别分布一致二是模型层面在逻辑回归里设置class_weightbalanced让少数类获得更高的错分代价避免模型为了整体准确率而牺牲小类别。两者配合使用既要分布一致又要模型端加权这一点需要多说一句话改用三层结构后我在各个二分类场景中见到很多效果不理想的情况绝大多数是因为只顾一侧而不是两侧同时处理。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy)stratifyy是这里最关键的参数它让训练集和测试集中三个类别的比例与原始数据集相同。random_state42固定随机种子保证复现结果一致。需要注意的是写完这段代码不要急着跑模型先打印一下划分后的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(C1.0, class_weightbalanced, max_iter2000, random_state42) 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)C1.0是正则化强度的倒数C 越小正则约束越强当文本特征维度高、噪声多的时候C 适当调小能抑制过拟合。但 C 的值不宜过度调小经验值是不要低于 0.3过小会让模型把所有非零特征都压向零准确率反而骤降。max_iter2000是为了防止梯度下降不收敛跳过警告如果你观察到了不收敛不用急着增大迭代次数先检查特征矩阵中是否有 NaN。分类报告里除了准确率更需要看宏平均 F1因为准确率在类别不均衡时很容易欺骗人。做完评估把模型和向量化器一起保存后面 Flask 部署时都要加载这一步漏掉的话后续 Web 系统没法独立运行。4.3 LinearSVC 的调参边界与避免踩坑LinearSVC 作为一个可选基线模型在这个项目里也有实现。它需要关注的关键参数是loss和class_weight。loss有hingeSVM 原版和squared_hinge平方合页损失两个可选值squared_hinge收敛更快很多比赛方案默认选它。另一个容易踩坑的地方是,LinearSVC的class_weight支持balanced但要注意SVC带核函数的版本在文本高维稀疏场景下很慢不推荐在 TF-IDF 特征上使用核函数这一点建议直接换成LinearSVC实践。from sklearn.svm import LinearSVC svm_model LinearSVC(losssquared_hinge, C0.8, class_weightbalanced, random_state42) svm_model.fit(X_train, y_train)当数据量在万级、特征维度在万级时LinearSVC训练时间通常不会超过两分钟它的模型文件也比基于神经网络的方式小很多适合轻量化部署。调参上主要动C在 0.5 到 1.5 之间做网格搜索同时用准确率和宏平均 F1 两个指标来选参单看一个指标容易翻车。5. 常见问题排查数据、调参与部署环节的翻车记录这一章把我在实际跑这个项目时遇到的三个高频问题做一个完整复盘每个问题都按「现象 → 原因 → 解决」来拆。这些都是我在处理各类中文文本分类任务中反复出现的典型问题如果你复现的过程里也遇到类似情况能少走弯路。5.1 问题一准确率只有 50%和论文写的不一样现象用自己的数据跑完准确率在五成左右徘徊跟数据集作者声称的七成以上差距很大。原因数据集划分时没有做stratify测试集里某一类样本占绝对多数模型只把多数类预测对了少数类全错。另一种可能是文本清洗时把否定词和程度副词放进了停用词表导致「不好」变成「」特征信号被整段抹掉。解决回到train_test_split加stratifyy同时检查停用词表里是否有「不、没、太、很、极」这类词把它们从停用词列表里移除重新跑训练。5.2 问题二TF-IDF 稀疏矩阵溢出内存现象fit_transform之后内存占用直接飙升到数 GB程序被 OOM 杀掉。原因max_features没有设置或设置过大词表维度膨胀到了百万级甚至千万级稀疏矩阵的非零元素数量失控。解决检查TfidfVectorizer的max_features是否在合理区间5 万到 10 万之间同时把min_df提到 3 或 5进一步滤除低频噪声词。我在处理百万级数据时也更倾向用max_features100000加min_df5在不显著掉精度的前提下把内存占比压得很低。5.3 问题三模型预测时把所有样本都判为负向现象上线后随便输几句正面的句子模型一律给负向。原因训练数据里负向样本占比超过 70%模型学会的策略就是「全都猜负向」整体准确率看起来不低但实际毫无可用性。解决对训练集做类别平衡用class_weightbalanced让少数类在损失函数里获得更高的权重同时用混淆矩阵观察每个类别的召回率而非只看整体准确率。看到混淆矩阵某一整行都是零就要回去看训练集的类别分布了。5.4 问题四打分结果和直觉明显不符现象一条明显正向的评论模型判定为中性或负向检查代码找不到明显问题。原因训练语料的领域和实际预测的领域不一致。比如用酒店评论训练的模型去判断数码产品吐槽词汇分布差异太大泛化性能自然下降。解决面向目标场景补充数据。常见做法是用前文提到的微博爬虫代码去定向补充该领域相关的语料重新标注并加入训练集再重新训练。另外一个细节是文本中表情符号的丢失比如把「[泪]」直接删除会削弱负向信号建议在清洗规则里把这类符号替换成「负面情绪」之类的标记词。5.5 问题五转换后的模型文件巨大Web 启动慢现象模型文件一到加载阶段就要几十秒页面久久没有响应。原因TF-IDF 词表和稀疏矩阵堆叠出来之后单模型文件超过 300MB这是维度设置过高导致的常见结果。解决先检查max_features是不是设得过大把 50 万降到 5 万再者joblib.dump默认压缩是关闭的加compress3参数能有效减小文件体积。这两步操作通常能把模型文件压到 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(forceTrue) 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(host0.0.0.0, port5001, debugFalse)这段代码的逻辑很直白构建一个 POST 接口/predict请求体里带text字段拿到文本后先走清洗和分词再用保存好的向量化器做转换模型预测后同时返回标签和概率分布前端就可以直接展示「这个句子的情感是正向置信度 91%」。改用更开放的host0.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 服务、用验证脚本固定回归基准是这套系统从「能跑」变成「可用」的最后一步。从那以后我每次改完数据处理流程或替换模型版本都不会跳过回归验证这样做让我在一次次修改清洗规则时不用反复试错省去了大量返工时间。希望这篇拆解能帮你把这个系统的结构吃透祝踩坑顺利、早日跑通。本文还有配套的精品资源点击获取