7.6. Mạng phần dư (ResNet)

Khi thiết kế các mạng ngày càng sâu, ta cần hiểu việc thêm các tầng sẽ tăng độ phức tạp và khả năng biểu diễn của mạng như thế nào. Quan trọng hơn là khả năng thiết kế các mạng trong đó việc thêm các tầng vào mạng chắc chắn sẽ làm tăng tính biểu diễn thay vì chỉ tạo ra một chút khác biệt. Để làm được điều này, chúng ta cần một chút lý thuyết.

7.6.1. Các Lớp Hàm Số

Coi \(\mathcal{F}\) là một lớp các hàm mà một kiến trúc mạng cụ thể (cùng với tốc độ học và các siêu tham số khác) có thể đạt được. Nói cách khác, với mọi hàm số \(f \in \mathcal{F}\), luôn tồn tại một số tập tham số \(W\) có thể tìm được bằng việc huấn luyện trên một tập dữ liệu phù hợp. Giả sử \(f^*\) là hàm cần tìm. Sẽ rất thuận lợi nếu hàm này thuộc tập \(\mathcal{F}\), nhưng thường không may mắn như vậy. Thay vào đó, ta sẽ cố gắng tìm các hàm số \(f^*_\mathcal{F}\) tốt nhất có thể trong tập \(\mathcal{F}\).
Ví dụ, có thể thử tìm \(f^*_\mathcal{F}\) bằng cách giải bài toán tối ưu sau:
(7.6.1)\[f^*_\mathcal{F} := \mathop{\mathrm{argmin}}_f L(X, Y, f) \text{ đối~tượng~thoả~mãn } f \in \mathcal{F}.\]

Khá hợp lý khi giả sử rằng nếu thiết kế một kiến trúc khác \(\mathcal{F}'\) mạnh mẽ hơn thì sẽ đạt được kết quả tốt hơn. Nói cách khác, ta kỳ vọng hàm số \(f^*_{\mathcal{F}'}\) sẽ “tốt hơn” \(f^*_{\mathcal{F}}\). Tuy nhiên, nếu \(\mathcal{F} \not\subseteq \mathcal{F}'\), thì không khẳng định được \(f^*_{\mathcal{F}'}\) “tốt hơn” \(f^*_{\mathcal{F}}\). Trên thực tế, \(f^*_{\mathcal{F}'}\) có thể còn tệ hơn. Và đây là trường hợp thường xuyên xảy ra — việc thêm các tầng không phải lúc nào cũng tăng tính biểu diễn của mạng mà đôi khi còn tạo ra những thay đổi rất khó lường. Fig. 7.6.1 minh hoạ rõ hơn điều này.

../_images/functionclasses.svg

Fig. 7.6.1 Hình trái: Các lớp hàm số tổng quát. Khoảng cách đến hàm cần tìm \(f^*\) (ngôi sao), trên thực tế có thể tăng khi độ phức tạp tăng lên. Hình phải: với các lớp hàm số lồng nhau, điều này không xảy ra.

Chỉ khi các lớp hàm lớn hơn chứa các lớp nhỏ hơn, thì mới đảm bảo rằng việc tăng thêm các tầng sẽ tăng khả năng biểu diễn của mạng. Đây là câu hỏi mà He và các cộng sự đã suy nghĩ khi nghiên cứu các mô hình thị giác sâu năm 2016. Ý tưởng trọng tâm của ResNet là mỗi tầng được thêm vào nên có một thành phần là hàm số đồng nhất. Điều này có nghĩa rằng, nếu ta huấn luyện tầng mới được thêm vào thành một ánh xạ đồng nhất \(f(\mathbf{x}) = \mathbf{x}\), thì mô hình mới sẽ hiệu quả ít nhất bằng mô hình ban đầu. Vì tầng được thêm vào có thể khớp dữ liệu huấn luyện tốt hơn, dẫn đến sai số huấn luyện cũng nhỏ hơn. Tốt hơn nữa, hàm số đồng nhất nên là hàm đơn giản nhất trong một tầng thay vì hàm null \(f(\mathbf{x}) = 0\).

Cách suy nghĩ này khá trừu tượng nhưng lại dẫn đến một lời giải đơn giản đáng ngạc nhiên, một khối phần dư (residual block). Với ý tưởng này, [He et al., 2016a] đã chiến thắng cuộc thi Nhận dạng Ảnh ImageNet năm 2015. Thiết kế này có ảnh hưởng sâu sắc tới việc xây dựng các mạng nơ-ron sâu.

7.6.2. Khối phần dư

Bây giờ, hãy tập trung vào mạng nơ-ron dưới đây. Ký hiệu đầu vào là \(\mathbf{x}\). Giả sử ánh xạ lý tưởng muốn học được là \(f(\mathbf{x})\), và được dùng làm đầu vào của hàm kích hoạt. Phần nằm trong viền nét đứt bên trái phải khớp trực tiếp với ánh xạ \(f(\mathbf{x})\). Điều này có thể không đơn giản nếu chúng ta không cần khối đó và muốn giữ lại đầu vào \(\mathbf{x}\). Khi đó, phần nằm trong viền nét đứt bên phải chỉ cần tham số hoá độ lệch khỏi giá trị \(\mathbf{x}\), bởi vì ta đã trả về \(\mathbf{x} + f(\mathbf{x})\). Trên thực tế, ánh xạ phần dư thường dễ tối ưu hơn, vì chỉ cần đặt \(f(\mathbf{x}) = 0\). Nửa bên phải Fig. 7.6.2 mô tả khối phần dư cơ bản của ResNet. Về sau, những kiến trúc tương tự đã được đề xuất cho các mô hình chuỗi (sequence model), sẽ đề cập ở chương sau.

../_images/residual-block.svg

Fig. 7.6.2 Sự khác biệt giữa một khối thông thường (trái) và một khối phần dư (phải). Trong khối phần dư, ta có thể nối tắt các tích chập.

ResNet có thiết kế tầng tích chập \(3\times 3\) giống VGG. Khối phần dư có hai tầng tích chập \(3\times 3\) với cùng số kênh đầu ra. Mỗi tầng tích chập được theo sau bởi một tầng chuẩn hóa theo batch và một hàm kích hoạt ReLU. Ta đưa đầu vào qua khối phần dư rồi cộng với chính nó trước hàm kích hoạt ReLU cuối cùng. Thiết kế này đòi hỏi đầu ra của hai tầng tích chập phải có cùng kích thước với đầu vào, để có thể cộng lại với nhau. Nếu muốn thay đổi số lượng kênh hoặc sải bước trong khối phần dư, cần thêm một tầng tích chập \(1\times 1\) để thay đổi kích thước đầu vào tương ứng ở nhánh ngoài. Hãy cùng xem đoạn mã bên dưới.

from d2l import mxnet as d2l
from mxnet import np, npx
from mxnet.gluon import nn
npx.set_np()

# Saved in the d2l package for later use
class Residual(nn.Block):
    def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs):
        super(Residual, self).__init__(**kwargs)
        self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1,
                               strides=strides)
        self.conv2 = nn.Conv2D(num_channels, kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2D(num_channels, kernel_size=1,
                                   strides=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm()
        self.bn2 = nn.BatchNorm()

    def forward(self, X):
        Y = npx.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        return npx.relu(Y + X)

Đoạn mã này tạo ra hai loại mạng: một loại cộng đầu vào vào đầu ra trước khi áp dụng hàm phi tuyến ReLU (khi use_1x1conv=True), còn ở loại thứ hai chúng ta thay đổi số kênh và độ phân giải bằng một tầng tích chập \(1 \times 1\) trước khi thực hiện phép cộng. Fig. 7.6.3 minh họa điều này:

../_images/resnet-block.svg

Fig. 7.6.3 Trái: khối ResNet thông thường; Phải: Khối ResNet với tầng tích chập 1x1

Giờ hãy xem xét tình huống khi cả đầu vào và đầu ra có cùng kích thước.

blk = Residual(3)
blk.initialize()
X = np.random.uniform(size=(4, 3, 6, 6))
blk(X).shape
(4, 3, 6, 6)

Chúng ta cũng có thể giảm một nửa kích thước chiều cao và chiều rộng của đầu ra trong khi tăng số kênh.

blk = Residual(6, use_1x1conv=True, strides=2)
blk.initialize()
blk(X).shape
(4, 6, 3, 3)

7.6.3. Mô hình ResNet

Hai tầng đầu tiên của ResNet giống hai tầng đầu tiên của GoogLeNet: tầng tích chập \(7\times 7\) với 64 kênh đầu ra và sải bước 2, theo sau bởi tầng gộp cực đại \(3 \times 3\) với sải bước 2. Sự khác biệt là trong ResNet, mỗi tầng tích chập theo sau bởi tầng chuẩn hóa theo batch.

net = nn.Sequential()
net.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3),
        nn.BatchNorm(), nn.Activation('relu'),
        nn.MaxPool2D(pool_size=3, strides=2, padding=1))

GoogLeNet sử dụng bốn mô-đun được tạo thành từ các khối Inception. ResNet sử dụng bốn mô-đun được tạo thành từ các khối phần dư có cùng số kênh đầu ra. Mô-đun đầu tiên có số kênh bằng số kênh đầu vào. Vì trước đó đã sử dụng tầng gộp cực đại với sải bước 2, nên không cần phải giảm chiều cao và chiều rộng ở mô-đun này. Trong các mô-đun sau, khối phần dư đầu tiên nhân đôi số kênh, đồng thời giảm một nửa chiều cao và chiều rộng.

Bây giờ ta sẽ lập trình mô-đun này. Chú ý rằng mô-đun đầu tiên được xử lý khác một chút.

def resnet_block(num_channels, num_residuals, first_block=False):
    blk = nn.Sequential()
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.add(Residual(num_channels, use_1x1conv=True, strides=2))
        else:
            blk.add(Residual(num_channels))
    return blk

Sau đó, chúng ta thêm các khối phần dư vào ResNet. Ở đây, mỗi mô-đun có hai khối phần dư.

net.add(resnet_block(64, 2, first_block=True),
        resnet_block(128, 2),
        resnet_block(256, 2),
        resnet_block(512, 2))

Cuối cùng, giống như GoogLeNet, ta thêm một tầng gộp trung bình toàn cục và một tầng kết nối đầy đủ.

net.add(nn.GlobalAvgPool2D(), nn.Dense(10))

Có 4 tầng tích chập trong mỗi mô-đun (không tính tầng tích chập \(1 \times 1\)). Cộng thêm tầng tích chập đầu tiên và tầng kết nối đầy đủ cuối cùng, mô hình có tổng cộng 18 tầng. Do đó, mô hình này thường được gọi là ResNet-18. Có thể thay đổi số kênh và các khối phần dư trong mô-đun để tạo ra các mô hình ResNet khác nhau, ví dụ mô hình 152 tầng của ResNet-152. Mặc dù có kiến trúc lõi tương tự như GoogLeNet, cấu trúc của ResNet đơn giản và dễ sửa đổi hơn. Tất cả các yếu tố này dẫn đến sự phổ cập nhanh chóng và rộng rãi của ResNet. Fig. 7.6.4 là sơ đồ đầy đủ của ResNet-18.

../_images/ResNetFull.svg

Fig. 7.6.4 ResNet-18

Trước khi huấn luyện, hãy quan sát thay đổi của kích thước đầu vào qua các mô-đun khác nhau trong ResNet. Như trong tất cả các kiến trúc trước, độ phân giải giảm trong khi số lượng kênh tăng đến khi tầng gộp trung bình toàn cục tổng hợp tất cả các đặc trưng.

X = np.random.uniform(size=(1, 1, 224, 224))
net.initialize()
for layer in net:
    X = layer(X)
    print(layer.name, 'output shape:\t', X.shape)
conv5 output shape:  (1, 64, 112, 112)
batchnorm4 output shape:     (1, 64, 112, 112)
relu0 output shape:  (1, 64, 112, 112)
pool0 output shape:  (1, 64, 56, 56)
sequential1 output shape:    (1, 64, 56, 56)
sequential2 output shape:    (1, 128, 28, 28)
sequential3 output shape:    (1, 256, 14, 14)
sequential4 output shape:    (1, 512, 7, 7)
pool1 output shape:  (1, 512, 1, 1)
dense0 output shape:         (1, 10)

7.6.4. Thu thập dữ liệu và Huấn luyện

Giống như các phần trước, chúng ta huấn luyện ResNet trên bộ dữ liệu Fashion-MNIST. Thay đổi duy nhất là giảm tốc độ học lại do kiến trúc mạng phức tạp hơn.

lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr)
loss 0.024, train acc 0.993, test acc 0.929
1884.4 examples/sec on gpu(0)
../_images/output_resnet_vn_ea6188_17_1.svg

7.6.5. Tóm tắt

  • Khối phần dư cho phép tham số hóa đến hàm đồng nhất \(f(\mathbf{x}) = \mathbf{x}\).
  • Thêm các khối phần dư làm tăng độ phức tạp của hàm số theo một cách chủ đích.
  • Chúng ta có thể huấn luyện hiệu quả mạng nơ-ron sâu nhờ khối phần dư chuyển dữ liệu liên tầng.
  • ResNet có ảnh hưởng lớn đến thiết kế sau này của các mạng nơ-ron sâu, cả tích chập và tuần tự.

7.6.6. Bài tập

  1. Tham khảo Bảng 1 trong [He et al., 2016a] để lập trình các biến thể khác nhau.
  2. Đối với các mạng sâu hơn, ResNet giới thiệu kiến trúc “thắt cổ chai” để giảm độ phức tạp của mô hình. Hãy thử lập trình kiến trúc đó.
  3. Trong các phiên bản sau của ResNet, tác giả đã thay đổi kiến trúc “tích chập, chuẩn hóa theo batch, và hàm kích hoạt” thành “chuẩn hóa theo batch, hàm kích hoạt, và tích chập”. Hãy tự lập trình kiến trúc này. Xem hình 1 trong [He et al., 2016b] để biết chi tiết.
  4. Chứng minh rằng nếu \(\mathbf{x}\) được tạo ra bởi ReLU thì khối ResNet sẽ bao gồm hàm số đồng nhất.
  5. Tại sao không thể tăng không giới hạn độ phức tạp của các hàm số, ngay cả với các lớp hàm lồng nhau?

7.6.7. Thảo luận

7.6.8. 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 Quang
  • Nguyễn Cảnh Thướng
  • Lê Khắc Hồng Phúc
  • Nguyễn Văn Cường
  • Nguyễn Đình Nam
  • Phạm Minh Đức
  • Phạm Hồng Vinh