#手写代码# 用Bert+CNN解决文本分类问题

NLP 同时被 3 个专栏收录
23 篇文章 0 订阅
72 篇文章 0 订阅
31 篇文章 0 订阅

1 配置文件

首先定义一个配置文件类,类里边存放Bert和CNN的一些超参数

class Config(object):
    '''
    配置参数
    '''
    def __init__(self,dataset):

        # 模型名称
        self.model_name='Bert CNN Model'
        # 训练集,测试集,检验集,类别,模型训练结果保存路径
        # self.train_path=dataset+'/data/dev.txt'
        # self.test_path=dataset+'/data/dev.txt'
        # self.dev_path=dataset+'/data/dev.txt'

        #数据集路径
        self.train_path=dataset+'/data/train.txt'
        self.test_path=dataset+'/data/test.txt'
        self.dev_path=dataset+'/data/dev.txt'

        # python 数据结构保存路径
        self.datasetpkl=dataset+'/data/dataset.pkl'
        # 类别路径
        self.class_list=[x.strip() for x in open(dataset+'/data/class.txt').readlines()]
        self.save_path=dataset+'/saved_dict/'+self.model_name+'.ckpt'

        # 配置使用检测GPU
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        # 若超过1000还没有提升就提前结束训练
        self.require_improvement=1000
        # 类别数
        self.num_classes = len(self.class_list)

        # 整体训练次数
        self.num_epoch=3
        # batch大小
        self.batch_size=128
        #每个序列最大token数
        self.pad_size=32
        #学习率
        self.learning_rate = 1e-5

        self.bert_path='bert_pretrain'

        # bert 分词器
        self.tokenizer=BertTokenizer.from_pretrained(self.bert_path) #定义分词器
        self.hidden_size=768  # Bert模型 token的embedding维度 = Bert模型后接自定义分类器(单隐层全连接网络)的输入维度

        # 每个n-gram的卷积核数量
        self.num_filters=256

        # 卷积核在序列维度上的尺寸 = n-gram大小 卷积核总数量=filter_size*num_filters
        self.filter_size=(2,3,4)

        self.dropout=0.1

在这个配置文件中,分别定义了一下内容:

  1. 测试集,训练集,开发集的原始数据存放路径
  2. 测试集,训练集,开发集转化成python内的数据结构后的存放路径
  3. 类别列表
  4. 模型训练使用的硬件(CPU还是GPU)
  5. 损失函数超过多少次没有提升提前结束训练
  6. epoch数(整个数据集循环训练多少轮)
  7. batch_size
  8. 序列最大token数
  9. 学习率
  10. 模型的保存路径(本质上是保存的模型参数)
  11. 分词器
  12. Bert模型的隐含层节点数(经过Bert模型后,词向量的维度)
  13. 每组n-gram的卷积核数量
  14. 每组卷积核在序列维度上的尺寸
  15. dropout比例

2 定义模型

我们自定义的模型要继承自 nn.Module

class Model(nn.Module):
    def __init__(self,config):
        super(Model,self).__init__()
        self.bert=BertModel.from_pretrained(config.bert_path)  #从路径加载预训练模型
        for param in self.bert.parameters():
            param.requires_grad = True # 使参数可更新
        self.convs=nn.ModuleList(
            # 输入通道数,输出通道数(卷积核数),卷积核维度
            [nn.Conv2d(1,config.num_filters,(k,config.hidden_size)) for k in config.filter_size]    #(k,config.hidden_size)  n-gram,embedding维度
        )
        self.dropout=nn.Dropout(config.dropout)
        self.fc=nn.Linear(config.num_filters*len(config.filter_size),config.num_classes ) #输入的最后一个维度,输出的最后一个维度 全连接层只改变数据的最后一个维度 由输入最后的一个维度转化为 类别数

    def conv_and_pool(self,x,conv):
        x=conv(x)   #[batch_size,channel_num,pad_size,embedding_size(1)]
        x=F.relu(x)
        x=x.squeeze(3) #[batch_size,channel_num,pad_size]
        x=F.max_pool1d(x,x.size(2)) #经过卷积之后,x
        x = x.squeeze(2)  # [batch_size,channel_num]
        return x

    def forward(self,x):
        context=x[0] #128*32 batch_size*seq_length
        mask=x[2]   #128*32 batch_size*seq_length

        # 第一个参数 是所有输入对应的输出  第二个参数 是 cls最后接的分类层的输出
        encoder_out,pooled = self.bert(context,attention_mask=mask,output_all_encoded_layers=False) # output_all_encoded_layers 是否将bert中每层(12层)的都输出,false只输出最后一层【128*768】
        out = encoder_out.unsqueeze(1)  #增加一个维度,[batch_size,channel_num,pad_size,embedding_num]  ->  [batch_size,channel_num,pad_size,embedding_num]
        out = torch.cat([self.conv_and_pool(out,conv) for conv in self.convs],1)
        out=self.fc(out) # 128*10
        return out

2.1 init(self,config)函数

init(self,config)函数中主要进行了如下操作:

  1. 加载预训练bert模型
  2. 将Bert模型中的参数设置为可更新
  3. 定义卷积核,具体过程请参考下文
  4. 定义dropout层
  5. 定义全连接网络层

使用 nn.Conv2d() 函数定义卷积核,nn.Conv2d() 函数的主要参数如下:

参数名称作用
in_channels输入数据的通道数(这里可以理解为使用了多少 word embedding方法)
out_channels输出通道数,表示使用了多少个卷积核
kernel_size卷积核尺寸,[n-gram大小,word_embedding大小(bert隐含层节点数)]

自定义卷积核时,每种n-gram对应一组卷积核(每组卷积核数量相同),上述定义卷积核的代码中,首先遍历每个配置类中的 filter_size,得到每组局卷积核在0维(序列维度上的长度),然后将bert隐含层节点数作为每组局卷积核1维上的长度,从而定义每组卷积核的尺寸(每组卷积核内的卷积核尺寸相同)

2.1 conv_and_pool()函数

conv_and_pool()函数内主要执行了以下过程:

  1. 在输入数据的序列维度上进行滑动,从而进行一维卷积
  2. 通过relu激活函数
  3. 删除embedding_size维度,在embedding_size维度上,卷积核尺寸=embedding_size尺寸,因此embedding_size为1,可以删除
  4. 在数据的序列维度上进行一维最大池化(将一个序列中所有token合并,形成句向量)
  5. 经过上一步的操作后,数据在序列上的维度变为1,因此删除此序列维度
  6. 返回卷积后的二维数据,两个维度分别为 [batch_size,channel_num]

关于PyTorch中一维卷积和二维卷积的区别请参考此文章:深入理解 PyTorch 的一维卷积和二维卷积,一维池化和二维池化

2.3 forward(self,x)函数

forward(self,x)函数是Bert中一个特殊文章函数,forward(self,x)函数详细解析请看此文章

这里输入数据的结构为 [输入的token序列,序列真实长度,mask序列],输入数据的格式和数据预处理部分相关,在上一篇文章中已经讲解过数据预处理的代码,这里不做赘述

def forward(self,x):
    context=x[0] #128*32 batch_size*seq_length
    mask=x[2]   #128*32 batch_size*seq_length

    # 第一个参数 是所有输入对应的输出  第二个参数 是 cls最后接的分类层的输出
    encoder_out,pooled = self.bert(context,attention_mask=mask,output_all_encoded_layers=False) # output_all_encoded_layers 是否将bert中每层(12层)的都输出,false只输出最后一层【128*768】
    out = encoder_out.unsqueeze(1)  #增加一个维度,[batch_size,channel_num,pad_size,embedding_num]  ->  [batch_size,channel_num,pad_size,embedding_num]
    out = torch.cat([self.conv_and_pool(out,conv) for conv in self.convs],1)
    out=self.fc(out) # 128*10 通道数=n-gram数*每种n-gram内的卷积数
    return out

forward(self,x)函数函数内主要执行了以下过程:

  1. 输入数据并得到预训练Bert模型的每个token对应的输出
  2. 增加一个维度,用于存储通道数(每个卷积核的输出对应一个通道)
  3. 遍历所有卷积核并依次对数据进行卷积,将不同卷积核得到的数据存储的不同通道中
  4. 将卷积后的数据放入全连接网络并返回输出数据 [batch_size,分类数]

关于bert模块两个返回值的深度解析请参考此文章 ->从源码层面,深入理解 Bert 框架

  • 1
    点赞
  • 3
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值