
简介这份毕业设计资料包围绕旅游景点评论情感分析任务展开采用Python语言与Django框架开发并通过RNCC模型实现方面级别情感分类。系统涵盖用户管理、评论文本采集与标注、自动分类及统计展示等模块首页展示用户量、累计评论数、已标注与未标注评论数并以柱状图呈现好评、中评、差评分布文本列表界面能够查看每条评论的编号、内容与标注状态并支持直接删除分类功能可输入景区相关文本点击后自动判定情感极性并弹出结果。整个项目从数据库设计到前端页面均有清晰实现适合计算机、软件工程等专业学生借鉴学习或二次开发。压缩包约82MB内含完整源码、MySQL数据库脚本和演示视频其中数据库脚本提供建表语句与示例数据演示视频辅助快速部署运行。目前已有193人学习下载对掌握情感分析语料库构建、RNCC模型集成以及Django与MySQL协作开发具有实际参考价值。1. 旅游景点方面级别情感分析游客的“又爱又恨”为什么必须拆开算账一个游客在OTA平台留下这样的评论“景色是真漂亮但排队排到怀疑人生门票还不便宜。”如果用整体情感分类器给这句话打分出的结果一定很尴尬——句子前半句强烈正面后半句强烈负面加权平均后变成一个模棱两可的中性分。但对景区运营方来说“景色漂亮”和“排队严重”是两条完全独立的业务信息揉在一起等于没分析。旅游景点方面级别情感分析要做的就是把一条评论按评价对象拆碎逐个判断情感极性。这个课题用到的是python生态里非常成熟的一条NLP流程先构建或复用一份带方面标签的语料库再做基于transformer微调的情感分析模型最后把语料、标注和预测结果存进MySQL。对毕业设计而言它同时覆盖了数据处理、模型训练和系统展示三个环节工作量适中、可视化强、答辩点清晰。适合想走NLP方向的学生也适合想从用户评论里挖结构化信息的从业者参考。2. 为什么整体情感分析会翻车方面抽取与模型选型2.1 一条“又爱又恨”的评论让整体模型怎么选都错我试跑基线模型时反复遇到这类输入“风景不错但停车收费太黑”。常规的文本情感分类器给它的概率分布往往是正面四成、负面四成、中性两成——模型很“诚实”因为它确实不知道该站哪边。整体sentence-level情感分析把整句话当成一个情感单元遇到包含多个评价对象的句子信息必然互相稀释。对景区来说“风景不错”和“停车收费黑”是两个独立的业务问题一个该继续宣传一个该整改停车场揉在一起等于全丢。方面级别aspect-level情感分析把任务拆成两个子任务第一步从评论文本中抽取出被评价的方面词或方面类别旅游场景里通常是景点景观、交通出行、门票价格、餐饮购物、服务管理这几类第二步对每个方面单独做情感极性判断输出正面、中性或负面。上面那句话会被结构化地标记为风景→正面停车收费→负面。模型的输出从一条模糊的极性分数变成一条可落库、可统计、可做经营决策的结构化记录。这种结构化输出的价值在于可聚合。同样是“又爱又恨”的一千条评论整体模型只能告诉你有争议方面级模型能告诉你景观正面率92%交通负面率61%门票负面率45%。景区拿到手的就是一张能排优先级的问题清单。这也是为什么很多真实业务系统宁可放弃整体的“好评率”也要做方面级统计。2.2 方面抽取先搞清楚评论在说哪个对象方面抽取Aspect Term Extraction是方面级分析的第一道关口词都抽不准后续情感判断就是无源之水。常见做法有三类按落地成本从低到高排。第一类是词典匹配。手工维护一份旅游领域方面词表比如“风景、门票、缆车、排队、停车场、纪念品”再用精确或模糊匹配在原句里定位。配合jieba分词做词边界切分后在受限语料上的命中率能做到六七成。优点是零训练、纯规则、完全可解释缺点也明显词表外的表达全部漏掉“人挤人”这种形容排队的口语说法不进词表就永远抽不出来。第二类是依存句法。用LTP或spaCy对评论做依存解析找形容词通常承担情感词角色通过nsubj、advmod这类依存弧关联到的名词中心词把名词中心词当作方面词。这个方案能发现词表外的新词但对口语化、省略主语的评论非常脆弱。旅游评论里有大量句子根本没主语比如“太坑了”“等了俩小时”依存树直接散架。第三类是序列标注也是我在这个项目里最终采用的路线。把方面抽取建模成BIO序列标注任务用BERT加CRF或BERT后接线性层做解码效果最稳标注3000条语料、跑二十来个epochF1能到80%上下。代价是需要标注数据所以语料库建设成了绕不开的一环这部分放在第3章展开。对新手建议第一类用来快速出demo第三类用来当主模型依存句法性价比最低调试成本高收益还不稳定。2.3 建模路线选型规则、传统机器学习还是直接上BERT方面抽取定了用序列标注后情感极性分类还有分支。最朴素的是情感词典直接查“不错、漂亮”归正面、“贵、黑”归负面再套一层否定词反转。这个方案做演示demo速度最快但遇到“不便宜”这种带否定词的表达、“价格还行吧”这种模糊说法词典马上失灵。传统机器学习路线是将方面词和原句的位置关系编码成特征比如方面词到情感词的距离、词性组合、词袋向量喂给SVM或逻辑回归。在小数据几千条上SVM效果可以跟浅层神经网络打平训练快、可解释但特征工程很脆换个景区、换个表达风格特征分布就漂了。深度模型是目前的主流也是最推荐拿来当毕设主模型的路线。BERT这类预训练模型在中文本上微调比从零训练LSTM收敛快得多、效果下限高得多。方面级任务与普通分类的区别在于需要把“方面词”信息显式注入模型而不是只把整句丢进去。最简单的做法是把输入构造成“[CLS] 方面词 [SEP] 评论 [SEP]”让CLS向量去融合方面词与上下文的交互后面接一个三分类头。这个方案在公开评测上比直接把整句分类高出8到12个点的F1代码量却几乎没增加。我做选型时的排序很直接只想交差演示词典和规则就够想认真写论文或让答辩有亮点直接上BERT微调把词典结果当baseline做对比实验。劝一句别一上来就搞多任务框架或用大模型做few-shot毕设周期和显卡资源经不起折腾。第4章的代码就按“BERT 方面词拼接”这条最稳的路线来写。3. 构建旅游景点语料库采集清洗、标注规范与数据入库3.1 语料来源自采还是用公开数据集做语料库第一件事是解决数据从哪来。公开的方面级情感数据集里和旅游沾边的很少多数集中在餐馆、酒店、笔记本领域英文有SemEval系列中文有COTE等但没有一个现成的“带方面标签的景区评论”标准集。所以这个课题几乎注定要自己标注一部分。自己采集评论时我建议优先选自己熟悉、且能通过开放接口或已授权渠道获取的数据很多平台有公开的脱敏数据集走正规渠道能省掉大堆麻烦。别为了凑语料去写高并发爬虫合规风险先不说被验证码封锁之后纯粹耽误进度。清洗流程是固定的剪枝操作去HTML标签、去Emoji和表情符号、去重复刷屏的复制评论、把全角字符统一转半角、过滤掉少于10个字的短评。短评像“很好”“不错”没有方面信息标注生成的样本全是少数派直接过滤反而省算力。清洗完保留原始文本一份、清洗文本一份不要覆盖原字段后面抽检和写数据说明都用得上。3.2 方面标签体系与标注格式旅游场景的方面类别设计不能太细太细会让标注一致性崩溃标注员会因为“排队算交通还是服务”这类边界问题吵起来也不能太粗太粗就退化成整体情感分析。我在项目中把方面类别收敛到五类见下表方面类别典型方面词示例业务含义景点景观风景、景色、环境、雪景游客对核心游览对象的评价交通出行缆车、停车、排队、交通到达景区及内部交通体验门票价格门票、票价、性价比价格感受与价值判断餐饮购物小吃、纪念品、餐厅景区内消费体验服务管理导游、服务、管理、卫生人员服务与运营管理标注格式直接决定训练代码好不好写。我推荐用JSON一条样本一个对象方面词和情感极性并列存放{ text: 风景很美但缆车排队两小时门票也不便宜。, aspects: [ {term: 风景, category: 景点景观, polarity: 1}, {term: 缆车, category: 交通出行, polarity: -1}, {term: 排队, category: 交通出行, polarity: -1}, {term: 门票, category: 门票价格, polarity: -1} ] }极性用1、0、-1分别代表正面、中性、负面。注意“排队”这个词本身是中性但在“排队两小时”的语境里是负面所以标注必须看上下文不能只看词本身。这也是标注规范培训里一定要讲的“语境极性”问题否则不同标注员会把同一个词标成不同情感标注一致性直接跌破及格线。标注一致性可以用Cohen‘s Kappa系数验收让两个人标同一批100条Kappa达到0.7以上才说明规范可用低于0.7就回去改规范、重新培训再标。这个系数写进论文里的说服力比“我们认真标注了3000条”强得多。建议每标注500条就做一次一致性抽检别等全部标完才发现规范失效。3.3 数据入库这个项目为什么必须带个数据库标题里既然写了“数据库”交付物里就得有一份能初始化、能查询、能演示数据流的库。常见选择是MySQL和SQLite二选一SQLite零配置、单文件、好迁移适合本地demoMySQL要装服务、配账号但更能体现完整的数据工程能力。毕设里我用MySQL原因很务实答辩老师几乎必问“表怎么设计、索引怎么建”MySQL答起来素材多。核心表拆成三张评论表存原始文本和清洗后文本标注表存标注结果预测结果表存模型输出。建表SQL如下CREATE TABLE t_comment ( id INT AUTO_INCREMENT PRIMARY KEY, source_text TEXT NOT NULL, clean_text TEXT, source_platform VARCHAR(32), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_aspect_label ( id INT AUTO_INCREMENT PRIMARY KEY, comment_id INT NOT NULL, aspect_term VARCHAR(64) NOT NULL, category VARCHAR(32) NOT NULL, polarity TINYINT NOT NULL COMMENT 1正面 0中性 -1负面, labeler VARCHAR(32), FOREIGN KEY (comment_id) REFERENCES t_comment(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_predict_result ( id INT AUTO_INCREMENT PRIMARY KEY, comment_id INT NOT NULL, aspect_term VARCHAR(64) NOT NULL, category VARCHAR(32), pred_polarity TINYINT, confidence FLOAT, model_version VARCHAR(32), predict_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三张表的分工一句话就能讲清楚t_comment存原料t_aspect_label存人工标注的“正确答案”t_predict_result存模型在测试集上的“回答”。人工标注和模型预测分开存是为了评估时能直接SQL join比如按类别统计混淆矩阵。字符集统一用utf8mb4而不是utf8因为旅游评论里Emoji和生僻字很常见utf8存Emoji会直接报Incorrect string value。这个坑相当常见后面避坑章还会展开。另外建议建一个统计视图用于演示时按方面类别算情感分布。写一条GROUP BY的SQL就能在答辩现场展示“哪些方面差评集中”这是演示视频里最有说服力的一页数据。4. 用Python训练方面级情感分析模型最小可跑代码与参数细节4.1 数据预处理把JSON语料转换成模型输入预处理阶段要做三件事读JSON、构造输入序列、对齐标签。前面的标注文件里一条样本对应多个方面每个方面要单独构造一条训练样本输入是“方面词[SEP]原评论文本”标签是该方面对应的极性类别。这样一条评论会膨胀成三到四条样本训练时互不影响模型也能学会“同一个句子在不同方面词约束下输出不同极性”。代码里有个容易翻车的细节方面词要先做文本归一化去掉“很、太、比较”这类程度副词前缀。因为方面词是名词性短语把“太黑”当方面词模型学到的就是字面匹配而不是上下文判断。预处理核心代码如下import json from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) def build_samples(json_path, max_len128): samples [] with open(json_path, r, encodingutf-8) as f: data json.load(f) for item in data: text item[text] for asp in item[aspects]: term asp[term].strip() # 标准方面级输入结构[CLS] 方面词 [SEP] 评论 [SEP] encoded tokenizer( term, text, max_lengthmax_len, truncationonly_second, paddingmax_length, return_tensorsNone ) samples.append({ input_ids: encoded[input_ids], token_type_ids: encoded[token_type_ids], attention_mask: encoded[attention_mask], label: asp[polarity] 1 # -1/0/1 - 0/1/2 }) return samples注意两个点一是tokenizer传两个字符串时transformers库会拼成“[CLS] 方面词 [SEP] 评论 [SEP]”这就是方面级情感分析的标准输入结构二是label做了偏移映射把-1/0/1变成0/1/2因为PyTorch交叉熵损失要求类别序号从0开始。truncation“only_second”的意思是评论太长只截断后半段方面词是焦点信息绝不能因为超长被切掉。4.2 BERT方面级情感分类模型核心训练代码模型本身非常简洁BERT编码器后接一个Dropout和Linear分类头transformers的BertForSequenceClassification把这一步直接封装好了。下面这段是完整可跑的微调代码按3000条左右的数据量、单张RTX 3060跑一个epoch大约十几分钟import torch from torch.utils.data import DataLoader, Dataset from transformers import BertForSequenceClassification, AdamW class AspectDataset(Dataset): def __init__(self, samples): self.input_ids torch.tensor([s[input_ids] for s in samples], dtypetorch.long) self.token_type_ids torch.tensor([s[token_type_ids] for s in samples], dtypetorch.long) self.attention_mask torch.tensor([s[attention_mask] for s in samples], dtypetorch.long) self.labels torch.tensor([s[label] for s in samples], dtypetorch.long) def __len__(self): return len(self.labels) def __getitem__(self, i): return (self.input_ids[i], self.token_type_ids[i], self.attention_mask[i], self.labels[i]) model BertForSequenceClassification.from_pretrained(bert-base-chinese, num_labels3) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) train_loader DataLoader(AspectDataset(train_samples), batch_size32, shuffleTrue) optimizer AdamW(model.parameters(), lr2e-5) model.train() for epoch in range(5): total_loss 0.0 for batch in train_loader: input_ids, token_type_ids, attention_mask, labels [b.to(device) for b in batch] outputs model( input_idsinput_ids, token_type_idstoken_type_ids, attention_maskattention_mask, labelslabels ) loss outputs.loss loss.backward() optimizer.step() optimizer.zero_grad() total_loss loss.item() print(fepoch {epoch1} avg_loss{total_loss / len(train_loader):.4f})逻辑说明BertForSequenceClassification自带分类头labels传进去时库内部会计算交叉熵损失省掉手写损失函数的步骤。token_type_ids区分方面词和评论两个片段让BERT的segment embedding参与交互。训练时每个batch结束后都要调用optimizer.zero_grad()漏掉的话梯度跨batch累加loss曲线会异常震荡。模型权重第一次使用会自动下载建议提前在方便的网络环境里下好并用本地路径替换bert-base-chinese。4.3 训练参数盘点学习率、batch size、epoch怎么定这组参数被不少人当成玄学其实有几个经验值可以稳落地。学习率在微调阶段用2e-5是BERT系列最稳妥的起点大于5e-5容易让loss跳崖式爆炸小于1e-5收敛慢到让人怀疑人生。batch size在小显存卡上取16到32只有4G显存的话把batch降到8再用梯度累积步数4模拟出32的效果显存压力小一个量级。epoch数建议先跑满5轮看验证集趋势。这个数据量下BERT第二三轮验证F1就到顶了后面几轮不加早停就是纯过拟合。留个“后悔药”每个epoch结束时保存验证F1最高的checkpoint别用最后一轮的权重。推理和评估代码要能脱离训练环境独立运行预测结果输出成JSON存到固定目录方便做演示视频时反复调用。4.4 评估指标准确率会骗人宏F1才是真话旅游评论的情感分布天然不平衡正面样本常常占六成以上。模型无脑全预测正面准确率也能到60%左右这个数答辩时根本拿不出手。正确的做法是分别算每个类别的精确率、召回率和F1再取平均得到宏F1Macro F1。类别不平衡越严重越要盯宏F1而不是准确率。评估报告一行代码就能出from sklearn.metrics import classification_report print(classification_report(y_true, y_pred, target_names[负面, 中性, 正面]))输出重点看负面和中性两行的F1。旅游评论里负面信息才是运营方最关心的负面类F1应该被当作核心指标写进论文。见过不少同学把准确率当唯一指标被答辩老师一句“你觉得这模型在真实场景敢用吗”直接问住。指标选择都讲不透整个系统的可信度就立不住。5. 避坑指南做这个课题最容易翻车的五个环节5.1 现象训练完模型预测结果全是正面模型在验证集上准确率看着还行但打印每条样本的预测结果发现几乎全是正面负面样本全被吞了。仔细看是类别不平衡正面样本占六成以上模型学会“猜正面”来最小化整体损失负面类根本没学到有效特征。解决办法有两个方向。一是给损失函数加类别权重让少数类的错误带来更大惩罚二是对负面样本做过采样。常见做法是给nn.CrossEntropyLoss传入weight代码如下class_weights torch.tensor([2.0, 1.2, 1.0]).to(device) # 负面、中性、正面 loss_fn torch.nn.CrossEntropyLoss(weightclass_weights)另外在评估时别只看准确率直接看每类别的F1。如果加了权重后整体准确率掉了一点但宏F1明显上涨说明方向对了。权重数值不用精细调负面给2.0、中性给1.2左右通常就够。5.2 现象读语料库和连接数据库双双中文乱码训练时打印样本中文全变成“景色”这类乱码写入MySQL后SELECT出来也是一片问号。原因是文件是用utf-8编码保存的但代码在Windows上用GBK默认编码读取了MySQL那边建表时用了latin1或utf8字符集对不上。解决分两头读文件明确指定编码open(path, r, encoding“utf-8”)连接MySQL时在连接串里加上charsetutf8mb4。建表时也强制指定DEFAULT CHARSETutf8mb4不要依赖数据库默认配置。5.3 现象MySQL连接不上程序启动即报错pymysql报OperationalError或者Access denied程序一启动就崩。最常见的三个原因MySQL服务没启动、pymysql库没装、账号密码或host写错。Windows下服务没启动时先去服务管理器确认MySQL服务状态或者命令行执行net start mysql库没装就pip install pymysql连接参数里host、port、user、password四个字段逐个核对别把密码里的大小写抄错。连接串示例import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, password你的密码, databasesentiment_db, charsetutf8mb4 )注意加深夜调试时的血泪经验如果刚装完MySQL还没初始化root密码用sudo mysql -u root登录后执行ALTER USER去设置密码不要跳过这一步直接改程序里的密码字段否则永远Access denied。5.4 现象显卡显存不够训练一启动就OOM报错RuntimeErrorCUDA out of memory模型和数据都塞不进去。直接把batch size降到8如果还OOM把max_len从128降到64并把梯度累积打开optimizer.zero_grad() loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()这一招能让你在4G显存的卡上跑完一个BERT微调任务。另外关掉训练时的梯度计算无关开销比如模型评估别在训练循环里做单独开一个eval模式。如果显存还是不够把model.half()转成半精度试试但注意评估时要用原精度否则输出概率分布会失真。5.5 现象loss不降反升越训练越差训练没几轮loss从2.1涨到3.5曲线完全没有下降趋势。最常见原因是学习率过大BERT微调学习率超过5e-5就容易让预训练权重被冲坏其次是数据预处理错位把input_ids和labels拼错了批次模型学的是随机映射还有一个隐蔽坑是模型没有真正切到训练模式漏了model.train()Dropout没生效。解决思路先打印一个batch的输入和标签核对对应关系确认样本一行的头和尾然后把学习率降到2e-5重新跑最后确认model.train()在训练循环前被调用。如果这三步都做了还发散用预训练模型带的中性样本做一次单条前向看看输出是否接近均匀分布用来判断加载的权重是否完整。6. 演示视频与答辩验证把模型效果讲成能验收的故事演示视频别一上来就录屏幕先按三段式脚本组织环境启动、模型推理、结果可视化。环境启动部分录Python环境激活、MySQL服务拉起、项目初始化跑通三个动作时间控制在半分钟以内。模型推理部分输入几条精心挑选的评论至少包含“风景很美但缆车排队太久”“门票贵但服务很好”这类多方面的句子让模型展示它能把一条评论拆成多个方面分头判断。结果可视化部分把预测结果在MySQL里查出来用一条聚合SQL展示各方面类别的正负面占比这一步是答辩现场最容易加分的镜头。可视化可以做一个30行左右的Streamlit小页面左侧输入评论右侧展示每个方面词的情感标签和置信度import streamlit as st st.title(旅游景点方面级情感分析) text st.text_area(输入评论) if text: result predict_aspects(model, tokenizer, text) st.json(result)代码里predict_aspects复用第4章的推理函数把模型输出转成JSON展示。这个页面不用花心思做复杂交互能证明“模型是可用的系统”就够了。答辩老师常问的三个问题要在演示里提前埋好答案一是为什么不用整体情感分析直接拿“又爱又恨”的例子说明信息稀释二是数据哪来的交代自建语料库的规模和Kappa一致性系数三是模型好在哪里用宏F1对比baseline至少比词典法高15到20个点。最后说个我自己的习惯毕设项目做完后把“从JSON到训练再到查询”的完整命令写成一条README从头照着跑一遍确认新机器上十分钟内能复现。演示视频里录的每一步都得真能跑通现场答辩时最怕的就是视频里行、到自己电脑上不行。模型这条路试跑了很多版本真正有效的永远是那个结构最简单、参数最保守的版本。希望这篇笔记帮你在做这个课题的路上少踩几个坑。本文还有配套的精品资源点击获取