• 【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-多标签文本分类实战】之七——训练-评估-测试与运行主程序

  • 相关阅读:
    torch
    Android实战——一步一步实现流动的炫彩边框
    防御---003
    vue父子组件实现表单双向绑定
    Git 同步远程新的同名分支
    GESP C++ 三级真题(2023年9月)T1 ⼩ 杨储蓄
    到底是哪里出了问题_蓝桥杯2023子矩阵
    1454. 活跃用户
    Java注释规范简介说明
    SpringBoot+Vue项目医院挂号系统的设计与实现
  • 原文地址:https://blog.csdn.net/qq_43592352/article/details/127087204