简介:面向毕业设计场景的Python中文情感分析完整工程,采用机器学习方法对酒店评论进行情感极性分类,适合正在做文本挖掘或情感分析课题的本科生参考。资源共2000个文件,压缩包约7.47MB,核心包含设计报告Word文档、5个Python脚本、原始与处理后的语料数据。其中文本语料以txt格式为主,涵盖2000条正向和2000条负向评论,可直接用于训练和测试;Python脚本覆盖评论预处理、分句、停用词过滤、词向量构建及PCA降维与SVM分类等关键环节,可复现从数据清洗到模型评估的完整流程。另含CSV文件、XML配置等辅助内容,目录结构清晰,便于定位设计报告与源码模块。目前已有949人学习,特别适合需要快速搭建情感分析基线、完成毕业设计代码与报告撰写的读者,可直接基于源码修改数据集或模型参数开展进一步实验。
1. 从毕业设计到可复现工程:Python酒店评论情感分析到底做了什么
拿到这份“基于Python实现酒店中文评论的情感分析”资源,第一反应是文件编号太舒服:1_process.py 一路排到 5_pca_svm.py,中间夹着分句、停用词、词向量,是一条完整的中文情感分析 pipeline。它要解决的事一句话能说清:给一条酒店评论,判断它是正向还是负向,全程用 Python 和机器学习,不依赖情感词典。资源自带 2000 条正向、2000 条负向的标注语料,外加一份设计报告 word,正好凑齐毕业设计要的数据、代码、报告三件套。适合三类人:要做机器学习情感分析毕业设计的学生、第一次想把中文 NLP 流程完整跑通的新手,以及想在免费源码包里找一个能改、能换数据集的情感分析基座的从业者。以下脚本的编号和职责,都按资源包内实际文件来对。
2. 数据预处理三条线:清洗、分句与停用词过滤
情感分析这类任务,模型只负责学习特征到标签的映射,特征的质量才是决定天花板的因素。这份资源的五个脚本里,前三个全在给数据做预处理,足见数据清洗在整套流程中的比重。处理后的 cut 文件直接决定了词向量训练的效果,这一步做得糙,后面 SVM 调参再细也救不回来。
2.1 先看清数据形态:2000_pos.txt 与 2000_neg.txt
源码包里的数据文件分为原始和处理后两部分:
| 文件 | 内容 | 用途 |
|---|---|---|
| 2000_pos.txt | 正向评论原始语料,每行一条 | 训练/测试的正样本 |
| 2000_neg.txt | 负向评论原始语料,每行一条 | 训练/测试的负样本 |
| 2000_pos_cut.txt | 正向评论切句、分词、去停用词后的结果 | 词向量脚本的输入 |
| 2000_neg_cut.txt | 负向评论切句、分词、去停用词后的结果 | 词向量脚本的输入 |
这里最值得注意的设计是“文件名即标签”。pos/neg 直接写在文件名里,评论内容按行存放,行号天然对应数据记录顺序。这样做避免了单独维护 label.txt 的麻烦,也降低了下游脚本写错的概率。代价是不太适合数据量大到需要分文件切割的场景,但 2000×2 的规模完全够用。
真正的难点在文本本身。酒店评论里夹杂着“房间很干净,前台服务热情,周边交通方便”这种句式规整的句子,也可能出现“卫生一般般,晚上隔音差得离谱!!!”这类带口语化表达和重复标点的噪点文本。清洗的第一步就是把第二种情况拉回可计算的轨道。设计报告 word 里一般会先放数据处理前后的对比表,拿到源码先对一遍这个表,能少踩一半坑。
2.2 1_process.py 与 2_cutsentence.py:清洗和分句的实际写法
1_process.py 处理的是原始 txt 里的脏数据。最常见需要清掉的包括 HTML 标签、URL、多余空白、非中文的杂符号。注意尽量不要把所有非中文字符一刀切删光,因为「WiFi」「OK」这类词在酒店评论里有真实情感信息。常见的做法是定向删除:
import re def clean_text(text: str) -> str: # 去掉HTML标签 text = re.sub(r'<.*?>', '', text) # 去掉URL text = re.sub(r'https?://\S+', '', text) # 保留中英文、数字和常见中英文标点,其余删掉 text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9。!?,、:;""''()\s]', '', text) # 把连续空白压缩成单个空格 text = re.sub(r'\s+', ' ', text).strip() return text这段正则的逻辑是:先消掉网页痕迹,再按字符集合做白名单保留。\u4e00-\u9fa5 是中文 Unicode 区间,a-zA-Z0-9 保留英文和数字,后面的标点列表按需调整。若你的数据里有很多繁体评论,可以在清洗前加一步 opencc 繁体转简体,这是一些毕业设计代码里会被忽略但在实践里很常见的补充。
分句脚本 2_cutsentence.py 干的事是把一条长评论按句末标点切开。切割粒度值得较真:一条评论经常是“服务不错,但房间里烟味太重”这种前半句好评、后半句差评的结构。如果整条评论直接做情感分类,SVM 学到的是“混合情绪”的整体分布,容易被某一部分带偏。按句切分后,正面句子和负面句子在训练集中的分布更均衡,模型学到的决策边界会更干净。
import re def cut_sentence(text: str) -> list: # 按中英文句末标点切分,标点本身不必保留 parts = re.split(r'[。!?!?]+', text) # 丢掉空串和纯空白行 return [p.strip() for p in parts if p.strip()]多数实现不会单独只做分句,而是把 jieba 分词直接揉进这一步,输出已经是「词 空格 词」的序列。源码包里的 cut 文件如果已经是分词后的形态,说明 2_cutsentence.py 集成了分词逻辑。这个设计值得借鉴:一步到位,减少中间文件的版本混乱。
2.3 3_stopword.py:停用词表与过滤的边界
停用词过滤是中文 NLP 里被误解较多的一步。网上流传的哈工大停用词表、百度停用词表都能直接用,但“酒店”“房间”这类主题词要不要过滤,常见的做法是看它们在正负样本里的区分度。“酒店”几乎每次评论都有,不承载情感,应当过滤;“房间”在“房间太小”“房间干净”里反而是关键,一刀切过滤会损伤模型。
STOPWORDS = set() with open('stopwords.txt', 'r', encoding='utf-8') as f: for line in f: w = line.strip() if w: STOPWORDS.add(w) def filter_stopwords(words: list) -> list: kept = [] for w in words: if w in STOPWORDS: continue if len(w) <= 1: continue if w.isdigit(): continue kept.append(w) return kept过滤规则建议叠三层:停用词表命中、单字词、纯数字。单字词如“的”“了”通常已被词表覆盖,但“好”“差”这种单字词反而带情感,所以len(w) <= 1的规则要看情况,若词表里没有“好”,这一句会直接把情感词删掉。我实际处理时会把这条改为“同时满足在停用词表里且长度为1的才删”,保守一点不会错。
提示:原始 txt 的编码不统一是预处理阶段最常见的报错来源。资源包解压后,建议先用文本编辑器确认编码,再决定 Python 侧用 utf-8 还是 gbk 读取。
3. 词向量与特征工程:getwordvecs 如何把中文评论变成可计算向量
数据清洗完,下一步就是特征工程。这一步决定 SVM 拿到的是一个“有语义的稠密向量”还是一个“稀疏到没法看的高维矩阵”。跑这一步骤之前先把环境确认好,pip install gensim jieba scikit-learn三件套装齐,VSCode 还是 PyCharm 都无所谓,重点是 Python 版本别停在 2.x。
3.1 为什么不用词袋模型:维度爆炸与同义词问题
很多第一次做情感分析的同学会直接上 CountVectorizer 或者 TF-IDF,把每条评论变成一个长度等于词典大小的向量。2000 条中文酒店评论的词典规模通常在一万到两万之间,这意味每个样本是一个上万维的稀疏向量。SVM 线性核能勉强处理,但有两个问题绕不开。第一是维度爆炸,上万维特征里有大量低频词,权重被稀释;第二是同义词问题,“酒店”和“宾馆”在词袋里是两个维度,二者在语义上几乎等价却不合并,模型要额外学一条规则才能认识到这点。
词向量的思路是把每个词映射成一个百来维的稠密向量,向量之间的距离体现语义相似度。这套资源里的 4_getwordvecs.py,按文件名推断就是在这个环节把分词后的数据训练成向量。常见实现是 gensim 的 Word2Vec,核心参数长这样:
| 参数 | 常见取值 | 作用 |
|---|---|---|
| vector_size | 100 | 词向量维度,太小丢语义,太大稀疏 |
| window | 5 | 上下文窗口,越大越偏向主题而非情感 |
| min_count | 1 | 低于该词频的词丢弃,情感词多为低频词,慎调大 |
| sg | 0 | 0 为 CBOW 训练快,1 为 skip-gram 低频词效果更好 |
| epochs | 5 | 训练轮数,数据量小时轮数可略多 |
3.2 4_getwordvecs.py 的典型实现:词级向量到句级向量
Word2Vec 训练完得到的是词级向量,SVM 需要的是一个句级向量。最朴素的聚合方式是 mean pooling:把一句话所有词的向量做平均。
from gensim.models import Word2Vec # 读取处理后的评论,每行已经是一个按空格分词的句子 sentences = [] for line in open('2000_pos_cut.txt', encoding='utf-8'): sentences.append(line.strip().split()) for line in open('2000_neg_cut.txt', encoding='utf-8'): sentences.append(line.strip().split()) # 参数与上面表格对应:vector_size=100, window=5, min_count=1 model = Word2Vec(sentences, vector_size=100, window=5, min_count=1, workers=4, epochs=5) def sentence_vec(words: list) -> list: vecs = [model.wv[w] for w in words if w in model.wv] if not vecs: return [0.0] * model.vector_size # 逐维度取平均,得到100维句向量 return [sum(vals) / len(vecs) for vals in zip(*vecs)]这段代码的要点是if w in model.wv这一句。训练时如果用了 min_count 过滤,语料里会有一些词不在词表里,直接取会抛 KeyError。跳过呢会导致短句只剩一两个词,聚合出来的句向量几乎是零向量;全零向量在 SVM 里会变成一个特殊点,干扰函数间隔。
再往下就是组装数据集:把 2000 条正样本和 2000 条负样本全部过一遍 sentence_vec,生成特征矩阵 X,再按“正样本在前、负样本在后”的顺序生成标签 y。4_getwordvecs.py 的输出在资源中应该是存成 .npy 或 .txt 供下一步读取,这一步的保存格式直接影响第五个脚本的开头怎么写,拿到源码先看这里。
3.3 特征矩阵的标准化与 PCA:为什么顺序错会出大事
5_pca_svm.py 第一步通常是对特征矩阵做 PCA 降维。但 PCA 有一个前置条件,各维度的量级要一致。词向量平均后的特征矩阵每个维度都来自同一个 embedding 空间,量级已经相对均衡,所以有些代码会跳过标准化直接 PCA,跑起来也像模像样。如果改用 tfidf 词向量或别的异构特征,千万记得先标准化再 PCA,否则第一主成分会被大数值维度的方差主导,情感信息被挤掉。
PCA 的 n_components 在这个场景下取多少合适,常见做法是先画累计解释方差比曲线,选一个拐点;如果不想画图,50 到 100 维是比较中庸的选择。原始 100 维降到这里,训练时间会短不少,判别力损失一般可接受。这一步是整条 pipeline 里最容易被当成“艺术”处理的环节,实际上它就是调参,跑完看测试集 F1 再回来调就行。
4. 常见问题排查:数据泄露、编码错乱与库版本不兼容
前两章的代码看起来顺,真到自己跑的时候,问题全出在“看起来没毛病”的细节上。以下四条是我拆这类源码包时遇到的高频坑,每条都按现象、原因、解决展开。
4.1 坑一:训练集准确率90%以上,换一条真实评论就翻车
现象:5_pca_svm.py 跑完,准确率 95% 甚至更高,但拿资源外的一条真实酒店评论去预测,结果永远朝向同一个类别。
原因:数据泄露。最典型的是把全量数据先做了特征工程或 PCA,才划分训练集测试集,测试集的信息在 fit 阶段就被模型看到了;另一种是按文件行顺序直接切前一半训练、后一半测试,而原始 txt 恰好按正样本在前、负样本在后排列,模型学到的其实是“靠前是正、靠后是负”的位置规则。
解决:切分必须发生在任何 fit 之前,PCA 和标准化都只在训练集上拟合。
from sklearn.model_selection import train_test_split from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC # X, y 来自上一步生成的词向量特征矩阵 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 标准化和PCA都在训练集上fit scaler = StandardScaler().fit(X_train) X_train_scaled = scaler.transform(X_train) X_test_scaled = scaler.transform(X_test) pca = PCA(n_components=80).fit(X_train_scaled) X_train_pca = pca.transform(X_train_scaled) X_test_pca = pca.transform(X_test_scaled) model = SVC(kernel='rbf', C=1.0, class_weight='balanced') model.fit(X_train_pca, y_train)这段代码的每一步都强调“只在训练集上 fit”。stratify=y保证正负样本在切分后比例一致;class_weight='balanced'是给少数类加权,如果采集的评论正负数量相差大,这一行能救回不少 F1。跑完把准确率和混淆矩阵一起打印,才能看出翻车具体是翻在哪个类别。
4.2 坑二:读 txt 直接报 UnicodeDecodeError
现象:脚本第一行 open 就报 UnicodeDecodeError,提示 gbk codec can't decode byte 0x...,或者反过来提示 utf-8 codec can't decode。
原因:txt 文件的编码不统一。Windows 记事本默认存的是 ANSI,Linux 和 macOS 默认 utf-8,源码包从网上各种渠道下载后被反复压缩解压,编码被改是常态。代码里写死encoding='utf-8'就会在 Windows 下报 gbk 错误,写死 gbk 又会在 Linux 下翻车。
解决:写一个带回退的读取函数,或者干脆统一转码。我一般是前者。
def read_lines(path: str) -> list: for enc in ('utf-8', 'gbk', 'utf-8-sig'): try: with open(path, 'r', encoding=enc) as f: return [line.strip() for line in f if line.strip()] except UnicodeDecodeError: continue raise RuntimeError(f'无法解码文件: {path}')顺序上把 utf-8 放第一位,是因为现在多数编辑器默认保存为 utf-8;utf-8-sig 放在最后是因为带 BOM 的文件用 utf-8 读会读出 \ufeff 字符。三个都失败就直接抛异常,不静默吞错——情感分析任务里,静默失败最危险,数据静默少了一行,后面所有统计都是错的基础。
4.3 坑三:gensim 版本不兼容,Word2Vec 构造直接报 TypeError
现象:4_getwordvecs.py 一运行就报TypeError: __init__() got an unexpected keyword argument 'size'。
原因:gensim 4.0 把 Word2Vec 的参数 size 改名为 vector_size。网上大量旧教程和旧源码都写size=100,在 gensim 4.x 下必报这个错。反过来,vector_size 在 gensim 3.x 下也不认识。
解决:两个方案二选一。改代码用vector_size=100适配新版 gensim;或者pip install gensim==3.8.3锁老版本。建议用前者,因为新版 gensim 对 Python 3.10+ 支持更好,后续想换文档向量、fastText 也方便。除了 size,老代码里model.wv.vocab在新版里已经改成model.wv.key_to_index,遍历词表时很容易踩到下一个坑。
4.4 坑四:准确率很高,F1 却难看
现象:classification_report 里准确率 90%,但差评的 recall 只有 60%。
原因:正负样本 2000+2000 平衡的话,单看准确率够用;但切分后如果刻意不设置 stratify,偶尔切出来的测试集正负比例失衡,SVM 的默认惩罚是对所有样本一视同仁。另一个原因在词向量:负向评论里的情感词在领域数据里训练出的词向量离中性词距离不够远,特征区分度低。
解决:用 classification_report 同时看每个类别的 precision 和 recall;若差评召回持续偏低,调大 C 或给 SVC 加class_weight='balanced'。C 从 0.1 到 10 做网格搜索,配合 5 折交叉验证选最优组合。这一步放到最后一个脚本里执行,算是对整套流程的参数封装。
5. 把酒店评论换成外卖/电商评论:四个修改点与一发验证
把这套底座迁移到新数据集,只改数据路径是远远不够的。我实际迁移过一轮,总结了四个必改点。
5.1 四个修改点
| 修改点 | 位置 | 注意事项 |
|---|---|---|
| 数据读取 | 各脚本开头的 open 路径 | 新领域数据若为 CSV 两列,把按行读取改成按列取文本,同时保留文件名做标签的约定 |
| 停用词表 | 3_stopword.py 的 STOPWORDS | 在通用词表上追加领域高频噪声词,如外卖场景加“配送”“包装”;“房间”“服务”这类原样保留 |
| 词向量 | 4_getwordvecs.py | 新领域语料必须重新训练 Word2Vec,垂直领域里的“酸梅汤”“骑手”在通用词向量里可能根本没有向量 |
| 分类器 | 5_pca_svm.py | 先默认 rbf 核 C=1.0,再看混淆矩阵决定要不要调 class_weight;评论长度分布不同时,PCA 维度也要重新画曲线确认 |
5.2 一次验证流程,替代“跑通就行”
换数据之后我最常强制自己做的事不是加调参,而是打一张完整验证表:
from sklearn.metrics import classification_report, confusion_matrix y_pred = model.predict(X_test_pca) print(classification_report(y_test, y_pred, target_names=['neg', 'pos'])) print(confusion_matrix(y_test, y_pred))看这张表的顺序很重要。先看 neg 行的 recall 够不够高,再看 pos 行有没有被误杀。如果两类的 recall 差超过 15 个百分点,先回头查数据清洗和词向量,不要急着调 SVM 的 C。准确率只统计整体,但实际业务大多关心“差评有没有被漏掉”——漏掉一条差评的代价远高于误伤一条好评。
从我自己的经验讲,情感分析整套流程的复杂度七成在数据和特征,两成在防止作弊性的高准确率,剩下一成才轮到调参。从那以后,我每次拿到新的情感分析源码包,都强制走一遍“先确认编码、再确认切分在 PCA 之前、最后打混淆矩阵看单类召回”这三步,省下了大量在错误方向上调参的时间。这份资源同样值得你这么做,希望帮到你。
本文还有配套的精品资源,点击获取