
简介面向旅游景点评论的方面级别情感分析项目包基于Python开发内置已标注的景点评论语料库与情感分类模型可直接用于课程设计、毕业设计或NLP方向学习。项目围绕景点评论文本构建了预处理、特征提取、情感分类等完整流程能够自动化判断评论的正面、负面或中立倾向适合希望掌握情感分析工程落地的开发者、数据分析师与科研人员。压缩包为zip格式大小约105.9MB目前已有123人学习。包内包含完整的Python源代码、带标注的景点评论语料库、系统安装运行文档、使用教程及案例分析可依据文档快速搭建环境、复现训练与测试流程。借助该项目可以实践SVM、随机森林、RNN、CNN等常用算法的情感分类应用理解从文本清洗、分词、特征表示到模型评估的完整链路。对于旅游平台、市场研究人员也可借助该系统洞察顾客评价与消费趋势为服务优化提供数据支撑。1. 景区评论情感分析从人工看差评到自动定级做旅游平台爬虫或景区舆情分析时最耗时的往往不是抓数据而是给每一条评论定性。运营后台每天涌入的点评里“景色绝美但入口找不到”这类混合评价占了相当比例如果只依赖整体评分差评背后的真实问题会被一条五星好评稀释掉。这个Python项目就是干这件事的它提供了一份已标注的旅游景点评论情感分析语料库外加从数据预处理、特征提取到模型训练和评估的完整Python工程源码。所谓“方面级别”是指不满足于判断整句是正面还是负面而是把情绪拆到“景色、交通、价格、餐饮”等具体维度上分别定级。课程设计、毕业设计、想入门NLP情感分析的开发者都能在上面快速改出一个能跑通、能换数据、能讲清原理的完整系统。2. 语料库构建与预处理从原始评论到可训练的标注数据2.1 语料库字段设计与标注维度打开语料库里的CSV文件每一行并不是一条评论而是“一条评论在一个方面上的情感判断”。也就是说一条同时提到景色和停车位的评论会在语料里出现两行分别对应两个不同的方面标签。这种结构是方面级别情感分析与普通文本情感分类的最大区别也是后续训练数据组织的基石。字段类型说明示例comment_idstr评论唯一编号C00021scenic_namestr景点名称大理古城contentstr原始评论文本古城夜景好看但停车位太小aspectstr方面标签交通sentimentint情感标签0负面/1中立/2正面0方面标签一般覆盖景色、交通、价格、餐饮、综合体验这几类。标注时最忌讳只标正负把“还可以”“一般”都归到负面里会让模型在学习中立表达时没有样本可用。我一般建议标注规范里明确规定出现明显情绪词才能标正面或负面中性描述和模糊表达一律归中立。这个规则能显著减少标注员之间的分歧。2.2 清洗规则与jieba分词细节原始评论文本里夹杂着HTML标签从网页抓取来的、表情符号、数字和各类乱码第一步要做规则清洗。下面的clean_text是一套常见的清洗流程import re import jieba import pandas as pd def load_stopwords(pathdata/stopwords.txt): with open(path, encodingutf-8) as f: return set([line.strip() for line in f]) def clean_text(text): # 去掉HTML标签 text re.sub(r[^], , text) # 去掉英文和数字景区评论里这些信息对情感判断贡献低 text re.sub(r[a-zA-Z0-9], , text) # 去掉方括号、花括号等噪声符号 text re.sub(r[【】\[\]{}()], , text) # 多个空白符合并为一个 text re.sub(r\s, , text) return text.strip() def cut_text(text, stop_words): words jieba.lcut(clean_text(text)) return [w for w in words if w not in stop_words]清洗顺序是有讲究的。先去掉HTML标签再处理符号是因为标签内部的英文和数字不应该进入后续分词流程先正则后统一处理能减少干扰。\s合并空白放在最后避免前面的正则把文本拼接后产生多余空格。分词层面jieba默认词典对景点名极不友好。“大理古城”会被切分成“大理/古城”“拈花湾”这种三字景区名更是直接拆散导致后面方面匹配时完全对不上。解决办法是在项目根目录放一个自定义词典文件data/scenic_terms.txt每行一个词再用load_userdict加载jieba.load_userdict(data/scenic_terms.txt)停用词表里除了常见的“的、了、是”我还习惯把“哇塞”“嘻嘻”“哈哈哈”这类语气词加进去。这类词在景区评论里出现频率高但对情感极性的区分度不稳定保留它们会让特征矩阵多出很多无意义的维度。2.3 标注数据质量校验拿到一份语料库第一件事不是训练而是校验标注质量。课程设计答辩时老师最爱问的就是“这批数据怎么保证标注准确”如果自己都说不清数据分布后面所有指标都站不住脚。用pandas检查每个方面在不同情感标签上的分布df pd.read_csv(data/aspect_sentiment.csv, encodingutf-8) stat df.groupby([aspect, sentiment]).size().unstack(fill_value0) print(stat)输出是类似下面这样的表格具体数值以你拿到的语料为准aspect0负面1中立2正面景色8632421交通2105796价格1898873餐饮1224166综合体验9470201这份分布表有两个信息量很大的点。第一交通和价格两个方面的负面样本明显多于正面这符合旅游评论的常识——人们更容易因为停车难、门票贵而抱怨。第二如果某个方面在三个情感标签上出现零值比如“中立”为0就要考虑是标注规范漏了还是这个方面的评论天然呈两极分化。对于后者可以在训练时用class_weightbalanced缓解类别不平衡而不是删掉少数类样本。提示对语料做二次抽检时随机抽100条由两个人各自重新标注一遍计算标注一致率。一致率低于80%时优先检查是标注规范问题还是标签体系问题不要急着去调模型。3. TF-IDF与上下文窗口特征把SVM训练成情感分类器3.1 为什么不能只数情感词最朴素的情感分析做法是准备一份正面词表和负面词表然后统计评论里两类词的数量谁多听谁的。这在“景色优美、空气清新”这类句子上有效遇到“不推荐下雨天去”“人工湖没什么可看的”就失效了。原因是“推荐”是正面词但前面有否定词“不”“可看”是中性偏正面的表达加上“没什么”整体语气就翻转了。所以特征工程至少要提供两个层面的信息一是词级别的N-gram组合让模型能看到“不/推荐”“没什么/可看”这类相邻词的共现二是方面词周围的上下文窗口让模型知道当前句子在说哪个方面。把这两组特征拼在一起比单独用全句TF-IDF要稳得多。3.2 窗口特征与TF-IDF组合实现下面这段代码是特征构建的核心。对每个“评论-方面”组合先从分词结果里定位方面词的位置截取它前后各3个词作为上下文窗口然后分别对全文和窗口做TF-IDF向量化最后水平拼接成一条特征向量from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split import numpy as np def pull_window(words, aspect, size3): try: idx words.index(aspect) except ValueError: idx -1 for i, w in enumerate(words): if aspect in w or w in aspect: idx i break if idx -1: return words[: size 1] start max(0, idx - size) end min(len(words), idx size 1) return words[start:end] full_texts, win_texts [], [] for _, row in df.iterrows(): words cut_text(row[content], stop_words) full_texts.append(row[content]) win_texts.append( .join(pull_window(words, row[aspect]))) tfidf_full TfidfVectorizer(max_features5000, ngram_range(1, 2)) tfidf_win TfidfVectorizer(max_features800) X_full tfidf_full.fit_transform(full_texts) X_win tfidf_win.fit_transform(win_texts) X np.hstack([X_full.toarray(), X_win.toarray()]) y df[sentiment].values这里有两个细节值得说明。第一全文TF-IDF和窗口TF-IDF必须用两个独立的向量器因为窗口词的词频统计范围远小于全文混在一起后窗口信息会被全文的词频稀释掉。第二pull_window在方面词无法精确匹配时会退化为取句子前几个词兜底这一步是防止某个评论文本里没有出现标准方面词时整个程序报错。ngram_range(1, 2)让模型能捕捉到“不/推荐”这种双词组合。窗口大小设为3而不是更大是因为在景区评论这种短文本里超过3个词的跨度基本就不再承载与方面词直接相关的信息了反而会增加特征维度。3.3 SVM训练与GridSearch参数调优特征维度在5000加800左右这个量级用SVM的RBF核非常合适。线性模型在特征交叉上能力有限而RBF核可以隐式建模非线性边界又不像深度学习那样需要大规模数据才能收敛。训练时用GridSearchCV同时搜索惩罚系数C和核宽度gammafrom sklearn.svm import SVC from sklearn.model_selection import GridSearchCV 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 ) param_grid { C: [0.1, 1, 10], gamma: [scale, 0.01, 0.1] } model SVC(kernelrbf, class_weightbalanced) grid GridSearchCV(model, param_grid, cv5, scoringf1_macro, n_jobs-1) grid.fit(X_train, y_train) print(best params:, grid.best_params_) y_pred grid.best_estimator_.predict(X_test) print(classification_report(y_test, y_pred, target_names[负面, 中立, 正面]))class_weightbalanced会根据每个类别的样本量自动放大少数类的权重应对第2.3节里类别不平衡的问题。scoringf1_macro是三个类别上F1分数的算术平均相比accuracy它不会因为负面样本占大头就让模型靠“全预测负面”拿到高分。gammascale是scikit-learn推荐的默认策略它会根据特征数量自动调整gamma的基准值在小数据集上通常比手动指定更稳。3.4 基线模型对比为了说明SVM在这个任务上的性价比我在同一份特征上跑了三个经典模型结果如下模型准确率macro-F1Logistic Regression0.8120.776Random Forest0.7980.752SVMRBF核0.8360.815SVM在中小规模文本分类上依然是性价比最高的选择。Random Forest对高维稀疏特征的拟合能力偏弱Logistic Regression虽然训练快但对特征交叉的表达有限。在课程设计这个量级几千条样本下SVM通常比微调BERT等预训练模型更容易获得稳定的分数——后者需要更多数据和更长的调参周期。4. 方面词抽取与方面级分类拆解一条多维评论4.1 从文本级到方面级不是分类问题是结构化问题“古城夜景好看但停车位太小”这句评论整体情感分类无论给正面还是负面都丢失了一半信息。方面级别情感分析的思路是先抽取出这条评论涉及哪些方面再对每个方面分别判断情感。所以整个系统的核心并不只是SVM分类器还包括如何把“夜景”映射到“景色”、把“停车位”映射到“交通”。4.2 词典加滑窗的快速路径课程设计阶段最稳定的做法是维护一份方面词映射表每个方面对应一组常用词命中即可。比如ASPECT_MAP { 景色: [景色, 风景, 风光, 夜景, 景观], 交通: [交通, 公交, 停车, 车位, 缆车, 索道], 价格: [门票, 价格, 贵, 便宜, 性价比, 收费], 餐饮: [餐厅, 饭店, 小吃, 美食, 口味], 综合体验: [服务, 管理, 排队, 人员, 整体] } def extract_aspect_by_dict(text): hits [] for aspect, words in ASPECT_MAP.items(): for w in words: if w in text: hits.append((aspect, text.index(w))) hits.sort(keylambda x: x[1]) return [h[0] for h in hits]按文本中出现的先后位置排序很重要它能让后续输出结果的顺序贴合用户阅读评论的自然顺序。这个方法的局限很明显同义词覆盖不全比如“性价比”和“划算”在语料里可能指向同一方面但映射表只收了前者。常见做法是把词典做成外部配置文件拿到新语料后先跑一遍把命中率为0的评论挑出来人工补充词表两轮迭代就能覆盖80%以上的情况。4.3 依存句法抽取的上升路径如果项目时间充裕另一个更完整的路线是借助句法分析工具抽取“方面词-情感词”对。核心思路是在“索道排队两小时”这个句子里“索道”和“排队”之间存在主谓或动宾关系借助依存句法分析能自动抽出这个组合。一条在词典路线下会被漏掉的评论在句法路线下可能被正确识别。不过这一方案有两个实际门槛。一是需要下载并加载一个完整的中文依存句法模型本地没有现成模型权重时配置成本不低二是句法分析结果里需要写规则过滤关系类型否则会把不相关的动宾结构也当成方面组合。我更建议把小数据集上的词典匹配作为主路径把句法分析留作扩展模块——答辩时这反而是一个加分项你证明了你在两种方案之间做过权衡。4.4 多方面结果的结构化输出把第3章的SVM模型和上面的方面抽取串起来一条评论的输出应该是这样的结构化数据{ comment: 古城夜景好看但停车位太小, aspects: [ {aspect: 景色, sentiment: 正面, confidence: 0.91}, {aspect: 交通, sentiment: 负面, confidence: 0.84} ] }confidence来自SVM的decision_function可以归一化成一个0到1之间的分数。这个输出格式的优点是后续无论做统计报表还是可视化面板都可以直接消费JSON字段不需要再写一套文本解析逻辑。5. 模型评估、模型保存与FastAPI部署落地5.1 混淆矩阵确认类别错误的方向只用classification_report看不够类别之间的混淆方向能告诉我们标注体系是否有问题。比如“中立”样本大量被预测成“负面”往往是语料里中立表达含混不清导致的from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix import matplotlib.pyplot as plt cm confusion_matrix(y_test, y_pred) ConfusionMatrixDisplay(cm, display_labels[负面, 中立, 正面]).plot() plt.savefig(output/confusion_matrix.png)5.2 模型与向量器一起持久化SVM模型本身只保存了支持向量和核参数真正部署时必须把两个TF-IDF向量器一并保存否则线上预测时文本向量化结果会和训练时不一致import joblib joblib.dump(grid.best_estimator_, model/svm_aspect_sentiment.pkl) joblib.dump(tfidf_full, model/tfidf_full.pkl) joblib.dump(tfidf_win, model/tfidf_win.pkl)5.3 FastAPI暴露预测接口课程设计里最常见的验收方式是启动一个HTTP服务Postman里发一条评论看返回结果。FastAPI写这个接口非常快from fastapi import FastAPI from pydantic import BaseModel import joblib app FastAPI() clf joblib.load(model/svm_aspect_sentiment.pkl) tfidf_full joblib.load(model/tfidf_full.pkl) tfidf_win joblib.load(model/tfidf_win.pkl) class CommentRequest(BaseModel): content: str class AspectResult(BaseModel): aspect: str sentiment: str app.post(/predict, response_modellist[AspectResult]) def predict(req: CommentRequest): aspects extract_aspect_by_dict(req.content) results [] for aspect in aspects: words cut_text(req.content, stop_words) win_text .join(pull_window(words, aspect, size3)) x np.hstack([ tfidf_full.transform([req.content]).toarray(), tfidf_win.transform([win_text]).toarray() ]) label clf.predict(x)[0] sentiment [负面, 中立, 正面][int(label)] results.append(AspectResult(aspectaspect, sentimentsentiment)) return results用uvicorn main:app --host 0.0.0.0 --port 8000启动后POST /predict直接返回JSON数组。这里要注意线上预测时的pull_window逻辑必须和训练时完全一致否则特征对齐不上。5.4 可视化与两个高频坑做了接口之后顺手把统计结果导出成图表让答辩演示时能直接展示“近一个月某景区各方面情感分布”。用pandas分组统计后导出一张柱状图就够了不必为此引入重型BI工具。踩过的两个坑务必注意。第一个是Windows环境下读取CSV用encodingutf-8会报错或乱码改用utf-8-sig读取BOM头不会带进DataFrame。第二个是自定义词典没加载就调pull_window导致“大理古城”被切碎成“大理/古城”方面词永远匹配不上特征窗口退化成取前几个词的兜底逻辑模型效果直接下降。先跑jieba.lcut(大理古城)确认切分结果再继续后面的步骤。本文还有配套的精品资源点击获取