iPhone X面部识别-用TensorFlow中的Siamese Networks解释
背景
在机器学习中,输入数据的质量常常超过模型体系结构和训练方案。与在低质量数据集中训练的更高级、更高调优的模型相比,拥有正确标记的高质量图像的大型、平衡数据集总是会带来更好的模型性能。然而,数据科学家并不总是能收到很好的数据集。机器学习的最大瓶颈是数据集的创建。
Apple已经建立了一个自动面部识别系统 - 仅举几个面部训练示例,它能够在测试中以极高的准确度正确识别个人。有几种模型协同工作以实现这一目标。以下是最重要的两个 - 1)能够区分真人和其他证件(如人的照片)的模型,2)能够区分人的模型。鉴于iPhone用户数量众多(以1000万为例),Apple如何建立一个足够强大的系统来识别一个人呢?
解决这个问题的第一步是数据扩充。通过旋转、模糊或裁剪图像数据,可以创建近似反映原始数据集中图像分布的合成图像。然而,这种方法不是完美的,这提供了一种规则化效果,如果网络在训练中已经不能很好地执行,则可能是不想要的。
第二步,也是更重要的一步,是使用一种称为siamese网络的通用架构。这里讨论的siamese网络的特殊特点是使用具有共享参数的模型为每个图像构建特征表示。损失的定义如下——如果两幅图像属于同一类,那么当它们相关的特征向量之间的距离较低时损失较小,当它们相关的特征向量之间的距离较高时损失较大。反之,如果两个图像属于不同的类,那么当图像特征表示相差很远时,损失就很小。该模型体系结构构建用于处理这两种情况:
- 稀疏数据
- 新数据
该体系结构已在苹果、百度等公司的全球部署。
现在,让我们在tensorflow中构建模型!
输入管道
由于暹罗(siamese)网络的严格输入参数,输入管道可能是我们编写的最复杂的代码段。基本要求是我们必须选择两个输入图像。这些输入图像必须具有相同的概率,属于同一类或不同的类。
虽然tensorflow中的健壮数据集类可以简单而有效地提供图像数据,但它并不是为siamese网络需求而设计的,因此需要一些技巧来实现。
为了提高效率,在下面的代码中,我从TFRecords格式读取图像数据。图像数据与tensorflow的数据集类一样容易
dataset = tf.data.Dataset.from_tensor_slices()
第一步是创建包含所有图像数据的数据集,Python代码如下:
def make_single_dataset(image_size, tfrecords_path="", shuffle_buffer_size=2000, repeat=True, train=True):
'''
Input:
image_size: size of input images to network
tfrecords_path: address to tfrecords file containing all image data
shuffle_buffer_size: number of images to load into a memory for a shuffling operation.
repeat (boolean): repeat dataset
train (boolean): use in training
Features:
image: image tensor
label: label tensor
height: original image height
width: original image width
addr: image address in file system
Returns:
Dataset
'''
image_size = tf.cast(image_size, tf.int32)
def _parse_function(example_proto):
features = {'/label': tf.FixedLenFeature((), tf.int64, default_value=1),
'/image': tf.FixedLenFeature((), tf.string, default_value=""),
'/height': tf.FixedLenFeature([], tf.int64),
'/width': tf.FixedLenFeature([], tf.int64),
'/addr': tf.FixedLenFeature((), tf.string, default_value="")}
parsed_features = tf.parse_single_example(example_proto, features)
image_buffer = parsed_features['/image']
image = tf.image.decode_jpeg(image_buffer,channels=3)
image = tf.cast(image, tf.float32)
S = tf.stack([tf.cast(parsed_features[set_name + '/height'], tf.int32),
tf.cast(parsed_features[set_name + '/width'], tf.int32), 3])
image = tf.reshape(image, S)
image = tf.image.convert_image_dtype(image, tf.float32)
image = tf.image.resize_images(image, [image_size, image_size])
return image, parsed_features['/label'], parsed_features['/addr']
filenames = [tfrecords_path]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.shuffle(shuffle_buffer_size)
dataset = dataset.map(_parse_function, num_parallel_calls=8)
return dataset
数据被一些缓冲区大小shuffled (较大的值增加时间,较小的值减少随机性),并映射到各自的图像和标签张量。回想一下,所有这些操作都是tensorflow graph的一部分。
接下来,必须将上述结构的两个数据集压缩到一起来创建siamese数据集。我们对这个数据集有一些体系结构和性能需求
- 数据必须re-shuffled
- 数据必须均匀分布
- 开销应该是分布式和并发
通过使用tensorflow的prefetch()、filter()和shuffle()实用程序,可以实现这一点。 - 在过滤之前重复操作可能导致无限循环!prefetch()操作允许CPU在训练gpu时加载一批图像。Python代码如下:
def combine_dataset(batch_size, image_size, same_prob, diff_prob, repeat=True, train=True):
'''
Input:
image size (int)
batch_size (int)
same_prob (float): probability of retaining images in same class
diff_prob (float): probability of retaining images in different class
train (boolean): train or validation
repeat (boolean): repeat elements in dataset
Return:
zipped dataset
'''
dataset_left = make_single_dataset(image_size, repeat=repeat, train=train)
dataset_right = make_single_dataset(image_size, repeat=repeat, train=train)
dataset = tf.data.Dataset.zip((dataset_left, dataset_right))
if train:
filter_func = create_filter_func(same_prob, diff_prob)
dataset = dataset.filter(filter_func)
if repeat:
dataset = dataset.repeat()
dataset = dataset.batch(batch_size)
dataset = dataset.prefetch(1)
return dataset
由于我们已经在tensorflow graph中工作,所以所有操作都必须由tensorflow支持。我在这里定义了filter_func。该操作根据图像对是否同时存在于同一数据集中随机筛选图像对。Python实现如下:
def create_filter_func(same_prob, diff_prob):
def filter_func(left, right):
_, right_label, _ = left
_, left_label, _ = right
label_cond = tf.equal(right_label, left_label)
different_labels = tf.fill(tf.shape(label_cond), diff_prob)
same_labels = tf.fill(tf.shape(label_cond), same_prob)
weights = tf.where(label_cond, same_labels, different_labels)
random_tensor = tf.random_uniform(shape=tf.shape(weights))
return all_weights > random_tensor
return filter_func
我们有我们的数据集!现在,我们建模。
模型
让我们定义一个基本模型。首先,我们可以定义卷积块。Python代码如下:
def conv2d(self, prev, filters, kernel_size, strides=1, padding="SAME", name="conv2d", reuse=False, batch_norm=True, maxpool=True):
layer = tf.layers.conv2d(prev, filters, [kernel_size, kernel_size], strides=strides, padding=padding,
kernel_regularizer=tf.contrib.layers.l2_regularizer(scale=0.1), name=name, reuse=reuse)
if maxpool:
layer = tf.layers.max_pooling2d(layer, pool_size=2, strides=2, padding='valid')
if batch_norm:
layer = tf.layers.batch_normalization(layer, fused=True)
layer = tf.nn.relu(layer)
return layer
通过这些卷积块,我们可以定义一个模型。给定一个输入图像,该模型返回一个flattened 1024-unit特征向量。Python代码如下:
def model(input_image):
'''
Input:
input_image: Tensor of shape (batch_size, image_size, image_size, 3)
Returns:
Feature Vector of shape
'''
prev = input_image
filters = [256, 128, 64, 32]
for i in range(4):
prev = conv2d(prev, filters[i], 3)
flatten = tf.layers.flatten(prev)
single_output = tf.layers.dense(flatten, 1024, activation=tf.sigmoid)
return single_output
回想一下,在一个siamese网络中,两个图像都是由同一个模型处理的。因此,我们可以在tensorflow中设置一个可变范围,允许我们在处理每一对输入时共享参数。Python代码如下:
def inference(left_input_image, right_input_image)
'''
left_input_image: 3D tensor input
right_input_image: 3D tensor input
label: 1 if images are from same category. 0 if not.
'''
with tf.variable_scope('feature_generator', reuse=tf.AUTO_REUSE) as sc:
left_features = model(left_input_image)
right_features = model(right_input_image)
merged_features = tf.abs(tf.subtract(left_features, right_features))
logits = tf.contrib.layers.fully_connected(merged_features, num_outputs=1, activation_fn=None)
logits = tf.reshape(logits, [-1])
return logits
一个densely connected层被附加到特征fector的绝对差异。需要注意的是,没有激活函数应用于这个最终的向量——这允许模型捕获输出的本机分布,并允许所有的神经元被表示。
最后,我们必须界定暹罗损失。在这个siamese网络的特殊变体中,我演示了对比损失——另一种流行的损失是Triplet loss。然而,Triplet loss需要与上面描述的不同的模型架构。
def loss(logits, left_label, right_label):
label = tf.equal(left_label, right_label)
label = tf.cast(label, tf.int32)
cross_entropy_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=logits, labels=label))
tf.losses.add_loss(cross_entropy_loss)
训练
首先,我们调用我们的数据集,并在图表中实例化张量,表示我们的左右输入图像及其相应的标签。Python代码如下:
train_dataset = combine_dataset(batch_size=128, image_size=224, same_prob=0.95, diff_prob=.001, train=True)
val_dataset = combine_dataset(batch_size=128, image_size=224, same_prob=0.95, diff_prob=.001, train=False)
handle = tf.placeholder(tf.string, shape=[])
iterator = tf.data.Iterator.from_string_handle(
handle, train_dataset.output_types, train_dataset.output_shapes)
train_iterator = train_dataset.make_one_shot_iterator()
val_iterator = val_dataset.make_one_shot_iterator()
left, right = self.iterator.get_next()
left_input_im, left_label, left_addr = left
right_input_im, right_label, right_addr = right
接下来,我们在tensorflow graph中添加损失。
logits = inference(left_input_im, right_input_im)
loss(logits, left_label, right_label)
最后,我们实例化一个优化器以最小化损失,并初始化器将所有变量实例化为某些值分布(可以为每个操作指定)。我在这里使用流行的Adam优化器。
total_loss = tf.losses.get_total_loss()
global_step = tf.Variable(0, trainable=False)
params = tf.trainable_variables()
gradients = tf.gradients(total_loss, params)
optimizer = tf.train.AdamOptimizer(learning_rate=0.001)
updates = optimizer.apply_gradients(zip(gradients, params), global_step=global_step)
global_init = tf.variables_initializer(tf.global_variables())
现在,我们可以开始在tensorflow session中进行训练。
with tf.Session() as sess:
# Instantiate all variables
sess.run(global_init)
training_handle = sess.run(train_iterator.string_handle())
validation_handle = sess.run(val_iterator.string_handle())
num_epochs = 20
num_iterations = # num_images // batch_size
for epoch in range(num_epochs):
for iteration in range(num_iterations):
feed_dict_train = {handle:training_handle}
feed_dict_val = {handle:validation_handle}
train_loss, _ = sess.run([total_loss, updates], feed_dict_train)
val_loss = sess.run([total_loss], feed_dict_val)
最后
这里描述了使用低级tensorflow API的基本连体网络。该网络在解析稀疏数据集时非常强大,可用于全球范围内的全球识别系统,包括iPhone X.