简介面向计算机相关专业毕业设计场景的Python实现酒店中文评论情感分析项目资源采用基于机器学习的情感极性分类路线通过训练标注语料构建分类模型对酒店评论文本进行正向/负向情感判别适合需要完成情感分析类课题或入门中文NLP实践的本科毕业生及开发者。资源包内容完整涵盖设计报告Word、Python源码、原始评论数据与处理后的语料数据共2000余个文件以txt评论文本及切分词料为主配以5个Python脚本覆盖分词、去停用词、词向量、PCA降维与SVM分类等环节、1个CSV数据文件及1份设计报告压缩包约7.47MB。项目流程清晰包含2000条正向与2000条负向酒店评论的原始文本及分词结果读者可对照源码完整复现从文本清洗、句子切分、停用词过滤、词向量获取到特征降维与情感分类的整套实践链路。目前已有951人学习下载适合作为毕业设计参考或中文情感分析入门的动手实践资料。1. 用机器学习把酒店评论变成情感类别这个毕业设计到底做了什么这个毕业设计做了一件很直接的事拿 2000 条正面酒店评论和 2000 条负面酒店评论用 Python 和机器学习训练一个情感分类模型让之后输入的任意中文酒店评论都能被自动判成好评或者差评。项目包含五个按顺序执行的 Python 源文件1_process、2_cutsentence、3_stopword、4_getwordvecs、5_pca_svm以及对应的原始文本和处理文本完整展示了中文评论情感分类的落地链路。对正在准备毕业设计的本科生而言这套脚本回答了“文本如何变成数字”“模型如何被训练”这两个核心问题对想快速验证情感分析 baseline 的从业者而言把脚本跑一遍再拿自己的数据替换掉评论文件你就有了一台能复用的情感分类流水线。2. 从原始评论文本到干净的训练集数据清洗、jieba 分词与停用词过滤的完整流程2.1 正负评论数据怎么标注文件是怎么组织的项目自带四份文本2000_pos.txt、2000_neg.txt 是原始评论2000_pos_cut.txt、2000_neg_cut.txt 是分词处理后的结果。命名里的 2000 指样本数量pos 和 neg 分别代表情感极性为正好评与负差评。文本按每行一条评论组织用 UTF-8 编码保存没有 CSV 表头、没有额外字段这是典型的“零依赖”数据格式读取简单但前提是脚本里必须明确指定 encoding。我拿到这类数据第一件事不是写代码而是先打开文件看 20 条。重点看三样东西评论里有没有夹杂评分数字有没有带“携程”“美团”等平台名甚至客服 ID有没有同一句话重复出现。这些都会直接影响后面清洗规则怎么写。项目里的 1_process.py 正是用来处理这些噪声的所以你最好在动脚本之前就清楚自己手里的噪声长什么样。2.2 1_process.py文本清洗的逻辑和代码骨架# 1_process.py —— 数据清洗与格式化输出处理后的通用文本文件 import re def clean_comment(text: str) - str: # 去掉文本里残留的超链接防止“http://...”被当成一个词参与特征统计 text re.sub(rhttp\S, , text) # 去掉 用户 这种提醒内容酒店评论里常见“携程客服”等 text re.sub(r\w, , text) # 去掉纯数字。注意这里会把“5分好评”变成“分好评”项目里把数字视为非情感信息 text re.sub(r\d, , text) # 多个空格压缩成一个方便后续按空格分词 text re.sub(r\s, , text) return text.strip() def process_file(src: str, dst: str) - None: # 统一使用 utf-8 读写避免 Windows 下 gbk 编码互相踩坑 with open(src, r, encodingutf-8) as f: lines f.readlines() cleaned [clean_comment(line) for line in lines if line.strip()] with open(dst, w, encodingutf-8) as f: f.write(\n.join(cleaned)) if __name__ __main__: process_file(2000_pos.txt, 2000_pos_clean.txt) process_file(2000_neg.txt, 2000_neg_clean.txt)这里把清洗分成四类规则链接、提醒、数字、空白。每条规则都用正则实现好处是规则集中在同一个函数里后面想加“去掉英文”“去掉重复标点”时只需要再加一个 re.sub 的事。注意第二条re.sub(r\d, , text)是有争议的酒店评论里的“5分好评”“住了3晚”这类数字删掉之后其实丢失了部分语境。我一般会先跑一版删数字的再跑一版保留数字的对比准确率让实验决定取舍而不是拍脑袋。这个项目选择了删数字说明当时数据里数字噪声大于信息量你换数据时最好也自己验证一次别直接照抄规则。2.3 2_cutsentence.py 与 3_stopword.py分词和去停用词的顺序为什么不能反# 2_cutsentence.py —— 用 jieba 做中文分词按空格拼接 import jieba def cut_line(line: str) - str: # lcut 返回 list相比 cut 的生成器更方便判断和拼接 words jieba.lcut(line.strip()) return .join(words) def cut_file(src: str, dst: str) - None: with open(src, r, encodingutf-8) as f: lines f.readlines() with open(dst, w, encodingutf-8) as f: for line in lines: if line.strip(): f.write(cut_line(line) \n) if __name__ __main__: cut_file(2000_pos_clean.txt, 2000_pos_cut.txt) cut_file(2000_neg_clean.txt, 2000_neg_cut.txt)分词这一步的核心参数是 jieba.lcut 的默认精确模式。项目里没有额外加载自定义词典也没开 HMM 开关这意味着酒店名、地名、服务人员名这类专有名词是按默认词库切分的。比如“如家酒店”可能被切成“如家/酒店”问题不大但“前台小姐姐”可能变成“前台/小/姐姐”这就直接影响情感词“小姐姐”的完整性。如果你发现切出来的词大量是单字优先考虑的是给 jieba 加载酒店领域词典把品牌名、常见房型词、服务词先固定住而不是去调 HMM。常见做法是在 cut_file 里加一行jieba.load_userdict(hotel_dict.txt)。# 3_stopword.py —— 加载停用词表过滤掉对情感极性没有贡献的词 stopwords set() def load_stopwords(path: str) - None: # 常见停用词表是每个词占一行读进来去掉换行 with open(path, r, encodingutf-8) as f: for line in f: w line.strip() if w: stopwords.add(w) def remove_stopwords(src: str, dst: str) - None: with open(src, r, encodingutf-8) as f: lines f.readlines() with open(dst, w, encodingutf-8) as f: for line in lines: kept [w for w in line.split() if w not in stopwords] f.write( .join(kept) \n) if __name__ __main__: load_stopwords(stopwords.txt) remove_stopwords(2000_pos_cut.txt, 2000_pos_processed.txt) remove_stopwords(2000_neg_cut.txt, 2000_neg_processed.txt)停用词表里常见的词有的、了、是、在、和、也、都、而、及、跟、很、太、最、更还会把标点符号和语气词一并纳入。为什么一定是“先分词后去停用词”因为中文没有天然空格分隔只有先通过 jieba 把“酒店/很/干净”切出来才能判断哪个词是停用词。如果反过来做程序只能按字符一个个判断得到的是“酒、店、很、干、净”特征维度变成单字情感信息碎得没法用。我第一次跑这个流程时就把顺序写反了SVM 准确率只有 56%跟抛硬币差不多。提示如果你发现 pos_cut 和 neg_cut 文件第一行和原文件对不上大概率是清洗阶段用 strip 后丢掉了行首空白这不是 bug是预期行为不影响后续建模。3. 特征工程与 SVM 分类五个脚本如何从头到尾跑通3.1 从句子到向量4_getwordvecs.py 的词袋与 TF-IDF 特征到这一步我们有的是“分词去停用词后的句子”。SVM 不能直接吃文本必须把每句话转成一个固定长度的数值向量。这就是 4_getwordvecs.py 干的事。项目采用的是词袋Bag of Words思路先统计所有训练句子里出现过的词构成一个词表然后每个句子用一个长度等于词表大小的向量表示句子中出现某个词就把对应位置计为 1 或词频。更进一步的改良是 TF-IDF词频 × 逆文档频率。“大酒店”“很好”这类在绝大多数句子里都出现的词会被降权而“隔音差”“霉味”这类只在特定负面评论里出现的词会被加重。# 4_getwordvecs.py —— 训练词表并生成特征向量 from sklearn.feature_extraction.text import CountVectorizer def build_and_fit(): # 读取处理后的正负样本合并成语料 with open(2000_pos_processed.txt, r, encodingutf-8) as f: pos_lines f.readlines() with open(2000_neg_processed.txt, r, encodingutf-8) as f: neg_lines f.readlines() corpus pos_lines neg_lines # min_df2 表示一个词至少在 2 条评论里出现才进入词表过滤只出现一次的噪声词 vectorizer CountVectorizer(min_df2, token_patternr[\u4e00-\u9fa5], max_features5000) X vectorizer.fit_transform(corpus) print(词表大小:, len(vectorizer.vocabulary_)) print(特征矩阵 shape:, X.shape) return vectorizer, X if __name__ __main__: build_and_fit()这里有两个参数值得说明。min_df2是防止单篇评论里出现的生造词把词表撑爆。2000 加 2000 条评论如果不限制词表能到一两万词限制后通常剩几千。token_patternr[\u4e00-\u9fa5]只保留中文汉字组成的词这个正则把英文、数字、符号都挡在外面因为清洗阶段已经去掉了大部分数字这里再挡一道是为了避免“ok”“服务ba”之类的杂讯进入词表。我在实际操作里会再顺手加一个max_features5000的上限让词表别超过 5000否则第 4 步 PCA 和高维 SVM 都会变得很慢。另外4_getwordvecs.py 在生成特征之后正负样本会各自保存最终喂给 5_pca_svm.py 的结构是“[4000, 词表长度] 的特征矩阵”加“[4000, 1] 的标签向量”。如果你手工混排了样本顺序请务必同步改标签否则模型就学反了。3.2 特征向量到预测结果5_pca_svm.py 的 PCA SVM# 5_pca_svm.py —— PCA 降维 SVM 分类器 from sklearn.decomposition import PCA from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, accuracy_score # 假设 X 是上一步得到的特征矩阵y 是对应的 0/1 标签 X, y load_feature_and_labels() X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # PCA把高维词袋特征压到保留 95% 方差所需的维度 pca PCA(n_components0.95, random_state42) X_train_pca pca.fit_transform(X_train.toarray()) if hasattr(X_train, toarray) else pca.fit_transform(X_train) X_test_pca pca.transform(X_test) # SVMRBF 核C1.0gammascale clf SVC(kernelrbf, C1.0, gammascale, random_state42, probabilityFalse) clf.fit(X_train_pca, y_train) y_pred clf.predict(X_test_pca) print(准确率:, accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred))这段代码的要点在三个参数上。PCA(n_components0.95)表示保留原始方差信息的 95%PCA 会自动算出需要的维度数。一般几千维的词袋特征95% 方差会落在 100 到 300 维之间。SVC(kernelrbf, C1.0, gammascale)是 sklearn 的默认推荐组合RBF 核适合这种特征维度不高、样本量不大的文本分类C 是误分类惩罚系数C 越大越不肯放过错误样本越容易过拟合gammascale 会根据输入维度自动缩放比手写 gamma0.001 更适合新手起步。test_size0.2, stratifyy是必须的正负样本各 2000 条如果不分层抽样测试集里可能出现 550 负 450 正的比例导致准确率表面正常、实际类别偏向。3.3 为什么要用 PCA SVM而不是朴素贝叶斯或深度学习这是设计报告里一定需要解释的问题也是一开始最容易回答不清的问题。我按这个项目的实际环境来说。样本量只有 4000 条深度学习至少要上万条才可能体现优势跑起来还要装 PyTorch 或 TensorFlow对课设环境的显卡和内存都不友好。朴素贝叶斯适合高维稀疏文本但它是线性模型对“虽然…但是…”这种转折结构基本无能为力。SVM 用 RBF 核能把样本映射到更高维空间对情感词分布重叠的情况更稳。PCA 在这里不是必须的但它能把几千维的词袋特征压成两三百维大大缩短 SVM 训练时间同时“先降维再分类”这个流程写在报告里比直接丢进 SVM 更容易说清楚特征工程的必要性。这套选型逻辑放在今天自然是偏传统的但在课程设计、毕业设计尺度上仍然成立。它的可解释性比深度模型强很多你把 PCA 后的前两个主成分画出来能直接看到正负样本在两个方向上的分离趋势这是答辩时非常好用的展示材料。这里还要补充一点PCA 只能用训练集的数据去 fit再对测试集做 transform绝对不能在 fit 之前把训练测试合并在一起做 PCA那样会发生信息泄漏测试集的评价分数会虚高。代码里先pca.fit_transform(X_train)再pca.transform(X_test)的顺序不是随意的。4. PCA 降维与 SVM 参数设置我的参数选择与调参记录4.1 PCA 降维的维度怎么定维度选择有两种路径。固定维度法写n_components200先跑通再改方差保真法写n_components0.9/0.95/0.99让 PCA 自动决定维度。n_components得到维度范围训练时间效果说明0.980~120 维快有一定信息损失准确率可能掉 1 到 2 个点0.95150~250 维中常见默认信息损失可控0.99300~500 维慢接近原始特征SVM 训练时间明显变长我自己的做法是先打印保留方差曲线找到拐点。sklearn 里用pca.explained_variance_ratio_算累计方差看前 200 个主成分累计贡献到哪个位置。如果 150 维已经贡献了 96% 方差就定 150如果 500 维还在 90%说明原始特征本来就散盲目降维会把有效信息丢掉。还有一个值得留意的细节PCA 对稀疏矩阵要先.toarray()转成稠密矩阵不然会报类型错误。2000 词表规模只有 4000 行 5000 列转稠密后也就是 4000 乘 5000 的 float64 矩阵内存大概 160MB可以接受。但如果词表放大到几万稠密矩阵会直接吃掉几个 G 内存这时候要么减少 max_features要么改用 TruncatedSVD 这类适合稀疏矩阵的降维方法。这个项目的规模不大所以 PCA 完全够用。4.2 SVM 核函数与 C、gamma 参数设置RBF 核有两个核心参数。C是误分类惩罚C1 起步然后试 0.1、1、10、100。gamma是 RBF 核的半径gamma 越大决策边界越复杂越容易过拟合用 scale 自动值起步再试 0.001、0.01、0.1。from sklearn.model_selection import GridSearchCV param_grid { C: [0.1, 1, 10], gamma: [0.001, 0.01, 0.1, scale] } grid GridSearchCV( SVC(kernelrbf, random_state42), param_grid, cv5, # 5 折交叉验证 scoringf1, # 类别不平衡时比 accuracy 更靠谱 n_jobs-1 ) grid.fit(X_train_pca, y_train) print(最优参数:, grid.best_params_) print(最优分数:, grid.best_score_)跑完 GridSearchCV 之后看best_params_。调参结论通常落在 C1 或 C10、gamma0.01 或 0.1 上。如果最优参数是 C100、gamma1基本说明特征维度压得不够或者数据里噪声太多这时候先回去检查清洗和 PCA别急着在这一组参数上收手。交叉验证的折数也需要说明cv5 表示把训练集分成 5 份轮流用 4 份训练、1 份验证最终取平均分。4000 条样本、5 折交叉验证每折验证集是 800 条这个规模对 SVM 来说很轻松。如果你把 cv 改成 10训练时间会长一些指标略有起伏但结论不会差太多。4.3 评估结果解读准确率、精确率、召回率、F1 的区别SVM 输出classification_report之后很容易只盯着 accuracy。对于酒店评论一条“把差评判成好评”和“把好评判成差评”的代价不一样差评被判成好评会让酒店误以为口碑很好等于放过了服务危机。所以必须有精确率和召回率的视角。精确率是预测为“好评”的样本里真正是好评的比例衡量“模型说好靠不靠谱”。召回率是真实好评里被模型识别出的比例衡量“模型会不会漏掉好评”。F1 是两者的调和平均调参时用 F1 比用准确率更能反映模型在两极上的平衡。一个比较健康的项目结果是准确率 88% 到 92%正负两个类别的精确率和召回率都接近没有出现“正类召回 95%、负类召回 62%”这种偏科。如果出现偏科先怀疑数据本身负样本里是不是混了大量中性评论比如“位置一般离地铁有点远但还能接受”这种句子在标注时容易被判成负面但算法看它其实很中性。5. 情感分析实战避坑编码问题、分词干扰与模型过拟合的典型排查5.1 打开 txt 全是乱码脚本却跑得通现象2000_pos.txt 在 Windows 记事本里打开是乱码但脚本运行不报错。原因文件实际是 UTF-8 编码Windows 记事本默认用 GBK 打开。脚本里encodingutf-8读对了所以不报错。真正危险的是你手动把文件“另存为 ANSI”后脚本反而读不进去了。解决所有数据文件统一用 UTF-8 无 BOM 保存脚本里所有open()都显式传encodingutf-8不要用不传 encoding 的默认写法。处理中文语料这一步不写清楚必出坑。5.2 加了停用词表准确率反而下降现象3_stopword.py 把停用词表从 120 词加到 300 词准确率掉了 3 个点。原因停用词表太过激进把“不”“没”“别”这类否定词也加进去了。“酒店位置不错”去掉“不错”后只剩“酒店位置”把否定词去掉更是灾难“很不满意”被过滤成“很满意”。解决停用词表只加虚词、语气词、助词和标点。所有带情感倾向的副词不、太、很先保留。表面看“很”不算情感词但“很差”“很满意”里的“很”是程度修饰去掉后情感强度打折。我现在的习惯是否定词无论如何不进停用词表甚至单独拿出来当特征。5.3 训练集准确率 97%测试集只有 74%现象训练集上 SVM 表现很好测试集上一塌糊涂。原因这是典型的过拟合。常见来源有三个词袋特征没有限制词表太大模型记死了训练样本C 设得太大模型为了少错一两个训练样本把边界画得很曲折PCA 保留太多方差比如 0.995把大量噪声也保留了。解决先看词表大小把min_df从 1 提高到 3 或 5再把 C 拉回 0.1 到 10 之间PCA 用 n_components0.95 重新跑。如果仍然过拟合SVM 里开class_weightbalanced也是候选方案但归根结底要先控制特征维度。5.4 预测新评论时模型报维度不匹配现象训练跑完保存了模型load 回来打算预测一条新评论clf.predict(X_new)直接报 ValueError说特征数量不一致。原因预测时的新评论只做了清洗和分词没有经过 vectorizer 转换。sklearn 的 CountVectorizer 必须用训练时 fit 出来的同一个对象转换新文本否则特征矩阵的列对不上。你重新 new 一个 CountVectorizer 再去 transform得到的词表是空的维度当然不匹配。解决训练时把 vectorizer、pca 和 svm 一起保存成一个对象import joblib joblib.dump({vectorizer: vectorizer, pca: pca, svm: clf}, model_bundle.pkl)预测时加载这同一个 bundle先 vectorizer.transform、再 pca.transform、最后 svm.predict顺序不能乱。5.5 正 2000 负 2000 看起来平衡结果还是偏向好评现象样本量完全平衡但测试结果差评的召回率只有 60%。原因酒店评论的“较差”往往写在负面数据里但这些评论可能含有很多正面词比如“早餐好吃但隔音太差”。模型被前面的正面词带偏了。解决这不是样本平衡问题是文本里的转折结构问题。可以先做粗粒度抽取把“但是”“不过”“然而”后面的从句截出来单独跑一个 SVM 或规则模型再看整句。这是本科毕设的进阶点了拿来做“未来展望”很合适。数据侧也可以做一个小动作把负面评论里含转折词但整体负面的样本抽出来人工确认标签是否真的负面很多时候会发现标注阶段就标错了。6. 给训练好的模型写一个端到端预测入口单条新评论的两分钟验证法在答辩和真实使用场景里考官并不总是关心训练准确率而是直接开口问“给我一条新评论你怎么知道它是好评还是差评”所以我习惯在项目里加一个 predict 函数把清洗、分词、去停用词、向量化、PCA、SVM 全部串起来。6.1 把分词、停用词、向量化、降维和 SVM 串成一个预测函数# predict_comment.py —— 新评论的完整推理链路 import re import joblib import jieba bundle joblib.load(model_bundle.pkl) vectorizer, pca, clf bundle[vectorizer], bundle[pca], bundle[svm] stopwords set() with open(stopwords.txt, r, encodingutf-8) as f: stopwords.update(line.strip() for line in f if line.strip()) def predict_comment(text: str) - int: # 1. 清洗 text re.sub(rhttp\S|\w|\d, , text) # 2. 分词 words jieba.lcut(text.strip()) # 3. 去停用词 kept [w for w in words if w not in stopwords] # 4. 向量化必须用训练时同一个 vectorizer x vectorizer.transform([ .join(kept)]) # 5. PCA 降维 x_pca pca.transform(x.toarray()) if hasattr(x, toarray) else pca.transform(x) # 6. 模型预测 return int(clf.predict(x_pca)[0]) if __name__ __main__: sample 房间很干净前台服务也很好下次还来 print(predict_comment(sample))这里最值得记住的习惯是清洗正则必须和训练脚本保持一致否则“5分好评”在训练时被删掉数字、预测时没删整个词频分布就对不上了。换数据、换停用词表之后一定要跑一遍回归测试用训练集里抽出来的 20 条句子重新预测确定链路没有折断。6.2 顺着预测入口往前走最少改动让它变成一个可复用服务把接口补一层之后这套毕设就能很自然地从“跑通代码”升级成“可复用模型”。如果时间允许还可以把单条预测封装成一个最小 Flask 接口输入一段评语、输出好评或差评就完整展示了生产可用性。多模态情感分析是当前的热门方向但在这个项目里先把文本模态的准确率和可解释性做好已经足够在答辩时站住脚。我第一次做这个项目的时候就是绕过了 predict 这一步只盯训练准确率结果答辩被问到“如何预测新评论”直接卡壳。从那以后我每写完一套分类流程都会强制自己补上这条端到端的预测链路并把 vectorizer、pca、svm 三个对象打包保存成 joblib。如果你也准备把这个项目当成毕设主线我建议你至少先走通这一步希望帮到你。本文还有配套的精品资源点击获取