酒店评论细粒度情感分析:Python实现属性抽取与情感判断 简介本资源为基于Python的酒店评论细粒度情感分析系统设计与实现面向计算机、数据科学相关专业的毕业设计或课程作业需求者也适合希望掌握文本挖掘与深度学习实战的开发者。系统覆盖从OTA平台评论爬取、数据清洗与分词去停用词到属性抽取、情感极性判断与强度量化的完整链路并借助BiLSTM-CRF、BERT微调与注意力机制提升细粒度识别精度最终以仪表盘、词云和时间趋势图呈现多维度分析结果。压缩包共2000个文件以1997个txt评论语料为主辅以2个py脚本和1个md说明文档整体约1.91MB便于直接运行与二次开发。已有99人学习下载读者可获取完整项目代码、正负样本数据、停用词表及PCA-SVM等对比实验脚本快速复现并理解细粒度情感分析全流程。1. 从一堆散装文件到能跑的系统这套酒店评论细粒度情感分析到底交付了什么如果你从 OTA 平台拉过酒店评论大概经历过这种绝望几万条文本躺在 CSV 里评分是有的但用户到底在骂什么、夸什么完全看不出来。一条“位置不错但隔音太差前台倒是挺热情”的评论粗粒度模型只会给你一个“中性”标签可运营真正想知道的是——隔音差到什么程度、前台热情是不是普遍现象。这套基于 Python 的酒店评论细粒度情感分析系统解决的就是这个问题把一条评论拆成多个属性再对每个属性单独判断情感极性和强度。资源包里的文件构成很直白app.py是主入口5_pca_svm.py是一条基于 PCA 降维加 SVM 分类的基线路线2000_neg.txt和2000_pos.txt是两万条量级的正负样本stopWord.txt是停用词表剩下几个neg.670.txt、pos.719.txt、pos.709.txt、neg.36.txt是分片数据。适合谁做毕业设计的学生、需要快速搭一个情感分析原型的课程作业选手以及想理解细粒度情感分析完整链路的初级 NLP 工程师。它不追求 SOTA但链路完整、文件清晰能让你在本地把“数据→属性→情感→可视化”跑通一遍。2. 数据管线拆解从原始评论到可训练样本的四个关键环节2.1 为什么先做分片和停用词而不是直接上模型很多人拿到2000_pos.txt这种文件就想直接fit结果跑出来的准确率惨不忍睹。原因很简单酒店评论里充斥着“酒店”“房间”“入住”这类高频但无区分度的词以及“的”“了”“是”这种停用词。如果不处理TF-IDF 会把权重全堆在这些词上模型学到的其实是“这条评论像不像酒店评论”而不是“这条评论是正面还是负面”。资源包里的stopWord.txt就是干这个的。常见做法是先加载通用停用词表再手动追加酒店领域专属停用词比如“酒店”“宾馆”“入住”“退房”“房间”这些在正负样本里都高频出现的词。注意这一步不要一刀切——像“隔音”“空调”“前台”这种属性词必须保留它们是细粒度分析的锚点。# 加载停用词并追加领域词 def load_stopwords(path, domain_wordsNone): with open(path, r, encodingutf-8) as f: stopwords set([line.strip() for line in f if line.strip()]) # 通用停用词里可能没有的酒店高频中性词 extra {酒店, 宾馆, 入住, 退房, 房间, 感觉, 觉得} stopwords.update(extra) if domain_words: stopwords - set(domain_words) # 属性词从停用词里剔除 return stopwords # 属性词白名单必须保留 ATTR_WORDS {隔音, 空调, 前台, 床品, 卫生间, WiFi, 位置, 早餐} stopwords load_stopwords(stopWord.txt, domain_wordsATTR_WORDS)这段代码的逻辑是先读基础停用词再补一批酒店场景下的中性高频词最后把属性词从停用词集合里减掉。参数domain_words就是你的属性白名单改这个集合就能控制哪些词不被过滤。失败时先看stopWord.txt的编码如果是 GBK 而你用 UTF-8 读会直接报UnicodeDecodeError。2.2 分词与文本清洗Jieba 的三种模式和该选哪个资源包里没有单独的分词脚本但app.py里大概率集成了 Jieba。Jieba 有三种模式精确模式、全模式、搜索引擎模式。做情感分析我一般用精确模式因为全模式会把“隔音差”切成“隔音”“差”“隔音差”引入噪声搜索引擎模式适合召回不适合分类。import jieba import re def clean_text(text): # 去 HTML 标签 text re.sub(r[^], , text) # 去特殊符号和表情保留中文、英文、数字和基本标点 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、], , text) return text.strip() def tokenize(text, stopwords): text clean_text(text) words jieba.lcut(text, cut_allFalse) # 精确模式 return [w for w in words if w not in stopwords and len(w) 1]clean_text里的正则[^\u4e00-\u9fa5a-zA-Z0-9。、]是保留中文、英文、数字和五个中文标点其余全删。len(w) 1过滤单字因为单字在情感分类里信息量太低。如果你发现分词后“隔音差”被切成了“隔音”和“差”可以在 Jieba 里加载自定义词典把“隔音差”作为一个词加进去。2.3 正负样本的读取与标签对齐2000_neg.txt和2000_pos.txt是分开的两个文件读取时要对齐标签。常见做法是正样本标 1负样本标 0然后合并打乱。注意文件里可能有空行或纯符号行读的时候要过滤掉。import pandas as pd def load_corpus(pos_path, neg_path, stopwords): data [] for path, label in [(pos_path, 1), (neg_path, 0)]: with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue tokens tokenize(line, stopwords) if len(tokens) 2: # 过滤太短的评论 continue data.append({text: .join(tokens), label: label}) df pd.DataFrame(data) return df.sample(frac1, random_state42).reset_index(dropTrue) df load_corpus(2000_pos.txt, 2000_neg.txt, stopwords) print(df[label].value_counts())random_state42保证每次打乱结果一致方便复现。len(tokens) 2是过滤“好”“差”这种单字评论它们对模型训练帮助不大。如果value_counts()出来正负样本差距很大说明某个文件读取有问题先检查文件行数。2.4 分片文件的使用场景neg.670.txt、pos.719.txt、pos.709.txt、neg.36.txt这几个文件看命名像是按数量分片的。常见做法是把2000_pos.txt和2000_neg.txt当训练集分片文件当测试集或增量数据。比如neg.36.txt只有 36 条适合做快速验证pos.719.txt和pos.709.txt可以合并成更大的正样本测试集。不要把它们和主训练集混在一起否则测试集泄漏准确率虚高。3. 细粒度情感分析的核心属性抽取与情感判断怎么落地3.1 属性抽取的两种路线词典匹配 vs NER 模型细粒度情感分析的第一步是抽属性。资源包里没有预训练 NER 模型所以最现实的路线是词典匹配加规则。常见做法是维护一个属性词典每个属性挂若干同义词比如“隔音”挂“噪音”“吵”“安静”“前台”挂“接待”“办理入住”。然后用 Jieba 分词后做匹配。ATTR_DICT { 隔音: [隔音, 噪音, 吵, 安静, 声音], 前台: [前台, 接待, 办理入住, 服务态度], 空调: [空调, 制冷, 制热, 温度], 床品: [床品, 床垫, 枕头, 被子, 床单], 卫生间: [卫生间, 浴室, 马桶, 淋浴, 热水], WiFi: [WiFi, wifi, 网络, 网速], 位置: [位置, 交通, 地铁, 周边, 地段], 早餐: [早餐, 早饭, 自助餐, 餐厅] } def extract_attributes(tokens): found set() for attr, synonyms in ATTR_DICT.items(): for syn in synonyms: if syn in tokens: found.add(attr) break return list(found)这段代码的逻辑是遍历属性词典只要评论分词结果里出现某个同义词就认为该属性被提及。ATTR_DICT是核心参数你可以按业务需求增删属性和同义词。失败时先看分词结果如果“隔音”被切成了“隔”和“音”说明 Jieba 词典里没有这个词需要手动jieba.add_word(隔音)。如果想上 NER 模型BiLSTM-CRF 或 BERT-NER 是常见选择但资源包里没有模型权重你需要自己标注数据训练。对于毕业设计或课程作业词典匹配足够跑通链路把精力放在后面的情感判断和可视化上更划算。3.2 情感极性判断PCA SVM 基线为什么值得先跑5_pca_svm.py这个文件名已经说明了路线先做 PCA 降维再用 SVM 分类。为什么先跑这个因为它快、可解释、不依赖 GPU。对于两万条量级的评论TF-IDF 加 PCA 加 SVM 在普通笔记本上几分钟就能出结果适合快速验证数据质量。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.decomposition import PCA from sklearn.svm import SVC from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 假设 df 已经加载好text 是分词后空格拼接的字符串 X_train, X_test, y_train, y_test train_test_split( df[text], df[label], test_size0.2, random_state42, stratifydf[label] ) pipeline Pipeline([ (tfidf, TfidfVectorizer(max_features5000, ngram_range(1, 2))), (pca, PCA(n_components100)), (svm, SVC(kernelrbf, C1.0, gammascale)) ]) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test) print(classification_report(y_test, y_pred))TfidfVectorizer的max_features5000控制词表大小ngram_range(1, 2)同时考虑单字和双字词组能捕捉“隔音差”这种组合。PCA(n_components100)把高维 TF-IDF 降到 100 维减少噪声和计算量。SVC的C1.0是正则化参数越大越容易过拟合gammascale是 RBF 核的默认设置。如果准确率低于 80%先检查停用词是否过滤干净再看正负样本是否平衡。3.3 属性级情感判断把问题拆成多个二分类细粒度分析的关键是不是判断整条评论的情感而是判断每个属性的情感。常见做法是对每个属性单独训练一个二分类器或者用一条评论里属性附近的词做局部判断。资源包里没有现成的属性级标注所以更现实的做法是基于规则在评论里找到属性词后看它前后窗口内有没有情感词。POS_WORDS {好, 棒, 满意, 干净, 热情, 方便, 舒适, 安静} NEG_WORDS {差, 吵, 脏, 慢, 冷漠, 失望, 贵, 旧} def attribute_sentiment(tokens, attr): synonyms ATTR_DICT.get(attr, [attr]) for i, token in enumerate(tokens): if token in synonyms: # 取属性词前后各 3 个词作为窗口 window tokens[max(0, i-3): i4] pos_score sum(1 for w in window if w in POS_WORDS) neg_score sum(1 for w in window if w in NEG_WORDS) if pos_score neg_score: return 正面 elif neg_score pos_score: return 负面 else: return 中性 return Nonewindow的大小是 3意味着属性词前后各取 3 个词。这个参数可以调窗口太小会漏掉远距离情感词太大会引入无关噪声。POS_WORDS和NEG_WORDS是情感词典你可以按酒店场景扩充比如加入“隔音好”“前台热情”这种短语。如果一条评论里同一个属性出现多次可以取多数投票或最后一次出现的结果。3.4 情感强度量化从“满意”到“非常满意”怎么打分情感强度是细粒度分析里容易被忽略的一环。用户说“满意”和“非常满意”运营的优先级完全不同。常见做法是给情感词赋权重正面词 1负面词 -1程度副词做乘法。DEGREE_WORDS {非常: 2.0, 很: 1.5, 特别: 2.0, 有点: 0.5, 稍微: 0.5} def sentiment_score(tokens, attr): synonyms ATTR_DICT.get(attr, [attr]) for i, token in enumerate(tokens): if token in synonyms: window tokens[max(0, i-3): i4] score 0.0 for j, w in enumerate(window): base 1.0 if w in POS_WORDS else (-1.0 if w in NEG_WORDS else 0.0) if base ! 0.0: # 看前一个词是不是程度副词 if j 0 and window[j-1] in DEGREE_WORDS: base * DEGREE_WORDS[window[j-1]] score base return round(max(-1.0, min(1.0, score / 3)), 2) # 归一化到 -1~1 return 0.0DEGREE_WORDS里“非常”和“特别”权重 2.0“有点”和“稍微”权重 0.5。score / 3是简单归一化把分数压到 -1 到 1 之间。这个参数可以根据你的数据分布调整如果大部分分数都挤在 0 附近说明情感词覆盖不够需要扩充词典。4. 避坑与排查这套资源跑不起来时先看这五条4.1 编码报错UnicodeDecodeError现象读2000_pos.txt或stopWord.txt时直接抛UnicodeDecodeError: utf-8 codec cant decode byte...。原因文件可能是 GBK 或 GB2312 编码Windows 下尤其常见。解决先用chardet检测编码或者直接用encodinggbk试读。如果文件里混了多种编码用errorsignore跳过非法字符但会丢数据慎用。4.2 分词后属性词丢失现象extract_attributes返回空列表明明评论里有“隔音”。原因Jieba 默认词典没有“隔音”这个词被切成了“隔”和“音”。解决在分词前调用jieba.add_word(隔音)或者加载自定义词典jieba.load_userdict(hotel_dict.txt)。资源包里没有自定义词典需要自己补。4.3 PCA 后准确率反而下降现象不加 PCA 时 SVM 准确率 85%加了 PCA 降到 78%。原因n_components100可能降得太狠丢失了关键信息。解决先跑PCA(n_components0.95)让算法自动保留 95% 方差或者画累计方差贡献率曲线找拐点。对于两万条评论200 到 300 维通常比 100 维更稳。4.4 正负样本不均衡导致模型偏向多数类现象classification_report里正样本 F1 0.9负样本 F1 0.5。原因2000_pos.txt和2000_neg.txt行数不一致或者清洗后负样本被过滤太多。解决先print(df[label].value_counts())确认比例如果差距超过 2:1用class_weightbalanced让 SVM 自动加权或者对少数类过采样。4.5 分片文件当训练集导致测试集泄漏现象模型在测试集上准确率 95%换一批真实评论掉到 60%。原因把pos.719.txt和pos.709.txt混进了训练集而测试集里又有相似样本。解决严格划分训练集和测试集分片文件只做验证或增量测试。我一般会留一个完全没参与训练的neg.36.txt做最终 sanity check。5. 从跑通到好用属性级可视化与增量迭代的两个技巧5.1 属性情感分布仪表盘用 Pandas 做交叉表跑通模型后最有价值的输出是每个属性的情感分布。不需要上前端框架Pandas 的crosstab加 Matplotlib 就能出图。import matplotlib.pyplot as plt # 假设 results 是 list of dict每条包含 comment_id, attr, sentiment results [] for idx, row in df.iterrows(): tokens row[text].split() for attr in extract_attributes(tokens): sent attribute_sentiment(tokens, attr) score sentiment_score(tokens, attr) results.append({comment_id: idx, attr: attr, sentiment: sent, score: score}) res_df pd.DataFrame(results) ct pd.crosstab(res_df[attr], res_df[sentiment], normalizeindex) ct.plot(kindbarh, stackedTrue, figsize(10, 6), colormapRdYlGn) plt.xlabel(占比) plt.title(各属性情感分布) plt.tight_layout() plt.savefig(attr_sentiment.png, dpi150)crosstab的normalizeindex让每行加起来等于 1方便看占比。stackedTrue是堆叠柱状图正面和负面一目了然。如果某个属性的中性占比超过 60%说明情感词典覆盖不够需要针对该属性补充领域词。5.2 增量迭代用分片文件做主动学习neg.670.txt和pos.719.txt这种分片文件最好的用法不是一次性跑完而是做主动学习。先跑一遍模型挑出置信度低的样本人工标注后加入训练集再重新训练。常见做法是用SVC的decision_function拿置信度取绝对值最小的 100 条。# 假设 pipeline 已经训练好 decision pipeline.decision_function(X_test) # 找置信度最低的样本 low_conf_idx np.argsort(np.abs(decision))[:100] low_conf_samples X_test.iloc[low_conf_idx] # 人工标注后加入训练集重新 fitnp.abs(decision)越小说明模型越不确定这些样本信息量最大。标注 100 条后重新训练通常能提升 2 到 5 个百分点。这个循环做两三轮模型就能适配你的具体数据分布。从那以后我每次拿到新的评论数据都先跑一遍属性抽取看哪些属性没被覆盖再补词典和标注。这套资源包的价值不在于模型多强而在于它把链路拆得足够清楚你可以按自己的数据替换每一环。希望帮到你。本文还有配套的精品资源点击获取