上采样

上采样(Upsampling)是卷积神经网络(CNN)中的一种操作,旨在增加特征图的空间尺寸,从而恢复图像的分辨率或生成更高分辨率的输出。

上采样的常见方法

  1. 最近邻插值(Nearest Neighbor Interpolation):这种方法通过复制最近的像素值来增加图像的尺寸。它简单且计算效率高,但可能导致图像出现锯齿状边缘。
  2. 双线性插值(Bilinear Interpolation):这种方法通过对邻近的四个像素进行加权平均来计算新像素值。它比最近邻插值更平滑,但计算复杂度稍高。
  3. 转置卷积(Transposed Convolution):这种方法通过学习卷积核来实现上采样,能够更好地恢复图像细节。反卷积在生成对抗网络(GANs)和自动编码器中广泛应用。

最近邻插值

双线性插值

双线性插值在两个方向上进行线性插值。给定图像 $I \in \mathbb{R} ^{h \times w}$ ,我们希望将其上采样到尺寸 $H \times W$ 。双线性插值通过对输入图像中邻近的四个像素进行加权平均来计算输出图像中的每个像素值。首先,计算出水平和垂直方向的缩放比例:

$$(s_x, s_y) = \left(\frac{w}{W}, \frac{h}{H}\right)$$

对于输出图像中的每个像素 $(i, j)$ ,计算其在输入图像中的对应位置 $(x, y)$

$$(x, y) = (i \cdot s_x, j \cdot s_y)$$

然后,确定 $(x, y)$ 周围的四个近邻 $(x_1, y_1)$ $(x_1, y_2)$ $(x_2, y_1)$ $(x_2, y_2)$ ,其对应的像素值分别记作 $p_{11}$ $p_{12}$ $p_{21}$ $p_{22}$ 。然后,先在水平方向上进行线性插值:

$$\begin{aligned} p_{1y} &= \frac{x_2 - x}{x_2 - x_1} p_{11} + \frac{x - x_1}{x_2 - x_1} p_{21} \\ p_{2y} &= \frac{x_2 - x}{x_2 - x_1} p_{12} + \frac{x - x_1}{x_2 - x_1} p_{22} \end{aligned}$$

双线性插值假定像素值在两个方向上是线性变化的。 考虑一维情形,给定两点 $x_1$ $x_2$ 及其对应的函数值 $p_1$ $p_2$ ,若像素值的变化是线性的,则对于两点之间的任意一点 $x$ 有如下关系成立:

$$\frac{p-p_1}{x-x_1} = \frac{p_2 - p_1}{x_2 - x_1}$$

整理得到:

$$p = \frac{x_2 - x}{x_2 - x_1} p_1 + \frac{x - x_1}{x_2 - x_1} p_2$$

然后,在垂直方向上进行线性插值,得到输出像素值 $p$

$$p = \frac{y_2 - y}{y_2 - y_1} p_{1y} + \frac{y - y_1}{y_2 - y_1} p_{2y}$$

在 PyTorch 中,可以使用 torch.nn.functional.interpolate 函数来实现双线性插值的上采样操作:

import torch
import torch.nn.functional as F
# 创建一个示例输入张量,形状为 (batch_size, channels, height, width)
input_tensor = torch.tensor([[[[1, 2], [3, 4]]]], dtype=torch.float32) # 形状为 (1, 1, 2, 2)
# 使用双线性插值进行上采样,目标尺寸为 (4, 4)
output_tensor = F.interpolate(input_tensor, size=(4, 4), mode='bilinear', align_corners=False)
print(output_tensor)

转置卷积

转置卷积(Transposed Convolution),也称反卷积(Deconvolution)。是一种通过学习卷积核来实现上采样的方法。与标准卷积不同,反卷积通过在输入特征图上插入零值来增加空间尺寸,然后应用卷积操作以生成更高分辨率的输出。

反卷积并不是卷积的逆操作,该名称只是一个谬误。

给定输入图像 $I = \begin{bmatrix}1&2\\3&4\end{bmatrix}$ 和卷积核 $K = \begin{bmatrix}0&1\\2&3\end{bmatrix}$ ,输入图像中的每个元素与卷积核进行数乘,形成 4 个小矩阵:

$$\begin{aligned} I_{11} &= \begin{bmatrix}0&1\\2&3\end{bmatrix}\\ I_{12} &= \begin{bmatrix}0&2\\4&6\end{bmatrix}\\ I_{21} &= \begin{bmatrix}0&3\\6&9\end{bmatrix}\\ I_{22} &= \begin{bmatrix}0&4\\8&12\end{bmatrix} \end{aligned}$$

四个矩阵按输入图像中元素的位置按步长为 1 进行排列,重叠部分相加,得到输出图像:

$$O = \begin{bmatrix} 0&1&2\\ 2&10&10\\ 6&10&10 \end{bmatrix}$$

对于输入图像 $\mathbf{I} \in \mathbb{R} ^ {h \times w}$ ,卷积核 $\mathbb{K} \in \mathbb{R} ^ {k^2}$ ,步长为 $(s_h, s_w)$ ,填充为 $(p_h, p_w)$ 的转置卷积操作,每个输入像素都会与卷积核进行数乘,形成 $hw$ $k \times k$ 的小矩阵。然后,这些小矩阵按输入图像中元素的位置以步长 $(s_h, s_w)$ 进行排列,重叠部分相加,得到输出图像,并按照填充去除边界的像素。因此,输出图像的尺寸为:

$$\begin{equation} \begin{aligned} H &= (h - 1) \cdot s_h - 2p_h + k \\ W &= (w - 1) \cdot s_w - 2p_w + k \end{aligned} \label{eq-deconv-output-size} \end{equation}$$

考虑对 $\mathbb{I} \in \mathbb{R} ^ {H \times W}$ 执行普通卷积操作,使用相同大小的卷积核 $\mathbb{K} \in \mathbb{R} ^ {k^2}$ ,步长为 $(s_h, s_w)$ ,填充为 $(p_h, p_w)$ ,则输出图像的尺寸为:

$$\begin{aligned} h &= \frac{H + 2p_h - k}{s_h} + 1 \\ w &= \frac{W + 2p_w - k}{s_w} + 1 \end{aligned}$$

$h$ $w$ 分别带入转置卷积的输出尺寸公式中,可以发现,转置卷积的输出尺寸正好是原始输入图像的尺寸 $H \times W$ 。这表明转置卷积 在空间尺寸上 与普通卷积操作是互逆的。一次规格相同的卷积操作,紧接着一次规格相同的转置卷积操作,可以恢复到原始的空间尺寸。

与原图尺寸无关的上采样核设计

$\eqref{eq-deconv-output-size}$ 中,令 $H = 2h$ $W = 2w$ ,可以得到下面的特解:

$$\begin{cases} k = 4\\ s_h = s_w = 2\\ p_h = p_w = 1 \end{cases}$$

这意味着使用大小为 $4 \times 4$ 的卷积核,步长为 2,填充为 1 的转置卷积操作,可以将输入图像的空间尺寸扩大为原图的 2 倍。这是深度学习中常用的上采样技巧。类似地,可以通过调整卷积核大小、步长和填充来设计不同的与原图尺寸无关的上采样核。

在 PyTorch 中,使用 torch.nn.ConvTranspose2d 类来实现转置参数化的转置卷积操作:

import torch
import torch.nn as nn
# 定义转置卷积层
deconv = nn.ConvTranspose2d(in_channels=1, out_channels=1, kernel_size=4, stride=2, padding=1)
# 创建一个示例输入张量,形状为 (batch_size, in_channels, height, width)
input_tensor = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32).unsqueeze(0).unsqueeze(0) # 形状为 (1, 1, 2, 2)
# 执行转置卷积操作
output_tensor = deconv(input_tensor)

torch.nn.Functional 模块中也提供了 conv_transpose2d 函数,可以实现非参数化的转置卷积操作:

import torch
import torch.nn.functional as F
# 创建一个示例输入张量,形状为 (batch_size, in_channels, height, width)
input_tensor = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32).unsqueeze(0).unsqueeze(0) # 形状为 (1, 1, 2, 2)
# 定义卷积核张量,形状为 (in_channels, out_channels, kernel_height, kernel_width)
kernel = torch.tensor([[0, 1], [2, 3]], dtype=torch.float32).unsqueeze(0).unsqueeze(0) # 形状为 (1, 1, 2, 2)
# 执行转置卷积操作
output_tensor = F.conv_transpose2d(input_tensor, kernel)
print(output_tensor)

这个例子的计算结果与前面的手动计算结果相同:

tensor([[[[ 0.,  1.,  2.],
          [ 2., 10., 10.],
          [ 6., 17., 12.]]]])