



























debug版本,无需cuda编译算子,纯pytorch可以跑, torch 1.11.0+cu113,py3.8
https://github.com/wuzuowuyou/Deformable-DETR-pytorch-pure
https://www.zhihu.com/question/454351120
detr预习:

https://www.cnblogs.com/yanghailin/p/19638764
deformable detr解决detr两个问题:
1)训练慢
2)小物体检测差
训练慢的问题原detr的一个点需要与整个图像素做关系运算,没必要,只与附近点做运算
小物体检测差的问题多尺度检测
def get_reference_points(spatial_shapes, valid_ratios, device):# spatial_shapes[4, 2] valid_ratios[b, 4, 2]
reference_points_list = []
for lvl, (H_, W_) in enumerate(spatial_shapes):
ref_y, ref_x = torch.meshgrid(torch.linspace(0.5, H_ - 0.5, H_, dtype=torch.float32, device=device),
torch.linspace(0.5, W_ - 0.5, W_, dtype=torch.float32, device=device))
#ref_y [76, 106]
#ref_y.reshape(-1) [8056]
#ref_y.reshape(-1)[None] [1, 8056]
#valid_ratios[:, None, lvl, 1] [2,1]
#ref_y [b, 8056] = [1, 8056] / [b,1]
#这里valid_ratios是h/H * H_ 就得到了实际的h, 再用参考点除以实际的h,就得到相对于实际大小图的相对位置
ref_y = ref_y.reshape(-1)[None] / (valid_ratios[:, None, lvl, 1] * H_) #[b, 5395] [1,8056] [2,1]
ref_x = ref_x.reshape(-1)[None] / (valid_ratios[:, None, lvl, 0] * W_) #[b, 5395]
ref = torch.stack((ref_x, ref_y), -1) ##[b, 5395, 2]
reference_points_list.append(ref) #reference_points_list4个, [b, 8056,2] [b, 2014,2] [b, 513,2] [b, 104,2]
reference_points = torch.cat(reference_points_list, 1) #[b, 10723, 2]
reference_points = reference_points[:, :, None] * valid_ratios[:, None] #torch.Size([b, 7237, 4, 2])
return reference_points
reference_points = reference_points[:, :, None] * valid_ratios[:, None] #torch.Size([b, 7237, 4, 2])
reference_points[b, 10723,2]
reference_points[:, :, None]【b, 10723,1,2】
valid_ratios[b, 4, 2]
valid_ratios[:, None] [2,1,4,2]
[b, 10723, 4, 2] = [b, 10723,1,2] * [b,1,4,2]
reference_points = reference_points[:, :, None] * valid_ratios[:, None] #torch.Size([b, 7237, 4, 2])
光看增广[b, 10723, 4, 2] = [b, 10723,1,2] * [b,1,4,2],即10723个点,每个点都在4个层级特征上的位置坐标。当前层级用这个公式没问题,但是第0层上点在第一层上面坐标如何表示呢?
reference_points是相对于自己有效长度上相对值,valid_ratios是有效值/归一化的值。比如第0层的一个点,reference_points0=h0/he0, h0是绝对值,he0是有效值,valid_ratios=he0/H, H是当前batch pad的统一值。单看第0层上面的点归一化到全局的H,reference_points00=(h0/he0) * (he0/H) = h0/H. 这里看he0可以消掉,没问题。 但是这个第0层上面的点继续到第一层上面reference_points01=(h0/he0) * (he1/H) ,这里he0和he1有效值不一样消不掉,这如何解释?
其实需要找的是第0层上点到第1、2、3层上坐标表示,这里由于每层的pad比例不一样,用相对坐标需要相对于每层的有效部分。


# 尺寸为:(B, sum(H_*W_), 2)。表示对于一个batch中的每一条数据,
# 它在所有特征图上的参考点(数量为sum(H_*W_))的像素坐标x,y。
# 这里x、y都做过处理,表示该参考点相对于padding部分的h、w的比值
# 例如,如果x和y>1,则说明该参考点在padding部分
# ============================================================================
reference_points = torch.cat(reference_points_list, 1)
# ============================================================================
# 尺寸为:(B, sum(H_*W_), level_num, 2)。表示对于batch中的每一条数据,
# 它的每一个特征层上的归一化坐标,在其他特征层(也包括自己)上的所有归一化坐标
# 假设对于某条数据:
# 特征图1的高度为H1,有效高度为HE1,其上某个ref点x坐标为h1。那么ref点归一化坐标可以表示成h1/H1
# 特征图2的高度为H2,有效高度为HE2,那么特征图1中的ref点在特征图2中,对应的归一化坐标应该是多少?
# 【常规思路】:正常情况下,你可能觉得,我只要对每一张特征图上的像素坐标例如h1,然后对任意两张特征图,
# 我取出像素点坐标一致的ref点,它不能表示两张特征图相同的位置吗?
# 【问题】:每张特征图padding的比例不一样,因此不能这么做。举例,
# 特征图1上绝对像素位置3.5的点,在特征图2上的位置是2.1,在有效图部分之外
# 【正确做法】:把特征图上的坐标表示成相对于有效部分的比例
# 【解】:我们希望 h1/HE1 = h2/HE2,在此基础上我们再要求 h2/H2
# 则我们有: h2 = (h1/HE1) * HE2,进一步有
# (h2/H2) = (h1/HE1) * (HE2/H2),而(h1/HE1)就是reference_points[:, :, None],
# (HE2/H2)就是valid_ratios[:, None]
# 所以,这里是先将不同特征图之间的映射转为“绝对坐标/有效长度”的表达,
# 然后再转换成该绝对坐标在整体长度上的比例
# ============================================================================
reference_points = reference_points[:, :, None] * valid_ratios[:, None]
return reference_points
在 DETR 里,encoder 用的是 标准 self-attention,所以:
复杂度是:O(N^2)
例如:
在 Deformable DETR 中,不再和全图计算,而是:
每个 query 只采样少量 key 点。
论文里的公式:
每个 query 的计算量:L*K
论文默认参数:
所以:4*4=16
也就是说:
一个像素(query)只和 16 个位置计算 attention
而不是 DETR 的 几千个位置。
| 模型 | 每个 query 计算的 key 数 |
|---|---|
| DETR | N(全图) |
| Deformable DETR | L × K = 16 |
举例:
| feature map | DETR | Deformable DETR |
|---|---|---|
| 50×50 | 2500 | 16 |
| 100×100 | 10000 | 16 |
所以 复杂度从 O(N^2)降为O(N)
这也是 Deformable DETR 能:
的原因。
def _reset_parameters(self):
constant_(self.sampling_offsets.weight.data, 0.)
thetas = torch.arange(self.n_heads, dtype=torch.float32) * (2.0 * math.pi / self.n_heads)
grid_init = torch.stack([thetas.cos(), thetas.sin()], -1)
grid_init = (grid_init / grid_init.abs().max(-1, keepdim=True)[0]).view(self.n_heads, 1, 1, 2).repeat(1, self.n_levels, self.n_points, 1)
for i in range(self.n_points):
grid_init[:, :, i, :] *= i + 1
with torch.no_grad():
self.sampling_offsets.bias = nn.Parameter(grid_init.view(-1))
constant_(self.attention_weights.weight.data, 0.)
constant_(self.attention_weights.bias.data, 0.)
xavier_uniform_(self.value_proj.weight.data)
constant_(self.value_proj.bias.data, 0.)
xavier_uniform_(self.output_proj.weight.data)
constant_(self.output_proj.bias.data, 0.)
在初始化sampling_offsets权重的时候,sampling_offset的weight初始化为0,bias初始化周期性角度
constant_(self.sampling_offsets.weight.data, 0.)
将采样偏移量的权重矩阵初始化为0。这意味着在训练初期,网络预测的偏移量主要由bias决定。
thetas = torch.arange(self.n_heads, dtype=torch.float32) * (2.0 * math.pi / self.n_heads)
假设 n_heads=8,生成8个均匀分布的角度:
thetas = [0, π/4, π/2, 3π/4, π, 5π/4, 3π/2, 7π/4]
= [0°, 45°, 90°, 135°, 180°, 225°, 270°, 315°]
目的: 让每个注意力头关注不同的方向。
grid_init = torch.stack([thetas.cos(), thetas.sin()], -1)
将角度转换为单位圆上的坐标点:
grid_init = [
[cos(0°), sin(0°)], # [1.0, 0.0] → 右
[cos(45°), sin(45°)], # [0.7, 0.7] → 右上
[cos(90°), sin(90°)], # [0.0, 1.0] → 上
[cos(135°), sin(135°)], # [-0.7, 0.7] → 左上
[cos(180°), sin(180°)], # [-1.0, 0.0] → 左
[cos(225°), sin(225°)], # [-0.7, -0.7] → 左下
[cos(270°), sin(270°)], # [0.0, -1.0] → 下
[cos(315°), sin(315°)] # [0.7, -0.7] → 右下
]
形状: [n_heads, 2] = [8, 2]
grid_init = (grid_init / grid_init.abs().max(-1, keepdim=True)[0]).view(self.n_heads, 1, 1, 2).repeat(1, self.n_levels, self.n_points, 1)
这行代码做了4件事:
a) 归一化
grid_init / grid_init.abs().max(-1, keepdim=True)[0]
将向量归一化到单位长度(虽然已经是单位向量了,这步主要是保险)。
b) reshape
.view(self.n_heads, 1, 1, 2)
形状变为: [8, 1, 1, 2]
c) 复制到所有层级和采样点
.repeat(1, self.n_levels, self.n_points, 1)
假设 n_levels=4, n_points=4,形状变为: [8, 4, 4, 2]
现在每个注意力头、每个特征层级、每个采样点都有相同的方向向量。
for i in range(self.n_points):
grid_init[:, :, i, :] *= i + 1
关键步骤! 为4个采样点设置不同的采样半径:
采样点0: 偏移量 × 1 (最近)
采样点1: 偏移量 × 2
采样点2: 偏移量 × 3
采样点3: 偏移量 × 4 (最远)
示例(以第一个注意力头为例):
原始向量: [1.0, 0.0] (向右)
采样点0: [1.0, 0.0] × 1 = [1.0, 0.0]
采样点1: [1.0, 0.0] × 2 = [2.0, 0.0]
采样点2: [1.0, 0.0] × 3 = [3.0, 0.0]
采样点3: [1.0, 0.0] × 4 = [4.0, 0.0]
6. 设置为bias参数
with torch.no_grad():
self.sampling_offsets.bias = nn.Parameter(grid_init.view(-1))
将初始化的偏移量展平并设置为bias。
最终形状: [n_heads × n_levels × n_points × 2] = [8 × 4 × 4 × 2] = [256]
可视化示例
假设有8个注意力头,4个采样点,在某个参考点周围的采样模式:
采样点2 (距离3)
↑
采样点1 (距离2)
↑
参考点 ● → → → (4个采样点沿着8个方向)
↓
采样点1 (距离2)
↓
采样点2 (距离3)
完整的8个方向 × 4个距离 = 32个采样点:
3
2 ↑ 2
3 ↑ ↑ ↑ 3
← ← ← ● → → →
3 ↓ ↓ ↓ 3
2 ↓ 2
3
设计优势
方向多样性: 8个方向覆盖360°
距离多样性: 4个不同距离形成多尺度感受野
对称性: 采样模式关于中心对称
可学习性: 这只是初始化,训练过程中会学习更优的偏移量
稳定训练: 良好的初始化避免训练初期的不稳定
这种初始化策略使得模型在训练开始时就能关注到周围不同方向和距离的特征,加速收敛并提高性能。

这里代码里面8,4,4解释下,8是8个方向,4是4个分辨率层级特征,4是4个偏移点



nn.Embedding(300,256)
↓
query_embed [b,300,256]
↓ nn.Linear + sigmoid
reference_points [b,300,2]
300 个 query,每个是一个 object query
经过一个 Linear:ref = sigmoid(Linear(query_embed))
得到:
reference_points: [b, 300, 2]
(x, y) ∈ [0,1]
关键理解:
每个 query 初始就带一个“我大概在图哪里找目标”的位置。
reference_points_input = reference_points[:, :, None, :] * src_valid_ratios[:, None, :, :]
把统一坐标 → 映射到每个 feature level 的有效区域,
每个 query,在每个 feature level 上的 reference point
out [b,300,256]
↓
MLP → tmp [b,300,4]
reference_points [b,300,2]
↓ inverse_sigmoid
reference
tmp[..., :2] += reference
↓
sigmoid
↓
out_coord [b,300,4]
二、关键问题:为什么要 tmp + reference?
❌ 如果不加 reference(普通 DETR)
模型是这样:
bbox = sigmoid(MLP(query))
👉 问题:
每一层都要 从头预测一个 bbox
学习难度很大
收敛慢(这也是 DETR 的痛点)
✅ Deformable-DETR 的做法
bbox = reference + Δbbox
👉 也就是:
🔥 只预测“偏移量”,而不是绝对位置
三、inverse_sigmoid + 相加的数学本质
这一段是很多人卡住的点 👇
1️⃣ reference 是 sigmoid 空间
reference_points ∈ (0,1)
2️⃣ 先做 inverse_sigmoid
reference = inverse_sigmoid(reference_points)
👉 变成:
(-∞, +∞)
3️⃣ 再加 tmp(MLP 输出)
tmp[..., :2] += reference
👉 等价于:
logit(new_x) = logit(old_x) + Δ
4️⃣ 再 sigmoid 回去
new_x = sigmoid(logit(old_x) + Δ)
👉 这个操作的真正含义是:
🔥 在 logit 空间做残差更新
而不是在 (0,1) 空间硬加。
四、为什么不用直接在 (0,1) 加?
如果直接:
x_new = x_old + Δ
问题:
容易越界(<0 或 >1)
梯度不稳定
更新不对称
👉 用 logit 空间的好处:
x_new = sigmoid(logit(x_old) + Δ)
优点:
✅ 永远在 (0,1)
✅ 更新是“相对的”(比例意义)
✅ 梯度更稳定
✅ 更容易收敛
五、从“检测角度”理解(更直观)
你可以这样理解:
第 0 层:
reference = (0.3, 0.6)
👉 粗略猜一个位置
第 1 层:
Δ = (+0.1, -0.05)
→ 新位置 ≈ (0.35, 0.58)
第 2 层:
Δ = (+0.02, +0.01)
→ 更精确
👉 本质:
🔥 每一层都在“微调 bbox”,而不是重新预测
六、为什么只对前两维 tmp[..., :2] += reference?
因为:
tmp: [x, y, w, h]
reference: [x, y]
👉 所以:
只 refine 中心点
w,h 是直接预测(或者在一些改进模型里也做 residual)
七、这一步和 reference point 的闭环关系
现在你可以把整个逻辑闭环起来:
🔁 一层 decoder 的完整闭环
1️⃣ 有 reference point(当前位置)
2️⃣ 用它做 deformable attention(去采样特征)
3️⃣ 得到更好的 feature(out)
4️⃣ 预测 bbox 偏移:
tmp + reference → 新 bbox
5️⃣ 更新 reference point(下一层用)
👉 形成:
🔥 位置 → 采样 → 更新位置 → 再采样
以下截图来自知乎https://www.zhihu.com/question/454351120












此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。