关于Detr
模型架构
总体架构
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次
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]