大家好,这一篇我带你从零做一个能跑、能看、能答辩的毕业设计:文本情感分类系统。这有一些练手的项目,完整项目:https://pan.quark.cn/s/1e54aa2ae950
老规矩,先聊清楚这东西是干嘛的。你去淘宝买个手机,评论区一堆话;你去豆瓣看电影,短评一片。商家也好、平台也好,最想知道的就是这些评论里大家到底是夸还是骂。人工看太费劲,机器来分就很快。这个项目做的就是这件事:给模型一句话,它告诉你这句话是正面情感还是负面情感,顺便告诉你它有多大的把握,比如"服务态度很好"→ 正面 84%。
我用的是最经典、最好讲、答辩时也最不容易被问倒的一套组合:jieba 中文分词 + TF-IDF 特征 + 逻辑回归分类器,然后再加一个Tkinter 图形界面,把数据预览、预测结果、分布图都放进一个窗口里,看着就比纯命令行高级不少。整个项目所有图片都统一用 SimHei 黑体来显示中文,Windows 下一般自带了,不会出现方块乱码。
一、项目结构长什么样
先说目录,心里有数了再往下看代码:
15_文本情感分类/ ├── data/ │ └── reviews.csv # 训练数据,120 条中文评论 ├── tokenizer.py # jieba 分词函数(训练和预测共用) ├── build_model.py # 训练模型 + 输出评估结果 + 画图 ├── app.py # Tkinter 图形界面 ├── 模型/ │ ├── vectorizer.pkl # 训练好的词表 │ └── model.pkl # 训练好的分类器 ├── 图/ │ ├── 01_数据分布.png # 正负样本数量柱状图 │ └── 02_混淆矩阵.png # 测试结果混淆矩阵热力图 └── requirements.txt运行顺序就两步:先build_model.py把模型训练出来,再app.py打开界面。下面一步步拆开讲。
二、数据是怎么准备的
网上有很多公开的情感数据集,但对毕设来说自己造一份小数据反而更可控:标签准、量不大、逻辑清晰,答辩时每一行你都能解释。我的reviews.csv一共120 条,正负各60 条,内容和淘宝/京东评论风格很像。
数据大概是这个样子的(节选):
| 文本 | 标签 |
|---|---|
| 这个手机屏幕非常清晰,运行速度很快,一点都不卡 | positive |
| 客服态度很差,问半天没人理,体验非常糟糕 | negative |
| 价格实惠,性价比很高,推荐购买 | positive |
| 东西质量太差了,用了一个星期就坏了,气人 | negative |
| 外观设计很漂亮,手感舒适,非常满意 | positive |
| 发货太慢了,等了半个月才到,太失望了 | negative |
注意一个细节:CSV 存成utf-8-sig编码(带 BOM),这样既不会乱码,Excel 打开也正常。读数据时用 pandas 一行搞定:
df = pd.read_csv(DATA_FILE, encoding="utf-8-sig")先说结论,这 120 条数据的分布是:
| 类别 | 条数 | 占比 |
|---|---|---|
| 正面 positive | 60 | 50.0% |
| 负面 negative | 60 | 50.0% |
| 合计 | 120 | 100% |
正负完全平衡,这样训练出来的模型不会偏科——它不会因为"负面数据多"就偷偷全判负面。
三、核心算法为什么这么选
很多人一看"情感分析"就以为得上深度学习,其实这种短文本二分类,传统机器学习就够了,而且胜在能讲清楚、答辩稳。三条流水线,环环相扣:
1. 分词(jieba)
中文和英文不一样,英文天然有空格分词,中文是"糊"成一串的。"服务态度很好"这句话,得先切成"服务 / 态度 / 很好"这样的词,模型才知道看哪些单元。jieba 是最常用的中文分词库,速度也快。
2. 特征化(TF-IDF)
机器不认识汉字,只能认数字。TF-IDF 做的事就是把每句话变成一个向量,向量的每一维代表一个词或者一个词组合。它的核心思想很朴素:一个词在一句话里出现的次数越多越重要(TF),但如果它在所有评论里到处都在出现,那它的区分度就下降(IDF)。比如"质量差"这个词,就比"东西"这个词更能代表负面情绪。
3. 分类(逻辑回归)
逻辑回归输出的不是简单的"是或否",而是"属于正面的概率",比如 0.84。这点特别适合做界面展示,能直接给用户一个置信度。它本身就是一个线性模型加上 sigmoid 激活,好解释,答辩老师问起来不慌。
四、训练代码逐段讲解
完整的训练脚本是build_model.py,我把它拆成几块讲。
第一部分:基础配置和字体设置
import os import sys import joblib import numpy as np import pandas as pd sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from tokenizer import jieba_tokenizer import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import (accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report) plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False两行rcParams很关键:第一行把 matplotlib 的默认字体换成SimHei,图里的中文标题、标签才不会变成小方块;第二行是让坐标轴的负号正常显示。下面那行sys.path.insert是为了让训练脚本能顺利引入同目录下的tokenizer.py,这样无论从哪个目录启动都不会找不到模块。
第二部分:加载数据
SEED = 42 def load_data(): df = pd.read_csv(DATA_FILE, encoding="utf-8-sig") df.columns = [c.strip() for c in df.columns] df = df.dropna().reset_index(drop=True) return dfdf.columns一行是防坑用的,防止表头里带看不见的空格;dropna()把有空值的行扔掉,保证训练数据干干净净。SEED = 42是随机种子,固定下来,保证每次跑出来的结果一致,答辩演示时前后数据对得上。
第三部分:jieba 分词函数
def jieba_tokenizer(text): return [w for w in jieba.lcut(text) if w.strip()]jieba.lcut是把一句话切成词列表,比如"客服态度很差"会切成"客服 / 态度 / 很差"。if w.strip()是过滤掉空白字符。为什么要单独放到tokenizer.py里而不是写在训练脚本里?因为训练和预测要共用同一个分词函数,而且这个函数会被存进模型文件,写到独立模块里才能保证保存、加载都不出错。
第四部分:训练模型
def build_model(X_train, y_train): vectorizer = TfidfVectorizer( tokenizer=jieba_tokenizer, ngram_range=(1, 2), sublinear_tf=True, token_pattern=None) clf = LogisticRegression(max_iter=1000, C=5.0) X_tr = vectorizer.fit_transform(X_train) clf.fit(X_tr, y_train) return vectorizer, clf这里有两个值得一提的调参点:
ngram_range=(1, 2):除了单个词,还把相邻的两个词当一个特征。这样"质量好"和"质量差"这种带修饰的词组能被单独识别出来,单看"质量"一个词反而分不清好坏。sublinear_tf=True:对词频做一次对数压缩,防止某个词出现次数太多把其他特征盖过去。
逻辑回归里max_iter=1000是给足迭代次数让它收敛,C=5.0是正则化强度的反比,调大一点让模型拟合得更充分。就这几行,模型已经能用了。
第五部分:训练集测试集划分
X_train, X_test, y_train, y_test = train_test_split( df["文本"], df["标签"], test_size=0.2, random_state=SEED, stratify=df["标签"])test_size=0.2表示 120 条里抽 96 条训练、24 条测试。stratify是分层抽样,保证测试集里正负比例和全量一样,都是 50% 对 50%,不然测试结果会有偏差。
第六部分:评估模型
acc = accuracy_score(y_test, y_pred) p = precision_score(y_test, y_pred, pos_label="positive") r = recall_score(y_test, y_pred, pos_label="positive") f1 = f1_score(y_test, y_pred, pos_label="positive")四个指标一起看,比单看准确率科学。准确率是整体预测对的占比;精确率是"预测为正面里真有几分是正面";召回率是"真正面里被找出几分";F1 是精确率和召回率的调和平均,两者偏科任何一头都会把它拉下来。测试集预测对错还会打印出来,方便答辩时随手指着说"你看这句预测对了,置信度 73%"。
第七部分:画图和保存模型
def draw_distribution(df): counts = df["标签"].value_counts() ... plt.title("训练数据中正面 / 负面评论数量分布") plt.ylabel("评论条数") plt.savefig(os.path.join(IMAGE_DIR, "01_数据分布.png"), dpi=150) def draw_confusion(cm): ... plt.title("测试集混淆矩阵") plt.savefig(os.path.join(IMAGE_DIR, "02_混淆矩阵.png"), dpi=150) joblib.dump(vectorizer, os.path.join(MODEL_DIR, "vectorizer.pkl")) joblib.dump(clf, os.path.join(MODEL_DIR, "model.pkl"))图一是正负样本数量对比柱状图,图二是混淆矩阵热力图,都保存到"图"目录。最后用joblib.dump把词表和分类器存成两个 pkl 文件,这就是训练和预测解耦的关键:界面启动时不用重新训练,直接加载现成的模型,秒开。
五、真实运行结果(数据展示)
我跑了一遍,真实输出是这样的:
总样本数: 120 正面比例:50.0% 负面比例:50.0% 测试集数量: 24 准确率 :79.17% 精确率 :76.92% 召回率 :83.33% F1 值 :80.00%分类报告:
| 类别 | 精确率 | 召回率 | F1 | 样本数 |
|---|---|---|---|---|
| 负面 | 0.82 | 0.75 | 0.78 | 12 |
| 正面 | 0.77 | 0.83 | 0.80 | 12 |
测试样本对照(前 8 条,对表示预测正确):
| 判定 | 预测 | 置信度 | 原文 |
|---|---|---|---|
| 对 | 正面 | 60% | 物流速度杠杠的,第二天就到了,包装也很严实 |
| 对 | 正面 | 71% | 锅炉加热快,热水充足,冬天用很舒服 |
| 对 | 正面 | 56% | 音质特别好,低音震撼,戴上就不想摘下来 |
| 对 | 正面 | 54% | 卖家服务热情,有问必答,售后很及时 |
| 对 | 负面 | 68% | 客服敷衍,退货一直被拖,态度恶劣 |
| 对 | 负面 | 55% | 信号极差,经常断网,打电话都听不清 |
| 对 | 负面 | 73% | 做工粗糙,缝隙很大,明显是次品 |
| 对 | 负面 | 79% | 防水效果差,下雨天没一会就进水了 |
在 120 条小数据上做到79% 的准确率、80% 的 F1,对短文本二分类来说已经是不错的成绩了。混淆矩阵里可以看到,模型在"负面判成正面"和"正面判成负面"上大概各错两三例,这就是小样本数据的正常表现,答辩时如实说清楚就是加分项。
六、图形界面是怎么做的
命令行输出再漂亮,也抵不上一个窗口来得直观。我用 Python 自带的Tkinter写了个界面,不需要额外装 UI 库。窗口分成三块:
- 顶部输入区:一个文本框,贴一句评论,点"开始预测"。
- 中间结果区:大字显示"正面情感 / 负面情感",颜色红绿区分,旁边再标置信度。
- 下方标签页:一页是数据集预览表格,一页是本次会话所有预测结果的分布柱状图。
关键代码是这样:
root = tk.Tk() SentimentApp(root, vectorizer, clf) root.mainloop()预测的核心就三行:
vec = self.vectorizer.transform([text]) label = self.clf.predict(vec)[0] conf = max(self.clf.predict_proba(vec)[0])注意这里用的是transform而不是fit_transform——因为词表已经在训练时建好了,预测时只需要把新句子套进同一套词表,这就是保存模型的好处。predict_proba返回的是两个概率,取最大值就是置信度,比如"这个耳机音质太棒了"预测正面,置信度 84%,界面上就显示绿色的大字"正面情感 84%"。
柱状图是用 matplotlib 的FigureCanvasTkAgg嵌进 Tkinter 窗口的:
self.fig = plt.Figure(figsize=(5, 3), dpi=100) self.ax = self.fig.add_subplot(111) self.canvas = FigureCanvasTkAgg(self.fig, master=tab2) self.canvas.get_tk_widget().pack(fill="both", expand=True)每点一次"开始预测",就会往self.records里追加一条结果,然后重画柱状图。你连续测十几句话,能看着柱子一点点长高,数据分布一目了然。界面的字体统一用了 SimHei,中文标题、按钮、提示文字都不会乱码。
七、怎么运行起来
环境要求:Python 3.8+,以及这几个库。
numpy pandas scikit-learn matplotlib joblib jieba装完库之后两步走:
- 先训练模型:在项目目录下运行
python build_model.py,运行完会看到评估结果,同时生成"模型"里的两个 pkl 文件和"图"里的两张图片。 - 再开界面:运行
python app.py,会弹出图形窗口,直接输入中文评论点预测就行。
如果直接打开app.py提示找不到模型文件,那就是第一步还没跑,先回去把build_model.py跑一遍。
八、答辩时老师可能问什么
几个大概率被问到的问题,提前想好答案:
问:为什么不用深度学习?
答:短文本二分类用传统机器学习已经能到 80% 左右的 F1,逻辑回归训练快、好解释、还不用显卡,更适合教学和毕设场景。如果数据量到几万条再考虑 BERT 之类的深度模型。
问:TF-IDF 到底在算什么?
答:TF 是一个词在文本里出现的频率,IDF 是这个词在整个语料里的稀有程度。两者相乘,既看重词语在句子里是否重要,又削弱那些到处都有的常见词。
问:置信度是什么?
答:逻辑回归输出的本质是一个概率,属于正面的概率 0.84,就说明模型有 84% 的把握认为这句话是正面的。
问:还能怎么改进?
答:增大训练数据量、加一个情感词典、试试支持向量机或者朴素贝叶斯做对比实验,都能让准确率再上一截。