苏剑林作者

基于GRU和am-softmax的句子相似度模型 | 附代码实现

前言:计算机视觉的朋友会知道,am-softmax 是人脸识别中的成果。所以这篇文章就是借鉴人脸识别的做法来做句子相似度模型,顺便介绍在 Keras 下各种 margin loss 的写法。

背景

细想之下会发现,句子相似度与人脸识别有很多的相似之处。

已有的做法

在我搜索到的资料中,深度学习做句子相似度模型,就只有两种做法:一是输入一对句子,然后输出一个 0/1 标签代表相似程度,也就是视为一个二分类问题,比如 Learning Text Similarity with Siamese Recurrent Networks [1] 中的模型是这样的:

▲ 将句子相似度视为二分类模型

包括今年拍拍贷的“魔镜杯”,也是这种格式。另外一种做法是输入一个三元组“(句子 A,跟 A 相似的句子,跟 A 不相似的句子)”,然后用 triplet loss 的做法解决,比如文章 Applying Deep Learning To Answer Selection: A Study And An Open Task [2] 中的做法。 

这两种做法其实也可以看成是一种,本质上是一样的,只不过 loss 和训练方法有所差别。但是,这两种方法却都有一个很严重的问题:负样本采样严重不足,导致效果提升非常慢。

使用场景

我们不妨回顾一下我们使用句子相似度模型的场景。一般来说,我们事先存好了很多 FAQ 对,也就是“问题-答案”的语料对。当我们碰到一个新问题时,我们就需要比较这个新问题与原来数据库中所有问题的相似度,找出最相似的那个,根据相似度和阈值决定是否做出回答。 

注意,这里边包含了两个要素,第一是“所有”,理论上来说,我们跟数据库中的所有问题都比较一次,然后找出最相似的;第二是“阈值”,我们也不知道新问题在数据库中是否有答案,因此这个阈值决定是我们是否要做出回应。如果不管三七二十一都取 top 1 来作答,那体验也会很差的。 

我们先来关心“所有”。“所有”意味着在训练的时候,对于每个句子,除了仅有的几个相似句是正样本,其它所有句子都应该作为负样本。但如果用前面的做法,其实我们很难完整地采样所有的负样本出来,而且就算可以做到,训练时间也非常长。这就是前面说的弊端所在。

来自人脸的帮助

我一直觉得,在机器学习领域中,其实不应该过分“划清界线”,比如有些读者觉得自己是做 NLP 的,于是就不碰图像,反过来做图像的,看到 NLP 的就远而避之。事实上,整个机器学习领域之间的沟壑并没有那么大,很多东西的本质都是一样的,只是场景不同而已。比如,所谓的句子相似度模型,其实几乎就完全对应于人脸识别任务,而人脸识别目前已经相当成熟了,显然我们是可以借鉴的。 

先不说模型,我们来想象一下人脸识别的使用场景。比如公司内可以用人脸识别打卡,当有了一个人脸识别模型后,我们事先会存好一些公司员工的人脸照片,然后每天上班时,先拍一张员工的人脸照(实时拍摄,显然不会跟已经存好的照片完全吻合),然后要判断他/她是不是公司的员工,如果是,还要确定是哪一位员工。 

试想一下,将上面的场景中,“人脸”换成“句子”,是不是就是句子相似度模型的使用场景呢? 

显然,句子相似度模型可以是说 NLP 中的人脸识别了。

模型

句子相似度和人脸识别在各方面都很相似:从模型的使用到构建乃至数据集的量级上,都是如此地接近。所以,几乎人脸识别的一切模型和技巧,都可以用在句子相似度模型上。 

作为分类问题

事实上,前面说的 triplet loss,也是训练人脸识别模型的标准方法之一。triplet loss 本身没有错,反而,如果能精调参数并且重新训练,它效果还可能非常好。只是在很多情况下,它实在是太低效了。当前,更标准的做法是:视为一个多分类问题。 

比如,假设训练集里边有 10 万个不同的人,每个人 5 张人脸图,那么就有 50 万张训练图片了。然后我们训练一个 CNN 模型,对图片提取特征,并构建一个 10 万分类的模型。没错,就是跟 mnist 一样的分类问题,只不过这时候分类数目大得多,有多少个不同的人就有多少类。那么,句子相似度问题也可以这样做,可以将训练集划分为很多组“同义句”,然后有多少组就有多少类,也将句子相似度问题当作分类问题来做。 

注意,这仅仅是训练,最后训练出来的分类模型可能毫无用处。这不难想象,我们可以用已有的人脸数据库来训练一个人脸识别模型,但我们的使用场景可能是公司打卡,也就是说要识别的人脸是公司内部的员工脸,他们显然不会在公开的人脸数据库中。所以分类模型是没有意义的,真正有意义的是分类之前的特征提取模型。比如,一个典型的 CNN 分类模型可以简写为两步:

其中 x 是输入,p 是每一类的概率输出,这里的 softmax 不用加偏置项。作为一个分类问题训练时,我们输出的是人脸图片 x 和对应的 one hot 标签 p,但是在使用的时候,我们不用整个模型,我们只用 CNN(x) 这部分,这部分负责将每一张人脸图片转化为一个固定长度的向量。 

有了这个转化模型(编码器,encoder),不管什么场景下,我们都可以对新人脸进行编码,然后转化为这些编码向量之间的比较,从而就不依赖原来的分类模型。所以,分类模型是一个训练方案,一旦训练完成,它就功成身退了,留下的是编码模型。

分类与排序

这样就可以了?还没有。前面说到,我们真正要做的是一个特征提取模型(编码器),并且用分类模型作为训练方案,而最后使用的方法是对特征提取模型的特征进行对比排序。 

我们要做特征排序,但是借助分类模型训练,这两者等价吗? 

答案是:相关但不等价分类问题是怎么做的呢?直观来看,它是选定了一些类别中心,然后说:每个样本都属于距离它最近的中心的那一类。 

当然这些类别中心也是训练出来的,而这里的“距离”可以有多种可能性,比如欧式距离、cos 值、内积都可以,一般的 softmax 对应的就是内积。分类问题的这种做法,就导致了下面的可能的分类结果:

▲ 一种可能的分类结果,其中红色点代表类别中心,其他点代表样本

这个分类结果有什么问题呢?我们留意图上的 z1,z2,z3 三个样本,其中 z1,z3 距离 c1 最近,所以它们是类别 1 的,z2 距离 c2 最近,所以它是类别 2 的,假设这个分类没有错,也就是说 z1,z3 它们可能是同义句,z2 跟它们不是同义的,又或者 z1,z3 是同一个人的人脸图,而 z2 则是另一个人的。

从分类角度,这结果很合理,但我们已经说过,我们最终不要分类模型,我们需要特征之间的比较。这样问题就很明显了:z1,z2 距离这么近,却不是同一类的,z1 跟 z3 距离这么远,却是同一类的。如果我们用特征排序的方法给 z1 找一个同义句,那么就会找到 z2 而不是 z3。

Loss

上面说的,就是分类与排序的不等价性,当然,从图上也可以看出,尽管不完全等价,分类模型还是给了大部分的特征一个合理的位置分布,只是在边缘附近的特征,就可能出现问题。

Margin Softmax

可以想象,问题出现在分类边界附近的那些点上面,而出现问题的原因,其实就是分类条件过于宽松,如果加强一下分类条件,就可以提升排序效果了,比如改为:每个样本与它所属类的距离,必须小于它跟其他类的距离的 1/2。 

原来我们只需要小于它与其他类的距离,现在不但要这样,还要小于其它距离的一半,显然条件加强了,而前一个图所示的分类结果就不够好了,因为虽然如图有 ‖z1−c1‖<‖z1−c2‖,但是没有做到 ‖z1−c1‖<1/2‖z1−c2‖,所以还需要进一步优化 loss。 

假如按照这个条件训练完成后,我们可以想象,这时候 z1,z2 的距离就被拉大了,而 z1,z3 的距离就被缩小了,这正是我们所希望的结果:增大类间距离,缩小类内距离。 

事实上,上面所说的方案,可以说就是人脸识别中很著名的方案 l-softmax [3]。人脸识别领域中,很多类似的 loss 被提出来,它们都是针对上述分类问题与排序问题的不等价性设计出来的,比如 a-softmax、am-softmax、aam-softmax等,它们都统称 margin softmax。而且,不仅有 margin softmax,还有 center loss,还有 triplet loss 的一些改进版本等等。

am-softmax

我不是做图像的,因此人脸识别的故事我就讲不下去了,还是回到本文的正题。上面说到人脸识别不能用纯粹的 softmax 分类,必须要用 margin softmax,而因为句子相似度模型和人脸识别模型的相似性,告诉我们句子相似度模型也是需要 margin softmax 的。总而言之,至少要挑一个 margin softmax 来实现呀。 

其中,效果比较好而最容易实现的方案,当数 am-softmax,本文就以它为例子来介绍这一类 margin softmax 的实现方案,最终实现一个句子相似度模型。

am-softmax的做法其实很简单,原来 softmax 是 p=softmax(zW),设:

那么 softmax 可以重新写为:

然后 loss 取交叉熵,也就是:

t 为目标标签。而 am-softmax 做了两件事情:

1. 将 z 和 ci 都做 l2 归一化,也就是说,内积变成了 cos 值;

2. 对目标 cos 值减去一个正数 m,然后做比例缩放 s。即 loss 变为:

其中 θi 代表 z,ci 的夹角。在 am-softmax 原论文中,所使用的是 s=30,m=0.35。

从 am-softmax 中,我们可以看到针对前面所提的问题的解决方案了。首先,s 的存在是必要的,因为 cos 的范围是 [−1,1],需要做好比例缩放,才允许 pt 能足够接近于 1(有必要的话)。当然,s 并不改变相对大小,因此这不是核心改变,核心是原来应该是 cosθt 的地方,换成了 cosθt−m。

随心所欲地margin 

前面提到,从分类问题到特征排序的不完全等价性,可以通过加强分类条件来解决,所谓加强,其实意思很简单,就是用一个新的函数 ψ(θt) 来代替 cosθt,只要:

我们都可以认为是一种加强,而 am-softmax 则是取 ψ(θt)=cosθt−m,这估计是满足上式的最简单粗暴的方案了(幸好,它效果也很好)。

理解了这种思想之后,其实我们可以构造各种各样的 ψ(θt) 了,毕竟理论上满足 (6) 式的都可以选取。前面我们也提到了 l-softmax 和 a-softmax,它们相当于选择了 ψ(θt)=cosmθt,其中 m 是一个整数。

但我们知道,cosmθt<cosθt 并非总是成立的,所以论文中基于 cosmθt 构造了一个分段函数出来,显得特别麻烦,而且也使得模型极难收敛。事实上,我试验过下面的方式:

结果媲美 am-softmax(在句子相似度任务上)。所以,上述可以作为 l-softmax 和 a-softmax 的一个简单的替代品了吧,我称为 simpler-a-softmax,有兴趣的读者可以试试在人脸上的效果。

实现

最后介绍本文对这些 loss 在 Keras 下的实现。测试环境的 Python 版本为 2.7,Keras 版本为 2.1.5,TensorFlow 后端。 

基本实现

用最基本的方式实现 am-softmax 并不困难,比如:

from keras.models import Model
from keras.layers import *
import keras.backend as K
from keras.constraints import unit_norm


x_in = Input(shape=(maxlen,))
x_embedded = Embedding(len(chars)+2,
                       word_size)(x_in)
x = CuDNNGRU(word_size)(x_embedded)
x = Lambda(lambda x: K.l2_normalize(x, 1))(x)

pred = Dense(num_train,
             use_bias=False,
             kernel_constraint=unit_norm())(x)

encoder = Model(x_in, x) # 最终的目的是要得到一个编码器
model = Model(x_in, pred) # 用分类问题做训练

def amsoftmax_loss(y_true, y_pred, scale=30, margin=0.35):
    y_pred = y_true * (y_pred - margin) + (1 - y_true) * y_pred
    y_pred *= scale
    return K.categorical_crossentropy(y_true, y_pred, from_logits=True)

model.compile(loss=amsoftmax_loss,
              optimizer='adam',
              metrics=['accuracy'])

Sparse版实现

上面的代码并不难理解,主要基于 y_true 是目标的 one hot 输入,这样一来,可以通过普通的乘法来取出目标的 cos 值,减去 margin 后再补回其他部分。 

如果仅仅是玩个 mnist 这样的 10 分类,那么上述代码完全足够了。但在人脸识别或句子相似度场景,我们面对的事实上是数万分类甚至数十万的分类,这种情况下如果还是用 one ho t输入,就显得非常消耗内存了(主要是准备数据时也麻烦一些)。

理想情况下,我们希望 y_true 只要输入对应分类的整数id。对于普通的交叉熵,Keras 也提供了 sparse_categorical_crossentropy 的方案,便是应对这种需求,那么 am-softmax 能不能写个 Sparse 版出来呢? 

一种比较简单的写法是,将转换 one hot 的过程写入到 loss 中,比如:

def sparse_amsoftmax_loss(y_true, y_pred, scale=30, margin=0.35):
    y_true = K.cast(y_true[:, 0], 'int32') # 保证y_true的shape=(None,), dtype=int32
    y_true = K.one_hot(y_true, K.int_shape(y_pred)[-1]) # 转换为one hot
    y_pred = y_true * (y_pred - margin) + (1 - y_true) * y_pred
    y_pred *= scale
    return K.categorical_crossentropy(y_true, y_pred, from_logits=True)

这样确实能达成目的,但只不过对问题进行了转嫁,并没有真正跳过转 one hot。我们可以用 TensorFlow 的 gather_nd 函数,来实现真正地跳过转 one hot 的过程,下面是参考的代码:

def sparse_amsoftmax_loss(y_true, y_pred, scale=30, margin=0.35):
    y_true = K.expand_dims(y_true[:, 0], 1) # 保证y_true的shape=(None, 1)
    y_true = K.cast(y_true, 'int32') # 保证y_true的dtype=int32
    batch_idxs = K.arange(0, K.shape(y_true)[0])
    batch_idxs = K.expand_dims(batch_idxs, 1)
    idxs = K.concatenate([batch_idxs, y_true], 1)
    y_true_pred = K.tf.gather_nd(y_pred, idxs) # 目标特征,用tf.gather_nd提取出来
    y_true_pred = K.expand_dims(y_true_pred, 1)
    y_true_pred_margin = y_true_pred - margin # 减去margin
    _Z = K.concatenate([y_pred, y_true_pred_margin], 1) # 为计算配分函数
    _Z = _Z * scale # 缩放结果,主要因为pred是cos值,范围[-1, 1]
    logZ = K.logsumexp(_Z, 1, keepdims=True) # 用logsumexp,保证梯度不消失
    logZ = logZ + K.log(1 - K.exp(scale * y_true_pred - logZ)) # 从Z中减去exp(scale * y_true_pred)
    return - y_true_pred_margin * scale + logZ

这个代码会比前一个带 one hot 的代码要略微快一些。实现的关键是用 tf.gather_nd 把目标列提取出来,然后用 logsumexp 计算对数配分函数,这估计是实现交叉熵的标准方法了。基于此,可以修改为其它形式的 margin softmax loss。现在就可以像 sparse_categorical_crossentropy 一样只输入类别 id 了,其它框架也可以参照着实现。 

效果预览

一个完整的句子相似度模型可以在这里浏览: 

https://github.com/bojone/margin-softmax/blob/master/sent_sim.py 

这是一个基于字的模型,所用到的语料 tongyiju.csv 如图(语料不共享,需要运行的读者请自行按照格式准备语料):

▲ 句子相似度语料格式

其中前面的 id 表示句子组别,用 \t 隔开,同一组的句子可以认为都是同一句,不同组的句子则是非同义句。

训练结果:训练集的分类问题上,能达到 90%+ 的准确率,而验证集(evaluate 函数)上,几种 loss 的 top1、top5、top10 的准确率分别为(没有精细调参):

值得一提的是,evaluate 函数完全是按照真实的使用环境测试的,也就是说,验证集的每一个句子都没有出现过在训练集中,运行 evaluate 函数时,仅仅是在验证集内部进行排序,如果按相似度排序后的前 n 个句子中出现了输入句子的同义句,那么 top n 的命中数就加 1。

因此,这样看来,准确率是很可观的,能满足工程使用了。下面是随便挑几个匹配的例子:

结论

本文阐述了笔者对句子相似度模型的理解,认为它的最佳做法并非二分类,也并非 triplet loss,而是模仿人脸识别中的 margin loss 来做,这是能最快速提升效果的方案。当然,我并没有充分比较各种方法,仅仅是从我自己对人脸识别的粗浅了解中觉得应该是那样。欢迎读者测试并一同讨论。

参考文献

[1]. Paul Neculoiu, Maarten Versteegh, Mihai Rotaru: Learning Text Similarity with Siamese Recurrent Networks. Rep4NLP@ACL 2016: 148-157

[2]. Feng, Minwei, et al. "Applying deep learning to answer selection: A study and an open task." 2015 IEEE Workshop on Automatic Speech Recognition and Understanding (ASRU). IEEE, 2015.

[3]. Liu W, Wen Y, Yu Z, et al. Large-Margin Softmax Loss for Convolutional Neural Networks[C]//Proceedings of The 33rd International Conference on Machine Learning. 2016: 507-516.

工程GRUam-softmax人脸识别句子相似度模型
1
相关数据
神经网络技术
Neural Network

(人工)神经网络是一种起源于 20 世纪 50 年代的监督式机器学习模型,那时候研究者构想了「感知器(perceptron)」的想法。这一领域的研究者通常被称为「联结主义者(Connectionist)」,因为这种模型模拟了人脑的功能。神经网络模型通常是通过反向传播算法应用梯度下降训练的。目前神经网络有两大主要类型,它们都是前馈神经网络:卷积神经网络(CNN)和循环神经网络(RNN),其中 RNN 又包含长短期记忆(LSTM)、门控循环单元(GRU)等等。深度学习是一种主要应用于神经网络帮助其取得更好结果的技术。尽管神经网络主要用于监督学习,但也有一些为无监督学习设计的变体,比如自动编码器和生成对抗网络(GAN)。

分类问题技术
Classification

分类问题是数据挖掘处理的一个重要组成部分,在机器学习领域,分类问题通常被认为属于监督式学习(supervised learning),也就是说,分类问题的目标是根据已知样本的某些特征,判断一个新的样本属于哪种已知的样本类。根据类别的数量还可以进一步将分类问题划分为二元分类(binary classification)和多元分类(multiclass classification)。

收敛技术
Convergence

在数学,计算机科学和逻辑学中,收敛指的是不同的变换序列在有限的时间内达到一个结论(变换终止),并且得出的结论是独立于达到它的路径(他们是融合的)。 通俗来说,收敛通常是指在训练期间达到的一种状态,即经过一定次数的迭代之后,训练损失和验证损失在每次迭代中的变化都非常小或根本没有变化。也就是说,如果采用当前数据进行额外的训练将无法改进模型,模型即达到收敛状态。在深度学习中,损失值有时会在最终下降之前的多次迭代中保持不变或几乎保持不变,暂时形成收敛的假象。

计算机视觉技术
Computer Vision

计算机视觉(CV)是指机器感知环境的能力。这一技术类别中的经典任务有图像形成、图像处理、图像提取和图像的三维推理。目标识别和面部识别也是很重要的研究领域。

交叉熵技术
Cross-entropy

交叉熵(Cross Entropy)是Loss函数的一种(也称为损失函数或代价函数),用于描述模型预测值与真实值的差距大小

人脸识别技术
Facial recognition

广义的人脸识别实际包括构建人脸识别系统的一系列相关技术,包括人脸图像采集、人脸定位、人脸识别预处理、身份确认以及身份查找等;而狭义的人脸识别特指通过人脸进行身份确认或者身份查找的技术或系统。 人脸识别是一项热门的计算机技术研究领域,它属于生物特征识别技术,是对生物体(一般特指人)本身的生物特征来区分生物体个体。

机器学习技术
Machine Learning

机器学习是人工智能的一个分支,是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、计算复杂性理论等多门学科。机器学习理论主要是设计和分析一些让计算机可以自动“学习”的算法。因为学习算法中涉及了大量的统计学理论,机器学习与推断统计学联系尤为密切,也被称为统计学习理论。算法设计方面,机器学习理论关注可以实现的,行之有效的学习算法。

参数技术
parameter

在数学和统计学裡,参数(英语:parameter)是使用通用变量来建立函数和变量之间关系(当这种关系很难用方程来阐述时)的一个数量。

语音识别技术
Speech Recognition

自动语音识别是一种将口头语音转换为实时可读文本的技术。自动语音识别也称为语音识别(Speech Recognition)或计算机语音识别(Computer Speech Recognition)。自动语音识别是一个多学科交叉的领域,它与声学、语音学、语言学、数字信号处理理论、信息论、计算机科学等众多学科紧密相连。由于语音信号的多样性和复杂性,目前的语音识别系统只能在一定的限制条件下获得满意的性能,或者说只能应用于某些特定的场合。自动语音识别在人工智能领域占据着极其重要的位置。

验证集技术
Validation set

验证数据集是用于调整分类器超参数(即模型结构)的一组数据集,它有时也被称为开发集(dev set)。

深度学习技术
Deep learning

深度学习(deep learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。 深度学习是机器学习中一种基于对数据进行表征学习的算法,至今已有数种深度学习框架,如卷积神经网络和深度置信网络和递归神经网络等已被应用在计算机视觉、语音识别、自然语言处理、音频识别与生物信息学等领域并获取了极好的效果。

张量技术
Tensor

张量是一个可用来表示在一些矢量、标量和其他张量之间的线性关系的多线性函数,这些线性关系的基本例子有内积、外积、线性映射以及笛卡儿积。其坐标在 维空间内,有 个分量的一种量,其中每个分量都是坐标的函数,而在坐标变换时,这些分量也依照某些规则作线性变换。称为该张量的秩或阶(与矩阵的秩和阶均无关系)。 在数学里,张量是一种几何实体,或者说广义上的“数量”。张量概念包括标量、矢量和线性算子。张量可以用坐标系统来表达,记作标量的数组,但它是定义为“不依赖于参照系的选择的”。张量在物理和工程学中很重要。例如在扩散张量成像中,表达器官对于水的在各个方向的微分透性的张量可以用来产生大脑的扫描图。工程上最重要的例子可能就是应力张量和应变张量了,它们都是二阶张量,对于一般线性材料他们之间的关系由一个四阶弹性张量来决定。

TensorFlow技术
TensorFlow

TensorFlow是一个开源软件库,用于各种感知和语言理解任务的机器学习。目前被50个团队用于研究和生产许多Google商业产品,如语音识别、Gmail、Google 相册和搜索,其中许多产品曾使用过其前任软件DistBelief。

准确率技术
Accuracy

分类模型的正确预测所占的比例。在多类别分类中,准确率的定义为:正确的预测数/样本总数。 在二元分类中,准确率的定义为:(真正例数+真负例数)/样本总数

PaperWeekly
PaperWeekly

PaperWeekly 是一个推荐、解读、讨论和报道人工智能前沿论文成果的学术平台,

PaperWeekly
PaperWeekly

推荐、解读、讨论和报道人工智能前沿论文成果的学术平台。

推荐文章
返回顶部