旅游景点方面级情感分析:语料库构建与模型训练实战
2026/9/15 3:25:55 网站建设 项目流程

简介:面向旅游景点评论的方面级别情感分析Python项目,聚焦从海量评价中自动识别正面、负面与中立情感,适合自然语言处理初学者、旅游平台开发者以及数据分析师作为课程设计或研究基线。压缩包大小约105.9MB,其中包含可直接运行的完整源码、已标注的景点评论语料库、系统说明文档与教程案例,虽然上游未提供具体文件数量与类型清单,但项目目录围绕数据预处理、特征提取、模型训练和前端展示分层组织,便于按模块查阅。目前已有123人浏览学习。系统覆盖分词、TF-IDF/词向量特征构造,以及SVM、随机森林或CNN/RNN等分类器的训练与评估流程,并提供可视化界面输出统计结果,能够帮助旅游网站掌握顾客满意度,也为研究人员优化情感分析模型提供真实语料支撑。下载后可以对照文档快速复现实验、替换自有评论数据,并基于预训练模型开展二次开发,是兼顾工程实践与算法探索的综合资源包。

1. 旅游景点方面级情感分析:语料库与模型这个项目到底在解决什么问题

打开任意一个景区的评论区,“环境很好,但排队两小时”这种混合情绪句式是最常见的数据形态。传统情感分析对整个句子的褒贬打一个分值,结果大概率是中性,这对运营方没有任何可操作性。而方面级别情感分析的要求是:从文本里拆出“哪一方面被评价了”,再分别判断“这个方面的态度是正面、负面还是中性”。它不是换了个说法做情绪分类,而是把一个粗粒度分类任务,变成“方面抽取加立场判定”的联合任务。所谓“旅游景点方面级别情感分析语料库与模型 .zip”,拆开看就是两件资产:一份把景点评论按方面维度标好的语料,以及一套能在该语料上完成训练的模型代码。本文将围绕“如何从零构建景点领域语料”“如何设计并训练方面级模型”“参数怎么设、坑在哪儿”“没有标注数据时如何冷启动”四条线,直接给出一线落地时可复用的方案。

2. 旅游景点语料库搭建:采集、清洗、标注与一致性检验

2.1 语料形态:跟通用情感语料比,景点语料多了一个“方面”维度

通用情感分析语料通常只有 sentence 和 label 两列;而景点方面级语料的最小记录单元是“方面项”。每个方面项要同时记录三个方面信息:表达方面字面(term)、方面类别(category)、情感立场(sentiment)。以“索道价格偏贵但风景绝美”为例,这句话里包含两个方面项:“索道价格”对应类别“交通”或“票价”,情感为负;“风景”对应类别“景观”,情感为正。上下文中还有一类常见现象,即“人少景美”这类没有任何实体名词的文本,看不见方面词,却表达了“拥挤程度”和“景观”两个方面的正负评价,这类样本在构建语料时需要特殊标记。

因此,语料文件的设计需要能够容纳一对多映射关系。我推荐采用 JSON Lines 格式,每一个记录是一整条评论,内部的 aspects 数组存所有方面项。每行就是一个独立样本,既方便 workers 按行标注,也方便模型加载时按行随机采样。

2.2 python爬虫与文本清洗:把评论转成可标注的JSONL

要做出“旅游景点方面级别情感分析语料库”,第一步是拿到足量的原始文本。常见做法是写 python爬虫 脚本抓取 OTA 平台公开的评论页,或者使用各平台开放 API。抓取目标建议覆盖多个省份的 5A/4A 景区,尽量保证地域和景区类型多样性,避免语料全集中在单一热点景区上。以下是一段简化但可运行的采集骨架:

import requests import json from parsel import Selector def fetch_comments(page_url, headers=None): """抓单页评论列表,返回评论文本列表。仅用于学习研究,注意遵守平台robots与条款。""" headers = headers or {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"} resp = requests.get(page_url, headers=headers, timeout=10) resp.encoding = resp.apparent_encoding sel = Selector(text=resp.text) items = [] for node in sel.css(".comment-item"): content = node.css(".comment-content::text").get() star = node.css(".star::attr(data-score)").get() if content and star: items.append({"content": content.strip(), "star": int(star or 0)}) return items def clean_text(raw: str) -> str: """清洗:去emoji、去占位符、统一引号、压缩空白。""" import re raw = re.sub(r"[\U0001F300-\U0001FAFF\u2600-\u27BF]", "", raw) raw = raw.replace(" ", " ").replace(""", "\"") raw = re.sub(r"\s+", " ", raw) return raw.strip()

这段代码的抓取部分不算重点,重点是文本清洗逻辑。清洗时不要做中文分词,不要做停用词过滤,更不要在此阶段做正负向判定;方面级模型依赖完整的字词序列,任何提前裁剪都会丢掉方面词边界信息。清洗只负责去除干扰字符和统一格式,清洗后的文本一定要人工抽样检查,尤其是在源码中出现了比较冷僻的字符时,直接丢弃比强行替换更安全。

抓取完成后,按景区分组保存原始 JSON,再由标注工具把批注结果合并成统一格式。注意去重公告数据,同一用户对不同景区的复制粘贴评论在语料库里非常常见,这类噪声不仅拉低标注效率,还会让模型在相似句子上偏向高频模板,需要重点排查。

2.3 标注流程与一致性:用Kappa系数拦住低质量标注

标注 scheme 设计是整个语料库的“宪法”。景点方面类别不宜太少也不宜太多,常见可用 8 到 12 个类别覆盖九成以上的场景。下表是我实际搭建“旅游景点方面级别情感分析语料库”时使用的方面类别体系,可以按业务需求增删:

方面类别说明示例
交通外部交通、接驳、索道“上山巴士太少”
门票/票价门票、优惠、性价比“门票有点贵”
排队/拥挤排队时长、人流量“排队排了一个小时”
景观风景、文化体验本身“云海太震撼了”
餐饮/购物餐饮质量、物价“山顶泡面20一桶”
服务/管理工作人员态度、秩序维护“检票员态度很差”
卫生厕所、垃圾处理“休息区很干净”
设施/安全步道、防护、指示牌“栈道扶手松动”

标注界面直接用一个共享在线表格就能完成,每条评论可以由两个标注者独立标注,然后用 Cohen’s Kappa 衡量两位标注者的一致性。Kappa 不只看“两人标注相同比例”,它要去掉随机一致的部分,因此比准确率更能反映标注质量。计算 Kappa 的 python 代码很短:

def cohen_kappa(c1, c2, n_classes): """ c1, c2: 两个标注者对同批样本的方面类别编码列表, 元素为0..n_classes-1 返回Kappa值; 0.6以下需要退回重标, 0.8以上才可用作训练集 """ import numpy as np c1 = np.asarray(c1); c2 = np.asarray(c2) n = len(c1) po = (c1 == c2).mean() p1 = np.bincount(c1, minlength=n_classes) / n p2 = np.bincount(c2, minlength=n_classes) / n pe = (p1 * p2).sum() return (po - pe) / (1 - pe + 1e-9)

这里 po 是观测一致率,p1/p2 是两位标注者各自标注频率分布,pe 是随机一致概率。如果 Kappa 低于 0.6,不要先怀疑标注者,先看看方面类别定义是不是有重叠,比如“排队/拥挤”和“服务/管理”在实际语境里容易混淆,需要重新修订标注文档。标注数据中有一类特殊情况,即无任何显式方面词的评论,可在 term 字段填 NULL,category 仍按语义归类,这样模型训练时就能学习“隐含方面”的上下文信号。

2.4 语料的目录组织与划分:模型训练前最后一步

建议把整套语料库在项目里组织成三段式目录:raw/存放爬虫原始结果,annotation/存放合并标注之后的 JSONL,split/存放按景区维度切好的 train/dev/test。切分时不能随机按行打乱,因为同一条评论经过转发可能产生多条相似文本,随机切会造成主题泄漏,导致模型在测试集上虚高。

data/ ├── raw/ │ ├── hangzhou_xihu.json │ └── lijiang_yulongxueshan.json ├── annotation/ │ ├── annotation_schema.md │ └── all_comments.jsonl └── split/ ├── train.jsonl ├── dev.jsonl └── test.jsonl

标注完成的 JSONL 单行示例如下:

{"id": "xj_00056", "text": "人少景美,但索道还要排队二十分钟", "aspects": [{"term": null, "category": "拥挤", "sentiment": "pos"}, {"term": "景观", "category": "景观", "sentiment": "pos"}, {"term": "索道", "category": "交通", "sentiment": "neg"}]}

模型输入直接读取该 JSONL,比 CSV 更健壮,因为评论中天然包含逗号和换行符。切分数据时,要保证同一个景区源的所有评论只出现在同一个数据集分区里,这是一个通用的、容易踩坑又必须遵守的规则。最后把类别名称映射成 id,term 空值统一用[UNK]占位,语料库部分就基本完成了。

3. 从全局情感分类到方面级模型:抽取与立场分类联合训练

3.1 任务建模:BIO序列标注加立场分类的双头结构

拿到标注语料后的下一个问题,是设计“模型”的结构。“旅游景点方面级别情感分析”常见且可靠的做法,是把任务分解成两个子任务:方面项边界识别与情感立场分类。边界识别可以建模为序列标注问题,对每个 token 打上 B(方面开始)、I(方面中间)、O(非方面)标记;情感立场则对每个预测出的方面 span 做三分类(pos/neg/neu)。

相比一次性输出完整三元组的生成式模型,双头结构的优势在中小语料上非常明显:前者数据利用率低、容易产生未登录方面词,后者两个任务共享底层语义编码,训练稳定。业界普遍使用的基于 transformer 的“序列标注+分类”联合模型就属于后一类。它先跑一个 BERT 得到上下文向量,再分别接线性分类头;训练时把序列标注的交叉熵损失与立场分类的交叉熵损失相加,一起回传。如果对推理延迟要求较高,也可以把 BERT 换成更轻量的结构。

3.2 基于BERT的实现:一个可训练的ABSAModel

下面给出一份可直接跑的 PyTorch 版模型骨架,核心是双头与 span 特征聚合。代码假设已安装transformerstorch,在 python 环境里装好后即可运行。

import torch import torch.nn as nn from transformers import BertModel, BertTokenizer class AspectABSAModel(nn.Module): """ 双头ABSA模型: head1: 序列标注, 给每个token打B/I/O head2: span情感分类, 对每个span向量预测pos/neg/neu """ def __init__(self, pretrained_name="hfl/rbt3", num_span_labels=3, num_senti_labels=3): super().__init__() self.bert = BertModel.from_pretrained(pretrained_name) hidden = self.bert.config.hidden_size self.span_head = nn.Linear(hidden, num_span_labels) self.senti_head = nn.Linear(hidden, num_senti_labels) def forward(self, input_ids, attention_mask, span_ids=None, span_masks=None): # span_ids: 每个batch样本的每个预测span包含的所有token索引, 展平形状 # span_masks: 与span_ids同形状的0/1掩码, 指示有效位置 h = self.bert(input_ids, attention_mask=attention_mask).last_hidden_state span_logits = self.span_head(h) # [B, L, 3] if span_ids is None: return span_logits, None flatten_h = h.reshape(-1, h.size(-1)) # 展平token向量 span_vec = flatten_h[span_ids] * span_masks.unsqueeze(-1) span_vec = span_vec.sum(dim=1) / span_masks.sum(dim=1, keepdim=True).clamp(min=1) senti_logits = self.senti_head(span_vec) # [B*K, 3] return span_logits, senti_logits

逻辑上,span_head负责方面词边界,senti_head负责对聚合后的 span 向量做情感分类。聚合采用 mean pooling,即把 span 内所有 token 的上下文表示平均,这样无论方面词长短都能得到一个固定维度的向量。span_ids允许不同长度的 span 混合在一个 batch 中,极大简化了训练循环里的数据整理。

训练循环中要特别注意两点:一是不同样品的 span 数量不一,span_ids需要 padding 到 batch 最大 span 数;二是在设计 loss 时,span_logits使用 token 级交叉熵,senti_logits只对有标签的 span 计算交叉熵,不参与 loss 的填充位置要掩码掉。另一个细节是解码阶段,预测时先用 argmax 拿到 B/I/O 标签序列,再按“B 开头接连续 I”的规则切出一个个 span,切分时不要直接把每个标了 I 的连续串全部当作一个 span。

如果需要在这个模型上做“多模态情感分析”的扩展,比如额外拼接景区图片特征,那么可以在last_hidden_state之后加一个 cross-attention 层,把视觉 token 与文本 token 对齐。不过对纯文本语料库来说,上述双头模型已经可以在公开测试集上达到可用水平。

3.3 隐含方面的处理:景区语料里最难标注的一类

“人少景美”这类句子干净利落,却没有任何一个 token 是“方面词”。序列标注模型在这样的样本上几乎无法学到 B/I/O,解决思路分为两层。第一层,在标注时允许 term 为 NULL,让模型把注意力转向上文关系;第二层,在模型结构中为情感分类 head 额外增加一个全局向量——把[CLS]或整句池化向量拼到 span 向量上。这样当某个样本没有显式方面词时,模型至少还能依据整句语义辅助判断“景”“人少”各自映射到什么类别。

cls_vec = h[:, 0, :] # [B, H] expanded_cls = cls_vec.unsqueeze(1).expand(-1, span_vec.size(0) // h.size(0), -1) # 实际需要考虑batch内span数均匀展开; 简化示意省略对齐细节

我在构建语料时遇到的最常见问题是把隐含方面直接标成整句情感。避免方法很简单:规定“一个方面记录必须能独立回答‘哪方面怎么样’”,无法拆出可感知对象时宁可减少一个方面记录,也不要做模糊标注。对模型而言,训练集中有一定比例的 NULL-term 样本会让方面抽取在困难句式上表现更稳,这一经验在“nlp情感分析”的相关讨论中反复被验证。

4. 训练配置、超参数与过拟合防控:小语料也能出稳定结果

4.1 数据划分与训练超参数

假设语料库规模在 5000 条左右,这是多数 python 项目自建语料的常见量级。训练配置不能照抄大模型的默认值,需要按小语料微调。以下是我训练过程中使用的参数表:

参数推荐值备注
模型hfl/rbt3中文轻量RoBERTa,显存占用低
batch size16显存不足可降到8并配合梯度累积
max length128景点评论95%短于该长度
epoch5小语料下不是越多越好
learning rate2e-5span_head与senti_head可用1e-4
warmup ratio0.1前10%步数预热
weight decay0.01缓解BERT过度拟合
label smoothing0.05只加在情感分类头

代码层面只需将上面模型的输出接入 Trainer 或自写训练循环。如果 epoch 数太多,dev 损失会在第 2、3 轮之后反弹,手动保存 dev F1 最高的检查点,而不是直接保留最后一个 checkpoint。这是 transformer 模型详解里反复强调的一个关键点。

4.2 评价指标:span级F1与三元组正确率

方面级模型的在线评估不能只看句子级准确率。常用指标有两个:span 级 F1,即预测的方面边界与真实边界是否完全一致;三元组正确率,即“方面边界+类别+情感”三项同时正确的比例。真实业务场景中,有的下游应用只关心类别与情感(“排队/拥挤”是否为负),有的则额外关心具体对象(“索道”与“售票窗口”不该混淆),两种场景应当采用不同的评估口径。

def compute_triple_correct(pred_triples, gold_triples): """按三元组完全一致计算精确率/召回率/F1""" tp = len(set(pred_triples) & set(gold_triples)) precision = tp / max(len(set(pred_triples)), 1) recall = tp / max(len(set(gold_triples)), 1) f1 = 2 * precision * recall / (precision + recall + 1e-9) return precision, recall, f1

其中每个 triple 的标准形式是(category, term, sentiment)。term 为 NULL 的样本无法匹配任何实体,需要先决定是否纳入评估;如果业务目标是舆情监控,通常会单独统计这类样本的比例,并作为模型能力短板列入下一步优化计划。

4.3 小语料过拟合防控的3个手段

第一,冻结低层参数。BERT 底层的语法知识是通用的,训练时只更新后两层加上任务头参数,可以用更少数据获得接近全量微调的指标。第二种是回译扩增,把原始评论翻译成其他语言再翻译回中文,主要增加句法多样性,对保持情感极性很有效。第三是对抗训练,最常用的是 FGM:在 embedding 梯度方向增加细微扰动再算一次梯度,相当于给模型加正则化,代码参考如下:

class FGM: def __init__(self, model, epsilon=0.5): self.model = model self.epsilon = epsilon self.backup = {} def attack(self): for name, param in self.model.named_parameters(): if param.requires_grad and param.grad is not None: self.backup[name] = param.data.clone() norm = torch.norm(param.grad) if norm > 0: r_at = self.epsilon * param.grad / norm param.data.add_(r_at) def restore(self): for name, param in self.model.named_parameters(): if name in self.backup: param.data = self.backup[name] self.backup.clear()

FGM 在训练循环里使用时,先正常前向计算 loss、backward 得到第一份梯度,然后attack()再次 forward-backward 累积梯度,最后restore()恢复参数再执行 optimizer step。注意 FGM 只能用于训练,推理阶段必须保证 embedding 无扰动,否则输出完全失真。

小语料的另一个隐性问题是类别不均衡,比如“景观”类样本远多于“卫生”类样本。最简单的处理方式是在CrossEntropyLoss中传入weight,数值按各类别样本数的倒数归一化,这比直接欠采样更能保留有用信息。

5. 冷启动与轻量化应用:没有标注数据时怎么把模型用起来

5.1 用大模型打底再蒸馏:低成本产出种子语料

实际项目里常出现一类情况:语料库和模型都没有竞品说得那么完整,现场只有一大批无标注评论。此时用大模型辅助标注是投入产出比最高的路径。具体做法:把待标注评论拼接成 Prompt,限令模型输出 JSON 数组[{"category": "交通", "term": "索道", "sentiment": "neg"}],然后抽几百条人工修正,再计算 Kappa 校验质量。合格后,把大模型输出当作软标签,训练第 3.2 节的小模型,这一步就是常见的蒸馏流程。

比起直接从零训练,这条路径能把“语料库与模型”的启动周期从两周压缩到两三天。代价是大模型有可能瞎编方面词,比如把“风景很美”拆成term: "风景很美",所以必须结合人工抽检,不能全量直接进训练集。

5.2 迁移到新景区与服务化部署:保持方面体系稳定

换一个新景区时,不需要完全重建语料。把已有语料里偏地域性的词(如“玉龙雪山”)替换成[MASK],并少量补充新景区的种子评论,就能保持模型稳定。推理部署时可以用 fastapi 封装一个极简的接口:

# app.py from fastapi import FastAPI, Request from pydantic import BaseModel import torch class Comment(BaseModel): text: str app = FastAPI() @app.post("/predict") async def predict(req: Request, comment: Comment): inputs = req.app.state.tokenizer(comment.text, return_tensors="pt") with torch.no_grad(): span_logits, _ = req.app.state.model(**inputs) preds = torch.argmax(span_logits, dim=-1)[0].tolist() return {"spans": decode_bio(preds, comment.text)}

注意decode_bio需要实现 BIO 到 span 的还原逻辑,最稳妥的方法是遍历标签序列,遇到 B 则开启一个 span,遇到连续 I 则继续延长,遇到 O 则结束当前 span。线上服务务必把模型切到 eval 模式并包在torch.no_grad()里,否则显存占用会随请求数线性上涨。对于容量有限的环境,可以加载半精度权重并把 max length 收缩到 64,延迟大约能再降三成。最终,把模型预测结果连同原始评论片段一起落库,定期回流到标注集中做增量迭代,标注成本会明显低于每次都从头整理。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询