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

Posted 征途黯然.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BERT-多标签文本分类实战之六——数据加载与模型代码相关的知识,希望对你有一定的参考价值。

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

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

  1、bert-base-uncasedbert的预训练文件;
  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_idsattention_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

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

到这里,数据加载的部分就结束了。我们需要在数字化数据外套一个数据加载器的原因是,回头在调epochbatch_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-多标签文本分类实战】之七——训练-评估-测试与运行主程序

以上是关于BERT-多标签文本分类实战之六——数据加载与模型代码的主要内容,如果未能解决你的问题,请参考以下文章

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

BERT-多标签文本分类实战之一——实战项目总览

BERT-多标签文本分类实战之一——实战项目总览

BERT-多标签文本分类实战之一——实战项目总览

BERT-多标签文本分类实战之五——BERT模型库的挑选与Transformers

BERT-多标签文本分类实战之五——BERT模型库的挑选与Transformers