简介:本资源为基于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_words=None): with open(path, 'r', encoding='utf-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_words=ATTR_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_all=False) # 精确模式 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', encoding='utf-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(frac=1, random_state=42).reset_index(drop=True) df = load_corpus('2000_pos.txt', '2000_neg.txt', stopwords) print(df['label'].value_counts())random_state=42保证每次打乱结果一致,方便复现。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_size=0.2, random_state=42, stratify=df['label'] ) pipeline = Pipeline([ ('tfidf', TfidfVectorizer(max_features=5000, ngram_range=(1, 2))), ('pca', PCA(n_components=100)), ('svm', SVC(kernel='rbf', C=1.0, gamma='scale')) ]) pipeline.fit(X_train, y_train) y_pred = pipeline.predict(X_test) print(classification_report(y_test, y_pred))TfidfVectorizer的max_features=5000控制词表大小,ngram_range=(1, 2)同时考虑单字和双字词组,能捕捉“隔音差”这种组合。PCA(n_components=100)把高维 TF-IDF 降到 100 维,减少噪声和计算量。SVC的C=1.0是正则化参数,越大越容易过拟合;gamma='scale'是 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): i+4] 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): i+4] 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 can't decode byte...。原因:文件可能是 GBK 或 GB2312 编码,Windows 下尤其常见。解决:先用chardet检测编码,或者直接用encoding='gbk'试读。如果文件里混了多种编码,用errors='ignore'跳过非法字符,但会丢数据,慎用。
4.2 分词后属性词丢失
现象:extract_attributes返回空列表,明明评论里有“隔音”。原因:Jieba 默认词典没有“隔音”这个词,被切成了“隔”和“音”。解决:在分词前调用jieba.add_word('隔音'),或者加载自定义词典jieba.load_userdict('hotel_dict.txt')。资源包里没有自定义词典,需要自己补。
4.3 PCA 后准确率反而下降
现象:不加 PCA 时 SVM 准确率 85%,加了 PCA 降到 78%。原因:n_components=100可能降得太狠,丢失了关键信息。解决:先跑PCA(n_components=0.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_weight='balanced'让 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'], normalize='index') ct.plot(kind='barh', stacked=True, figsize=(10, 6), colormap='RdYlGn') plt.xlabel('占比') plt.title('各属性情感分布') plt.tight_layout() plt.savefig('attr_sentiment.png', dpi=150)crosstab的normalize='index'让每行加起来等于 1,方便看占比。stacked=True是堆叠柱状图,正面和负面一目了然。如果某个属性的中性占比超过 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 个百分点。这个循环做两三轮,模型就能适配你的具体数据分布。
从那以后我每次拿到新的评论数据,都先跑一遍属性抽取,看哪些属性没被覆盖,再补词典和标注。这套资源包的价值不在于模型多强,而在于它把链路拆得足够清楚,你可以按自己的数据替换每一环。希望帮到你。
本文还有配套的精品资源,点击获取