ARTICLE DETAIL

资讯详情

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

基于Bi-LSTM与注意力机制的景区评论情感分析实战

基于Bi-LSTM与注意力机制的景区评论情感分析实战 简介本资源面向深度学习与自然语言处理方向的本科或研究生毕业设计场景提供一套基于融合对抗训练与注意力机制的Bi-LSTM网络用于景区评论情感分析的完整Python实现。项目覆盖从数据标注、word2vec词向量训练、模型训练到未标注语句情感预测与评价分数生成的完整流程适合具备一定Python与TensorFlow基础、希望完成情感分类课题或复现相关算法的学习者。压缩包共18个文件约191KB包含4个ipynb交互式笔记、4个py源码模块、word2vec词向量文件、语料与停用词数据、日志及模型保存目录等其中config.py负责参数配置dataSet.py完成数据预处理model.py给出基线模型tools.py提供序列长度与评价指标计算函数main_load与main_save两个笔记分别支持加载预训练模型和读取训练数据进行预测。目前已有248人学习下载可帮助读者快速理解对抗训练与注意力机制在Bi-LSTM中的融合方式并借助现成脚本完成训练、预测与结果评估。1. 景区评论情感分析一条 Bi-LSTM 加注意力到底能解决什么做景区运营的朋友经常丢给我一个 Excel几千条来自 OTA 和点评平台的游客评论问我能不能自动分出好评差评、再告诉我大家到底在骂什么。人工标注 500 条就要一整天而且标准还会漂移。这个标题讲的方案就是用 Python 搭一套「Bi-LSTM 注意力机制 对抗训练」的情感分析模型把这件事自动化。它适合三类人做文本分类毕设的学生、需要舆情监控的文旅从业者、想搞懂注意力机制怎么落到代码里的 NLP 入门者。核心链路是中文分词 → 词向量 → 双向 LSTM 编码 → 注意力加权 → 情感极性输出再叠加对抗扰动提升泛化。下面按能复现的顺序拆开讲。2. 数据准备与中文预处理的落地细节景区评论和标准影评数据集最大的差别是口语化严重、夹杂 emoji、有大量地名和方言词还有「排队两小时」「厕所脏」这种短句。直接套用英文情感分析的流程会翻车预处理这一步决定了后面模型的上限。2.1 景区评论数据的采集与标注口径数据来源常见有三类OTA 平台导出的评论、景区自建问卷的开放题、社交平台的打卡文本。我一般先统一成两列text和labellabel 用 0/1 二分类负面/正面如果要做细粒度可以扩成 1-5 星再映射。标注口径必须提前定死否则「一般般」这种中性评论会让两个人标出相反结果。一个可复现的最小数据集结构如下data/ train.csv # 训练集约 6400 条 dev.csv # 验证集约 800 条 test.csv # 测试集约 800 条 stopwords.txt # 中文停用词表划分比例按 8:1:1注意要按时间或景区分层抽样避免某个景区的评论全落在测试集里导致指标虚高。这一步没有代码但比调参重要。2.2 用 jieba 做分词与清洗的完整脚本中文必须先分词。景区评论里「九寨沟」不能被切成「九寨」「沟」所以要把景区名、常见地名加进 jieba 的自定义词典。import jieba import re import pandas as pd # 加载自定义词典保证景区专有名词不被切碎 jieba.load_userdict(data/user_dict.txt) def clean_text(text): # 去掉 URL、用户、多余空白 text re.sub(rhttp\S|\S, , str(text)) text re.sub(r\s, , text).strip() # 只保留中文、英文、数字 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) return text def tokenize(text): text clean_text(text) # 精确模式分词过滤单字和停用词 words [w for w in jieba.lcut(text) if len(w) 1] return words df pd.read_csv(data/train.csv) df[tokens] df[text].apply(tokenize) print(df[[text, tokens]].head())逻辑说明clean_text负责去噪tokenize负责切词并过滤长度为 1 的字因为单字在情感分析里噪声大于信号。参数上len(w) 1这个阈值可以调如果你的数据里「差」「赞」这类单字情感词很关键就改成 1并配合停用词表过滤。停用词表建议用哈工大停用词表打底再手动补上「景区」「游客」这类高频但无情感指向的词。2.3 词向量初始化从零训练还是加载预训练词向量有两种路线一是用 Word2Vec 在自己的评论语料上训练二是加载腾讯词向量或中文预训练词向量。景区领域词比较特殊我一般用「预训练 领域微调」先加载预训练向量覆盖大部分常用词未登录词用随机初始化然后在训练中一起微调。import numpy as np from gensim.models import KeyedVectors def build_embedding_matrix(word2id, emb_path, emb_dim200): # 加载预训练词向量 w2v KeyedVectors.load_word2vec_format(emb_path, binaryFalse) vocab_size len(word2id) matrix np.random.normal(0, 0.1, (vocab_size, emb_dim)) hit 0 for word, idx in word2id.items(): if word in w2v: matrix[idx] w2v[word] hit 1 print(f命中率: {hit / vocab_size:.2%}) return matrix参数说明emb_dim常用 100 或 200维度越高表达力越强但显存吃紧np.random.normal(0, 0.1, ...)是未登录词的初始化标准差别设太大否则训练初期梯度爆炸。命中率低于 70% 就要考虑换词向量或扩大领域语料这个数字是判断词向量是否可用的第一指标。3. Bi-LSTM 加注意力模型的搭建与训练预处理做完进入模型主体。这一章把网络结构、注意力计算、对抗训练三块讲清楚每一块都给能直接跑的代码。3.1 Bi-LSTM 编码层为什么比单向更适合评论评论的情感往往由后半句决定比如「风景很美但是排队太久了」单向 LSTM 读到「但是」时已经丢失了前面的信息。Bi-LSTM 用前向和后向两个 LSTM 分别编码再把两个方向的隐状态拼接每个词都能同时看到上下文。import torch import torch.nn as nn class BiLSTMEncoder(nn.Module): def __init__(self, emb_matrix, hidden_dim128, dropout0.3): super().__init__() vocab_size, emb_dim emb_matrix.shape # 用预训练矩阵初始化 embedding并冻结前几轮 self.embedding nn.Embedding.from_pretrained( torch.tensor(emb_matrix, dtypetorch.float32), freezeFalse, padding_idx0 ) self.lstm nn.LSTM( emb_dim, hidden_dim, batch_firstTrue, bidirectionalTrue, num_layers1 ) self.dropout nn.Dropout(dropout) def forward(self, x): emb self.dropout(self.embedding(x)) out, _ self.lstm(emb) # out: [B, L, 2*hidden] return out参数说明hidden_dim128是单方向维度双向拼接后是 256num_layers1对中小数据集够用层数多了容易过拟合freezeFalse表示词向量参与微调如果数据量小于 5000 条建议设成 True 冻结。padding_idx0保证 padding 不参与梯度。3.2 注意力机制让模型自己挑出「排队」「脏」这些关键词注意力机制的作用是给每个词的隐状态算一个权重情感词权重高中性词权重低。常见做法是加性注意力Bahdanau或点积注意力这里用加性注意力参数量小、对小数据集友好。class Attention(nn.Module): def __init__(self, hidden_dim): super().__init__() self.proj nn.Linear(hidden_dim * 2, hidden_dim * 2) self.query nn.Linear(hidden_dim * 2, 1, biasFalse) def forward(self, lstm_out, maskNone): # lstm_out: [B, L, 2H] score self.query(torch.tanh(self.proj(lstm_out))) # [B, L, 1] score score.squeeze(-1) # [B, L] if mask is not None: # padding 位置置为极小值softmax 后权重趋近 0 score score.masked_fill(mask 0, -1e9) weight torch.softmax(score, dim1) # [B, L] context torch.bmm(weight.unsqueeze(1), lstm_out) # [B, 1, 2H] return context.squeeze(1), weight逻辑说明proj把隐状态映射到注意力空间query打成一个标量分数masked_fill是关键——不加 mask 的话 padding 也会分到权重模型会学到「句子越长越负面」这种伪相关。返回的weight可以可视化用来解释模型到底关注了哪些词这在毕设答辩里是加分项。3.3 融合对抗训练FGM 扰动提升泛化对抗训练的思路是在词向量上加一个微小扰动让模型在「最坏情况」下仍然预测正确从而提升鲁棒性。FGMFast Gradient Method是最轻量的实现几乎不增加训练时间。class FGM: def __init__(self, model, eps1.0): self.model model self.eps eps self.backup {} def attack(self): for name, param in self.model.named_parameters(): if param.requires_grad and embedding in name: self.backup[name] param.data.clone() norm torch.norm(param.grad) if norm ! 0: r_at self.eps * 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 {}参数说明eps1.0是扰动幅度太大模型学不动太小没效果一般 0.5-1.0 之间调只对 embedding 层做扰动是因为那里参数最多、最容易被攻击。训练循环里先正常前向反向再attack()后二次前向反向最后restore()相当于每个 batch 训练两次。3.4 训练循环与关键超参设置把上面三块拼起来训练循环要处理类别不平衡差评通常少于好评用加权交叉熵。from torch.utils.data import DataLoader from sklearn.metrics import f1_score def train(model, loader, optimizer, criterion, fgm, device): model.train() for batch in loader: x, mask, y [b.to(device) for b in batch] # 正常训练 logits model(x, mask) loss criterion(logits, y) loss.backward() # 对抗训练 fgm.attack() logits_adv model(x, mask) loss_adv criterion(logits_adv, y) loss_adv.backward() fgm.restore() optimizer.step() optimizer.zero_grad()超参建议学习率 1e-3 配 Adambatch size 64训练 10-15 轮早停看验证集 F1。类别不平衡时给少数类权重设为多数类的 2-3 倍。验证指标别只看准确率景区评论里差评识别率负面类的 recall才是运营真正关心的。4. 避坑与排查这套模型最容易翻车的五个地方模型跑通不难难的是指标上不去还找不到原因。下面五条是我实际踩过的坑按「现象 → 原因 → 解决」写。4.1 验证集准确率 95% 但上线一塌糊涂现象本地验证集 F1 到 0.95换一批新评论预测全是正面。原因训练集和验证集来自同一批数据随机划分同一景区的相似评论同时出现在两边造成数据泄漏。解决按景区或时间做分组划分用GroupShuffleSplit而不是train_test_split宁可指标掉 5 个点也要保证划分干净。4.2 注意力权重全均匀看不出重点现象可视化注意力权重每个词几乎一样。原因没加 maskpadding 稀释了权重或者注意力层学习率太大还没收敛。解决确认masked_fill生效把注意力层单独设小学习率比如主网络的 0.1 倍训练轮数加到 20 轮再看。4.3 加了对抗训练反而掉点现象FGM 一开验证 F1 比不加还低。原因eps设太大或者对抗训练和正常训练共用同一个 optimizer 导致梯度累积异常。解决eps从 0.5 开始试对抗那一步用单独的optimizer_adv或者干脆把对抗损失乘 0.5 的系数再加进去。4.4 显存爆了batch size 只能开到 8现象CUDA out of memory。原因序列长度没截断景区评论有的长达 500 字padding 到最大长度浪费显存。解决按分位数截断比如取 95 分位长度通常 128-200超长截断、不足补齐batch_firstTrue下用pad_sequence动态 padding。4.5 分词把情感词切没了现象模型对「不推荐」「没意思」这类否定词不敏感。原因jieba 把「不推荐」切成「不」「推荐」过滤单字时把「不」丢了否定信息丢失。解决把常见否定词加进自定义词典或者用len(w) 1保留单字再靠停用词表去掉真正无意义的字。5. 从能跑到好用注意力可视化与推理加速的两个技巧模型训完只是开始毕设答辩或实际部署时能解释、能快速推理才是加分项。这里给两个我常用的进阶技巧。第一个是注意力可视化把每个词的权重画成热力图直接看出模型关注了哪些词。做法是把Attention返回的weight存下来和分词结果对齐import matplotlib.pyplot as plt def plot_attention(tokens, weights, save_pathattn.png): # weights 是 [L] 的 numpy 数组 plt.figure(figsize(max(6, len(tokens) * 0.5), 2)) plt.imshow(weights.reshape(1, -1), cmapReds, aspectauto) plt.xticks(range(len(tokens)), tokens, rotation45, haright) plt.yticks([]) plt.colorbar() plt.tight_layout() plt.savefig(save_path, dpi150)注意weights要先把 padding 部分去掉再和 tokens 对齐否则词和权重会错位。这个图放在论文或汇报里比一堆指标数字更有说服力。第二个是推理加速。Bi-LSTM 是序列模型逐条推理慢实际部署时可以做两件事一是把模型转成 ONNX 或 TorchScript二是对短文本做 batch 推理。我一般用 TorchScriptmodel.eval() scripted torch.jit.script(model) scripted.save(sentiment_model.pt) # 推理时 loaded torch.jit.load(sentiment_model.pt) with torch.no_grad(): logits loaded(x, mask)TorchScript 能省掉 Python 解释开销在 CPU 上通常有 1.5-2 倍加速。如果还要更快可以把 Bi-LSTM 换成 CNN 或蒸馏成小模型但那是另一个话题了。最后说个我自己的习惯每次改完模型结构先在一个 200 条的小子集上跑 2 轮确认 loss 在降、注意力有权重、没有 shape 报错再上全量数据。这个「小步快跑」的习惯帮我省了无数次等一晚上结果发现维度对不上的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表