本文承接上一篇:利用深度学习进行点云匹配(五)。
之前的文章介绍了3D match如何进行点云匹配,总结来说它使用了Siamese network的设计思想,在Point cloud,Mesh和Depth map三种数据中找出匹配的对应点,围绕对应点取出一个立方块,计算它的TDF,将匹配的体素块送入模型中,要求训练的损失变得越来越小,同时为对应点找到一个随机的不对应点(这个点也是有要求的),并将它们的体素块送入模型中,要求训练的损失变得越来越大。今天主要是记录一些最近做的学习记录。
一,Marvin框架
在3D match中进行训练使用的框架是Marvin,官网给出的介绍是:A minimalist GPU-only N-dimensional ConvNet framework,这个专门用于CNN模型的框架特点是小并且快,使用的是基本的C,C++,并且只调用CUDA和cuDNN作为依赖项。所有的代码都存储在了marvin.hpp和marvin.cu中。这是github地址:https://github.com/PrincetonVision/marvin
要使用Marvin框架,主要的是需要掌握其中json文件的写法。从这里可以看出,Marvin框架应该是参考了Caffe框架,虽然我对Caffe了解不多,但是我也大致了解Caffe的核心也是一个配置文件,只要按照格式设置好网络结构和损失函数等层即可。官网给出了MNIST的数据集的例子用于学习,其中还有关于如何使用MNIST做Siamese network的代码。(ps:因为大部分人可能还是不清楚孪生神经网络和普通网络设计的不同在哪,所以我会在博客最下面用mnist数据集做一个直观的对比。)
二,损失函数:Contrastive Loss (对比损失)
第二点很重要的是弄清楚损失函数是什么,论文中有这么一句话:We use the L2 norm as a similarity metric between descriptors, modeled during training with the contrastive loss function 。这句话的含义是:我们使用L2范数作为描述子之间的相似性度量,在训练期间使用对比度损失函数建模。这句话可能会让一些人产生疑问,L2也就是平方损失函数,是可以求导的,因此在一些涉及回归的机器学习问题中是可以作为一种度量的,根据之前的分析,用来作为度量两个512维描述子的函数也应该是可以的,那么这里为什么又提到了Contrastive Loss?到底谁是模型真正的损失函数?其实这里Contrastive Loss的确是模型真正的损失函数,为什么这么说呢,因为我们的网络模型采用的是孪生神经网络,它每次接收的是一个三元组,其中两个是数据,第三个是0或者1, 0代表两个数据是不同的,1代表相同(代码是这样的)。如果我们直接使用L2优化损失,那么输入相同的应该是L2越小越好没问题,但是我们做不到输入不同的会使得损失函数变大这个要求,换句话说,损失函数必须有一个参数是关系到我们输入的是0还是1的判断的。
首先我们证明一下损失函数是Contrastive Loss,这是代码中的截图:来自于training文件中的net.json文件。
可以清楚的看到不管是匹配对还是不匹配对的损失函数都被设置为Contrastive。
接下来说一说Contrastive Loss,学过caffe的应该知道,这个是caffe的孪生神经网络中使用的损失函数。为什么会使用它,我们来看它的公式:
我们可以分析一下这个损失函数,当输入的是相同的图片对时,y=1,此时式子只剩下左边,可以看出这就是一个L2损失函数,我们的优化方向自然是越小越好。当y=0时,此时式子只剩下右边,输入的是不匹配的对,此时我们的目标时让它们的距离变大,也就是d变大,而d前加一个负号,可以看出我们还是可以通过使总体损失变小来达到d变大的目的。max函数在这里保证了损失的非负性。
这里再解释一下margin参数的含义,在此之前我们先区分一下输入的数据,Anchor代表源数据,Positive代表与它相同的数据,二者应该是1,Negative代表不同的数据,二者应该是0。当 margin 值越小时,loss 也就较容易的趋近于 0,于是 Anchor 与 Positive 都不需要拉的太近,Anchor 与 Negative 不需要拉的太远,就能使得 loss 很快的趋近于 0。这样训练得到的结果,不能够很好的区分相似的图像。当 Anchor 越大时,就需要使得网络参数要拼命地拉近 Anchor、Positive 之间的距离,拉远 Anchor、Negative 之间的距离。如果 margin 值设置的太大,很可能最后 loss 保持一个较大的值,难以趋近于 0 。因此,设置一个合理的 margin 值很关键,这是衡量相似度的重要指标。简而言之,margin 值设置的越小,loss 很容易趋近于 0 ,但很难区分相似的图像。margin 值设置的越大,loss 值较难趋近于 0,甚至导致网络不收敛,但可以较有把握的区分较为相似的图像。
三:输入数据如何制作?
这一点是我一直以来忽视的一点,经过老师提醒我才意识到,孪生神经网络输入的不是普通数据,而是有一个标签,来区分是否是相同数据,因此这种数据一般不可能直接获得,都是需要在原始数据上进行处理才能得到的,而且这种处理十分重要,之前在看kaggle的鲸鱼分类比赛中,有人就使用了孪生神经网络,在制作数据集上就使用了十分复杂的方法,可以说这种模型能否成功很大取决于相同数据和不同数据是如何制作的。
因为3Dmatch做的是点云匹配,论文作者曾经指出,在当时是没有专门的匹配点云数据集的,也达不到深度学习所需要的数据量要求。因此作者想到了一个很巧妙的方法:使用3D 场景重建中的数据集。我们知道rgb-d重建数据集一般是对某一个物体的不同角度的几十张拍摄照片,我们以一个桌子为例,有了各个不同视角对同一个桌子的照片,我们可以从里面选取对应点(比如桌子角)制作数据集。而且重建数据集还有一个优点很重要,这个我们下面再说。接下来我就从头走一遍训练过程。
首先我们看github中给出的从RGB-D Reconstructions 中训练3Dmatch描述子的步骤:
编译Marvin这步不用看,是使用Marvin框架的必须步骤。第二步是下载重建数据集,分别创建了训练文件夹和测试文件夹。这里我挑选一个比较小的数据集sun3d-harvard_c11-hv_c11_2下载下来看了一下:
截图显示一下里面的数据集如上,可以看出是三个一组,是室内某个场景的rgb图加上深度图以及对应的旋转平移矩阵。而我们最原始的数据就是它们,我们要从中制作出对应关键点和不对应的关键点,从而训练模型。
接下来就是从头开始训练我们的3D match模型:
可以看出这一步直接调用了train net.json就可以了,但是重要文件并不只是net.json,我们看一下training文件夹:
这里面net.json就是网络结构设计,而marvin.cu和marvin.hpp是marvin框架的东西,compile.sh是编译模型,这些都暂时不用理会,我们需要查看match.hpp和utils.hpp两个文件,其中utils.hpp主要是一些工具函数。所以我们打开match.hpp进行分析。
打开后可以发现,这里面其实主要就是一个函数和一个类,函数是Get1NNSearchDist
类是MatchDataLayer
作用也解释的很清楚,那么在哪里使用这些东西呢?这时我们就要打开marvin.hpp文件夹,前面说到写json文件定义网络结构,但是这还不算完,我们还需要自己定义一个类Net,因为网络结构是孪生神经网络,所以估计作者需要修改的地方还是很多的。先看看Net的位置,
我这里尽量截了一个大图,这就是从json文件中导入定义模型的类,我看了一下代码很多,但是我们不用去细看,只需要知道这个就是导入网络结构的类。这里面从json文件中导入了一层层的网络结构,但是数据层我们必须自己定义,这就是我们上面提到的MatchDataLayer类,我们的重点也是研究这个类,弄明白它就明白如何从rgb-d数据转化为需要的TDF值了。
这个类里面有几个主要的成员函数,我们主要说两个,第一个:GetLocalPointvoxel_grid_TDF(),这个函数的功能是得到给定点的体素值,第二个是prefetch()函数,我查看了marvin中的关于数据层的定义,这个函数在数据层中就是用于数据的构建,所以我们主要看这个函数。
先介绍一下prefetch()函数的作用,这个函数就是构建我们需要的数据,也就是对应关键点和不对应关键点,具体是怎么做的呢?代码中显示用p1,p2,p3代表三个点,如下:
接下来我们选取第一个关键点p1,这里作者是这样做的,首先随机选择一张图片上的一个像素,这样就有了关键点的两个坐标,这个时候再去深度图中读出它的深度,作为第三个坐标,当然现在是在相机坐标系中,作者结合相机参数和帧参数将三个坐标对应到世界坐标系下的x,y,z坐标,这样p1关键点的坐标就没有问题了。大致截一下这部分代码,有兴趣可以对应看一下:
接下来我们要选取第二个关键点p2,p2肯定不能是随机选取的,因为它必须和p1对应,也就是说它是关键点p1在其他帧中的对应点。那么应该怎么提取这个点呢?这里就涉及到我之前提到的rgb-d重建数据集的一个优点了。因为是重建数据集,所以每一帧图片都有对应的参数,通过相机参数和帧的参数,我们可以直接求出关键点在另一个帧中的位置是什么?这个很好理解,因为重建数据集本身必须先做出一些关键点的匹配,比如sift等算法,才可以基于匹配的关键点进行重建。而作者恰好运用了这个特征,通过一系列参数变换,直接可以求出剩下所有帧中对应关键点的位置,再从中随机选择,当然这个选择是有要求的,正如论文中所说的:To find correspondences for an interest point, we map its 3D position in the reconstruction into all RGB-D frames for which the 3D point lies within the frame’s camera view frustum and is not occluded.The locations of the cameras from which the RGB-D frames are taken are enforced to be at least 1m apart, so that the views between observation pairs are sufficiently wide-baselined.We then extract two local 3D patches around the interest point from two of these RGB-D frames, and use them as a matching pair.简单翻译一下就是:为了找到兴趣点的对应关系,我们将其在重建中的3D位置映射到所有RGB-D帧,其中3D点位于帧的摄像机视锥体内并且不被遮挡。拍摄RGB-D帧的相机的位置被强制相隔至少1米,使得观察对之间的视图足够宽基线。然后,我们从两个RGB-D帧中的兴趣点周围提取两个局部3D块,并将它们用作匹配对。这里的代码过多,我不做粘贴,有兴趣可以自己研究。
最后是p3点,p3点是p1的不对应点,而一幅图中不对应点可以说是成千上万,作者在论文中也没有多加限制:To obtain nonmatching pairs, we extract local 3D patches from randomly picked depth frames of two interest points (at least 0.1m
apart) randomly sampled from the surface of the reconstruction.翻译就是:为了获得不匹配的对, 我们从随机选取的两个兴趣点 (至少间隔0.1 米) 的深度帧中从重建表面随机采样的局部3d 块中提取。
接下来分别计算三个点的TDF体素网格,这里调用了我们之前提到的函数:GetLocalPointvoxel_grid_TDF(),我们截一下这个操作:
最后把数据和标签直接送入到模型中去:
ok,以上就是数据如何创建的,现在我们回顾一下这个过程,再回答几个问题。
1,作者是怎么样保存对应点和不对应点的数据的?
答:作者没有进行保存,可以看到作者是直接从原始的rgb-d数据集中随机选取p1,然后通过算法得出p2,p3,计算出它们的TDF体素网格,积累到一个batch就直接送入到神经网络中,这种做法有一个很大的优点,就是我们可以得到无数的关键点,虽然数据是有限的,但是每一个图片上可以选取的点却是无穷的,因此我们可以无限生成数据。
2,我们提到的数据都是rgb-d数据集,那么点云是怎么训练的?
我们之前提到3D match可以完成点云或者深度图的匹配,但是我们并没有看到原始的点云数据,那么点云模型是如何得出的?其实这个很好解答,首先论文中已经指出,作者使用的只是rgb-d重建数据集,We learn the function ψ by making use of data from existing high quality RGB-D scene reconstructions.作者说我们通过利用现有高质量RGB-D场景重建的数据来学习函数ψ。函数ψ就是代指匹配关系。其次,作者再从rgb-d得到TDF时,其实是使用了两种方式,我们可以看github给出的介绍:
第一种方式是:深度图映射到3D相机空间中的点云,并从点云转换为TDF体素网格(见上文)
第二种替代方案是:深度图转换为TSDF体素(参见此处的说明)并计算每个体素的绝对值(也称为投影TDF值,其在视图边界和缺失深度区域附近表现不同)
所以作者是现将深度图映射到3D相机空间中的点云,因此直接匹配点云数据也是可以的,之前的文章也给出了一个兔子点云模型匹配的示例。
以上就是数据如何制作出的,最后我们对比一下孪生神经网络和普通神经网络再mnist的区别,代码来自于Marvin的github。连接是:https://github.com/PrincetonVision/marvin/tree/master/examples/mnist
我们对比其中的lenet.json和siamese.json
首先是数据输入层:lenet是这样的:
而siamese是这样的:
可以看出孪生神经网络有两个输入了,普通网络是一个输入。
接下来我们选取一个卷积层进行对比:
左边是lenet,右边是siamese,可以看出都是CNN,只不过右边的输入输出都是两个。
最后是损失函数:
可以看出一个是softmax函数用于做多分类,一个是Contrastive,是孪生神经网络的特有函数。
当然以上是marvin框架的结构设计,不同的深度学习库对于孪生神经网络的写法也是不同的,我们只是通过这个框架对比出孪生神经网络和普通网络结构的区别。
这篇博客写的很长,有很多废话,写得不太简洁,也有可能有一些错误,希望大家可以指出,一定及时改正!