DCP(Deep Closest Point)论文阅读笔记以及详析

DCP论文阅读笔记

前言

本文中图片仓库位于github,所以如果阅读的时候发现图片加载困难。建议挂个梯子。

作者博客:https://codefmeister.github.io/

转载前请联系作者。未经允许谢绝转载盗用。

论文

Deep Closest Point: Learning Representations for Point Cloud Registration
Author: Wang, Yue; Solomon, Justin

Main Attribution

基于ICP迭代最近点算法,提出基于深度学习的DCP算法。解决了ICP想要采用深度学习方法时遇到的一系列问题。

我们先回顾一下ICP算法的基本步骤:

for each iteration:
    find corresponding relations of points between two scan(using KNN)
    using SVD to solve Rotation Matrix and Translation vector
    update cloud Data

概括起来就是: 寻找最近点对关系,使用SVD求解刚体变换。如此循环往复。

结合论文,个人理解将ICP算法扩展到深度学习存在着以下的难点(可能存在各种问题,笔者深度学习的相关知识很薄弱):

  • 首先,点对关系如果是确定的话,沿着网络反向传播可能存在问题。
  • SVD分解求解刚体变换,如何求梯度?(Confirmed by paper)

而文章克服了这些问题,主要有如下贡献:

  • 提出了能够解决传统ICP算法试图推广时存在的困难的一系列子网络架构。
  • 提出了能进行pair-wise配准的网络架构
  • 评估了在采用不同设置的情况下的网络表现
  • 分析了是global feature有用还是local feature对配准更加有用

网络架构

模型包含三个部分:
(1) 一个将输入点云映射到高维空间embedding的模块,具有扰动不变性(指DGCNN当点云输入时点的前后顺序发生变化,输出不会有任何改变) 或者 刚体变换不变性(指PointNet对于旋转平移具有不变的特性)。该模块的作用是寻找两个输入点云之间的点的对应关系. 可选的模块有PointNet(Focus于全局特征), DGCNN(结合局部特征和全局特征)

(2) 一个基于注意力attention的Pointer网络模块,用于预测两个点云之间的soft matching关系(类似于一种基于概率的soft match,之所以soft是由于它并没有显式规定点 x i x_i xi必须与哪个点 x j x_j xj有对应关系,而是通过一个softmax得到的各点和某点 x i x_i xi存在对应关系的概率乘以各点数据,得到一个类似于概率的对应点坐标。 该模块采用的是Transformer

(3) 一个可微SVD分解层,用于输出刚体变换矩阵。

问题阐述

熟悉点云配准的同学应该知道,问题十分清晰。这里直接粘一下原文。

值得一提的是,作者分析了一下ICP的算法步骤。和我们上面描述的一样。就是用上次更新后的信息寻找最近关系,然后用寻找到的对应关系SVD求解得到 R , t R,t R,t. 所以如果初始值一开始生成的是很差的corresponding relation,那么一下就会陷入局部最优。

而作者的思路就是:使用学习的网络来得到特征,通过特征获得一个更好的对应关系 m ( ⋅ ) m(\cdot) m(),用这个 m ( ⋅ ) m(\cdot) m()去计算刚体变换信息。

代码分析与对应模块详解

我们采用一种Top-Down的视角来分析整个代码。先从整体入手,然后逐渐拆解模块进行分析。

整体模块

DCP网络结构分为三个Part,从代码中就可以很清晰的看出来:第一个Module模块emd_nn用于抽象特征,第二个Module模块pointer用于match,第三个Module模块head用于求解刚体变换矩阵,具体代码如下:

class DCP(nn.Module):
    def __init__(self, args):       # args 是一个存放各种参数的namespace
        super(DCP,self).__init__()
        self.emb_dims = args.embdims    # 欲抽象到的特征维度,default为 512
        self.cycle = args.cycle         # ba的刚体变换关系是否重新进入网络计算
        if args.emb_nn == 'pointnet':   # emb_nn就是上文所说的第一个模块,若选择PointNet
            self.emb_nn = PointNet(emb_dims=self.emb_dims)
        elif args.emb_nn == 'dgcnn':    # 若选择DGCNN
            self.emb_nn = DGCNN(emb_dims=self.emb_dims)
        else:
            raise Exception('Not implemented')      # 其他网络尚未实现

        if args.pointer == 'identity':              # 不使用Transformer, hard match
            self.pointer = Identity()
        elif args.pointer == 'transformer':         # soft matching by tranformer
            self.pointer = Transformer(args = args)
        else:
            raise Exception('Not implemented')  
        
        if args.head == 'mlp':                      # 直接用MLP预测输出矩阵
            self.head = MLPHead(args=args)
        elif args.head == 'svd':                    # 使用可微的SVD分解层
            self.head = SVDHead(args=args)
        else:
            raise Exception("Not implemented")
    
    def forward(self, *input):
        src = input[0]
        tgt = input[1]
        src_embedding = self.emb_nn(src)
        tgt_embedding = self.emb_nn(tgt)    # Module Part 1             (batch_size, emb_dims, num_points)

        src_embedding_p, tgt_embedding_p = self.pointer(src_embedding, tgt_embedding)  # Module Part 2

        src_embedding = src_embedding + src_embedding_p
        tgt_embedding = tgt_embedding + tgt_embedding_p

        rotation_ab, translation_ab = self.head(src_embedding, tgt_embedding, src, tgt) # Module Part 3
        if self.cycle:
            rotation_ba, translation_ba = self.head(tgt_embedding, src_embedding, tgt, src)
        
        else:
            rotation_ba = rotation_ab.transpose(2,1).contiguous()
            translation_ba = -torch.matmul(rotation_ba, translation_ab.unsqueeze(2)).squeeze(2)
        return rotation_ab, translation_ab, rotation_ba, translation_ba

用于抽象feature的Module1:emb_nn

考虑emb_nn,我们有两个选择: 其一是PointNet, 其二是DGCNN.

PointNet抽象的特征是global feature, 而DGCNN结合了local featureglobal feature.

我们希望得到的是对每一个点抽象而得的特征(即每一个点都有其embedding),并利用两个点云之间点的embedding来生成映射关系(即Match关系). 所以我们要得到的是per-point feature而不是one feature per cloud

出于上述原因,我们在最后一层的聚合函数aggregation function之前生成每个点的representation
F X = { x 1 L , x 2 L , . . . , x i L , . . . , x N L } F_X = \{x_1^L,x_2^L, ..., x_i^L,...,x_N^L\} FX={x1L,x2L,...,xiL,...,xNL}
F Y = { y 1 L , y 2 L , . . . , y i L , . . . , y N L } F_Y = \{y_1^L, y_2^L, ..., y_i^L, ..., y_N^L\} FY={y1L,y2L,...,yiL,...,yNL}
上标L代表第L层的输出(假定共有L层)。

PointNet

x i l x_i^l xil是第 i i i个点在第 l l l层后的embedding,而 h θ l h_{\theta}^l hθl是第 l l l层的非线性映射函数。PointNet的forward mechanism可以用如下公式给出:
x i l = h θ l ( x i l − 1 ) x_i^l = h_{\theta}^l(x_i^{l-1}) xil=hθl(xil1)
作者@WangYue在github上公布的代码,使用的PointNet的网络架构如下:

class PointNet(nn.Module):
    def __init__(self, emb_dims=512):
        super(PointNet, self).__init__()
        self.conv1 = nn.Conv1d(3, 64, kernel_size=1, bias=False)
        self.conv2 = nn.Conv1d(64, 64, kernel_size=1, bias=False)
        self.conv3 = nn.Conv1d(64, 64, kernel_size=1, bias=False)
        self.conv4 = nn.Conv1d(64, 128, kernel_size=1, bias=False)
        self.conv5 = nn.Conv1d(128, emb_dims, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm1d(64)
        self.bn2 = nn.BatchNorm1d(64)
        self.bn3 = nn.BatchNorm1d(64)
        self.bn4 = nn.BatchNorm1d(128)
        self.bn5 = nn.BatchNorm1d(emb_dims)

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        x = F.relu(self.bn4(self.conv4(x)))
        x = F.relu(self.bn5(self.conv5(x)))
        return x

从上述代码中,可以看出,作者使用的PointNet并没有input-transformfeature-transform这两个Module,相当于只应用MLP不断对输入点云进行抽象,直到高维空间。

存疑:为什么不加Transform-Net? 如果加上效果训练效果如何? 没有cat,cat之后效果如何?

DGCNN

DGCNN是作者@WangYue提出的一种网络架构,其特点是EdgeConv。可以结合全局特征与局部特征。
x i l = f ( { h θ l ( x i l − 1 , x j l − 1 ) ; ∀ j ∈ N i } ) x_i^l = f({\{} h_{\theta}^l(x_i^{l-1},x_j^{l-1}); \forall j \in N_i {\}}) xil=f({hθl(xil1,xjl1);jNi})
f f f是每一层后的聚合函数。 N i N_i Ni指的是和点 x i x_i xi存在KNN关系的点的集合。

get_graph_feature是返回egde-feature的函数。这并不是我们关注的重点,代码简要粘贴一下。

def knn(x, k):
    inner = -2 * torch.matmul(x.transpose(2, 1).contiguous(), x)
    xx = torch.sum(x ** 2, dim=1, keepdim=True)
    pairwise_distance = -xx - inner - xx.transpose(2, 1).contiguous()

    idx = pairwise_distance.topk(k=k, dim=-1)[1]  # (batch_size, num_points, k)
    return idx


def get_graph_feature(x, k=20):
    # x = x.squeeze()
    idx = knn(x, k=k)  # (batch_size, num_points, k)
    batch_size, num_points, _ = idx.size()
    device = torch.device('cuda')

    idx_base = torch.arange(0, batch_size, device=device).view(-1, 1, 1) * num_points

    idx = idx + idx_base

    idx = idx.view(-1)

    _, num_dims, _ = x.size()

    x = x.transpose(2,
                    1).contiguous()  # (batch_size, num_points, num_dims)  -> (batch_size*num_points, num_dims) #   batch_size * num_points * k + range(0, batch_size*num_points)
    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), dim=3).permute(0, 3, 1, 2)

    return feature

而网络中使用的DGCNN代码如下:

class DGCNN(nn.Module):
    def __init__(self, emb_dims=512):
        super(DGCNN, self).__init__()
        self.conv1 = nn.Conv2d(6, 64, kernel_size=1, bias=False)
        self.conv2 = nn.Conv2d(64, 64, kernel_size=1, bias=False)
        self.conv3 = nn.Conv2d(64, 128, kernel_size=1, bias=False)
        self.conv4 = nn.Conv2d(128,256, kernel_size=1, bias=False)
        self.conv5 = nn.Conv2d(512, emb_dims, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.bn2 = nn.BatchNorm2d(64)
        self.bn3 = nn.BatchNorm2d(128)
        self.bn4 = nn.BatchNorm2d(256)
        self.bn5 = nn.BatchNorm2d(emb_dims)

    def forward(self,x):
        batch_size, num_dims, num_points = x.size()
        x = get_graph_feature(x)
        x = F.relu(self.bn1(self.conv1(x)))
        x1 = x.max(dim=-1, keepdim=True)[0]

        x = F.relu(self.bn2(self.conv2(x)))
        x2 = x.max(dim=-1, keepdim=True)[0]

        x = F.relu(self.bn3(self.conv3(x)))
        x3 = x.max(dim=-1, keepdim=True)[0]

        x = F.relu(self.bn4(self.conv4(x)))
        x4 = x.max(dim=-1, keepdim=True)[0]

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

        x = F.relu(self.bn5(self.conv5(x))).view(batch_size, -1, num_points)    
        return x            # (batch_size, emb_dims, num_points)

可以明显发现与原DGCNN不同的地方是: 作者这里每次forward前传时,并没有再对抽象出来的feature寻找knn进行进一步抽象。而是单纯的不断经过MLP。 对比一下该部分原代码:

        x = get_graph_feature(x, k=self.k)      # (batch_size, 3, num_points) --> (batch_size, 3*2, num_points, k)
        x = self.conv1(x)                       # (batch_size, 3*2, num_points, k) --> (batch_size, 64, num_points, k)
        x1 = x.max(dim=-1, keepdim=False)[0]    # (batch_size, 64, num_points, k) --> (batch_size, 64, num_points)
        
        x = get_graph_feature(x1, k=self.k)     # (batch_size, 64, num_points) --> (batch_size, 64*2, num_points, k)
        x = self.conv2(x)                       # (batch_size, 64*2, num_points, k) --> (batch_size, 64, num_points, k)
        x2 = x.max(dim=-1, keepdim=False)[0]    # (batch_size, 64, num_points, k) --> (batch_size, 64, num_points)
        
        x = get_graph_feature(x2, k=self.k)     # (batch_size, 64, num_points) --> (batch_size, 64*2, num_points, k)
        x = self.conv3(x)                       # (batch_size, 64*2, num_points, k) --> (batch_size, 128, num_points, k)
        x3 = x.max(dim=-1, keepdim=False)[0]    # (batch_size, 128, num_points, k) --> (batch_size, 128, num_points)
        
        x = get_graph_feature(x3, k=self.k)     # (batch_size, 128, num_points) --> (batch_size, 128*2, num_points, k)
        x = self.conv4(x)                       # (batch_size, 128*2, num_points, k) --> (batch_size, 256, num_points, k)
        x4 = x.max(dim=-1, keepdim=False)       # (batch_size, 256, num_points, k) --> (batch_size, 256, num_points)
        
        x = torch.cat((x1, x2, x3, x4), dim=1)  # (batch_size, 64+64+128+256, num_points)

差别十分明显。相当于网络结构中红框的部分消失了:

此处同样存疑,作者在论文里未提及此细节。

用于Match(寻找点对关系)的Module2

基于Attention机制的Transformer

使用Attention机制的初衷在于:我们想让配准变得更加task specify。也就是说,不再独立地关注两个输入点云 X X X, Y Y Yembedding feature,而是关注 X X X, Y Y Y之间的一些联合特性。于是乎,自然而然的想到了Attention机制。基于attention,设计一个可以捕捉self-attentionconditional attention的模块,用于学习 X , Y X,Y X,Y点云之间的某些联合信息。

我们将由上一个Module对两个点云各自独立生成的embedding特征 F X , F Y F_X,F_Y FX,FY作为输入,那么就有:
Φ X = F X + ϕ ( F X , F Y ) \Phi_X = F_X + \phi(F_X,F_Y) ΦX=FX+ϕ(FX,FY)
Φ Y = F Y + ϕ ( F Y , F X ) \Phi_Y = F_Y + \phi(F_Y,F_X) ΦY=FY+ϕ(FY,FX)
其中, ϕ \phi ϕ是Transformer学习得到的映射函数: ϕ : R N × P × R N × P → R N × P \phi: R^{N \times P} \times R^{N \times P} \to R^{N \times P} ϕ:RN×P×RN×PRN×P.

我们将 ϕ \phi ϕ当做一个残差项,基于 F X , F Y F_X,F_Y FX,FY的输入顺序,为 F X , F Y F_X,F_Y FX,FY提供一个附加的改变项。

Notice we treat ϕ \phi ϕ as a residual term, providing an additive change to F X F_X FX and F Y F_Y FY depending on the order of its input.

之所以采取将 F X → Φ X F_X \to \Phi_X FXΦXMotivation:以一种适应 Y Y Y中点的组织结构(个人理解即输入顺序)的方式改变 X X XFeature embedding。对 F Y → Φ Y F_Y \to \Phi_Y FYΦY,动机相同。

The idea here is that the map F X → Φ X F_X \to \Phi_X FXΦX modifies the features associated to the points in X in a fashion that is knowledgeable about the structure of Y Y Y.

选择Transformer提供的非对称函数作为 ϕ \phi ϕ。Transformer由一些堆叠的encoder-decoder组成,是一种解决Seq2Seq问题的经典架构。关于Transformer的更多信息,移步我的另一篇博客《图解Transformer(译)》

此Module中,encoder接收 F X F_X FX并通过self-attention layerMLP把它编码到其embedding space,而decoder有两个部分组成,第一个部分接收另一个集合 F Y F_Y FY, 然后像encoder一样将之编码到embedding space。另一个部分使用co-attention对两个已经映射到embedding space的点云进行处理。 所以输出 Φ Y \Phi_Y ΦY, Φ Y \Phi_Y ΦY既含有 F X F_X FX的信息,又含有 F Y F_Y FY的信息。

这里的Motivation是:将两个点云之间的匹配关系问题(match problem)类比为Sq2Sq问题。(点云是在两个输入的点的序列中寻找对应关系,而Sq2Sq问题是在输入句子中寻找单词之间的联系关系)。

为了避免不可微分的hard assignment,我们使用概率角度的一种方式来生成soft map,将一个点云映射到另一个点云。所以,每一个 x i ∈ X x_i \in X xiX都被赋予了一个概率向量:
m ( x i , Y ) = s o f t m a x ( Φ y Φ x i T ) m(x_i,Y) = softmax(\Phi_y \Phi_{x_i}^T) m(xi,Y)=softmax(ΦyΦxiT)
在这里, Φ Y ∈ R N × P \Phi_Y \in R^{N \times P} ΦYRN×P代表Y经过Attention Module后生成的embedding。而 Φ x i \Phi_{x_i} Φxi代表矩阵 Φ X \Phi_X ΦX的第 i i i行。 所以可以将 m ( x i , Y ) m(x_i, Y) m(xi,Y) 看做一个将每个 x i x_i xi映射到 Y Y Y中元素的soft pointer

下面我们关注文中Transformer的实现。其架构为:

class Transformer(nn.Module):
    def __init__(self, args):
        super(Transformer, self).__init__()
        self.emb_dims = args.emb_dims
        self.N = args.n_blocks
        self.dropout = args.dropout
        self.ff_dims = args.ff_dims     # Feed_forward Dims
        self.n_heads = args.n_heads     # Multihead Attention的头数
        c = copy.deepcopy()
        attn = MultiHeadedAttention(self.n_heads, self.emb_dims)
        ff = PositionwiseFeedForward(self.emb_dims, self.ff_dims, self.dropout)
        self.model = EncoderDecoder(Encoder(EncoderLayer(self.emb_dims, c(attn), c(ff), self.dropout),self.N),
                                    Decoder(DecoderLayer(self.emb_dims, c(attn), c(attn), c(ff), self.dropout), self.N),
                                    nn.Sequential(),
                                    nn.Sequential(),
                                    nn.Sequential())
        
    
    def forward(self, *input):
        src = input[0]      # batch_size, emb_dims, num_points
        tgt = input[1]
        src = src.transpose(2, 1).contiguous()      # batch_size, num_points, emb_dims
        tgt = tgt.transpose(2, 1).contiguous()
        tgt_embedding = self.model(src, tgt, None, None).transpose(2, 1).contiguous()
        src_embedding = self.model(tgt, src, None, None).transpose(2, 1).contiguous()
        return src_embedding, tgt_embedding
                

上述代码是Transformer的实现。看起来有点绕,我们进一步关注其forward函数.

流入Transformer的data: src,tgt: (batch_size, emb_dims, num_points)
经过transopose.contiguous: src,tgt: (batch_size, num_points, emb_dims)
随后将src, tgt传入self.model,得到了tgt_embedding, src_embedding.

关注self.model,在__init__中定义了self.model:

    self.model = EncoderDecoder(Encoder(EncoderLayer(self.emb_dims, c(attn), c(ff), self.dropout), self.N),
                                Decoder(DecoderLayer(self.emb_dims, c(attn), c(attn), c(ff), self.dropout),self.N),
                                nn.Sequential(),
                                nn.Sequential(),
                                nn.Sequential())

self.model 整体是一个EncoderDecoder类。其传入的参数有一个Encoder,一个Decoder,三个Sequential.

Encoder传入的参数有两个,第一个是EncoderLayer,第二个是self.N。作为第一个参数的EncoderLayer传入了四个参数,分别是self.emb_dims, c(attn), c(ff), self.dropout.

Decoder传入的参数也是两个,第一个是DecoderLayer,第二个是self.N。作为第一个参数的DecoderLayer传入了五个参数,分别是self.emb_dims, c(attn), c(attn), c(ff), self.dropout.

这里的ccopy.deepcopy(),即深拷贝。完全复制一个新的对象,所以这些网络之间参数并不共享。

首先来关注EncoderDecoder类:

class EncoderDecoder(nn.Module):
    def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.tgt_embed = src_embed
        self.generator = generator

    def forward(self, src, tgt, src_mask, tgt_mask):
        # Take in and process masked src and target sequences
        return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)

    def encode(self, src, src_mask):
        return self.encoder(self.src_embed(src), src_mask)

    def decode(self, memory, src_mask, tgt, tgt_mask):
        return self.generator(self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask))

从上述代码可以看出,EncoderDecoder类构造时传入的五个参数分别为: encoder, decoder, src_embed, tgt_embed, generator.

其前传机制forwardself.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)。即先将src,src_mask进行encode,将编码后的结果同src_mask, tgt, tgt_mask一同decode

encode函数,是这样定义的: self.encoder(self.src_embed(src), src_mask), 即先将src通过src_embed网络,然后根据其mask再通过encoder网络。

decode函数,是:self.generator(self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)), 也就是说, tgt先经过tgt_embed网络,然后随memory, src_mask, tgt_mask一同传入decoder网络,decoder网络的输出再流入generator网络。

所以我自己梳理了一下整个EncoderDecoder大概结构如下:

进一步关注EncoderDecoder

def clones(module, N):
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])


class Encoder(nn.Module):
    def __init__(self, layer, N):
        super(Encoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

从上述代码,可以看出: Encoder在构造时需要传入两个参数,一个为layer, 一个为N。而在构造函数中,通过调用clones方法将传入的layer深复制(deepcopy)了N次,并作为一个ModuleList存储在self.layers成员变量中。

clones方法的执行效果可从下例中窥见:

net = nn.Sequential(nn.Linear(5,7), nn.BatchNorm1d(5))
print('net',net)
net_clones = clones(net, 3)
print('net_clones', net_clones)

其执行结果是:将net复制了3次,装在一个ModuleList中返回。并且值得一提的是,这里是copy.deepcopy(),深复制,所以参数之间不共享。

而LayerNorm定义如下:

class LayerNorm(nn.Module):
    def __init__(self, feature, eps=1e-6):
        super(LayerNorm, self).__init__()
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))
        self.eps = eps
    
    def forward(self, x):
        mean = x.mean(-1, keepdim=True)         # 最后一维的均值
        std = x.std(-1, keepdim=True)           # 最后一维的标准差
        return self.a_2 * (x - mean) / (std + self.eps) + self.b_2      # 对最后一维进行Norm

可见LayerNorm的作用是对输入的数据x,对其最后一维进行归一化操作。

而Encoder整个的前传机制为:对于构造时生成的ModuleList,依次将x通过ModuleList中的每个网络layer,即x = layer(x, mask), 然后再将输出通过LayerNorm对最后一维进行归一化操作返回。 Encoder的网络结构图如下:

图9:Encoder的网络结构

进一步关注,EncoderEncoderDecoder构造时,传入的layer参数为:EncoderLayer(self.emb_dims, c(attn), c(ff), self.dropout)。 解读EncoderLayer:

class EncoderLayer(nn.Module):
    def __init__(self, size, self_attn, feed_forward, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 2)
        self.size = size
    
    def forward(self, x, mask):
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        return self.sublayer[1](x, self.feedforward)

EncoderLayer在构造时需要传入的参数有:size, self_attn, feed_forward, dropout。 其中self_attn, feed_forward两个网络,以及size作为成员变量存储。而另一个成员变量sublayer通过clones方法将SublayerConnection(size, dropout)复制两遍,存着一个ModuleList.

观察SublayerConnection:

class SublayerConnection(nn.Module):
    def __init__(self, size, dropout = None):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
    
    def forward(self, x, sublayer):
        return x + sublayer(self.norm(x))

SublayerConnection在构造时,只传入了size, dropout两个参数,用于构造LayerNorm。而前传forward的时候,返回的是x + sublayer(self.norm(x)), 即将x通过了LayerNorm后,再通过作为参数传入的网络sublayer,最后与x相加,返回。

搞清楚SublayerConnection的机制后,我们回看EncoderLayerforward机制:x先通过一个传入网络为attnsublayer,然后再通过一个传入网络为feedforwardsublayer. EncoderLayer网络结构如图:

图10: EncoderLayer 网络结构图

接下来关注attn,即MultiHeadedAttention:

def attention(query, key, value, mask=None, dropout=None):
    d_k = query.size(-1)
    scores = torch.matmul(query, key.transpose(-2, -1).contiguous()) / math.sqrt(d_k)   # (nbatches, h, num_points, num_points)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    p_attn = F.softmax(scores, dim=-1)  # (nbatches, h, num_points, num_points)
    return torch.matmul(p_attn, value), p_attn


class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout = 0.1):
        # h: number of heads ;  d_model: dims of model(emb_dims)
        super(MultiHeadedAttention, self).__init__()
        assert d_model % h == 0
        # we assume d_v always equals d_k
        self.d_k = d_model // h         # d_k 是每个head的dim
        self.h = h
        self.linears = clones(nn.Linear(d_model, d_model), 4)
        self.attn = None
        self.dropout = None

    def forward(self, query, key, value, mask=None):
        if mask is not None:
            # Same mask applied to all h heads
            mask = mask.unsqueeze(1)
        nbatches = query.size(0)

        # 1) Do all the linear projections in batch from d_model => h x d_k
        query, key, value = [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2).contiguous() for l,x in zip(self.linears, (query, key, value))]      # (nbatches, h, num_points, d_k)

        # 2) Apply attention on all the projected vectors in batch.
        x, self.attn = attention(query, key, value, mask = mask, dropout= self.dropout)

        # 3) "Concat" using a view and apply a final linear.
        x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
        return self.linears[-1](x)

MultiHeadAttention其构造函数有两个参数:h, d_model. 其中hhead的个数,而d_model实际上就是emb_dims。我们总是规定emb_dims是可以整除h的,否则每个attention的维度不是整数。d_k = d_model // h即是每个head的维度。同时在构造时,在self.linears中存储了一个ModuleList,ModuleList中有四个Linear线性映射d_model --> d_model

MultiHeadAttention的前传机制:

(1) 通过线性映射linear projection,生成query, key, value.

zip方法将三个线性映射绑定到query, key, value上,相当于指定了其生成的矩阵。(这里的query, key, value只是用于生成query, key, value的原始数据,事实上都是x)。 将query, key, value分别通过对应的Linear Projection投影生成真正的query, key, value。 输入的xshapenbatches, num_points, emb_dims (emb_dimd_model). 经过对应的Linear Projection后,生成的shapenbatches, num_points, emb_dims, 通过view()变为nbatches, num_points, self.h, self.d_k。 随后又进行了transpose(1,2).contiguous(), 那么最后生成的query, key, valueshapenbatches, self.h, num_points, self.d_k.

需要说明的是,按照Transformer的理论,MultiHeadAttention的生成矩阵(即我们在上面用的投影应该是每个head有一个单独的projection),但是因为这个projection是学习得到的,所以我们只用一个projection然后再进行view()分割得到MultiHead,在理论上应该能得到相同的效果。

(2) 根据得到的query, key, value计算Self-Attention.

Self-Attention的计算: s o f t m a x ( Q × K T d k ) V softmax(\frac{Q \times K^T}{\sqrt{d_k}}) V softmax(dk Q×KT)V

返回的z的shape为:nbatches, self.h, num_points, self.d_k

(3) 通过view进行所谓的Concatenate,将之应用于Linear网络,输出。

首先进行一个transpose(1,2),随后改变其内存分布contiguous,然后再通过view(),相当于把多个头的attention拼接起来。此时的shape为:nbatches, num_points, h * d_k.

再应用于第四个Linear上,输出的shape: (nbatches, num_points, d_model)

整个Attention的网络结构如下:

Attention的网络结构(Mask并无标出)

下面关注PositionwiseFeedForward网络结构:

class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout = 0.1):
        super(PositionwiseFeedForward, self).__init__()
        self.w_1 = nn.Linear(d_model, d_ff)
        self.norm = nn.Sequential()
        self.w_2 = nn.Linear(d_ff, d_model)
        self.dropout = None

    def forward(self, x):
        x = F.relu(self.w_1(x)).transpose(2,1).contiguous()
        x = self.norm(x).transpose(2,1).contiguous()
        x = self.w_2(x)
        return x

PositionwistFeedForward的网络结构比较简单,不再单独分析。
整个Encoder的结构分析完毕。 下面再关注一下Decoder:

class Decoder(nn.Module):
    def __init__(self, layer, N):
        super(Decoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, memory, src_mask, tgt_mask):
        for layer in self.layers:
            x = layer(x, memory, src_mask, tgt_mask)
        return self.norm(x)


class DecoderLayer(nn.Module):
    # Decoder is made of self-attn, src-attn, and feed forward(defined below)

    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
        super(DecoderLayer, self).__init__()
        self.size = size
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 3)

    def forward(self, x, memory, src_mask, tgt_mask):
        m = memory
        x = self.sublayer[0](x, lambda x: self.self_attn(x,x,x,tgt_mask))
        x = self.sublayer[1](x, lambda x: self.src_attn(x,m,m,src_mask))
        return self.sublayer[2](x, self.feed_forward)

Decoder的网络结构如图:

Decoder网络结构图

而DecoderLayer,接收的变量有x,memory,src_mask,tgt_mask,相当于先计算self-attention,再计算co-attention,最后feed-forward,其网络结构为:

至此Transformer应该已经分析清楚了。

用于SVD求解的模块Module3

我们的最终目的是求出刚体变换矩阵。使用上一个Module中计算出的soft pointer,可以生成一个平均意义上的match point
y i ^ = ( Y m ) T m ( x i , Y ) \hat{y_i} = (Y_m)^T m(x_i,Y) yi^=(Ym)Tm(xi,Y)
这里, Y m Y_m Ym是指一个 R N × 3 R^{N \times 3} RN×3的矩阵,包含着 Y Y Y中所有点的信息。 根据这样一种match关系,便可以通过SVD求解出刚体变换矩阵。

为了能够使得梯度反向传播,我们必须对SVD进行求导,由于我们使用SVD求解的只是3x3,可以使用其导数的近似形式。SVD求导的详细请参考论文:

Estimating the jacobian of the singular value decomposition: Theory and applications

class SVDHead(nn.Module):
    def __init__(self, args):
        super(SVDHead, self).__init__()
        self.emb_dims = args.emb_dims
        self.reflect = nn.Parameter(torch.eye(3), requires_grad=False)
        self.reflect[2, 2] = -1
    
    def forward(self, *input):
        src_embedding = input[0]
        tgt_embedding = input[1]
        src = input[2]
        tgt = input[3]
        batch_size = src.size(0)

        d_k = src_embedding.size(1)
        scores = torch.matmul(src_embedding.transpose(2, 1).contiguous(), tgt_embedding) / math.sqrt(d_k)
        scores = torch.softmax(scores, dim=2)

        src_corr = torch.matmul(tgt, scores.transpose(2, 1).contiguous())

        src_centered = src - src_mean(dim=2, keepdim=True)

        src_corr_centered = src_corr - src_corr.mean(dim=2, keepdim = True)

        H = torch.matmul(src_centered, src_corr_centered.transpose(2,1).contiguous())

        U,S,V = [],[],[]
        R = []

        for i in range(src.size(0)):
            u, s, v = torch.svd(H[i])
            r = torch.matmul(v, u.transpose(1, 0).contiguous())
            r_det = torch.det(r)
            if r_det < 0:
                u, s, v = torch.svd(H[i])
                v = torch.matmul(v, self.reflect)
                r = torch.matmul(v, u.transpose(1, 0).contiguous())

            R.append(r)

            U.append(u)
            S.append(s)
            V.append(v)

        U = torch.stack(U, dim=0)
        V = torch.stack(V, dim=0)
        S = torch.stack(S, dim=0)
        R = torch.stack(R, dim=0)

        t = torch.matmul(-R, src.mean(dim=2, keepdim = True)) + src_corr.mean(dim=2, keepdim=True)

        return R, t.view(batch_size, 3)
        

疑惑: 没在源码中看到有针对SVD求导的优化。

损失函数

L o s s = ∣ ∣ R X Y T R X Y g − I ∣ ∣ 2 + ∣ ∣ t X Y − t X Y g ∣ ∣ 2 + λ ∣ ∣ θ ∣ ∣ 2 Loss = ||R_{XY}^TR_{XY}^g - I||^2 + ||t_{XY} - t_{XY}^g||^2 + \lambda||\theta||^2 Loss=RXYTRXYgI2+tXYtXYg2+λθ2

关于DCP_v1和DCP_v2

DCP_v1 没有应用Attention机制。

DCP_v2 应用了Attention机制。

深入探究

关于特征提取:选择PointNet还是DGCNN?

PointNet 学习的是全局特征, 而DGCNN通过构建k-NN Graph学习到的是局部集合特征。

从文中的实验结果可以看出,使用DGCNN作为emb_net,比PointNet的性能始终要好。

关于计算刚体变换:选择MLP还是SVD?

MLP在理论上可以模拟任何非线性映射。 而SVD是针对任务进行有目的性设计的网络。

从文中的实验结果可以看出,使用SVD计算rigid transformation总是更优。

结语

以上就是我个人对DCP的笔记记录以及一些解读。

如果觉得文章对您有帮助,欢迎点赞留言交流。给作者买杯咖啡就更感激不过了!

  • 25
    点赞
  • 71
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
DCP(Distributed Constraint Problem)算法是一种用于解决分布式约束问题的算法。在Python中,您可以使用分布式计算库(如Pyro或Celery)来实现DCP算法。下面是一个简单的示例代码: ``` import random from distributed import Client, LocalCluster # 定义约束函数 def constraint_function(x, y): return x + y # 定义DCP算法 def dcp_algorithm(): # 创建本地集群 cluster = LocalCluster() client = Client(cluster) # 生成随机的输入 x = random.randint(1, 10) y = random.randint(1, 10) # 将输入分发到集群中的多个工作节点 future_x = client.scatter(x) future_y = client.scatter(y) # 在集群中执行约束函数 result = client.submit(constraint_function, future_x, future_y) # 获取计算结果 result = client.gather(result) # 关闭集群和客户端 client.close() cluster.close() return result # 运行DCP算法 result = dcp_algorithm() print("Result:", result) ``` 这个示例代码中,我们首先定义了一个约束函数`constraint_function`,然后实现了一个简单的DCP算法`dcp_algorithm`。在`dcp_algorithm`函数中,我们使用`LocalCluster`创建了一个本地集群,并通过`Client`连接到集群。然后,我们生成了随机的输入,并使用`client.scatter`将输入分发到集群中的多个工作节点。接下来,我们使用`client.submit`在集群中执行约束函数,并使用`client.gather`获取计算结果。最后,我们关闭了集群和客户端,并返回计算结果。 请注意,这只是一个简单的示例,实际使用DCP算法时,您可能需要根据具体的问题和约束函数进行相应的修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值