机器学习 / A LEAF IN THE GARDEN

转置卷积

机器学习 · 动手深度学习。来自 Daily Learning 的学习笔记。

步骤

  • 输入图像 XX 每个像素 xx 与卷积核 KK 中所有元素相乘
  • 得到和卷积核大小相同的矩阵 K′K'
  • 每个 K′K' 按原始像素位置放置
  • 重叠处求和得到输出图像 YY

trans_conv

步幅

xx 移动一格时,K′K' 移动的格数就是卷积的步幅

转置卷积步幅 >1\gt 1 时,可以增大高宽

也相当于把 XX 的像素拉开,空隙插入 s−1s-1 个零,再做普通转置卷积

填充

和卷积填充相反,剥离 YY 外缘的 pp 圈像素

转置

对于卷积 Y=X∗KY = X * K

  • 可以对 KK 构造一个带状矩阵 VV
  • X,YX, Y 展开为向量 X′,Y′X', Y'
  • 使卷积相当于矩阵乘法 Y′=VX′Y' = VX'

此时转置卷积等价于 X′=VTY′X' = V^TY'

  • 卷积: (h,w)→(h′,w′)(h, w) \to (h', w')
  • 转置卷积: (h′,w′)→(h,w)(h', w') \to (h, w)

形状换算

输入高(宽)为 nn,卷积核 kk,步幅 ss,填充 pp,输出高(宽)为 n′n'

  • 卷积: n′=n+2p−ks+1n' = \cfrac{n + 2p - k}{s} + 1
  • 转置卷积: n=s(n′−1)+k−2pn = s(n' - 1) + k - 2p

如果让高宽成倍增加,则 k=2p+sk=2p+s

实现

代码 4 · In [1]
import torch
from torch import nn
import util
已保存输出
/root/autodl-tmp/envs/daily_learning/lib/python3.11/site-packages/requests/__init__.py:92: RequestsDependencyWarning: Unable to find acceptable character detection dependency (chardet or charset_normalizer).
  warnings.warn(

基本转置卷积运算

代码 6 · In [2]
def trans_conv(X, K):
    h, w = K.shape
    # 创建一个全 0 矩阵承接输出
    Y = torch.zeros((X.shape[0] + h - 1, X.shape[1] + w - 1))
    for i in range(X.shape[0]):
        for j in range(X.shape[1]):
            Y[i:i+h, j:j+w] += X[i, j] * K # 逐位置叠加 K'
    return Y

验证

代码 8 · In [3]
K = torch.FloatTensor([[1, 2], [3, 4]])
X = torch.FloatTensor([[0, 1], [2, 3]])
trans_conv(X, K)
已保存输出
tensor([[ 0.,  1.,  2.],
        [ 2., 10., 10.],
        [ 6., 17., 12.]])

直接调用 api 也是一样的

代码 10 · In [4]
X = X.reshape((1, 1, 2, 2)) # (N, C, H, W)
K = K.reshape((1, 1, 2, 2)) # (C_out, C_in, H, W)
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, bias=False)
tconv.weight.data = K
tconv(X).squeeze() # (N, C, H, W) -> (H, W)
已保存输出
tensor([[ 0.,  1.,  2.],
        [ 2., 10., 10.],
        [ 6., 17., 12.]], grad_fn=<SqueezeBackward0>)

填充、步幅和通道数

代码 12 · In [5]
tconv = nn.ConvTranspose2d(1, 1, kernel_size=3, padding=1, bias=False) # 剥离一圈
tconv.weight.data = K
tconv(X).squeeze((0, 1)) # (N, C, H, W) -> (H, W)
已保存输出
tensor([[10.]], grad_fn=<SqueezeBackward2>)
代码 13 · In [6]
tconv = nn.ConvTranspose2d(1, 1, kernel_size=3, stride=2, bias=False)
tconv.weight.data = K
tconv(X).squeeze()
已保存输出
tensor([[ 0.,  0.,  1.,  2.],
        [ 0.,  0.,  3.,  4.],
        [ 2.,  4.,  3.,  6.],
        [ 6.,  8.,  9., 12.]], grad_fn=<SqueezeBackward0>)
代码 14 · In [7]
conv = nn.Conv2d(1, 8, kernel_size=2, stride=2, padding=2)
tconv = nn.ConvTranspose2d(8, 1, kernel_size=2, stride=2, padding=2)
Y = conv(X)
X.shape, Y.shape, tconv(Y).shape
已保存输出
(torch.Size([1, 1, 2, 2]), torch.Size([1, 8, 3, 3]), torch.Size([1, 1, 2, 2]))

与矩阵乘法的联系

代码 16 · In [12]
def conv_to_mm(X, K):
    """卷积转矩阵乘法"""
    h, w = K.shape
    V = torch.zeros((X.shape[0] + h - 1, X.shape[1] + w - 1, X.shape[0], X.shape[1]))
    for i in range(X.shape[0]):
        for j in range(X.shape[1]):
            V[i:i+h, j:j+w, i, j] = K
    return V.reshape((X.shape[0] + h - 1) * (X.shape[1] + w - 1), X.numel())
V = conv_to_mm(X.squeeze(), K.squeeze())
V
已保存输出
tensor([[1., 0., 0., 0.],
        [2., 1., 0., 0.],
        [0., 2., 0., 0.],
        [3., 0., 1., 0.],
        [4., 3., 2., 1.],
        [0., 4., 0., 2.],
        [0., 0., 3., 0.],
        [0., 0., 4., 3.],
        [0., 0., 0., 4.]])
代码 17 · In [13]
Y = torch.matmul(V, X.squeeze().reshape(-1))
Y.reshape(3, 3)
已保存输出
tensor([[ 0.,  1.,  2.],
        [ 2., 10., 10.],
        [ 6., 17., 12.]])
代码 18 · In [14]
Z = torch.matmul(V.T, Y.reshape(-1))
Z.reshape(2, 2)
已保存输出
tensor([[ 48.,  75.],
        [108., 129.]])