尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

旅游景点方面级情感分析:语料库构建与模型训练实战

旅游景点方面级情感分析:语料库构建与模型训练实战 简介面向旅游景点评论的方面级别情感分析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, headersNone): 抓单页评论列表返回评论文本列表。仅用于学习研究注意遵守平台robots与条款。 headers headers or {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)} resp requests.get(page_url, headersheaders, timeout10) resp.encoding resp.apparent_encoding sel Selector(textresp.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(nbsp;, ).replace(quot;, \) 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, minlengthn_classes) / n p2 np.bincount(c2, minlengthn_classes) / n pe (p1 * p2).sum() return (po - pe) / (1 - pe 1e-9)这里 po 是观测一致率p1/p2 是两位标注者各自标注频率分布pe 是随机一致概率。如果 Kappa 低于 0.6不要先怀疑标注者先看看方面类别定义是不是有重叠比如“排队/拥挤”和“服务/管理”在实际语境里容易混淆需要重新修订标注文档。标注数据中有一类特殊情况即无任何显式方面词的评论可在 term 字段填 NULLcategory 仍按语义归类这样模型训练时就能学习“隐含方面”的上下文信号。2.4 语料的目录组织与划分模型训练前最后一步建议把整套语料库在项目里组织成三段式目录raw/存放爬虫原始结果annotation/存放合并标注之后的 JSONLsplit/存放按景区维度切好的 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 更健壮因为评论中天然包含逗号和换行符。切分数据时要保证同一个景区源的所有评论只出现在同一个数据集分区里这是一个通用的、容易踩坑又必须遵守的规则。最后把类别名称映射成 idterm 空值统一用[UNK]占位语料库部分就基本完成了。3. 从全局情感分类到方面级模型抽取与立场分类联合训练3.1 任务建模BIO序列标注加立场分类的双头结构拿到标注语料后的下一个问题是设计“模型”的结构。“旅游景点方面级别情感分析”常见且可靠的做法是把任务分解成两个子任务方面项边界识别与情感立场分类。边界识别可以建模为序列标注问题对每个 token 打上 B方面开始、I方面中间、O非方面标记情感立场则对每个预测出的方面 span 做三分类pos/neg/neu。相比一次性输出完整三元组的生成式模型双头结构的优势在中小语料上非常明显前者数据利用率低、容易产生未登录方面词后者两个任务共享底层语义编码训练稳定。业界普遍使用的基于 transformer 的“序列标注分类”联合模型就属于后一类。它先跑一个 BERT 得到上下文向量再分别接线性分类头训练时把序列标注的交叉熵损失与立场分类的交叉熵损失相加一起回传。如果对推理延迟要求较高也可以把 BERT 换成更轻量的结构。3.2 基于BERT的实现一个可训练的ABSAModel下面给出一份可直接跑的 PyTorch 版模型骨架核心是双头与 span 特征聚合。代码假设已安装transformers和torch在 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_namehfl/rbt3, num_span_labels3, num_senti_labels3): 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_idsNone, span_masksNone): # span_ids: 每个batch样本的每个预测span包含的所有token索引, 展平形状 # span_masks: 与span_ids同形状的0/1掩码, 指示有效位置 h self.bert(input_ids, attention_maskattention_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(dim1) / span_masks.sum(dim1, keepdimTrue).clamp(min1) 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-4warmup 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, epsilon0.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_tensorspt) 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延迟大约能再降三成。最终把模型预测结果连同原始评论片段一起落库定期回流到标注集中做增量迭代标注成本会明显低于每次都从头整理。本文还有配套的精品资源点击获取
返回列表