☰
基于BERT的电商评论属性级情感分析:从环境搭建到模型优化实战
2026/10/8 19:04:03 网站建设 项目流程

简介:这份资源是围绕电商评论场景展开的观点挖掘与情感分析实战项目,基于PyTorch与BERT实现,参考NER网络结构完成属性/观点的联合抽取与分类,适合计算机、人工智能、通信工程等专业学生、教师及企业员工学习,也可作为毕设、课程设计或项目立项的参考案例。压缩包共8个文件,以3个Python源码文件为核心,涵盖模型定义、EDA分析与训练脚本,另附1个Markdown说明文档及若干占位空文件,整体约9KB,结构精简便于快速上手。项目将属性起始位置按BEIS标注为8类,并对观点与属性组合进行28类分类,运行环境为Python3.6、PyTorch1.0.1与pytorch-pretrained-bert0.6.2。已有66人学习,读者可借此理解BERT在评论观点挖掘中的建模思路、数据标注方式与训练流程,并在此基础上修改扩展功能。

1. 从一份电商评论挖掘源码说起:BERT 做属性级情感分析到底怎么落地

电商评论里最值钱的信息不是「好评」「差评」这种粗粒度标签,而是「屏幕清晰但续航拉胯」这种把评价对象和情感绑在一起的细粒度表达。这份基于 BERT 的电商评论观点挖掘与情感分析项目,走的就是属性级情感分析(ABSA)里最经典的一条路线:把观点挖掘拆成序列标注任务,用 BERT 做编码器,再挂两个输出头分别预测属性/观点的位置和属性-观点配对分类。项目基于 PyTorch 和 pytorch-pretrained-bert 0.6.2,参考 NER 网络结构,第一个全连接层输出 BEIS 标注下的 8 个类别(属性开始、属性内部、属性结束、单字属性、观点开始、观点内部、观点结束、单字观点),第二个输出层做属性加观点的 28 类组合分类。它适合想入门 BERT 微调、做课程设计或毕设、又不想从零搭 NER 框架的人。下面按「资源是什么 → 怎么跑起来 → 坑在哪 → 怎么改」的顺序拆一遍。

2. 环境与数据准备:python3.6 + pytorch1.0.1 这套老组合怎么装才不翻车

2.1 为什么锁定 python3.6 和 pytorch-pretrained-bert 0.6.2

这个项目写于 BERT 刚开源不久的时间点,用的是pytorch-pretrained-bert这个早期库,后来它才被拆成transformers。如果你直接pip install transformers再跑这份代码,大概率在from pytorch_pretrained_bert import BertTokenizer这一行就报 ModuleNotFoundError。所以第一件事不是急着改代码,而是先把版本对齐。python3.6 现在官方已经停止维护,但用 conda 建一个独立环境仍然能装到,这是最省事的路径。常见做法是用 conda 而不是系统 python,避免污染全局环境。

# 建一个 python3.6 的独立环境,命名随意,这里叫 absa conda create -n absa python=3.6 conda activate absa # 装指定版本的 pytorch,1.0.1 对应 cuda9.0 或 cpu 版 # 如果你没有 GPU,直接装 cpu 版即可,代码里 device 会自动判断 pip install torch==1.0.1 torchvision==0.2.1 # 关键:装老版 bert 库,不要装 transformers pip install pytorch-pretrained-bert==0.6.2 # 其余依赖 pip install numpy pandas tqdm

逻辑说明:conda create隔离环境是这套老代码能跑通的前提,因为 python3.6 的 pip 源里很多包已经不再更新。torch==1.0.1是项目 README 里写明的版本,装高了会出现torch.nn接口不兼容。pytorch-pretrained-bert==0.6.2提供BertModel、BertTokenizer、BertAdam这些类,代码里models.py直接 import 的就是它们。参数上唯一要改的是 cuda 版本,如果你机器上是 cuda10 以上,torch1.0.1 可能装不上,这时要么降 cuda,要么用 cpu 版先跑通逻辑。

2.2 数据目录结构:TRAIN、TEST、checkpoints 三个空文件夹是干嘛的

解压后你会看到viewpoint-mining-master目录,里面有output、TRAIN、TEST、checkpoints四个空文件夹,还有README.md、code目录,code下是models.py、EDA.py、train_v2.py。空文件夹不是漏打包,而是约定好的输入输出位置。TRAIN放训练集,TEST放测试集,checkpoints存模型权重,output存预测结果。数据格式通常是每行一条样本,字段用制表符或特定分隔符隔开,包含文本、属性标签序列、观点标签序列。如果你手头没有现成数据,可以先用少量自己标注的评论跑通流程,再考虑扩量。

# 进入项目根目录 cd viewpoint-mining-master # 确认目录结构,空文件夹需要自己放数据 ls -R # 典型的数据文件命名,具体以 README 为准 # TRAIN/train.tsv TEST/test.tsv

逻辑说明:ls -R递归列出所有文件,能快速确认code下的脚本和空目录位置。参数上要注意,TRAIN和TEST里的文件名必须和train_v2.py里读取的路径一致,否则会报 FileNotFoundError。我一般会先打开train_v2.py搜open(或read_csv,看它到底读哪个路径,再决定数据放哪、叫什么名字。这一步是很多新手卡住的地方,不是代码错,是路径没对上。

2.3 预训练模型权重放哪:BERT 中文模型不是 pip 装完就有的

pytorch-pretrained-bert只提供加载权重的接口,不附带权重文件。你需要自己准备 BERT 中文预训练模型,通常是bert-base-chinese的 pytorch 版本,包含pytorch_model.bin、vocab.txt、config.json三个文件。把它们放在一个目录里,然后在代码里把bert_model_path指向这个目录。常见做法是放在项目根目录下新建bert-base-chinese文件夹。注意不要用 tensorflow 版本的权重,后缀和变量名对不上,加载会报错。

# models.py 里通常有类似这样的加载逻辑 from pytorch_pretrained_bert import BertModel, BertTokenizer # 指向你放权重文件的目录,不是单个文件 bert_path = "./bert-base-chinese" tokenizer = BertTokenizer.from_pretrained(bert_path) bert = BertModel.from_pretrained(bert_path)

逻辑说明:from_pretrained接收的是目录路径,它会自动找目录下的config.json和pytorch_model.bin。参数上,vocab.txt必须和权重配套,用错词表会导致 token id 对不上,模型输出全是乱码般的低分。如果你下载的是bert-base-chinese的压缩包,解压后确认这三个文件在同一层,不要多套一层文件夹。

3. 模型结构与训练脚本:两个输出头分别管什么,train_v2.py 怎么读

3.1 BEIS 标注 8 类 + 属性观点组合 28 类,两个全连接层的分工

这个项目的核心在models.py。BERT 编码后取最后一层隐状态,接两个全连接层。第一个头做序列标注,每个 token 输出 8 类之一,对应 BEIS 方案:B-ASP(属性开始)、I-ASP(属性内部)、E-ASP(属性结束)、S-ASP(单字属性)、B-OPI、I-OPI、E-OPI、S-OPI。第二个头做分类,把属性片段和观点片段组合起来,输出 28 类,表示「属性类别 + 观点类别」的配对关系。为什么是 28 而不是更多,取决于你的属性类别数和观点类别数的组合,项目里已经固定好。这种双头结构比单纯做 NER 多了一步配对,能直接给出「哪个属性对应哪个观点」的结果。

# models.py 结构示意,非逐行照抄,重点看两个头的维度 class BertABSAModel(nn.Module): def __init__(self, bert_path, num_tags=8, num_relations=28): super().__init__() self.bert = BertModel.from_pretrained(bert_path) hidden = self.bert.config.hidden_size # 中文 base 是 768 # 第一个头:序列标注,输出 8 类 self.tag_classifier = nn.Linear(hidden, num_tags) # 第二个头:属性-观点组合分类,输出 28 类 self.relation_classifier = nn.Linear(hidden * 2, num_relations) def forward(self, input_ids, token_type_ids, attention_mask): outputs, _ = self.bert(input_ids, token_type_ids, attention_mask, output_all_encoded_layers=False) # outputs 形状 [batch, seq_len, hidden] tag_logits = self.tag_classifier(outputs) # 组合分类通常取属性片段和观点片段的表示拼接 # 具体拼接方式看 train_v2.py 里的调用 return tag_logits

逻辑说明:num_tags=8对应 BEIS 的 8 个标签,num_relations=28对应组合类别数。hidden_size对bert-base-chinese是 768,如果你换 small 版会变,要同步改。output_all_encoded_layers=False表示只取最后一层,取 True 会返回所有层,显存翻倍且这里用不上。参数上,tag_classifier的输入维度必须等于hidden,写错会在第一次 forward 时报维度不匹配。第二个头的hidden*2是因为要把属性表示和观点表示拼起来,具体在训练脚本里实现。

3.2 train_v2.py 的训练循环:batch、学习率、epoch 怎么设

train_v2.py是主训练脚本,负责读数据、转 token、前向、算 loss、反向、存 checkpoint。BERT 微调的学习率通常设得很小,2e-5 到 5e-5 之间,项目里一般用BertAdam而不是普通 Adam,因为它带 warmup 和权重衰减。batch size 受显存限制,中文 base 模型在 8G 显存上大概能跑到 16 到 32。epoch 一般 3 到 5 就够,再多容易过拟合。这些参数在脚本开头通常以 argparse 或直接常量的形式出现,跑之前先看一眼。

# 典型启动方式,具体参数名以 train_v2.py 为准 python code/train_v2.py \ --train_path TRAIN/train.tsv \ --test_path TEST/test.tsv \ --bert_path ./bert-base-chinese \ --checkpoint_dir checkpoints \ --batch_size 16 \ --lr 3e-5 \ --epochs 3

逻辑说明:--train_path和--test_path指向你放好的数据文件,路径写错直接报错退出。--batch_size 16是显存和效果的折中,显存不够就降到 8,但太小会让 batch norm 类操作不稳定,这里 BERT 内部是 layer norm,影响小一些。--lr 3e-5是微调常用值,设 1e-3 这种大会把预训练权重冲垮,loss 先降后炸。--epochs 3先跑通看 loss 曲线,如果验证集指标还在涨再加。checkpoint 会存到checkpoints目录,文件名一般带 epoch 或 step。

3.3 EDA.py 做什么:数据探查不是可选项

EDA.py是数据探索脚本,用来统计标签分布、句子长度分布、属性观点配对数量。很多人跳过它直接训练,结果发现某类标签样本极少,模型根本学不会。先跑 EDA 能提前发现类别不平衡,决定要不要做重采样或调 loss 权重。常见做法是看每个标签的出现次数,如果某个组合类只有个位数样本,那它在测试集上基本预测不出来,这是数据问题不是模型问题。

# EDA.py 里常见的统计逻辑示意 import pandas as pd df = pd.read_csv("TRAIN/train.tsv", sep="\t") # 统计标签序列里每个标签出现次数 from collections import Counter tag_counter = Counter() for tags in df["tag_seq"]: tag_counter.update(tags.split()) print(tag_counter) # 统计句子长度,决定 max_seq_len 设多少 print(df["text"].str.len().describe())

逻辑说明:sep="\t"要和实际数据分隔符一致,用错会把整行读成一列。tag_counter输出每个 BEIS 标签的频次,如果S-OPI特别少,说明单字观点样本稀缺。describe()给出长度分位数,max_seq_len一般设成 95 分位以上,设太小会截断长评论丢信息,设太大浪费显存。这一步花十分钟,能省后面几小时的调参。

4. 避坑与排查:这套老代码最容易翻车的五个地方

4.1 报 ModuleNotFoundError: No module named 'pytorch_pretrained_bert'

现象:装完依赖跑脚本,第一行 import 就报找不到模块。原因:装成了transformers或者根本没装pytorch-pretrained-bert。解决:pip uninstall transformers再pip install pytorch-pretrained-bert==0.6.2,确认pip list里有这个包。注意包名里是下划线,import 时也是下划线。

4.2 加载 BERT 权重时报 KeyError 或 size mismatch

现象:from_pretrained时抛 KeyError,或者提示某个权重 shape 不匹配。原因:下载的权重不是 pytorch 版,或者用了 base 的 config 配了 large 的权重。解决:确认目录下是pytorch_model.bin而不是bert_model.ckpt,确认config.json里的hidden_size和权重一致。中文 base 是 768,large 是 1024,混用必错。

4.3 训练 loss 一直是 nan 或者不下降

现象:跑几个 step 后 loss 变 nan,或者一直卡在 8 点几不动。原因:学习率太大,或者数据里有空样本导致除零。解决:把 lr 降到 2e-5,加梯度裁剪clip_grad_norm_,检查数据里有没有空行。另外确认标签 id 没有越界,8 类标签的 id 应该在 0 到 7 之间,出现 8 就会让 cross entropy 报错或产生 nan。

4.4 显存不够 CUDA out of memory

现象:跑到一半报 out of memory。原因:batch size 太大,或者 max_seq_len 设太长。解决:先把 batch size 减半,再把 max_seq_len 从 128 降到 64 试试。如果还不行,检查是不是在验证时没加torch.no_grad(),导致计算图一直累积。这个项目里验证和预测阶段一定要包在no_grad里。

4.5 预测结果全是同一个标签

现象:模型在测试集上所有 token 都预测成 O 或者同一个类。原因:类别极度不平衡,或者训练轮数不够模型还没学到。解决:先跑 EDA 看标签分布,对稀有类做重采样;把 epoch 加到 5 再看;检查 loss 是不是在下降。如果 loss 降了但预测还是单一,可能是第二个分类头的学习率没跟上,可以给两个头设不同的 lr。

5. 进阶改法与验证:换预训练模型、加 CRF、看混淆矩阵

跑通之后想提升效果,有几个方向可以试。第一是换预训练模型,把bert-base-chinese换成bert-base-chinese的 whole word masking 版本,或者换 RoBERTa 的中文版,接口类似但词表不同,要同步换vocab.txt。第二是在序列标注头后面加 CRF 层,BEIS 标签之间有转移约束,比如 I-ASP 不能直接跟 B-OPI,CRF 能学这些约束,通常能涨一两个点。第三是看混淆矩阵,把 28 类的预测和真实标签做交叉表,找出最容易被混淆的组合类,针对性补数据。

# 加 CRF 的示意,需要先 pip install pytorch-crf from torchcrf import CRF class BertABSAModelWithCRF(nn.Module): def __init__(self, bert_path, num_tags=8): super().__init__() self.bert = BertModel.from_pretrained(bert_path) self.tag_classifier = nn.Linear(self.bert.config.hidden_size, num_tags) self.crf = CRF(num_tags, batch_first=True) def forward(self, input_ids, token_type_ids, attention_mask, tags=None): outputs, _ = self.bert(input_ids, token_type_ids, attention_mask, output_all_encoded_layers=False) emissions = self.tag_classifier(outputs) if tags is not None: # 训练时算负对数似然 loss = -self.crf(emissions, tags, mask=attention_mask.bool()) return loss else: # 预测时维特比解码 return self.crf.decode(emissions, mask=attention_mask.bool())

逻辑说明:CRF(num_tags, batch_first=True)的batch_first要和输入维度顺序一致,这里 emissions 是[batch, seq_len, num_tags],所以设 True。mask用 attention_mask 转 bool,把 padding 位置排除,否则 CRF 会把 padding 也纳入转移计算。训练时返回负对数似然当 loss,预测时decode返回最优路径。参数上,CRF 会额外增加一点显存和计算量,但通常值得。验证方法上,除了看准确率,一定要看每个类别的 F1,属性级任务里稀有类的 F1 比整体准确率更能反映真实水平。

# 看混淆矩阵,sklearn 一行搞定 from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt cm = confusion_matrix(true_labels, pred_labels) sns.heatmap(cm, annot=True, fmt="d", cmap="Blues") plt.xlabel("Predicted") plt.ylabel("True") plt.savefig("output/confusion_matrix.png")

逻辑说明:confusion_matrix接收两个一维数组,分别是真实标签和预测标签。annot=True把数值标在格子里,fmt="d"保证显示整数。存到output目录,和项目约定一致。看的时候重点找非对角线上的大数字,那就是模型分不清的类别对。我一般会先把混淆矩阵跑出来再决定下一步是补数据还是改结构,而不是盲目调参。从那以后我每次跑完训练都强制走一遍混淆矩阵和分类报告,确认没有某个类被完全忽略。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询