如何利用深度学习进行语义分割
目前,语义分割是计算机视觉领域的关键问题之一。纵观全局,语义分割是为完成场景理解铺平道路的高级任务之一。场景理解作为核心计算机视觉问题的重要性突出表现在,越来越多的应用通过从图像中推断知识来滋养。其中一些应用包括自动驾驶车辆,人机交互,虚拟现实等。随着近年来深度学习的普及,许多语义分割问题正在使用深层架构来解决,最常见的是卷积神经网络,其超越了其他方法在准确性和效率方面有很大的优势。
什么是语义分割?
语义分割是从粗到细的推理过程中的一个自然步骤:
起源可以位于分类,其中包括对整个输入进行预测。
下一步是定位/检测,它不仅提供类别,还提供关于这些类别的空间位置的附加信息。
最后,语义分割通过对每个像素进行精确的预测来达到精细的推断,这样每个像素都被标记为它的组成部分。
经常被用作语义分割系统的基础:
AlexNet:多伦多开创性的深度CNN,以84.6%的测试精度赢得2012年ImageNet大赛。它由5个卷积层组成,最大汇聚层,ReLUs为非线性,3个完全卷积层和dropout。
VGG-16:这款牛津模型以92.7%的准确率赢得2013年ImageNet竞赛。它用一堆的卷积层在第一个层中使用小的接受域而不是用大量的接受域。
GoogLeNet:这个Google的网络赢得了2014年的ImageNet竞赛,精确度为93.3%。它由22层和新引入的构建块组成,称为inception模块。该模块由Network-in-Network层,池化操作,大尺寸卷积层和小尺寸卷积层组成。
ResNet:这款微软模型以96.4%的准确率赢得了2016年ImageNet竞赛。由于其深度(152层)和残余块的引入而众所周知。残留块通过引入身份跳过连接来解决训练真正深层体系结构的问题,以便层可以将其输入复制到下一层。
现有的语义分割方法有哪些?
一般的语义分割结构可以广泛地认为是一个编码器网络,接着是一个解码器网络:
编码器通常是一个预先训练的分类网络,如VGG/RESNET,然后是解码器网络。
解码器的任务是语义地将编码器所学习的判别特征(较低分辨率)投影到像素空间(更高分辨率)上以获得密集分类。
与分类不同的是,深度网络的最终结果是唯一重要的,语义分割不仅需要在像素级上进行识别,而且还需要一个机制来将在编码器的不同阶段学习的识别特性投射到像素空间上。不同的方法使用不同的机制作为解码机制的一部分。让我们来探讨一下这三种主要的方法:
1 -Region-Based的语义分割。
基于区域的方法通常遵循“segmentation using recognition”pipeline,它首先从图像中提取出自由的区域,然后描述它们,然后是基于区域的分类。在测试时,基于区域的预测被转换成像素预测,通常是根据包含它的最高得分区域标记一个像素。
R-CNN (具有CNN特征的区域)是基于区域的方法的一项代表性工作。它根据对象检测结果执行语义分割。具体而言,R-CNN首先利用选择性搜索来提取大量对象提议,然后计算每个对象提议的CNN特征。最后,它使用类特定线性SVM对每个区域进行分类。与主要用于图像分类的传统CNN结构相比,R-CNN可以解决更复杂的任务,如对象检测和图像分割,甚至成为两个领域的重要基础。此外,R-CNN可以建立在任何CNN基准结构之上,例如AlexNet,VGG,GoogLeNet和ResNet。
对于图像分割任务,R-CNN为每个区域提取了2种类型的特征:全区域特征和前景特征,发现它们在将它们连接在一起作为区域特征时可以导致更好的性能。由于使用高判别性CNN功能,R-CNN实现了显着的性能改进。但是,它也面临着分段任务的两个缺点:
该功能与分段任务不兼容。
该特征不包含用于精确边界生成的足够空间信息。
生成基于细分的提案需要时间,并且会极大地影响最终的性能。
2 - 完全基于卷积网络的语义分割
原始 Fully Convolutional Network(FCN)学习从像素到像素的映射,而无需提取区域提案。FCN网络管道是经典CNN的延伸。主要思想是使古典CNN能够输入任意大小的图像。限制CNN接受和生产仅用于特定尺寸输入的标签来自完全连接的固定层。与他们相反,FCN只有卷积层和汇集层,使他们能够对任意大小的输入进行预测。
在这个特定FCN中的一个问题是,通过传播通过几个交替的卷积层和汇聚层,输出特征映射的分辨率被下采样。因此,FCN的直接预测通常是低分辨率的,导致相对模糊的对象边界。已经提出了各种更先进的基于FCN的方法来解决这个问题,包括SegNet,DeepLab-CRF和扩张卷积。
3 - 弱监督语义分割
语义分割中的大多数相关方法依赖于具有像素方式分割掩模的大量图像。但是,手动标注这些蒙版相当耗时,令人沮丧且商业上昂贵。因此,最近提出了一些弱监督方法,它们致力于利用带注释的边界框来实现语义分割。
例如,Boxsup使用边界框注释作为对网络进行训练的监督,并迭代地改进了用于语义分割的估计掩码。简单的做法是将弱监管限制作为输入标签噪声的问题,并将递归训练作为去噪策略。像素级标记解释了多实例学习框架内的分割任务,并添加了一个额外的层来约束模型,以将更多权重分配给重要的像素,用于图像级分类。
用全卷积网络进行语义分割
在本节中,让我们逐步介绍最流行的语义分割架构 - 全卷积网络(FCN)。我们将使用Python 3中的TensorFlow库以及其他依赖项(如Numpy和Scipy)来实现它。
在本练习中,我们将使用FCN标记图像中道路的像素。我们将与基蒂路数据集(http://www.cvlibs.net/datasets/kitti/eval_road.php)一起进行道路/车道检测。
以下是FCN架构的主要特点:
FCN从VGG16传输知识以执行语义分割。
VGG16的完全连接层被转换为完全卷积层,使用1x1卷积。该过程以低分辨率生成班级在场热图。
这些低分辨率语义特征图的上采样使用转置卷积(使用双线性插值滤波器初始化)完成。
在每个阶段,通过添加来自VGG16中较低层的更粗糙但更高分辨率的特征图的特征来进一步细化上采样过程。
在每个卷积块之后引入跳过连接以使后续块能够从先前汇集的特征中提取更多抽象的,显着的特征。
有3个版本的FCN(FCN-32,FCN-16,FCN-8)。我们将实施FCN-8,详细分步骤如下:
编码器:将预先训练的VGG16用作编码器。解码器从VGG16的第7层开始。
FCN第8层:VGG16的最后完全连接层被1x1卷积代替。
FCN第9层: FCN第8层被上采样2次以使尺寸与VGG 16的第4层匹配,使用带参数的转置卷积:(kernel =(4,4),stride =(2,2),paddding ='相同')。之后,在VGG16的第4层和FCN第9层之间添加了跳过连接。
FCN第10层: FCN第9层上采样2次以使尺寸与VGG16的第3层匹配,使用带参数的转置卷积:(kernel =(4,4),stride =(2,2),paddding ='same' )。之后,在VGG 16的第3层和FCN第10层之间添加了跳过连接。
FCN第11层: FCN第10层被上采样4次以使尺寸与输入图像尺寸匹配,因此我们使用带参数的转置卷积返回实际图像并且深度等于类的数量:(kernel =(16,16) ,stride =(8,8),paddding ='same')。
步骤1
我们首先将预先训练好的VGG-16模型加载到TensorFlow中。在TensorFlow会话和VGG文件夹路径(可在此下载http://www.cs.toronto.edu/~frossard/post/vgg16/)中,我们返回VGG模型中的张量元组,包括图像输入,keep_prob(控制丢失率),第3层,第4层和图层7。
def load_vgg(sess, vgg_path):
# load the model and weights
model = tf.saved_model.loader.load(sess, ['vgg16'], vgg_path)
# Get Tensors to be returned from graph
graph = tf.get_default_graph()
image_input = graph.get_tensor_by_name('image_input:0')
keep_prob = graph.get_tensor_by_name('keep_prob:0')
layer3 = graph.get_tensor_by_name('layer3_out:0')
layer4 = graph.get_tensor_by_name('layer4_out:0')
layer7 = graph.get_tensor_by_name('layer7_out:0')
return image_input, keep_prob, layer3, layer4, layer7
第2步
现在我们专注于使用VGG模型中的张量创建FCN图层。给定VGG图层输出的张量和要分类的类的数量,我们返回该输出最后一层的张量。特别是,我们将1x1卷积应用于编码器层,然后使用跳过连接和上采样将解码器层添加到网络。
def layers(vgg_layer3_out, vgg_layer4_out, vgg_layer7_out, num_classes):
# Use a shorter variable name for simplicity
layer3, layer4, layer7 = vgg_layer3_out, vgg_layer4_out, vgg_layer7_out
# Apply 1x1 convolution in place of fully connected layer
fcn8 = tf.layers.conv2d(layer7, filters=num_classes, kernel_size=1, name="fcn8")
# Upsample fcn8 with size depth=(4096?) to match size of layer 4 so that we can add skip connection with 4th layer
fcn9 = tf.layers.conv2d_transpose(fcn8, filters=layer4.get_shape().as_list()[-1],
kernel_size=4, strides=(2, 2), padding='SAME', name="fcn9")
# Add a skip connection between current final layer fcn8 and 4th layer
fcn9_skip_connected = tf.add(fcn9, layer4, name="fcn9_plus_vgg_layer4")
# Upsample again
fcn10 = tf.layers.conv2d_transpose(fcn9_skip_connected, filters=layer3.get_shape().as_list()[-1],
kernel_size=4, strides=(2, 2), padding='SAME', name="fcn10_conv2d")
# Add skip connection
fcn10_skip_connected = tf.add(fcn10, layer3, name="fcn10_plus_vgg_layer3")
# Upsample again
fcn11 = tf.layers.conv2d_transpose(fcn10_skip_connected, filters=num_classes,
kernel_size=16, strides=(8, 8), padding='SAME', name="fcn11")
return fcn11
第3步
下一步是优化我们的神经网络,也就是构建TensorFlow丢失函数和优化器操作。这里我们使用交叉熵作为我们的损失函数,而亚当作为我们的优化算法。
def optimize(nn_last_layer, correct_label, learning_rate, num_classes):
# Reshape 4D tensors to 2D, each row represents a pixel, each column a class
logits = tf.reshape(nn_last_layer, (-1, num_classes), name="fcn_logits")
correct_label_reshaped = tf.reshape(correct_label, (-1, num_classes))
# Calculate distance from actual labels using cross entropy
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=correct_label_reshaped[:])
# Take mean for total loss
loss_op = tf.reduce_mean(cross_entropy, name="fcn_loss")
# The model implements this operation to find the weights/parameters that would yield correct pixel labels
train_op = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss_op, name="fcn_train_op")
return logits, train_op, loss_op
步骤4
在这里,我们定义train_nn函数,其中包含重要参数,包括时期数量,批量大小,损失函数,优化器操作以及输入图像的占位符,标签图像,学习率。对于训练过程,我们还将keep_probability设置为0.5,将learning_rate设置为0.001。为了跟踪进展情况,我们还打印了培训期间的损失。
def train_nn(sess, epochs, batch_size, get_batches_fn, train_op,
cross_entropy_loss, input_image,
correct_label, keep_prob, learning_rate):
keep_prob_value = 0.5
learning_rate_value = 0.001
for epoch in range(epochs):
# Create function to get batches
total_loss = 0
for X_batch, gt_batch in get_batches_fn(batch_size):
loss, _ = sess.run([cross_entropy_loss, train_op],
feed_dict={input_image: X_batch, correct_label: gt_batch,
keep_prob: keep_prob_value, learning_rate:learning_rate_value})
total_loss += loss;
print("EPOCH {} ...".format(epoch + 1))
print("Loss = {:.3f}".format(total_loss))
print()
第5步
最后,是时候训练我们的网络!在这个运行函数中,我们首先使用load_vgg,layers和optimize函数来构建我们的网络。然后我们使用train_nn函数训练网络并保存推理数据以用于记录。
def run():
# Download pretrained vgg model
helper.maybe_download_pretrained_vgg(data_dir)
# A function to get batches
get_batches_fn = helper.gen_batch_function(training_dir, image_shape)
with tf.Session() as session:
# Returns the three layers, keep probability and input layer from the vgg architecture
image_input, keep_prob, layer3, layer4, layer7 = load_vgg(session, vgg_path)
# The resulting network architecture from adding a decoder on top of the given vgg model
model_output = layers(layer3, layer4, layer7, num_classes)
# Returns the output logits, training operation and cost operation to be used
# - logits: each row represents a pixel, each column a class
# - train_op: function used to get the right parameters to the model to correctly label the pixels
# - cross_entropy_loss: function outputting the cost which we are minimizing, lower cost should yield higher accuracy
logits, train_op, cross_entropy_loss = optimize(model_output, correct_label, learning_rate, num_classes)
# Initialize all variables
session.run(tf.global_variables_initializer())
session.run(tf.local_variables_initializer())
print("Model build successful, starting training")
# Train the neural network
train_nn(session, EPOCHS, BATCH_SIZE, get_batches_fn,
train_op, cross_entropy_loss, image_input,
correct_label, keep_prob, learning_rate)
# Run the model with the test images and save each painted output image (roads painted green)
helper.save_inference_samples(runs_dir, data_dir, session, image_shape, logits, keep_prob, image_input)
print("All done!")
关于我们的参数,我们选择epochs = 40,batch_size = 16,num_classes = 2和image_shape =(160,576)。经过2次试验合格后,dropout = 0.5,dropout = 0.75,我们发现第二次试验的结果更好,平均损失更好。