罪魁祸首是CenterNet代码中,在计算loss前进行的gather feature操作,说实话这个操作初见简直蒙圈,我缓了将近三个月才缓明白这个到底在干嘛。
上代码:
1 | def _gather_feat(feat, ind, mask=None): |
比如计算dep损失前的:
1 | dep_loss += self.crit_reg(output['dep'], batch['reg_mask'], |
将网络输出的深度,加上一个dataloader输出一个batch的gt中的reg_mask和ind(最后才理解这个ind指index,说实话之前想过是index但是你放神经网络代码里能明白这到底在干嘛吗,后面细说这个)和gt的深度,简直看蒙了。
要理解这一行就需要把最初gather feature看明白,下面就开始吧
1 | def _transpose_and_gather_feat(feat, ind): |
ind是一个(B,max_obj)维度张量,在一个B中表示图中某一个点的坐标上是hm的峰值,数值是ct[0]+ct[1]* img.w,图像宽×y + x。后面细说。
1.首先第一句
1 | feat = feat.permute(0, 2, 3, 1).contiguous() |
将tensor的维度进行变换。将B C H W 转为 B H W C。
举个栗子,torch.size(1,2,3,4)经过转换过后变成 torch.size(1,3,4,2)。
permute参数中每一个代表变换后这个位置的维度数字是变换前的第几个,栗子中将原本第1个位置(从0开始)的[2]放到了第3个位置(也就是最后)
contiguous的作用是让tensor在计算机中存储变成连续的,因为permute变换没有改变物理存储结构。
2.第二句
1 | feat = feat.view(feat.size(0), -1, feat.size(3)) |
view作用也是变换维度,和permute不同,这个改变物理结构,同时可以实现将tensor展开(而permute必须size相同)
参数-1代表自动进行维数匹配。
举个栗子,torch.size(2,3,3,2)进行上面的view后变成torch.size(2,9,2)
3.第三句
1 | def _gather_feat(feat, ind, mask=None): |
1 | ind = ind.unsqueeze(2).expand(ind.size(0), ind.size(1), dim) |
需要记住的是,ind的维度是[batch, max_obj]是2维的,max_obj是设定的一幅图像当中最多目标的个数,可以自己任意设置。经过前两句的变换,输入feat的维度是B -1 C,(-1待表H×W),是3维的。
肯定要把维数变得一样,那么就unsqueeze(2),在第3个轴进行增维。
举个栗子:
[[2,0,0,0], [1,3,4,5]] ==> [ [ [2],[0],[0],[0] ], [ [1],[3],[4],[5] ] ] size(2,4,1)
如果unsqueeze(0) ==> [ [ [2,0,0,0], [1,3,4,5 ] ] ] size(1,2,3)
如果unsqueeze(1) ==> [ [ [2,0,0,0] ], [ [1,3,4,5] ] ] size(2,1,4)
这样就变成了3维的。
4.接下来史诗级难点!!!
1 | feat = feat.gather(1, ind) |
官网给出的解释:
Gathers values along an axis specified by dim.
For a 3-D tensor the output is specified by:
1 | out[i][j][k] = input[index[i][j][k]][j][k] # if dim == 0 |
If input
is an n-dimensional tensor with size (x_0, x_1…, x_{i-1}, x_i, x_{i+1}, …, x_{n-1})(x0,x1…,x**i−1,x**i,x**i+1,…,x**n−1) and dim = i
, then index
must be an nn -dimensional tensor with size (x_0, x_1, …, x_{i-1}, y, x_{i+1}, …, x_{n-1})(x0,x1,…,x**i−1,y,x**i+1,…,x**n−1) where y \geq 1y≥1 and out
will have the same size as index
.
已经晕了是不是,但是要用语言解释的话就是,在input的选的维度上的数值使用同位置的index位置上的数值来代替。
要理解为什么gather就可以将BHWC中关键点用ind提取出来,明确ind中记录的是原先HW上一点x+W×y。再将这个放到gather的功能里面去看,只可意会啊啊啊,理解起来不容易,感觉这样说都是废话,但是目前我也是意会阶段,逐元素推导有点麻烦。
若一个size(1,H,W,C)的input如下size(1,3,3,2),将C放到一个格子里。
0,1 | 2,3 | 4,5 |
---|---|---|
6,7 | 8,9 | 10,11 |
12,13 | 14,15 | 16,17 |
那么设ind为[2,5,6]
进行下面的实验
1 | >>>input = np.empty([2,3,3,2],dtype=np.int64) |
观察feat的第一个Batch,前三个正是表格中第2,5,6个格子!(从0开始)。这样就得到了我们需要的feature的像素点。至于其他的格子,我们使用之前传入的reg_mask,在不是hm峰值的部分为0,峰值处为1,则将feat的其余部分置为了0,在训练时不会计算loss。
到了这里相信你已经意会了,还有啥不明白的做个实验尝试一下应该没问题啦^V^
2023.5.25更新
跑的更新的代码中,突然这一句报错。
1 | img2lidars = data['lidar2img'].inverse() |
1 | RuntimeError: CUDA error: operation not supported when calling `cusparseCreate(handle)` |
查了一下原来是4090显卡的问题,网上其他人在3090上就没问题,于是稍微改一下代码。
1 | device = data['lidar2img'][0].device |
这样就跑起来了,因为numpy只能操作cpu的数据,所以这里要转一下,这也是没有办法的事情,谁让目前的cuda11.1+4090使用inverse会报错嘞。