3.7. Cách lập trình súc tích Hồi quy Softmax¶
Giống như cách Gluon giúp việc lập trình hồi quy tuyến tính ở Section 3.3 trở nên dễ dàng hơn, ta sẽ thấy nó cũng mang đến sự tiện lợi tương tự (hoặc có thể hơn) khi lập trình các mô hình phân loại. Một lần nữa, chúng ta bắt đầu bằng việc nhập các gói thư viện.
from d2l import mxnet as d2l
from mxnet import gluon, init, npx
from mxnet.gluon import nn
npx.set_np()
Chúng ta sẽ tiếp tục làm việc với bộ dữ liệu Fashion-MNIST và giữ kích thước batch bằng \(256\) như ở mục trước.
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
3.7.1. Khởi tạo Tham số Mô hình¶
Như đã đề cập trong Section 3.4, tầng đầu ra của hồi quy
softmax là một tầng kết nối đầy đủ (Dense
). Do đó, để xây dựng mô
hình, ta chỉ cần thêm một tầng Dense
với 10 đầu ra vào đối tượng
Sequential
. Việc sử dụng Sequential
ở đây không thực sự cần
thiết, nhưng ta nên hình thành thói quen này vì nó sẽ luôn hiện diện khi
ta xây dựng các mô hình sâu. Một lần nữa, chúng ta khởi tạo các trọng số
một cách ngẫu nhiên với trung bình bằng không và độ lệch chuẩn bằng
\(0.01\).
net = nn.Sequential()
net.add(nn.Dense(10))
net.initialize(init.Normal(sigma=0.01))
3.7.2. Hàm Softmax¶
Ở ví dụ trước, ta đã tính toán kết quả đầu ra của mô hình và sau đó đưa
các kết quả này vào hàm mất mát entropy chéo. Về mặt toán học, cách làm
này hoàn toàn có lý. Tuy nhiên, từ góc độ điện toán, sử dụng hàm mũ có
thể là nguồn gốc của các vấn đề về ổn định số học (được bàn trong
Section 18.9). Hãy nhớ rằng, hàm softmax tính
\(\hat y_j = \frac{e^{z_j}}{\sum_{i=1}^{n} e^{z_i}}\), trong đó
\(\hat y_j\) là phần tử thứ \(j^\mathrm{th}\) của yhat
và
\(z_j\) là phần tử thứ \(j^\mathrm{th}\) của biến đầu vào
y_linear
.
Nếu một phần tử \(z_i\) quá lớn, \(e^{z_i}\) có thể sẽ lớn hơn
giá trị cực đại mà kiểu float
có thể biểu diễn được (đây là hiện
tượng tràn số trên). Lúc này mẫu số hoặc tử số (hoặc cả hai) sẽ tiến tới
inf
và ta gặp phải trường hợp \(\hat y_i\) bằng \(0\),
inf
hoặc nan
. Trong những tình huống này, giá trị trả về của
cross_entropy
có thể không được xác định một cách rõ ràng. Một mẹo
để khắc phục việc này là: đầu tiên ta trừ tất cả các \(z_i\) đi
\(\text{max}(z_i)\), sau đó mới đưa chúng vào hàm softmax
. Bạn
có thể nhận thấy rằng việc tịnh tiến mỗi \(z_i\) theo một hệ số
không đổi sẽ không làm ảnh hưởng đến giá trị trả về của hàm softmax
.
Sau khi thực hiện bước trừ và chuẩn hóa, một vài \(z_j\) có thể có
giá trị âm lớn và do đó \(e^{z_j}\) sẽ xấp xỉ 0. Điều này có thể dẫn
đến việc chúng bị làm tròn thành 0 do khả năng biễu diễn chính xác là
hữu hạn (tức tràn số dưới), khiến \(\hat y_j\) tiến về không và giá
trị \(\text{log}(\hat y_j)\) tiến về -inf
. Thực hiện vài bước
lan truyền ngược với lỗi trên, ta có thể sẽ đối mặt với một loạt giá trị
nan
(not-a-number: không phải số) đáng sợ.
May mắn thay, mặc dù ta đang thực hiện tính toán với các hàm mũ, kết quả
cuối cùng ta muốn là giá trị log của nó (khi tính hàm mất mát entropy
chéo). Bằng cách kết hợp cả hai hàm (softmax
và cross-entropy
)
lại với nhau, ta có thể khắc phục vấn đề về ổn định số học và tránh gặp
khó khăn trong quá trình lan truyền ngược. Trong phương trình bên dưới,
ta đã không tính \(e^{z_j}\) mà thay vào đó, ta tính trực tiếp
\(z_j\) do việc rút gọn \(\log(\exp(\cdot))\).
Ta vẫn muốn giữ lại hàm softmax gốc để sử dụng khi muốn tính đầu ra của
mô hình dưới dạng xác suất. Nhưng thay vì truyền xác suất softmax vào
hàm mất mát mới, ta sẽ chỉ truyền các giá trị logit (các giá trị khi
chưa qua softmax) và tính softmax cùng log của nó trong hàm mất mát
softmax_cross_entropy
. Hàm này cũng sẽ tự động thực hiện các mẹo
thông minh như log-sum-exp (xem thêm
Wikipedia).
loss = gluon.loss.SoftmaxCrossEntropyLoss()
3.7.3. Thuật toán Tối ưu¶
Ở đây, chúng ta sử dụng thuật toán tối ưu hạ gradient ngẫu nhiên theo minibatch với tốc độ học bằng \(0.1\). Lưu ý rằng cách làm này giống hệt cách làm ở ví dụ về hồi quy tuyến tính, minh chứng cho tính khái quát của bộ tối ưu hạ gradient ngẫu nhiên.
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1})
3.7.4. Huấn luyện¶
Tiếp theo, chúng ta sẽ gọi hàm huấn luyện đã được khai báo ở mục trước để huấn luyện mô hình.
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
Giống lần trước, thuật toán hội tụ tới một mô hình có độ chính xác 83.7% nhưng chỉ khác là cần ít dòng lệnh hơn. Lưu ý rằng trong nhiều trường hợp, Gluon không chỉ dùng các mánh phổ biến mà còn sử dụng các kỹ thuật khác để tránh các lỗi kĩ thuật tính toán mà ta dễ gặp phải nếu tự lập trình mô hình từ đầu.
3.7.5. Bài tập¶
- Thử thay đổi các siêu tham số như kích thước batch, số epoch và tốc độ học. Theo dõi kết quả sau khi thay đổi.
- Tại sao độ chính xác trên tập kiểm tra lại giảm sau một khoảng thời gian? Chúng ta giải quyết việc này thế nào?
3.7.6. Thảo luận¶
3.7.7. 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 Duy Du
- Vũ Hữu Tiệp
- Lê Khắc Hồng Phúc
- Lý Phi Long
- Phạm Minh Đức
- Dương Nhật Tân
- Nguyễn Văn Tâm
- Phạm Hồng Vinh