简介面向计算机相关专业学生完成毕业设计或课程设计这份资源围绕旅游景点评论的方面级别情感分析任务给出从语料库、模型训练到Django Web展示的完整源码方案。项目后端使用Django框架涵盖数据库与ORM设计、评论文本预处理、情感分类模型构建可结合深度学习或传统机器学习方法、前端交互页面以及部署说明适合正在搭建情感分析系统、需要参考项目结构或直接复用代码的开发者。压缩包约70.56MB内含项目源码、标注语料库、部署说明文档等上游未提供文件总数与类型明细故不展开罗列。目前已有101人学习下载可见该课题受到一定关注。借助该资源可以快速理解Django MVT架构在真实项目中的组织方式熟悉从评论数据采集清洗、模型训练到结果展示的完整流程节省大量开发与排错时间对完成毕业设计或冲刺课程设计答辩有直接帮助。1. 打开压缩包前先想清楚这个毕设题目真正在考你什么压缩包解压后核心就两部分一份景点评论语料库一套旅游景点方面级别情感分析的模型源码。如果只把它当「跑通一个情感分析 demo」来做答辩很容易被问住——方面级别情感分析要的不是「这条点评是好评还是差评」而是把点评里的每个方面拆开单独判情感。一条「景色很美但人太多」在整句粒度上是中性的在方面级别却同时存在正面「景色」和负面「人流」。这篇就按我做这个方向时的顺序来讲先把任务定义和选型逻辑立住再讲语料库怎么攒、标注怎么落最后给模型源码的改动点和常见翻车现场。适合两类人拿这个压缩包做本科毕设、想真正讲明白的人以及想从句子级情感分析往细粒度方向转的 python 开发者。2. 方面级别情感分析不是「打好评差评」任务定义、输出结构与选型逻辑方面级别情感分析Aspect-Based Sentiment AnalysisABSA在旅游场景下可以拆成三件事找出评论里提到了哪些「方面词」比如「景色」「交通」「门票」判断这些方面是明确写出来的还是隐含在句子里最后对每个方面单独判一个情感极性。常见误区是「我只要把每句话贴个好/中/差标签就行」——但「景色很美但人太多了」这句话句子级标签怎么贴都别扭贴「中性」等于丢掉信息贴「混合」又没法做细粒度统计。2.1 先拆任务方面抽取、类别识别与极性判定可以独立也可以联合经典实现方式是「先抽取、后分类」的两阶段管道。第一阶段把评论当成序列标注任务用 BIO 标签标出「景色的开始/持续/结束」第二阶段把每个方面片段连同它的上下文通常是前后几个词取出来单独过一遍分类器。这样做的好处是中间产物能可视化答辩时可以贴出「模型抽到了什么」「每个方面被判定成什么」解释成本低。坏处是错误会在管道里滚雪球一个方面词在第一阶段漏抽了它的情感极性就永远没有机会进入第二阶段。后来更常见的做法是共享编码器的联合训练。BERT 对整句话编码一次同一个上下文向量既用来算「每个 token 是不是方面词」也用来算「方面区间的情感极性」。两个任务的梯度在 BERT 层汇合抽取任务帮分类任务定位该看哪里分类任务帮抽取任务排除「不是方面词但离情感词很近」的干扰词。对旅游点评这种句式短、口语重的文本共享编码方案是性价比最高的选择不需要在源码里额外引入依存句法解析器。隐式方面也要提前规划。「下次还来」没有提到任何景点名词但表达了正向的整体体验「人挤人走不动」隐含了负面的人流方面。如果模型只在名词上做抽取这类样本会全部变成无标签噪音。我的处理方式是把它单独标成一个「整体体验」类别不奢望模型从看不见的名词里无中生有但至少让分类头有机会把无方面词的句子引导到正确极性上。2.2 为什么整句打分在景点评论上不够用三个典型反例第一个反例是对比句「风景值得看但排队两小时劝退」。从句子里能同时读出正面风景和负面排队若用整句的 1-5 分标签二分模型会拿「值得看」和「劝退」对冲成一个中庸分数这恰恰掩盖了评论里最有价值的信息。第二个反例是评价对象反转「虽然是 5A 景区设施却配不上名气」。这里「设施」是负向「名气」接近中性「5A 景区」甚至带一点正面预期。整句打分模型会把「5A」和「设施」混进同一个向量输出一个不稳定分数换一句「5A 景区不是白叫的」结果就完全相反。第三个反例是建议式表达「建议避开节假日去否则检票都是问题」。句子没有一个显式形容词但「检票」这个词后面拖着明显的负面情绪。方面级别模型的优势是它不只看情感词还看句法上的评价对象——「检票」就是那个对象极性是负。这种样本在旅游语料里占比不低做标注规范时一定要单独列一档。这三个反例的共同点是句子层面没有单一情感分布评论者是在给不同的方面分别打分。旅游点评相比商品评论更强调方面多样性同一条评论可能同时讨论景色、交通、餐饮、人群密度而平台打分却只是一个 1-5 分整数。要从整句分数反推方面级意见基本不可能。2.3 模型应该输出什么三元组结构与公开数据集的差距一个方面级模型的训练目标通常组织成三元组列表。常见输出样例长这样{ text: 景色很美但人太多了。, aspects: [ {term: 景色, polarity: positive, start: 0, end: 2}, {term: 人, polarity: negative, start: 7, end: 8} ] }这里的 start/end 是字符级偏移训练时通过分词器的 char_to_token 方法换算成 token 范围再生成一个 token 级别的 0/1 mask 给抽取头。这样设计的好处是「抽取和分类共用一套输入」预测结束后也能把 token 位置反映射回原文直接展示给老师看。公开数据集方面SemEval-2014 Task 4 的 Laptop/Restaurant 标注是 ABSA 的经典基准但直接搬到旅游场景有两个差距。第一是语言和领域的偏差英文餐厅评论的训练分布和中文景点口语差别很大餐饮里「服务」「食物」占主导景点点评里却是「人流」「体力消耗」「性价比」这类方面更多。第二是情感类别粒度公开集通常只分正向/负向/中性而你在自建语料时往往想多加一个「冲突」对应「风景好但人多」这种同时正负的方面。所以做毕业设计时用公开预训练模型 自建语料库微调比直接拿公开 ABSA 模型做推理靠谱得多。2.4 选型逻辑为什么是 BERT而不是词典规则或传统机器学习词典规则可以做一个快速基线维护「景点方面词表 情感词表」用位置邻近匹配给方面打分。速度极快、完全可解释但语义反转和否定句式会让它集体翻车「没有想象中那么挤」会被判成负面。SVM/CRF 时代的做法是把词性、位置、依存句法路径做成特征小样本上表现稳定但特征靠人挑换一个领域就要重新设计一整套特征模板。BERT 的思路是让模型从语料库里自己学哪些词是方面词再配合中文预训练阶段的通用知识在 1000 到 3000 条标注样本上就能训出能支撑答辩的效果。我也不建议在这个题目上直接上生成式大模型做端到端抽取可解释性差、本地部署成本高而且本科毕设的篇幅里很难把 prompt 工程讲出深度。BERT 加两个线性头的结构既贴合源码包里的常见实现又能在论文里画出清晰的模型结构图这是选题上的稳妥路子。3. 语料库从零到能训练清洗规则、标注规范与 python 转换脚本语料库是整个题目里最重的一环。模型源码可以跑通就放一边但语料库质量直接决定你后面答辩时敢不敢说结论。这一章按照「拿原始评论 → 清洗 → 标注 → 转成训练 JSON → 小样本扩充」的顺序走每一段都对应一个可落地的动作。3.1 数据从哪来公开点评文本的采集与取舍先说数据来源。旅游景点的公开点评文本通常来自两类渠道一类是公开数据集或开源语料一类是各点评平台展示页的文本。这里只处理「你有权使用」的数据——如果是手工录入、公开可下载的语料或者自己在平台逐条复制保存的样例都问题不大但不要批量爬取后声称是自己的版权数据毕设查重和原创声明都会栽在这上面。我一般会按三个口径收集知名景区的高赞点评、中低分点评、以及短评论少于 20 字的长尾样本。高赞点评句式完整容易标出清晰方面中低分点评里否定句和转折句多用来逼模型学反义短评论则是「值得去」「别去」这类极简表达词少但极性明确。三部分按 4:3:3 混合能让语料分布更接近真实场景。清洗阶段做这几件事去重同一用户同一文本只留一条、去平台营销句式「推荐大家来」「一键三连」之类、去掉包含网址和表情符号的碎片、把全角半角标点归一。另外把超过 200 字的评论体单独放一个文件后面训练时单独处理不直接混进主训练集。3.2 标注规范方面词边界、极性四分类与多标签句的处理标注规范是整个语料库建设的核心文件。我建议用四分类而不是三分类多一个「冲突」类。下表是四分类的标注口径极性含义典型句式示例正向该方面得到正面评价「景色很美」「很值得」景色 → 正向负向该方面得到负面评价「人太多」「排队久」人流 → 负向中性陈述事实不含评价「门票 80 元」门票 → 中性冲突同一方面既有正面又有负面「景美但太远」距离 → 冲突方面词边界按「原文子串」标注不要概括、不要改写。比如「人太多」里的方面词是「人」不是「人多」更不是「人流」「检票排队一小时」里的方面词是「检票」。这个规则看着简单实际执行时标注员很容易把方面词写成名词短语导致后面 start/end 定位失败所以规范里必须明确写一句只标原文里出现过的字符序列。多标签句是另一个要提前说清楚的点。一条评论里出现 4 个方面词很正常每个方面各带一个极性。标注时不要合并、不要只标第一个宁可漏一条空白文本也不要让两个标注员对「这句话该标几个方面」有歧义。我一般会让两个人各标一遍然后逐条对比差异Kappa 一致性低于 0.7 的部分重新讨论控制在 0.8 左右再进入训练集。3.3 用 python 把原始标注转成训练 JSON脚本与参数说明标注结果通常存成表格或者 JSON下一步是把它转换成模型训练要的格式。下面这个脚本就是做这件事的逻辑很直白# convert_raw.py # 把人工标注的原始 JSON 转成模型训练用的 JSON 行格式 import json import re def normalize_text(text: str) - str: text re.sub(r\s, , text) # 连续空白压成单空格 text re.sub(r…, 。, text) # 省略号统一为句号 text text.replace(, ).replace(, ) return text.strip() def to_training_sample(sample: dict) - dict: # sample: {text: 原文, annotations: [{term: 景色, polarity: 2}]} text normalize_text(sample[text]) aspects [] for ann in sample[annotations]: start text.find(ann[term]) # 在原文里定位方面词 if start -1: # 找不到说明标注和原文不一致直接丢弃而不是瞎猜位置 continue aspects.append({ term: ann[term], start: start, end: start len(ann[term]), polarity: ann[polarity], # 0: 负向, 1: 中性, 2: 正向, 3: 冲突 }) return {text: text, aspects: aspects} if __name__ __main__: raw json.load(open(annotations.json, encodingutf-8)) with open(train.json, w, encodingutf-8) as f: for sample in raw: item to_training_sample(sample) if item[aspects]: # 空方面样本不写进训练集 f.write(json.dumps(item, ensure_asciiFalse) \n)逻辑说明脚本的作用是让标注和训练解码彻底解耦。标注阶段只维护“原文 方面词 极性”三个人能看懂的信息训练阶段再按 start/end 字节位置生成 token mask。用text.find定位是最保守的做法它要求标注里的 term 必须与原文完全一致不一致就丢弃从而让标注规范问题在转换阶段暴露出来而不是等训练完才发现模型永远学不到某些方面。polarity 用 0 到 3 的整数而不是字符串是为了后面直接当交叉熵标签少一层映射。参数上需要注意的是ensure_asciiFalse保证中文以可读形式落盘start/end是字符偏移而不是字节偏移中文场景千万别用len(text.encode(utf-8))去算否则 BERT 的 char_to_token 会错位。3.4 数据扩充与分布控制小样本下的三个操作标注成本有限时可以做三种轻量扩充。第一种是同义词替换但只替换方面词之外的部分比如「特别」「非常」「很」互相替换保持方面词不变防止改变边界。第二种是回译中文→英文→中文但只引入少量因为回译容易把「人太多」这种口语改成书面语反而偏离来源分布。第三种是句式打乱把「虽然 A 但 B」换成「B 不过 A」用规则生成不引入外部模型。分布控制更重要。统计每个极性类别的样本数如果「冲突」类只有个位数我一般先不删除而是单独做校验集避免训练时被其他三类淹没。毕业设计不要求每个类别都刷到极高准确率但要在论文里诚实地写出「冲突类样本仅有 N 条结论仅供参考」这比藏着数据的短板要稳得多。4. 基于 BERT 的模型源码怎么改训练流程、关键参数与评估口径拿到源码包之后先别急着跑。先把模型结构读懂再把参数调到匹配自己的语料规模最后才谈训练和评估。这一章按这个顺序来模型源码的骨架是「BERT 编码 抽取头 分类头」大部分问题都出在这三个部件的接缝处。4.1 模型结构共享编码器 抽取头 分类头源码里最常见的结构不是「序列标注完再分类」的两段式而是让 BERT 只编码一次得到[batch, seq_len, hidden_size]的序列输出后接两个头。抽取头是一个线性层对每个 token 输出一个 0/1 分数判断它是不是方面词的一部分分类头把方面区间内的 token 特征做平均池化再线性映射到 4 个极性类别。这个结构在训练阶段需要两个标签一个是 token 级 0/1 mask来自第 3 章 JSON 里的 start/end一个是样本级极性标签来自该方面的 polarity。推理阶段先由抽取头得到方面词位置再用同样的位置去分类头取极性。使用共享编码器最大的优势是错误不会沿着管道一路放大抽取和分类互相提供梯度信号预训练模型在这里像一个黑匣子你不需要理解它内部每个头在做什么但要想清楚两个头的输入输出怎么对齐。4.2 python 核心代码模型定义片段与参数说明下面这段代码是模型定义的核心片段把它和源码包里的文件对照着看能快速定位改动点# model_head.py # 在 BERT 上接两个输出头一个做方面词抽取一个做方面情感分类 from transformers import BertModel, BertTokenizerFast import torch import torch.nn as nn class AspectModel(nn.Module): def __init__(self, bert_pathbert-base-chinese, num_labels4): super().__init__() self.bert BertModel.from_pretrained(bert_path) hidden self.bert.config.hidden_size # 抽取头判断每个 token 是不是方面词的一部分 self.extract_head nn.Linear(hidden, 1) # 分类头把方面区间内的 token 特征池化后再判极性 self.sentiment_head nn.Linear(hidden, num_labels) def forward(self, input_ids, attention_mask, aspect_maskNone): out self.bert(input_idsinput_ids, attention_maskattention_mask) seq out.last_hidden_state # [batch, len, hidden] # 方面抽取对每个 token 打 0/1 分 logits_extract self.extract_head(seq).squeeze(-1) loss_extract None if aspect_mask is not None: loss_extract nn.functional.binary_cross_entropy_with_logits( logits_extract, aspect_mask.float()) # 方面情感训练时用 gold mask预测时用抽取结果做 mask mask (aspect_mask if aspect_mask is not None else (logits_extract.sigmoid() 0.5).float()) mask mask.unsqueeze(-1) masked (seq * mask).sum(dim1) / mask.sum(dim1).clamp(min1) logits_senti self.sentiment_head(masked) # [batch, num_labels] return logits_extract, logits_senti, loss_extract这代码有两点要注意。一是mask.sum(dim1).clamp(min1)因为一条评论里可能没有抽到任何方面词分母为 0 会直接 nan这个 clamp 是最便宜的后悔药。二是aspect_mask只在训练时传入预测时由阈值 0.5 决定哪些 token 算方面词这个阈值后续可以在验证集上调一般 0.3 到 0.6 之间都有得调。bert_path用社区最常见的中文预训练权重如果你的显存紧张可以换成distilbert类的小模型但效果会掉几个点毕设阶段不推荐折腾。4.3 必调的五个参数学习率、max_len、batch、epoch、warmup 的配合关系预训练模型微调的参数范围相对固定但五个参数必须相互配合不是单独调了就有用。下面是常用取值和配合逻辑参数常用值说明learning_rate2e-5 ~ 5e-5超过 5e-5 容易让 BERT 预训练权重学飞max_len128景点点评 90% 在 128 token 内够用batch_size16 / 32显存小就 8配合梯度累积epochs3 ~ 5语料只有一两千条时 3 轮足够warmup_ratio0.1前 10% 步数线性升温避免开局震荡配合逻辑一句话小语料配小学习率、少 epoch长文本才需要加大 max_len但 max_len 每翻一倍显存占用接近翻倍。如果本机没 GPUbatch 降到 4再把训练集裁一半在 CPU 上也能跑起来只是时间要按小时算。Windows 本机上直接跑也没问题唯一要注意的是别在默认路径下乱放预训练权重下载中断后权重文件残缺会报各种莫名其妙的错。4.4 评估别只看准确率方面级 F1、混淆矩阵与答辩展示分类问题最容易犯的错是看整体准确率。但「冲突」类样本本来就少模型全预测成「正向」准确率也有 80% 以上答辩时经不起追问。我一般用三个方面级指标方面抽取 F1预测的 token 区间和 gold 区间的匹配程度、极性分类 F1gold 方面上的分类表现只算正确位置上的极性、以及整体匹配准确率方面和极性都对的样本比例。答辩展示时一张混淆矩阵比十张 loss 曲线截图更有说服力。把「冲突」被分到「负向」或「正向」的具体案例打出来比如「景美但太远」被模型判成正面你可以在论文里分析这是「转折连接词建模不足」导致的典型错误老师会认为你真正理解了这个任务。如果只放一张准确率截屏问题就大了。5. 避坑与排查五个最常见问题的定位与修复训练这类模型代码能跑通只是开始。这章按我最常遇到的五个问题来写每条都按「现象 → 原因 → 解决」排好基本覆盖了自建语料场景下大半的翻车现场。5.1 现象训练 loss 一直降验证 F1 却纹丝不动原因标注一致性没达标。如果两个标注员对一个方面词的边界理解不同同一批数据里「人太多」有时标「人」有时标「人太多」模型会反复改判断边界最后干脆只学最频繁的模式。这也是为什么我在第 3 章强调标注一致性先到 0.8 再训练。解决把一个方面边界分歧最大的 20 条样本抽出来统一改成「最短可读子串」规则——「人」和「人太多」统一选「人」。然后重新跑转换脚本确认 JSON 里的 start/end 跟着变不用动模型源码。5.2 现象短方面词预测结果总是错位「人」被标成「人太」原因BERT 分词把中文切成子词时「人太多」会被切成「人」「太」「多」三个 token。抽取头在 token 级别输出 0/1如果 gold mask 把「太」「多」也算进方面区间模型就会学到「人后面跟的词也是方面词」这种错误关联。解决转换脚本里只把和原文 start/end 完全重合的 token 置为 1。分词边界和字符偏移不一致时用 tokenizer 的char_to_token(start)和char_to_token(end - 1)取区间而不是简单地把 start/end 所在 token 全标 1。5.3 现象batch 稍微调大就 OOM调小又训不动原因BERT 的显存占用主要由 max_len 和 batch 同时决定很多人只敢调 batch却忽略了 max_len 对显存的影响是指数级别的。解决先把 max_len 从 256 砍到 128观察 F1 是否明显下降景点点评文本普遍短这个回退通常无感。再配合梯度累积比如 batch 8 accumulation 4 等价于 batch 32 的效果显存占用却低得多。如果还 OOM就把模型切成半精度训练代码只需在训练循环外面加一句model.half()但要记得输入也要转成 half。5.4 现象词典规则基线分数比 BERT 还高论文不知道怎么写原因这种现象在旅游领域很常见因为点评里大量出现「美」「值」「挤」这类强情感词词典规则直接命中而 BERT 反而被一些长尾句式带偏。如果测试集又只有 200 条规则碰巧赢是正常的。解决把测试集按「是否包含强情感词」分层单独统计不含强情感词的子集上的表现。BERT 在转折句、否定句上的优势会在这个子集里显示出来。论文里把这个实验作为「难点子集分析」写入比藏着基线的结果要体面得多。5.5 现象一条 180 字的长评论预测结果里方面位置全部排到句尾原因超过 max_len 的文本被从中间截断方面词如果落在被截掉的部分模型根本看不到但位置映射却还拿原始 start/end 去算结果错位。解决先统计语料长度分布把 95 分位长度作为 max_len而不是拍脑袋用 512。真正超长的评论用「头尾保留」法保留前 64 token 和后 64 token中间用[SEP]拼接。确实有 longformer 这类中文长文本模型能处理更长的依赖但对景点点评来说收益很小头尾保留法能覆盖绝大多数情况。6. 比直接跑通更进一步试试「抽取分类」双任务联合训练如果你已经把 pipeline 跑顺想再往上拔一点我建议试一个改动成本很低、答辩时很好讲的技巧把第 4 章的两个头改成共享编码器的双任务联合训练。实现上就是不再让抽取头的结果单独决定分类头的输入而是让两个 loss 一起回传核心代码改动如下# train_step.py # 双任务 loss 组合抽取 loss 与分类 loss 一起回传 loss loss_extract * 0.5 loss_sentiment loss.backward() optimizer.step()这里的 0.5 是抽取头的权重系数是我常用的起点。训练几轮之后如果发现抽取头过拟合表现为验证集抽取 F1 不再涨、情感 F1 继续涨就把它降到 0.3反过来如果方面词错漏严重就抬到 0.7。两个头共享 BERT 编码器回传时梯度会自动叠加不需要手动做任何梯度拼接。这个技巧的好处是误差不再沿管线单向传——抽取阶段漏掉的方面分类阶段的信息还能反过来修正抽取头。验证方法也很简单准备 150 到 200 条带标注的留出集分别跑「先抽取后分类」和「双任务联合」两种配置对比整体匹配准确率。我习惯每次训练后都把这个对比结果打出来存成一个表格语气上不用提前站队数据出来之后自然会产生可讲结论。这个题目真正值钱的地方不在模型结构本身而在语料库建设和评估口径设计上把这两个环节做扎实源码跑通只是起点。希望帮到你。本文还有配套的精品资源点击获取