我的实践:pytorch框架下基于BERT实现文本情感分类
当前,在BERT等预训练模型的基础上进行微调已经成了NLP任务的一个定式了。为了了解BERT怎么用,在这次实践中,我实现了一个最简单的NLP任务,即文本情感分类。
文章目录
- 1.基于BERT进行情感分类的基本思路
- 2.数据集准备
- 3.数据预处理
- 4.数据处理成dataSet
- 5.模型的搭建
- 6.模型的训练
1.基于BERT进行情感分类的基本思路
所谓情感分类就是指判断句子是积极情感还是消极情感,例如说“今天这顿饭太美味了”是积极的情感,“今天这顿饭简直吃不下去”是消极的情感。
基于BERT完成情感分类的基本思路如图所示。我们知道BERT是一个预训练模型,我们把句子扔给它的时候,它对应每个字都会输出一个向量。但是在把句子扔给BERT之前,我们会在句子最前面增加一个特殊符号[CLS]。对应这个[CLS],BERT也会输出一个向量,我们就是利用这个向量来进行情感分类。为什么可以直接利用这个向量呢?这是因为BERT内部采用的是自注意力机制,自注意力机制的特点是考虑全局又聚焦重点,实际上[CLS]对应的向量已经嵌入了整个句子的信息,而且重点词字嵌入的信息权重要大。所以,我们将这个向量扔给一个全连接层,就可以完成分类任务了。由于BERT已经是一个预训练模型了,我们在做情感分类时可以将BERT的参数固定住,不再调整,而只是调整全连接层的参数,我在这次实践中就是这么做的。当然也可以同时调整BERT和全连接层的参数,但是BERT模型较大,消耗的时间会长一些。
2.数据集准备
我在网上下了一个数据集(点击可下载,提取码为zfh3),csv格式的,包含两列,一列是句子,一列是标签,如下图所示。我才这个数据集应该是来自大众点评。。。,数据里面标签为0的时候表示是消极的情感,标签为1时表示的是积极的情感。这个数据集总共有11987行,由于我用的是CPU电脑,速度实在太慢,所以我只用了200条数据,其中150条数据用于训练,50条数据用于测试。
3.数据预处理
利用BERT实现情感分类的关键就是要把数据处理成BERT需要的输入形式。BERT的输入包括三个部分:第一个部分 是句子中每个字对应的id,我们用input_ids表示,这个id需要用到BERT的字库,字库里面每个字所排的次序就是id。第二个部分 是mask,我们用input_mask表示,假设我们设置BERT输入的句子最大长度是128,如果我们的句子长度是100,那么mask前100个填1,后面28个填0。第三部分 是句子标识符id,我们用segment_ids表示,如果第一句全为0,如果是第二句全为1,以此类推,由于情感分类只涉及到一个句子,所以该标识符都是0。
将一个句子处理成上面这样的输入,要经过两步,第一步 是对句子进行分词,在英文里面叫做“tokenize”,分词后的结果称为“tokens”。对于中文来说,分词后的结果很简单,就是一个一个的字。完成该项工作可以使用tokenizer.tokenize(text)。下图分词的一个示例。
完成分词后,第二步 要将tokens转换成id,对于中文来说,就是把一个一个的字转换成字对应的id。此外呢,还要获取input_mask和segment_ids。实现该步骤可以使用tokenizer.encode_plus()。下图是一个示例,第二个参数max_seq_length是指BERT输入句子的最大长度。
下面是数据预处理的代码,保存在dataProcessor.py文件中。
import pandas as pd
import os
import logginglogging.basicConfig(format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',datefmt='%m/%d/%Y %H:%M:%S',level=logging.INFO)
logger = logging.getLogger(__name__)class InputExample(object):"""A single training/test example for simple sequence classification."""def __init__(self, text, label=None):self.text = textself.label = labelclass InputFeatures(object):"""A single set of features of data."""def __init__(self, input_ids, input_mask, segment_ids, label_id):self.input_ids = input_idsself.input_mask = input_maskself.segment_ids = segment_idsself.label_id = label_idclass DataProcessor(object):"""Base class for data converters for sequence classification data sets."""def get_train_examples(self, data_dir):"""Gets a collection of `InputExample`s for the train set."""raise NotImplementedError()def get_dev_examples(self, data_dir):"""Gets a collection of `InputExample`s for the dev set."""raise NotImplementedError()def get_test_examples(self, data_dir):"""Gets a collection of `InputExample`s for the test set."""raise NotImplementedError()def get_labels(self):"""Gets the list of labels for this data set."""raise NotImplementedError()@classmethoddef _read_csv(cls, input_file, quotechar=None):"""Reads a tab separated value file."""# dicts = []data = pd.read_csv(input_file)return dataclass MyPro(DataProcessor):'''自定义数据读取方法,针对json文件Returns:examples: 数据集,包含index、中文文本、类别三个部分'''def get_train_examples(self, data_dir):return self._create_examples(self._read_csv(os.path.join(data_dir, 'train_data.csv')), 'train')def get_dev_examples(self, data_dir):return self._create_examples(self._read_csv(os.path.join(data_dir, 'dev_data.csv')), 'dev')def get_test_examples(self, data_dir):return self._create_examples(self._read_csv(os.path.join(data_dir, 'test_data.csv')), 'test')def get_labels(self):return [0, 1]def _create_examples(self, data, set_type):examples = []for index, row in data.iterrows():# guid = "%s-%s" % (set_type, i)text = row['review']label = row['label']examples.append(InputExample(text=text, label=label))return examplesdef convert_examples_to_features(examples, label_list, max_seq_length, tokenizer, show_exp=True):'''Loads a data file into a list of `InputBatch`s.Args:examples : [List] 输入样本,句子和labellabel_list : [List] 所有可能的类别,0和1max_seq_length: [int] 文本最大长度tokenizer : [Method] 分词方法Returns:features:input_ids : [ListOf] token的id,在chinese模式中就是每个分词的id,对应一个word vectorinput_mask : [ListOfInt] 真实字符对应1,补全字符对应0segment_ids: [ListOfInt] 句子标识符,第一句全为0,第二句全为1label_id : [ListOfInt] 将Label_list转化为相应的id表示'''label_map = {}for (i, label) in enumerate(label_list):label_map[label] = ifeatures = []for (ex_index, example) in enumerate(examples):# 分词tokens = tokenizer.tokenize(example.text)# tokens进行编码encode_dict = tokenizer.encode_plus(text=tokens,max_length=max_seq_length,pad_to_max_length=True,is_pretokenized=True,return_token_type_ids=True,return_attention_mask=True)input_ids = encode_dict['input_ids']input_mask = encode_dict['attention_mask']segment_ids = encode_dict['token_type_ids']assert len(input_ids) == max_seq_lengthassert len(input_mask) == max_seq_lengthassert len(segment_ids) == max_seq_lengthlabel_id = label_map[example.label]if ex_index < 5 and show_exp:logger.info("*** Example ***")logger.info("tokens: %s" % " ".join([str(x) for x in tokens]))logger.info("input_ids: %s" % " ".join([str(x) for x in input_ids]))logger.info("input_mask: %s" % " ".join([str(x) for x in input_mask]))logger.info("segment_ids: %s" % " ".join([str(x) for x in segment_ids]))logger.info("label: %s (id = %d)" % (example.label, label_id))features.append(InputFeatures(input_ids=input_ids,input_mask=input_mask,segment_ids=segment_ids,label_id=label_id))return features
4.数据处理成dataSet
数据处理成dataSet的核心就是把BERT模型的输入处理成tensor格式,下面是代码,保存在dataset.py文件中
import torch
from torch.utils.data import Datasetclass MyDataset(Dataset):def __init__(self, features, mode):self.nums = len(features)self.input_ids = [torch.tensor(example.input_ids).long() for example in features]self.input_mask = [torch.tensor(example.input_mask).float() for example in features]self.segment_ids = [torch.tensor(example.segment_ids).long() for example in features]self.label_id = Noneif mode == 'train' or 'test':self.label_id = [torch.tensor(example.label_id) for example in features]def __getitem__(self, index):data = {'input_ids': self.input_ids[index],'input_mask': self.input_mask[index],'segment_ids': self.segment_ids[index]}if self.label_id is not None:data['label_id'] = self.label_id[index]return datadef __len__(self):return self.nums
5.模型的搭建
模型的搭建很简单,模型的第一层是BERT,将BERT输出的第一个向量,即[CLS]对应的向量,传递给一个线性层,该线性层作为一个分类器输出维度为2的向量。BERT模型会有两个输出:一个输出是序列输出,即句子的每一个字都输出一个向量,叫做seq_out,这个输出一般用于实体识别等句子标注任务;另一个输出是pooled_out,即[CLS]对应的向量,这个输出一般用于句子分类。我们用pooled_out这个输出。这里我把BERT的参数冻结住了,只调整线性层的参数,所以用x = pooled_out.detach()对反向传播进行截断。下面是模型搭建的代码,
from torch import nn
import os
from transformers import BertModelclass ClassifierModel(nn.Module):def __init__(self,bert_dir,dropout_prob=0.1):super(ClassifierModel, self).__init__()config_path = os.path.join(bert_dir, 'config.json')assert os.path.exists(bert_dir) and os.path.exists(config_path), \'pretrained bert file does not exist'self.bert_module = BertModel.from_pretrained(bert_dir)self.bert_config = self.bert_module.configself.dropout_layer = nn.Dropout(dropout_prob)out_dims = self.bert_config.hidden_sizeself.obj_classifier = nn.Linear(out_dims, 2)def forward(self,input_ids,input_mask,segment_ids,label_id=None):bert_outputs = self.bert_module(input_ids=input_ids,attention_mask=input_mask,token_type_ids=segment_ids)seq_out, pooled_out = bert_outputs[0], bert_outputs[1]#对反向传播及逆行截断x = pooled_out.detach()out = self.obj_classifier(x)return out
6.模型的训练
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from model import *
from dataset import *
from dataProcessor import *
import matplotlib.pyplot as plt
import time
from transformers import BertTokenizer
from transformers import logginglogging.set_verbosity_warning()
# 加载训练数据
datadir = "data"
bert_dir = "bert\\bert-chinese"
my_processor = MyPro()
label_list = my_processor.get_labels()train_data = my_processor.get_train_examples(datadir)
test_data = my_processor.get_test_examples(datadir)tokenizer = BertTokenizer.from_pretrained(bert_dir)train_features = convert_examples_to_features(train_data, label_list, 128, tokenizer)
test_features = convert_examples_to_features(test_data, label_list, 128, tokenizer)
train_dataset = MyDataset(train_features, 'train')
test_dataset = MyDataset(test_features, 'test')
train_data_loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_data_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=True)train_data_len = len(train_dataset)
test_data_len = len(test_dataset)
print(f"训练集长度:{train_data_len}")
print(f"测试集长度:{test_data_len}")# 创建网络模型
my_model = ClassifierModel(bert_dir)# 损失函数
loss_fn = nn.CrossEntropyLoss()# 优化器
learning_rate = 5e-3
#optimizer = torch.optim.SGD(my_model.parameters(), lr=learning_rate)
# Adam 参数betas=(0.9, 0.99)
optimizer = torch.optim.Adam(my_model.parameters(), lr=learning_rate, betas=(0.9, 0.99))
# 总共的训练步数
total_train_step = 0
# 总共的测试步数
total_test_step = 0
step = 0
epoch = 50writer = SummaryWriter("logs")
# writer.add_graph(myModel, input_to_model=myTrainDataLoader[1], verbose=False)
# writer.add_graph(myModel)
train_loss_his = []
train_totalaccuracy_his = []
test_totalloss_his = []
test_totalaccuracy_his = []
start_time = time.time()
my_model.train()
for i in range(epoch):print(f"-------第{i}轮训练开始-------")train_total_accuracy = 0for step, batch_data in enumerate(train_data_loader):# writer.add_images("tarin_data", imgs, total_train_step)print(batch_data['input_ids'].shape)output = my_model(**batch_data)loss = loss_fn(output, batch_data['label_id'])train_accuracy = (output.argmax(1) == batch_data['label_id']).sum()train_total_accuracy = train_total_accuracy + train_accuracyoptimizer.zero_grad()loss.backward()optimizer.step()total_train_step = total_train_step + 1train_loss_his.append(loss)writer.add_scalar("train_loss", loss.item(), total_train_step)train_total_accuracy = train_total_accuracy / train_data_lenprint(f"训练集上的准确率:{train_total_accuracy}")train_totalaccuracy_his.append(train_total_accuracy)# 测试开始total_test_loss = 0my_model.eval()test_total_accuracy = 0with torch.no_grad():for batch_data in test_data_loader:output = my_model(**batch_data)loss = loss_fn(output, batch_data['label_id'])total_test_loss = total_test_loss + losstest_accuracy = (output.argmax(1) == batch_data['label_id']).sum()test_total_accuracy = test_total_accuracy + test_accuracytest_total_accuracy = test_total_accuracy / test_data_lenprint(f"测试集上的准确率:{test_total_accuracy}")print(f"测试集上的loss:{total_test_loss}")test_totalloss_his.append(total_test_loss)test_totalaccuracy_his.append(test_total_accuracy)writer.add_scalar("test_loss", total_test_loss.item(), i)
# for parameters in myModel.parameters():
# print(parameters)
end_time = time.time()
total_train_time = end_time-start_time
print(f'训练时间: {total_train_time}秒')
writer.close()
plt.plot(train_loss_his, label='Train Loss')
plt.legend(loc='best')
plt.xlabel('Steps')
plt.show()
plt.plot(test_totalloss_his, label='Test Loss')
plt.legend(loc='best')
plt.xlabel('Steps')
plt.show()plt.plot(train_totalaccuracy_his, label='Train accuracy')
plt.plot(test_totalaccuracy_his, label='Test accuracy')
plt.legend(loc='best')
plt.xlabel('Steps')
plt.show()
尽管只用了150个数据,但是训练效果还是不错的,训练准确度达到了90%以上,测试准确度在85%以上。
我的实践:pytorch框架下基于BERT实现文本情感分类相关推荐
- 复盘:基于attention的多任务多模态情绪情感识别,基于BERT实现文本情感分类(pytorch实战)
复盘:基于attention机制的多任务多模态情绪情感识别(pytorch实战),基于BERT实现文本情感分类 提示:系列被面试官问的问题,我自己当时不会,所以下来自己复盘一下,认真学习和总结,以应对 ...
- 基于Bert的文本情感分类
详细代码已上传到github: click me 摘 要: 情感分类是对带有感情色彩的主观性文本进行分析.推理的过程,即分析说话人的态度,推断其所包含的情感类别.传统机器学习在处理情感分类问 ...
- NLP之基于TextCNN的文本情感分类
TextCNN 文章目录 TextCNN 1.理论 1.1 基础概念 **最大汇聚(池化)层:** ![请添加图片描述](https://img-blog.csdnimg.cn/10e6e1ed6bf ...
- bert中文文本情感分类 微博评论挖掘之Bert实战应用案例-文本情感分类
Bert模型全称Bidirectional Encoder Representations from Transformers,主要分为两个部分:1训练语言模型(language model)的预训练 ...
- 基于LSTM搭建文本情感分类的深度学习模型:准确率95%
向AI转型的程序员都关注了这个号
- 【Kesci】【预选赛】2019中国高校计算机大赛——大数据挑战赛(基于FastText的文本情感分类)
比赛链接:https://www.kesci.com/home/competition/5cb80fd312c371002b12355f 预选赛题--文本情感分类模型 本预选赛要求选手建立文本情感分类 ...
- BERT实战(1):使用DistilBERT作为词嵌入进行文本情感分类,与其它词向量(FastText,Word2vec,Glove)进行对比
这次根据一篇教程Jay Alammar: A Visual Guide to Using BERT for the First Time学习下如何在Pytorch框架下使用BERT. 主要参考了中文翻 ...
- 基于Transformer的文本情感分析编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制 + Positional Encoding位置编码)
日萌社 人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新) Encoder编码器-Decoder解码器框架 + Atten ...
- PyTorch环境下对BERT进行Fine-tuning
PyTorch环境下对BERT进行Fine-tuning 本文根据Chris McCormick的BERT微调教程进行优化并使其适应于数据集Quora Question Pairs里的判断问题对是否一 ...
最新文章
- 对比学习系列论文CPCforHAR(一):Contrastive Predictive Coding for Human Activity Recognition
- nyoj 791 Color the fence(贪心)
- Photon——Setup and Config 设置与配置
- 从C10K到C10M高性能网络的探索与实践
- 【Java类加载机制】深入类加载器(二)自定义加密、解密类加载器
- PHP易混淆函数的区分方法及意义
- 在local模式下的spark程序打包到集群上运行
- .NET Core中的验证组件FluentValidation的实战分享
- 分数诚可贵的飞鸽传书2012绿色版
- python mysql异常处理_python-处理PyMySql异常-最佳做法
- oracle11g分区表维护,Oracle11g维护分区(一)AddingPartitions
- python 菜鸟-python菜鸟教程
- win10系统计算机物理地址,Win10如何修改物理地址?Win10修改网卡物理地址(MAC)的两种方法...
- 完美解决苹果电脑mac终端无法输入大写T的问题
- 一些特殊字符(如:←↑→↓等箭头符号)的Unicode码值
- mysql分析问卷_问卷调查相关表
- MySQL主主从复制+TomCat高可用实践案例
- 当学术沾染名利,约翰伯努利对儿子的嫉恨,影响数学界几十年发展
- 网络营销中的动态定价策略
- 一步步教你搭建Android开发环境(有图有真相)--“自吹自擂:史上最详细、最啰嗦、最新的搭建教程”
热门文章
- 宅家36天咸鱼翻身入职腾讯,好文推荐
- [Network licensing error: license fill“e:\maxplus2\license\license.dat“is missing or corrupted]解决方法
- Unity人工智能编程精粹学习笔记 实现AI角色的自主移动——操控行为
- LAMP项目部署实战1
- 什么是 :kail LINUX
- js中国标准时间转化为年月日,时间戳
- 二、搜索蓝牙并连接(安卓蓝牙ble教程)
- tar解压包的时候出现错误 gzip: stdin: not in gzip format
- session会话中removeAttribute()和invalidate()的区别是什么
- LB集群——LVS负载均衡介绍(DR模式、TUN模式、NAT模式)