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
在这个配置文件中,分别定义了一下内容:
- 测试集,训练集,开发集的原始数据存放路径
- 测试集,训练集,开发集转化成python内的数据结构后的存放路径
- 类别列表
- 模型训练使用的硬件(CPU还是GPU)
- 损失函数超过多少次没有提升提前结束训练
- epoch数(整个数据集循环训练多少轮)
- batch_size
- 序列最大token数
- 学习率
- 模型的保存路径(本质上是保存的模型参数)
- 分词器
- Bert模型的隐含层节点数(经过Bert模型后,词向量的维度)
- 每组n-gram的卷积核数量
- 每组卷积核在序列维度上的尺寸
- 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)函数中主要进行了如下操作:
- 加载预训练bert模型
- 将Bert模型中的参数设置为可更新
- 定义卷积核,具体过程请参考下文
- 定义dropout层
- 定义全连接网络层
使用 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()函数内主要执行了以下过程:
- 在输入数据的序列维度上进行滑动,从而进行一维卷积
- 通过relu激活函数
- 删除embedding_size维度,在embedding_size维度上,卷积核尺寸=embedding_size尺寸,因此embedding_size为1,可以删除
- 在数据的序列维度上进行一维最大池化(将一个序列中所有token合并,形成句向量)
- 经过上一步的操作后,数据在序列上的维度变为1,因此删除此序列维度
- 返回卷积后的二维数据,两个维度分别为 [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)函数函数内主要执行了以下过程:
- 输入数据并得到预训练Bert模型的每个token对应的输出
- 增加一个维度,用于存储通道数(每个卷积核的输出对应一个通道)
- 遍历所有卷积核并依次对数据进行卷积,将不同卷积核得到的数据存储的不同通道中
- 将卷积后的数据放入全连接网络并返回输出数据 [batch_size,分类数]
关于bert模块两个返回值的深度解析请参考此文章 ->从源码层面,深入理解 Bert 框架