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

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加上一层全连接:
在这里插入图片描述

  • 输入层: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中最大一项对应的分类。

效果:

在这里插入图片描述
可以看出使用了这两个改进之后性能有明显的提升。

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个词。不足的部分用’ &lt; p a d &gt; &lt;pad&gt; <pad>'替代,长的部分舍去。因此词到实体的距离的取值就在[-89,89]之间。

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

def tokenizer(text): # create a tokenizer function
    # 返回 a list of <class 'spacy.tokens.token.Token'>
    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类。
在这里插入图片描述

5. 踩过的坑

5.1 自定义Loss

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

写完loss之后可以自行测试一下,看看计算的梯度是否与理论相符:
在这里插入图片描述
比如这个基于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版本的实现

Sure! Here's a simple code example using PyTorch to classify images of cats and dogs: ```python import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms # Define the neural network architecture class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(3, 16, 3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(16, 32, 3, padding=1) self.fc1 = nn.Linear(32 * 56 * 56, 128) self.fc2 = nn.Linear(128, 2) def forward(self, x): x = self.pool(torch.relu(self.conv1(x))) x = self.pool(torch.relu(self.conv2(x))) x = x.view(-1, 32 * 56 * 56) x = torch.relu(self.fc1(x)) x = self.fc2(x) return x # Set device (GPU/CPU) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Load and transform the dataset transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) trainset = torchvision.datasets.ImageFolder(root='train_data_path', transform=transform) trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2) # Define the network, loss function, and optimizer net = Net().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) # Training loop for epoch in range(5): running_loss = 0.0 for i, data in enumerate(trainloader, 0): inputs, labels = data[0].to(device), data[1].to(device) optimizer.zero_grad() outputs = net(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() if i % 2000 == 1999: print(f"[{epoch + 1}, {i + 1}] loss: {running_loss / 2000:.3f}") running_loss = 0.0 print("Training finished!") # Save the trained model torch.save(net.state_dict(), 'model.pth') ``` Note: 1. Replace `'train_data_path'` with the actual path to your training dataset folder, containing separate subfolders for cats and dogs. 2. Make sure you have the necessary dependencies installed (e.g., `torch`, `torchvision`). This code defines a CNN (Convolutional Neural Network) architecture using PyTorch for classifying images of cats and dogs. It loads and preprocesses the dataset, trains the network, and saves the trained model. You can modify the network architecture, hyperparameters, and other aspects to fit your specific requirements.
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值