.. raw:: html
.. raw:: html
.. raw:: html
.. _sec_softmax_scratch:
Lập trình Hồi quy Sofmax từ đầu
===============================
.. raw:: html
Giống như khi ta lập trình hồi quy tuyến tính từ đầu, hồi quy (softmax)
logistic đa lớp cũng là một kĩ thuật căn bản mà bạn nên hiểu biết tường
tận các chi tiết để có thể tự xây dựng lại. Sau khi tự lập trình lại mọi
thứ thì ta cũng sẽ dùng Gluon để so sánh như ở phần hồi quy tuyến tính.
Để bắt đầu, chúng ta nhập các thư viện quen thuộc vào.
.. code:: python
from d2l import mxnet as d2l
from mxnet import autograd, np, npx, gluon
from IPython import display
npx.set_np()
.. raw:: html
Ta sẽ làm việc trên tập dữ liệu Fashion-MNIST, vừa được giới thiệu trong
:numref:`sec_fashion_mnist`, thiết lập một iterator với kích thước
batch là :math:`256`.
.. code:: python
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
.. raw:: html
Khởi tạo các Tham số của Mô hình
--------------------------------
.. raw:: html
Giống như ví dụ về hồi quy tuyến tính, mỗi mẫu sẽ được biểu diễn bằng
một vector có chiều dài cố định. Mỗi mẫu trong tập dữ liệu thô là một
ảnh :math:`28 \times 28`. Trong phần này, chúng ta sẽ trải phẳng mỗi tấm
ảnh thành một vector có kích thước là :math:`784`. Sau này ta sẽ bàn về
các chiến lược công phu hơn có khả năng khai thác cấu trúc không gian
của ảnh, còn bây giờ ta hãy xem mỗi điểm ảnh là một đặc trưng.
.. raw:: html
Nhắc lại trong hồi quy softmax, mỗi lớp sẽ có một đầu ra. Vì tập dữ liệu
của chúng ta có :math:`10` lớp, mạng của chúng ta sẽ có :math:`10` đầu
ra. Do đó, các trọng số sẽ tạo thành một ma trận :math:`784 \times 10`
và các hệ số điều chỉnh sẽ tạo thành một vector :math:`1 \times 10`.
Cũng như hồi quy tuyến tính, ta sẽ khởi tạo các trọng số :math:`W` bằng
nhiễu Gauss và các hệ số điều chỉnh sẽ được khởi tạo bằng :math:`0`.
.. code:: python
num_inputs = 784
num_outputs = 10
W = np.random.normal(0, 0.01, (num_inputs, num_outputs))
b = np.zeros(num_outputs)
.. raw:: html
Hãy nhớ rằng ta cần *đính kèm gradient* vào các tham số của mô hình. Cụ
thể hơn, ta cho MXNet biết rằng ta sẽ muốn tính các gradient theo các
tham số này và cần phân bổ bộ nhớ để lưu trữ chúng trong tương lai.
.. code:: python
W.attach_grad()
b.attach_grad()
.. raw:: html
.. raw:: html
.. raw:: html
Softmax
-------
.. raw:: html
Trước khi xây dựng mô hình hồi quy softmax, hãy ôn nhanh tác dụng của
các toán tử như ``sum`` trên những chiều cụ thể của một ``ndarray``. Cho
một ma trận ``X``, chúng ta có thể tính tổng tất cả các phần tử (mặc
định) hoặc chỉ trên các phần tử trong cùng một trục, *ví dụ*, cột
(``axis=0``) hoặc cùng một hàng (``axis=1``). Lưu ý rằng nếu ``X`` là
một mảng có kích thước ``(2, 3)``, tính tổng các cột
(``X.sum (axis=0)``) sẽ trả về một vector (một chiều) có kích thước là
``(3,)``. Nếu chúng ta muốn giữ số lượng trục giống với mảng ban đầu
thay vì thu gọn kích thước trên các trục đã tính toán (dẫn đến một mảng
2 chiều có kích thước ``(1, 3)``), ta có thể chỉ định ``keepdims=True``
khi gọi hàm ``sum``.
.. code:: python
X = np.array([[1, 2, 3], [4, 5, 6]])
print(X.sum(axis=0, keepdims=True), '\n', X.sum(axis=1, keepdims=True))
.. parsed-literal::
:class: output
[[5. 7. 9.]]
[[ 6.]
[15.]]
.. raw:: html
Bây giờ chúng ta đã có thể bắt đầu xây dựng hàm softmax. Lưu ý rằng việc
thực thi hàm softmax bao gồm hai bước: Đầu tiên, chúng ta lũy thừa từng
giá trị (sử dụng ``exp``). Sau đó tính tổng trên mỗi hàng để lấy các
hằng số chuẩn hóa cho mỗi mẫu (vì mỗi mẫu là một hàng). Cuối cùng, chia
mỗi hàng theo hằng số chuẩn hóa của nó để đảm bảo rằng kết quả có tổng
bằng :math:`1`. Trước khi xem đoạn mã, chúng ta hãy nhớ lại các bước này
được thể hiện trong phương trình sau:
.. math::
\mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(X_{ij})}{\sum_k \exp(X_{ik})}.
.. raw:: html
Mẫu số hoặc hằng số chuẩn hóa đôi khi cũng được gọi là hàm phân hoạch
(*partition function*) và logarit của nó được gọi là hàm log phân hoạch
(*log-partition function*). Tên gốc của hàm được định nghĩa trong `cơ
học thống
kê `__
với phương trình liên quan đến mô hình hóa phân phối trên một tập hợp
các phần tử.
.. code:: python
def softmax(X):
X_exp = np.exp(X)
partition = X_exp.sum(axis=1, keepdims=True)
return X_exp / partition # The broadcast mechanism is applied here
.. raw:: html
Có thể thấy rằng với bất kỳ đầu vào ngẫu nhiên nào thì mỗi phần tử đều
được biến đổi thành một số không âm. Hơn nữa, mỗi hàng đều có tổng là 1
nên thoả mãn yêu cầu làm một giá trị xác suất. Chú ý rằng đoạn mã trên
tuy đúng về mặt toán học nhưng chúng tôi đã hơi cẩu thả về mặt lập trình
vì đã không kiểm tra vấn đề tràn số trên và dưới gây ra bởi các giá trị
vô cùng lớn hoặc vô cùng nhỏ trong ma trận, không giống với cách đã thực
hiện tại :numref:`sec_naive_bayes`.
.. code:: python
X = np.random.normal(size=(2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(axis=1)
.. parsed-literal::
:class: output
(array([[0.22376052, 0.06659239, 0.06583703, 0.29964197, 0.3441681 ],
[0.63209665, 0.03179282, 0.194987 , 0.09209415, 0.04902935]]),
array([1. , 0.99999994]))
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Mô hình
-------
.. raw:: html
Sau khi đã định nghĩa hàm softmax, chúng ta có thể bắt đầu lập trình mô
hình hồi quy softmax. Đoạn mã sau định nghĩa lượt truyền xuôi qua mạng.
Chú ý rằng chúng ta sẽ làm phẳng mỗi ảnh gốc trên batch thành một vector
có độ dài ``num_inputs`` bằng hàm ``reshape``, trước khi truyền dữ liệu
sang mô hình đã khởi tạo.
.. code:: python
def net(X):
return softmax(np.dot(X.reshape(-1, num_inputs), W) + b)
.. raw:: html
Hàm mất mát
-----------
.. raw:: html
Tiếp đến chúng ta cần lập trình hàm mất mát entropy chéo đã được giới
thiệu ở :numref:`sec_softmax`. Đây có lẽ là hàm mất mát thông dụng
nhất trong phần lớn nghiên cứu về học sâu vì hiện nay số lượng bài toán
phân loại đã vượt xa hơn số lượng bài toán hồi quy.
.. raw:: html
Nhắc lại rằng hàm entropy chéo lấy đầu vào là đối log hợp lý của xác
suất dự đoán được gán cho nhãn thật :math:`-\log P(y \mid x)`. Thay vì
lặp qua các dự đoán của mô hình bằng vòng lặp ``for`` trong Python (có
xu hướng kém hiệu quả), chúng ta có thể sử dụng hàm ``pick`` để dễ dàng
chọn các phần tử thích hợp từ ma trận của các giá trị softmax. Dưới đây,
ta minh hoạ cách sử dụng hàm ``pick`` trong một ví dụ đơn giản với ma
trận có :math:`3` lớp và :math:`2` mẫu.
.. code:: python
y_hat = np.array([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y_hat[[0, 1], [0, 2]]
.. parsed-literal::
:class: output
array([0.1, 0.5])
.. raw:: html
Bây giờ chúng ta có thể lập trình hàm mất mát entropy chéo hiệu quả hơn
chỉ với một dòng lệnh.
.. code:: python
def cross_entropy(y_hat, y):
return - np.log(y_hat[range(len(y_hat)), y])
.. raw:: html
.. raw:: html
.. raw:: html
Độ chính xác cho bài toán Phân loại
-----------------------------------
.. raw:: html
Với phân phối xác suất dự đoán ``y_hat``, ta thường chọn lớp có xác suất
dự đoán cao nhất khi phải đưa ra một dự đoán *cụ thể* vì nhiều ứng dụng
trong thực tế có yêu cầu như vậy. Ví dụ, Gmail cần phải phân loại một
email vào trong các danh mục sau: Email chính (Primary), Mạng xã hội
(Social), Nội dung cập nhật (Updates) hoặc Diễn đàn (Forums). Có thể các
xác suất được tính toán bên trong nội bộ hệ thống, nhưng cuối cùng kết
quả vẫn chỉ là một trong các danh mục.
.. raw:: html
Các dự đoán được coi là chính xác khi chúng khớp với lớp thực tế ``y``.
Độ chính xác phân loại được tính bởi tỉ lệ các dự đoán chính xác trên
tất cả các dự đoán đã đưa ra. Dù ta có thể gặp khó khăn khi tối ưu hóa
trực tiếp độ chính xác (chúng không khả vi), đây thường là phép đo chất
lượng được quan tâm nhiều nhất và sẽ luôn được tính khi huấn luyện các
bộ phân loại.
.. raw:: html
Độ chính xác được tính toán như sau: Đầu tiên, dùng lệnh
``y_hat.argmax(axis=1)`` nhằm lấy ra các lớp được dự đoán (được cho bởi
chỉ số của phần tử lớn nhất trên mỗi hàng). Kết quả trả về sẽ có cùng
kích thước với biến ``y`` và bây giờ ta chỉ cần so sánh hai vector này.
Vì toán tử ``==`` so khớp cả kiểu dữ liệu của biến (ví dụ một biến
``int`` và một biến ``float32`` không thể bằng nhau), ta cần đưa chúng
về cùng một kiểu dữ liệu (ở đây ta chọn kiểu ``float32``). Kết quả sẽ là
một ``ndarray`` chứa các giá trị 0 (false) và 1 (true), giá trị trung
bình này mang lại kết quả mà ta mong muốn.
.. code:: python
# Saved in the d2l package for later use
def accuracy(y_hat, y):
if y_hat.shape[1] > 1:
return float((y_hat.argmax(axis=1) == y.astype('float32')).sum())
else:
return float((y_hat.astype('int32') == y.astype('int32')).sum())
.. raw:: html
Ta sẽ tiếp tục sử dụng biến ``y_hat`` và ``y`` đã được định nghĩa trong
hàm ``pick``, lần lượt tương ứng với phân phối xác suất được dự đoán và
nhãn. Có thể thấy rằng kết quả dự đoán lớp từ ví dụ đầu tiên là
:math:`2` (phần từ lớn nhất trong hàng là :math:`0.6` với chỉ số tương
ứng là :math:`2`) không khớp với nhãn thực tế là :math:`0`. Dự đoán lớp
ở ví dụ thứ hai là :math:`2` (phần tử lớn nhất hàng là :math:`0.5` với
chỉ số tương ứng là :math:`2`) khớp với nhãn thực tế là :math:`2`. Do
đó, ta có độ chính xác phân loại cho hai ví dụ này là :math:`0.5`.
.. code:: python
y = np.array([0, 2])
accuracy(y_hat, y) / len(y)
.. parsed-literal::
:class: output
0.5
.. raw:: html
Tương tự như trên, ta có thể đánh giá độ chính xác của mô hình ``net``
trên tập dữ liệu (được truy xuất thông qua ``data_iter``).
.. code:: python
# Saved in the d2l package for later use
def evaluate_accuracy(net, data_iter):
metric = Accumulator(2) # num_corrected_examples, num_examples
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.size)
return metric[0] / metric[1]
.. raw:: html
``Accumulator`` ở đây là một lớp đa tiện ích, có tác dụng tính tổng tích
lũy của nhiều số.
.. code:: python
# Saved in the d2l package for later use
class Accumulator(object):
"""Sum a list of numbers over time."""
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a+float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0] * len(self.data)
def __getitem__(self, i):
return self.data[i]
.. raw:: html
Vì ta đã khởi tạo mô hình ``net`` với trọng số ngẫu nhiên nên độ chính
xác của mô hình lúc này sẽ gần với việc phỏng đoán ngẫu nhiên, tức độ
chính xác bằng :math:`0.1` với :math:`10` lớp.
.. code:: python
evaluate_accuracy(net, test_iter)
.. parsed-literal::
:class: output
0.0811
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Huấn luyện Mô hình
------------------
.. raw:: html
Vòng lặp huấn luyện cho hồi quy softmax trông khá quen thuộc nếu bạn đã
xem cách lập trình cho hồi quy tuyến tính tại
:numref:`sec_linear_scratch`. Ở đây, chúng ta tái cấu trúc
(*refactor*) lại đoạn mã để sau này có thể tái sử dụng. Đầu tiên, chúng
ta định nghĩa một hàm để huấn luyện với một epoch dữ liệu. Lưu ý rằng
``updater`` là một hàm tổng quát để cập nhật các tham số của mô hình và
sẽ nhận kích thước batch làm đối số. Nó có thể là một wrapper của
``d2l.sgd`` hoặc là một đối tượng huấn luyện Gluon.
.. code:: python
# Saved in the d2l package for later use
def train_epoch_ch3(net, train_iter, loss, updater):
metric = Accumulator(3) # train_loss_sum, train_acc_sum, num_examples
if isinstance(updater, gluon.Trainer):
updater = updater.step
for X, y in train_iter:
# Compute gradients and update parameters
with autograd.record():
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.size)
# Return training loss and training accuracy
return metric[0]/metric[2], metric[1]/metric[2]
.. raw:: html
Trước khi xem đoạn mã thực hiện hàm huấn luyện, ta định nghĩa một lớp
phụ trợ để minh họa dữ liệu. Mục đích của nó là đơn giản hoá các đoạn mã
sẽ xuất hiện trong những chương sau.
.. code:: python
# Saved in the d2l package for later use
class Animator(object):
def __init__(self, xlabel=None, ylabel=None, legend=[], xlim=None,
ylim=None, xscale='linear', yscale='linear', fmts=None,
nrows=1, ncols=1, figsize=(3.5, 2.5)):
"""Incrementally plot multiple lines."""
d2l.use_svg_display()
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# Use a lambda to capture arguments
self.config_axes = lambda: d2l.set_axes(
self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts
def add(self, x, y):
"""Add multiple data points into the figure."""
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
if not self.fmts:
self.fmts = ['-'] * n
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)
.. raw:: html
Hàm huấn luyện sau đó sẽ chạy qua nhiều epoch và trực quan hoá quá trình
huấn luyện.
.. code:: python
# Saved in the d2l package for later use
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
animator = Animator(xlabel='epoch', xlim=[1, num_epochs],
ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
animator.add(epoch+1, train_metrics+(test_acc,))
.. raw:: html
Nhắc lại, chúng ta sử dụng giải thuật hạ gradient ngẫu nhiên theo
minibatch để tối ưu hàm mất mát của mô hình. Lưu ý rằng số lượng epoch
(``num_epochs``), và hệ số học (``lr``) là hai siêu tham số được hiệu
chỉnh. Bằng cách thay đổi các giá trị này, chúng ta có thể tăng độ chính
xác khi phân loại của mô hình. Trong thực tế, chúng ta sẽ chia dữ liệu
của mình theo ba hướng tiếp cận khác nhau gồm: dữ liệu huấn luyện, dữ
liệu kiểm định và dữ liệu kiểm tra; sử dụng dữ liệu kiểm định để chọn ra
những giá trị tốt nhất cho các siêu tham số.
.. code:: python
num_epochs, lr = 10, 0.1
def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
.. figure:: output_softmax-regression-scratch_vn_6ac99c_37_0.svg
.. raw:: html
.. raw:: html
.. raw:: html
Dự đoán
-------
.. raw:: html
Giờ thì việc huấn luyện đã hoàn thành, mô hình của chúng ta đã sẵn sàng
để phân loại ảnh. Cho một loạt các ảnh, chúng ta sẽ so sánh các nhãn
thật của chúng (dòng đầu tiên của văn bản đầu ra) với những dự đoán của
mô hình (dòng thứ hai của văn bản đầu ra).
.. code:: python
# Saved in the d2l package for later use
def predict_ch3(net, test_iter, n=6):
for X, y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true+'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(X[0:n].reshape(n, 28, 28), 1, n, titles=titles[0:n])
predict_ch3(net, test_iter)
.. figure:: output_softmax-regression-scratch_vn_6ac99c_39_0.svg
.. raw:: html
Tóm tắt
-------
.. raw:: html
Với hồi quy softmax, chúng ta có thể huấn luyện các mô hình cho bài toán
phân loại đa lớp. Vòng lặp huấn luyện rất giống với vòng lặp huấn luyện
của hồi quy tuyến tính: truy xuất và đọc dữ liệu, định nghĩa mô hình và
hàm mất mát, và rồi huấn luyện mô hình sử dụng các giải thuật tối ưu.
Rồi bạn sẽ sớm thấy rằng hầu hết các mô hình học sâu phổ biến đều có quy
trình huấn luyện tương tự như vậy.
.. raw:: html
Bài tập
-------
.. raw:: html
1. Trong mục này, chúng ta đã lập trình hàm softmax dựa vào định nghĩa
toán học của phép toán softmax. Điều này có thể gây ra những vấn đề
gì (gợi ý: thử tính :math:`\exp(50)`)?
2. Hàm ``cross_entropy`` trong mục này được lập trình dựa vào định nghĩa
của hàm mất mát entropy chéo. Vấn đề gì có thể xảy ra với cách lập
trình như vậy (gợi ý: xem xét miền của hàm log)?
3. Bạn có thể tìm ra giải pháp cho hai vấn đề trên không?
4. Việc trả về nhãn có khả năng nhất có phải lúc nào cũng là ý tưởng tốt
không? Ví dụ, bạn có dùng phương pháp này cho chẩn đoán bệnh hay
không?
5. Giả sử rằng chúng ta muốn sử dụng hồi quy softmax để dự đoán từ tiếp
theo dựa vào một số đặc trưng. Những vấn đề gì có thể xảy ra nếu dùng
một tập từ vựng lớn?
.. raw:: html
.. raw:: html
.. raw:: html
Thảo luận
---------
- `Tiếng Anh `__
- `Tiếng Việt `__
.. raw:: html
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
- Bùi Nhật Quân
- Lý Phi Long
- Phạm Hồng Vinh
- Lâm Ngọc Tâm
- Vũ Hữu Tiệp
- Lê Khắc Hồng Phúc
- Nguyễn Cảnh Thướng
- Phạm Minh Đức
- Nguyễn Quang Hải
- Đinh Minh Tân
- Lê Cao Thăng