·请参考本系列目录:​​【BERT-多标签文本分类实战】之一——实战项目总览​​·下载本实战项目资源:>=点击此处=<

  前5篇文章中,介绍了实战项目的前置知识,下面正式介绍项目的代码。本项目主要分为6部分:

【BERT-多标签文本分类实战】之六——数据加载与模型代码_多标签文本分类


  1、​​bert-base-uncased​​​:​​bert​​的预训练文件;

  2、​​model​​​:存放​​bert​​模型代码;

  3、​​Reuters-21578​​:存放数据集;

  4、​​run.py​​:项目运行主程序;

  5、​​utils.py​​:处理数据集并且预加载;

  6、​​train_eval.py​​:模型训练、验证、测试代码。

  本篇介绍:5、utils.py:处理数据集并且预加载2、model:存放bert模型代码

[1] 数据集文件的构成

  实战项目中数据集文件共6个:

【BERT-多标签文本分类实战】之六——数据加载与模型代码_数据集_02


  其中,​​reutersNLTK.xlsx​​​是原数据集文件,训练集​​train.csv​​​、验证集​​dev.csv​​​、测试集​​test.csv​​​是之前拆分好的,​​class.txt​​​是标签目录,​​label.pkl​​是压缩存储的标签,方便快速读取用的。

[2] 加载数据集

  加载数据集的目标是:1)把文本数据转化成​​BERT​​​模型的词序、Mask 码,为输入进​​BERT​​作准备;2)把文本标签转化成独热数组。

def build_dataset(config):
# ## 读取标签
label_list = pkl.load(open(config.label_path, 'rb'))
print(f"标签个数======== {len(label_list)}")

def convert_to_one_hot(Y, C):
list = [[0 for i in C] for j in Y]

for i, a in enumerate(Y):
for b in a:
if b in C:
list[i][C.index(b)] = 1
else:
list[i][len(C) - 1] = 1
return list

def load_dataset(path, pad_size=32):
df = pd.read_csv(path, encoding='utf-8', sep=',')
data = df['content']

sentences = data.values

labels = []
# 把标签读成数组
for ls in df['label']:
labels.append(re.compile(r"'(.*?)'").findall(ls))
# 把数组转成独热
labels_id = convert_to_one_hot(labels, label_list)
contents = []
count = 0

for i, content in tqdm(enumerate(sentences)):
label = labels_id[i]
encoded_dict = config.tokenizer.encode_plus(
content, # 输入文本
add_special_tokens=True, # 添加 '[CLS]' 和 '[SEP]'
max_length=pad_size, # 填充 & 截断长度
pad_to_max_length=True,
padding='max_length',
truncation='only_first',
return_attention_mask=True, # 返回 attn. masks.
return_tensors='pt' # 返回 pytorch tensors 格式的数据
)
token = config.tokenizer.tokenize(content)
seq_len = len(token)
count += seq_len
contents.append((torch.squeeze(encoded_dict['input_ids'],0), label, seq_len, torch.squeeze(encoded_dict['attention_mask'],0)))
print(f"数据集地址========{path}")
print(f"数据集总词数========{count}")
print(f"数据集文本数========{len(sentences)}")
print(f"数据集文本平均词数========{count / len(sentences)}")
return contents
train = load_dataset(config.train_path, config.pad_size)
dev = load_dataset(config.dev_path, config.pad_size)
test = load_dataset(config.test_path, config.pad_size)
return train, dev, test

  代码如上。

  首先,加载​​label.pkl​​​文件。对于每一条文本,先提取它的标签,然后转化成独热数组。接下来通过​​tokenizer.encode_plus​​​编码文本,得到​​input_ids​​​与​​attention_mask​​​。最后把这些数据都存到数组​​contents​​中。

[3] 数据集加载器

  在第二节中,只是把显式的文本数据,转化成了数字化的​​Tensor​​格式。如何控制一个batch中有多少文本?如何控制数据的随机性等等?

  这就需要数据集加载器

class DatasetIterater(object):
def __init__(self, batches, batch_size, device):
self.batch_size = batch_size
self.batches = batches
self.n_batches = len(batches) // batch_size
self.residue = False # 记录batch数量是否为整数
if len(batches) % self.n_batches != 0:
self.residue = True
self.index = 0
self.device = device

def _to_tensor(self, datas):
x = torch.LongTensor([_[0].detach().numpy() for _ in datas]).to(self.device)
y = torch.LongTensor([_[1] for _ in datas]).to(self.device)

# pad前的长度(超过pad_size的设为pad_size)
seq_len = torch.LongTensor([_[2] for _ in datas]).to(self.device)
mask = torch.LongTensor([_[3].detach().numpy() for _ in datas]).to(self.device)
return (x, seq_len, mask), y

def __next__(self):
if self.residue and self.index == self.n_batches:
batches = self.batches[self.index * self.batch_size: len(self.batches)]
self.index += 1
batches = self._to_tensor(batches)
return batches

elif self.index >= self.n_batches:
self.index = 0
raise StopIteration
else:
batches = self.batches[self.index * self.batch_size: (self.index + 1) * self.batch_size]
self.index += 1
batches = self._to_tensor(batches)
return batches

def __iter__(self):
return self

def __len__(self):
if self.residue:
return self.n_batches + 1
else:
return self.n_batches


def build_iterator(dataset, config):
iter = DatasetIterater(dataset, config.batch_size, config.device)
return iter

  这个完全是自定义的数据加载器,直接用就可以,不展开介绍。

到这里,数据加载的部分就结束了。我们需要在数字化数据外套一个数据加载器的原因是,回头在调​​epoch​​​、​​batch_size​​这些参数的时候,数据加载器能够自动帮我们分配好这些文本数据。

[4] BERT模型代码

  ​​BERT​​​模型代码分为两个文件,一个是​​BaseConfig.py​​​保存通用配置,一个是​​bert.py​​保存实际代码。

  ​​BaseConfig.py​

class BaseConfig(object):

"""配置参数"""
def __init__(self, dataset):
self.train_path = dataset + '/data/train.csv' # 训练集
self.dev_path = dataset + '/data/dev.csv' # 验证集
self.test_path = dataset + '/data/test.csv' # 测试集
self.label_path = dataset + '/data/label.pkl' # 标签集
self.vocab_path = dataset + '/data/vocab.pkl' # 词表
self.class_list = [x.strip() for x in open(
dataset + '/data/class.txt', encoding='utf-8').readlines()] # 类别名单
self.num_classes = len(self.class_list) # 类别数
self.n_vocab = 0 # 词表大小,在运行时赋值
""""""
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
""""""
pretrained_path = './bert-base-uncased'
self.bert = BertModel.from_pretrained(pretrained_path)
self.tokenizer = BertTokenizer.from_pretrained(pretrained_path)
""""""
self.require_improvement = 1000 # 若超过1000batch效果还没提升,则提前结束训练
self.num_epochs = 100 # epoch数
self.batch_size = 32 # mini-batch大小
self.pad_size = 150 # 每句话处理成的长度(短填长切)
self.learning_rate = 5e-3 # 学习率
self.embed = 768

  ​​bert.py​

class Config(BaseConfig):
"""配置参数"""
def __init__(self, dataset):
BaseConfig.__init__(self, dataset)
self.model_name = 'bert'
self.save_path = dataset + '/saved_dict/' + self.model_name + '.ckpt' # 模型训练结果
self.log_path = dataset + '/log/' + self.model_name


class Model(nn.Module):
def __init__(self, config):
super(Model, self).__init__()

self.bert = config.bert
for param in self.bert.parameters():
param.requires_grad = False
self.fc = nn.Linear(config.embed, config.num_classes)

def forward(self, x):
context = x[0] # 输入的句子
mask = x[2] # 对padding部分进行mask,和句子一个size,padding部分用0表示,如:[1, 1, 1, 1, 0, 0]
_ = self.bert(context, attention_mask=mask)
out = self.fc(_[1]) # [batch_size, hidden_size * 2] = [128, 256]
return out

  这里就是定义了一个​​bert​​​模型,和一个全连接层。把​​bert​​​的输出放到​​fc​​中做分类。很朴素但是很吊。。。。。效果是真的甩开CNN、RNN系模型一截。

【注意】最终的输出,理解为概率!!

[5] 进行下一篇实战

  ​​【BERT-多标签文本分类实战】之七——训练-评估-测试与运行主程序​​