ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

旅游评论方面级情感分析:从语料库标注到模型部署

旅游评论方面级情感分析:从语料库标注到模型部署 简介面向毕业设计的Python旅游景点方面级别情感分析语料库建设与模型实现资源包适合正在准备自然语言处理或情感分析相关课题的本专科学生也适合需要快速搭建完整项目框架的开发者。内容围绕语料库标注、模型实现与系统测试展开完整覆盖相关开发技术介绍、可行性分析、性能需求分析、系统总体设计、语料库标注内容设计、数据库设计以及系统首页、文本列表、分类操作、标注页面和新增用户等模块实现层次清晰。压缩包约74.75MB主要文件类型包括Python源码、MySQL数据库脚本和说明文档源码可直接运行或二次开发数据库提供预设表结构与语料样例说明文档按章节梳理从需求到测试的毕业设计撰写思路。目前已有306人学习下载对于需要参考系统设计、补充实验数据或准备论文答辩的读者具有较高的实操借鉴价值。1. 旅游景点评论的方面级情感分析核心不是模型而是语料库旅游景点评论的方面级情感分析核心不是模型有多少层而是有没有一份能支撑训练的中文语料库。用整体情感极性来判断景区评论效果很差一句“风景确实不错但缆车排队两个小时”整体正向可运营改进点其实在“缆车”。本项目正是把这类问题拆成方面级情感分析来解先用标注语料库沉淀“方面词-属性-极性”三元组再以 Python 生态实现管理系统和分类模型最后通过 B/S 窗口完成浏览、标注、导出和评估。资源包含源码、MySQL 数据库脚本和说明文档适合课程设计、毕设起步以及想快速搭建 ABSA 流程的工程师参考。2. 方面级情感分析语料库标注规范与一致性校验2.1 从原始评论到 aspect-polarity 三元组文本情感分析有三个粒度篇章级、句子级、方面级。方面级情感分析需要从一条评论中自动发现观点目标aspect及其情感极性polarity。对于“风景确实不错但缆车排队两个小时”原子事件可以拆成方面词风景 → 极性 positive方面词缆车 → 极性 negative如果只按整条评论打标签得到的是 positive信息量远不够。语料库的价值就在这里把不可直接训练的非结构化文本转成结构化三元组(评论ID, 方面词, 类别, 极性)。本项目的标注范围限定在旅游景点相关评论常见类别包括风景、交通、门票、餐饮、住宿、服务、排队时长等。为了让不同标注员按同一口径打标需要在设计阶段把“方面词”和“极性”的取值完全锁定避免出现“不错”“还行”这种无法计算的潦草结论。我在处理这一类中文评论时会把“对比转折”作为重点标注对象。凡是出现“但是”“不过”“就是”等转折词前后两个子句需要分别抽取方面。这个规则放在标注说明文档最前面能明显减少初标阶段的返工量。2.2 标注字段、字典与 MySQL 数据表设计数据库表设计是整个语料库工程项目里最基础的一环它决定了标注页面、导出脚本和模型训练脚本要怎么写。这个项目不需要额外引入向量数据库——语料规模在百万条以下时MySQL 的关系模型完全够用查询、聚合、权限控制都更贴近工程习惯。项目数据库命名为tourism_absa核心表设计如下。表名字段类型说明sample_commentid,content_text,source_url,is_active,created_atINT / TEXT / VARCHAR景点评论原始文本aspect_annotationid,sample_id,annotator_id,aspect_term,aspect_category,polarityINT / VARCHAR标注结果一条评论对应多条记录sys_polaritycode,labelVARCHAR枚举字典POSITIVE / NEGATIVE / NEUTRAL标注状态放在aspect_annotation.status字段0 代表待标注1 代表标注中2 代表已确认3 代表审核不通过。这样在导出训练集时只需要筛选status 2的记录审计线上效果时也能回查是谁、在什么时间改过标签。对应的建表 DDL 片段如下CREATE DATABASE IF NOT EXISTS tourism_absa DEFAULT CHARSET utf8mb4; CREATE TABLE sample_comment ( id INT PRIMARY KEY AUTO_INCREMENT, content_text TEXT NOT NULL, source_url VARCHAR(255) COMMENT 评论来源链接, is_active TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT旅游景点评论原始表; CREATE TABLE aspect_annotation ( id INT PRIMARY KEY AUTO_INCREMENT, sample_id INT NOT NULL, annotator_id INT NOT NULL, aspect_term VARCHAR(64) NOT NULL COMMENT 方面词如缆车/风景, aspect_category VARCHAR(32) NOT NULL COMMENT 方面类别如交通/景观, polarity VARCHAR(16) NOT NULL DEFAULT neutral, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_sample (sample_id), KEY idx_status (status) ) ENGINEInnoDB COMMENT方面级标注结果表;这里使用utf8mb4而不使用utf8是因为景点评论中可能包含 emoji 表情、生僻字和特殊符号utf8mb4可以覆盖四字节字符避免写入时出现Incorrect string value错误。aspect_term用的是可变长字符串中文场景下 64 个字符足够容纳绝大多数观点目标“方面类别”单独成一个字段是为了后期按类别做统计分析和类别均衡采样。提示如果 MySQL 版本低于 5.5无法使用utf8mb4与ON UPDATE DATETIME的组合建议先升级到 5.7 以上再导入tourism_absa.sql。2.3 标注一致性计算与质量筛选多个标注员同时标同一批数据时首先需要量化“大家是否标得一致”。一般使用 Cohens Kappa 而不是简单准确率因为简单准确率无法排除随机一致的影响。下面这个函数可以直接从数据库中读取两位标注员对同一批评论的方面词极性判断并计算 Kappa。def cohen_kappa(labels_a, labels_b): # labels_a 和 labels_b 是等长的列表元素为字符串格式的极性标签 categories sorted(set(labels_a) | set(labels_b)) matrix [[0] * len(categories) for _ in range(len(categories))] index {label: i for i, label in enumerate(categories)} for lab_a, lab_b in zip(labels_a, labels_b): matrix[index[lab_a]][index[lab_b]] 1 total len(labels_a) if total 0: return 0.0 po sum(matrix[i][i] for i in range(len(categories))) / total row_sum [sum(row) for row in matrix] col_sum [sum(matrix[i][j] for i in range(len(categories))) for j in range(len(categories))] pe sum(row_sum[i] * col_sum[i] for i in range(len(categories))) / (total * total) return (po - pe) / (1 - pe) if pe ! 1 else 0.0函数中po是观测一致率pe是随机一致率期望。Kappa 值高于 0.7 时说明标注达到可接受的质量低于 0.5 时就要排查标注说明中的歧义点比如“价格还行”到底算 neutral 还是 positive。筛选完成后可以通过下面的 SQL 直接查看每个类别的分布SELECT aspect_category, polarity, COUNT(*) AS cnt FROM aspect_annotation WHERE status 2 GROUP BY aspect_category, polarity ORDER BY cnt DESC;这一步输出的类别分布直接决定后面模型训练是否要做类别权重或欠采样。“排队时长”通常负例扎堆“住宿”则容易集中在 neutral如果不看分布就直接训练模型会对小类别召回率非常低。3. Python B/S 架构实现从标注闭环到训练数据导出3.1 按 MVC 拆分的 Flask 应用与数据库连接层这个项目的 Web 端采用 B/S 结构标注人员只需打开浏览器就能完成新增用户、浏览文本列表、给文本分类、提交标注等操作。服务端采用 Flask 实现传统 MVC 三层app/controllers/路由层负责接收请求、调用业务逻辑、返回页面或 JSONapp/models/数据层封装 MySQL 读写不直接放 SQLapp/views/templates/模板层渲染 Jijia2 页面。实际目录结构通常在资源包的src或web目录下。核心路由与视图函数的映射如下表路由方法视图函数功能/GETindex()系统首页 / 文本列表/annotatePOSTcreate_annotation()保存标注结果/exportGETexport_dataset()导出训练 CSV我不建议把数据库连接写在每个视图函数里而是统一放在app/models/db.py中# app/models/db.py import pymysql from pymysql.cursors import DictCursor def get_connection(): conn pymysql.connect( host127.0.0.1, userroot, password******, databasetourism_absa, charsetutf8mb4, cursorclassDictCursor, autocommitFalse, ) return conn这里把autocommit设为False是为了在批量插入标注结果时先做完整性校验最后统一commit避免半途失败导致脏数据。charset与建表时的utf8mb4保持一致防止 Python 字符串与 MySQL 通信时出现字符集转换乱码。注意cursorclassDictCursor让查询结果以字典形式返回视图层用row[aspect_term]比用row[0]可读性高很多。3.2 文本分类标注页面的核心业务逻辑系统首页会列出待标注的评论点击进入“文本分类标注页面”后右侧显示当前评论内容操作区让标注员填写方面词、选择类别和极性。标注页面的表单是动态生成的不同类别的单选按钮绑定到同一个polarity字段。提交时走下面的路由# app/controllers/annotate_controller.py from flask import Blueprint, request, jsonify from app.models.db import get_connection annotate_bp Blueprint(annotate, __name__) annotate_bp.route(/annotate, methods[POST]) def create_annotation(): sample_id request.form.get(sample_id, typeint) aspect_term request.form.get(aspect_term, ).strip() aspect_category request.form.get(aspect_category, ).strip() polarity request.form.get(polarity, neutral).lower() if not sample_id or not aspect_term: return jsonify({code: 400, msg: sample_id 和 aspect_term 不能为空}), 400 conn get_connection() try: with conn.cursor() as cursor: cursor.execute( INSERT INTO aspect_annotation (sample_id, annotator_id, aspect_term, aspect_category, polarity, status) VALUES (%s, %s, %s, %s, %s, %s) , (sample_id, 1, aspect_term, aspect_category, polarity, 2) ) conn.commit() return jsonify({code: 200, msg: 标注成功}) except Exception as exc: conn.rollback() return jsonify({code: 500, msg: str(exc)}), 500 finally: conn.close()参数说明request.form.get(sample_id, typeint)利用 Flask 的类型转换器把表单里的字符串sample_id直接转成整数如果为空会返回Nonepolarity统一先转小写避免页面传入POSITIVE和数据库里的positive不一致status直接置为2在演示环境里代表“标注员已确认”真实多轮标注流程会先置为1等另一位标注员复审后再改状态。在写这段业务时最容易踩的坑是长评论导致的超时。Flask 默认没有请求体大小限制但浏览器端如果一次 POST 返回超大内容可能导致代理层直接断开。我一般会在前端把“方面词”和“方面类别”做成自动补全的下拉框限制输入长度并让提交按钮在请求期间禁用防止重复点击产生重复标注。3.3 目标数据集导出与类别均衡处理模型训练不是直接连数据库读记录而是先导出一个稳定的快照文件。导出时只选择已经确认的标注且要求同一评论至少包含两个不同方面词的记录否则这组数据对方面级任务的学习价值有限。# export_dataset.py import pandas as pd import pymysql conn pymysql.connect( host127.0.0.1, userroot, password******, databasetourism_absa, charsetutf8mb4 ) sql SELECT c.content_text, a.aspect_term, a.aspect_category, a.polarity FROM aspect_annotation a JOIN sample_comment c ON a.sample_id c.id WHERE a.status 2 AND EXISTS ( SELECT 1 FROM aspect_annotation b WHERE b.sample_id a.sample_id AND b.status 2 GROUP BY b.sample_id HAVING COUNT(*) 2 ); df pd.read_sql(sql, conn) conn.close() df.to_csv(dataset/aspect_train.csv, indexFalse, encodingutf-8-sig) print(df.groupby([aspect_category, polarity]).size())导出格式是标准的 CSV一行为一个“评论-方面词”对。encodingutf-8-sig是为了兼容 Windows 上的 Excel 打开乱码问题如果后续直接用 Python 训练改回utf-8更节省空间。为什么要求同一条评论至少有两个方面词因为方面级情感分析的核心场景就是一句评论包含多个观点如果训练数据里全是单方面评论模型在推理阶段就会倾向于把整句情绪映射到唯一的方面词上遇到“风景很好但排队太久”这种句子时第二个方面词很容易被忽略。4. 模型实现把标注语料训练成可本地加载的情感分类器4.1 先跑 Baseline 还是直接上 Transformer方面级情感分析在工程上一般有两种建模路线路线 A将评论和方面词拼接成一条输入文本使用文本分类模型输出三分类极性路线 B对每个方面词单独抽取上下文表示训练一个序列标注或抽取模型再分类极性。这里推荐路线 A因为它更容易利用现有预训练模型且在标注数据较少时比端到端抽取模型更稳定。容易踩的误区是不要一上来就用最大尺寸的模型。如果标注样本只有几千条BERT 类模型很容易过拟合训练集准确率逼近 0.95而验证集 macro-F1 不到 0.7。所以我会先用 TF-IDF LogisticRegression 建立基线再切到预训练模型对比收益。教研版的源码包里一般同时保留baseline_lr.py和train_bert.py两个入口。方面级情感分析属于文本模态任务不涉及多模态情感分析中的图片、语音分支。这样切分可以让标注流程聚焦在“文字观点”上避免因图片内容干扰导致标注口径漂移。Transformer 模型的具体结构这里不做完整展开只提醒一点在方面级任务中评论与方面词拼接后的位置编码对结果影响很大不要把max_length调得过小。4.2 基于 PyTorch 的本地模型训练脚本训练入口是一个标准的 HuggingFace Trainer 脚本。输入数据从dataset/aspect_train.csv读取构造text [SEP] aspect_term作为模型输入。下面给出核心训练片段# train_bert.py import pandas as pd from sklearn.model_selection import train_test_split from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments, ) df pd.read_csv(dataset/aspect_train.csv) label2id {positive: 0, neutral: 1, negative: 2} df[label] df[polarity].map(label2id) train_df, valid_df train_test_split( df, test_size0.2, random_state42, stratifydf[label] ) model_name bert-base-chinese # 首次会下载之后可以缓存到本地离线加载 tokenizer AutoTokenizer.from_pretrained(model_name) def tokenize_examples(texts, aspects, labels): inputs tokenizer( texts, aspects, paddingmax_length, truncationTrue, max_length128, ) inputs[labels] labels return inputs # 实际训练时请对 train_df / valid_df 调用 tokenize_examples # 再用 torch.utils.data.TensorDataset 封装后传给 Trainer。参数说明max_length128控制单条输入长度。景点评论通常不超过 200 字128 足以覆盖绝大多数上下文超过部分会被截断train_test_split中的stratifydf[label]在划分时按标签比例采样避免类别分布随机漂移paddingmax_length会固定补齐到 128 token适合小数据量实验如果样本量超过 5 万建议改为paddingTrue按 batch 动态 padding能明显加快训练速度。训练完成后模型权重和词表会保存到./checkpoint/目录。后续部署时使用AutoModelForSequenceClassification.from_pretrained(./checkpoint/)从本地加载模型不再依赖外网连接。这一点在“B/S 架构本地模型服务”中很重要太多工程故障都发生在服务器上临时在线下载模型失败导致整个推理接口不可用。4.3 评估指标、类别不均衡与错误分析只盯着准确率评估方面级情感分析会失真。如果“neutral”占到全部样本的 60%一个“全部预测为 neutral”的模型准确率也有 0.6。因此必须看 macro-F1 和每个类别的分类报告。from sklearn.metrics import classification_report # preds 和 labels 分别是模型输出和真实标签 # target_names 按 label2id 的键顺序传入 report classification_report( labels, preds, target_names[positive, neutral, negative], digits4, ) print(report)同一份语料在不同建模方案上的预期效果量级如下表模型准确率Macro-F1训练速度适用阶段TF-IDF LogisticRegression0.700.64秒级基线验证、类别均衡检查BiLSTM Attention0.730.68分钟级小规模深度学习实验BERT-base-Chinese0.800.76GPU 加班级正式实验与最终部署表格数据是我在类似规模中文评论语料上的经验值不是资源包里写死的结论。拿到源码后实际跑出的宏观数字与标注质量、样本量、评论领域强相关。错误分析时优先看混淆矩阵里negative被误判为positive的记录。这类错误往往发生在“风景不错可惜缆车太久”这种转折句模型把aspect_term里的“缆车”当成整体正面语境的附属物。如果这种错误占比高可以尝试把输入格式调整为aspect_term放在开头再拼接原文比如缆车[SEP]风景不错可惜缆车太久让方面词处于更早的注意力位置。类别不均衡严重时可以在TrainingArguments对应的数据采样器里加WeightedRandomSampler给样本量小的 polarity 提权。5. 进阶技巧把训练好的模型封装成本地情感分析服务在动手跑服务前需要先确认 Python 环境。Python 安装本身并不复杂但要注意 3.8 以上版本才能完整支持transformers的训练与推理低于 3.8 时部分动态图接口会报类型错误。语料库标注和模型训练都完成之后最后一个实用步骤是让系统能在本地持续对外提供预测而不是每次跑完训练就关机。这里的关键是把模型加载、数据校验、请求响应放到同一个常驻进程中。# serve_model.py import torch from flask import Flask, request, jsonify from transformers import AutoTokenizer, AutoModelForSequenceClassification app Flask(__name__) model_path ./checkpoint/ tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForSequenceClassification.from_pretrained(model_path) model.eval() label_map {0: positive, 1: neutral, 2: negative} app.route(/predict, methods[POST]) def predict(): payload request.get_json() text payload.get(text, ).strip() aspect payload.get(aspect, ).strip() if not text or not aspect: return jsonify({error: text and aspect are required}), 400 inputs tokenizer( text, aspect, truncationTrue, paddingmax_length, max_length128, return_tensorspt ) with torch.no_grad(): logits model(**inputs).logits pred_id int(torch.argmax(logits, dim-1).item()) return jsonify({aspect: aspect, polarity: label_map[pred_id]}) if __name__ __main__: app.run(host0.0.0.0, port8000)这段服务代码里有两个容易忽略的点。第一模型实例在模块加载时创建不能放在predict函数内部否则每个请求都要重新加载一次权重压力测试时响应时间会从几十毫秒爆炸到几秒。第二推理阶段用torch.no_grad()包裹关闭自动求导图推理显存占用会明显下降。启动服务后在另一台机器上可以用curl做冒烟验证curl -X POST http://127.0.0.1:8000/predict \ -H Content-Type: application/json \ -d {text: 风景确实不错但缆车排队太久, aspect: 缆车}预期返回{aspect: 缆车, polarity: negative}。如果返回的是positive需要检查训练数据里是否缺少“转折子句分别标注”的样例并回到标注平台补充该类型数据。这个本地服务还可以继续加一层极小改动在加载模型时把torch.set_num_threads(4)写进去限制 CPU 环境上的并发线程数避免多请求时上下文切换开销过大。对于静态语料离线预测这个线程数设置通常比启动多个 worker 更有效。本文还有配套的精品资源点击获取
返回列表