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\(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 (softmaxcross-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))\).

(3.7.1)\[\begin{split}\begin{aligned} \log{(\hat y_j)} & = \log\left( \frac{e^{z_j}}{\sum_{i=1}^{n} e^{z_i}}\right) \\ & = \log{(e^{z_j})}-\text{log}{\left( \sum_{i=1}^{n} e^{z_i} \right)} \\ & = z_j -\log{\left( \sum_{i=1}^{n} e^{z_i} \right)}. \end{aligned}\end{split}\]

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)
../_images/output_softmax-regression-gluon_vn_91b0d4_11_0.svg

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

  1. 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.
  2. 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