.. _sec_transposed_conv:
Chuyển đổi Convolution
======================
Các lớp CNN chúng ta đã thấy cho đến nay, chẳng hạn như các lớp phức tạp
(:numref:`sec_conv_layer`) và các lớp tổng hợp
(:numref:`sec_pooling`), thường làm giảm (downsample) kích thước không
gian (chiều cao và chiều rộng) của đầu vào, hoặc giữ chúng không thay
đổi. Trong phân đoạn ngữ nghĩa phân loại ở cấp pixel, sẽ thuận tiện nếu
kích thước không gian của đầu vào và đầu ra giống nhau. Ví dụ: kích
thước kênh tại một điểm ảnh đầu ra có thể giữ kết quả phân loại cho
pixel đầu vào ở cùng một vị trí không gian.
Để đạt được điều này, đặc biệt là sau khi các kích thước không gian bị
giảm bởi các lớp CNN, chúng ta có thể sử dụng một loại lớp CNN khác có
thể tăng (upsample) kích thước không gian của bản đồ tính năng trung
gian. Trong phần này, chúng tôi sẽ giới thiệu *chuyển đổi*, mà còn được
gọi là \* phân số strided convolution\* :cite:`Dumoulin.Visin.2016`,
để đảo ngược các hoạt động lấy mẫu xuống bằng sự covolution.
.. raw:: html
.. raw:: html
.. code:: python
from mxnet import init, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
.. raw:: html
.. raw:: html
.. code:: python
import torch
from torch import nn
from d2l import torch as d2l
.. raw:: html
.. raw:: html
Hoạt động cơ bản
----------------
Bỏ qua các kênh bây giờ, chúng ta hãy bắt đầu với hoạt động phức tạp
chuyển tiếp cơ bản với sải chân 1 và không có đệm. Giả sử rằng chúng ta
được đưa ra một tensor đầu vào :math:`n_h \times n_w` và một hạt nhân
:math:`k_h \times k_w`. Trượt cửa sổ hạt nhân với sải chân 1 cho
:math:`n_w` lần trong mỗi hàng và :math:`n_h` lần trong mỗi cột mang lại
tổng cộng :math:`n_h n_w` kết quả trung gian. Mỗi kết quả trung gian là
một tensor :math:`(n_h + k_h - 1) \times (n_w + k_w - 1)` được khởi tạo
dưới dạng số không. Để tính từng tensor trung gian, mỗi phần tử trong
tensor đầu vào được nhân với hạt nhân sao cho tensor
:math:`k_h \times k_w` kết quả thay thế một phần trong mỗi tensor trung
gian. Lưu ý rằng vị trí của phần thay thế trong mỗi tensor trung gian
tương ứng với vị trí của phần tử trong tensor đầu vào được sử dụng cho
tính toán. Cuối cùng, tất cả các kết quả trung gian được tóm tắt để tạo
ra đầu ra.
Ví dụ, :numref:`fig_trans_conv` minh họa cách chuyển đổi phức tạp với
một hạt nhân :math:`2\times 2` được tính toán cho một tensor đầu vào
:math:`2\times 2`.
.. _fig_trans_conv:
.. figure:: ../img/trans_conv.svg
Transposed convolution with a :math:`2\times 2` kernel. The shaded
portions are a portion of an intermediate tensor as well as the input
and kernel tensor elements used for the computation.
Chúng ta có thể triển khai hoạt động chuyển tiếp cơ bản này
``trans_conv`` cho một ma trận đầu vào ``X`` và một ma trận hạt nhân
``K``.
.. raw:: html
.. raw:: html
.. code:: python
def trans_conv(X, K):
h, w = K.shape
Y = np.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
return Y
.. raw:: html
.. raw:: html
.. code:: python
def trans_conv(X, K):
h, w = K.shape
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
return Y
.. raw:: html
.. raw:: html
Trái ngược với sự phức tạp thông thường (trong
:numref:`sec_conv_layer`) mà \* giảm các yếu tố đầu vào thông qua hạt
nhân, sự biến đổi chuyển đổi *phát sóng * yếu tố đầu vào thông qua hạt
nhân, do đó tạo ra một đầu ra lớn hơn đầu vào. Chúng ta có thể xây dựng
tensor đầu vào ``X`` và tensor kernel ``K`` từ
:numref:`fig_trans_conv` đến xác nhận đầu ra của việc triển khai ở
trên của hoạt động phức tạp chuyển tiếp hai chiều cơ bản.
.. raw:: html
.. raw:: html
.. code:: python
X = np.array([[0.0, 1.0], [2.0, 3.0]])
K = np.array([[0.0, 1.0], [2.0, 3.0]])
trans_conv(X, K)
.. parsed-literal::
:class: output
array([[ 0., 0., 1.],
[ 0., 4., 6.],
[ 4., 12., 9.]])
.. raw:: html
.. raw:: html
.. code:: python
X = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
trans_conv(X, K)
.. parsed-literal::
:class: output
tensor([[ 0., 0., 1.],
[ 0., 4., 6.],
[ 4., 12., 9.]])
.. raw:: html
.. raw:: html
Ngoài ra, khi đầu vào ``X`` và hạt nhân ``K`` đều là hàng chục bốn
chiều, chúng ta có thể sử dụng API cấp cao để có được kết quả tương tự.
.. raw:: html
.. raw:: html
.. code:: python
X, K = X.reshape(1, 1, 2, 2), K.reshape(1, 1, 2, 2)
tconv = nn.Conv2DTranspose(1, kernel_size=2)
tconv.initialize(init.Constant(K))
tconv(X)
.. parsed-literal::
:class: output
array([[[[ 0., 0., 1.],
[ 0., 4., 6.],
[ 4., 12., 9.]]]])
.. raw:: html
.. raw:: html
.. code:: python
X, K = X.reshape(1, 1, 2, 2), K.reshape(1, 1, 2, 2)
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, bias=False)
tconv.weight.data = K
tconv(X)
.. parsed-literal::
:class: output
tensor([[[[ 0., 0., 1.],
[ 0., 4., 6.],
[ 4., 12., 9.]]]], grad_fn=)
.. raw:: html
.. raw:: html
Đệm, sải bước và nhiều kênh
---------------------------
Khác với trong sự phức tạp thông thường nơi đệm được áp dụng cho đầu
vào, nó được áp dụng cho đầu ra trong quá trình chuyển đổi. Ví dụ: khi
chỉ định số đệm ở hai bên của chiều cao và chiều rộng là 1, các hàng và
cột đầu tiên và cuối cùng sẽ bị xóa khỏi đầu ra phức tạp được chuyển
đổi.
.. raw:: html
.. raw:: html
.. code:: python
tconv = nn.Conv2DTranspose(1, kernel_size=2, padding=1)
tconv.initialize(init.Constant(K))
tconv(X)
.. parsed-literal::
:class: output
array([[[[4.]]]])
.. raw:: html
.. raw:: html
.. code:: python
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, padding=1, bias=False)
tconv.weight.data = K
tconv(X)
.. parsed-literal::
:class: output
tensor([[[[4.]]]], grad_fn=)
.. raw:: html
.. raw:: html
Trong quá trình chuyển đổi, các bước tiến được chỉ định cho các kết quả
trung gian (do đó đầu ra), không phải cho đầu vào. Sử dụng cùng một đầu
vào và kernel tenors từ :numref:`fig_trans_conv`, thay đổi sải chân từ
1 đến 2 làm tăng cả chiều cao và trọng lượng của hàng chục trung gian,
do đó tensor đầu ra trong :numref:`fig_trans_conv_stride2`.
.. _fig_trans_conv_stride2:
.. figure:: ../img/trans_conv_stride2.svg
Transposed convolution with a :math:`2\times 2` kernel with stride of
2. The shaded portions are a portion of an intermediate tensor as
well as the input and kernel tensor elements used for the
computation.
Đoạn mã sau đây có thể xác nhận đầu ra phức tạp transposed cho sải chân
của 2 trong :numref:`fig_trans_conv_stride2`.
.. raw:: html
.. raw:: html
.. code:: python
tconv = nn.Conv2DTranspose(1, kernel_size=2, strides=2)
tconv.initialize(init.Constant(K))
tconv(X)
.. parsed-literal::
:class: output
array([[[[0., 0., 0., 1.],
[0., 0., 2., 3.],
[0., 2., 0., 3.],
[4., 6., 6., 9.]]]])
.. raw:: html
.. raw:: html
.. code:: python
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, stride=2, bias=False)
tconv.weight.data = K
tconv(X)
.. parsed-literal::
:class: output
tensor([[[[0., 0., 0., 1.],
[0., 0., 2., 3.],
[0., 2., 0., 3.],
[4., 6., 6., 9.]]]], grad_fn=)
.. raw:: html
.. raw:: html
Đối với nhiều kênh đầu vào và đầu ra, sự tích tụ được chuyển tiếp hoạt
động theo cách tương tự như sự phức tạp thông thường. Giả sử rằng đầu
vào có :math:`c_i` các kênh và sự phức tạp transposed gán một tensor hạt
nhân :math:`k_h\times k_w` cho mỗi kênh đầu vào. Khi nhiều kênh đầu ra
được chỉ định, chúng tôi sẽ có hạt nhân :math:`c_i\times k_h\times k_w`
cho mỗi kênh đầu ra.
Như trong tất cả, nếu chúng ta cung cấp :math:`\mathsf{X}` vào một lớp
phức tạp :math:`f` để xuất ra :math:`\mathsf{Y}=f(\mathsf{X})` và tạo ra
một lớp tích hợp chuyển đổi :math:`g` với các siêu tham số tương tự như
:math:`f` ngoại trừ số lượng kênh đầu ra là số kênh trong
:math:`\mathsf{X}`, sau đó :math:`g(Y)` sẽ có hình dạng tương tự như
:math:`\mathsf{X}`. Điều này có thể được minh họa trong ví dụ sau.
.. raw:: html
.. raw:: html
.. code:: python
X = np.random.uniform(size=(1, 10, 16, 16))
conv = nn.Conv2D(20, kernel_size=5, padding=2, strides=3)
tconv = nn.Conv2DTranspose(10, kernel_size=5, padding=2, strides=3)
conv.initialize()
tconv.initialize()
tconv(conv(X)).shape == X.shape
.. parsed-literal::
:class: output
True
.. raw:: html
.. raw:: html
.. code:: python
X = torch.rand(size=(1, 10, 16, 16))
conv = nn.Conv2d(10, 20, kernel_size=5, padding=2, stride=3)
tconv = nn.ConvTranspose2d(20, 10, kernel_size=5, padding=2, stride=3)
tconv(conv(X)).shape == X.shape
.. parsed-literal::
:class: output
True
.. raw:: html
.. raw:: html
.. _subsec-connection-to-mat-transposition:
Kết nối với Ma trận Transposition
---------------------------------
Sự phức tạp chuyển tiếp được đặt tên theo chuyển vị ma trận. Để giải
thích, trước tiên chúng ta hãy xem làm thế nào để thực hiện các phức tạp
bằng cách sử dụng phép nhân ma trận. Trong ví dụ dưới đây, chúng ta định
nghĩa một :math:`3\times 3` đầu vào ``X`` và một hạt nhân ghép
:math:`2\times 2` ``K``, và sau đó sử dụng hàm ``corr2d`` để tính toán
đầu ra phức tạp ``Y``.
.. raw:: html
.. raw:: html
.. code:: python
X = np.arange(9.0).reshape(3, 3)
K = np.array([[1.0, 2.0], [3.0, 4.0]])
Y = d2l.corr2d(X, K)
Y
.. parsed-literal::
:class: output
array([[27., 37.],
[57., 67.]])
.. raw:: html
.. raw:: html
.. code:: python
X = torch.arange(9.0).reshape(3, 3)
K = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
Y = d2l.corr2d(X, K)
Y
.. parsed-literal::
:class: output
tensor([[27., 37.],
[57., 67.]])
.. raw:: html
.. raw:: html
Tiếp theo, chúng ta viết lại hạt nhân phức tạp ``K`` dưới dạng ma trận
trọng lượng thưa ``W`` chứa rất nhiều số không. Hình dạng của ma trận
trọng lượng là (:math:`4`, :math:`9`), trong đó các phần tử phi bằng
không đến từ hạt nhân phức tạp ``K``.
.. raw:: html
.. raw:: html
.. code:: python
def kernel2matrix(K):
k, W = np.zeros(5), np.zeros((4, 9))
k[:2], k[3:5] = K[0, :], K[1, :]
W[0, :5], W[1, 1:6], W[2, 3:8], W[3, 4:] = k, k, k, k
return W
W = kernel2matrix(K)
W
.. parsed-literal::
:class: output
array([[1., 2., 0., 3., 4., 0., 0., 0., 0.],
[0., 1., 2., 0., 3., 4., 0., 0., 0.],
[0., 0., 0., 1., 2., 0., 3., 4., 0.],
[0., 0., 0., 0., 1., 2., 0., 3., 4.]])
.. raw:: html
.. raw:: html
.. code:: python
def kernel2matrix(K):
k, W = torch.zeros(5), torch.zeros((4, 9))
k[:2], k[3:5] = K[0, :], K[1, :]
W[0, :5], W[1, 1:6], W[2, 3:8], W[3, 4:] = k, k, k, k
return W
W = kernel2matrix(K)
W
.. parsed-literal::
:class: output
tensor([[1., 2., 0., 3., 4., 0., 0., 0., 0.],
[0., 1., 2., 0., 3., 4., 0., 0., 0.],
[0., 0., 0., 1., 2., 0., 3., 4., 0.],
[0., 0., 0., 0., 1., 2., 0., 3., 4.]])
.. raw:: html
.. raw:: html
Nối đầu vào ``X`` từng hàng để có được một vectơ có chiều dài 9. Sau đó,
phép nhân ma trận của ``W`` và ``X`` vectơ hóa cho một vectơ có chiều
dài 4. Sau khi định hình lại nó, chúng ta có thể thu được kết quả tương
tự ``Y`` từ hoạt động phức tạp ban đầu ở trên: chúng ta chỉ thực hiện
các phép phức tạp bằng cách sử dụng phép nhân ma trận.
.. raw:: html
.. raw:: html
.. code:: python
Y == np.dot(W, X.reshape(-1)).reshape(2, 2)
.. parsed-literal::
:class: output
array([[ True, True],
[ True, True]])
.. raw:: html
.. raw:: html
.. code:: python
Y == torch.matmul(W, X.reshape(-1)).reshape(2, 2)
.. parsed-literal::
:class: output
tensor([[True, True],
[True, True]])
.. raw:: html
.. raw:: html
Tương tự như vậy, chúng ta có thể thực hiện các phép chuyển đổi bằng
cách sử dụng phép nhân ma trận. Trong ví dụ sau, chúng ta lấy đầu ra
:math:`2 \times 2` ``Y`` từ sự phức tạp thông thường ở trên làm đầu vào
cho sự phức tạp được chuyển tiếp. Để thực hiện thao tác này bằng cách
nhân ma trận, chúng ta chỉ cần chuyển vị ma trận trọng lượng ``W`` với
hình dạng mới :math:`(9, 4)`.
.. raw:: html
.. raw:: html
.. code:: python
Z = trans_conv(Y, K)
Z == np.dot(W.T, Y.reshape(-1)).reshape(3, 3)
.. parsed-literal::
:class: output
array([[ True, True, True],
[ True, True, True],
[ True, True, True]])
.. raw:: html
.. raw:: html
.. code:: python
Z = trans_conv(Y, K)
Z == torch.matmul(W.T, Y.reshape(-1)).reshape(3, 3)
.. parsed-literal::
:class: output
tensor([[True, True, True],
[True, True, True],
[True, True, True]])
.. raw:: html
.. raw:: html
Cân nhắc việc thực hiện sự phức tạp bằng cách nhân ma trận. Với một
vector đầu vào :math:`\mathbf{x}` và một ma trận trọng lượng
:math:`\mathbf{W}`, chức năng lan truyền chuyển tiếp của sự phức tạp có
thể được thực hiện bằng cách nhân đầu vào của nó với ma trận trọng lượng
và xuất ra một vector :math:`\mathbf{y}=\mathbf{W}\mathbf{x}`. Kể từ khi
backpropagation tuân theo quy tắc chuỗi và
:math:`\nabla_{\mathbf{x}}\mathbf{y}=\mathbf{W}^\top`, chức năng truyền
ngược của sự phức tạp có thể được thực hiện bằng cách nhân đầu vào của
nó với ma trận trọng lượng transposed :math:`\mathbf{W}^\top`. Do đó,
lớp tích lũy chuyển tiếp chỉ có thể trao đổi chức năng lan truyền về
phía trước và chức năng lan truyền ngược của lớp ghép: các chức năng lan
truyền về phía trước và lan truyền ngược của nó nhân vectơ đầu vào của
chúng với :math:`\mathbf{W}^\top` và :math:`\mathbf{W}`, tương ứng.
Tóm tắt
-------
- Ngược lại với sự phức tạp thông thường làm giảm các phần tử đầu vào
thông qua hạt nhân, sự phức tạp chuyển phát các phần tử đầu vào thông
qua hạt nhân, từ đó tạo ra một đầu ra lớn hơn đầu vào.
- Nếu chúng ta cho :math:`\mathsf{X}` vào một lớp phức hợp :math:`f` để
xuất ra :math:`\mathsf{Y}=f(\mathsf{X})` và tạo ra một lớp tích hợp
chuyển đổi :math:`g` với các siêu tham số tương tự như :math:`f`
ngoại trừ số lượng kênh đầu ra là số kênh trong :math:`\mathsf{X}`,
thì :math:`g(Y)` sẽ có hình dạng tương tự như :math:`\mathsf{X}`.
- Chúng ta có thể thực hiện các phức tạp bằng cách sử dụng phép nhân ma
trận. Lớp ghép chuyển tiếp chỉ có thể trao đổi chức năng lan truyền
về phía trước và chức năng lan truyền ngược của lớp ghép.
Bài tập
-------
1. Trong :numref:`subsec-connection-to-mat-transposition`, đầu vào
covolution ``X`` và đầu ra phức tạp transposed ``Z`` có hình dạng
tương tự. Họ có cùng giá trị không? Tại sao?
2. Có hiệu quả khi sử dụng phép nhân ma trận để thực hiện các phức tạp
không? Tại sao?
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html