点云学习的动态图卷积神经网络.

点云是一种灵活的几何表示,通常是大多数3D采集设备的原始输出。然而点云自身缺乏拓扑信息,所以需要设计一个可以恢复拓扑信息的模型,从而达到丰富点云表示能力的目的。本文设计了一种动态图卷积神经网络(Dynamic Graph CNN, DGCNN),为了挖掘局部几何结构,构造了一个局部邻域图;局部邻域图不固定,在网络的每一层都动态更新,特征空间中的邻近性和输入不同,这样会导致信息在整个点云中的非局部扩散。

为了提取局部邻域图中的信息,DGCNN设计了一个EdgeConv模块。EdgeConv作用在近邻图上,动态地对网络中每一层的近邻图进行计算。该模块考虑了局部邻域信息和全局形状信息,具有排序不变性,可以被嵌入任意现有的网络中。

⚪ EdgeConv

记\(\mathbf{X}=\left\{\mathbf{x}_{1}, \ldots, \mathbf{x}_{n}\right\} \subseteq \mathbb{R}^{F}\)为输入点云,其中$n$是点的数量,$F$是点的维度。通常对于输入点$F=3$,每个点都包括了3D坐标\(\mathbf{x}_{i}=\left(x_{i}, y_{i}, z_{i}\right)\),在其他情况下,还会包括颜色、法向量等,在网络的其他层,$F$表示点的特征维度。

计算一个有向图\(\mathcal{G}=(\mathcal{V}, \mathcal{E})\)表示局部点云结构,其中\(\mathcal{V}=\{1, \ldots, n\}, \mathcal{E} \subseteq \mathcal{V} \times \mathcal{V}\)分别是顶点和边。在最简单的情况下,构造一个\(\mathrm{X}\)的KNN图\(\mathcal{G}\)。该图包括self-loop,每个节点都会指向自己。定义边特征为\(\boldsymbol{e}_{i j}=h_{\Theta}\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)\),其中\(h_{\Theta}: \mathbb{R}^{F} \times \mathbb{R}^{F} \rightarrow \mathbb{R}^{F^{\prime}}\)是非线性函数,可学习参数为\(\boldsymbol{\Theta}\)。

通过使用以通道为单位的对称聚合操作\(\square\) (如$\sum,\max$) 定义EdgeConv操作:在与从每个顶点发出的所有边相关联的边特征上进行聚合操作。在第$i$个顶点的EdgeConv输出表示为:

\[\mathbf{x}_{i}^{\prime}=\mathop{\square}\limits_{j:(i, j) \in \mathcal{E}} h_{\Theta}\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)\]

其中\(\mathbf{x}_{i}\)是中心点,\(\left\{\mathbf{x}_{j}:(i, j) \in \mathcal{E}\right\}\)是\(\mathbf{x}_{i}\)的近邻点。给定带有$n$个点的$F$维点云,EdgeConv会产生一个相同数量点的$F^{\prime}$维点云。

⚪ 非线性函数$h_{\Theta}$的选择

① 卷积式

\[x_{i m}^{\prime}=\sum_{j:(i, j) \in \mathcal{E}} \boldsymbol{\theta}_{m} \cdot \mathbf{x}_{j}\]

其中\(\Theta=\left(\theta_{1}, \ldots, \theta_{M}\right)\)对$M$个不同的滤波器权值进行编码。每个$\theta_{m}$都有着与\(\mathbf{x}\)相同的维度,$\cdot$表示内积。

② PointNet式

\[h_{\Theta}\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)=h_{\Theta}\left(\mathbf{x}_{i}\right)\]

只对全局形状信息编码,而不考虑局部邻域结构。

③ PCNN式

\[x_{i m}^{\prime}=\sum_{j \in \mathcal{V}}\left(h_{\boldsymbol{\theta}}\left(\mathbf{x}_{j}\right)\right) g\left(u\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)\right)\]

其中$g$是高斯核,$u$被用于计算欧式空间中的距离。

④ PointNet++式

\[h_{\boldsymbol{\Theta}}\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)=h_{\boldsymbol{\Theta}}\left(\mathbf{x}_{j}-\mathbf{x}_{i}\right)\]

仅对局部信息进行编码,将整个形状划分为很多块,丢失了全局结构信息。

⑤ 本文使用的对称边函数

\[h_{\boldsymbol{\Theta}}\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)=\bar{h}_{\boldsymbol{\Theta}}\left(\mathbf{x}_{i}, \mathbf{x}_{j}-\mathbf{x}_{i}\right)\]

既结合了全局形状结构(通过以\(\mathbf{x}_{i}\)为中心的坐标决定),又考虑到了局部邻域信息(通过\(\mathbf{x}_{j}-\mathbf{x}_{i}\)获取)。

⚪ EdgeConv的具体实现

可以通过下式表示EdgeConv的操作:

\[\begin{aligned} e_{i j m}^{\prime}&=\operatorname{ReLU}\left(\boldsymbol{\theta}_{m} \cdot\left(\mathbf{x}_{j}-\mathbf{x}_{i}\right)+\boldsymbol{\phi}_{m} \cdot \mathbf{x}_{i}\right) \\ x_{i m}^{\prime}&=\max _{j:(i, j) \in \mathcal{E}} e_{i j m}^{\prime} \end{aligned}\]

相当于设置\(\Theta=\left(\theta_{1}, \ldots, \theta_{M}, \phi_{1}, \ldots, \phi_{M}\right)\),$\square$选择为$\max$。

实验表明,利用每一层所产生的特征空间中的最近邻来重新计算近邻图是有用的。在每一层,都有不同的\(\mathcal{G}^{(l)}=\left(\mathcal{V}^{(l)}, \mathcal{E}^{(l)}\right)\),其中第$l$层的边的形式为\(\left(i, j_{i 1}\right), \ldots,\left(i, j_{i k_{l}}\right)\),也就是\(\mathbf{x}_{j_{i 1}}^{(l)}, \ldots, x_{j_{i k_{l}}}^{(l)}\)是距离\(\mathbf{x}_{i}^{(l)}\)最近的$k_{l}$个点。网络学习如何构造每层中的\(\mathcal{G}\),而不是在网络开始预测前就已经固定好了。在实现时在距离空间中计算距离矩阵,然后对每个单点取最近的$k$个点。

⚪ EdgeConv的性质

EdgeConv具有排序不变性(Permutation Invariance),考虑到每一层的输出为:

\[\mathbf{x}_{i}^{\prime}=\max _{j:(i, j) \in \mathcal{E}} h_{\boldsymbol{\Theta}}\left(\mathbf{x}_{i}, \mathbf{x}_{j}\right)\]

由于$\max$是一个对称函数,所以输出层\(\mathrm{x}_{i}^{\prime}\)相对于输入\(\mathbf{x}_{j}\)是排序不变的。全局最大池化操作对于聚合点特征也是排序不变的。

EdgeConv具有一部分的平移不变性(translation invariance),因为边函数公式不受平移的影响,也可以选择性的受平移影响。考虑在点\(\mathbf{x}_{j}\)和点\(\mathbf{x}_{i}\)上进行平移,当平移$T$时,有(省略ReLU):

\[\begin{aligned} e_{i j m}^{\prime}&=\boldsymbol{\theta}_{m} \cdot\left(\mathbf{x}_{j}+T-(\mathbf{x}_{i}+T)\right)+\boldsymbol{\phi}_{m} \cdot (\mathbf{x}_{i}+T) \\ &=\boldsymbol{\theta}_{m} \cdot\left(\mathbf{x}_{j}-\mathbf{x}_{i}\right)+\boldsymbol{\phi}_{m} \cdot (\mathbf{x}_{i}+T) \\ \end{aligned}\]

如果令\(\boldsymbol{\phi}_{m}=\mathbf{0}\)时,只考虑\(\mathbf{x}_{j}-\mathbf{x}_{i}\),那么该操作是完全平移不变的。但是模型会损失局部信息的获取。

⚪ DGCNN网络

def knn(x, k):
    inner = -2*torch.matmul(x.transpose(2, 1), x)
    xx = torch.sum(x**2, dim=1, keepdim=True)
    pairwise_distance = -xx - inner - xx.transpose(2, 1)
    idx = pairwise_distance.topk(k=k, dim=-1)[1]
    return idx  # (batch_size, num_points, k)

def get_graph_feature(x, k=20, idx=None):
    batch_size = x.size(0)
    num_dims   = x.size(1)
    num_points = x.size(2)
    x = x.view(batch_size, -1, num_points)
    if idx is None:
        idx = knn(x, k=k)   # (batch_size, num_points, k)
    idx_base = torch.arange(0, batch_size, device=x.device).view(-1, 1, 1)*num_points
    idx = idx + idx_base
    idx = idx.view(-1)
    
    x = x.transpose(2, 1).contiguous()   # (batch_size, num_points, num_dims)
    feature = x.view(batch_size*num_points, -1)[idx, :]
    feature = feature.view(batch_size, num_points, k, num_dims) 
    x = x.view(batch_size, num_points, 1, num_dims).repeat(1, 1, k, 1)
    feature = torch.cat((feature-x, x), dim=3).permute(0, 3, 1, 2).contiguous()
    return feature # (batch_size, 2*num_dims, num_points, k)

class DGCNN(nn.Module):
    def __init__(self, k=20, emb_dims =1024, cls=40, normal_channel=True):
        super(DGCNN, self).__init__()
        in_channel = 3 if normal_channel else 6
        self.k = k
        self.emb_dims = emb_dims
        self.output_channel = cls
        self.conv1 = nn.Sequential(nn.Conv2d(in_channel, 64, kernel_size=1, bias=False),
                                   nn.BatchNorm2d(64),
                                   nn.LeakyReLU(negative_slope=0.2))
        self.conv2 = nn.Sequential(nn.Conv2d(64*2, 64, kernel_size=1, bias=False),
                                   nn.BatchNorm2d(64)
                                   nn.LeakyReLU(negative_slope=0.2))
        self.conv3 = nn.Sequential(nn.Conv2d(64*2, 128, kernel_size=1, bias=False),
                                   nn.BatchNorm2d(128),
                                   nn.LeakyReLU(negative_slope=0.2))
        self.conv4 = nn.Sequential(nn.Conv2d(128*2, 256, kernel_size=1, bias=False),
                                   nn.BatchNorm2d(256),
                                   nn.LeakyReLU(negative_slope=0.2))
        self.conv5 = nn.Sequential(nn.Conv1d(512, self.emb_dims, kernel_size=1, bias=False),
                                   nn.BatchNorm1d(self.emb_dims),
                                   nn.LeakyReLU(negative_slope=0.2))
        self.linear1 = nn.Linear(self.emb_dims*2, 512, bias=False)
        self.bn6 = nn.BatchNorm1d(512)
        self.linear2 = nn.Linear(512, 256)
        self.bn7 = nn.BatchNorm1d(256)
        self.linear3 = nn.Linear(256, self.output_channel)
        self.reg=nn.LogSoftmax(dim=1)

    def forward(self, x):
        batch_size = x.size(0)
        x = get_graph_feature(x, k=self.k)
        x = self.conv1(x)
        x1 = x.max(dim=-1, keepdim=False)[0]

        x = get_graph_feature(x1, k=self.k)
        x = self.conv2(x)
        x2 = x.max(dim=-1, keepdim=False)[0]

        x = get_graph_feature(x2, k=self.k)
        x = self.conv3(x)
        x3 = x.max(dim=-1, keepdim=False)[0]

        x = get_graph_feature(x3, k=self.k)
        x = self.conv4(x)
        x4 = x.max(dim=-1, keepdim=False)[0]

        x = torch.cat((x1, x2, x3, x4), dim=1)

        x = self.conv5(x)
        x1 = F.adaptive_max_pool1d(x, 1).view(batch_size, -1)
        x2 = F.adaptive_avg_pool1d(x, 1).view(batch_size, -1)
        x = torch.cat((x1, x2), 1)

        x = F.leaky_relu(self.bn6(self.linear1(x)), negative_slope=0.2)
        x = F.leaky_relu(self.bn7(self.linear2(x)), negative_slope=0.2)
        x = self.linear3(x)
        x = self.reg(x)
        return x