先介绍原作者的实验细节
为了比较不同模型的结果,使用Adam对所有基线进行了优化,学习率为0.001,$\beta_1=0.9$,$\beta_1=0.999$。实验中使用的其他实验超参数如下图所示。另外,由于实体之间的文档级距离,距离首先被划分为几个小单元{1,2,…,$2^k$},其中每个单元与可训练的距离嵌入(trainable distance embedding)相关联。
Types of Named Entities
在本文中,我们修改了Tjong Kim Sang and De Meulder(2003)中使用的现有命名实体类型,以更好地为DocRED服务。这些类型包括“Person(PER)”、“Organization(ORG)”、“Location(LOC)”、“Time(TIME)”、“Number(NUM)”和“other types(MISC)”。DocRED中命名实体的类型及其涵盖的内容如下图所示。
List of Relations
我们在DocRED中提供了关系列表,包括Wikidata IDs、relation names和Wikidata中的描述,如表10和表11所示
首先我们从官网下载得到数据集与代码原始的data包含五个文件
训练集:train_annotated.json(Supervised) train_distant.json(Weakly Supervised)
验证集:dev.json
测试集:test.json
关系字典:rel_info.json
Data Format: { 'title', 'sents': [ [word in sent 0], [word in sent 1] ] 'vertexSet': [ [ { 'name': mention_name, 'sent_id': mention in which sentence, 'pos': postion of mention in a sentence, 'type': NER_type} {anthor mention} ], [anthoer entity] ] 'labels': [ { 'h': idx of head entity in vertexSet, 't': idx of tail entity in vertexSet, 'r': relation, 'evidence': evidence sentences' id } ] }
接下来打开json文件看下不同文件之间数据格式的差别
首先是人工标注的训练数据集train_annotated.json
可以看到每一篇文章被标注为这样的四部分,顶点集(entity set),标签集,主题title,句子
1) 先介绍vertexSet顶点集,可以看到这篇文章有17个entity
还可以看到第一个entity节点的4个mentions在文章中哪句话以及在对应的句子中的相对位置在哪,如下图所示
2)标签集,首先我们可以知道这篇文章拥有13个标签(13个关系)
Evidence:是论文提到的人工标注的Supporting Evidence
3) title项
4) sent项,可知这篇文章有8个句子,每个句子长度如下图所示
接下来介绍弱监督训练集,总体和有监督一样,区别在于label项,没有evidence
验证集与人工标注的训练数据集train_annotated.json格式相同,论文中也介绍过。
测试集没有label项需要我们拿来作预测,其他项同人工标注训练数据集:
Download metadata from TsinghuaCloud or GoogleDrive for baseline method and put them into prepro_data folder
分别有6个文件:
char_vec.npy Glove预训练好的字符向量文件
vec.npy Glove预训好的单词向量文件
char2id.json 自定义BLANK(空),UNK(未识别),常见字母,生僻希腊字母等字符对应id
ner2id.json {"BLANK": 0, "ORG": 1, "LOC": 2, "TIME": 3, "PER": 4, "MISC": 5, "NUM": 6}
rel2id.json 一个关系转换为id的字典"P1412": 38, "P206": 33, "P205": 77
word2id.json 文章中所有可能出现的所有字符单词标点符号对应的id
数据准备完毕,开始preprocessing data ,运行gen_data.py
创建解析器,添加路径参数
1 parser = argparse.ArgumentParser() 2 parser.add_argument('--in_path', type = str, default = "data") 3 parser.add_argument('--out_path', type = str, default = "prepro_data") 4 args = parser.parse_args() 5 in_path = args.in_path 6 out_path = args.out_path
从data文件夹中获取对应预处理数据的文件名
1 train_distant_file_name = os.path.join(in_path, 'train_distant.json') 2 train_annotated_file_name = os.path.join(in_path, 'train_annotated.json') 3 dev_file_name = os.path.join(in_path, 'dev.json') 4 test_file_name = os.path.join(in_path, 'test.json')
通过rel2id文件获得id2rel文件
1 rel2id = json.load(open(os.path.join(out_path, 'rel2id.json'), "r")) 2 id2rel = {v:u for u,v in rel2id.items()} 3 json.dump(id2rel, open(os.path.join(out_path, 'id2rel.json'), "w"))
调用init()函数生成处理后的文件
1 init(train_distant_file_name, rel2id, max_length = 512, is_training = True, suffix='') 2 init(train_annotated_file_name, rel2id, max_length = 512, is_training = False, suffix='_train') 3 init(dev_file_name, rel2id, max_length = 512, is_training = False, suffix='_dev') 4 init(test_file_name, rel2id, max_length = 512, is_training = False, suffix='_test')
可以看到弱监督训练集(也叫做远程监督数据集)设置is_training=True,suffix=’’
下面来介绍init()函数
加载要处理的文件
1 ori_data = json.load(open(data_file_name))
len(ori_data)是文档数目,这个for循环以文档为单位进行循环
1 for i in range(len(ori_data)):(1)
以下为循环(1)的内部
1 Ls = [0] 2 L = 0 3 for x in ori_data[i]['sents']: 4 L += len(x) 5 Ls.append(L)
Ls记录第i篇文档每句话的起始绝对位置
获取第i篇文档的顶点集
1 vertexSet = ori_data[i]['vertexSet']
1 # point position added with sent start position 2 for j in range(len(vertexSet)): 3 for k in range(len(vertexSet[j])): 4 vertexSet[j][k]['sent_id'] = int(vertexSet[j][k]['sent_id']) 5 6 sent_id = vertexSet[j][k]['sent_id'] 7 dl = Ls[sent_id] 8 pos1 = vertexSet[j][k]['pos'][0] 9 pos2 = vertexSet[j][k]['pos'][1] 10 vertexSet[j][k]['pos'] = (pos1+dl, pos2+dl)
因为一个entity(实体节点)可能有很多mentions,所以要用双重循环遍历到每一个mentions,获得每一个mention他在文档中属于第几句话,通过Ls记录的每句话的起始绝对位置与相对位置相加,得到该mention在文档中的绝对位置并返回给pos标签处
处理过的新vertexSet覆盖掉原来的老顶点集vertexSet
1 ori_data[i]['vertexSet'] = vertexSet
获取要处理文档的labels项,有就返回json字典labels项对应的内容,否则返回一个[]。(因为我们处理的test.json需要进行预测,无labels项)
1 labels = ori_data[i].get('labels', [])
1 fact_in_train = set([]) 2 fact_in_dev_train = set([]) 3 train_triple = set([]) 4 new_labels = [] 5 for label in labels: 6 rel = label['r'] 7 assert(rel in rel2id) 8 label['r'] = rel2id[label['r']] 9 10 train_triple.add((label['h'], label['t'])) 11 12 13 if suffix=='_train': 14 for n1 in vertexSet[label['h']]: 15 for n2 in vertexSet[label['t']]: 16 fact_in_dev_train.add((n1['name'], n2['name'], rel)) 17 18 19 if is_training: 20 for n1 in vertexSet[label['h']]: 21 for n2 in vertexSet[label['t']]: 22 fact_in_train.add((n1['name'], n2['name'], rel)) 23 24 else: 25 # fix a bug here 26 label['intrain'] = False 27 label['indev_train'] = False 28 29 for n1 in vertexSet[label['h']]: 30 for n2 in vertexSet[label['t']]: 31 if (n1['name'], n2['name'], rel) in fact_in_train: 32 label['intrain'] = True 33 34 if suffix == '_dev' or suffix == '_test': 35 if (n1['name'], n2['name'], rel) in fact_in_dev_train: 36 label['indev_train'] = True 37 38 39 new_labels.append(label)
我们遍历labels集,我们首先通过rel保存获得的关系代码(例如P17),然后我们将P17对应的id=1,重新存入label[‘r’]。train_triple将此关系对应的头实体与尾实体存入(因为是个set所以重复的就略去了)
第一个if判断,如果该文件是有监督训练数据集,关系头实体对应的所有mentions的name,关系尾实体对应的所有menitons的name,以及保存的关系代码rel,全部双重遍历以关系三元组的形式保存在了fact_in_dev_train中。
第二个if判断,如果该文件是弱监督训练数据集关系头实体对应的所有mentions的name,关系尾实体对应的所有menitons的name,以及保存的关系代码rel,全部双重遍历以关系三元组的形式保存在了fact_in_train中。
这个else与第二个if是搭配关系,也就是按照作者的想法如果,只要不是远程监督训练集都会有这两个新标签 :label['intrain'] = False label['indev_train'] = False
首先打上两个新标签,赋值为False
之后执行双重循环,判断该关系的头实体对应的所有mentions的name,尾实体对应的所有mentions的name,形成的三元组是否在有监督训练数据集&弱监督训练数据集中出现过,出现过标签改为True
最后把修改过的label保存到list中new_labels.append(label)
1 na_triple = [] 2 for j in range(len(vertexSet)): 3 for k in range(len(vertexSet)): 4 if (j != k): 5 if (j, k) not in train_triple: 6 na_triple.append((j, k))
遍历每篇文章中所有的entity(注意是entity不是mentions),将不存在关系的两个entity存入na_triple中
data = [ ] 每篇文章处理完,调用append(itme)保存信息(该list在循环(1)之外)
1 item = {} 2 item['vertexSet'] = vertexSet 3 item['labels'] = new_labels 4 item['title'] = ori_data[i]['title'] 5 item['na_triple'] = na_triple 6 item['Ls'] = Ls 7 item['sents'] = ori_data[i]['sents'] 8 data.append(item)
到此为止循环(1)结束
文件保存
1 # saving 2 print("Saving files") 3 if is_training: 4 name_prefix = "train" 5 else: 6 name_prefix = "dev" 7 json.dump(data , open(os.path.join(out_path, name_prefix + suffix + '.json'), "w")) #生成新的json数据
如果是远程监督训练数据集则文件名前缀定为train
如果是人工标注训练数据集,验证集以及测试集,则前缀名为dev
1 char2id = json.load(open(os.path.join(out_path, "char2id.json"))) 2 word2id = json.load(open(os.path.join(out_path, "word2id.json"))) 3 ner2id = json.load(open(os.path.join(out_path, "ner2id.json")))
根据预处理文件文章篇数目初始化numpy矩阵,char_limit限制了每个单词字符最大长度不超过16
1 sen_tot = len(ori_data) 2 sen_word = np.zeros((sen_tot, max_length), dtype = np.int64) 3 sen_pos = np.zeros((sen_tot, max_length), dtype = np.int64) 4 sen_ner = np.zeros((sen_tot, max_length), dtype = np.int64) 5 sen_char = np.zeros((sen_tot, max_length, char_limit), dtype = np.int64)
接下来重点阅读分析encoding部分
1 for i in range(len(ori_data)): 2 item = ori_data[i] 3 words = [] 4 for sent in item['sents']: 5 words += sent 6 7 for j, word in enumerate(words): 8 word = word.lower() 9 10 if j < max_length: 11 if word in word2id: 12 sen_word[i][j] = word2id[word] 13 else: 14 sen_word[i][j] = word2id['UNK'] 15 16 for c_idx, k in enumerate(list(word)): #list('you') = ['y','o','u'] 17 if c_idx>=char_limit: 18 break #break语句只能跳出当前层次的循环 19 sen_char[i,j,c_idx] = char2id.get(k, char2id['UNK']) 20 21 for j in range(j + 1, max_length): 22 sen_word[i][j] = word2id['BLANK'] 23 24 vertexSet = item['vertexSet'] 25 26 for idx, vertex in enumerate(vertexSet, 1): #idx从下标1开始 27 for v in vertex: 28 sen_pos[i][v['pos'][0]:v['pos'][1]] = idx 29 sen_ner[i][v['pos'][0]:v['pos'][1]] = ner2id[v['type']]
words列表存储第i篇文章的所有单词
接下来将单词全部转换为小写(因为word2id.json文件中全部都是小写单词),如果该篇文章所有单词小于max_length,在word2id.json中能找到的单词对应id的存入到sen_word的numpy矩阵中,找不到对应id的存入['UNK']对应的id
第16-19行的循环体,将每个单词的每个字符保存在sen_char的numpy矩阵中(长度大于16直接截断),
第21-22行循环体是文章长度不足512,用word2id['BLANK']进行padding补足
第26-29行循环体是将第i篇文章中的mentions使用idx在sen_pos矩阵中进行填充(idx表明是文中第几个实体),同一个entity的不同mentions的idx值都是一样的,以同一entity的第一个mention的idx为准。而且还将节点命名实体类型的id在sen_ner中进行填充
最后是文件保存部分
1 print("Finishing processing") 2 np.save(os.path.join(out_path, name_prefix + suffix + '_word.npy'), sen_word) 3 np.save(os.path.join(out_path, name_prefix + suffix + '_pos.npy'), sen_pos) 4 np.save(os.path.join(out_path, name_prefix + suffix + '_ner.npy'), sen_ner) 5 np.save(os.path.join(out_path, name_prefix + suffix + '_char.npy'), sen_char) 6 print("Finish saving")
train_distant.json 通过gen_data.py
init(train_distant_file_name, rel2id, max_length = 512, is_training = True, suffix='')
生成了train.json ,train_char.npy train_ner.npy train_pos.npy train_word.npy(101873,512) train_char.npy (101873,512,16)
可以看到处理过后的弱监督训练数据集,多了na_triple(记录无关系实体对)和Ls(记录句子开头绝对位置)项,vertexSet实体顶点集中的pos标签对应的相对位置转换为绝对位置,
train_annotated.json通过gen_data.py
init(train_annotated_file_name, rel2id, max_length = 512, is_training = False, suffix='_train')
dev_train.json dev_train_char.npy dev_train_ner.npy dev_train_pos.npy dev_train_word.npy (3053,512) dev_train_char.npy (3053,512,16)
其余几项和上文描述相同,由人工注释训练数据集产生的dev_train.json在label项中intrain很多都为True,我感觉作者在这里写了一个Bug,逻辑混乱了没改过来
因为是远程监督数据集先执行的init( )函数所以,fact_in_train中会先存储该数据集中存在关系的mentions对,然后人工标注训练数据集再执行init( ),人工标注训练数据集中存在关系的mentions对添加到了fact_in_dev_train中,此时继续调用else,之前在远程监督数据集中出现的关系三元组被标记为True,就能区分开来,哪些是两个训练集都有的三元组,哪些不是两个训练集都有的三元组。至于label['indev_train']=False,就没有判断的必要了(因为本身这个数据集的所有关系都在人工监督数据集中,我判断我自己:))
dev.json通过gen_data.py
init(dev_file_name, rel2id, max_length = 512, is_training = False, suffix='_dev')
dev_dev.json dev_dev_char.npy dev_dev_ner.npy dev_dev_pos.npy dev_train_word.npy(1000,512) dev_dev_char.npy (1000,512,16)
验证集生成的数据与dev_train.json格式相同。
test.json通过gen_data.py
init(test_file_name, rel2id, max_length = 512, is_training = False, suffix='_test')
dev_test.json dev_test_char.npy dev_test_ner.npy dev_test_pos.npy dev_test_word.npy(1000,512) dev_test_char.npy (1000,512,16)
标签项等着我们分类判断,其余几项做了同上文相同的处理。