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_units
và units
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ínhparams
của Block.
5.4.4. Bài tập¶
- 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.
- 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\).
- 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