Classifying Relations by Ranking with Convolutional Neural Networks实现(pytorch)

Classifying Relations by Ranking with Convolutional Neural Networks实现(pytorch)

    • 1. 问题描述
    • 2. 网络结构
      • 效果:
    • 3. 数据预处理
    • 4. 模型实现
    • 5. 踩过的坑
      • 5.1 自定义Loss
      • 5.2 F值计算
      • 5.3 Loss 值溢出
    • 6. 实验结果分析

1. 问题描述

这是关系抽取的一篇经典文章Classifying Relations by Ranking with Convolutional Neural Networks。使用的是SemEval2010 Task 8 数据集,共有9种二元关系,每种关系对实体的前后排列敏感,加上other分类一共19个小分类。

2. 网络结构

论文的主要是在Zeng 2014的CNN基础上做的改进,最大的变化是损失函数,不再使用softmax+cross-entropy的方式,而是margin based的ranking-loss。还有一个对于other分类的特殊处理。

先看网络结构,不考虑loss的改变,实际上的网络结构没有什么变化,同样是一层CNN加上一层全连接:
Classifying Relations by Ranking with Convolutional Neural Networks实现(pytorch)_第1张图片

  • 输入层:word embedding+position embedding
  • 卷积层:长度固定为3的卷积核(实际实现的代码是多个卷积核)
  • Pooling: max pooling
  • 全连接层:线性相乘得到对应每个类别的score

全连接层之后没有使用常用的softmax+cross entropy,而是自定义了一个基于margin的ranking loss:
在这里插入图片描述
在这里有两个分数, s θ ( x ) y + s_{\theta}(x)_{y^{+}} sθ(x)y+指的是句子x正确分类对应的score,而 s θ ( x ) c − s_{\theta}(x)_{c^{-}} sθ(x)c指的是从全连接层得到的分数向量中除去 s θ ( x ) y + s_{\theta}(x)_{y^{+}} sθ(x)y+之外最大的分量,也就是分数最大的错误得分。这样做在训练的过程中会使得 s θ ( x ) y + s_{\theta}(x)_{y^{+}} sθ(x)y+不断变大而 s θ ( x ) c − s_{\theta}(x)_{c^{-}} sθ(x)c不断变小。其中 m + m^{+} m+ m − m^{-} m表示正确与错误的margin。

Other类别的特殊处理:因为Other类别比较杂,因此对其特殊处理。在计算loss的时候如果样本的label是Other,那么就将其Loss的第一项归零。而计算最终结果时只有当Other外类别的score都小于0时才归为Other类,否则选择其它score中最大一项对应的分类。

效果:

Classifying Relations by Ranking with Convolutional Neural Networks实现(pytorch)_第2张图片
可以看出使用了这两个改进之后性能有明显的提升。

3. 数据预处理

明确我们需要的输入为每个句子的word embedding以及每个词与目标的两个实体之间的positional embedding。

举个例子:
在这里插入图片描述
每一个数据有三行,第一行是文本,第二行是关系label,第三行是注释。word embedding就是将每个词都转换成对应的词向量,这里可以用开源的glove等。而positional embedding是指每个词距离两个entity之间的距离映射而成的embedding。比如上图第二句中的wrapped,距离前一个实体child距离为3,距离后一个实体距离为-5,而positional embedding是用这两个数字映射得到的embedding。假如每个词的词向量维度是 d w d_{w} dw,每个距离对应的向量维度是 d p d_{p} dp,那么整合之后每个词语对应的embedding维度为 d w + 2 ∗ d p d_{w}+2*d_{p} dw+2dp。其中positional embedding的初始化为随机初始化。

文本为英文,使用spacy库中的分词工具来处理英文分词(主要是标点和缩写等等)。然后计算出每个词到两个实体的距离,分别存为CSV格式的训练集验证集与测试集。具体的代码在github的data_util.py文件中。

这样每个CSV中文件的格式如下:
在这里插入图片描述
有三列,代表label,原始句子,句子中每个词对应的位置向量。

之后需要把每个输入变成sent_len * ( d w + 2 ∗ d p d_{w}+2*d_{p} dw+2dp)维度的矩阵,sent_len表示的是句子的长度。这里由于每个句子的长度不同,因此统一句子的长度为90个词。不足的部分用’ < p a d > <pad> <pad>'替代,长的部分舍去。因此词到实体的距离的取值就在[-89,89]之间。

其中位置的表示已经用数字表示出来了,只需要在模型中加一个embedding层进行lookup即可。而句子需要分词后建立词表,然后将每个词对应成词典中的序号。这里使用了torchtext库直接完成,这部分代码如下:

def tokenizer(text): # create a tokenizer function
    # 返回 a list of 
    return [tok.text for tok in nlp.tokenizer(text)]

def emb_tokenizer(l):
    r = [y for x in eval(l) for y in x]
    return r

TEXT = data.Field(sequential=True, tokenize=tokenizer,fix_length=args.sent_len)
LABEL = data.Field(sequential=False, unk_token=None)
POS_EMB = data.Field(sequential=True,unk_token=0,tokenize=emb_tokenizer,use_vocab=False,pad_token=0,fix_length=2*args.sent_len)

print('loading data...')
train,valid,test = data.TabularDataset.splits(path='../data/SemEval2010_task8_all_data',
                                              train='SemEval2010_task8_training/TRAIN_FILE_SUB.CSV',
                                              validation='SemEval2010_task8_training/VALID_FILE.CSV',
                                              test='SemEval2010_task8_testing_keys/TEST_FILE_FULL.CSV',
                                              format='csv',
                                              skip_header=True,csv_reader_params={'delimiter':'\t'},
                                              fields=[('relation',LABEL),('sentence',TEXT),('pos_embed',POS_EMB)])
TEXT.build_vocab(train,vectors='glove.6B.300d')
LABEL.build_vocab(train)

train_iter, val_iter, test_iter = data.Iterator.splits((train,valid,test),
                                                             batch_sizes=(args.batch_size,len(valid),len(test)),
                                                             device=args.device,
                                                             sort_key=lambda x: len(x.sentence),
#                                                              sort_within_batch=False,
                                                             repeat=False)

torchtext库的使用可以参考官方文档以及这个博客还有知乎文章。总的来说这个库能够完成分词、去停用词、建立词表映射、设定预训练的词向量等诸多操作,而且最后能够以iterator的形式给出处理完的结果,很是方便。

4. 模型实现

其实也没得说的,具体还是要看代码。

可以先实现成正常的cnn+softmax+cross entropy的形式,然后在这个基础上进行修改。主要的修改是加入了positional embedding,需要与原本的word embedding合并以后再进行卷积的操作。正常的cnn实现可以参考前一篇文章pytorch 实现textCNN。也可以直接看我的github。

实际上改进的部分在于Loss的计算,因此在cnn结构的部分基本没有变化,主要的变化是Loss的设计,这里遇到了不少坑,还是直接看代码吧。

还有一个是对于Other类别的特殊处理。在Loss的计算里已经加上了,不过在判断结果的时候也会有一个特殊处理。在train.py的getResult函数里。

除此之外还有个要注意的点就是虽然实际上有19个(9*2+1)分类,但实际上在计算F值的时候不计算Other类,同时将剩下的18类当做实体前后不敏感的9类计算。如下图所示,cnn模型计算score时当做18类,而计算accuracy和F值时候当做9类。
Classifying Relations by Ranking with Convolutional Neural Networks实现(pytorch)_第3张图片

5. 踩过的坑

5.1 自定义Loss

才疏学浅,还以为将Loss表示出来pytorch就能完成自动求导。我毕竟还是too young。可以参考知乎问题pytorch如何自定义loss?首先就是继承自nn.Module,然后是必须让计算图能够连起来,不然没法计算梯度。

写完loss之后可以自行测试一下,看看计算的梯度是否与理论相符:
Classifying Relations by Ranking with Convolutional Neural Networks实现(pytorch)_第4张图片
比如这个基于margin的ranking loss,与每个样本score向量中的一个或两个分量有关系(与最大的score对应的label是否是other有关),可以观察对应的梯度是否相符。

5.2 F值计算

就是不计算other以及把18类归为9类计算的事情。不好好看论文的结果。

5.3 Loss 值溢出

再看一眼loss的定义:
在这里插入图片描述
随着训练过程的持续,正确分类的score不断变大,错误分类的score不断变小。经过exponential操作以后就会导致溢出。

报的错误是cuda runtime error (59) : device-side assert triggered at /pytorch/aten/src/THC/generic/THCTensorCopy.c:70类似的,而且根本定位不了。
搜来搜去都说大致是因为可能是数组下标越界,然而找了一两天才发现并不是,而是因为溢出。

我的解决方法是修改它的loss:
原来的loss为: L = l o g ( 1 + e x p ( γ ( m + − s θ ( x ) y + ) ) + l o g ( 1 + e x p ( γ ( m − − s θ ( x ) c − ) ) L=log(1+exp(\gamma(m^{+}-s_{\theta}(x)_{y^{+}}))+log(1+exp(\gamma(m^{-}-s_{\theta}(x)_{c^{-}})) L=log(1+exp(γ(m+sθ(x)y+))+log(1+exp(γ(msθ(x)c)),我修改后的Loss为
L = l o g ( 1 + e x p ( γ ( m + − s θ ( x ) y + ) ) + l o g ( 1 + e x p ( γ ( − 100 + s θ ( x ) y + ) ) + l o g ( 1 + e x p ( γ ( m − − s θ ( x ) c − ) ) + l o g ( 1 + e x p ( γ ( − 100 − s θ ( x ) c − ) ) L=log(1+exp(\gamma(m^{+}-s_{\theta}(x)_{y^{+}}))+log(1+exp(\gamma(-100+s_{\theta}(x)_{y^{+}}))+log(1+exp(\gamma(m^{-}-s_{\theta}(x)_{c^{-}}))+log(1+exp(\gamma(-100-s_{\theta}(x)_{c^{-}})) L=log(1+exp(γ(m+sθ(x)y+))+log(1+exp(γ(100+sθ(x)y+))+log(1+exp(γ(msθ(x)c))+log(1+exp(γ(100sθ(x)c))
这个Loss大致能够保证正确label的score不超过100,错误label的score不超过-100。不过我比较想知道不修改loss的话应该怎么避免这个溢出的问题。

6. 实验结果分析

拼了老命调参最后的结果F值只有77左右,距离文中的84还相去甚远,感觉可能不全是调参的问题,暂时先这样吧。

训练集上非other类别的准确率已经接近100%了,所以应该不是模型表现力不够的问题,那就是过拟合了。不过L2系数加了一点之后就开始学不到东西了。dropout已经加到0.7,再加结果也会变得更差。在embedding层后面以及卷积层后面都加了dropout了。
分析一下可能导致效果不好的原因:

  1. 可能是修改后的loss导致的,暂时没招。
  2. 可能是句子的处理时统一截断长度为90以及对于padding的符号没有计算相应的到实体的距离的问题。对于padding的符号,到实体的距离都padding为一个固定的值了。但我感觉这样做应该问题不大吧毕竟已经是padding而不是有意义的词了。而且原文也没提到怎么处理这部分。
  3. 词向量问题。文中参数先是词向量维度是400,大概是自己训练的吧因为glove里也没发现有400维的数据。但这个概率也不大,因为之前的文章里有cnn+cross entropy的使用的是50维的词向量也比77的效果更好。
  4. 还是调参问题。已经尽力了。。。
  5. 未知问题。尽力了。

最后感谢github某老哥的tensorflow版本的实现。

你可能感兴趣的:(nlp)