《MaLSTM原始论文:Siamese Recurrent Architectures for Learning Sentence Similarity》

MaLSTM模型(ManhaĴan LSTM,孪生神经网络)

介绍模型的构建之前,我们先介绍下孪生神经网络(Siamese Network)和其名字的由来。

孪生神经网络(Siamese Network)和其名字的由来:Siamese和Chinese有点像。Siamese是古时候泰国的称呼,中文译作暹罗。Siamese在英语中是“孪生”、“连体”的意思。为什么孪生和泰国有关系呢?

十九世纪泰国出生了一对连体婴儿,当时的医学技术无法使两人分离出来,于是两人顽强地生活了一生,1829年被英国商人发现,进入马戏团,在全世界各地表演,1839年他们访问美国北卡罗莱那州后来成为马戏团的台柱,最后成为美国公民。1843年4月13日跟英国一对姐妹结婚,恩生了10个小孩,昌生了12个,姐妹吵架时,兄弟就要轮流到每个老婆家住三天。1874年恩因肺病去世,另一位不久也去世,两人均于63岁离开人间。两人的肝至今仍保存在费城的马特博物馆内。从此之后“暹罗双胞胎”(Siamese twins)就成了连体人的代名词,也因为这对双胞胎让全世界都重视到这项特殊疾病。

所以孪生神经网络就是有两个共享权值的网络的组成,或者只用实现一个,另一个直接调用,有两个输入,一个输出。1993年就已经被用来进行支票签名的验证。

孪生神经网络通过两个输入,被DNN进行编码,得到向量的表示之后,根据实际的用途来制定损失函数。比如我们需要计算相似度的时候,可以使用余弦相似度,或者使用exp(−∣∣hleft−hright∣∣)exp({-||h^{left}-h^{right}||)}exp(hlefthright)来确定向量的距离。

孪生神经网络被用于有多个输入和一个输出的场景,比如手写字体识别、文本相似度检验、人脸识别等

在计算相似度之前,我们可以考虑在传统的孪生神经网络的基础上,在计算相似度之前,把我们的编码之后的向量通过多层神经网络进行非线性的变化,结果往往会更加好,那么此时其网络结构大致如下:

其中Network1和network2为权重参数共享的两个形状相同的网络,用来对输入的数据进行编码,包括(word-embedding,GRU,biGRU等),Network3部分是一个深层的神经网络,包含(batchnorm、dropout、relu、Linear等层)

MaLSTM指的是将原本的计算余弦相似度改为一个线性层来计算相似度,由于使用的是线性层,所以可以尽可能的保留了向量的空间语义关系。

3. 代码实现

3.1 数据准备

3.1.1 对文本进行分词分开存储

这里的分词可以对之前的分词方法进行修改

def cut_sentence_by_word(sentence):# 对中文按照字进行处理,对英文不分为字母letters = string.ascii_lowercase + "+" + "/"  # c++,ui/ueresult = []temp = ""for word in line:if word.lower() in letters:temp += word.lower()else:if temp != "":result.append(temp)temp = ""result.append(word)if temp != "":result.append(temp)return resultdef jieba_cut(sentence,by_word=False,with_sg=False,use_stopwords=False):if by_word:return cut_sentence_by_word(sentence)ret = psg.lcut(sentence)if use_stopwords:ret = [(i.word, i.flag) for i in ret if i.word not in stopwords_list]if not with_sg:ret = [i[0] for i in ret]return ret
3.1.2 准备word Sequence代码

该处的代码和seq2seq中的代码相同,直接使用

3.1.3 准备DatasetDataLoader

和seq2seq中的代码大致相同

3.2 模型的搭建

前面做好了准备工作之后,就需要开始进行模型的搭建。

虽然我们知道了整个结构的大致情况,但是我们还是不知道其中具体的细节。

2016年AAAI会议上,有一篇Siamese Recurrent Architectures for Learning Sentence Similarity的论文(地址:https://www.aaai.org/ocs/index.php/AAAI/AAAI16/paper/download/12195/12023)。整个结构如下图:

可以看到word 经过embedding之后进行LSTM的处理,然后经过exp来确定相似度,可以看到整个模型是非常简单的,之后很多人在这个结构上增加了更多的层,比如加入attention、dropout、pooling等层。

那么这个时候,请思考下面几个问题:

  1. attention在这个网络结构中该如何实现

    • 之前我们的attention是用在decoder中,让decoder的hidden和encoder的output进行运算,得到attention的weight,再和decoder的output进行计算,作为下一次decoder的输入

    • 那么在当前我们可以把句子A的output理解为句子B的encoder的output,那么我们就可以进行attention的计算了

      和这个非常相似的有一个attention的变种,叫做self attention。前面所讲的Attention是基于source端和target端的隐变量(hidden state)计算Attention的,得到的结果是源端的每个词与目标端每个词之间的依赖关系。Self Attention不同,它分别在source端和target端进行,仅与source input或者target input自身相关的Self Attention,捕捉source端或target端自身的词与词之间的依赖关系。

  2. dropout用在什么地方

    • dropout可以用在很多地方,比如embedding之后
    • BiGRU结构中
    • 或者是相似度计算之前
  3. pooling是什么如何使用

    • pooling叫做池化,是一种降采样的技术,用来减少特征(feature)的数量。常用的方法有max pooling 或者是average pooling

3.2.1 编码部分

    def forward(self, *input):sent1, sent2 = input[0], input[1]#这里使用mask,在后面计算attention的时候,让其忽略pad的位置mask1, mask2 = sent1.eq(0), sent2.eq(0)# embeds: batch_size * seq_len => batch_size * seq_len * batch_sizex1 = self.embeds(sent1)x2 = self.embeds(sent2)# batch_size * seq_len * dim => batch_size * seq_len * hidden_sizeoutput1, _ = self.lstm1(x1)output2, _ = self.lstm1(x2)# 进行Attention的操作,同时进行形状的对齐# batch_size * seq_len * hidden_sizeq1_align, q2_align = self.soft_attention_align(output1, output2, mask1, mask2)# 拼接之后再传入LSTM中进行处理# batch_size * seq_len * (8 * hidden_size)q1_combined = torch.cat([output1, q1_align, self.submul(output1, q1_align)], -1)q2_combined = torch.cat([output2, q2_align, self.submul(output2, q2_align)], -1)# batch_size * seq_len * (2 * hidden_size)q1_compose, _ = self.lstm2(q1_combined)q2_compose, _ = self.lstm2(q2_combined)# 进行Aggregate操作,也就是进行pooling# input: batch_size * seq_len * (2 * hidden_size)# output: batch_size * (4 * hidden_size)q1_rep = self.apply_pooling(q1_compose)q2_rep = self.apply_pooling(q2_compose)# Concate合并到一起,用来进行计算相似度x = torch.cat([q1_rep, q2_rep], -1)def submul(self,x1,x2):mul = x1 * x2sub = x1 - x2return torch.cat([sub,mul],dim=-1)
atttention的计算

实现思路:

  1. 先获取attention_weight
  2. 在使用attention_weight和encoder_output进行相乘
    def soft_attention_align(self, x1, x2, mask1, mask2):'''x1: batch_size * seq_len_1 * hidden_sizex2: batch_size * seq_len_2 * hidden_sizemask1:x1中pad的位置为1,其他为0mask2:x2中pad 的位置为1,其他为0'''# attention: batch_size * seq_len_1 * seq_len_2attention_weight = torch.matmul(x1, x2.transpose(1, 2))#mask1 : batch_size,seq_len1mask1 = mask1.float().masked_fill_(mask1, float('-inf'))#mask2 : batch_size,seq_len2mask2 = mask2.float().masked_fill_(mask2, float('-inf'))# weight: batch_size * seq_len_1 * seq_len_2weight1 = F.softmax(attention_weight + mask2.unsqueeze(1), dim=-1)#batch_size*seq_len_1*hidden_sizex1_align = torch.matmul(weight1, x2)#同理,需要对attention_weight进行permute操作weight2 = F.softmax(attention_weight.transpose(1, 2) + mask1.unsqueeze(1), dim=-1)x2_align = torch.matmul(weight2, x1)
Pooling实现

池化的过程有一个窗口的概念在其中,所以max 或者是average指的是窗口中的值取最大值还是取平均估值。整个过程可以理解为拿着窗口在源数据上取值

窗口有窗口大小(kernel_size,窗口多大)和步长(stride,每次移动多少)两个概念

  • >>> input = torch.tensor([[[1,2,3,4,5,6,7]]])
    >>> F.avg_pool1d(input, kernel_size=3, stride=2)
    tensor([[[ 2.,  4.,  6.]]]) #[1,2,3] [3,4,5] [5,6,7]的平均估值
    

def apply_pooling(self, x):# input: batch_size * seq_len * (2 * hidden_size)#进行平均池化p1 = F.avg_pool1d(x.transpose(1, 2), x.size(1)).squeeze(-1)#进行最大池化p2 = F.max_pool1d(x.transpose(1, 2), x.size(1)).squeeze(-1)# output: batch_size * (4 * hidden_size)return torch.cat([p1, p2], 1)
相似度计算部分

相似度的计算我们可以使用一个传统的距离计算公式,或者是exp的方法来实现,但是其效果不一定好,所以这里我们使用一个深层的神经网络来实现,使用pytorch中的Sequential对象来实现非常简单

self.fc = nn.Sequential(nn.BatchNorm1d(self.hidden_size * 8),nn.Linear(self.hidden_size * 8, self.linear_size),nn.ELU(inplace=True),nn.BatchNorm1d(self.linear_size),nn.Dropout(self.dropout),nn.Linear(self.linear_size, self.linear_size),nn.ELU(inplace=True),nn.BatchNorm1d(self.linear_size),nn.Dropout(self.dropout),nn.Linear(self.linear_size, 2),nn.Softmax(dim=-1)
)

在上述过程中,我们使用了激活函数ELU,而没有使用RELU,因为在有噪声的数据中ELU的效果往往会更好。

ELU(∗x∗)=max(0,x)+min(0,α∗(exp(x)−1))ELU(*x*)=max(0,x)+min(0,α∗(exp(x)−1))ELU(x)=max(0,x)+min(0,α(exp(x)1)),其中α\alphaα在torch中默认值为1。

通过下图可以看出他和RELU的区别,RELU在小于0的位置全部为0,但是ELU在小于零的位置是从0到-1的。可以理解为正常的数据汇总难免出现噪声,小于0的值,而RELU会直接把他处理为0,认为其实正常值,但是ELU却会保留他,所以ELU比RELU更有鲁棒性


模型结构完整代码:

"""
实现排序模型
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import config# MaLSTM神经网络
class DnnSort(nn.Module):def __init__(self):super(DnnSort, self).__init__()self.embedding = nn.Embedding(num_embeddings=len(config.dnn_ws), embedding_dim=300, padding_idx=config.dnn_ws.PAD)self.lstm1 = nn.LSTM(input_size=300,hidden_size=config.dnnsort_hidden_size,num_layers=config.dnnsort_number_layers,dropout=config.dnnsort_lstm_dropout,bidirectional=config.dnnsort_bidriectional,batch_first=True)self.lstm2 = nn.LSTM(input_size=config.dnnsort_hidden_size * 8,hidden_size=config.dnnsort_hidden_size,num_layers=config.dnnsort_number_layers,dropout=config.dnnsort_lstm_dropout,bidirectional=False,batch_first=True)# 把编码之后的结果进行DNN,得到 不同类别的概率self.fc = nn.Sequential(nn.BatchNorm1d(config.dnnsort_hidden_size * 4),nn.Linear(config.dnnsort_hidden_size * 4, config.dnnsort_hidden_size * 2),nn.ELU(inplace=True),nn.BatchNorm1d(config.dnnsort_hidden_size * 2),nn.Dropout(config.dnnsort_lstm_dropout),nn.Linear(config.dnnsort_hidden_size * 2, config.dnnsort_hidden_size * 2),nn.ELU(inplace=True),nn.BatchNorm1d(config.dnnsort_hidden_size * 2),nn.Dropout(config.dnnsort_lstm_dropout),nn.Linear(config.dnnsort_hidden_size * 2, 2),# nn.Softmax(dim=-1))def forward(self, *input):sent1, sent2 = input#这里使用mask,在后面计算attention的时候,让其忽略pad的位置mask1 = sent1.eq(config.dnn_ws.PAD)mask2 = sent2.eq(config.dnn_ws.PAD)sent1_embeded = self.embedding(sent1)sent2_embeded = self.embedding(sent2)# 输入的2个句子向量sent1_embeded、sent2_embeded都通过同一个共享网络:lstm1output1, _ = self.lstm1(sent1_embeded)  # [batch_size,seq_len_1,hidden_size*2]output2, _ = self.lstm1(sent2_embeded)  # [batch_size,seq_len_2,hidden_size*2]output1_align, output2_align = self.soft_attention_align(output1, output2, mask1, mask2)# 得到attention result# _output1:[batch_size,seq_len1,hidden_size*8]# _output2:[batch_size,seq_len2,hidden_size*8]_output1 = torch.cat([output1, output1_align, self.submul(output1, output1_align)], dim=-1)_output2 = torch.cat([output2, output2_align, self.submul(output2, output2_align)], dim=-1)# 通过lstm2处理output1_composed, _ = self.lstm2(_output1)  # 将形状还原为[batch_size,seq_len1,hidden_size]output2_composed, _ = self.lstm2(_output2)  # 将形状还原为[batch_size,seq_len2,hidden_size]# 池化【将seq_len1/seq_len2这个维度去掉,防止sent1与sent2句子长度不一致导致的权重不一致】output1_pooled = self.apply_pooling(output1_composed)output2_pooled = self.apply_pooling(output2_composed)x = torch.cat([output1_pooled, output2_pooled], dim=-1)  # [bathc-Size,hidden_size*4]result = self.fc(x)return resultdef apply_pooling(self, input):"""在seq_len的维度进行池化:param input: batch_size,seq_len,hidden_size:return:"""avg_output = F.avg_pool1d(input.transpose(1, 2), input.size(1)).squeeze(-1)  # [batch_size,hidden_size]max_output = F.max_pool1d(input.transpose(1, 2), input.size(1)).squeeze(-1)return torch.cat([avg_output, max_output], dim=-1)  # [batch_size,hidden_size*2]def submul(self, x1, x2):_sub = x1 - x2_mul = x1 * x2return torch.cat([_sub, _mul], dim=-1)  # [batch_size,seq_len,hidden_size*4]def soft_attention_align(self, x1, x2, mask1, mask2):"""进行互相attention,返回context vector:param x1: [batch_size,seq_len_1,hidden_size*2]:param x2: [batch_size,seq_len_2,hidden_size*2]:param mask1: 【batch_size,seq_len1】:param mask2: [batch_size,seq_len2】:return:"""# 0. 把mask替换为-infmask1 = mask1.float().masked_fill_(mask1, float("-inf"))mask2 = mask2.float().masked_fill_(mask2, float("-inf"))# 1. 把一个作为encoder,另一个作为decoder# 1. attenion weight:[batch_size,seq_len_2,seq_len_1]attention_anergies = torch.bmm(x2, x1.transpose(1, 2))  # [batch_size,seq_len2,seq_len1]# 把x1作为encoder,x2作为decoderweight1 = F.softmax(attention_anergies + mask1.unsqueeze(1), dim=-1)  # [batch_size,seq_len2,seq_len1]# 2. 得到context vectorx2_align = torch.bmm(weight1, x1)  # [batch_size,seq_len2,hidden_size*2]# 把x2作为encoder,x1作为decoderweight2 = F.softmax(attention_anergies.transpose(1, 2) + mask2.unsqueeze(1), dim=-1)  # [batch_size,seq_len1,seq_len2]x1_align = torch.bmm(weight2, x2)  # [batch_size,seq_len1,hidden_Szie*2]return x1_align, x2_align

3.3 模型的训练

损失函数部分

在孪生神经网络中我们经常会使用对比损失(Contrastive Loss),作为损失函数,对比损失是Yann LeCun提出的用来判断数据降维之后和源数据是否相似的问题。在这里我们用它来判断两个句子的表示是否相似。

对比损失的计算公式如下:
L=12N∑n=1N(yd2+(1−y)max(margin−d,0)2)L = \cfrac{1}{2N}\sum^N_{n=1}(yd^2 + (1-y)max(margin-d,0)^2) L=2N1n=1N(yd2+(1y)max(margind,0)2)
其中d=∣∣an−bn∣∣2d = ||a_n-b_n||_2d=anbn2,代表两个两本特征的欧氏距离,y表示是否匹配,y=1表示匹配,y=0表示不匹配,margin是一个阈值,比如margin=1。

上式可分为两个部分,即:

  1. y = 1时,只剩下左边,∑yd2\sum yd^2yd2,即相似的样本,如果距离太大,则效果不好,损失变大
  2. y=0的时候,只剩下右边部分,即样本不相似的时候,如果距离小的话,效果反而不好,损失变大

下图红色是相似样本的损失,蓝色是不相似样本的损失

但是前面我们已经计算出了相似度,所以在这里我们有两个操作

  1. 使用前面的相似度的结果,把整个问题转化为分类(相似,不相似)的问题,或者是转化为回归问题(相似度是多少)
  2. 不是用前面相似度的计算结果部分,只用编码之后的结果,然后使用对比损失。最后在获取距离的时候使用欧氏距离来计算器相似度

使用DNN+均方误差来计算得到结果

def train(model,optimizer,loss_func,epoch):model.tarin()for batch_idx, (q,simq,q_len,simq_len,sim) in enumerate(train_loader):optimizer.zero_grad()output = model(q.to(config.device),simq.to(config.device))loss = loss_func(output,sim.to(config.deivce))loss.backward()optimizer.step()if batch_idx%100==0:print("...")torch.save(model.state_dict(), './DNN/data/model_paramters.pkl')torch.save(optimizer.state_dict(),"./DNN/data/optimizer_paramters.pkl")model = SiameseNetwork().cuda()
loss =  torch.nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
for epoch in range(1,config.epoch+1):train(model,optimizer,loss,epoch)

使用对比损失来计算得到结果

#contrastive_loss.py
import torch
import torch.nn
class ContrastiveLoss(torch.nn.Module):"""Contrastive loss function."""def __init__(self, margin=1.0):super(ContrastiveLoss, self).__init__()self.margin = margindef forward(self, x0, x1, y):# 欧式距离diff = x0 - x1dist_sq = torch.sum(torch.pow(diff, 2), 1)dist = torch.sqrt(dist_sq)mdist = self.margin - dist#clamp(input,min,max),和numpy中裁剪的效果相同dist = torch.clamp(mdist, min=0.0)loss = y * dist_sq + (1 - y) * torch.pow(dist, 2)loss = torch.sum(loss) / 2.0 / x0.size()[0]return loss

之后只需要把原来的损失函数改为当前的损失函数即可




参考资料:
原文GitHub代码

NLP-文本匹配-2016:MaLSTM(ManhaĴan LSTM,孪生神经网络模型)【语句相似度计算:用于文本对比,内容推荐,重复内容判断】【将原本的计算余弦相似度改为一个线性层来计算相似度】相关推荐

  1. 基于一个线性层的softmax回归模型和MNIST数据集识别自己手写数字

    原博文是用cnn识别,因为我是在自己电脑上跑代码,用不了处理器,所以参考Mnist官网上的一个线性层的softmax回归模型的代码,把两篇文章结合起来识别. 最后效果 源代码识别mnist数据集的准确 ...

  2. 【NLP】深度文本匹配综述

    目  录 1.研究背景与意义  2.深度学习在自然语言处理的应用  3.深度文本匹配与传统文本匹配  4.深度文本匹配国内外研究现状  4.1基于单语义表达的文本匹配 4.2基于多语义表达的文本匹配 ...

  3. 文本匹配(语义相似度/行为相关性)技术综述

    NLP 中,文本匹配技术,不像 MT.MRC.QA 等属于 end-to-end 型任务,通常以文本相似度计算.文本相关性计算的形式,在某应用系统中起核心支撑作用,比如搜索引擎.智能问答.知识检索.信 ...

  4. 谈谈文本匹配和多轮检索

    非常详细全面的文本匹配和多轮检索发展整理,建议收藏 1. 关于文本匹配  文本匹配是NLP的基础任务之一,按照论文中的实验对传统的文本匹配任务进行分类,大致可以分为「文本检索(ad-hoc),释义识别 ...

  5. 文本匹配相关方向总结(数据,场景,论文,开源工具)

    Motivation 前不久小夕在知乎上写了一个回答<NLP有哪些独立研究方向>,于是有不少小伙伴来问分类和匹配的参考资料了,鉴于文本分类的资料已经超级多了,就不写啦(不过分类相关的tri ...

  6. 文本匹配相关方向打卡点总结

    Motivation 前不久小夕在知乎上写了一个回答<NLP有哪些独立研究方向>[1],于是有不少小伙伴来问分类和匹配的参考资料了,鉴于文本分类的资料已经超级多了,就不写啦(不过分类相关的 ...

  7. 深度文本匹配在智能客服中的应用

    参加2018 AI开发者大会,请点击↑↑↑ 作者 | 云知声 目录 一. 深度文本匹配的简介 1. 文本匹配的价值 2. 深度文本匹配的优势 3. 深度文本匹配的发展路线 二. 智能客服的简介 1.  ...

  8. 深度之眼Pytorch打卡(十三):Pytorch全连接神经网络部件——线性层、非线性激活层与Dropout层(即全连接层、常用激活函数与失活 )

    前言   无论是做分类还是做回归,都主要包括数据.模型.损失函数和优化器四个部分.数据部分在上一篇笔记中已经基本完结,从这篇笔记开始,将学习深度学习模型.全连接网络MLP是最简单.最好理解的神经网络, ...

  9. bert模型可以做文本主题识别吗_文本匹配方法系列––BERT匹配模型

    1.概述 在介绍深层次交互匹配方法之前,本文接着多语义匹配方法[1]介绍基于BERT模型实现文本匹配的方法.将其单独介绍主要因为BERT实现文本匹配操作方便且效果优秀,比较适用于工业应用场景.关于be ...

最新文章

  1. Java 性能优化:教你提高代码运行的效率
  2. php 筛选数组,php数组如何按照字段筛选
  3. python入门学习的第三天
  4. 【剑指offer】面试题30:包含min函数的栈
  5. 东莞村财登录显示服务器断开,“东莞村财”APP运行一年多,还有很多村民股东未注册...
  6. 吴恩达-coursera-机器学习-week8
  7. Nosql部署集群环境创建 Redis 集群管理集群
  8. 谷歌服务框架_谷歌服务框架下载_谷歌服务框架全版本整理
  9. 怎么将pdf转换成jpg图片格式
  10. 开发PLO编译器过程的一些体会
  11. Lab 3:自行车码表
  12. MenuetOS.net最小的linux.
  13. 谷歌浏览器自带记笔记功能
  14. The Shawshank Redemption-14
  15. JPG和TIFF图像转换
  16. 京东抢购工具 监控工具 秒杀工具
  17. 京东抢购失败?试试用Python准时自动抢购!七夕秒抢种礼物!
  18. 如何微信多开,Mac电脑 同时登陆一个或多个微信、QQ
  19. SLAM论文笔记-使用点和线特征的激光雷达-单目视觉里程计
  20. excel上传和下载

热门文章

  1. Zero系列三部曲:Zero、Zero-Offload、Zero-Infinity
  2. dicom支持的文件格式及缩写
  3. C语言反三角函数的实现
  4. js四舍五入和计算精度问题
  5. Sublime Text 2/3 输入法修复[Ubuntu(Debian)]
  6. 计算机电源 n305p-04,N255PD-00 L305P-00 L305P-01 N305P-00 305W DELL电源
  7. 用栈解决骑士周游问题
  8. 单片机 ADC0809模数转换实验
  9. 助力设计师,培养UI设计思维【萧蕊冰】
  10. CC00038.kafka——|Hadoopkafka.V23|——|kafka.v23|消费者拦截器参数配置|