·请参考本系列目录:【BERT-多标签文本分类实战】之一——实战项目总览
·下载本实战项目资源:>=点击此处=<
前5篇文章中,介绍了实战项目的前置知识,下面正式介绍项目的代码。本项目主要分为6部分:
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个:
其中,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系模型一截。
【注意】最终的输出,理解为概率!!