关系分类抽取模型项目的任务是给定句子和句子中的两个实体,判断这两个实体之间的关系,项目文件中带有质量较高的数据集。项目的思路大致是将关系抽取转化成对两个实体的关系进行分类。
大致可以分为三块:model,也就是要定义我们模型各层的结构;dataset,把要训练的数据集改写成model需要的格式;train,定义训练、验证和测试的参数和过程。dataset是由model的输入层和输出层决定的,所以只要我们自己明确了model的输入和输出,dataset通过一些Python基础很容易就实现了。
torch
transformers==4.0.1
tqdm
sklearn
BERT主要分为两个部分。一个是训练语言模型(language model)的预训练(run_pretraining.py)部分。另一个是训练具体任务(task)的fine-tune部分,预训练部分巨大的运算资源。
data_utils.py部分
定义MyTokenizer类,实现对中文关系的切分,def __init__(self, pretrained_model_path=None, mask_entity=False):
进行初始化,加载相应的参数
class MyTokenizer(object):
def __init__(self, pretrained_model_path=None, mask_entity=False):
self.pretrained_model_path = pretrained_model_path or 'bert-base-chinese'
self.bert_tokenizer = BertTokenizer.from_pretrained(self.pretrained_model_path)
self.mask_entity = mask_entity
def tokenize(self, item):
定义read_data函数实现从文件中读取数据
def read_data(input_file, tokenizer=None, max_len=128):
tokens_list = []
e1_mask_list = []
e2_mask_list = []
tags = []
with open(input_file, 'r', encoding='utf-8') as f_in:
for line in tqdm(f_in):
line = line.strip()
item = json.loads(line)
if tokenizer is None:
tokenizer = MyTokenizer()
tokens, pos_e1, pos_e2 = tokenizer.tokenize(item)
if pos_e1[0] < max_len - 1 and pos_e1[1] < max_len and \
pos_e2[0] < max_len - 1 and pos_e2[1] < max_len:
tokens_list.append(tokens)
e1_mask = convert_pos_to_mask(pos_e1, max_len)
e2_mask = convert_pos_to_mask(pos_e2, max_len)
e1_mask_list.append(e1_mask)
e2_mask_list.append(e2_mask)
tag = item['relation']
tags.append(tag)
return tokens_list, e1_mask_list, e2_mask_list, tags
列表长度实现
def __len__(self):
return len(self.tags)
索引index实现 def __getitem__(self, idx):
class SentenceREDataset(Dataset):
def __init__(self, data_file_path, tagset_path, pretrained_model_path=None, max_len=128):
def __getitem__(self, idx):
if torch.is_tensor(idx):
idx = idx.tolist()
sample_tokens = self.tokens_list[idx]
sample_e1_mask = self.e1_mask_list[idx]
sample_e2_mask = self.e2_mask_list[idx]
sample_tag = self.tags[idx]
encoded = self.tokenizer.bert_tokenizer.encode_plus(sample_tokens, max_length=self.max_len, pad_to_max_length=True)
sample_token_ids = encoded['input_ids']
sample_token_type_ids = encoded['token_type_ids']
sample_attention_mask = encoded['attention_mask']
sample_tag_id = self.tag2idx[sample_tag]
sample = {
'token_ids': torch.tensor(sample_token_ids),
'token_type_ids': torch.tensor(sample_token_type_ids),
'attention_mask': torch.tensor(sample_attention_mask),
'e1_mask': torch.tensor(sample_e1_mask),
'e2_mask': torch.tensor(sample_e2_mask),
'tag_id': torch.tensor(sample_tag_id)
}
return sample
导入BertTokenizer和BertModel模型from transformers import BertTokenizer, BertModel
首先明确model的输入输出,前面已经介绍了本项目的任务是对给定的句子和句子中的两个实体判断关系,所以输入就是句子、实体1和实体2,输出就是关系编号。然后看forward函数的输入输出,forward函数的参数包括以下5个:
1、token_ids:这个比较常见,标记着句子中每个字在词表中的位置。
2、token_type_ids:区分两个句子的编码,但是在本次项目中只输入一个句子。
3、attention_mask:标记哪些位置要进行self-attention操作,其他位置都是pad。
4、e1_mask:标记第一个实体的位置。
5、e2_mask:标记第二个实体的位置。
输出的是模型对两个实体的关系预测结果。
模型的中间层实际上体现在forward函数中,上面的初始化函数对各层的定义顺序没有要求,所以看forward函数才能真正掌握模型的数据流动过程。
sequence_output, pooled_output = self.bert_model(input_ids=token_ids, token_type_ids=token_type_ids, attention_mask=attention_mask, return_dict=False)
①、第一步对e_mask向量进行升维,从(batch_size,seq_len)变为(batch_size,1,seq_len),这步的作用是对齐e_mask和hidden_output的维度,使得后面二者可以相乘。
e_mask_unsqueeze = e_mask.unsqueeze(1)
②、第二步求出每个实体的字数,使用的sum函数可以统计向量中值为1的元素数量,然后再进行升维。
length_tensor = (e_mask != 0).sum(dim=1).unsqueeze(1)
③、第三步要计算实体所有字向量的平均值,用e_mask和hidden_output相乘,其中e_mask为0的位置,与非实体位置的字向量相乘,e_mask为1的位置与实体位置的每个字向量相乘,这样就实现了实体每个字的字向量相加。这里使用了bmm批量矩阵乘法函数,二者的维度分别为(batch_size,1,seq_l)和(batch_size,seq_len,embedding_dim),在理解bmm函数时可以先不考虑batch_size维度,因为三维矩阵可以看做是多个二维矩阵的结合,bmm函数每次分别取两个二维矩阵相乘,然后将所有结果组合成一个三维矩阵。也就是说:
[batch_size,1,max_len] × [batch_size,max_len,dim] 可以看做是
batch_size ×([1,max_len] × [max_len,dim])= batch_size × [1,dim] = [batch_size,1,dim]
然后再将中间那个维度去掉,得到最后结果(batch_size,embedd_dim)。
sum_vector = torch.bmm(e_mask_unsqueeze.float(), hidden_output).squeeze(1)
④、最后将相加后的向量除以实体长度,这样就实现了实体字向量的求均值。在这里就可以看出第二步对字数升维的作用,是为了对齐维度。
avg_vector = sum_vector.float() / length_tensor.float()
concat_h = torch.cat([pooled_output, e1_h, e2_h], dim=1)
concat_h = self.norm(concat_h)
logits = self.hidden2tag(self.drop(concat_h))