.. raw:: html .. raw:: html .. raw:: html .. _sec_conv_layer: Phép Tích chập cho Ảnh ====================== .. raw:: html Giờ chúng ta đã hiểu cách các tầng tích chập hoạt động trên lý thuyết, hãy xem chúng hoạt động trong thực tế như thế nào. Dựa vào ý tưởng mạng nơ-ron tích chập là kiến trúc hiệu quả để khám phá cấu trúc của dữ liệu ảnh, chúng tôi vẫn sẽ sử dụng loại dữ liệu này khi lấy ví dụ. .. raw:: html Toán tử Tương quan Chéo ----------------------- .. raw:: html Như ta đã biết, tầng *tích chập* là cái tên có phần không chính xác, vì phép toán mà chúng biểu diễn là phép tương quan chéo (*cross correlation*). Trong một tầng tích chập, một mảng đầu vào và một mảng *hạt nhân tương quan* được kết hợp để tạo ra mảng đầu ra bằng phép toán tương quan chéo. Hãy tạm thời bỏ qua chiều kênh và xem phép toán này hoạt động như thế nào với dữ liệu và biểu diễn ẩn hai chiều. Trong :numref:`fig_correlation`, đầu vào là một mảng hai chiều với chiều dài 3 và chiều rộng 3. Ta kí hiệu kích thước của mảng là :math:`3 \times 3` hoặc (:math:`3`, :math:`3`). Chiều dài và chiều rộng của hạt nhân đều là 2. Chú ý rằng trong cộng đồng nghiên cứu học sâu, mảng này còn có thể được gọi là *hạt nhân tích chập*, *bộ lọc* hay đơn thuần là *trọng số* của tầng. Kích thước của cửa sổ hạt nhân là chiều dài và chiều rộng của hạt nhân (ở đây là :math:`2 \times 2`). .. raw:: html .. _fig_correlation: .. figure:: ../img/correlation.svg Phép tương quan chéo hai chiều. Các phần được tô màu là phần tử đầu tiên của đầu ra cùng với các phần tử của mảng đầu vào và mảng hạt nhân được sử dụng trong phép toán: :math:`0\times0+1\times1+3\times2+4\times3=19`. .. raw:: html Trong phép tương quan chéo hai chiều, ta bắt đầu với cửa sổ tích chập đặt tại vị trí góc trên bên trái của mảng đầu vào và di chuyển cửa sổ này từ trái sang phải và từ trên xuống dưới. Khi cửa sổ tích chập được đẩy tới một vị trí nhất định, mảng con đầu vào nằm trong cửa sổ đó và mảng hạt nhân được nhân theo từng phần tử, rồi sau đó ta lấy tổng các phần tử trong mảng kết quả để có được một giá trị số vô hướng duy nhất. Giá trị này được ghi vào mảng đầu ra tại vị trí tương ứng. Ở đây, mảng đầu ra có chiều dài 2 và chiều rộng 2, với bốn phần tử được tính bằng phép tương quan chéo hai chiều: .. math:: 0\times0+1\times1+3\times2+4\times3=19,\\ 1\times0+2\times1+4\times2+5\times3=25,\\ 3\times0+4\times1+6\times2+7\times3=37,\\ 4\times0+5\times1+7\times2+8\times3=43. .. raw:: html .. raw:: html .. raw:: html Lưu ý rằng theo mỗi trục, kích thước đầu ra *nhỏ hơn* một chút so với đầu vào. Bởi vì hạt nhân có chiều dài và chiều rộng lớn hơn một, ta chỉ có thể tính độ tương quan chéo cho những vị trí mà ở đó hạt nhân nằm hoàn toàn bên trong ảnh, kích thước đầu ra được tính bằng cách lấy đầu vào :math:`H \times W` trừ kích thước của bộ lọc tích chập :math:`h \times w` bằng :math:`(H-h+1) \times (W-w+1)`. Điều này xảy ra vì ta cần đủ không gian để ‘dịch chuyển’ hạt nhân tích chập qua tấm hình (sau này ta sẽ xem làm thế nào để có thể giữ nguyên kích thước bằng cách đệm các số không vào xung quanh biên của hình ảnh sao cho có đủ không gian để dịch chuyển hạt nhân). Kế tiếp, ta lập trình quá trình ở trên trong hàm ``corr2d``. Hàm này nhận mảng đầu vào ``X`` với mảng hạt nhân ``K`` và trả về mảng đầu ra ``Y``. .. code:: python from mxnet import autograd, np, npx from mxnet.gluon import nn npx.set_np() # Saved in the d2l package for later use def corr2d(X, K): """Compute 2D cross-correlation.""" h, w = K.shape Y = np.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) for i in range(Y.shape[0]): for j in range(Y.shape[1]): Y[i, j] = (X[i: i + h, j: j + w] * K).sum() return Y .. raw:: html Ta có thể xây dựng mảng đầu vào ``X`` và mảng hạt nhân ``K`` như hình trên để kiểm tra lại kết quả của cách lập trình phép toán tương quan chéo hai chiều vừa rồi. .. code:: python X = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) K = np.array([[0, 1], [2, 3]]) corr2d(X, K) .. parsed-literal:: :class: output array([[19., 25.], [37., 43.]]) .. raw:: html .. raw:: html .. raw:: html Tầng Tích chập -------------- .. raw:: html Tầng tích chập thực hiện phép toán tương quan chéo giữa đầu vào và hạt nhân, sau đó cộng thêm một hệ số điều chỉnh để có được đầu ra. Hai tham số của tầng tích chập là hạt nhân và hệ số điều chỉnh. Khi huấn luyện mô hình chứa các tầng tích chập, ta thường khởi tạo hạt nhân ngẫu nhiên, giống như cách ta làm với tầng kết nối đầy đủ. .. raw:: html Bây giờ ta đã sẵn sàng lập trình một tầng tích chập hai chiều dựa vào hàm ``corr2d`` ta vừa định nghĩa ở trên. Trong hàm khởi tạo ``__init__``, ta khai báo hai tham số của mô hình ``weight`` và ``bias``. Hàm tính lượt truyền xuôi ``forward`` gọi hàm ``corr2d`` và cộng thêm hệ số điều chỉnh. Cũng giống cách gọi phép tương quan chéo :math:`h \times w`, ta cũng gọi các tầng tích chập là phép tích chập :math:`h \times w`. .. code:: python class Conv2D(nn.Block): def __init__(self, kernel_size, **kwargs): super(Conv2D, self).__init__(**kwargs) self.weight = self.params.get('weight', shape=kernel_size) self.bias = self.params.get('bias', shape=(1,)) def forward(self, x): return corr2d(x, self.weight.data()) + self.bias.data() .. raw:: html .. raw:: html .. raw:: html Phát hiện Biên của Vật thể trong Ảnh ------------------------------------ .. raw:: html Hãy quan sát một ứng dụng đơn giản của tầng tích chập: phát hiện đường biên của một vật thể trong một bức ảnh bằng cách xác định vị trí các điểm ảnh thay đổi. Đầu tiên, ta dựng một ‘bức ảnh’ có kích thước là :math:`6\times 8` điểm ảnh. Bốn cột ở giữa có màu đen (giá trị 0) và các cột còn lại có màu trắng (giá trị 1). .. code:: python X = np.ones((6, 8)) X[:, 2:6] = 0 X .. parsed-literal:: :class: output array([[1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.]]) .. raw:: html Sau đó, ta tạo một hạt nhân ``K`` có chiều cao bằng :math:`1` và chiều rộng bằng :math:`2`. Khi thực hiện phép tương quan chéo với đầu vào, nếu hai phần tử cạnh nhau theo chiều ngang có giá trị giống nhau thì đầu ra sẽ bằng 0, còn lại đầu ra sẽ khác không. .. code:: python K = np.array([[1, -1]]) .. raw:: html Ta đã sẵn sàng thực hiện phép tương quan chéo với các đối số ``X`` (đầu vào) và ``K`` (hạt nhân). Bạn có thể thấy rằng các vị trí biên trắng đổi thành đen có giá trị 1, còn các vị trí biên đen đổi thành trắng có giá trị -1. Các vị trí còn lại của đầu ra có giá trị 0. .. code:: python Y = corr2d(X, K) Y .. parsed-literal:: :class: output array([[ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.]]) .. raw:: html Bây giờ hãy áp dụng hạt nhân này cho chuyển vị của ma trận điểm ảnh. Như kỳ vọng, giá trị tương quan chéo bằng không. Hạt nhân ``K`` chỉ có thể phát hiện biên dọc. .. code:: python corr2d(X.T, K) .. parsed-literal:: :class: output array([[0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]]) .. raw:: html .. raw:: html .. raw:: html .. raw:: html .. raw:: html Học một Bộ lọc -------------- .. raw:: html Việc thiết kế bộ phát hiện biên bằng sai phân hữu hạn ``[1, -1]`` thì khá gọn gàng nếu ta biết chính xác đây là những gì cần làm. Tuy nhiên, khi xét tới các bộ lọc lớn hơn và các tầng tích chập liên tiếp, việc chỉ định chính xác mỗi bộ lọc cần làm gì một cách thủ công là bất khả thi. .. raw:: html Bây giờ ta hãy xem liệu có thể học một bộ lọc có khả năng tạo ra ``Y`` từ\ ``X`` chỉ từ các cặp (đầu vào, đầu ra) hay không. Đầu tiên chúng ta xây dựng một tầng tích chập và khởi tạo một mảng ngẫu nhiên làm bộ lọc. Tiếp theo, trong mỗi lần lặp, ta sẽ sử dụng bình phương sai số để so sánh ``Y`` và đầu ra của tầng tích chập, sau đó tính toán gradient để cập nhật trọng số. Để đơn giản, trong tầng tích chập này, ta sẽ bỏ qua hệ số điều chỉnh. .. raw:: html Trước đây ta đã tự xây dựng lớp ``Conv2D``. Tuy nhiên, do ta sử dụng các phép gán một phần tử, Gluon sẽ gặp một số khó khăn khi tính gradient. Thay vào đó, ta sử dụng lớp ``Conv2D`` có sẵn của Gluon như sau. .. code:: python # Construct a convolutional layer with 1 output channel # (channels will be introduced in the following section) # and a kernel array shape of (1, 2) conv2d = nn.Conv2D(1, kernel_size=(1, 2)) conv2d.initialize() # The two-dimensional convolutional layer uses four-dimensional input and # output in the format of (example, channel, height, width), where the batch # size (number of examples in the batch) and the number of channels are both 1 X = X.reshape(1, 1, 6, 8) Y = Y.reshape(1, 1, 6, 7) for i in range(10): with autograd.record(): Y_hat = conv2d(X) l = (Y_hat - Y) ** 2 l.backward() # For the sake of simplicity, we ignore the bias here conv2d.weight.data()[:] -= 3e-2 * conv2d.weight.grad() if (i + 1) % 2 == 0: print('batch %d, loss %.3f' % (i + 1, l.sum())) .. parsed-literal:: :class: output batch 2, loss 4.949 batch 4, loss 0.831 batch 6, loss 0.140 batch 8, loss 0.024 batch 10, loss 0.004 .. raw:: html Có thể thấy sai số đã giảm xuống còn khá nhỏ sau 10 lần lặp. Bây giờ hãy xem mảng bộ lọc đã học được. .. code:: python conv2d.weight.data().reshape(1, 2) .. parsed-literal:: :class: output array([[ 0.9895 , -0.9873705]]) .. raw:: html Thật vậy, mảng bộ lọc học được rất gần với mảng bộ lọc ``K`` mà ta tự định nghĩa trước đó. .. raw:: html .. raw:: html .. raw:: html Tương quan Chéo và Tích chập ---------------------------- .. raw:: html Hãy nhớ lại kiến thức của phần trước về mối liên hệ giữa phép tương quan chéo và tích chập. Trong hình trên, ta dễ dàng nhận thấy điều này. Đơn giản chỉ cần lật bộ lọc từ góc dưới cùng bên trái lên góc trên cùng bên phải. Trong trường hợp này, chỉ số trong phép lấy tổng được đảo ngược, nhưng ta vẫn thu được kết quả tương tự. Để thống nhất với các thuật ngữ tiêu chuẩn trong tài liệu học sâu, ta sẽ tiếp tục đề cập đến phép tương quan chéo như là phép tích chập, mặc dù đúng ra chúng hơi khác nhau một chút. .. raw:: html Tóm tắt ------- .. raw:: html - Về cốt lõi, phần tính toán của tầng tích chập hai chiều là phép tương quan chéo hai chiều. Ở dạng đơn giản nhất, phép tương quan chéo thao tác trên dữ liệu đầu vào hai chiều và bộ lọc, sau đó cộng thêm hệ số điều chỉnh. - Chúng ta có thể thiết kế bộ lọc để phát hiện các biên trong ảnh. - Chúng ta có thể học các tham số của bộ lọc từ dữ liệu. .. raw:: html Bài tập ------- .. raw:: html 1. Xây dựng hình ảnh ``X`` với các cạnh chéo. - Điều gì xảy ra nếu bạn áp dụng bộ lọc ``K`` lên nó? - Điều gì xảy ra nếu bạn chuyển vị ``X``? - Điều gì xảy ra nếu bạn chuyển vị ``K``? 2. Khi thử tự động tìm gradient cho lớp ``Conv2D`` mà ta đã tạo, bạn thấy loại thông báo lỗi nào? 3. Làm thế nào để bạn biểu diễn một phép tính tương quan chéo như là một phép nhân ma trận bằng cách thay đổi các mảng đầu vào và mảng bộ lọc? 4. Hãy thiết kế thủ công một số bộ lọc sau. - Bộ lọc để tính đạo hàm bậc hai có dạng như thế nào? - Bộ lọc của toán tử Laplace là gì? - Bộ lọc của phép tích phân là gì? - Kích thước tối thiểu của bộ lọc để có được đạo hàm bậc :math:`d` là bao nhiêu? .. raw:: html .. raw:: html Thảo luận --------- - `Tiếng Anh `__ - `Tiếng Việt `__ Những người thực hiện --------------------- Bản dịch trong trang này được thực hiện bởi: - Đoàn Võ Duy Thanh - Nguyễn Văn Cường - Lê Khắc Hồng Phúc - Phạm Hồng Vinh - Lý Phi Long - Phạm Minh Đức - Trần Yến Thy