恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
旅游评论方面级情感分析实战:语料构建与模型部署
首页
资讯中心
/
旅游评论方面级情感分析实战:语料构建与模型部署
旅游评论方面级情感分析实战:语料构建与模型部署
发布时间:2026/10/19 8:38:41
做一个旅游评论情感分析项目很多人的第一反应是这不就是判断一条评论是好评还是差评嘛。但真正接触过业务需求就会发现光知道这条评论整体是正面的完全不够用。游客在一条评论里可以同时夸风景、骂排队、吐槽门票贵你要的是风景方面是正面的、排队管理方面是负面的、门票性价比方面是负面的这种细粒度结论。这就是方面级情感分析Aspect-Based Sentiment AnalysisABSA解决的核心问题。这个项目是我在某段业务场景里做的一个完整闭环从零构建了一套旅游景点领域的方面级情感分析语料库同时训练了可落地的分析模型源码和文档一并整理交付。整个项目涉及语料标注规范设计、模型选型、训练调参、推理优化四个大块每一步都有可以复现的代码和踩坑记录。无论你是准备做NLP方向的毕设还是做评论挖掘类业务功能这篇文章都能给你提供一条可以直接照走的路线。1. 项目整体设计与思路拆解1.1 ABSA任务定义与旅游场景的特殊性方面级情感分析在学术上通常拆成两个子任务方面抽取Aspect Term Extraction和方面情感分类Aspect Sentiment Classification。前者负责从评论里找出游客具体在说什么对象后者负责判断评论对该对象的情感极性。放在旅游场景里这两个子任务的形态会变得很具体。一条评论昨天去了山上的观景台视野确实开阔但缆车排队快两个小时工作人员态度也一般里面同时涉及观景台/视野这个正面对象、缆车排队这个负面对象、工作人员态度这个负面对象。如果只做一个评论级的情感分类这条评论大概率被标成中性或负面很多有价值的信息就丢了。旅游评论还有两个特殊难点。第一是隐含方面评论里可能根本不出现具体名词比如人太多了体验很不好这里的人太多隐含的是游客密度/排队体验方面模型需要从句意去推断而不只是做词典匹配。第二是方面权重不均衡不同类型的景点自然风景区、主题乐园、博物馆关注的方面差异很大一个刚建好的玻璃栈道景区可能90%的评论都在讲刺激感和安全性传统通用领域的情感词表在这里基本失效。1.2 项目目标与交付边界动手之前我给自己定的目标是三个语料可复用、模型可复现、代码可落地。语料可复用意思是标注规范要公开透明别人拿到标注文档就能理解和延续标注工作而不是只有一堆含义模糊的标签。模型可复现意思是训练脚本要能从原始数据一路跑到指标输出中间不需要人工干预。代码可落地意思是训练好的模型不只是留在Notebook里演示精度而是能封装成接口在真实评论数据上跑出结构化结果。这些目标直接决定了后续所有技术选型。比如我不追求单点指标刷到领域最先进水平而是追求整套pipeline的稳定性和可解释性不在模型结构上玩花活而是用成熟框架把数据质量做到位。1.3 技术路线选型的对比与取舍我对比了三条技术路线花费了不少心思路线一两阶段pipeline方案先做方面抽取再做情感分类两个模型相对独立。优点是每个环节都能单独调试和评估出错时容易定位缺点是误差会累计如果方面抽取漏了某个方面后续情感分类就算再准也白搭。路线二端到端联合模型用一个模型同时输出方面和情感。这类方案理论上可以利用两个任务之间的相关性互相增强但实现复杂度高训练起来也不稳定而且中间表征无法直接检查出了问题很难排查。路线三基于大模型的方案把整条评论塞给生成式大模型让它以结构化格式直接输出所有方面和情感。这个方案在小样本实验里效果其实不错但当时面对两个现实约束一是推理速度不足以支撑线上批量评论分析二是成本高企每万条评论的费用让人心疼。而且大模型的输出格式不稳定偶尔出现标签缺失后续纠错也麻烦。最终我选择了路线一基于BERT两阶段实现。理由很简单这套方案的每个环节都有成熟的评估指标便于我控制项目进度中间产物抽取出的方面词和方面类别可以直接可视化给业务方看沟通成本低后续如果要换更强的主干模型只需要替换特征提取器而不需要重写整个pipeline。2. 语料库构建标注规范与质量控制2.1 数据采集与清洗策略语料来源是公开的旅游平台评论数据采集时只保留了不涉及个人信息的内容字段并且做了去标识化处理。这里必须提醒一句爬取公开评论数据时要关注平台的使用条款和数据合规性我这边是在已有的授权数据集基础上做二次处理并不是从某个具体平台硬爬。原始评论大概收集了30000条清洗后剩约12000条。清洗规则有几条每一轮都在砍数据量但非常有价值去掉纯颜文字和纯表情评论这类样本没有完整的语义信息去掉长度小于10个字的评论比如好好好三个字标注和训练都容易出问题去掉明显是商家模板回复的内容比如感谢您的光临我们会继续努力这类内容没有真实情感对完全相同的文本做去重有些用户会一键连发多条相同评论。清洗完我刻意做了一个分层抽样保证不同热度等级的景点在语料里都有覆盖。这个操作不少见但很关键如果语料全部来自一个网红景点模型学到的词汇分布就非常偏。2.2 方面类别体系设计方面类别是这套语料的核心骨架直接决定模型的输出空间。我没有参考通用情感分析体系直接照搬而是自己先抽样看了500条评论统计高频词再结合旅游场景常识归纳出四大类十二小类的层级结构。大类设计上我当时定了四个自然与景观、交通与设施、服务与管理、餐饮与住宿。大类下面的小类比如自然与景观拆成景色观赏性环境清洁度天气与季节体验交通与设施拆成外部交通到达景区内部接驳排队等待体验等。这个设计当时经历了几轮迭代。一开始我分了六个大类标注到一半发现导游服务和讲解质量标注员经常混在一起不好区分后来干脆合并成服务与管理大类下的人员服务小类。还有性价比这个方面游客经常会说门票不值这个价但值不值很难界定边界我后来把它归入服务与管理大类下单独的小类只标记明确的性价比评价语句。关于粒度的把握我的原则是宁可粗一点保证标注一致性也不要为了炫技设计过细的类别导致标注员吵成一团。最终十二个小类对于业务分析已经够用每个类别在语料中都有足够的样本量支撑模型训练。2.3 标注规约与一致性检验标注规约是整个项目里最花时间的部分。我写了一份完整的标注文档核心规约包括几大块显式方面和隐含方面怎么界定。显式方面指句中出现明确的词比如风景缆车隐含方面指句中没有直接对应名词只能通过语义推断。规约里规定隐含方面如果置信度较高就标如果模棱两可就弃标因为标错了反而误导模型。情感极性一共四级正、负、中、冲突。冲突指一条评论在同一方面上有正反两种态度比如景色很美但人多到崩溃这句话在景色方面是正在排队体验方面是负但如果放在整体体验这个隐含方面上可能就是冲突。规约要求标注冲突时必须写出依据词方便复核。标注流程采用双人独立标注加仲裁机制。两个标注员分别标注用一致性公式算出Kappa系数低于阈值就重标。实操下来前200条语料的Kappa只有0.62主要问题是隐含方面和情感强度的判断差异。后来我们把规约细化把容易争议的例子整理成标注FAQ第二轮Kappa就提到了0.81。这部分我有个很深的体会语料质量不是靠标注员自觉而是靠标注规约的颗粒度。我们前后改了四版规约每一版都是因为标注过程中出现了新的边界案例。最终交付的标注文档有将近40页包含了每一类方面的定义、正例反例、争议场景说明这套规范本身的价值已经不亚于标注数据。3. 模型设计与核心实现3.1 总体Pipeline架构整个处理流程分三步文本预处理、方面抽取、方面情感分类。第一步把原始评论切成标准格式第二步从评论中抽取所有方面词方面类别第三步逐一对每个抽取出的方面判断情感极性。第一步预处理做得很朴素但很扎实包括全角半角统一、繁体转简体、URL和数字串归一化然后按句子切分。旅游评论普遍比较口语化分词甚至不需要单独做因为后面的模型用的是字级输入不存在分词错误传播的问题。这是选BERT的一条隐形红利。第二步方面抽取我用的是序列标注方案标签体系是BIO格式B表示方面词起始I表示方面词内部O表示非方面词。方面词的边界和类别直接做成联合标签比如B-风景I-风景B-服务等。这样模型一次前向就能同时完成找出方面词和判断方面类别两件事。第三步方面情感分类输入的是评论全文方面词方面类别输出是该方面对应的情感极性。这里有个工程细节同一个方面词在全文出现多次怎么办。我当时的处理是取该方面词在评论中第一次出现的位置作为锚点然后把整句输入模型靠注意力机制去定位对应的情感表达词。这个方案实测比单独截取窗口效果好因为有些情感词跨句子出现比如虽然缆车排队很长但是上面风景真的值了情感判断需要看到后句才能反转。3.2 方面抽取模型BERTBiLSTMCRF方面抽取的模型结构不是简单的BERT加全连接层而是BERTBiLSTMCRF三层结构。BERT负责把每个字编码成上下文向量BiLSTM在BERT输出基础上进一步建模序列依赖CRF层负责在解码时保证标签序列的合法性比如避免出现B-风景后面直接跟O再跟I-风景这样不符合BIO约束的序列。这个结构对方面抽取来说非常适配。CRF层带来的约束看着不起眼实际上能直接砍掉一批低级的标签顺序错误。做对比实验时BERTBiLSTMCRF相比纯BERTSoftmax在方面抽取F1上提升了大概1.8个百分点主要提升就来自于消除了不合法的标签序列。前向传播时我做了两次实验对比一是直接用BERT对每个字输出标签概率二是在中间加一层CRF。第二次的效果更稳定但代价是推理速度有所下降。考虑到后续要批量跑历史评论数据做分析CRF带来的收益还是值得的。3.3 方面情感分类模型基于位置信息的注意力加权方面情感分类我用的是BERT位置编码的方案。具体来说输入格式是[CLS] 评论全文 [SEP] 方面词 [SEP]然后在BERT的隐层向量上把对应方面词位置附近的token权重放大再做池化分类。这么做的原因是虽然评论全文方面词两个segment的输入方式已经能让模型感知到在分析哪个方面但直接全量池化容易把噪声句子和真正表达情感的句子混在一起位置加权可以强化方面上下文的信息。位置加权的思路并不复杂对输入中的每个token计算它到方面词首次出现位置的相对距离然后生成一个权重向量距离越近权重越大。这个权重和BERT输出的隐向量逐元素相乘后再做平均池化接一个三层MLP输出四个情感极性的概率分布。实际测试中这个方案比单纯用[SEP]分隔输入的分类模型F1高2个点左右。原因是旅游评论中情感词大量集中在方面词前后的一小段范围显式的位置信息等于给模型提供了一个先验注意力分布。3.4 训练细节与参数配置训练阶段的超参数我是这样定的基本沿用了中文BERT的标准配置预训练模型用的是中文BERT-base序列最大长度128batch size 16。学习率方面BERT主干学习率设为2e-5下层MLP学习率设为5e-5用的是分层学习率策略。优化器是AdamWwarmup比例0.1训练轮数方面抽取模型约4轮、情感分类模型约3轮配合早停机制防止过拟合。这里要特别提一下类别不平衡问题。旅游评论的情感极性分布天然不均衡正面样例占了近六成负面大约两成五中性和冲突各占不到一成。如果直接按原始分布训练模型会把所有样本都倾向预测为正面表现在业务上就是负面评论经常漏报。我的处理办法有三个组合使用效果最好情感分类的损失函数改用Focal Loss降低易分类样本的loss权重让模型更关注难分的负面和中立样本训练时对负面样本做过采样但比例控制在1.2倍以内避免过分改变真实分布评估时坚持用宏平均F1而不是准确率因为准确率在类别不均衡下太容易虚高。最终评估结果方面抽取的F1是87.2方面情感分类的宏平均F1是83.5。两个数字单看都还不错但组合成一个pipeline之后端到端的准确率会打一些折扣因为第二步的错误有一部分是由第一步的遗漏带来的。这个误差传播问题在后面章节会细说。4. 实操过程从数据预处理到模型部署4.1 数据预处理代码结构整个项目的代码组织结构按照数据-模型-服务三层拆分源码里每个目录都有对应的README说明。核心预处理逻辑大概长这样import re from collections import Counter def clean_review(text: str) - str: # 统一全半角、繁体转简体、URL/数字归一化 text text.replace(, ,).replace(。, .).replace(, !).replace(, ?) text text.replace( , ) text re.sub(r(https?://)?\S\.(com|cn|net|org)[^\s]*, URL, text) text re.sub(r\d, NUM, text) return text def split_sentence(text: str) - list: parts re.split(r[!?。], text) return [p.strip() for p in parts if len(p.strip()) 1]切句的粒度可以看成是给模型准备的专业内容单元。旅游评论里一句话往往就是一个完整的方面情感组合把这个单元喂给模型比直接整段喂更容易对齐标签。标签编码我放在了独立的模块里保证训练、验证、推理三段代码共用同一套映射表。这里有个教训前期项目里有过一次训练和推理用了不同标签映射的情况上线后模型输出乱码排查了半天才找到原因。从那以后我把编码表和标签名都做了版本号管理用单独的文件保存谁都不敢拍脑袋改。4.2 训练脚本与评估逻辑训练脚本的设计目标是一键复现所以我把所有可调参数都抽到了配置文件里包括模型路径、数据路径、学习率、epoch数、随机种子等。配置文件里刻意锁死了随机种子目的就是让复现实验时能拿到一模一样的结果。from transformers import AutoTokenizer, AutoModelForTokenClassification, Trainer, TrainingArguments tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForTokenClassification.from_pretrained(bert-base-chinese, num_labelslen(label2id)) training_args TrainingArguments( output_dir./checkpoints/aspect_extract, learning_rate2e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs4, warmup_ratio0.1, evaluation_strategyepoch, save_strategyepoch, logging_steps50, seed42, fp16True, )评估代码里我做了一个比较得意的设计除了常规的精确率、召回率、F1还单独统计了方面级混淆矩阵。比如餐饮类别被错分成服务的概率是多少风景方面被漏掉的句子有什么共性。这个矩阵看起来不起眼但定位错误类型非常有用后面做badcase分析全靠它。4.3 模型推理封装与性能优化训练完成的模型不能直接拿来用我封装了一个分析服务类对外暴露三个方法抽取方面、判断情感、整体分析。整体分析内部串行调用前两个方法直接返回结构化结果class TourismAbsaService: def __init__(self, extract_model_path: str, sentiment_model_path: str): self.extractor AspectExtractor(extract_model_path) self.sentiment SentimentClassifier(sentiment_model_path) def analyze(self, review: str) - dict: aspects self.extractor.extract(review) results [] for aspect_word, aspect_type in aspects: polarity self.sentiment.predict(review, aspect_word, aspect_type) results.append({ aspect_word: aspect_word, aspect_type: aspect_type, polarity: polarity }) return {review: review, aspects: results}推理性能是踩过坑的。最开始直接用原生PyTorch跑单卡GPU上一条评论大概耗时80毫秒看着不多但批量分析10万条历史评论就得好几个小时。后来做了两个优化把模型导出成ONNX格式配合动态输入维度单条推理耗时压到40毫秒左右推理时做了动态batch聚合把多条评论按长度分组后一次性喂给模型整体吞吐提升了接近一倍。这里有个细节值得说动态batch聚合不是简单地把所有评论直接塞进一个batch而是按长度相近的分组避免同一个batch里长短差异过大导致padding浪费。40毫秒的单条延迟用来做离线批量分析完全够用如果将来要接实时接口可以再考虑量化但短期内没必要为了几毫秒牺牲精度。5. 踩坑实录与问题排查技巧5.1 标注不一致花了最多时间的坑项目前期最大的瓶颈不是模型而是标注质量。两个标注员对同一条评论的标注结果Kappa系数初期只有0.6左右意味着有将近四成样本的标注是有争议的。排查后发现分歧主要集中在两类样本一是隐含方面标注员对人多到底算排队体验还是游客管理各执一词二是多情感共存的句子不同标注员对冲突情感的拆分粒度不一样。解决方案是逐条梳理争议样本把高频争议点写进标注规约的FAQ里并且强制要求标注员每周做一次校准会议只看最近一周标注结果中的分歧案例。这个方法听着很笨效果却立竿见影两周后Kappa就稳定在0.8以上了。5.2 模型漏识别负面评论问题第一次跑完情感分类模型整体F1是0.79看起来还行但一拆类别就发现严重问题负面样本的召回率只有0.58。这意味着四成左右的负面评论被模型漏掉了。排查下来有两层原因。一是数据层面的负面样本占比太少训练时模型见到的负面模式不足二是情感表达的多样性旅游评论里的负面表达经常是隐性的比如等了两个小时门票涨了没有强烈的负面情绪词模型很难从字面上判断。针对第一层原因我用Focal Loss加过采样解决针对第二层原因我给训练数据加了位置信息辅助模型学会关注等待时间价格变动这类隐含负面指标词。两轮迭代之后负面召回率提到了0.76全局宏平均F1也升到了0.835。5.3 长尾方面类别的冷启动问题天气与季节体验这个类别在全部语料里只有两百多个样本模型在这个类别上的F1一度低到0.42。这是典型的长尾类别冷启动问题模型遇到过少几乎学不到稳定的模式。我的处理办法是回填数据从原始30万条未标注评论里先用模型粗筛一遍把包含天气下雨晴天等关键词的评论捞出来再由人工快速标注总共补了两千多条样本。这个办法的代价是增加了大约三天的标注工作量但换来了该类别F1从0.42稳定提升到0.78性价比很高。5.4 端到端误差传播的量化分析pipeline方案必然存在误差传播但具体传播多少我做了量化方面抽取单步F1是0.872情感分类单步宏F1是0.835理论上端到端应该有0.73左右。实际端到端F1是0.69差了将近4个点。这4个点主要来自于方面抽取漏抽导致情感分类无米下锅和方面抽取抽错类别导致情感分类判断错对象两类错误。解决误差传播我试过两个方向。一个方向是训练一个端到端的纠错模块对pipeline输出做二次校验但收益不明显还引入了新的复杂度。另一个方向是提升第一步精度重点优化方面抽取的边界识别——就是把风景这种一个词的方面词给准确定界别把风景很漂亮但是一起抽出来。后者的效果更直接最终端到端F1提升到了0.72已经接近理论值。6. 可扩展方向与个人体会6.1 从预训练模型到大模型的衔接思路现在回看这个项目一个很自然的后续方向是利用大模型做语料增强或模型蒸馏。大模型的强项在于少样本理解和复杂情感推断可以直接用它对低置信度样本做二次标注也可以把它生成的伪标签作为监督信号来训练小模型。我建议的做法是保留现在两阶段pipeline作为推理主框架用大模型做离线的困难样本挖掘和标签修正这样既能提升语料质量又不用承担大模型的推理成本。6.2 跨景点类型迁移时要注意什么不同景点类型的评论分布差异很大把自然风景区训练好的模型直接迁到主题乐园上效果会打折扣。如果要做迁移至少需要做两件事一是对比目标领域语料和源领域语料的方面分布必要时扩充类别体系二是用少量目标领域数据做微调迁移学习需求几百条数据就能有明显改善。这方面值得进一步展开做跨领域实验。6.3 我的几点实际体会这个项目做下来最大的体会就一句话方面级情感分析项目80%的成败在数据20%在模型。模型结构再先进也扛不住标注混乱、类别定义不清、样本分布畸形的数据。所以如果让我重来一次我会把更多时间花在标注规约设计和质量校验上而不是反复调整模型结构。第二个体会是两阶段pipeline虽然被很多论文说成过时方案但它在工程落地的时候依然表现稳定因为每一步都能检查、能干预、能量化损失。对要做实际业务的人来说稳定可维护比单点指标好看重要得多。最后给准备复现这个项目的同学一个建议不要一上来就追求大而全的所有类别体系先定四个大类、跑通整套流程后面再迭代增加细分类别。先活下去再优化这是所有NLP项目通用的原则。另外还有一个小技巧想分享一下对于评论中频繁出现的同一方面词不同表达比如景色风景view在做数据预处理时可以把它们人工归并成同一组候选项这样对后面的情感分类是很有帮助的因为模型不需要再从零学习这些词其实说的是同一个方面。这个小操作看着不起眼实测下来能把个别类别的F1提高1到2个点而且几乎不增加成本。