# 笔记
1、长文本划分为短文本的时候,是以模型底层分词的个数来判断的
2、输入的X必须转换成id,输入的Y也必须转换成id,这样X输入得到的结果P才能和Y做比对,所有的模型都是围绕这个根基做转换,确认好X和Y,把这些都转换成id输入模型。
3、只有一个实体全连接层768X2,每个个关系都有对应的全连接层参数768X3
4、随着step加大,w_ent的权重递减,w_rel权重递增。先关注实体,保证实体抽准确,后面关注关系的抽取,由于目前工作原因,更多细节待闲时在进行解读。
5、输入的X就是一个句子的ID拉平组成的句子对,以及滑动拆分得到实体的偏移量信息,输入的tag就是三个东西,主实体和客实体两个实体的实体信息,关系的主实体客实体的头部信息,关系的主实体客实体的尾部信息。([(1, 4, 1), (7, 10, 1)], [(13, 1, 7, 1)], [(13, 4, 10, 1)])
# 修改地方
1、把文件夹tplinker改成tplinker1
2、config.py 77行
"bert_path": "../../pretrained_models/bert-base-cased",改成
"bert_path": "../pretrained_models/bert-base-cased",
3、config.py 43行
"batch_size": 6, 改成
"batch_size": 32
第5行改成duie2
第16行,改成whole_span
4、train.py 210/217行
num_workers = 6 改成
num_workers = 0
5、创建extra_data文件夹存储原始百度数据
6、创建ori_data文件夹存储经过第一次处理的数据
7、preprocess文件夹新建trans_duie2.py,把原始数据经过一次处理,做一定的数据清洗
8、preprocess文件夹新建build_data.py,生产可以输入模型的数据,做一定的数据清洗,并且通过正则找到字符偏移
9、build_data_config.yaml
20行,设置为false,1/2/3/7行做好相关的文件夹配置
10、common文件夹的utils.py文件56-63行,增加相关百度数据处理的代码
11、train.py 第5行增加sys.path.append(os.path.split(os.path.abspath(os.path.dirname(__file__)))[0],防止linux运行报错
# CPU换GPU
1、config.py batch_size 2>32
2、train.py num_workers 6>0
# debug记录
1、滑动窗口没20个字符间隔一个起点,每一次都按相同最大长度来取原始数据的的位置索引
信息抽取两大难题:
一、暴露偏差
二、实体重叠、关系重叠
联合抽取的结构化预测双头标注才能同时解决上述两个问题,百度的联合抽取的结构化预测但是序列标注,能解决暴露偏差的问题,不能解决实体重叠和关系重叠的问题。
TPLinker标注,实体的头尾,主客实体的头,主客实体的尾。
1、先通过实体矩阵,得到所以的主实体和客实体集合,相当于得到所有的实体,再通过下面的方式准确的组合,弄成头实体尾key的字典,方便后面做查询。
2、通过关系的连个矩阵分别得到,主客实体对的尾的集合和主客实体对的头集合,
3、通过主客实体的头查询前面的字段,得到主客完整的实体,然后通过前面得到的尾实体,删除不满足的主客实体组合。
总结,就是先全实体》全主客实体对》满足要求的主客实体对
为什么TPLinker不适合直接用在NER上,而要用TPLinker_plus?
个人理解:讨论这个问题就要先了解最初的TPLinker设计模式,除了HandShaking外,作者还预定义了三大种类型ent, head_rel, tail_rel,每个类型下又有子类型,ent:{"O":0,"ENT-H2T":1}, head_rel:{"O":0, "REL-SH2OH":1, "REL-OH2SH":2}, head_tail:{"O":0, "REL-ST2OT":1, "REL-OT2ST":2}。在模型实际做分类时,三大类之间是独立的。以head_rel为例,其原数据整理得y_true矩阵shape为(batch_size, rel_size, shaking_seq_len),这里rel_size即有多少种关系。模型预测的结果y_pred矩阵shape为(batch_size, rel_size, shaking_seq_len, 3)。可以想象,这样的y_true矩阵已经很稀疏了,只有0,1,2三种标签。而如果换做NER,这样(batch_size, ent_size, shaking_seq_len)的矩阵将更加稀疏(只有0,1两种标签),对于一个(ent_size,shaking_seq_len)的矩阵来说,可能只有1至2个地方为1,这将导致模型无限地将预测结果都置为0,从而学习失败(事实实验也是这样)。作者在TPLinker中是如何解决这一问题的呢?其实作者用了个小trick回避了这一问题,具体做法是不再区分实体的类型,将所有实体都看作是DEFAULT类型,这样就把y_true压缩成了(batch_size,shaking_seq_len),降低了矩阵的稀疏性。作者对于这一做法的解释是"Because it is not necessary to recognize the type of entities for the relation extraction task since a predefined relation usually has fixed types for its subject and object.",即实体类别信息对关系抽取不太重要,因为每种关系某种程度上已经预定义了实体类型。综上,如果想直接把TPLinker应用到NER上是不合适的。
而TPLinker_plus改变了这一做法,他不再将ent, head_rel, tail_rel当做三个独立任务,而是将所有的关系与标签组合,形成一个大的标签库,只用一个HandShaking矩阵表示句子中的所有关系。举个例子,假设有以下3个关系(或实体类型):主演、出生于、作者,那么其与标记标签EH-ET,SH-OH,OH-SH,ST-OT,OT-ST组合后会产生15种tag,这极大地扩充了标签库。相应的,TPLinker_plus的输入也就变成了(batch_size,shaking_seq_len,tag_size)。这样的改变让矩阵中的非0值相对增多,降低了矩阵的稀疏性。(这只是一方面原因,更加重要原因的请参考问题2)
TPLinker_plus还做了哪些优化?
任务模式的转变:从问题1最后的结论可以看出,TPLinker_plus扩充标签库的同时,也将模型任务由原来的多分类任务转变成了多标签分类任务,即每个句子形成的shaking_seq可以出现多个的标签,且出现的数量不确定。形如
# 设句子的seq_len=10,那么shaking_seq=55
# 标签组合有8种tag_size=8
[
[0,0,1,0,1,0,1,0],
[1,0,1,0,0,0,0,1],
...
# 剩下的53行
]
损失函数:对于多标签分类问题,原本的损失函数不再适用。作者使用了一种新的损失函数,关于这个损失函数原理,可以参考苏神的文章将“softmax+交叉熵”推广到多标签分类问题 (先点个star再走呀)
TPLinker-NER中几个关键词怎么理解?
对于一个text中含有n个token的情况
shaking_matrix:n*n的矩阵,若shaking_maxtrix[i][j]=1表示从第i个token到第j个token为一个实体。(实际用到的只有上三角矩阵,以为实体的起始位置一定在结束位置前。)
matrix_index:上三角矩阵的坐标,(0,0),(0,1),(0,2)...(0,n-1),(1,1),(1,2)...(1,n-1)...(n-1,n-1)。
shaking_index:上三角矩阵的索引,长度为$\frac{n(n+1)}{2}$,即[0,1,2,...,n(n+1)/2 - 1]
shaking_ind2matrix_ind:将索引映射到矩阵坐标,即[(0,0),(0,1),...,(n-1,n-1)]
matrix_ind2shaking_ind:将坐标映射到索引,即
[[0, 1, 2, ..., n-1],
[0, n, n+1, n+2, ..., 2n-2]
...
[0, 0, 0, ..., n(n+1)/2 - 1]]
spot:一个实体对应的起止span和类型id,例如实体“北京”在矩阵中起始位置在7,终止位置在9,类型为LOC"(id:3),那么其对应spot为(7, 9, 3)。
输入的seq_hiddens维度是[batch,seq_len, hidden_size],其获得就可以简单看成是一句话经过bert后的编码,这里使用的是transformers这个python包,用的其AutoModel的即train.py下的278行,说白了就是用bert作为底层的encoder,下面假设seq_len是5
回到HandshakingKernel(上上副图),这里不得不讲一下论文中的优化到上三角,假设我们一句话有5个单词,本来矩阵是55,但是优化后只要上三角就可以啦,其实第一行是5列,第二行就是4列,第三行是3列,第四行是2列,第四行是1列,然后把他们平铺成一个序列即5+4+3+2+1.
从当前往后看即5,4,3,2,1 主要这里是j>=i就是要包括自身,因为自身单独一个单词可能就是一个实体
所以HandshakingKernel主要就是在做这个事情:
(1)代码中144行其实就是一个个遍历行,146行就是从当前取到最后,当是第一行时,ind是0,hidden_each_step维度是[batch,1,hidden_size]代表整句话第一个word的编码,为了进行拼接147行repeat_hiddens在第二个维度进行了复制,维度变成了[batch,5,hidden_size],相当于将当前单词编码复制了5份,visible_hiddens维度就是[batch,5,hidden_size],是从当前单词往后(包括自身)各个单词的编码,现在要计算得到上三角第一行的编码,即150行的shaking_hiddens,将当前单词和其后的各个单词的编码进行concat维度是[batch,5,hidden_size2],然后151行又过了一个MLP层,转化为了shaking_hiddens [batch,5,hidden_size]
(2)当是上三角第二行时,ind是1,hidden_each_step维度是[batch,1,hidden_size]代表第二个单词的编码,visible_hiddens维度就是[batch,4,seq_len],代表其后的各个单词的编码,为了拼接repeat_hiddens维度是[batch,4,hidden_size]即将hidden_each_step第二个单词复制了4份,shaking_hiddens此时是[batch,4,hidden_size*2],然后151行又过了一个MLP层,转化为了shaking_hiddens[batch,4,hidden_size]
(3)同理当是上三角第三行时,最后shaking_hiddens维度是[batch,3,hidden_size],以此例推
所以163行的shaking_hiddens_list是一个列表,就是记录上三角一行行的编码,当句子有5个单词时,该列表有五个元素,维度分别是:
[batch,5,hidden_size],[batch,4,hidden_size],[batch,3,hidden_size],[batch,2,hidden_size],[batch,1,hidden_size]
161行long_shaking_hiddens在第二个维度进行concat即维度是:[batch,5+4+3+2+1,hidden_size],平铺变成了一个sequence。
long_shaking_hiddens就是公共编码就是shaking_hiddens4ent。
输入的seq_hiddens维度是[batch,seq_len, hiddensize],是一句话经过bert编码过后的值,而HandshakingKernel函数的作用是将矩阵变为上三角矩阵,即本身矩阵为[seq_len * seq_len],在经过函数过后为每一行都减1,最后通过long_shakinghiddens把函数把结果铺平,得到[seq_len+(seq_len -1) + (seq_len -2)…+1],对应了图片部分。
整个函数先是循环每句话中的词,当ind是0时,hidden_each_step代表了循环的每个词的编码[batch,1,hidden_size],visiblehiddens是循环到的这个单词以及之后的单词的编码,维度就是[batch,seq_len,hidden_size],repeat_hiddens对hidden_each_step的第二个维度进行了复制,维度为[batch,seq_len,hidden_size],将当前单词和其后的各个单词的编码进行拼接维度是[batch,seq_len,hidden_size*2]组成上三角矩阵的一行,在经过MLP层后shakinghiddens的维度是[batch,seq_len,hidden_size],之后每一行依次类推。