一、前言
2020年注定是会被史学家们大书特书的一年,身为2020年各种“奇观”的见证者,不留下点什么怎么对得起这段历史?所以接下来,从这篇最简单的文章开始,我希望能给自己的2020留下点更难忘的记忆。
二、孪生网络介绍
孪生神经网络,英文名Siamese Network,Siamese译作“暹罗人”或“暹罗的”(类似Chinese),那为什么Siamese Network被称为孪生神经网络呢?
图中间的两位是十九世纪出生于泰国的一对连体婴儿,因为当时的医疗技术无法将二人分开,所以二人就一直这样顽强地生活着。直到1829年他们被英国马戏团看中,开始进入马戏团进行全球巡演,因为长相奇特,迅速风靡全球。1843年兄弟俩和英国的一对姐妹结婚,并生下了22个孩子(就问你惊不惊喜?)。1874年兄弟二人因肺病在同一天去世,从此Siamese twins(暹罗双胞胎)就成了连体婴儿的代名词。那么Siamese自然也就有了“连体”、“孪生”的意思了。
下面说回正题,孪生神经网络,顾名思义,就是两个长得一模一样的神经网络,甚至如果你愿意,在代码层面,他们可以是同一个网络。一般情况下,孪生神经网络通过共享权值的方式实现“孪生”,如下图所示,就是孪生神经网络的结构图。
如图,网络部分我这里并没有画出具体的网络结构,倒不是身为灵魂画手的我故意偷懒,而是孪生神经网络并不是CNN或LSTM等某种具体的神经网络,而是一种设计神经网络的框架结构或思想,具体实现上可以使用任何一种神经网络进行“孪生”,只要该网络符合上述结构即可。
三、孪生网络的计算过程
孪生神经网络的主要作用从它的名字就可以猜出来,总结起来就4个字:求相似度。所以网络的输入和输出也就很好理解了。
孪生神经网络有两个输入向量,我们将这两个向量分别喂到两个共享权值的神经网络中,从而得到两个新的向量映射,通过计算两个新向量的相似度,然后估算损失,更新网络。
简单说就是把两个输入(两句话,两张图片,两段语音...)映射到一个新的空间,然后计算新空间中二者的距离,更新权值,使相似输入的距离在该空间中更接近。
根据不同的应用场景,上述计算过程可能会发生一些变化,比如相似度的计算需要根据不同场景选择不同的算法,共享网络的设计也要根据数据量和应用场景灵活选择。
三、孪生网络的应用场景
上面已经提过,孪生神经网络主要是用来衡量两个输入的相似度,所以不难想象,孪生神经网络的应用场景是非常广泛的。在所有需要计算相似度的场景里,它都可以派上用场。特别是在样本数据不足,数据集比较小的情况下,孪生神经网络一般总能取得比其它同类算法更好的效果。
具体的应用场景,比如:
- 在QA系统中计算两个词或两个句子的相似度
- 在人脸识别系统中比对两张人脸的相似度(指纹识别,声纹识别同理)
- 在量化系统中查找和现在最匹配的历史走势(这是我猜的,理论上应该可行)
- ......
四、当孪生网络遇上BERT
说了这么多,接下来,就以一个QA系统的实例演示一下Siamese network的实际应用。因为这不是一个构建QA系统的教程,所以下面我会省略很多QA系统基础方面的内容,继续阅读之前,需要对QA有一个基本的了解。
这里我们会采用BERT进行句子的编码,因为BERT太过有名,这里就不多做介绍了,后面我会单独写一篇文章,详细介绍一下BERT的大恩人,在工业界大有一统江湖之气势的Transformer及其设计精妙的self-attention机制。
1. 样本创建
数据集不同,创建的方法也不一样。但最终都是要将数据创建为如下形式:
input1 | input2 | label |
---|---|---|
打工这方面 | 打工是不可能打工的 | 0 |
这辈子都不可能 | 永远都不可能 | 1 |
窃格瓦拉 | 周立齐 | 1 |
... | ... | ... |
注:“1”代表相似,“0”代表不相似
这里有3点需要注意:
- 要注意正、负样本的均衡问题。因为是QA对,所以负样本的数量会比正样本多的多,因此这里建议,正样本使用全采样,负样本使用随机采样,最后正负样本的比例最好保持在1:1
- 如果条件允许,建议在样本创建阶段就用BERT对句子进行编码,可以提高训练的速度和降低训练复杂度。
- 在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()
创建完成后,模型结构如上图所示
3. 学习曲线
有BERT加持的孪生网络分类效果是不是很逆天?当然,除了模型本身比较优秀以外,还有一个不可忽略的事实是我使用的数据集只针对某一领域,且每一条数据都是经过人工标注的,数据质量比较高。
五、总结
孪生神经网络是一种简单但非常有效的用于相似度计算的网络,它适用于各种场景下的相似度计算,特别是在样本数量比较少的情况下,孪生神经网络一般总能取得比其它同类算法更好的效果。在文本相似度领域,有了BERT的加持,孪生神经网络相比传统的Cosine Similarity(余弦相似度),在分类效果上的提升更加显著。