气有浩然 学无止境

当孪生神经网络遇上BERT

一、前言

2020年注定是会被史学家们大书特书的一年,身为2020年各种“奇观”的见证者,不留下点什么怎么对得起这段历史?所以接下来,从这篇最简单的文章开始,我希望能给自己的2020留下点更难忘的记忆。

二、孪生网络介绍

孪生神经网络,英文名Siamese Network,Siamese译作“暹罗人”或“暹罗的”(类似Chinese),那为什么Siamese Network被称为孪生神经网络呢?

img

图中间的两位是十九世纪出生于泰国的一对连体婴儿,因为当时的医疗技术无法将二人分开,所以二人就一直这样顽强地生活着。直到1829年他们被英国马戏团看中,开始进入马戏团进行全球巡演,因为长相奇特,迅速风靡全球。1843年兄弟俩和英国的一对姐妹结婚,并生下了22个孩子(就问你惊不惊喜?)。1874年兄弟二人因肺病在同一天去世,从此Siamese twins(暹罗双胞胎)就成了连体婴儿的代名词。那么Siamese自然也就有了“连体”、“孪生”的意思了。

下面说回正题,孪生神经网络,顾名思义,就是两个长得一模一样的神经网络,甚至如果你愿意,在代码层面,他们可以是同一个网络。一般情况下,孪生神经网络通过共享权值的方式实现“孪生”,如下图所示,就是孪生神经网络的结构图。

img

如图,网络部分我这里并没有画出具体的网络结构,倒不是身为灵魂画手的我故意偷懒,而是孪生神经网络并不是CNN或LSTM等某种具体的神经网络,而是一种设计神经网络的框架结构或思想,具体实现上可以使用任何一种神经网络进行“孪生”,只要该网络符合上述结构即可。

三、孪生网络的计算过程

孪生神经网络的主要作用从它的名字就可以猜出来,总结起来就4个字:求相似度。所以网络的输入和输出也就很好理解了。

孪生神经网络有两个输入向量,我们将这两个向量分别喂到两个共享权值的神经网络中,从而得到两个新的向量映射,通过计算两个新向量的相似度,然后估算损失,更新网络。

简单说就是把两个输入(两句话,两张图片,两段语音...)映射到一个新的空间,然后计算新空间中二者的距离,更新权值,使相似输入的距离在该空间中更接近。

根据不同的应用场景,上述计算过程可能会发生一些变化,比如相似度的计算需要根据不同场景选择不同的算法,共享网络的设计也要根据数据量和应用场景灵活选择。

三、孪生网络的应用场景

上面已经提过,孪生神经网络主要是用来衡量两个输入的相似度,所以不难想象,孪生神经网络的应用场景是非常广泛的。在所有需要计算相似度的场景里,它都可以派上用场。特别是在样本数据不足,数据集比较小的情况下,孪生神经网络一般总能取得比其它同类算法更好的效果。

具体的应用场景,比如:

  1. 在QA系统中计算两个词或两个句子的相似度
  2. 在人脸识别系统中比对两张人脸的相似度(指纹识别,声纹识别同理)
  3. 在量化系统中查找和现在最匹配的历史走势(这是我猜的,理论上应该可行)
  4. ......

四、当孪生网络遇上BERT

说了这么多,接下来,就以一个QA系统的实例演示一下Siamese network的实际应用。因为这不是一个构建QA系统的教程,所以下面我会省略很多QA系统基础方面的内容,继续阅读之前,需要对QA有一个基本的了解。

这里我们会采用BERT进行句子的编码,因为BERT太过有名,这里就不多做介绍了,后面我会单独写一篇文章,详细介绍一下BERT的大恩人,在工业界大有一统江湖之气势的Transformer及其设计精妙的self-attention机制。

1. 样本创建

数据集不同,创建的方法也不一样。但最终都是要将数据创建为如下形式:

input1 input2 label
打工这方面 打工是不可能打工的 0
这辈子都不可能 永远都不可能 1
窃格瓦拉 周立齐 1
... ... ...

注:“1”代表相似,“0”代表不相似

这里有3点需要注意:

  1. 要注意正、负样本的均衡问题。因为是QA对,所以负样本的数量会比正样本多的多,因此这里建议,正样本使用全采样,负样本使用随机采样,最后正负样本的比例最好保持在1:1
  2. 如果条件允许,建议在样本创建阶段就用BERT对句子进行编码,可以提高训练的速度和降低训练复杂度。
  3. 在BERT中"Rubbish in,Rubbish out"效应会更加明显,所以对于样本,要宁缺毋滥。

2. 网络构建

接下来,我们用TensorFlow2.0内置的Keras创建孪生网络。

# 计算相似度
def euclidean_distance(vects):
    x, y = vects
    sum_square = K.sum(K.square(x - y), axis=1, keepdims=True)
    return K.sqrt(K.maximum(sum_square, K.epsilon()))

def eucl_dist_output_shape(shapes):
    shape1, shape2 = shapes
    return (shape1[0], 1)

def contrastive_loss(y_true, y_pred):
    margin = 1
    square_pred = K.square(y_pred)
    margin_square = K.square(K.maximum(margin - y_pred, 0))
    return K.mean(y_true * square_pred + (1 - y_true) * margin_square)

def accuracy(y_true, y_pred):
    return K.mean(K.equal(y_true, K.cast(y_pred < 0.5, y_true.dtype)))

# 创建要孪生的网络
def create_base_network(input_shape):
    input = Input(shape=input_shape)
    x = Flatten()(input)
    x = Dense(128, activation='relu')(x)
    x = Dropout(0.1)(x)
    x = Dense(128, activation='relu')(x)
    x = Dropout(0.1)(x)
    x = Dense(128, activation='relu')(x)
    return Model(input, x)

# 创建分类网络
def create_model():
    input_shape = (768, )
    base_network = create_base_network(input_shape)

    input_a = Input(shape=input_shape)
    input_b = Input(shape=input_shape)

    processed_a = base_network(input_a)
    processed_b = base_network(input_b)

    distance = Lambda(euclidean_distance,
                      output_shape=eucl_dist_output_shape)([processed_a, processed_b])

    model = Model([input_a, input_b], distance)

    # train
    rms = RMSprop()
    model.compile(loss=contrastive_loss, optimizer=rms, metrics=[accuracy])
    return model

model = create_model()
model.summary()

img

创建完成后,模型结构如上图所示

3. 学习曲线

img

有BERT加持的孪生网络分类效果是不是很逆天?当然,除了模型本身比较优秀以外,还有一个不可忽略的事实是我使用的数据集只针对某一领域,且每一条数据都是经过人工标注的,数据质量比较高。

五、总结

孪生神经网络是一种简单但非常有效的用于相似度计算的网络,它适用于各种场景下的相似度计算,特别是在样本数量比较少的情况下,孪生神经网络一般总能取得比其它同类算法更好的效果。在文本相似度领域,有了BERT的加持,孪生神经网络相比传统的Cosine Similarity(余弦相似度),在分类效果上的提升更加显著。

六、相关论文

⬆️