简介这是一套面向计算机相关专业学生与项目实战学习者的Python酒店评论情感分析系统可直接用于毕业设计、课程设计或期末大作业。项目以中文酒店评论为数据对象完成从文本预处理、情感分类到可视化展示的完整流程适合具备一定Python基础、希望积累NLP实战经验的学习者。压缩包共43个文件约56.55MB包含6个py源码文件、1个ui界面文件、1个h5模型权重、1个csv测试数据、1个json配置及20张png与3张jpg效果图另附字体、图标、qrc资源与使用说明覆盖训练、测试、界面交互等模块。目前已有291人学习下载。项目经过严格调试可直接运行读者能获得完整源码、项目说明与界面演示素材便于快速理解情感分析系统的实现思路、复现运行结果并在此基础上二次开发或撰写论文。1. 酒店评论情感分析从一堆中文差评里挖出可落地的运营信号做酒店运营的朋友给我看过一份后台导出两万三千条评论评分从 1 到 5 星混在一起运营团队每周靠人工翻前两百条来猜「客人到底在骂什么」。这个量级下人工抽样基本等于玄学翻到的往往是情绪最激烈的那几条真正高频但语气平淡的抱怨反而被淹没。基于 Python 的酒店评论情感分析要解决的就是这件事把非结构化的中文评论文本批量转成可统计、可分组、可追踪的极性标签再按房型、门店、时间维度聚合出运营能直接用的结论。它适合两类人一类是想拿一个完整 NLP 小项目练手的 Python 学习者另一类是真的要处理评论数据的运营或数据分析岗。整条链路不复杂——读取、清洗、分词、向量化、建模、评估、聚合但每一步都有中文场景特有的坑下面按能复现的顺序拆开讲。2. 数据从哪来、标签怎么定先把输入和口径钉死2.1 评论数据的三种来源与取舍酒店评论数据的获取方式直接决定后面清洗的工作量。常见做法有三种我一般按下面的优先级选来源典型形态优点代价平台开放接口/合作数据JSON带评分字段结构干净评分可直接当弱标签需要权限字段口径不统一公开数据集CSV/TXT已脱敏拿来即用适合练手领域偏通用酒店特征弱自采页面文本HTML 抓取后解析贴合真实业务反爬、编码、噪声都要自己扛如果目标是跑通方法公开数据集足够如果目标是给某家酒店做分析自采或接口数据才有业务价值。这里要提醒一句抓取要遵守目标站点的 robots 协议和相关规定只取公开可见内容控制频率别把人家服务打挂。评分字段是天然的弱标签。行业里普遍把 4~5 星当正面、1~2 星当负面、3 星丢弃或单列中性因为 3 星评论经常是「还行但有槽点」混进任何一类都会污染边界。丢弃 3 星会损失样本但换来的是更干净的决策边界新手项目里这个取舍很划算。2.2 用 pandas 读入并做第一轮体检拿到数据先别急着建模先看清分布。下面这段是标准的第一轮体检脚本import pandas as pd # 读入注意中文编码常见 utf-8 / gbk / utf-8-sig df pd.read_csv(hotel_reviews.csv, encodingutf-8-sig) # 统一列名避免后续到处改 df df.rename(columns{content: text, score: rating}) # 只保留文本和评分两列去掉空文本 df df[[text, rating]].dropna(subset[text]) df[text] df[text].astype(str).str.strip() df df[df[text].str.len() 5] # 太短的评论没有分析价值 # 评分分布看类别是否失衡 print(df[rating].value_counts().sort_index()) # 按规则打弱标签4-5 正面1-2 负面3 星丢弃 df df[df[rating] ! 3].copy() df[label] (df[rating] 4).astype(int) # 1 正面0 负面 print(df[label].value_counts(normalizeTrue))逻辑说明encodingutf-8-sig是为了处理带 BOM 的导出文件用utf-8读会出现首列列名带隐藏字符的诡异问题。str.len() 5过滤掉「好」「差」这种单词评论它们对模型只有噪声没有信息。最后一步把评分映射成 0/1astype(int)把布尔转成整数标签方便后续 sklearn 直接吃。参数说明长度阈值 5 是我在中文短评上的经验值英文场景可以放到 3如果负面样本占比低于 15%后面训练要加class_weightbalanced否则模型会倾向于全预测正面准确率看着高但召回惨不忍睹。2.3 中文清洗正则要清掉什么、保留什么中文评论的噪声和英文完全不同重点是表情符号、连续标点、无意义重复字符和夹杂的英文/数字。清洗原则是「去掉不承载情感的符号保留可能承载情感的标点和否定词」。import re def clean_text(s): s re.sub(rhttp\S|www\.\S, , s) # 去链接 s re.sub(r\[.*?\], , s) # 去 [表情] 这类占位 s re.sub(r[\U0001F300-\U0001FAFF], , s) # 去 emoji 码位 s re.sub(r(.)\1{2,}, r\1\1, s) # 连续重复压成两个 s re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、], , s) # 只留中文/字母/数字/常用标点 s re.sub(r\s, , s).strip() return s df[clean] df[text].apply(clean_text) print(df[[text, clean]].head(5))逻辑说明(.)\1{2,}把「好好好好好」压成「好好」保留两个是因为叠词在中文里有时表程度「慢慢」全压成一个会丢信息。最后一条正则用白名单方式保留中文、字母、数字和几个关键标点感叹号和问号对情感强度有指示作用别一刀切掉。参数说明\U0001F300-\U0001FAFF覆盖了绝大多数 emoji 码位但新版 emoji 可能超出范围如果发现残留可以扩到\U0001FAFF以上。清洗后建议再跑一次长度分布如果平均长度骤降说明正则误伤了正常文本要回头检查白名单。3. 从分词到向量化中文情感分析的特征工程怎么做3.1 jieba 分词与停用词表的正确用法中文没有空格分词是绕不开的一步。jieba 是入门首选速度快、词典可扩展。酒店领域有一批高频专有词比如「前台」「隔音」「早餐」「退房」默认词典可能切错需要自定义。import jieba # 加载领域词典每行一个词 jieba.load_userdict(hotel_dict.txt) # 停用词表一行一个 with open(stopwords.txt, encodingutf-8) as f: stopwords set(line.strip() for line in f) def tokenize(s): words jieba.lcut(s) # 过滤停用词、单字、纯数字 return [w for w in words if w not in stopwords and len(w) 1 and not w.isdigit()] df[tokens] df[clean].apply(tokenize) print(df[tokens].head(3))逻辑说明load_userdict要在第一次lcut之前调用否则不生效这是新手最常踩的顺序坑。过滤单字是因为中文单字歧义太大「好」和「不好」里的「好」含义相反留着反而干扰。停用词表要包含「的」「了」「是」这类虚词但不要把「不」「没」「别」加进去它们是否定词直接决定情感极性。参数说明len(w) 1这个阈值对酒店评论合适但如果你的数据里「脏」「吵」「贵」这类单字评价很多就得放宽到 1并单独处理否定词。停用词表建议用哈工大或百度停用词表做底再手动补充领域词。3.2 TF-IDF 与词袋小数据量下的稳妥选择向量化有两条路词袋/TF-IDF 和词向量。酒店评论这种任务样本量通常几千到几万TF-IDF 加线性模型的组合往往比词向量加深度模型更稳训练快、可解释、不容易过拟合。from sklearn.feature_extraction.text import TfidfVectorizer # 把 token 列表拼回空格分隔的字符串TF-IDF 需要字符串输入 df[seg] df[tokens].apply(lambda x: .join(x)) tfidf TfidfVectorizer( max_features20000, # 控制维度防止稀疏爆炸 ngram_range(1, 2), # 加入二元词组捕捉「不 干净」这类否定搭配 min_df3, # 出现少于 3 次的词丢弃 max_df0.9 # 出现在 90% 以上文档的词丢弃多为无信息高频词 ) X tfidf.fit_transform(df[seg]) y df[label].values print(X.shape)逻辑说明ngram_range(1, 2)是关键。中文里「不干净」被分词切成「不」和「干净」两个词单看「干净」是正面词模型会判错加入二元组后「不 干净」作为一个特征出现极性就被正确捕捉。这是中文情感分析里提升最明显的一个参数。参数说明max_features20000是维度和效果的平衡点样本少于 5000 时可以降到 10000。min_df3过滤低频噪声但如果你的数据本身很小比如只有几百条要降到 1 或 2否则大量词被丢掉。max_df0.9去掉几乎每篇都出现的词注意别设太低否则会把「酒店」这种领域核心词也误删。3.3 训练集测试集划分与基线模型划分要分层抽样保证训练集和测试集的类别比例一致否则评估结果会飘。from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) clf LogisticRegression(max_iter1000, class_weightbalanced) clf.fit(X_train, y_train) pred clf.predict(X_test) print(classification_report(y_test, pred, target_names[负面, 正面]))逻辑说明stratifyy保证分层random_state42保证可复现这两个参数在项目里必须写死不然每次跑结果都不一样没法对比调优。class_weightbalanced自动按类别频率加权处理正负样本不均衡。参数说明max_iter1000是因为 TF-IDF 特征维度高默认的 100 次迭代经常不收敛会报ConvergenceWarning。逻辑回归在这个任务上通常能到 0.85 以上的 F1如果明显低于这个数先回去查清洗和分词而不是急着换模型。4. 模型选型与调参什么时候该换、换什么4.1 线性模型、树模型、深度模型的边界同一个任务上不同模型的适用场景差别很大选错方向会白费很多时间。模型适用样本量训练速度可解释性酒店评论场景建议逻辑回归 TF-IDF1k~50k秒级强可看权重首选基线朴素贝叶斯1k~50k秒级中快速验证效果略低LightGBM10k分钟级中特征多时尝试BERT 类预训练5k小时级GPU弱追求极致效果时上我的习惯是先用逻辑回归跑出基线记下 F1再决定要不要投入上深度模型。很多酒店评论任务里逻辑回归和 BERT 的差距只有两三个点但训练成本和部署复杂度差一个量级。如果业务只要「大致分个正负」线性模型完全够用。4.2 用网格搜索找关键参数逻辑回归真正值得调的参数就两个正则强度和惩罚类型。用 GridSearchCV 一次跑完。from sklearn.model_selection import GridSearchCV param_grid { C: [0.1, 1, 10], # 正则强度越小正则越强 penalty: [l2], # l1 在稀疏高维下收敛慢先用 l2 solver: [liblinear, lbfgs] } grid GridSearchCV( LogisticRegression(max_iter1000, class_weightbalanced), param_grid, cv5, scoringf1, n_jobs-1 ) grid.fit(X_train, y_train) print(grid.best_params_, grid.best_score_)逻辑说明scoringf1而不是默认的 accuracy因为类别不均衡时 accuracy 会骗人。cv5做五折交叉验证比单次划分更稳。n_jobs-1用满所有 CPU 核。参数说明C越小正则越强欠拟合风险上升越大越容易过拟合。酒店评论数据上C1通常是个好起点。solver选liblinear适合小数据lbfgs适合大数据两者结果接近时可以按速度选。4.3 用错误样本反推特征问题模型跑完别只看指标把分错的样本捞出来看这是提升效果最快的路径。import numpy as np # 找出测试集里预测错误的样本 mis_idx np.where(pred ! y_test)[0] for i in mis_idx[:10]: print(原文:, df.iloc[i][clean][:80]) print(真实:, y_test[i], 预测:, pred[i]) print(- * 40)逻辑说明这段代码把错分样本的原文打出来人工扫一遍就能发现规律。常见模式有三类一是反讽「真是太好了空调坏了三天」二是混合情感「房间干净但隔音差」三是领域词未登录新出现的房型名被切碎。反讽和混合情感是线性模型的天然短板看到大量这类错误就该考虑上深度模型或做句子级拆分。参数说明mis_idx[:10]先看十条如果错误集中在某类模式再针对性统计。这一步不需要调参纯粹是诊断但往往比盲目调参有效得多。5. 避坑与排查中文酒店评论里最容易翻车的五件事5.1 否定词被分词切碎导致极性反转现象模型把「不干净」「没热水」判成正面F1 在负面类上明显偏低。原因jieba 把「不干净」切成「不」和「干净」TF-IDF 只看到「干净」这个正面词否定信息丢失。解决在TfidfVectorizer里开ngram_range(1, 2)让「不 干净」作为二元特征进入同时在自定义词典里把「不干净」「没热水」这类高频否定搭配加进去让分词不拆开它们。5.2 类别不均衡导致模型全预测多数类现象正面样本占 85%模型准确率 0.85但负面样本召回接近 0业务上完全没用。原因默认损失函数对多数类友好模型学到「全猜正面」就能拿高准确率。解决训练时加class_weightbalanced评估时看 F1 和各类的 recall 而不是 accuracy。如果负面样本实在太少考虑对负面类做上采样或用imblearn的 SMOTE但文本数据上采样要谨慎容易过拟合。5.3 停用词表误删否定词现象清洗后「不」「没」「别」全部消失模型对否定完全无感。原因直接套用通用停用词表而通用表里往往包含这些高频虚词。解决加载停用词后手动剔除否定词或者干脆用白名单方式只保留实词。检查方法很简单清洗后搜一下「不」还在不在。5.4 训练测试集划分前做了全局向量化现象离线指标漂亮上线后效果暴跌。原因先对整个数据集fit_transform再划分测试集的词表信息泄漏进了训练过程属于数据泄漏。解决严格先划分再fit。正确顺序是train_test_split→ 在训练集上fit_transform→ 用同一个 vectorizer 对测试集transform。这个坑很隐蔽指标虚高时优先怀疑它。5.5 评分当标签但评分本身有噪声现象模型学到的规律和人工判断对不上某些明显差评被判正面。原因评分是用户随手打的存在「文字骂得很凶但给了 4 星」的情况弱标签本身有噪声。解决抽样人工核对一批标签估算噪声比例。如果噪声超过 10%考虑清洗标签或引入人工标注的小规模高质量数据集做校准。别指望模型能学会纠正标签错误它只会忠实拟合噪声。6. 把情感分数变成运营能用的聚合指标模型输出 0/1 只是中间产物真正有价值的是按维度聚合。我一般会做三件事按门店算负面率、按时间看趋势、按关键词定位问题。# 假设原始 df 里还有 hotel_id 和 date 两列 df[pred] clf.predict(tfidf.transform(df[seg])) df[is_negative] (df[pred] 0).astype(int) # 按门店聚合负面率 by_hotel df.groupby(hotel_id)[is_negative].agg([mean, count]) by_hotel by_hotel[by_hotel[count] 30] # 样本太少的门店不参与排名 print(by_hotel.sort_values(mean, ascendingFalse).head(10)) # 按月份看趋势 df[month] pd.to_datetime(df[date]).dt.to_period(M) trend df.groupby(month)[is_negative].mean() print(trend)逻辑说明count 30是统计显著性的一道粗筛样本太少的门店负面率波动极大排名没有意义。按月份聚合能看出某次改造或某次事件后的效果变化这是运营最关心的。参数说明30 这个阈值可以按业务调整门店评论量大就提到 50 或 100。趋势分析要注意季节性酒店有淡旺季单看一个月容易误判最好做同比而不是环比。再进一步可以对负面评论单独做关键词提取定位具体问题from sklearn.feature_extraction.text import CountVectorizer neg_texts df[df[is_negative] 1][seg] cv CountVectorizer(max_features20, ngram_range(1, 2)) cv.fit(neg_texts) print(cv.get_feature_names_out())逻辑说明只在负面样本上统计高频词得到的就是「客人最常抱怨什么」。ngram_range(1, 2)同样是为了捕捉「隔音 差」这类搭配。参数说明max_features20只取前 20 个够运营看重点。如果发现高频词里混进了「酒店」「房间」这种中性词把它们加进停用词表再跑一遍。最后说个我自己的习惯每次做完一个情感分析项目我都会留一份「错分样本清单」和「标签噪声比例」的记录。这两个数字比任何 F1 都更能说明模型能不能真的上线用。指标好看但错分样本全是反讽那这个模型在真实场景里就是个黑匣子运营不敢信。希望帮到你。本文还有配套的精品资源点击获取