5.4. Các tầng Tuỳ chỉnh

Một trong những yếu tố dẫn đến thành công của học sâu là sự đa dạng của các tầng. Những tầng này có thể được sắp xếp theo nhiều cách sáng tạo để thiết kế nên những kiến trúc phù hợp với nhiều tác vụ khác nhau. Ví dụ, các nhà nghiên cứu đã phát minh ra các tầng chuyên dụng để xử lý ảnh, chữ viết, lặp trên dữ liệu tuần tự, thực thi quy hoạch động, v.v… Dù sớm hay muộn, bạn cũng sẽ gặp (hoặc sáng tạo) một tầng không có trong Gluon. Đối với những trường hợp như vậy, bạn cần xây dựng một tầng tuỳ chỉnh. Phần này sẽ hướng dẫn bạn cách thực hiện điều đó.

5.4.1. Các tầng không có Tham số

Để bắt đầu, ta tạo một tầng tùy chỉnh (một Khối) không chứa bất kỳ tham số nào. Bước này khá quen thuộc nếu bạn còn nhớ phần giới thiệu về Block của Gluon tại Section 5.1. Lớp CenteredLayer chỉ đơn thuần trừ đi giá trị trung bình từ đầu vào của nó. Để xây dựng nó, chúng ta chỉ cần kế thừa từ lớp Block và lập trình phương thức forward.

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

class CenteredLayer(nn.Block):
    def __init__(self, **kwargs):
        super(CenteredLayer, self).__init__(**kwargs)

    def forward(self, x):
        return x - x.mean()

Hãy cùng xác thực rằng tầng này hoạt động như ta mong muốn bằng cách truyền dữ liệu vào nó.

layer = CenteredLayer()
layer(np.array([1, 2, 3, 4, 5]))
array([-2., -1.,  0.,  1.,  2.])

Chúng ta cũng có thể kết hợp tầng này như là một thành phần để xây dựng các mô hình phức tạp hơn.

net = nn.Sequential()
net.add(nn.Dense(128), CenteredLayer())
net.initialize()

Để kiểm tra thêm, chúng ta có thể truyền dữ liệu ngẫu nhiên qua mạng và chứng thực xem giá trị trung bình đã về 0 hay chưa. Chú ý rằng vì đang làm việc với các số thực dấu phẩy động, chúng ta sẽ thấy một giá trị khác không rất nhỏ.

y = net(np.random.uniform(size=(4, 8)))
y.mean()
array(3.783498e-10)

5.4.2. Tầng có Tham số

Giờ đây ta đã biết cách định nghĩa các tầng đơn giản, hãy chuyển sang việc định nghĩa các tầng chứa tham số có thể điều chỉnh được trong quá trình huấn luyện. Để tự động hóa các công việc lặp lại, lớp Parameter và từ điển ParameterDict cung cấp một số tính năng quản trị cơ bản. Cụ thể, chúng sẽ quản lý việc truy cập, khởi tạo, chia sẻ, lưu và nạp các tham số mô hình. Bằng cách này, cùng với nhiều lợi ích khác, ta không cần phải viết lại các thủ tục tuần tự hóa (serialization) cho mỗi tầng tùy chỉnh mới.

Lớp Block chứa biến params với kiểu dữ liệu ParameterDict. Từ điển này ánh xạ các xâu kí tự biểu thị tên tham số đến các tham số mô hình (thuộc kiểu Parameter). ParameterDict cũng cung cấp hàm get giúp việc tạo tham số mới với tên và chiều cụ thể trở nên dễ dàng.

params = gluon.ParameterDict()
params.get('param2', shape=(2, 3))
params
(
  Parameter param2 (shape=(2, 3), dtype=<class 'numpy.float32'>)
)

Giờ đây chúng ta đã có tất cả các thành phần cơ bản cần thiết để tự tạo một phiên bản tùy chỉnh của tầng Dense trong Gluon. Chú ý rằng tầng này yêu cầu hai tham số: một cho trọng số và một cho hệ số điều chỉnh. Trong cách lập trình này, ta sử dụng hàm kích hoạt mặc định là hàm ReLU. Trong hàm __init__, in_unitsunits biểu thị lần lượt số lượng đầu vào và đầu ra.

class MyDense(nn.Block):
    # units: the number of outputs in this layer; in_units: the number of
    # inputs in this layer
    def __init__(self, units, in_units, **kwargs):
        super(MyDense, self).__init__(**kwargs)
        self.weight = self.params.get('weight', shape=(in_units, units))
        self.bias = self.params.get('bias', shape=(units,))

    def forward(self, x):
        linear = np.dot(x, self.weight.data()) + self.bias.data()
        return npx.relu(linear)

Việc đặt tên cho các tham số cho phép ta truy cập chúng theo tên thông qua tra cứu từ điển sau này. Nhìn chung, bạn sẽ muốn đặt cho các biến những tên đơn giản biểu thị rõ mục đích của chúng. Tiếp theo, ta sẽ khởi tạo lớp MyDense và truy cập các tham số mô hình. Lưu ý rằng tên của Khối được tự động thêm vào trước tên các tham số.

dense = MyDense(units=3, in_units=5)
dense.params
mydense0_ (
  Parameter mydense0_weight (shape=(5, 3), dtype=<class 'numpy.float32'>)
  Parameter mydense0_bias (shape=(3,), dtype=<class 'numpy.float32'>)
)

Ta có thể trực tiếp thực thi các phép tính truyền xuôi có sử dụng các tầng tùy chỉnh.

dense.initialize()
dense(np.random.uniform(size=(2, 5)))
array([[0.        , 0.01633355, 0.        ],
       [0.        , 0.01581812, 0.        ]])

Các tầng tùy chỉnh cũng có thể được dùng để xây dựng mô hình. Chúng có thể được sử dụng như các tầng kết nối dày đặc được lập trình sẵn. Ngoại lệ duy nhất là việc suy luận kích thước sẽ không được thực hiện tự động. Để biết thêm chi tiết về cách thực hiện việc này, vui lòng tham khảo tài liệu MXNet.

net = nn.Sequential()
net.add(MyDense(8, in_units=64),
        MyDense(1, in_units=8))
net.initialize()
net(np.random.uniform(size=(2, 64)))
array([[0.06508517],
       [0.0615553 ]])

5.4.3. Tóm tắt

  • Ta có thể thiết kế các tầng tùy chỉnh thông qua lớp Block. Điều này cho phép ta định nghĩa một cách linh hoạt các tầng có cách hoạt động khác với các tầng có sẵn trong thư viện.
  • Một khi đã được định nghĩa, các tầng tùy chỉnh có thể được gọi trong những bối cảnh và kiến trúc tùy ý.
  • Các khối có thể có các tham số cục bộ, được lưu trữ dưới dạng đối tượng ParameterDict trong mỗi thuộc tính params của Block.

5.4.4. Bài tập

  1. Thiết kế một tầng có khả năng học một phép biến đổi affine của dữ liệu.
  2. Thiết kế một tầng nhận đầu vào và tính toán phép giảm tensor, tức trả về \(y_k = \sum_{i, j} W_{ijk} x_i x_j\).
  3. Thiết kế một tầng trả về nửa đầu của các hệ số Fourier của dữ liệu. Gợi ý: hãy tra cứu hàm fft trong MXNet.

5.4.5. Thảo luận

5.4.6. 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 Lê Quang Nhật
  • Nguyễn Văn Cường
  • Phạm Hồng Vinh
  • Lê Khắc Hồng Phúc
  • Phạm Minh Đức
  • Nguyễn Duy Du