☰
酒店评论中文情感分析系统:基于PCA降维与SVM分类的Python源码解析
2026/10/3 3:29:01 网站建设 项目流程

简介:面向计算机相关专业在校生及毕设、课设人群,这份基于Python的酒店评论中文情感分析系统,完整包含源码、设计文档与数据集,可满足中文评论文本情感分类项目实践与答辩展示需求。资源包共2000个文件,以txt文本为主(1999个),涵盖正负向评论语料、停用词表及说明文档,另附1个Python主程序脚本,整体仅1.77MB,下载与运行都很轻量。目前已有360人学习/下载,代码经测试可正常运行,作者给出的项目答辩平均分达96.5分,适合直接作为毕设或课设完整参考,也便于在此基础上扩展其他分类功能。从内容预览可见,语料按正负向分类整理,并涉及PCA与SVM等经典情感分类方法,配合txt格式的预处理数据与说明文字,读者可较完整地复盘从数据准备到模型评估的流程,降低入门门槛。

1. 酒店评论中文情感分析系统:一份能跑通的毕设源码,拆开看到底有什么

做酒店评论中文情感分析这个方向的毕设或课设,十个人里有八个不是死在算法上,而是死在数据编码、分词和跑不通这三件事上。这套基于 Python 的酒店评论中文情感分析系统源码,是把语料、停用词表、主脚本和设计文档打包好的完整项目,主方案是 PCA 降维加 SVM 分类,正负语料大概四千条,评审平均分 96.5,属于导师认可、能直接拿去答辩的那种交付物。适合正在做毕设、课程设计、期末大作业的计算机相关专业学生,也适合想找一个标准文本分类流程练手的小白。下文我会把解压后的每个文件、跑通流程和踩过的坑完整拆一遍。

2. 资源结构先行:pos/neg 语料、停用词表与 5_pca_svm.py 的分工

2.1 从文件名反推数据组织:pos..txt 与 neg..txt 的命名规律

解压这份 zip 之后,第一件事不是急着点开 5_pca_svm.py,而是先把所有文件摊开看一遍。我拿到别人项目的第一习惯永远是先盘文件再盘代码,因为文件命名和目录摆放能直接反映作者的实验组织方式。这份资源里实际出现的是下面这些文件:

5_pca_svm.py stopWord.txt neg.36.txt neg.670.txt neg.730.txt pos.318.txt pos.545.txt pos.554.txt pos.709.txt pos.719.txt

看命名就能反推出两条信息。pos 前缀是正面评论语料,neg 前缀是负面评论语料,情感分析二分类的标签构造就是靠这两个前缀区分的。点后面的数字大概率是文件内的评论条数,pos.318.txt 就是 318 条正面评论,neg.670.txt 就是 670 条负面评论。按这个假设加一下,正面语料合计 318+545+554+709+719=2845 条,负面语料合计 36+670+730=1436 条,总共 4281 条评论,正负样本比例接近 2:1,是典型的不均衡二分类场景。

这个 2:1 的比例不是小事,后面训练集划分和准确率评估都要记得它的存在。如果直接用默认参数切分,测试集里的正负比例会跟着倾斜,导致准确率虚高。neg.36.txt只有 36 条,我怀疑是早期测试的小样本或者某次爬虫只拿到这么多,读数据的时候不能把它漏掉,否则负面样本就少了几十条,结果对不上。

主脚本名5_pca_svm.py的编号同样有信息量。它大概率是作者课设过程中写过的第 5 个脚本,前面可能还有 1_xxx、2_xxx 之类的对比方案,最终定稿交的是 5 号。这说明作者至少试验了四轮方案才落到 PCA 加 SVM。这个编号习惯在后面对比实验结果时很有用,如果你想做方案对比,把不同算法的脚本独立编号,比在一个脚本里反复注释切换干净得多。

2.2 stopWord.txt:停用词表在中文情感分析里为什么不能省

stopWord.txt 是整个项目里容易被忽略但实际很关键的文件,它就是一份普通的中文停用词表,里面大概率是「的、了、吗、啊、在、是、和」这类高频但没有任何情感倾向的虚词。为什么中文情感分析特别依赖停用词表,因为中文分词不像英文那样天然按空格切词,jieba 切完一段评论会产出一大堆没有语义的助词语气词,如果全部喂进向量化器,「酒店很不错」和「酒店很不错了啊」就会被拆出完全不同的特征维度。

更隐蔽的问题是,不滤掉这些虚词的话,TF-IDF 的特征空间会被高频无意义词占掉大量维度,而且它们的文档频率太高,会把 IDF 的区分度拉平,真正有情感判别力的词反而排不进特征前列。我做这类项目时一般会额外做两步:第一步把停用词表读成 set 集合,分词后逐词查表过滤;第二步保留否定词和程度副词,比如「不、没、太、很、非常」,因为它们是情感翻转和强度表达的关键词,一旦误加进停用词表,模型基本就废了。这一点在避坑章节会专门展开。

作者把停用词独立成 txt 而不是写在代码里,是值得表扬的做法。替换停用词表不需要改代码,直接换文件就行,答辩的时候还可以顺势说一句「停用词表独立维护,方便针对不同领域扩充」,这是很加分的工程细节。

2.3 设计文档里最值得先读的三块内容

资源包里带了设计文档,很多人拿到了文档反而不知道怎么用。我的建议是不要从头到尾通读,而是带着三个问题去翻:系统目标是什么、数据怎么来的、最终方案为什么是 PCA+SVM。这三个问题对应答辩时评委最可能追问的三个点。

系统目标部分会写清楚这个系统解决什么场景,比如用户点评文本的自动情感分类、为酒店评分提供参考,这块内容可以直接抄进你自己的开题报告或摘要。数据描述部分要重点看作者是否交代了正负样本来源和数量,如果你打算换数据集,这部分也是最先要替换的地方。

最值得花时间的是实验结果部分,也就是为什么最后选了 5 号脚本对应的 PCA+SVM 方案。毕设答辩几乎必问「你这个方案和朴素贝叶斯比怎么样、为什么不用深度学习方法」,如果文档里写了对比实验,答辩会轻松很多。如果文档没写,我建议你自己在代码里补一组朴素贝叶斯对照,对比数据是评委认的实打实论据。文档这三块内容的关系整理成一张表会更清楚:

文档章节先读理由答辩用法
系统目标与需求分析交代场景与用户定位直接支撑开题与摘要
数据来源与预处理说明语料构成和清洗方案应对数据来源追问
实验对比与选型结论写清 PCA+SVM 的选型依据支撑「为什么用这个方案」

到这里资源结构已经清晰了,下面一章直接进入环境搭建和首次运行。

3. 跑通是第一目标:Python 环境、目录摆放与一次完整运行

3.1 环境准备:Python 3.8 + jieba + scikit-learn 的具体版本选择

跑这套代码之前先确定 Python 版本。这个项目用到的核心库是 jieba 和 scikit-learn,两个库在 Python 3.8 下稳定性最好。Python 3.10 之后 numpy 做过大版本升级,老项目直接跑容易遇到 sklearn 内部接口的兼容性问题,所以我给的建议是不要用最新的 Python,老老实实建一个 3.8 的虚拟环境,十分钟能省下一下午的排错时间。

conda create -n sentiment python=3.8 -y conda activate sentiment pip install jieba scikit-learn pandas numpy

第一行创建名为 sentiment 的虚拟环境并指定 Python 3.8,第二行激活,第三行安装四个依赖。jieba 负责中文分词,scikit-learn 提供 TfidfVectorizer、PCA、SVC,pandas 和 numpy 负责数据读取和矩阵运算。安装完成后先验证导入是否正常,这一步能同时确认虚拟环境激活成功和依赖安装成功:

python -c "import jieba, sklearn; print('jieba:', jieba.__version__); print('sklearn:', sklearn.__version__)"

如果打印出版本号,环境就算就绪了。很多人在全局环境里装了一堆包结果版本互相打架,所以强烈建议虚拟环境独立跑项目,换机器之后导出 requirements.txt 到新环境安装即可。

3.2 目录组织与数据加载路径的写法

解压之后推荐把项目固定在一个目录里,不要散落在桌面或下载文件夹。目录结构保持和压缩包内一致即可:

hotel_sentiment/ ├── 5_pca_svm.py ├── stopWord.txt ├── neg.36.txt ├── neg.670.txt ├── neg.730.txt ├── pos.318.txt ├── pos.545.txt ├── pos.554.txt ├── pos.709.txt ├── pos.719.txt └── 设计文档.pdf

目录摆好之后先检查一件事:脚本里读文件用的是绝对路径还是相对路径。不少课程设计源码是作者在自己电脑上写死的路径,比如C:/Users/xxx/Desktop/pos.318.txt,换台机器直接 FileNotFoundError。我拿到别人项目的第一改法永远是统一改成基于脚本所在目录的路径:

import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) STOP_WORD_PATH = os.path.join(BASE_DIR, "stopWord.txt") POS_FILES = [os.path.join(BASE_DIR, f) for f in ["pos.318.txt", "pos.545.txt", "pos.554.txt", "pos.709.txt", "pos.719.txt"]] NEG_FILES = [os.path.join(BASE_DIR, f) for f in ["neg.36.txt", "neg.670.txt", "neg.730.txt"]]

os.path.abspath(__file__)取到当前脚本的绝对路径,再用os.path.dirname拿到所在目录,最后用os.path.join拼出完整文件路径。这样做的好处是无论你在 PyCharm、VS Code 还是命令行里运行,路径都不会错,因为这些工具的工作目录经常不一样,裸用相对路径很容易在某一个环境下跑崩。

提示:想快速确认路径没写错,可以在脚本开头加一行print(BASE_DIR),运行时看一眼输出是否符合预期。

3.3 首次运行:观察输出、确认流程闭环

环境装好、路径改好之后,直接运行主脚本:

python 5_pca_svm.py

第一次跑的目标只有一个:看到完整的输出流程,不要中途抛异常。正常情况你会依次看到数据加载条数、分词进度、TF-IDF 特征维度、PCA 降维后的维度,最后是 SVM 的训练准确率和测试准确率。数据量在四千条量级时,分词这一步会花几十秒到一两分钟,期间屏幕没有输出是正常的,不要急着 Ctrl+C 终止。

如果中途报错,按优先级排查三个位置。第一是文件路径有没有写对,报错信息里出现 FileNotFoundError 就去检查 BASE_DIR 的拼接结果;第二是编码问题,出现 UnicodeDecodeError 就去检查 open 的 encoding 参数,先按 utf-8 读,不行再换 gbk;第三是 numpy 版本不兼容,出现 AttributeError 之类就执行pip install "numpy<1.24"降版本。跑通之后别急着看结果,先把整段流程理解了再谈调参,这套「先通后调」的顺序能帮你少走很多弯路。

4. 核心流程拆解:从原始评论到 SVM 分类结果的四层处理

4.1 数据加载与标签构造:读文件、归一化、打标

5_pca_svm.py 的数据加载部分,功能上等价于下面这段代码。把所有 pos 文件读进来打标签 1,所有 neg 文件读进来打标签 0,一条评论对应一个标签,这是文本分类任务的标准组织方式。

def load_data(file_list, label): texts, labels = [], [] for f in file_list: with open(f, "r", encoding="utf-8", errors="ignore") as fp: for line in fp: line = line.strip() if line: texts.append(line) labels.append(label) return texts, labels texts, labels = [], [] texts += load_data(POS_FILES, 1) # 1 表示正面 texts += load_data(NEG_FILES, 0) # 0 表示负面

strip()去掉每行首尾空白,if line过滤空行,这两个细节决定了后续特征质量。如果不过滤空行,向量化时会出现大量空白文档,影响 IDF 计算。errors="ignore"是兜底手段,遇到个别损坏字符会直接忽略而不是崩掉,如果数据编码已经清干净,正式提交时可以把这一项去掉。

这里有个小坑要注意:多个 pos 文件是依次追加进列表的,所以 texts 的顺序和 POS_FILES 列表的顺序绑定。之后如果对数据做抽样或者可视化,保持列表顺序一致才不会让标签错位。

4.2 中文分词与停用词过滤:jieba 的用法与参数

中文语料不能直接被向量化器处理,必须先分词。这个项目用的分词工具大概率是 jieba,配合停用词表做过滤,核心逻辑如下:

import jieba def load_stopwords(path): with open(path, "r", encoding="utf-8") as fp: return set(line.strip() for line in fp) stopwords = load_stopwords(STOP_WORD_PATH) def tokenize(text): words = jieba.lcut(text) return [w for w in words if w.strip() and w not in stopwords and len(w) > 1]

jieba.lcut返回分词后的 list,比生成器版本的jieba.cut更适合直接做列表推导。过滤条件有三层:w.strip()去掉空白分词结果,w not in stopwords去掉停用词,len(w) > 1丢掉单字。单字在情感分析里绝大多数是「啊、呢、哈」这类语气词,但也有例外,比如「好」「差」是单字且情感极强,如果误伤了这两个词会很可惜。

所以我的习惯是把断言词和否定词单独拉一个保留名单,在过滤之后重新加回去。分词模式上,jieba.lcut默认的精确模式最适合情感分析场景,不建议开全模式或搜索引擎模式,那会把句子拆得更碎、噪声更大。想针对酒店领域增强效果,可以用jieba.load_userdict加载自定义词典,把「前台服务」「空调噪音」「性价比超高」这类领域词加进去,让 jieba 不再把它们切成碎片。

4.3 文本向量化与 PCA 降维:TF-IDF 的维度陷阱

分词之后进入向量化环节。情感分析里最常用的特征表达是 TF-IDF,它同时考虑了词频和文档区分度,比纯词频更能突出情感词。这一层的实现长这样:

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.decomposition import PCA vectorizer = TfidfVectorizer( max_features=10000, ngram_range=(1, 2), min_df=2 ) corpus = [" ".join(tokenize(t)) for t in texts] X = vectorizer.fit_transform(corpus) print("原始特征维度:", X.shape[1]) pca = PCA(n_components=0.95) X_pca = pca.fit_transform(X.toarray()) print("降维后维度:", X_pca.shape[1])

max_features=10000把词表规模压到一万以内,防止维度爆炸;ngram_range=(1, 2)同时保留单词和二元词组,像「不/满意」这种组合只有在二元词组形态下才能被模型看见;min_df=2表示一个词至少在 2 条评论里出现过才保留,过滤掉只在一条评论里出现的生僻词。这三个参数是文本分类里最值得调的三个旋钮,直接影响最终准确率。

PCA 这里有个关键前提:它只能处理稠密矩阵,所以必须先X.toarray()。四千条评论、一万维特征,展开成稠密数组大约是 4000×10000 的 float 矩阵,内存占用几百 MB,勉强能跑,但数据量再翻一倍就会崩。n_components=0.95表示保留 95% 的方差贡献,这是最常用的经验值,既压了维度又保留大部分信息。

4.4 SVM 分类与评估:核函数、C 值与准确率

特征降维完成后进入分类器环节。SVM 是文本情感分析里的经典选择,尤其适合样本量几千、特征维度几百的中等规模任务。核心代码如下:

from sklearn.model_selection import train_test_split from sklearn.svm import SVC from sklearn.metrics import accuracy_score, classification_report X_train, X_test, y_train, y_test = train_test_split( X_pca, labels, test_size=0.2, random_state=42, stratify=labels ) clf = SVC(kernel="rbf", C=1.0, gamma="scale", random_state=42) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print("准确率:", accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names=["负面", "正面"]))

三处设置需要解释。stratify=labels是分层抽样,保证训练集和测试集的正负比例与全量数据一致,这个问题在资源结构那章就埋过伏笔,正负 2:1 的不均衡数据如果不分层,切出来的测试集可能全是正面评论,准确率虚高到九成以上但实际没有意义。random_state=42固定随机种子,保证每次运行划分一致,这是复现实验的基本要求,答辩时和评委对结果就靠它。kernel="rbf"配合gamma="scale",表示用高斯核且让 gamma 按特征维度自动缩放。

参数策略上,我一般先把 C 保持默认 1.0 跑通,再在 0.1 到 100 之间做网格搜索。SVM 对特征缩放敏感,但 PCA 输出的各维度方差已经近似一致,所以这里不用额外做 StandardScaler。训练完成后打印 accuracy 和 classification_report,重点看两类各自的 precision、recall、f1,而不是只看一个整体准确率,因为不均衡数据下整体准确率会骗人。

5. 避坑指南:编码、分词、降维与跨机器复现的五个典型问题

5.1 现象:Python 报 UnicodeDecodeError,说 'utf-8' codec can't decode

现象:运行脚本到加载数据直接抛UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6...,程序一秒挂掉。

原因:txt 文件本身不是 UTF-8 编码,大概率是 Windows 默认的 GBK 或 GB2312。不同人用不同工具保存语料,编码各不相同,如果按 utf-8 硬读就会在遇到中文字节时爆掉。

解决:读文件时显式指定编码并加上容错参数,改成open(f, "r", encoding="utf-8", errors="ignore")。如果还是乱码或者报错,先用 chardet 检测文件真实编码再读:

import chardet def detect_encoding(path): with open(path, "rb") as fp: return chardet.detect(fp.read(1024))["encoding"] with open(path, "r", encoding=detect_encoding(path), errors="ignore") as fp: data = fp.read()

注意 chardet 不是标准库,没装的话先执行pip install chardet。检测出编码后,建议一次性把所有 txt 统一转成 UTF-8 保存,后面就不用天天带errors="ignore"兜底了。

5.2 现象:准确率一直在 75% 上下打转,打印分词语料全是「的、了、是」

现象:跑了十遍结果都差不多,准确率上不去。把分词结果打印出来一看,整条评论被拆成「酒店/的/环境/是/真/的/很/差」,停用词占了一大半。

原因:stopWord.txt 没有被加载进过滤流程,或者过滤条件写反了,比如写成w in stopwords而不是w not in stopwords。还有一个隐蔽原因:停用词表里混进了「不、没、太」这类否定词,导致「不满意」被切了第一刀就丢了「不」字,情感直接翻转。

解决:先验证停用词表加载是否生效,打印集合前二十个词看看;再排查过滤条件逻辑;最后检查停用词表内容,把否定词和程度副词从表里摘出来。我一般会在代码里加一段调试输出方便定位:

sample = texts[0] print("原文:", sample) print("分词:", tokenize(sample)) print("停用词命中数:", len(jieba.lcut(sample)) - len(tokenize(sample)))

5.3 现象:PCA 降维后准确率反而比不降维时低了一截

现象:不降维直接跑 SVM 准确率 82%,加上 PCA 降到 100 维之后掉到 74%,越调越差。

原因:PCA 的维度设得不对。两种常见错误:一是把n_components写成一个固定整数比如 100,而数据本身的方差集中在前 30 维,后面 70 维全是噪声,等于给 SVM 喂了额外干扰;二是反过来,固定值设得太小,把可区分信息也扔了。

解决:不要拍脑袋定维度,用累计方差曲线看拐点,保留 90% 到 95% 方差对应的维度:

import matplotlib.pyplot as plt pca_full = PCA().fit(X.toarray()) ratio = pca_full.explained_variance_ratio_.cumsum() for k in [50, 100, 200, 300, 500]: print(f"前 {k} 维累计方差: {ratio[k-1]:.4f}")

如果前 50 维已经累计到 92% 以上,说明不用留到 100 维;如果 300 维还不到 90%,那就要回头把ngram_range或者min_df调一下,数据本身的可分性可能不够。

5.4 现象:训练时X.toarray()直接 MemoryError,内存十几个 G 都不够

现象:跑 PCA 之前执行X.toarray(),内存占用瞬间拉满,接着进程被杀。

原因:TF-IDF 输出的是稀疏矩阵,里面绝大部分是零值。toarray()把它硬展开成稠密矩阵,四千条评论、一万维特征就是四千万格浮点数,内存以 GB 为单位上涨。如果max_features没设上限,直接就是几十 GB 的矩阵。

解决:要么砍特征维度,把max_features降到 3000 到 5000,min_df提到 3;要么干脆不碰稠密矩阵,用支持稀疏输入的TruncatedSVD替代 PCA:

from sklearn.decomposition import TruncatedSVD svd = TruncatedSVD(n_components=200, random_state=42) X_reduced = svd.fit_transform(X) # 不需要 toarray()

TruncatedSVD 在稀疏矩阵上直接做截断奇异值分解,效果和 PCA 在语义上等价但不用展开稠密矩阵,数据量再大两倍也能扛住。这也是换数据后最容易遇到的卡点,建议优先改成这个方案。

5.5 现象:在自己电脑跑通了,答辩演示换一台机器直接 ModuleNotFoundError

现象:昨天还跑得好好的,换了一台电脑运行,提示ModuleNotFoundError: No module named 'jieba',又或者报了路径错误。

原因:新机器没有安装项目依赖,或者脚本里还残留绝对路径。课程设计项目最容易在这个环节翻车,因为作者自己的电脑装了全套环境,从来没验证过干净环境能不能跑。

解决:项目根目录放一个 requirements.txt 把依赖锁住,到新机器上直接一条命令装完:

pip install -r requirements.txt

requirements.txt 至少包含 jieba、scikit-learn、pandas、numpy 四行,建议在你自己验证通过的机器上执行pip freeze > requirements.txt,把本机实际版本锁进去,不要手写空版本号。同时把脚本里所有文件读取路径统一改成基于__file__的相对定位,也就是第 3 章写的那种写法。我在答辩前通常会把整个项目拷到一台干净的虚拟机里从零跑一遍,跑通了才敢上台。

6. 把系统变成自己的:新语料替换、结果验证与模型保存

6.1 替换成自己爬的评论数据

毕设答辩时评委最常问的一句话是「这个系统能不能处理其他场景的文本」。要让回答有底气,最好把语料换掉实测一次。替换步骤很简单:把新评论整理成纯文本格式,每行一条,正面文件加进 POS_FILES 列表,负面文件加进 NEG_FILES 列表,每类保持几百条以上的量级,再跑一遍脚本。注意先去掉爬来的 HTML 标签、表情符号和 URL,保持评论纯净度。

6.2 用混淆矩阵做量化验证

调参阶段别只看 accuracy 一个数字,把混淆矩阵打印出来看两类各自的错误分布。负面评论被误判成正面的比例,直接影响实际后果。classification_report 里 recall 低的那一类就是当前模型的主要短板,针对它补语料或调词权重,比盲目调 SVM 的 C 值有效得多。

6.3 用 joblib 保存模型和向量化器

训练好之后保存模型,让系统能对新评论做单条预测,而不是每次重新训练。保存和加载的代码如下:

import joblib joblib.dump(clf, "svm_model.pkl") joblib.dump(vectorizer, "tfidf_vectorizer.pkl") def predict_single(text, model, vec, reducer): words = " ".join(tokenize(text)) x = vec.transform([words]) x = reducer.transform(x) # TruncatedSVD 支持稀疏输入;若用 PCA 先 .toarray() return model.predict(x)[0]

保存时一定要把向量化器和降维器一起存下来,测试时对单条评论走完全相同的「分词 → 向量化 → 降维 → 分类」链路,少存任何一个组件都会导致维度对不上报错。

我自己的血泪教训是第一次做类似项目时,代码里写死了本地绝对路径,结果答辩前夜在虚拟机上一跑就崩,半夜调了两个小时才发现是路径问题。从那以后我每次拿到别人的源码,都先强制走一遍「改路径 → 建虚拟环境 → 从零跑通 → 再读代码」的流程。这套源码配合设计文档和语料,按这个顺序操作是可以踏实交付的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询