为什么使用 unet 进行重塑和置换以进行分割?

Why Reshape and Permute for segmentation with unet?

我正在用 unet 做图像语义分割工作。我对像素分类的最后一层感到困惑。 Unet代码是这样的:

...
reshape = Reshape((n_classes,self.img_rows * self.img_cols))(conv9)
permute = Permute((2,1))(reshape)
activation = Activation('softmax')(permute)
model = Model(input = inputs, output = activation) 
return model
...

我可以像这样不使用 Permute 只重塑形状吗?

reshape = Reshape((self.img_rows * self.img_cols, n_classes))(conv9)

更新:

发现直接reshape方式训练结果不对:

reshape = Reshape((self.img_rows * self.img_cols, n_classes))(conv9) // the loss is not convergent

我的groundtruth是这样生成的:

X = []
Y = []
im = cv2.imread(impath)
X.append(im)
seg_labels = np.zeros((height, width, n_classes))
for spath in segpaths:
    mask = cv2.imread(spath, 0)
    seg_labels[:, :, c] += mask
Y.append(seg_labels.reshape(width*height, n_classes))

为什么直接reshape不行?

您的代码仍然可以运行,因为形状相同,但结果(反向传播)将不同,因为张量的值将不同。例如:

arr = np.array([[[1,1,1],[1,1,1]],[[2,2,2],[2,2,2]],[[3,3,3],[3,3,3]],[[4,4,4],[4,4,4]]])
arr.shape
>>>(4, 2, 3)

#do reshape, then premute
reshape_1 = arr.reshape((4, 2*3))
np.swapaxes(reshape_1, 1, 0)
>>>array([[1, 2, 3, 4],
          [1, 2, 3, 4],
          [1, 2, 3, 4],
          [1, 2, 3, 4],
          [1, 2, 3, 4],
          [1, 2, 3, 4]])

#do reshape directly
reshape_2 = arr.reshape(2*3, 4)
reshape_2
>>>array([[1, 1, 1, 1],
          [1, 1, 2, 2],
          [2, 2, 2, 2],
          [3, 3, 3, 3],
          [3, 3, 4, 4],
          [4, 4, 4, 4]])

完成重塑和置换以在每个像素位置获取 softmax。添加到@meowongac 的答案中, Reshape 保留了元素的顺序。在这种情况下,由于必须交换通道尺寸,因此 Reshape 后跟 Permute 是合适的。

考虑到 (2,2) 图像在每个位置有 3 个值的情况,

arr = np.array([[[1,1],[1,1]],[[2,2],[2,2]],[[3,3],[3,3]]]) 
>>> arr.shape
(3, 2, 2)
>>> arr
array([[[1, 1],
        [1, 1]],

       [[2, 2],
        [2, 2]],

       [[3, 3],
        [3, 3]]])

>>> arr[:,0,0]
array([1, 2, 3])

每个位置的通道值为[1,2,3]。目标是将通道轴(长度3)交换到最后。

>>> arr.reshape((2,2,3))[0,0] 
array([1, 1, 1])   # incorrect

>>> arr.transpose((1,2,0))[0,0] # similar to what permute does.
array([1, 2, 3])  # correct 

此处有更多示例 link:https://discuss.pytorch.org/t/how-to-change-shape-of-a-matrix-without-dispositioning-the-elements/30708

你显然误解了每个操作的含义和最终目标:

  • 最终目标:class每个像素的化,即沿语义 class 轴的 softmax
  • 原代码中这个目标是如何实现的?让我们逐行查看代码:
reshape = Reshape((n_classes,self.img_rows * self.img_cols))(conv9) # L1
permute = Permute((2,1))(reshape) # L2
activation = Activation('softmax')(permute) # L3
  • L1 的输出暗淡 = n_class-by-n_pixs, (n_pixs=img_rows x img_cols)
  • L2 的输出暗淡 = n_pixs-by-n_class
  • L3 的输出暗淡 = n_pixs-by-n_class
  • 请注意,默认的 softmax 激活应用于最后一个轴,即 n_class 代表的轴,即语义 class 轴。

因此,这个原始代码完成了语义分割的最终目标。


让我们重新访问您要更改的代码,即

reshape = Reshape((self.img_rows * self.img_cols, n_classes))(conv9) # L4
  • L4 的输出暗淡 = n_pixs-by-n_class

我的猜测是你认为L4的输出暗淡匹配L2的,因此L4是一个捷径,相当于执行L1和L2。

但是,匹配形状并不一定意味着匹配轴的物理意义。为什么?一个简单的例子会解释。

假设您有 2 个语义 classes 和 3 个像素。要查看差异,假设所有三个像素都属于同一个 class。

换句话说,真值张量看起来像这样

# cls#1 cls#2
[   [0, 1], # pixel #1
    [0, 1], # pixel #2
    [0, 1], # pixel #3
]

假设您有一个完美的网络并为每个像素生成准确的响应,但您的解决方案将创建如下所示的张量

# cls#1 cls#2
[   [0, 0], # pixel #1
    [0, 1], # pixel #2
    [1, 1], # pixel #3
]

其形状与ground truth相同,但与坐标轴的物理意义不符。

这进一步让softmax操作变得毫无意义,因为它应该应用于class维度,但这个维度在物理上并不存在。因此,在应用 softmax 后会导致以下错误输出,

# cls#1 cls#2
[   [0.5, 0.5], # pixel #1
    [0, 1], # pixel #2
    [0.5, 0.5], # pixel #3
]

即使在 理想假设 下,这也完全搞乱了训练。


因此,记下张量各轴的物理意义是个好习惯。当你做任何张量重塑操作时,问问自己轴的物理意义是否以你预期的方式改变了。

例如,如果你有一个形状为 batch_dim x img_rows x img_cols x feat_dim 的张量 T,你可以做很多事情,但并非所有事情都有意义(由于轴的物理意义有问题)

  1. (错误)将其重塑为 whatever x feat_dim,因为 whatever 维度在 batch_size 可能不同的测试中毫无意义。
  2. (错误)将其重塑为 batch_dim x feat_dim x img_rows x img_cols,因为第 2 维不是特征维,第 3 维和第 4 维也不是。
  3. (正确)置换轴 (3,1,2),这将引导您得到形状为 batch_dim x feat_dim x img_rows x img_cols 的张量,同时保持每个轴的物理意义。
  4. (正确)将其重塑为 batch_dim x whatever x feat_dim。这也是有效的,因为whatever=img_rows x img_cols相当于像素位置维度,batch_dimfeat_dim的含义都没有改变。