DETR: End-to-End Object Detection with Transformers 网络解析
说明:
资源:
如上图所示,左图为DETR中的transformer结构,右图为文章Attention Is All You Need中的结构图,基本上还是一致的。
李宏毅老师讲到transformer实际上是seq2seq model with ‘self-attention’,所以下面着重来讲一下DETR里面涉及到的一些细节问题
其实NLP中transformer需要处理的是一些序列数据,那么为了处理序列数据首先可以想到的就是RNN结构,这种结构考虑了上下文关系,相对于CNN(感受野问题)来讲具有优势。但是如下图所示的RCNN模块存在一个问题:任务难以并行
因为an to bn 的计算依赖 an-1 to bn-1的中间结果,这就意味着任务必须是串行的,这对于目前所说的大数据,并行计算,云计算来讲是不合适的。而相对于RNN来讲CNN则更加适合并行计算,那么self-attention模块就是一个典型的合理解。如下图所示:
那么这里的核心思想就是怎么去构建这个self-attention结构。(x1, x2, x3, x4)表示我们的输入,(b1, b2, b3, b4)表示我们的输出
这里q, k, v可以按照词袋模型的思路来理解,那么q是输入的矩阵,而k则是数据字典,那么v就是我们将将输入的矩阵q通过对应数据字典进行编码后形成的新的特征矩阵。因此他们的公式可以分别表示为
q i = W q a i q^i = W^qa^i qi=Wqai
k i = W k a i k^i = W^ka^i ki=Wkai
v i = W v a i v^i = W^va^i vi=Wvai
有了这个数学表示,那么接下来我们需要的是拿q1与(k1, k2, k3, k4)进行点乘,然后是q2, q3, q4,这个过程表示如下
其中
α 1 , i = q 1 ⋅ k i / d \alpha_{1,i} = q^1\cdot k^i/ \sqrt{d} α1,i=q1⋅ki/d
α ^ 1 , i = e x p ( α 1 , i ) / ∑ j e x p ( α 1 , j ) \hat{\alpha}_{1,i} = exp(\alpha_{1,i})/\sum_j{exp(\alpha_{1,j})} α^1,i=exp(α1,i)/j∑exp(α1,j)
d是q和k的维度。到这里我们获得了输入x1与四个数据字典k相关的“权值”,那么为了获得最后的“词袋模型编码后的向量”,就需要alpha-head与v进行一定的操作,如下图所示
b 1 = ∑ i α ^ 1 , i v i b^1 = \sum_i{\hat{\alpha}_{1,i}v^i} b1=i∑α^1,ivi
同理很容易就可以计算出(b1, b2, b3, b4),具体的矩阵推导可以查看李宏毅老师的视频和PPT,很容易可以理解为什么说这个结构可以替代RNN进行并行加速。而multi-head self-attention就很好理解了,就是多层head的堆叠,这是深度学习中很常见的网络构建方式,下图是一个2 heads的例子,可以与上图进行对比,很明显可以明白差异。
这里Add就是矩阵的加法,Norm指的是Layer Norm,其主要是Batch Norm是在一个batch之间来正则化,而Layer Norm则只是考虑一个图上的正则化。具体可以阅读文献。
DETR中的FFN实质上就是FC+ReLu+FC这种形式
引入这个张量的原因是因为输入Transformer的张量被转换成了[c, HW],对于图像来说就失去了像素的空间分布信息,这不符合Transformer处理序列数据的初衷,那么就势必要引入位置编码。
这个张量作者做了两种尝试,不过由于实验效果基本一致,所以就采用了人工生成的方式。
一种是学习得到:
class PositionEmbeddingLearned(nn.Module):
"""
Absolute pos embedding, learned.
"""
def __init__(self, num_pos_feats=256):
super().__init__()
self.row_embed = nn.Embedding(50, num_pos_feats)
self.col_embed = nn.Embedding(50, num_pos_feats)
self.reset_parameters()
def reset_parameters(self):
nn.init.uniform_(self.row_embed.weight)
nn.init.uniform_(self.col_embed.weight)
def forward(self, tensor_list: NestedTensor):
x = tensor_list.tensors
h, w = x.shape[-2:]
i = torch.arange(w, device=x.device)
j = torch.arange(h, device=x.device)
x_emb = self.col_embed(i)
y_emb = self.row_embed(j)
pos = torch.cat([
x_emb.unsqueeze(0).repeat(h, 1, 1),
y_emb.unsqueeze(1).repeat(1, w, 1),
], dim=-1).permute(2, 0, 1).unsqueeze(0).repeat(x.shape[0], 1, 1, 1)
return pos
第二种是人工生成:
class PositionEmbeddingSine(nn.Module):
"""
This is a more standard version of the position embedding, very similar to the one
used by the Attention is all you need paper, generalized to work on images.
"""
def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None):
super().__init__()
self.num_pos_feats = num_pos_feats
self.temperature = temperature
self.normalize = normalize
if scale is not None and normalize is False:
raise ValueError("normalize should be True if scale is passed")
if scale is None:
scale = 2 * math.pi
self.scale = scale
def forward(self, tensor_list: NestedTensor):
x = tensor_list.tensors
mask = tensor_list.mask
assert mask is not None
not_mask = ~mask
y_embed = not_mask.cumsum(1, dtype=torch.float32)
x_embed = not_mask.cumsum(2, dtype=torch.float32)
if self.normalize:
eps = 1e-6
y_embed = y_embed / (y_embed[:, -1:, :] + eps) * self.scale
x_embed = x_embed / (x_embed[:, :, -1:] + eps) * self.scale
dim_t = torch.arange(self.num_pos_feats, dtype=torch.float32, device=x.device)
dim_t = self.temperature ** (2 * (dim_t // 2) / self.num_pos_feats)
pos_x = x_embed[:, :, :, None] / dim_t
pos_y = y_embed[:, :, :, None] / dim_t
pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3)
pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3)
pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2)
return pos
人工生成位置编码的方式还是延续了NLP中Transformer位置编码的生成方式,不同的是因为图像是三维[C, H, W],因此除了通道维之外DETR在H和W方向上分别进行了编码。核心公式表示为
P E p o s , 2 i = s i n ( p o s / 1000 0 2 i / d m o d e l ) PE_{pos,2i} = sin(pos/10000^{2i/d_{model}}) PEpos,2i=sin(pos/100002i/dmodel)
P E p o s , 2 i + 1 = c o s ( p o s / 1000 0 2 i / d m o d e l ) PE_{pos,2i+1} = cos(pos/10000^{2i/d_{model}}) PEpos,2i+1=cos(pos/100002i/dmodel)
至于说为什么这样子编码可以表示位置不同,可以参考这个博客
这个张量是在解码过程中引入的,它的维度和输出的目标集合数量是一致的,可以大致理解为“向量表示的图像上的关注点”。DETR中是通过学习得到的,初始化代码如下所示
self.query_embed = nn.Embedding(num_queries, hidden_dim)
调用的时候
hs = self.transformer(self.input_proj(src), mask, self.query_embed.weight, pos[-1])[0]
不同于常见的检测器,DETR没有使用NMS,通常来讲预测出来的目标集合为N,每个元素是(类别, 坐标)即(c, b)。而真值NGT的数目通常来讲每张图像上数目是不同的,这里就引入了几个问题:
看到网上很多分析DETR损失函数的,所以这里我就不介绍了,如果有需要再说吧。不过值得一提的是,DETR也用到了这个思想:从不同深度提取特征图进行损失计算,可以缩短损失反向传播到不同深度的路径,降低梯度消失造成的影响,加快网络收敛并在一定程度上提高精度。
暂时更新到这里…