资讯动态

旅游景点评论五级情感分析:语料库构建与模型选型实战

发布时间:2026/10/9 12:44:19 来源:尧图企业网站定制
简介这份资源是面向自然语言处理初学者与课程设计开发者的旅游景点方面级情感分析项目包围绕景点评论的情感倾向判定展开提供从数据预处理、分词、特征提取到分类建模的完整Python实现适合作为NLP课程设计、毕业设计或旅游数据分析的实践参考。压缩包为zip格式整体约105.9MB内含完整项目源码、已标注的景点评论情感语料库、预训练模型及系统文档说明源码覆盖数据清洗、模型训练、测试与应用脚本语料库可直接用于训练与验证。目前已有125人学习下载具备一定参考热度。读者可据此掌握文本预处理与特征工程流程理解SVM、随机森林及RNN、CNN等模型在情感分类中的落地方式并借助文档快速复现实验、调整模型参数为旅游舆情分析与消费者满意度研究提供可复用的工具与案例。1. 旅游景点评论的情感分级从二分类到五级强度为什么值得重做一遍做旅游行业的数据分析绕不开景点评论。绝大多数团队的第一版模型都是二分类好评、差评。上线之后很快会发现这个粒度根本不够用。运营想知道的是「哪些景点在服务维度上被反复吐槽」「哪些景点的性价比评价正在从一般滑向较差」二分类给不出这种区分度。于是就有了级别情感分析——把情感强度拆成五档比如非常负面、负面、中性、正面、非常正面或者按星级映射成 1 到 5 级。这个标题里的「语料库与模型」说的就是两件事一套带级别标注的旅游景点评论数据集以及能在这套数据上跑起来的分类模型。它解决的核心问题是标注粒度和领域适配——通用情感模型在旅游场景里经常翻车因为「排队两小时」「风景绝了但厕所一言难尽」这种混合表达通用模型要么判成中性要么直接判错。适合谁用做旅游平台评论运营的、做景区舆情监控的、以及想拿一个中文多分类任务练手又不想用烂大街电商数据集的 Python 工程师。我自己的经验是级别情感分析真正的难点不在模型结构而在标注一致性和类别边界。五级标注里「负面」和「非常负面」的界限不同标注员的理解能差出 20% 的标注分歧。所以这套东西值不值得做取决于你愿不愿意在语料构建上花掉整个项目 60% 以上的时间。愿意后面模型部分反而轻松。2. 语料库怎么建级别标注、领域词表和清洗流水线2.1 级别标签体系先定死再谈标注级别情感分析的第一步不是写代码是定标签体系。旅游评论有个特点情感表达经常和具体维度绑定比如「位置」「服务」「卫生」「性价比」「景色」。如果只给整条评论一个级别标签信息损失很大。常见做法是双轨标注整条评论一个总体级别同时给提到的维度各打一个级别。标签定义我一般用五级并且给每一级写清楚判定标准避免标注员自由发挥级别标签名判定标准典型表达1非常负面明确表达强烈不满有投诉或劝退意图「千万别来」「坑到离谱」2负面有明确不满但语气克制「有点失望」「不太推荐」3中性陈述事实无明显情感倾向「门票 60排队 40 分钟」4正面明确满意但无强烈推荐「还不错」「值得一看」5非常正面强烈推荐有情绪高点「此生必去」「超出预期」这张表要写进标注手册每个标注员人手一份。级别边界模糊的样本比如「风景很好但人太多了」按维度拆开标总体级别取主导情感。主导情感怎么判看评论者最终的行为意图——是推荐还是劝退。2.2 领域词表旅游场景的情感词和程度词通用情感词典在旅游场景里不够用。「网红」「出片」「踩雷」「照骗」这些词通用词典要么没有要么极性判反。所以需要单独维护一份旅游领域情感词表至少包含三类词情感极性词出片正、踩雷负、照骗负、绝了正但要看上下文、一般般弱负程度副词超级、极其、有点、稍微、完全不、一点都不否定词不、没、别、毫无、谈不上程度词和否定词要参与规则打分作为模型特征的补充。我一般会把这份词表存成 JSON方便后续更新# tourism_sentiment_lexicon.json 结构示例 { positive: { 出片: 2, 绝了: 2, 值得: 1, 惊艳: 2, 舒服: 1, 干净: 1, 热情: 1, 划算: 1 }, negative: { 踩雷: -2, 照骗: -2, 坑: -2, 失望: -1, 脏: -1, 贵: -1, 排队: -0.5, 吵: -1 }, degree: { 超级: 1.5, 极其: 1.5, 非常: 1.4, 很: 1.2, 有点: 0.7, 稍微: 0.6, 完全不: -1.0 }, negation: [不, 没, 别, 毫无, 谈不上] }这份词表不是拿来直接分类的而是用来做两件事一是给标注员参考减少边界分歧二是作为模型输入的特征之一尤其是当标注数据量不够大的时候规则特征能兜底。2.3 清洗流水线去噪、去重、去广告旅游评论的脏数据比电商评论更杂。常见的有导游或商家刷的好评模板、复制粘贴的攻略长文、纯表情符号、以及大量重复的「好评返现」式短评。清洗流水线我一般按这个顺序走import re import hashlib from collections import Counter def clean_text(text): # 1. 去除 URL 和联系方式 text re.sub(rhttp[s]?://\S, , text) text re.sub(r\d{11}, , text) # 手机号 # 2. 去除表情符号保留中文、英文、数字、常用标点 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、], , text) # 3. 压缩连续重复字符 text re.sub(r(.)\1{3,}, r\1\1, text) # 4. 去除首尾空白 return text.strip() def dedup(records): seen set() result [] for r in records: # 用文本哈希去重避免完全重复的刷评 h hashlib.md5(r[text].encode(utf-8)).hexdigest() if h not in seen: seen.add(h) result.append(r) return result def filter_short(records, min_len8): # 过滤掉过短评论这类评论级别信息不足 return [r for r in records if len(r[text]) min_len]这段代码的逻辑很直白先去 URL 和手机号防止广告再去表情和特殊符号减少噪声然后压缩重复字符比如「好好好好好」压成「好好」最后用 MD5 去重过滤掉长度不足 8 个字的短评。参数上min_len我一般设 8低于这个长度的评论人都不一定能判准级别模型更不行。去重阈值用完全匹配不做模糊去重因为旅游评论里「很好」和「非常好」是不同级别模糊去重会误伤。清洗完之后还要做一步人工抽检。我习惯随机抽 200 条自己标一遍和标注员的结果对比。如果一致率低于 85%说明标注手册需要返工别急着往下走。3. 模型选型从 TextCNN 到 Longformer 中文模型级别分类该用哪个3.1 基线模型TextCNN 和 BiLSTMAttention语料建好之后第一个要跑的肯定是基线。级别情感分析本质是文本多分类TextCNN 和 BiLSTMAttention 是最稳的两个起点。TextCNN 快适合快速验证标注质量BiLSTMAttention 能捕捉长距离依赖对「虽然……但是……」这种转折句更友好。TextCNN 的核心参数就几个卷积核尺寸一般取 2、3、4每种 128 或 256 个dropout 0.5学习率 1e-3 配 Adam。词向量可以用预训练的也可以从头训。如果标注数据只有几千条建议用预训练词向量冻结 embedding只训分类头。BiLSTMAttention 的隐藏层维度一般 128 或 256双向拼接后过 attention 池化再接全连接分类。学习率要调小一点5e-4 左右不然容易震荡。这两个模型在旅游评论五级分类上如果标注质量过关F1 大概能到 0.65 到 0.75 之间。别指望更高级别分类的天然难度就在那。3.2 升级路线Longformer 中文模型处理长评论旅游评论有个特点短评多但长评也不少。攻略型评论动辄几百字里面情感转折好几次。BERT 类模型的最大输入长度是 512 个 token长评论会被截断截断的位置如果正好是情感转折点分类就错了。这时候 Longformer 中文模型就有优势它的滑动窗口注意力机制能把输入长度扩展到 4096 甚至更长而且计算复杂度是线性的。用 Longformer 做级别分类关键参数是attention_window一般设 512表示每个 token 只关注前后各 512 个 token。这个值太小长距离依赖抓不住太大显存吃不消。我一般从 256 开始试显存够就往上加。学习率用 2e-5 到 3e-5warmup 比例 0.1训练轮数 3 到 5 轮就够再多容易过拟合。from transformers import LongformerForSequenceClassification, LongformerTokenizer import torch model_name your-local-longformer-chinese # 替换为实际可用的中文 Longformer 权重路径 tokenizer LongformerTokenizer.from_pretrained(model_name) model LongformerForSequenceClassification.from_pretrained( model_name, num_labels5, # 五级分类 attention_window512 # 滑动窗口大小 ) def encode(texts, max_len2048): # Longformer 支持长输入max_len 可以设到 2048 或 4096 return tokenizer( texts, paddingmax_length, truncationTrue, max_lengthmax_len, return_tensorspt ) # 训练时注意batch_size 要调小长序列显存占用高 # 一般 2 到 4配合梯度累积这段代码里num_labels5对应五级标签attention_window512是滑动窗口大小max_len2048是输入截断长度。注意 Longformer 的显存占用和序列长度成正比batch_size 设 2 到 4 比较稳不够就上梯度累积。训练轮数别多3 轮左右看验证集 F1不涨就停。3.3 类别不平衡和标签平滑五级分类里中性样本往往最多非常负面和非常正面最少。直接训练模型会偏向多数类。常见做法是加类别权重或者用标签平滑。类别权重按逆频率算weight total / (num_classes * count)。标签平滑把硬标签变成软标签比如 1.0 变成 0.9剩下 0.1 均分给其他类能缓解过拟合。import torch.nn as nn # 类别权重根据训练集统计计算 class_counts [500, 800, 2000, 900, 400] # 示例按实际数据填 total sum(class_counts) weights torch.tensor([total / (5 * c) for c in class_counts]) criterion nn.CrossEntropyLoss(weightweights, label_smoothing0.1)label_smoothing0.1是经验值0.05 到 0.15 之间都行。权重别设太极端不然少数类召回上去了精确率掉得厉害。我一般会同时看宏平均 F1 和加权 F1两个都涨才说明模型真的在变好。4. 训练与评估五级分类的指标陷阱和验证集构建4.1 别只看准确率看相邻级别的混淆五级分类有个坑准确率看着不低但模型可能把「非常负面」和「负面」搞混把「正面」和「非常正面」搞混。这种相邻级别的混淆对业务来说有时候可以接受有时候不行。比如舆情预警把「非常负面」判成「负面」会漏掉最该关注的样本。所以评估时必须看混淆矩阵尤其是相邻级别的误判率。from sklearn.metrics import confusion_matrix, classification_report import numpy as np def evaluate(y_true, y_pred, labels[1,2,3,4,5]): cm confusion_matrix(y_true, y_pred, labelslabels) print(混淆矩阵) print(cm) print(\n分类报告) print(classification_report(y_true, y_pred, labelslabels, digits3)) # 重点看相邻级别误判 for i in range(len(labels)-1): adj_err cm[i][i1] cm[i1][i] total cm[i].sum() cm[i1].sum() print(f级别 {labels[i]} 与 {labels[i1]} 相邻误判率{adj_err/total:.3f})这段代码输出混淆矩阵和分类报告最后单独算相邻级别的误判率。如果这个值超过 0.15说明级别边界定义有问题要么回去改标注手册要么考虑合并相邻级别。我自己的经验是五级分类里 2 和 3 的边界最难判因为「有点失望」和「陈述事实」之间经常只差一个语气词。4.2 验证集要按景点分层抽样旅游评论的数据分布往往和景点强相关。热门景点的评论多冷门景点的评论少。如果随机划分验证集可能出现验证集里全是热门景点模型在冷门景点上表现很差但你看不出来。所以验证集要按景点分层抽样保证每个景点的评论在训练集和验证集里都有。from sklearn.model_selection import train_test_split def stratified_split(records, test_size0.2, seed42): # 按景点 ID 分层 spot_ids [r[spot_id] for r in records] train, val train_test_split( records, test_sizetest_size, stratifyspot_ids, random_stateseed ) return train, valstratifyspot_ids保证每个景点的样本按比例分到训练和验证集。如果某个景点样本太少比如少于 10 条可以考虑合并到「其他」类或者直接不参与分层。验证集比例一般 0.2数据量小的时候可以降到 0.15。4.3 早停和模型选择训练轮数不是越多越好。级别分类任务上验证集 F1 通常在第 2 到第 4 轮达到峰值之后开始过拟合。我一般设 patience2验证集 F1 连续两轮不涨就停保存验证集 F1 最高的那个 checkpoint。best_f1 0 patience 2 wait 0 for epoch in range(10): train_one_epoch(model, train_loader, optimizer, criterion) val_f1 evaluate_f1(model, val_loader) if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), best_model.pt) wait 0 else: wait 1 if wait patience: print(f早停于第 {epoch1} 轮最佳 F1{best_f1:.4f}) break这段早停逻辑很标准关键是patience别设太大2 到 3 就行。保存最佳模型而不是最后一轮模型这个习惯能省掉很多后悔药。5. 避坑与排查级别情感分析里最容易翻车的 5 个地方5.1 标注一致性低于 80%模型怎么调都白搭现象模型在验证集上 F1 卡在 0.6 上不去换模型、调参数都没用。 原因标注员之间对级别边界的理解不一致训练数据本身就有噪声。模型学到的是一堆矛盾的标签自然学不好。 解决先算标注一致性。随机抽 100 条让两个标注员各标一遍算 Cohens Kappa。低于 0.8 就回去改标注手册把边界案例写进去重新培训标注员。别急着调模型。5.2 长评论截断把关键情感句切掉了现象长评论的分类准确率明显低于短评论尤其是那些先夸后骂的评论。 原因BERT 类模型默认截断到 512 token长评论的后半段被切掉而情感转折往往在后半段。 解决换 Longformer 中文模型把 max_len 提到 2048 或 4096。如果显存不够可以用滑动窗口分段推理再把各段结果融合。融合策略我一般用加权平均靠近结尾的段权重高一点因为评论结尾往往表达最终态度。5.3 中性样本过多导致模型「偷懒」现象模型把所有样本都判成中性准确率看着有 40% 多但其他级别召回率接近 0。 原因中性样本占比过高模型发现全判中性就能拿到不低的准确率于是不再学习区分。 解决加类别权重或者对中性样本做下采样。下采样比例我一般控制在中性样本不超过其他类平均数的 2 倍。同时看宏平均 F1别被准确率骗了。5.4 领域词表没更新新词极性判反现象模型对「出片」「绝绝子」这类新词的情感极性判断不稳定。 原因预训练词向量里这些词的表示不够准确或者词表里根本没有。 解决定期更新领域情感词表把新词加进去。如果用的是规则模型混合方案规则部分要优先匹配新词。纯模型方案的话可以在训练数据里补充包含新词的样本做增量训练。5.5 验证集泄露同一用户的评论同时出现在训练和验证集现象验证集 F1 虚高上线后效果掉一大截。 原因同一用户可能发多条评论如果按评论随机划分同一用户的不同评论可能同时出现在训练和验证集造成信息泄露。 解决按用户 ID 分组划分保证同一用户的所有评论只出现在训练集或验证集之一。如果用户 ID 缺失用评论时间景点 ID 做近似分组。6. 进阶技巧用滑动窗口推理把长评论的级别判准Longformer 虽然支持长输入但显存和推理速度是代价。如果不想换模型又想把长评论判准滑动窗口推理是个实用的折中方案。思路很简单把长评论切成多个重叠的窗口每个窗口单独过模型得到各级别概率再按窗口位置加权融合。import torch import torch.nn.functional as F def sliding_window_inference(text, tokenizer, model, window512, stride256, max_len2048): # 分词 tokens tokenizer.encode(text, add_special_tokensFalse) if len(tokens) window: # 短文本直接推理 inputs tokenizer(text, return_tensorspt, truncationTrue, max_lengthwindow) with torch.no_grad(): logits model(**inputs).logits return F.softmax(logits, dim-1).squeeze() # 长文本切窗口 windows [] for start in range(0, len(tokens), stride): end min(start window, len(tokens)) windows.append(tokens[start:end]) if end len(tokens): break # 每个窗口推理 probs_list [] for w in windows: w_text tokenizer.decode(w) inputs tokenizer(w_text, return_tensorspt, truncationTrue, max_lengthwindow) with torch.no_grad(): logits model(**inputs).logits probs_list.append(F.softmax(logits, dim-1).squeeze()) # 加权融合越靠后的窗口权重越高 weights torch.linspace(0.5, 1.0, len(probs_list)) weights weights / weights.sum() final_probs sum(w * p for w, p in zip(weights, probs_list)) return final_probs这段代码里window512是每个窗口的 token 数stride256是滑动步长重叠一半能避免窗口边界切掉关键句。权重用linspace(0.5, 1.0)越靠后的窗口权重越高因为评论结尾通常表达最终态度。这个策略在长评论上比直接截断能提升 5 到 10 个百分点的 F1代价是推理时间翻倍。验证这个方案有没有效我一般会单独抽一批长评论超过 512 token 的对比直接截断和滑动窗口推理的 F1。如果提升不明显说明你的数据里长评论的情感转折不集中在结尾那就得换别的策略比如按句子切分后做句子级分类再融合。最后说个习惯级别情感分析的项目我从来不会在标注质量没验证之前碰模型。血泪经验是标注一致性 0.7 的数据用再大的模型也救不回来。先把语料做扎实模型部分反而是整个流程里最确定的一环。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑