接上一篇,接下来我们要研究的是如何训练出点云中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的思想。
此外,还想说的是,以上都是根据我的个人理解写成,我写出来的一个目的也是希望有人看到能指出我的理解中的错误,因为哦我只是一个初学者,对此的了解并不是很多,谢谢。