使用深度学习进行点云匹配(三)

接上一篇,接下来我们要研究的是如何训练出点云中3DMatch描述子。这里我们要引入一个深度学习框架:Marvin,正如我们耳详能熟的keras,pytorch,tensorflow等深度学习框架一样,Marvin也是一个深度学习框架,那么它的特征是什么呢?Marvin 是普林斯顿大学视觉工作组http://vision.princeton.edu/新推出的C++框架,它只支持GPU下运行使用,也就是说你的电脑必须是N卡,同时安装好CUDA7.5和CUDNN5.1(官方给出),作为一款C++框架,开发者认为它的优点在于使用简单,内存消耗小,运算速度快,总之是一款非常好的深度学习框架。这是项目主页介绍:http://marvin.is/,这是github地址:https://github.com/PrincetonVision/marvin因为是基于C++,所以理解起来应该不会太困难,因此我决定直接研究网络结构。

注意:接下来会涉及到CNN的很多知识,不懂的话请自行百度一下。

 // Start Marvin network
  marvin::Net convnet("3dmatch-net-test.json");        #加载网络的配置

这一句是加载网络配置,Net是一个类,关于它的定义可以去marvin.hpp进行搜索。它生成的对象convnet是我们的网络,需要传入一个参数,这个参数是一个json文件,打开3dmatch-net-test.json,里面配置了我们网络训练和测试的一些参数,以及网络结构,让我们打开这个文件看一下:

"train":
		{
			"path": "train",
			"solver": "SGD",
			"regularizer": "L2",
			"momentum": 0.99,
			"weight_decay": 0.0005,
			"base_lr": 0.01,
			"lr_policy": "LR_inv",
			"lr_gamma": 0.0001,
			"lr_power": 0.75,
			"max_iter": 1000000,
			"train_iter": 1,
			"snapshot_iter": 500,
			"display_iter": 1,
			"test_iter": 5,
			"test_interval": 100,
			"debug_mode": false,
			"GPU_solver": 0,
			"GPU": [0]
		},
	"test":
		{
			"debug_mode": false,
			"GPU": 0
		},

首先可以看到定义了训练和测试的不同参数,我们重点看一下train,里面有一下参数我们很好理解,比如优化算法是SGD,目标函数是L2,可以看到这里与论文中使用L2范数作为描述子之间的相似性度量是一致的,接下来还有学习率的设置,以及动量和学习率衰减等参数,最大迭代次数设置为1000000,下面还有GPU的设置等参数。

"layers":[
        {
            "type": "PlaceHolderData",
            "name": "dataTest",
            "phase": "Testing",
            "dim": [50,1,30,30,30],
            "out": ["data"]
        },
		{
			"in": ["data"],
			"type": "Convolution",
			"name": "conv1",
			"num_output": 64,
			"window": [3,3,3],
			"padding": [0,0,0],
			"stride": [1,1,1],
			"upscale": [1,1,1],
			"weight_lr_mult": 1.0,
			"weight_filler": "Xavier",
			"bias_lr_mult": 2.0,
			"bias_filler": "Constant",
			"bias_filler_param": 0.0,
			"train_me": true,
			"out": ["conv1"]
		},
		{
			"in": ["conv1"],
			"type": "Activation",
			"name": "relu1",
			"mode": "ReLU",
			"out": ["conv1"]
		},

接下来是有关网络层的定义,因为比较长,所以我只截取了输入配置和第一个卷积层和ruelu层,可以看到对于demo中的示例,这里输入的dim是50,1,30,30,30,50代表批量大小,1在我看来代表灰度图,最后30,30,30代表TDF体素网格,整个输入类似于2D数据输入时的写法。接下来看第一个卷积层的定义,我们说几个重要参数,可以看到输出是64,卷积核采取的是3*3*3,padding为0,步长为1,使用Xavier算法初始化网络的权重,并将偏差初始化为0,这些与论文中的介绍也是一致的。可以看到作者的创新之处,对于3D数据,作者设计了3维体素,3维的卷积核,作者在论文中也比较了3D TDF编码与2D 编码,并通过实验论证3D TDF的优越性。接下来是relu层,只是使用了relu函数。此外还有池化层,作者在整个网络结构中只是用了一次池化,因为他觉得TDF太小了,如果池化太多会丢掉太多信息。接下来我们贴一下作者给出的整个网络的架构:

最后得到的描述子是一个512维的向量。可以看出这个网络结构并不复杂。之前还有一个问题没有说到,就是最后的标签是怎么得到的,作者在论文的网络训练中曾经提到:在不同角度的图或者不同帧中对于相同的关键点周围提取TDF体素块,将它们送入上述网络中,最后制作出512维的描述子向量,通过不断减小它们的L2距离,来优化网络参数,同时去不同关键点的描述子,不断增大它们的L2距离,来训练网络。这就是我理解的作者训练的意图。(不对请指正)。

convnet.Malloc(marvin::Testing);
  convnet.loadWeights("3dmatch-weights-snapshot-137000.marvin");
  marvin::Response * rData;
  marvin::Response * rFeat;
  rData = convnet.getResponse("data");
  rFeat = convnet.getResponse("feat");
  std::cout << "3DMatch network architecture successfully loaded into Marvin!" << std::endl;

接下来我们回到了demo.cu文件,可以看到作者将模型直接设为了测试模式,同时加载了预训练的权重文件,其实这一步就是做了一个迁移学习,毕竟我们的demo中使用的3D描述子不可能从0开始训练,作者介绍他训练出符合要求的3D描述子用了7个数据集,在单个NVIDIA Tesla K40c上训练了大约8天,超过1600万个3D块,其中包括800万个对应关系和800万个不对应关系,这个训练量不是一般学生可以尝试的。那么什么是迁移学习呢,其实这是一个在图像分类领域常用的方法,具体操作就是人们发现对于那些已经针对某些数据集训练好的权重,对于其他类似数据集的分类,只需要进行小小的改动,不需要重新训练权重,也可以取得非常好的效果,我们把这种叫做预训练模型,比如常用的imagenet数据集,就有很多已经训练好的网络权重文件,如vgg,resnet等等。因此在这里我们进行训练点云描述子的时候,可以使用人家已经训练好的权重,这样我们无疑会节约很多时间并收到不错的效果。

// Run forward passes with Marvin to get 3DMatch descriptors for each keypoint
  int batch_size = 50;                      //训练10轮,每轮50个点
  int desc_size = 512;
  StorageT * batch_TDF = new StorageT[batch_size * 30 * 30 * 30];
  float * desc_3dmatch = new float[num_keypts * desc_size];
  std::cout << "Computing 3DMatch descriptors for " << num_keypts << " keypoints..." << std::endl;

接下来就是设置一些参数,每次的批量大小是50,最后描述子的大小是512.batch_TDF是将每次训练的50个TDF放到一起送入网络中。

for (int keypt_idx = batch_idx * batch_size; keypt_idx < (batch_idx + 1) * batch_size; ++keypt_idx) {

      int batch_keypt_idx = keypt_idx - batch_idx * batch_size;
      float keypt_grid_x = keypts_grid[keypt_idx * 3 + 0];
      float keypt_grid_y = keypts_grid[keypt_idx * 3 + 1];
      float keypt_grid_z = keypts_grid[keypt_idx * 3 + 2];

      // std::cout << keypt_idx << " " << batch_keypt_idx << std::endl;
      // std::cout << "    " << keypt_grid_x << " " << keypt_grid_y << " " << keypt_grid_z << std::endl;

      // Get local TDF around keypoint      获取关键点周围的局部TDF
      StorageT * local_voxel_grid_TDF = new StorageT[30 * 30 * 30];
      int local_voxel_idx = 0;
      for (int z = keypt_grid_z - 15; z < keypt_grid_z + 15; ++z)
        for (int y = keypt_grid_y - 15; y < keypt_grid_y + 15; ++y)
          for (int x = keypt_grid_x - 15; x < keypt_grid_x + 15; ++x) {
            local_voxel_grid_TDF[local_voxel_idx] = CPUCompute2StorageT(voxel_grid_TDF[z * voxel_grid_dim_x * voxel_grid_dim_y + y * voxel_grid_dim_x + x]);
            local_voxel_idx++;
          }
      for (int voxel_idx = 0; voxel_idx < 30 * 30 * 30; ++voxel_idx)
        batch_TDF[batch_keypt_idx * 30 * 30 * 30 + voxel_idx] = local_voxel_grid_TDF[voxel_idx];
      delete [] local_voxel_grid_TDF;
    }

可以看到这一部分的主要操作就是将选取的50个兴趣点的TDF值得到,并放入batch_TDF中。

 // Pass local TDF patches through Marvin
    cudaMemcpy(rData->dataGPU, batch_TDF, rData->numBytes(), cudaMemcpyHostToDevice);
    marvin::checkCUDA(__LINE__, cudaGetLastError());
    convnet.forward();

这部分的关键操作就是把数据从CPU送到GPU,然后执行了模型的forward()操作,开始训练。

 // Copy descriptor vectors from GPU to CPU memory
    StorageT * desc_vecs = new StorageT[batch_size * desc_size];
    cudaMemcpy(desc_vecs, rFeat->dataGPU, rFeat->numBytes(), cudaMemcpyDeviceToHost);
    marvin::checkCUDA(__LINE__, cudaGetLastError());

    for (int desc_val_idx = 0; desc_val_idx < batch_size * desc_size; ++desc_val_idx)
      desc_3dmatch[batch_idx * batch_size * desc_size + desc_val_idx] = CPUStorage2ComputeT(desc_vecs[desc_val_idx]);

    delete [] desc_vecs;
  }

接下来是将训练好的描述子存放到desc_3dmatch中。

// Save keypoints as binary file (Nx3 float array, row-major order)
  std::cout << "Saving keypoints to disk (keypts.bin)..." << std::endl;
  std::string keypts_saveto_path = out_prefix_filename + ".keypts.bin";
  std::ofstream keypts_out_file(keypts_saveto_path, std::ios::binary | std::ios::out);
  float num_keyptsf = (float) num_keypts;
  keypts_out_file.write((char*)&num_keyptsf, sizeof(float));
  for (int keypt_val_idx = 0; keypt_val_idx < num_keypts * 3; ++keypt_val_idx)
    keypts_out_file.write((char*)&keypts[keypt_val_idx], sizeof(float));
  keypts_out_file.close();

  // Save 3DMatch descriptors as binary file (Nx512 float array, row-major order)
  std::cout << "Saving 3DMatch descriptors to disk (desc.3dmatch.bin)..." << std::endl;
  std::string desc_saveto_path = out_prefix_filename + ".desc.3dmatch.bin";
  std::ofstream desc_out_file(desc_saveto_path, std::ios::binary | std::ios::out);
  float desc_sizef = (float) desc_size;
  desc_out_file.write((char*)&num_keyptsf, sizeof(float));
  desc_out_file.write((char*)&desc_sizef, sizeof(float));
  for (int desc_val_idx = 0; desc_val_idx < num_keypts * desc_size; ++desc_val_idx)
    desc_out_file.write((char*)&desc_3dmatch[desc_val_idx], sizeof(float));
  desc_out_file.close();

最后将关键点和3DMatch描述子存储成二进制文件,保存到本地,这就对应了demo.m文件中的关键点和描述子文件从何而来。本来文件中还有保存TDF网格的操作,不过作者可能认为用不到,把它注释掉了,感兴趣可以看一看。好,现在我们终于分析完了demo文件的几乎是所有部分的代码了,也明白了结果是如何而来的。接下来我可能再分析一下训练描述子的部分。毕竟这个文件是使用了fin-ture的思想。

此外,还想说的是,以上都是根据我的个人理解写成,我写出来的一个目的也是希望有人看到能指出我的理解中的错误,因为哦我只是一个初学者,对此的了解并不是很多,谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值