企业大模型实战S1.3:基于Bert的文本情感与主题分类
本文介绍了一个基于BERT的多任务文本分类模型,用于同时识别汽车行业用户评论的主题和情感。模型采用BERT作为特征提取器,通过两个分类头分别处理多标签主题分类(10类)和三分类情感分析。数据预处理使用MultiLabelBinarizer处理多标签主题,情感标签通过数值累加转换为单标签。训练过程中结合BCEWithLogitsLoss和CrossEntropyLoss两种损失函数,并监控训练/测试
继S1.2的实战后,我们来做一个基于Bert的文本主题与情感识别。虽然我们还不知道Bert的相关原理,但是本节我们主要来了解一下大模型的下游任务。
数据集是来自真实的汽车行业用户观点数据,(提取码:M2Hm)数据示例:

其中标签有两类,分别是主题和情感。训练集数据中主题被分为10类,包括:动力、价格、内饰、配置、安全性、外观、操控、油耗、空间、舒适性;情感分为3类,分别用0,1,-1表示中立、正向、负向
项目需求:
情感识别是三分类(互斥,single-label),主题识别是多标签分类(multi-label)
指标采用accuracy、precision、recall、F1,绘制训练和测试的loss、accuracy曲线
如果有多个标签,情感采用数字加和的方式,sum大于0为正向,小于0为负向,0为中性
多标签分类的准确率和召回率指标采用macro平均
注:我们不需要完全陷入数学公式,具体原理我们会在后续的章节详细刨析,代码中几乎所有部分我已注释,大家先掌握大模型处理文本的流程,但是一定要手写代码感受一下。
# 导入了所需的库和模块。
# 包括PyTorch及其子模块、transformers库中的BERT模型和分词器、数据处理库Pandas和NumPy、绘图库matplotlib、评估分类性能的库sklearn等。
import torch
import torch.nn as nn#导入 PyTorch 的神经网络模块,包含常用的层(如线性层、激活函数)和损失函数等,用于定义模型结构
from torch.utils.data import Dataset, DataLoader
#导入数据加载工具,Dataset用于自定义数据集格式,DataLoader用于批量加载数据并支持并行处理,是高效训练的基础
from transformers import BertTokenizer, BertModel
#导入 BERT 模型相关工具。BertTokenizer用于文本预处理(如分词、转换为词向量索引),BertModel是预训练的 BERT 模型,可用于特征提取或微调任务(如文本分类、情感分析等)
import pandas as pd
#导入 Pandas,用于处理结构化数据(如 CSV 表格),方便读取、清洗和转换数据集。
import numpy as np
import matplotlib
matplotlib.use('Agg') # 设置后端为 Agg,适合无 GUI 环境,适合在无图形界面的环境(如服务器)中生成图片文件
import matplotlib.pyplot as plt
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, classification_report
from sklearn.preprocessing import MultiLabelBinarizer#用于多标签分类任务中的标签预处理(将多类别标签转换为二进制矩阵)
from sklearn.model_selection import train_test_split
import os#导入操作系统接口,用于文件路径处理、目录创建等操作(如保存模型、图片时指定路径)
# 这个CarOpinionDataset类继承自PyTorch的Dataset,用于封装数据集。它将文本数据进行编码,并将主题和情感标签转换为张量
class CarOpinionDataset(Dataset):
def __init__(self, texts, topics, sentiments, tokenizer, max_length):
self.texts = texts
self.topics = topics
self.sentiments = sentiments
self.tokenizer = tokenizer#BERT 分词器(如BertTokenizer),用于文本预处理
self.max_length = max_length#文本最大长度(超过则截断,不足则补全),保证输入模型的序列长度一致
def __len__(self):
return len(self.texts)#返回数据集的样本总数,是Dataset类的强制实现方法
def __getitem__(self, idx):#根据索引 idx 获取单条样本,并完成预处理(文本编码 + 标签转张量),是Dataset类的核心方法
text = str(self.texts[idx])#通过索引 idx 提取单条文本、主题标签、情感标签
topic = self.topics[idx]
sentiment = self.sentiments[idx]
encoding = self.tokenizer.encode_plus(
text,
add_special_tokens=True,#添加 BERT 要求的特殊符号(如句首[CLS]、句尾[SEP])
max_length=self.max_length,#超过max_length的文本截断
return_token_type_ids=False,
padding='max_length',#不足max_length的文本用[PAD]补全,保证序列长度统一
truncation=True,
return_attention_mask=True,#返回注意力掩码(标记哪些是真实文本、哪些是补全的[PAD],模型会忽略[PAD]部分)
return_tensors='pt'#直接返回 PyTorch 张量(避免后续手动转换)
)
return {
'input_ids': encoding['input_ids'].flatten(),
'attention_mask': encoding['attention_mask'].flatten(),
'topics': torch.tensor(topic, dtype=torch.float), # 因主题可能是多标签(如一条文本同时涉及 “价格” 和 “油耗”,标签为[1,1,0,...]),用浮点型适配多标签分类的损失计算
'sentiments': torch.tensor(sentiment, dtype=torch.long)#情感是单标签分类(如 0 = 负面、1 = 中性、2 = 正面),用长整型适配分类任务的标签格式
}
#这段代码定义了一个基于 BERT 预训练模型的多任务分类器 BertClassifier,继承自 PyTorch 的 nn.Module,核心功能是同时完成主题分类和情感分类两个任务。模型通过 BERT 提取文本特征,再分别通过两个独立的线性层输出分类结果,适合处理需要同时预测文本主题和情感倾向的场景(如汽车评论的主题识别 + 情感分析)
class BertClassifier(nn.Module):
def __init__(self, bert_model_name, num_topics, num_sentiments):#定义模型的核心组件,包括预训练 BERT、dropout 层和两个分类头
super(BertClassifier, self).__init__()
self.bert = BertModel.from_pretrained(bert_model_name)#加载预训练 BERT 模型,作为文本特征提取器。BERT 能将输入文本转换为包含上下文信息的高维特征向量
self.dropout = nn.Dropout(0.5)#训练时随机让 50% 的神经元失活,用于减轻过拟合
self.topic_classifier = nn.Linear(self.bert.config.hidden_size, num_topics)#主题分类头,是一个线性层(nn.Linear),输入维度为 BERT 的隐藏层维度(self.bert.config.hidden_size,通常为 768),输出维度为num_topics(主题类别数),负责输出主题分类的 logits;
self.sentiment_classifier = nn.Linear(self.bert.config.hidden_size, num_sentiments)
def forward(self, input_ids, attention_mask):#input_ids:文本经分词器编码后的数字序列(BERT 的输入格式);
attention_mask:注意力掩码(标记哪些位置是真实文本,哪些是填充的[PAD],BERT 会忽略填充部分)。
outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)
pooled_output = outputs.pooler_output
pooled_output = self.dropout(pooled_output)#提取句子级特征向量,应用 dropout 层,增强模型泛化能力调用 BERT 模型处理输入,返回的outputs包含多个信息,其中 pooler_output 是 BERT 对整个句子的全局特征表示(从[CLS] token 的隐藏状态转换而来
topic_logits = self.topic_classifier(pooled_output)#将处理后的特征输入主题分类头,输出主题分类的 logits(未经过 softmax 的原始分数)
sentiment_logits = self.sentiment_classifier(pooled_output)
return topic_logits, sentiment_logits#后续可通过损失函数(如主题用 BCEWithLogitsLoss,情感用 CrossEntropyLoss)计算损失并反向传播更新参数
# 这个函数从文件中读取数据,将文本、主题和情感标签分别存储在列表中。主题和情感标签通过映射转换为数字。
def read_data(file_path):
texts = []
topics = []
sentiments = []
topic_mapping = {'动力': 0, '价格': 1, '内饰': 2, '配置': 3, '安全性': 4, '外观': 5, '操控': 6, '油耗': 7, '空间': 8, '舒适性': 9} # 主题标签映射
sentiment_mapping = {'0': 0, '1': 1, '-1': 2} # 情感标签映射,将 -1 映射为 2
with open(file_path, 'r', encoding='utf-8') as file:
for line in file:
parts = line.strip().split('\t')
text = parts[0]#假设文件每行数据用制表符\t分隔为两部分(第一部分是文本,第二部分是标签组合);text = parts[0]提取第一部分作为原始文本,存入texts列表
topic_labels = []
sentiment_label = 0
for label in parts[1].split(' '):
#假设标签部分用空格分隔多个 “主题 #情感” 组合(如 “动力 #1 价格 #-1” 表示文本涉及 “动力” 主题且情感为正面,涉及 “价格” 主题且情感为负面)
#循环拆分每个 “主题 #情感” 标签:topic_label, sentiment = label.split('#')
#将主题标签转换为数字:topic_mapping[topic_label],存入topic_labels列表(一条文本可能对应多个主题,因此是列表形式);
将情感标签转换为数字:sentiment_mapping[sentiment],并累加得到sentiment_label(这里逻辑是将多个情感标签的数值相加,可能用于简化为单标签,具体需结合数据实际场景)
topic_label, sentiment = label.split('#')
topic_labels.append(topic_mapping[topic_label])
sentiment_label += sentiment_mapping[sentiment]
texts.append(text)
topics.append(topic_labels)
sentiments.append(sentiment_label)
return texts, topics, sentiments
# 读取训练和测试数据,使用MultiLabelBinarizer将多标签数据转换为二进制形式,并创建数据加载器。
def prepare_data(tokenizer, max_length, train_path, test_path):
train_texts, train_topics, train_sentiments = read_data(train_path)
test_texts, test_topics, test_sentiments = read_data(test_path)
mlb = MultiLabelBinarizer(classes=list(range(10))) # 10 个主题类别
train_topics = mlb.fit_transform(train_topics)
test_topics = mlb.transform(test_topics)
train_dataset = CarOpinionDataset(train_texts, train_topics, train_sentiments, tokenizer, max_length)
test_dataset = CarOpinionDataset(test_texts, test_topics, test_sentiments, tokenizer, max_length)
train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=8)
return train_loader, test_loader, mlb
def train(model, train_loader, test_loader, optimizer, device, num_epochs, criterion_topic, criterion_sentiment):
train_losses = []
test_losses = []
train_accuracies = []
test_accuracies = []
# scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)
for epoch in range(num_epochs):
model.train()
total_loss = 0
all_preds_topic = []
all_labels_topic = []
all_preds_sentiment = []
all_labels_sentiment = []
for batch in train_loader:
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
topics = batch['topics'].to(device)
sentiments = batch['sentiments'].to(device)
optimizer.zero_grad()
topic_logits, sentiment_logits = model(input_ids, attention_mask)
loss_topic = criterion_topic(topic_logits, topics)
loss_sentiment = criterion_sentiment(sentiment_logits, sentiments)
loss = loss_topic + loss_sentiment
total_loss += loss.item()
loss.backward()
optimizer.step()
# 主题分类的预测
topic_preds = (torch.sigmoid(topic_logits) > 0.5).float()
all_preds_topic.extend(topic_preds.cpu().detach().numpy())
all_labels_topic.extend(topics.cpu().numpy())
# 情感分类的预测
_, sentiment_preds = torch.max(sentiment_logits, dim=1)
all_preds_sentiment.extend(sentiment_preds.cpu().detach().numpy())
all_labels_sentiment.extend(sentiments.cpu().numpy())
train_losses.append(total_loss / len(train_loader))
train_accuracy_topic = accuracy_score(all_labels_topic, all_preds_topic)
train_accuracy_sentiment = accuracy_score(all_labels_sentiment, all_preds_sentiment)
train_accuracies.append((train_accuracy_topic + train_accuracy_sentiment) / 2)
print(f"Epoch {epoch+1}/{num_epochs}, Train Loss: {train_losses[-1]}, Train Accuracy: {train_accuracies[-1]}")
model.eval()
total_loss = 0
all_preds_topic = []
all_labels_topic = []
all_preds_sentiment = []
all_labels_sentiment = []
with torch.no_grad():
for batch in test_loader:
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
topics = batch['topics'].to(device)
sentiments = batch['sentiments'].to(device)
topic_logits, sentiment_logits = model(input_ids, attention_mask)
loss_topic = criterion_topic(topic_logits, topics)
loss_sentiment = criterion_sentiment(sentiment_logits, sentiments)
loss = loss_topic + loss_sentiment
total_loss += loss.item()
# 主题分类的预测
topic_preds = (torch.sigmoid(topic_logits) > 0.5).float()
all_preds_topic.extend(topic_preds.cpu().numpy())
all_labels_topic.extend(topics.cpu().numpy())
# 情感分类的预测
_, sentiment_preds = torch.max(sentiment_logits, dim=1)
all_preds_sentiment.extend(sentiment_preds.cpu().numpy())
all_labels_sentiment.extend(sentiments.cpu().numpy())
test_losses.append(total_loss / len(test_loader))
test_accuracy_topic = accuracy_score(all_labels_topic, all_preds_topic)
test_accuracy_sentiment = accuracy_score(all_labels_sentiment, all_preds_sentiment)
test_accuracies.append((test_accuracy_topic + test_accuracy_sentiment) / 2)
print(f"Epoch {epoch+1}/{num_epochs}, Test Loss: {test_losses[-1]}, Test Accuracy: {test_accuracies[-1]}")
return train_losses, test_losses, train_accuracies, test_accuracies
# 这个函数实现了模型的训练过程。它在每个epoch中计算训练和测试损失,并评估准确率。
# 使用了二元交叉熵损失函数(BCEWithLogitsLoss)进行多标签分类,使用交叉熵损失函数(CrossEntropyLoss)进行情感分类。
def evaluate(model, test_loader, device, mlb):
model.eval()
all_preds_topic = []
all_labels_topic = []
all_preds_sentiment = []
all_labels_sentiment = []
with torch.no_grad():
for batch in test_loader:
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
topics = batch['topics'].to(device)
sentiments = batch['sentiments'].to(device)
topic_logits, sentiment_logits = model(input_ids, attention_mask)
# 主题分类的预测
topic_preds = (torch.sigmoid(topic_logits) > 0.5).float()
all_preds_topic.extend(topic_preds.cpu().numpy())
all_labels_topic.extend(topics.cpu().numpy())
# 情感分类的预测
_, sentiment_preds = torch.max(sentiment_logits, dim=1)
all_preds_sentiment.extend(sentiment_preds.cpu().numpy())
all_labels_sentiment.extend(sentiments.cpu().numpy())
print("Topic Classification Report:")
print(classification_report(all_labels_topic, all_preds_topic, target_names=[str(i) for i in range(10)], zero_division=0))
print("Sentiment Classification Report:")
print(classification_report(all_labels_sentiment, all_preds_sentiment, target_names=['neutral', 'positive', 'negative']))
return all_preds_topic, all_labels_topic, all_preds_sentiment, all_labels_sentiment
def main():
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
tokenizer_path = '/root/bert-base-uncased'
tokenizer = BertTokenizer.from_pretrained(tokenizer_path)
model = BertClassifier(tokenizer_path, num_topics=10, num_sentiments=3).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5,weight_decay=0.01)
criterion_topic = nn.BCEWithLogitsLoss() # 多标签分类使用 BCEWithLogitsLoss
criterion_sentiment = nn.CrossEntropyLoss() # 情感分类使用 CrossEntropyLoss
train_path = '/root/train.txt' # 修改为远程服务器上的训练集路径
test_path = '/root/test.txt' # 修改为远程服务器上的测试集路径
train_loader, test_loader, mlb = prepare_data(tokenizer, max_length=128, train_path=train_path, test_path=test_path)
num_epochs = 50
train_losses, test_losses, train_accuracies, test_accuracies = train(model, train_loader, test_loader, optimizer, device, num_epochs, criterion_topic, criterion_sentiment)
all_preds_topic, all_labels_topic, all_preds_sentiment, all_labels_sentiment = evaluate(model, test_loader, device, mlb)
# 绘制 loss 曲线和 accuracy 曲线
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(train_losses, label='Train Loss')
plt.plot(test_losses, label='Test Loss')
plt.title('Loss Curve')
plt.xlabel='Epoch'
plt.ylabel='Loss'
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(train_accuracies, label='Train Accuracy')
plt.plot(test_accuracies, label='Test Accuracy')
plt.title='Accuracy Curve'
plt.xlabel='Epoch'
plt.ylabel='Accuracy'
plt.legend()
plt.savefig('/tmp/loss_accuracy_curves.png') # 保存图像到文件,而不是显示图像
# plt.show()
if __name__ == "__main__":
main()
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)