3.关于Detr

关于Detr

模型架构

3.关于Detr_第1张图片

总体架构
class Transformer(nn.Module):

    def __init__(self, d_model=512, nhead=8, num_encoder_layers=6,
                 num_decoder_layers=6, dim_feedforward=2048, dropout=0.1,
                 activation="relu", normalize_before=False,
                 return_intermediate_dec=False):
        super().__init__()
		# encoder层
        encoder_layer = TransformerEncoderLayer(d_model, nhead, dim_feedforward,dropout, activation, normalize_before)
        encoder_norm = nn.LayerNorm(d_model) if normalize_before else None
       	# 创建
        self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers, encoder_norm)
	    # encoder层
        decoder_layer = TransformerDecoderLayer(d_model, nhead, dim_feedforward,dropout, activation, normalize_before)
        decoder_norm = nn.LayerNorm(d_model)
        # 创建decoder层
        self.decoder = TransformerDecoder(decoder_layer, num_decoder_layers, decoder_norm,return_intermediate=return_intermediate_dec)

        self._reset_parameters()

        self.d_model = d_model
        self.nhead = nhead

dataset

使用的数据集是coco数据集,现成的使用

backbone

首先是backbone是用一个resnet进行特征图提取,再加入positional encoding,加入到encoder

def build_backbone(args):
    # 构建位置信息
    position_embedding = build_position_encoding(args)
    train_backbone = args.lr_backbone > 0
    return_interm_layers = args.masks
    # 构建backbone
    backbone = Backbone(args.backbone, train_backbone, return_interm_layers, args.dilation)
    # 结果位置信息和resnet提取出来的特征图信息
    model = Joiner(backbone, position_embedding)
    model.num_channels = backbone.num_channels
    # 返回结果
    return model

class Backbone(BackboneBase):
    """ResNet backbone with frozen BatchNorm."""
    def __init__(self, name: str,
                 train_backbone: bool,
                 return_interm_layers: bool,
                 dilation: bool):
        backbone = getattr(torchvision.models, name)(
            replace_stride_with_dilation=[False, False, dilation],
            pretrained=is_main_process(), norm_layer=FrozenBatchNorm2d)
        num_channels = 512 if name in ('resnet18', 'resnet34') else 2048 # 使用resnet模型
        super().__init__(backbone, train_backbone, num_channels, return_interm_layers)
encoder

在encoder中进行embedding,将特征信息转换成多维度向量,通过transformer的self-attention机制,生成特征k,v

encoder_layer = TransformerEncoderLayer(d_model, nhead, dim_feedforward,dropout, activation, normalize_before)
class TransformerEncoderLayer(nn.Module):
    #省略初始化
    def forward_post(self,
                     src,
                     src_mask: Optional[Tensor] = None,
                     src_key_padding_mask: Optional[Tensor] = None,
                     pos: Optional[Tensor] = None):
        q = k = self.with_pos_embed(src, pos) #只有K和Q 加入了位置编码;并没有对V做? V只是提供数值,不需要位置信息
        # 就是transformer里面的multi-self-attention
        src2 = self.self_attn(q, k, value=src, attn_mask=src_mask,
                              key_padding_mask=src_key_padding_mask)[0] #两个返回值:自注意力层的输出,自注意力权重;只需要第一个
        # dropout,归一化,还原维度
        src = src + self.dropout1(src2)
        src = self.norm1(src)
        src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))

        src = src + self.dropout2(src2)
        src = self.norm2(src)

        return src
decoder

紧接着是decoder,初始化object queries,都初始化为0,同时加上位置信息,首先q自己先进行self-attention,更新q,再由decoder提供q,encoder提供k和v,来进行multi-attention,整合多维度的信息,同时也是做多次,获得多个输出特征结果,这样的过程回经过6次

3.关于Detr_第2张图片

decoder_layer = TransformerDecoderLayer(d_model, nhead, dim_feedforward,dropout, activation, normalize_before)
class TransformerDecoderLayer(nn.Module):
    # 省略初始化
    def forward_post(self, tgt, memory,  # memory 是由encoder计算得到k和v
                     tgt_mask: Optional[Tensor] = None,
                     memory_mask: Optional[Tensor] = None,
                     tgt_key_padding_mask: Optional[Tensor] = None,
                     memory_key_padding_mask: Optional[Tensor] = None,
                     pos: Optional[Tensor] = None,
                     query_pos: Optional[Tensor] = None):
       	# 初始化decoder的q和k,初始化都为0
        q = k = self.with_pos_embed(tgt, query_pos)
	    # decoder自身的 q和k进行self-attention
        tgt2 = self.self_attn(q, k, value=tgt, attn_mask=tgt_mask,
                              key_padding_mask=tgt_key_padding_mask)[0]

        tgt = tgt + self.dropout1(tgt2)
        tgt = self.norm1(tgt)
		# 由encoder生成的memory提供k,v,以及一次计算后decoder提供q,来进行multihead_attn,得到结果
        tgt2 = self.multihead_attn(query=self.with_pos_embed(tgt, query_pos),
                                   key=self.with_pos_embed(memory, pos),
                                   value=memory, attn_mask=memory_mask,
                                   key_padding_mask=memory_key_padding_mask)[0]
		# 经过维度调整,还原回原来的返回
        tgt = tgt + self.dropout2(tgt2)
        tgt = self.norm2(tgt)
        tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt))))

        tgt = tgt + self.dropout3(tgt2)
        tgt = self.norm3(tgt)

        return tgt
收尾

将特征信息输出,进行分类和回归任务,最终画出特征图框

def forward(self, samples: NestedTensor):
    """ 
    前向传播函数,接收 NestedTensor 输入,该输入包括:
        - samples.tensor: 批量图像,形状为 [batch_size x 3 x H x W]
        - samples.mask: 二值掩码,形状为 [batch_size x H x W],在填充像素上值为 1

    返回一个包含以下元素的字典:
        - "pred_logits": 所有查询的分类 logits(包括无对象的类别)。形状为 [batch_size x num_queries x (num_classes + 1)]
        - "pred_boxes": 所有查询的归一化边界框坐标,表示为 (center_x, center_y, height, width)。这些值在 [0, 1] 范围内,
                        相对于每张图像的大小(不考虑可能的填充)。有关如何检索未归一化的边界框,请参见 PostProcess。
        - "aux_outputs": 可选,仅在启用了辅助损失时返回。它是一个包含两个上述键的字典列表,每个字典对应于一个解码器层。
    """
    # 如果输入是列表或张量,则将其转换为 NestedTensor
    if isinstance(samples, (list, torch.Tensor)):
        samples = nested_tensor_from_tensor_list(samples)
    
    # 从骨干网络中提取特征和位置编码
    features, pos = self.backbone(samples)
    src, mask = features[-1].decompose()
    
    print(src.shape)  # 调试:打印特征图的形状
    print(mask.shape)  # 调试:打印掩码的形状
    
    assert mask is not None  # 确保掩码不为 None
    
    # 通过变换器进行处理
    hs = self.transformer(
        self.input_proj(src),  # 将特征图投影到变换器的输入空间
        mask,                  # 指示有效区域的二值掩码
        self.query_embed.weight,  # 查询嵌入
        pos[-1]                # 位置编码
    )[0]
    
    print(hs.shape)  # 调试:打印变换器输出的形状
    
    # 生成类别 logits 和边界框坐标
    outputs_class = self.class_embed(hs)
    outputs_coord = self.bbox_embed(hs).sigmoid()
    
    # 准备输出字典
    out = {'pred_logits': outputs_class[-1], 'pred_boxes': outputs_coord[-1]}
    
    # 如果启用了辅助损失,则包括辅助输出
    if self.aux_loss:
        out['aux_outputs'] = self._set_aux_loss(outputs_class, outputs_coord)
    
    return out

计算损失,使用match(匈牙利算法)计算三个损失边界框成本、分类成本和 GIoU 成本,广义交并比 (GIoU) 是一种用于评估边界框预测质量的指标,旨在改进传统的交并比 (IoU)。

def forward(self, outputs, targets):
    """ 执行匹配操作

    参数:
        outputs: 这是一个包含至少以下条目的字典:
            - "pred_logits": 形状为 [batch_size, num_queries, num_classes] 的张量,表示分类 logits
            - "pred_boxes": 形状为 [batch_size, num_queries, 4] 的张量,表示预测的边界框坐标

        targets: 这是一个目标列表(长度为 batch_size),每个目标是一个字典,包含:
            - "labels": 形状为 [num_target_boxes] 的张量(其中 num_target_boxes 是目标中真实对象的数量),包含类别标签
            - "boxes": 形状为 [num_target_boxes, 4] 的张量,包含目标边界框坐标

    返回:
        返回一个大小为 batch_size 的列表,包含 (index_i, index_j) 的元组,其中:
            - index_i 是选择的预测索引(按顺序)
            - index_j 是对应选择的目标索引(按顺序)
        对于每个批次元素,满足 len(index_i) = len(index_j) = min(num_queries, num_target_boxes)
    """
    bs, num_queries = outputs["pred_logits"].shape[:2]  # 获取批次大小和查询数量

    # 将张量展平以计算批次中的成本矩阵
    out_prob = outputs["pred_logits"].flatten(0, 1).softmax(-1)  # [batch_size * num_queries, num_classes],应用 softmax 得到类别概率
    out_bbox = outputs["pred_boxes"].flatten(0, 1)  # [batch_size * num_queries, 4],展平预测边界框

    # 也将目标标签和框展平并拼接
    tgt_ids = torch.cat([v["labels"] for v in targets])  # 将所有目标的标签拼接在一起
    tgt_bbox = torch.cat([v["boxes"] for v in targets])  # 将所有目标的边界框拼接在一起

    # 计算分类成本。与损失不同,我们不使用 NLL,而是通过 1 - proba[target class] 来近似。
    # 1 是一个常量,不影响匹配结果,可以省略。
    cost_class = -out_prob[:, tgt_ids]  # 分类成本,负号是因为我们要最小化成本

    # 计算边界框之间的 L1 成本
    cost_bbox = torch.cdist(out_bbox, tgt_bbox, p=1)  # 计算边界框之间的 L1 距离

    # 计算边界框之间的 GIoU 成本
    cost_giou = -generalized_box_iou(box_cxcywh_to_xyxy(out_bbox), box_cxcywh_to_xyxy(tgt_bbox))  # 计算广义交并比的负值

    # 最终成本矩阵
    C = self.cost_bbox * cost_bbox + self.cost_class * cost_class + self.cost_giou * cost_giou  # 结合边界框成本、分类成本和 GIoU 成本
    C = C.view(bs, num_queries, -1).cpu()  # 调整形状以适配每个批次

    # 计算每个目标的边界框数量
    sizes = [len(v["boxes"]) for v in targets]
    
    # 使用匈牙利算法进行最优匹配
    indices = [linear_sum_assignment(c[i]) for i, c in enumerate(C.split(sizes, -1))]
    
    # 返回每个批次的匹配结果,包含预测索引和目标索引
    return [(torch.as_tensor(i, dtype=torch.int64), torch.as_tensor(j, dtype=torch.int64)) for i, j in indices]



你可能感兴趣的:(Transformer,计算机视觉,目标检测,transformer)