Keras* 实现类似 Siamese 的网络
本指南将帮助您在 Keras 中编写复杂的神经网络,例如 Siamese 网络。它还解释了在 Keras 中编写自定义层的过程。
摘要
深度学习彻底改变了机器学习领域。卷积神经网络 (CNN) 在解决图像识别、图像重建和各种其他计算机视觉问题方面变得非常流行。TensorFlow* 和 Keras* 等库使程序员的工作更加轻松。但是,这些库并不直接支持复杂网络和不常用层。本指南将帮助您在 Keras 中编写复杂的神经网络,例如 Siamese 网络。它还解释了在 Keras 中编写自定义层的过程。
引言
人员重识别定义为识别给定图像对中是否存在同一个人。在处理此问题时面临的一些挑战是由于照片从不同视角拍摄以及光照强度变化导致不同人的照片看起来相似,从而产生误报。归一化 X 相关模型1 用于解决人员重识别问题。本指南演示了使用 Keras 实现归一化 X 相关模型的步骤,该模型是 Siamese 网络2 的一种变体。
归一化 X 相关模型概述
Arulkumar Subramaniam 及其同事1 提出了一种用于解决二元分类问题的深度神经网络。图 1 概述了归一化 X 相关 (normxcorr) 模型。首先,图像通过 conv-pool-conv-pool 层进行特征提取。使用这些层的想法是为了提取图像的特征,因此 conv 层的权重是共享的(即,两个图像都通过相同的层)。提取特征后,有必要建立特征之间的相似性。这由归一化相关层完成,该层是一个自定义层,将在本指南稍后讨论。该层基本上获取一个小的 5×5 patch,然后围绕另一个特征图进行卷积,并计算归一化相关值,如下所示:
我们将特征图表示为属于图像的 X 和 Y。考虑到图 1 中的尺寸,我们从 X 图中以给定深度为 (x,y) 的中心取一个 patch,并与 Y(a,b) 计算 normxcorr,其中 1 <= a <= 12 且 y – 2 <= b <= y + 2。因此,对于每个 X(x,y),会生成 5×12=60 个值,并沿输出特征图的深度存储。这会在所有深度进行,因此我们得到输出尺寸为 12×37×1500(即 60×25)。
在图 2 中,为了演示起见,图像大小假定为 8×8。如果我们考虑图像 1 中以红色方块标记的 5×5 大小的块为中心的 patch,我们将计算此 patch 与图像 2 中绿色方块标记的 patch(即沿图像的整个宽度)之间的归一化 X 相关,以及高度在 [3 - 2, 3 + 5] 之间,即 [1,5]。因此,单个 patch 在图像 1 中生成的总值数量是允许的宽度×高度(即 8×5=40)。这些值沿输出特征图的深度存储。因此,对于一个 patch,我们生成一个 1×1×40 的输出。考虑到整个图像,我们将得到一个 8×8×40 大小的特征图。但是,如果输入有多个通道,则计算出的特征图会堆叠在一起。因此,输出特征图的高度和宽度保持不变,但深度会乘以输入图像的深度。因此,8×8×5 的输入图像将生成 8×8×(40×5)(即 8×8×200)的输出特征图。对于以蓝色块为中心的 patch,我们看到为了满足标准,我们需要添加填充。因此,在这种情况下,图像会用零填充。
在归一化 X 相关层之后,添加了两个 conv 层和池化层,以简洁地整合更多的上下文信息。在此之上,添加了两个全连接层,并应用了 softmax 激活函数。
有关架构的更多信息,请参阅论文“深度神经网络与非精确匹配用于人员重识别”。
深入代码
以下代码在 Intel® AI DevCloud 上进行了测试。还使用了以下库和框架:Python* 3(2018 年 2 月版)、Keras*(版本 2.1.2)、Intel® 针对 TensorFlow* 的优化(版本 1.3.0)、NumPy(版本 1.14.0)。
import keras
import sys
from keras import backend as K
from keras.layers import Conv2D, MaxPooling2D, Dense,Input, Flatten
from keras.models import Model, Sequential
from keras.engine import InputSpec, Layer
from keras import regularizers
from keras.optimizers import SGD, Adam
from keras.utils.conv_utils import conv_output_length
from keras import activations
import numpy as np
这些是我们在此模型中需要实现的一些 Keras 和其他库的导入。
a = Input((160,60,3))
b = Input((160,60,3))
这些为输入图像创建了占位符。
model = Sequential()
model.add(Conv2D(kernel_size = (5,5), filters = 20,input_shape = (160,60,3), activation = 'relu'))
model.add(MaxPooling2D((2,2)))
model.add(Conv2D(kernel_size = (5,5), filters = 25, activation = 'relu'))
model.add(MaxPooling2D((2,2)))
这些是需要在图像之间共享的层。因此,我们创建了一个这些层的模型。
feat_map1 = model(b)
feat_map2 = model(a)
model(a)
将其接收到的输入传递给模型并返回输出层。对两个层都执行此操作,以便它们共享相同的模型并输出两个特征图 feat_map1
和 feat_map2
。
normalized_layer = Normalized_Correlation_Layer(stride = (1,1), patch_size = (5, 5))([feat_map1, feat_map2])
这是自定义层,用于建立从图像提取的特征图之间的相似性。我们将特征图作为列表输入。其实现将在本指南稍后提到。
final_layer = Conv2D(kernel_size=(1,1), filters=25, activation='relu')(normalized_layer)
final_layer = Conv2D(kernel_size=(3,3), filters=25, activation = None)(final_layer)
final_layer = MaxPooling2D((2,2))(final_layer)
final_layer = Dense(500)(final_layer)
final_layer = Dense(2, activation = "softmax")(final_layer)
这些是在归一化相关层之上添加的层。
x_corr_mod = Model(inputs=[a,b], outputs = final_layer)
最后,创建了一个新模型,其输入是作为列表传递的图像,该模型会产生一个二元输出。
该模型的层可视化可以在论文“人员重识别的非精确匹配深度神经网络论文补充材料”中找到。
归一化相关层
这不是 Keras 提供的层,因此我们必须使用 Keras 后端提供的支持自己编写该层。
class Normalized_Correlation_Layer(Layer):
create a class inherited from keras.engine.Layer.
def __init__(self, patch_size=(5,5),
dim_ordering='tf',
border_mode='same',
stride=(1, 1),
activation=None,
**kwargs):
if border_mode != 'same':
raise ValueError('Invalid border mode for Correlation Layer '
'(only "same" is supported as of now):', border_mode)
self.kernel_size = patch_size
self.subsample = stride
self.dim_ordering = dim_ordering
self.border_mode = border_mode
self.activation = activations.get(activation)
super(Normalized_Correlation_Layer, self).__init__(**kwargs)
此构造函数仅将作为参数传递的值设置为类变量,并通过调用构造函数来初始化其父类。
def compute_output_shape(self, input_shape):
return(input_shape[0][0], input_shape[0][1], input_shape[0][2],
self.kernel_size[0] * input_shape[0][2]*input_shape[0][-1])
此函数返回该层输出的特征图的形状作为元组。第一个元素是图像数量,第二个是行数,第三个是列数,最后一个是深度,即高度移动范围×宽度移动范围×深度。在我们的例子中是 5×12×25。
def get_config(self):
config = {'patch_size': self.kernel_size,
'activation': self.activation.__name__,
'border_mode': self.border_mode,
'stride': self.subsample,
'dim_ordering': self.dim_ordering}
base_config = super(Correlation_Layer, self).get_config()
return dict(list(base_config.items()) + list(config.items()))
此函数将作为参数传递的配置添加到构造函数中,将其附加到父类的配置之后,然后返回它。Keras 在获取配置时会调用此函数。
def call(self, x, mask=None):
此函数在每次迭代时调用。此函数根据模型接收输入作为特征图。
input_1, input_2 = x
stride_row, stride_col = self.subsample
inp_shape = input_1._keras_shape
从列表中分离输入并将一些变量加载到局部变量中,以便稍后更容易引用。
output_shape = self.compute_output_shape([inp_shape, inp_shape])
这使用了前面编写的函数来获取所需的输出形状并将其存储在变量中。
padding_row = (int(self.kernel_size[0] / 2),int(self.kernel_size[0] / 2))
padding_col = (int(self.kernel_size[1] / 2),int(self.kernel_size[1] / 2))
input_1 = K.spatial_2d_padding(input_1, padding =(padding_row,padding_col))
input_2 = K.spatial_2d_padding(input_2, padding = ((padding_row[0]*2, padding_row[1]*2),padding_col))
此代码块为特征图添加填充。这是必需的,因为我们也以 (0,0) 和其他边缘为中心提取 patch。因此,在我们的情况下,我们需要添加 2 的填充。但是,对于第二个输入的特征图,我们需要以第一个特征图中心 patch 的偏移量 2 来提取 patch。因此,对于 (0, 0) 处的 patch,我们需要考虑第二个特征图的 (0,0)、(0,1)、(0,2)、(0,-1)、(0,-2) 处的 patch,并且 X 处的值相同。因此,我们需要添加 4 的填充,
output_row = output_shape[1]
output_col = output_shape[2]
并将它们存储在变量中。
output = []
for k in range(inp_shape[-1]):
循环遍历所有深度。
xc_1 = []
xc_2 = []
for i in range(padding_row[0]):
for j in range(output_col):
xc_2.append(K.reshape(input_2[:, i:i+self.kernel_size[0], j:j+self.kernel_size[1], k],
(-1, 1,self.kernel_size[0]*self.kernel_size[1])))
这是针对特征图 2 的 patch 完成的,我们在其中添加了额外的填充(即,不是以特征图为中心的 patch,并且位于第一行)。
for i in range(output_row):
slice_row = slice(i, i + self.kernel_size[0])
slice_row2 = slice(i + padding_row[0], i +self.kernel_size[0] + padding_row[0])
for j in range(output_col):
slice_col = slice(j, j + self.kernel_size[1])
xc_2.append(K.reshape(input_2[:, slice_row2, slice_col, k],
(-1, 1,self.kernel_size[0]*self.kernel_size[1])))
xc_1.append(K.reshape(input_1[:, slice_row, slice_col, k],
(-1, 1,self.kernel_size[0]*self.kernel_size[1])))
从两个特征图中提取 5×5 大小的 patch,并分别存储在 xc_1
和 xc_2
中。在这种情况下,这些 patch 被展平并重塑为 (-1,1,25) 的形式。
for i in range(output_row, output_row+padding_row[1]):
for j in range(output_col):
xc_2.append(K.reshape(input_2[:, i:i+ self.kernel_size[0], j:j+self.kernel_size[1], k],
(-1, 1,self.kernel_size[0]*self.kernel_size[1])))
这是提取特征图 2 的 patch,但这些 patch 以特征图底部为中心。
xc_1_aggregate = K.concatenate(xc_1, axis=1)
这些 patch 沿着 axis=1
连接,以便对于任何给定的深度,它们都具有 (-1, 60, 25) 的形状。
xc_1_mean = K.mean(xc_1_aggregate, axis=-1, keepdims=True)
xc_1_std = K.std(xc_1_aggregate, axis=-1, keepdims=True)
xc_1_aggregate = (xc_1_aggregate - xc_1_mean) / xc_1_std
这只是第一个特征图特征归一化的实现。
xc_2_aggregate = K.concatenate(xc_2, axis=1)
xc_2_mean = K.mean(xc_2_aggregate, axis=-1, keepdims=True)
xc_2_std = K.std(xc_2_aggregate, axis=-1, keepdims=True)
xc_2_aggregate = (xc_2_aggregate - xc_2_mean) / xc_2_std
同样,对于第二个图像的特征图。
xc_1_aggregate = K.permute_dimensions(xc_1_aggregate, (0, 2, 1))
block = []
len_xc_1= len(xc_1)
for i in range(len_xc_1):
#This for loop is to compute the product of a given patch of feature map 1
#and the feature maps on which it is supposed to
sl1 = slice(int(i/inp_shape[2])*inp_shape[2],
int(i/inp_shape[2])*inp_shape[2]+inp_shape[2]*self.kernel_
size[0])
#This calculates which are the patches of feature map 2 to be considered
#for a given patch of first feature map.
block.append(K.reshape(K.batch_dot(xc_2_aggregate[:,sl1,:],
xc_1_aggregate[:,:,i]),(-1,1,1,inp_shape[2] *self.kernel_size[0])))
计算点积(即归一化相关性)并将其存储在“block”中。
block = K.concatenate(block, axis=1)
block= K.reshape(block,(-1,output_row,output_col,inp_shape[2] *self.kernel_size[0]))
output.append(block)
连接计算出的归一化相关值,对其进行重塑(它们是顺序计算的,因此重塑会更容易),然后将其附加到“output”。
output = K.concatenate(output, axis=-1)
沿“output”的深度连接在每个深度计算的输出特征图。
output = self.activation(output)
return output
如果将激活作为参数发送,则应用激活并返回生成的输出。
应用
这种网络可以有多种应用,例如在犯罪现场匹配人的身份。该网络可以泛化以查找两幅图像之间的相似性(即,查找两幅图像中是否存在相同的水果)。
进一步范围
代码是顺序运行的,缺乏并行性。可以使用 multiprocessing 等库在多个核心上并行化 patch 的矩阵乘法。这将有助于加快训练时间。通过找到图像 patch 之间更合适的相似性度量,可以提高模型的准确性。
致谢
我想感谢 Intel® Student Ambassador Program for AI,该计划为我提供了必要的培训资源,包括Intel® AI DevCloud 以及帮助我使用 DevCloud 的技术支持。
参考文献
- Subramaniam, M. Chatterjee, and A. Mittal. “深度神经网络与非精确匹配用于人员重识别.” In NIPS 2016.
- Dong Yi, Zhen Lei, Shengcai Liao, Stan Z. Li. “人员重识别的深度度量学习.” In ICPR, volume 2014.
- GitHub* 上的代码
有关编译器优化的更完整信息,请参阅我们的优化通知。