【OpenPCDet】稀疏卷积SPConv-v1.2代码解读(1)

2023-11-04 01:59

本文主要是介绍【OpenPCDet】稀疏卷积SPConv-v1.2代码解读(1),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

【3D卷积】

        以下左图展示了一个2D卷积,使用一个3x3的卷积核,在单通道图像上进行卷积,其中Padding为1,得到输出。右图为一个单通道的3D卷积,与2D卷积不同之处在于,输入图像多了一个 depth 维度,卷积核也多了一个depth维度,之前2D卷积上3x3的卷积核现在变成了3x3x3。这里的3D不是通道导致的,而是深度(多层切片,多帧视频),因此,虽然输入和卷积核和输出都是3D的,但都可以是单通道的。
 

【3D稀疏卷积】

        标准的3D卷积直接用于类似3D点云检测/分割等3D任务时,因为该类场景中输入特征的稀疏性,会带来严重的冗余计算量。所以,面对稀疏场景发展除了3D稀疏卷积。当然,2D也有稀疏卷积。

对于稀疏卷积有两种:

一种是Spatially Sparse Convolution ,在spconv中为SparseConv3d。就像普通的卷积一样,只要kernel 覆盖一个 active input site,就可以计算出output site。

另一种是Submanifold Sparse Convolution, 在spconv中为SubMConv3d。只有当kernel的中心覆盖一个 active input site时,卷积输出才会被计算。

【Second引入3D稀疏卷积】

        Second论文中,作者在VoxleNet论文的基础上作了进一步的发展。考虑到VoxleNet模型中3D卷积运算量较大,速度不佳。作者引入了稀疏3D卷积来代替,在检测速度和内存使用方面都做了优化。作和开源了3D稀疏卷积的实现:GitHub - traveller59/spconv: Spatial Sparse Convolution Library。

截止目前已更新至spconv 2.x版本。我这里作代码解读仍然时基于早期的1.2版本,对于理解思想来说,问题不大。不得不佩服作者强大的代码工程能力!

【Second网络结构中的3D稀疏卷积】

        分析OpenPCDet中Second的网络结构,3D稀疏卷积使用在3D骨干网模块:VoxelBackBone8x中。它接收MeanVFE模块的结果,经过精心设计好的3D稀疏卷积和3D稀疏子流卷积的有效组合,得到输出特征,并送入HeightCompression作深度方向的压缩,后面就是我们熟悉的2D检测网络的结构:2D骨干网络-->RPN-->分类/回归-->后处理。

 在Second原始论文中给出了BACKBONE_3D部分的示意图,但是这与OpenPCDet中Second具体的网络参数有所差异,注意区分。下图是Second中BACKBONE_3D部分的表示。

OpenPCDet中的Second我们可以参考具体的代码实现。

class VoxelBackBone8x(nn.Module):def __init__(self, model_cfg, input_channels, grid_size, **kwargs):super().__init__()self.model_cfg = model_cfgnorm_fn = partial(nn.BatchNorm1d, eps=1e-3, momentum=0.01)self.sparse_shape = grid_size[::-1] + [1, 0, 0] #e.g. array([  41, 1600, 1408])self.conv_input = spconv.SparseSequential(spconv.SubMConv3d(input_channels, 16, 3, padding=1, bias=False, indice_key='subm1'),norm_fn(16),nn.ReLU(),)block = post_act_blockself.conv1 = spconv.SparseSequential(block(16, 16, 3, norm_fn=norm_fn, padding=1, indice_key='subm1'),)self.conv2 = spconv.SparseSequential(# [1600, 1408, 41] -> [800, 704, 21]block(16, 32, 3, norm_fn=norm_fn, stride=2, padding=1, indice_key='spconv2', conv_type='spconv'),block(32, 32, 3, norm_fn=norm_fn, padding=1, indice_key='subm2'),block(32, 32, 3, norm_fn=norm_fn, padding=1, indice_key='subm2'),)self.conv3 = spconv.SparseSequential(# [800, 704, 21] -> [400, 352, 11]block(32, 64, 3, norm_fn=norm_fn, stride=2, padding=1, indice_key='spconv3', conv_type='spconv'),block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm3'),block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm3'),)self.conv4 = spconv.SparseSequential(# [400, 352, 11] -> [200, 176, 5]block(64, 64, 3, norm_fn=norm_fn, stride=2, padding=(0, 1, 1), indice_key='spconv4', conv_type='spconv'),block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm4'),block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm4'),)last_pad = 0last_pad = self.model_cfg.get('last_pad', last_pad)self.conv_out = spconv.SparseSequential(# [200, 150, 5] -> [200, 150, 2]spconv.SparseConv3d(64, 128, (3, 1, 1), stride=(2, 1, 1), padding=last_pad,bias=False, indice_key='spconv_down2'),norm_fn(128),nn.ReLU(),)self.num_point_features = 128....def forward(self, batch_dict):"""Args:batch_dict:batch_size: intvfe_features: (num_voxels, C)voxel_coords: (num_voxels, 4), [batch_idx, z_idx, y_idx, x_idx]Returns:batch_dict:encoded_spconv_tensor: sparse tensor"""voxel_features, voxel_coords = batch_dict['voxel_features'], batch_dict['voxel_coords']pdb.set_trace()batch_size = batch_dict['batch_size']input_sp_tensor = spconv.SparseConvTensor(features=voxel_features,                     #e.g. torch.Size([16000, 4])indices=voxel_coords.int(),                  #e.g. torch.Size([16000, 4]) spatial_shape=self.sparse_shape,             #e.g. array([41, 1600, 1408])batch_size=batch_size)x = self.conv_input(input_sp_tensor)x_conv1 = self.conv1(x)x_conv2 = self.conv2(x_conv1)   #stride 2,downsamplex_conv3 = self.conv3(x_conv2)   #stride 2,downsamplex_conv4 = self.conv4(x_conv3)   #stride 2,downsample# for detection head# [200, 176, 5] -> [200, 176, 2]out = self.conv_out(x_conv4)....

对于VoxelBackbone8x模块的前向推理(forward)部分,其输入字典中最重要的内容为voxel_features和voxel_coords。他们分别表示有效的输入特征,以及这些有效特征的空间位置。voxel_features的size为(N,4),通常不同帧点云的有效特征的数量是不同的,N表示当前batch中总的有效输入特征的数量。可见,就VoxelBackBone8x模块来说,输入feature map其实是不固定的。具体到无论是3D标准稀疏卷积还是3D子流形卷积,其输入特征也是可变的。这会给我们做进一步做spconv的部署带来挑战。就我们常用的TensorRT推理引擎来说,它就要求输入特征是固定的。注意,这里说的固定跟TensorRT中的dynamic shape不是一回事。

【spconv模块】

        在Second中spconv是作为Pytorch的一个自定义扩展模块在使用。对于一般的操作,我们扩展Pytorch模块很容易,只用使用Python来扩展即可。只需要继承torch.nn.Module并实现其__init__,forward等方法,求导的函数是不需要设置的,会自动按照求导规则求导师。像Second代码中的HeightCompression模块就是这样一个例子。这种扩展方式即插即用,不需要编译。

class HeightCompression(nn.Module):def __init__(self, model_cfg, **kwargs):super().__init__()self.model_cfg = model_cfgself.num_bev_features = self.model_cfg.NUM_BEV_FEATURESdef forward(self, batch_dict):"""Args:batch_dict:encoded_spconv_tensor: sparse tensorReturns:batch_dict:spatial_features:"""encoded_spconv_tensor = batch_dict['encoded_spconv_tensor']spatial_features = encoded_spconv_tensor.dense()N, C, D, H, W = spatial_features.shape   #e.g. torch.Size([1, 128, 2, 200, 176])spatial_features = spatial_features.view(N, C * D, H, W)batch_dict['spatial_features'] = spatial_featuresbatch_dict['spatial_features_stride'] = batch_dict['encoded_spconv_tensor_stride']return batch_dict

但是对于像对3D稀疏卷积这样复杂的操作进行优化,实现其扩展模块,单纯靠Pytorch已实现的operator的组合已经无法做到。正如spconv的实现,它采用C++和CUDA来扩展自定义模块。在 PyTorch 中直接扩展底层C++算子主要有三种方式,native_functions.yaml、C++ extension方式、OP register方式。spconv中使用了OP register这种方式。spconv代码主要分为python部分代码和c++/cuda部分代码两部分,对于其中重要内容后文我们做详细分析。

  python部分目录

├── setup.py
├── spconv
│   ├── conv.py
│   ├── functional.py
│   ├── identity.py
│   ├── __init__.py
│   ├── modules.py
│   ├── ops.py
│   ├── pool.py
│   ├── tables.py
│   ├── test_utils.py
│   └── utils
│       ├── __init__.py
│       └── __pycache__

c++/cuda部分目录

├── include
│   ├── cuhash
│   ├── paramsgrid.h
│   ├── spconv
│   ├── tensorview
│   ├── torch_utils.h
│   └── utility
│       └── timer.h
├── src
│   ├── cuhash
│   ├── spconv
│   │   ├── all.cc
│   │   ├── CMakeLists.txt
│   │   ├── cublas_gemm.cc
│   │   ├── indice.cc
│   │   ├── indice.cu
│   │   ├── maxpool.cc
│   │   ├── maxpool.cu
│   │   ├── pillar_scatter.cu
│   │   ├── pool_ops.cc
│   │   ├── reordering.cc
│   │   ├── reordering.cu
│   │   └── spconv_ops.cc
│   └── utils
│       ├── all.cc
│       ├── CMakeLists.txt
└── third_party

【参考文献】

稀疏卷积 Sparse Convolution Net - 知乎

PyTorch扩展自定义PyThon/C++(CUDA)算子的若干方法总结 - 知乎

这可能是关于Pytorch底层算子扩展最详细的总结了! - 知乎

PyTorch算子底层源码解读--Op Registration - 知乎

通俗易懂的解释Sparse Convolution过程 - 知乎

Spconv代码解读 - 知乎

这篇关于【OpenPCDet】稀疏卷积SPConv-v1.2代码解读(1)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/342451

相关文章

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

java之Objects.nonNull用法代码解读

《java之Objects.nonNull用法代码解读》:本文主要介绍java之Objects.nonNull用法代码,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录Java之Objects.nonwww.chinasem.cnNull用法代码Objects.nonN

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

python+opencv处理颜色之将目标颜色转换实例代码

《python+opencv处理颜色之将目标颜色转换实例代码》OpenCV是一个的跨平台计算机视觉库,可以运行在Linux、Windows和MacOS操作系统上,:本文主要介绍python+ope... 目录下面是代码+ 效果 + 解释转HSV: 关于颜色总是要转HSV的掩膜再标注总结 目标:将红色的部分滤

在C#中调用Python代码的两种实现方式

《在C#中调用Python代码的两种实现方式》:本文主要介绍在C#中调用Python代码的两种实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#调用python代码的方式1. 使用 Python.NET2. 使用外部进程调用 Python 脚本总结C#调

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

Java中&和&&以及|和||的区别、应用场景和代码示例

《Java中&和&&以及|和||的区别、应用场景和代码示例》:本文主要介绍Java中的逻辑运算符&、&&、|和||的区别,包括它们在布尔和整数类型上的应用,文中通过代码介绍的非常详细,需要的朋友可... 目录前言1. & 和 &&代码示例2. | 和 ||代码示例3. 为什么要使用 & 和 | 而不是总是使

Java强制转化示例代码详解

《Java强制转化示例代码详解》:本文主要介绍Java编程语言中的类型转换,包括基本类型之间的强制类型转换和引用类型的强制类型转换,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录引入基本类型强制转换1.数字之间2.数字字符之间引入引用类型的强制转换总结引入在Java编程语言中,类型转换(无论

SpringCloud负载均衡spring-cloud-starter-loadbalancer解读

《SpringCloud负载均衡spring-cloud-starter-loadbalancer解读》:本文主要介绍SpringCloud负载均衡spring-cloud-starter-loa... 目录简述主要特点使用负载均衡算法1. 轮询负载均衡策略(Round Robin)2. 随机负载均衡策略(

解读spring.factories文件配置详情

《解读spring.factories文件配置详情》:本文主要介绍解读spring.factories文件配置详情,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录使用场景作用内部原理机制SPI机制Spring Factories 实现原理用法及配置spring.f