本文阐述和实现了计算机视觉中的一个重要概念——语义分割。
1、语义分割
几十年来,图像分割一直是计算机视觉中的一项复杂任务。现在深度学习更容易了。图像分割与图像分类不同。在图像分类中,它只会对有特定标签的对象进行分类,如马、汽车、房子等,而图像分割算法也会对未知的对象进行分割。图像分割也被称为语义分割。
语义分割是对像素级图像的理解。换句话说,每个像素都被分配给图像中的一个特定类。
在计算机视觉上的深度学习影响之前,其他机器学习方法,如用于分割的随机森林。
近年来,卷积神经网络(CNN)在图像分类方面取得了巨大的成功。
在典型的CNN结构中,输入层后为卷积层,然后连接到全连接层,然后用softmax对图像进行分类。CNN是对图像是否有特定对象进行分类,但更难回答“图像中的对象在哪里”。这是因为全连接层不保留空间信息。以下模型是连接层空间问题的解决方案。
完全卷积网络(FCN)
池化层也是CNN除全连通层外,保存空间信息的主要问题之一。池化层能够聚集上下文,同时丢弃“where”信息。但在文本分割中,我们需要保留'where'上下文来将每个像素映射到相应的对象类。
为了解决这一问题,使用encoder-decoder架构,编码器使用池层逐渐减少空间维度,译码器逐渐恢复对象细节和空间维度。还可以使用从编码器到解码器的跳转连接来帮助解码器更好地恢复对象细节。
如上图所示,FCN是用解码器代替完全连接层的一种类型架构。
编码器
它由连接1x1卷积层而不是完全连接层的卷积层组成。
用于FCN-8的编码器是在ImageNet上预训练的用于分类的VGG16模型。
解码器
解码器是FCN的第二部分,我们使用一系列转置卷积对编码器输出进行上采样。
一个上采样层的典型实现如下:
output = tf.layers.conv2d_transpose(input, num_classes, 4, strides=(2, 2))
转置卷积层增加了4D输入张量的高度和宽度尺寸。
Skip connection
跳过连接是保留通过编码器丢失的信息的方式。来自编码器的汇聚层的输出与解码器的第一层输出相结合,使用元素加法操作。然后它被送入解码器的下一层。因此网络能够做出更精确的分段决策。
在以下示例中,我们通过elementwise addition(tf.add)将前一层的结果与第四个池化结果结合起来。
# make sure the shapes are the same!
skip1 = tf.add(conv_1x1_4th_layer,upsampling1,name =“skip1”)
然后我们可以用另一个转置卷积层来跟踪它。
upsampling2 = tf.layers.conv2d_transpose(skip1,num_classes,4,strides =(2,2),padding ='same',kernel_regularizer = tf.contrib.layers.l2_regularizer(1e-3),name ='upsampling2')
我们将再次使用第三个池化层输出重复此操作。
skip2 = tf.add(conv_1x1_3th_layer,upsampling2,name =“skip2”)
分类和损失
最后是定义损失,这将帮助我们训练FCN,就像我们在CNN的训练一样。
FCN的目标是将每个像素分配给适当的类。我们可以使用交叉熵损失方法,类似于我们在CNN中使用的方法。
在输入交叉熵损失函数之前,输出张量是4D,所以我们必须将其重新调整为2D。
我们的标签也应该像logits一样重新塑造。
logits = tf.reshape(input, (-1, num_classes))
label_reshaped = tf.reshape(correct_label,(-1,num_classes))
logits和标签现在是2D张量,其中每一行代表一个像素,每列代表一个类。然后我们可以将这两个输入到损失函数中,如下所示:
cross_entropy_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits, labels_reshaped))
2.实现
该项目基于FCN8,它使用VGG16作为编码器。第一步是加载下载的VGG16模型。
该模型不是vanilla VGG16,而是一个完全卷积版本,它已经包含1x1卷积来代替全连接层。
然后,我们使用我们定义为项目一部分的load_vgg函数加载VGG图层以获取input_layer,第3层,第4层和第7层。
input_layer, keep_prob_tensor, layer3, layer4, layer7 = load_vgg(sess, vgg_path)
一旦我们有了来自VGG19的上述层,我们就定义了FCN解码器的第二部分。在解码器中,我们需要将上述图层的输出转换为1x1卷积,然后使用卷积转置进行上采样。我们还将跳过连接定义为此功能的一部分。我们在层功能下定义这些功能,如下所述(Python代码):
def layers(vgg_layer3_out, vgg_layer4_out, vgg_layer7_out, num_classes):
"""
Create the layers for a fully convolutional network. Build skip-layers using the vgg layers.
:param vgg_layer3_out: TF Tensor for VGG Layer 3 output
:param vgg_layer4_out: TF Tensor for VGG Layer 4 output
:param vgg_layer7_out: TF Tensor for VGG Layer 7 output
:param num_classes: Number of classes to classify
:return: The Tensor for the last layer of output
"""
# 1X1 connvolution of the layer 7
conv_1x1_7th_layer = tf.layers.conv2d(vgg_layer7_out,num_classes, 1,padding = 'same',
kernel_regularizer= tf.contrib.layers.l2_regularizer(1e-3),
name='conv_1x1_7th_layer')
# Upsampling x 4
upsampling1 = tf.layers.conv2d_transpose(conv_1x1_7th_layer,
num_classes,
4,
strides= (2, 2),
padding= 'same',
kernel_regularizer= tf.contrib.layers.l2_regularizer(1e-3),
name='upsampling1')
# 1X1 convolution of the layer 4
conv_1x1_4th_layer = tf.layers.conv2d(vgg_layer4_out,
num_classes,
1,
padding = 'same',
kernel_regularizer= tf.contrib.layers.l2_regularizer(1e-3),
name='conv_1x1_4th_layer')
skip1 = tf.add(conv_1x1_4th_layer, upsampling1, name="skip1")
# Upsampling x 4
upsampling2 = tf.layers.conv2d_transpose(skip1,
num_classes,
4,
strides= (2, 2),
padding= 'same',
kernel_regularizer= tf.contrib.layers.l2_regularizer(1e-3),
name='upsampling2')
# 1X1 convolution of the layer 3
conv_1x1_3th_layer = tf.layers.conv2d(vgg_layer3_out,
num_classes,
1,
padding = 'same',
kernel_regularizer= tf.contrib.layers.l2_regularizer(1e-3),
name='conv_1x1_3th_layer')
skip2 = tf.add(conv_1x1_3th_layer, upsampling2, name="skip2")
# Upsampling x 8.
upsampling3 = tf.layers.conv2d_transpose(skip2, num_classes, 16,
strides= (8, 8),
padding= 'same',
kernel_regularizer= tf.contrib.layers.l2_regularizer(1e-3),
name='upsampling3')
return upsampling3
然后我们定义优化函数,其中定义了Corss熵损失和优化器,Python实现如下
def optimize(nn_last_layer, correct_label, learning_rate, num_classes):
"""
Build the TensorFLow loss and optimizer operations.
:param nn_last_layer: TF Tensor of the last layer in the neural network
:param correct_label: TF Placeholder for the correct label image
:param learning_rate: TF Placeholder for the learning rate
:param num_classes: Number of classes to classify
:return: Tuple of (logits, train_op, cross_entropy_loss)
"""
# TODO: Implement function
# Reshape the label same as logits
label_reshaped = tf.reshape(correct_label, (-1,num_classes))
# Converting the 4D tensor to 2D tensor. logits is now a 2D tensor where each row represents a pixel and each column a class
logits = tf.reshape(nn_last_layer, (-1, num_classes))
# Name logits Tensor, so that is can be loaded from disk after training
logits = tf.identity(logits, name=’logits’)
# Loss and Optimizer
cross_entropy_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=label_reshaped))
reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)
reg_constant = 1e-3
loss = cross_entropy_loss + reg_constant * sum(reg_losses)
train_op = tf.train.AdamOptimizer(learning_rate= learning_rate).minimize(loss)
return logits, train_op, loss
训练模型的最后一步是定义超参数并通过上述函数循环图像。
sess.run(tf.global_variables_initializer())
# Training cycle
for epoch in range(epochs):
print("Epoch {}".format(epoch + 1))
training_loss = 0
training_samples_length = 0
for image, label in get_batches_fn(batch_size):
training_samples_length += len(image)
_, loss = sess.run([train_op, cross_entropy_loss], feed_dict={
input_image: image,
correct_label: label,
keep_prob: 0.5,
learning_rate: 0.0001
})
training_loss += loss
print(loss)
# Total training loss
training_loss /= training_samples_length
print("**************Total loss************")
print(training_loss)
我们最终选择的超参数是Epochs = 50和batch_size = 5。
以下是我们设法进行图像分割的几张图片。请记住,我们只考虑两类:道路或不道路。
本文暂时没有评论,来添加一个吧(●'◡'●)