判断随机抽取代码_bert关系抽取

解读的是苏剑林大神在百度关系抽取比赛中的代码,源代码看这里

数据转换

苏神把原始数据(数据下载地址)进行了转换,只提取出其中的(1)句子文本(2)spo列表,存成json,如下图:

77caf217bbf9966b9785046c3b573bad.png

这个json加载进来是一个列表,列表中每一个样本是一个dict,存储了上述的两个字段:text和spo_list,这两个字段是模型训练所需要的。

数据修复

3e5906ed6ce9556ae89e147eca71ecc0.png

传入的是一个dict,也就是上述json list中的一个样本,例如下图的例子:

f9d5e5d7c3869122a4e63887a9ad64af.png

首先,用正则抽取出text句子所有出现的书名号中的内容,比如这里something=["男人有泪", "好好先生"]。

然后,依次判断spo_list中的每一个三元组。如果predicate是”所属专辑“则记录此时的主语(肯定是歌曲)到歌曲list中,此时的宾语(肯定是专辑名)到专辑list中。【这里认为”所属专辑“的spo肯定是标注正确的】

这样遍历一遍之后,就得到当前text的歌曲list和专辑list。

再来第二次遍历spo_list,如果有”歌手“”作词“”作曲“任何一个关系,检查它的主语(这三种关系的主语肯定是歌曲名),如果主语是专辑而不是歌曲,则是错误的标注结果,丢弃这个spo。【这里认为这三种关系的主语有可能存在标注错误因此需要修复】

这样一来则做完了repair。

88fbf1cbfa04dbb39793016e07f82a29.png

这里predicates是一个dict,它是train_data,保存的是:每种predicate的spo有哪些,predicate是key,value是在train_data中该predicate出现过的所有spo。

比如:"作曲": [["七里香", "作曲", "周杰伦"], ["还在你身边", "作曲", "刘力豪"], ...]

数据生成器

ff587dc9bf011e65f368a62fe473bf7e.png

t1: 当前句子(也就是text)它的token_ids

t2: 当前句子的segment_ids(不太明白bert为什么要返回这个,为了标志是不同的句子?)

s1: 用来指示当前句子中(中文字符list),哪些位置id是“主语起始位”,只有0/1的1d array,是则为1,不是则为0。这里是针对所有的subjects。

s2: 同s1,指示哪些位置是“主语终止位”,它也是0/1的

k1: 当前句很可能存在多种关系,因此k1初始存储的是所有“主语起始ids”,后来从中随机选择了一个id

k2: 同上,初始存储的是“主语终止ids”,后来也是随机选择一个id,这和上面s1/s2不同,s1和s2标志的句中所有subject的位置,而k1/k2是标志了随机选择出的1个subject位置。

这里需要注意下,实际上k1及其对应的k2是成对出现的,也就是说,在随机选择一个k1的时候,其正确对应的k2就应该固定了,但是这里作者还是随机选择了k2的位置(有可能选到正确的主语终止id,也可能选到错误的主语终止id)。

如果很幸运,随机选择的k2正好和k1是配对的,也就是:

for j in items.get((k1, k2), []):
    o1[j[0]][j[2]] = 1
    o2[j[1]-1][j[2]] = 1

items.get可以取到,则会更新该主语的o1,o2和predicate标志位

o1/o2: 只有0/1的2d array,shape=(len(tokens), num_classes),指示的是上面随机选择出的那个subject的所有objects的“宾语起始id”和“宾语终止id”,且其对应的关系predicate

1b3c0b77eda9cddacd20b2f740ee651f.png

在(七里香,歌手,周杰伦)这个spo中,宾语是周杰伦,宾语起始位标志为红色1,宾语终止位标志位蓝色1,其余位置均为0

fde28ab06c87d104d5cd141a548dbdd6.png

T1/T2: [batch_size, sent_max_len_in_batch],存储token_ids和segment_ids

S1/S2: [batchsize, sent_max_len_in_batch],存储每一句的全部subjects的0/1标志位

K1/K2: [batch_size, 1],存储每一句的“天选”subject起始和终止id

O1/O2: [batch_size, sent_max_len_in_batch, num_classes],存储每一句那一个“天选”subject所对应的全部objects和predicates

NOTE: 用0作为padding

模型构建

f3af9c10d5625810780ff8d97422af0a.png

作者一上来定义了很多Input,这八个输入数据在上面已经讲过了,但是模型中怎么用,看到这里还是不清楚的。

ded8bbcbebff47d60532d1f7b6f4ffb0.png

实际上模型部分,需要分成 两段来看。

第一段是真正的定义模型,这部分只用到t1/t2和k1/k2。意思是说,该模型只需要接收一个句子(t1/t2表征)和句中某一个主语的起始idx和终止idx,比如(k1=20,k2=24)表示句中第20~24的字符构成的主语),给定这两个输入,就能输出该主语在句中所有的关系predicates及其宾语。多么完美的设计呀!

看代码比较懵逼,又是Dense又是Average的。画个图比较清楚:

62efb619f12bd93e120319d5c826791a.png
注意: 图中MLB表示max_len_batch,B表示batch

绿色圈出来的部分是输入,也就是t1/t2/k1/k2。蓝色圈出来的是输出,也就是ps1/ps2和po1/po2,分别指示句子中哪些部分是主语(ps1/ps2),哪些部分是带有关系类型的宾语(po1/po2)。

如何得到ps1/ps2呢?这里是将t1/t2也就是token_ids和segment_ids传入bert,bert会对整句的每个字做embedding,用t表示,返回t=[B, MLB, 768],对应每个字都有一个768维的向量表示。然后分别传入两个Dense层(sigmoid激活值在0~1之间表示概率),将[B,MLB,768]->[B,MLB,1]表示每个字是主语起始位(或主语终止位)的概率。这部分和k1/k2没有关系(不存在choice one),因此提取的是整个句子上所有主语

如何得到po1/po2呢?我们想一想,宾语及关系与什么有关?当然是整句和主语词啦!没错,那么显然为了得到po1/po2我们需要传入t和“天选”主语词k1/k2。作者在这里做了一个embedding提取操作,t是整句所有字符的embeddings,从t中抽取出k1和k2位置字符的embeddings那么我们就知道了主语的表示,代码中主语的表示是kv。得到主语的表示后,结合整句表示t,得到加入主语信息后的整句表示t。同上t作为输入,加入两个Dense层,就得到每个字是宾语起始位(或宾语终止位)的概率,而且给定主语的情况下,一旦宾语确定则spo的关系肯定是确定的,因此在object和predicate是一起指示的,也就是为什么po1/po2的shape=[B,MLB,num_class],表示句中每个字符作为每个关系的宾语起始位(或宾语终止位)的概率有多大(0~1之间)。

第二段是计算损失。这部分才会用到s1/s2和o1/o2。由于在第一部分得到了模型预测的ps1/ps2,它俩对应的ground truth是s1/s2,和预测的po1/po2,它俩对应的ground truth是o1/o2,接下来就可以算loss了。因为激活是sigmoid,所以是二分类binary_crossentropy。还需要注意的是binary_crossentropy(o1, po1)输出结果是[B,MLB,num_class],表示每个字在每一类关系上的loss,之后在第2维度num_class做的是sum而不是取max。为什么?因为给定主语的时候,它在句中可能有多个关系predicate,也就有可能对应多个宾语,如果取max,就只选择一个最有可能的1个宾语关系,这显然和ground truth o1/o2的设置不一样(前面有特别讲过o1/o2是标志了所有宾语而不是只有一个宾语被标为1),因此这里要看模型是不是在所有num_class上都能做出正确决定(如果都能,那么sum的loss也会很小)

Keras Callbacks

keras模型都有fit()或fit_generator(),就是模型定义好后,开始训练的。这俩函数比较重要的地方是callbacks列表,需要传入回调类的实体list。

回调类可以帮助我们在training阶段做任何想做的事情,比如保存最优模型啦,到一定阶段修改learning rate啦,观察统计dev指标啦等等。

我们可以继承keras.callbacks.Callback来定义自己的callback类,只需重写其中的6个方法即可

  • on_train_begin
  • on_train_end
  • on_epoch_begin
  • on_epoch_end
  • on_batch_begin
  • on_batch_end

你看,这样我们就可以在train的最开始阶段,每个batch的阶段,每个epoch的阶段,分别做一些想做的事情。

自定义回调类Evaluate

270917784114476b2d92e6c68f31b679.png

作者重写了两个方法: on_batch_begin()和on_epoch_end()

在每个epoch结束后的固定操作就是evaluation:返回F1,precision和recall,然后将F1与best F1进行比较,存储best model。

e5cbff2aa6190c6be932c5a1469f52f9.png

遍历dev_data的每一个数据(每个都有text和spo_list两个字段),R是模型预测出的spolist,T是真实的ground truth spo_list。因此,A是它们两者的交集,就是TP。B是预测出的spo数量,那么A/B就是预测对的占全部预测的比例,即precision。C表示ground truth spo的个数,因此A/C就是预测对的占ground truth的比例,即recall。

还记录了原始句子text,ground truth spo,预测出的spo,预测相比gt多出来的spo,没有预测出来的spo这五类。

抽取模型预测结果

这是extract_items()函数的功能。在dev和test时都会用到。

c0ca7f0854d2b71abb4a178ff66ac9c1.png

首先会输入一个句子(注意只有一句,所以这里batch_size=1),tokenize后得到_t1/_t2=[1, sent_len],再传入subject_model输出主语的起始、终止位置概率_k1/_k2

得到概率后,我想知道哪些位置的概率值比较大,因此用np.where筛选。这个函数的返回值有两个,取第一个值,所以索引0,这样我们就得到当前句子,哪些位置是主语起始位,哪些位置是主语终止位。

_subjects存储当前句子所有符合条件的subject和他们的起始终止位。

acbbf4e3c2d4ebea841b1bf9229b3eca.png

如果在当前句子抽取到了合适的主语subjects,则进行后续的抽取objects和predicate,如果连主语都找不到,则该句子就直接返回空[]。

首先,刚才得到的_t1=[1, sent_len],在第0维重复,则_t1=[len(_subjects), sent_len],t2同理。意思是每个主语都要提取它的object和predicate,因此,整句的表示_t1要复制len(_subjects)次,类比于此时batch_size=len(_subjects)

接下来,就是k1和k2,在上面我们知道k1和k2=[batch_size, 1],因此在这里应该是[len(_subjects), 1]。得到的_k1和_k2的shape=[len(_subjects), 1]

这样输入到object_model输出的_o1和_o2=[len(_subjects), sent_len, num_class],表示每个主语它所对应的所有objects的起始终止位置概率。

_oo1, _oo2 = np.where(_o1[i] > 0.5), np.where(_o2[i] > 0.4)

_o1[i]=[sent_len, num_class],数组的每行有num_class个元素 。返回的_oo1是个tuple

In [136]: a = np.random.rand(4,6)

In [137]: a
Out[137]:
array([[0.26501788, 0.42240895, 0.62156192, 0.03454611, 0.80249999,
        0.34420373],
       [0.50437012, 0.9199876 , 0.44104429, 0.0844013 , 0.04764323,
        0.48283732],
       [0.45646247, 0.70486308, 0.96938077, 0.81548676, 0.63082475,
        0.21882434],
       [0.40202736, 0.81626156, 0.24711037, 0.13700467, 0.6880018 ,
        0.22962576]])

In [138]: aa = np.where(a>0.5)

In [139]: aa
Out[139]: (array([0, 0, 1, 1, 2, 2, 2, 2, 3, 3]), array([2, 4, 0, 1, 1, 2, 3, 4, 1, 4]))

第一个表示符合条件的元素的所在行,第二个表示符合条件元素的所在列,这两个tuple对应起来就能定位到元素。

那么再看作者的实现,用zip把这两部分打包起来,每次_ooo1和_c1都是(宾语起始位, 关系),_ooo2和_c2都是(宾语终止位, 关系),如果这两者_c1=_c2且_ooo1<=_ooo2,则找到关系为c1,宾语也自然提取出来。每一个_ooo1只要找到终止位就break。

这样,就可以提取出当前句子text_in模型能抽取出的spo_list,返回即可。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值