.. raw:: html
.. raw:: html
.. raw:: html
.. _sec_linear_gluon:
Cách lập trình súc tích Hồi quy Tuyến tính
==========================================
.. raw:: html
Sự quan tâm nhiệt thành và rộng khắp với học sâu trong những năm gần đây
đã tạo cảm hứng cho các công ty, học viện và những người đam mê tới học
sâu phát triển nhiều framework mã nguồn mở hoàn thiện, giúp tự động hóa
các công việc lặp đi lặp lại trong quá trình triển khai các thuật toán
học dựa trên gradient. Trong chương trước, chúng ta chỉ dựa vào (i)
``ndarray`` để lưu dữ liệu và thực hiện tính toán đại số tuyến tính; và
(ii) ``autograd`` để thực hiện tính đạo hàm. Trên thực tế, do các
iterator dữ liệu, các hàm mất mát, các bộ tối ưu và các tầng của mạng
nơ-ron (thậm chí là toàn bộ kiến trúc) rất phổ biển, các thư viện hiện
đại đã cài đặt sẵn những thành phần này cho chúng ta.
.. raw:: html
Mục này sẽ hướng dẫn bạn cách để xây dựng mô hình hồi quy tuyến tính
trong phần :numref:`sec_linear_scratch` một cách súc tích với Gluon.
.. raw:: html
Tạo Tập dữ liệu
---------------
.. raw:: html
Chúng ta bắt đầu bằng việc tạo một tập dữ liệu như ở mục trước.
.. code:: python
from d2l import mxnet as d2l
from mxnet import autograd, gluon, np, npx
npx.set_np()
true_w = np.array([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
.. raw:: html
.. raw:: html
.. raw:: html
Đọc tập dữ liệu
---------------
.. raw:: html
Thay vì tự viết iterator riêng để đọc dữ liệu thì ta có thể gọi mô-đun
``data`` của Gluon để xử lý việc này. Bước đầu tiên sẽ là khởi tạo một
``ArrayDataset``. Hàm tạo của đối tượng này sẽ lấy một hoặc nhiều
``ndarray`` làm đối số. Tại đây, ta truyền vào hàm hai đối số là
``features`` và ``labels``. Kế tiếp, ta sử dụng ``ArrayDataset`` để khởi
tạo một\ ``DataLoader``, lớp này yêu cầu ta truyền vào một giá trị
``batch_size`` và giá trị Boolean ``shuffle`` để cho biết chúng ta có
muốn ``DataLoader`` xáo trộn dữ liệu trên mỗi epoch (một lần duyệt qua
toàn bộ tập dữ liệu) hay không.
.. code:: python
# Saved in the d2l package for later use
def load_array(data_arrays, batch_size, is_train=True):
"""Construct a Gluon data loader"""
dataset = gluon.data.ArrayDataset(*data_arrays)
return gluon.data.DataLoader(dataset, batch_size, shuffle=is_train)
batch_size = 10
data_iter = load_array((features, labels), batch_size)
.. raw:: html
Bây giờ, ta có thể sử dụng ``data_iter`` theo cách tương tự như cách ta
gọi hàm ``data_iter`` trong phần trước. Để biết rằng nó có hoạt động
được hay không, ta có thể thử đọc và in ra minibatch đầu tiên.
.. code:: python
for X, y in data_iter:
print(X, '\n', y)
break
.. parsed-literal::
:class: output
[[-0.38151944 -1.3118169 ]
[ 0.02099029 -0.73851013]
[-0.31846237 -0.9492751 ]
[-0.17870884 -0.20225924]
[ 0.0901759 0.27758422]
[-2.2036052 1.2875317 ]
[-0.47063652 -0.59975994]
[ 0.24499686 -1.6080191 ]
[ 0.43582228 0.72849447]
[-0.1742568 1.9691626 ]]
[[ 7.871762 ]
[ 6.737613 ]
[ 6.796183 ]
[ 4.4967184]
[ 3.427059 ]
[-4.5830226]
[ 5.289975 ]
[10.171097 ]
[ 2.5919693]
[-2.8460252]]
.. raw:: html
.. raw:: html
.. raw:: html
Định nghĩa Mô hình
------------------
.. raw:: html
Khi ta lập trình hồi quy tuyến tính từ đầu (trong
:numref:`sec_linear_scratch`), ta đã định nghĩa rõ ràng các tham số
của mô hình và lập trình các tính toán cho giá trị đầu ra sử dụng các
phép toán đại số tuyến tính cơ bản. Bạn *nên* biết cách để làm được điều
này. Nhưng một khi mô hình trở nên phức tạp hơn và đồng thời khi bạn
phải làm điều này gần như hàng ngày, bạn sẽ thấy vui mừng khi có sự hỗ
trợ từ các thư viện. Tình huống này tương tự như việc lập trình blog của
riêng bạn lại từ đầu. Làm điều này một hoặc hai lần thì sẽ bổ ích và
mang tính hướng dẫn, nhưng bạn sẽ trở thành một nhà phát triển web “khó
ở” nếu mỗi khi cần một trang blog bạn lại phải dành ra cả một tháng chỉ
để phát triển lại từ đầu.
.. raw:: html
.. raw:: html
.. raw:: html
Đối với những tác vụ tiêu chuẩn, chúng ta có thể sử dụng các tầng đã
được định nghĩa trước trong Gluon, điều này cho phép chúng ta tập trung
vào những tầng được dùng để xây dựng mô hình hơn là việc phải tập trung
vào cách lập trình các tầng đó. Để định nghĩa một mô hình tuyến tính,
đầu tiên chúng ta cần nhập vào mô-đun ``nn``, giúp ta định nghĩa một
lượng lớn các tầng trong mạng nơ-ron (lưu ý rằng “nn” là chữ viết tắt
của “neural network”). Đầu tiên ta sẽ định nghĩa một biến mô hình là
``net``, tham chiếu đến một thực thể của lớp ``Sequential``. Trong
Gluon, ``Sequential`` định nghĩa một lớp chứa nhiều tầng được liên kết
với nhau. Khi nhận được dữ liệu đầu vào, ``Sequential`` sẽ truyền dữ
liệu vào tầng đầu tiên, kết quả đầu ra từ đó trở thành đầu vào của tầng
thứ hai và cứ tiếp tục như thế ở các tầng kế tiếp. Trong ví dụ tiếp
theo, mô hình chúng ta chỉ có duy nhất một tầng, vì vậy không nhất thiết
phải sử dụng ``Sequential``. Tuy nhiên vì hầu hết các mô hình chúng ta
gặp phải trong tương lai đều có nhiều tầng, do đó dù sao cũng nên dùng
để làm quen với quy trình làm việc tiêu chuẩn nhất.
.. code:: python
from mxnet.gluon import nn
net = nn.Sequential()
.. raw:: html
Hãy cùng nhớ lại kiến trúc của mạng đơn tầng như đã trình bày tại
:numref:`fig_singleneuron`. Tầng đó được gọi là *kết nối đầy đủ* bởi
vì mỗi đầu vào được kết nối lần lượt với từng đầu ra bằng một phép nhân
ma trận với vector. Trong Gluon, tầng kết nối đầy đủ được định nghĩa
trong lớp ``Dense``. Bởi vì chúng ta chỉ mong xuất ra một số vô hướng
duy nhất, nên ta gán giá trị là :math:`1`.
.. _fig_singleneuron:
.. figure:: ../img/singleneuron.svg
Hồi quy tuyến tính là một mạng nơ-ron đơn tầng.
.. code:: python
net.add(nn.Dense(1))
.. raw:: html
Để thuận tiện, điều đáng chú ý là Gluon không yêu cầu chúng ta chỉ định
kích thước đầu vào mỗi tầng. Nên tại đây, chúng ta không cần thiết cho
Gluon biết có bao nhiêu đầu vào cho mỗi tầng tuyến tính. Khi chúng ta cố
gắng truyền dữ liệu qua mô hình lần đầu tiên, ví dụ: khi chúng ta thực
hiện ``net(X)`` sau đó, Gluon sẽ tự động suy ra số lượng đầu vào cho mỗi
tầng. Chúng ta sẽ mô tả cách hoạt động của cơ chế này một cách chi tiết
hơn trong chương “Tính toán trong Học sâu”.
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Khởi tạo Tham số Mô hình
------------------------
.. raw:: html
Trước khi sử dụng ``net``, chúng ta cần phải khởi tạo tham số cho mô
hình, chẳng hạn như trọng số và hệ số điều chỉnh trong mô hình hồi quy
tuyến tính. Chúng ta sẽ nhập mô-đun ``initializer`` từ MXNet. Mô-đun này
cung cấp nhiều phương thức khác nhau để khởi tạo tham số cho mô hình.
Gluon cho phép dùng ``init`` như một cách ngắn gọn (viết tắt) để truy
cập đến gói ``initializer``. Bằng cách gọi ``init.Normal(sigma=0.01)``,
chúng ta sẽ khởi tạo ngẫu nhiên các *trọng số* từ một phân phối chuẩn
với trung bình bằng :math:`0` và độ lệch chuẩn bằng :math:`0.01`. Mặc
định, tham số *hệ số điều chỉnh* sẽ được khởi tạo bằng không. Cả hai
vector trọng số và hệ số điều chỉnh sẽ có gradient kèm theo.
.. code:: python
from mxnet import init
net.initialize(init.Normal(sigma=0.01))
.. raw:: html
Đoạn mã nguồn trên trông khá đơn giản nhưng bạn đọc hãy chú ý một vài
điểm khác thường ở đây. Chúng ta khởi tạo các tham số cho một mạng mà
thậm chí Gluon chưa hề biết số chiều của đầu vào là bao nhiêu! Nó có thể
là :math:`2` trong trường hợp của chúng ta nhưng cũng có thể là
:math:`2000`. Gluon khiến chúng ta không cần bận tâm về điều này bởi ở
hậu trường, quá trình khởi tạo thực sự vẫn đang bị *trì hoãn*. Quá trình
khởi tạo thực sự chỉ bắt đầu khi chúng ta truyền dữ liệu vào mạng lần
đầu tiên. Hãy ghi nhớ rằng, do các tham số chưa thực sự được khởi tạo,
chúng ta không thể truy cập hoặc thao tác với chúng.
.. raw:: html
.. raw:: html
.. raw:: html
Định nghĩa Hàm mất mát
----------------------
.. raw:: html
Trong Gluon, mô-đun ``loss`` định nghĩa các hàm mất mát khác nhau. Chúng
ta sẽ sử dụng mô-đun ``loss`` được thêm vào dưới tên gọi là ``gloss``,
để tránh nhầm lẫn nó với biến đang giữ hàm mất mát mà ta đã chọn. Trong
ví dụ này, chúng ta sẽ sử dụng triển khai trong Gluon của mất mát bình
phương (``L2Loss``).
.. code:: python
from mxnet.gluon import loss as gloss
loss = gloss.L2Loss() # The squared loss is also known as the L2 norm loss
.. raw:: html
.. raw:: html
.. raw:: html
Định nghĩa Thuật toán Tối ưu
----------------------------
.. raw:: html
Minibatch SGD và các biến thể liên quan đều là các công cụ chuẩn cho
việc tối ưu hóa mạng nơ-ron, vì vậy Gluon có hỗ trợ SGD cùng với một số
biến thể của thuật toán này thông qua lớp ``Trainer``. Khi khởi tạo lớp
``Trainer``, ta cần chỉ định các tham số để tối ưu hóa (có thể lấy từ
mạng thông qua ``net.collect_params()``), thuật toán tối ưu muốn sử dụng
(``sgd``) và một từ điển gồm các siêu tham số cần thiết cho thuật toán
tối ưu. SGD chỉ yêu cầu giá trị của ``learning_rate``, (ở đây chúng ta
đặt nó bằng 0.03).
.. code:: python
from mxnet import gluon
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03})
.. raw:: html
.. raw:: html
.. raw:: html
Huấn luyện
----------
.. raw:: html
Bạn có thể thấy rằng việc biểu diễn mô hình thông qua Gluon đòi hỏi
tương đối ít dòng lệnh. Chúng ta không cần phải khởi tạo từng tham số
riêng lẻ, định nghĩa hàm mất mát hay lập trình thuật toán hạ gradient
ngẫu nhiên. Lợi ích mà Gluon mang lại sẽ rất lớn khi chúng ta bắt đầu
làm việc với những mô hình phức tạp hơn. Tuy nhiên, một khi ta có các
mảnh ghép cơ bản, vòng lặp huấn luyện lại rất giống với những gì ta đã
làm khi lập trình mọi thứ từ đầu.
.. raw:: html
Nhắc lại rằng: với số lượng epoch nhất định, trong mỗi epoch chúng ta sẽ
duyệt qua toàn bộ tập dữ liệu (``train_data``), lần lượt lấy từng
minibatch chứa dữ liệu đầu vào và các nhãn gốc tương ứng. Đối với mỗi
minibatch, chúng ta cần tuân thủ theo trình tự sau:
.. raw:: html
- Đưa ra dự đoán bằng cách gọi ``net(X)`` và tính giá trị mất mát ``l``
(lượt truyền xuôi).
- Tính gradient bằng cách gọi ``l.backward()`` (lượt truyền ngược).
- Cập nhật các tham số của mô hình bằng cách gọi bộ tối ưu SGD (chú ý
rằng ``trainer`` đã biết các tham số cần tối ưu, nên ta chỉ cần
truyền thêm kích thước của minibatch).
.. raw:: html
Ngoài ra, ta tính giá trị mất mát sau mỗi epoch và in nó ra màn hình để
giám sát tiến trình.
.. code:: python
num_epochs = 3
for epoch in range(1, num_epochs + 1):
for X, y in data_iter:
with autograd.record():
l = loss(net(X), y)
l.backward()
trainer.step(batch_size)
l = loss(net(features), labels)
print('epoch %d, loss: %f' % (epoch, l.mean().asnumpy()))
.. parsed-literal::
:class: output
epoch 1, loss: 0.024762
epoch 2, loss: 0.000089
epoch 3, loss: 0.000051
.. raw:: html
.. raw:: html
.. raw:: html
Dưới đây, ta so sánh các tham số của mô hình đã được học thông qua việc
huấn luyện trên tập dữ liệu hữu hạn với các tham số được dùng để tạo ra
tập dữ liệu. Để truy cập các tham số trong Gluon, trước hết ta truy cập
tầng ta quan tâm thông qua biến ``net``, sau đó truy cập trọng số
(``weight``) và hệ số điều chỉnh (``bias``) của tầng đó. Để truy cập giá
trị tham số dưới dạng một mảng ``ndarray``, ta sử dụng phương thức
``data``. Giống với phiên bản lập trình từ đầu của chúng ta, các tham số
ước lượng có giá trị gần với giá trị chính xác của chúng.
.. code:: python
w = net[0].weight.data()
print('Error in estimating w', true_w.reshape(w.shape) - w)
b = net[0].bias.data()
print('Error in estimating b', true_b - b)
.. parsed-literal::
:class: output
Error in estimating w [[ 0.00061905 -0.00038671]]
Error in estimating b [0.00049639]
.. raw:: html
.. raw:: html
.. raw:: html
Tóm tắt
-------
.. raw:: html
- Sử dụng Gluon giúp việc lập trình các mô hình trở nên ngắn gọn hơn
rất nhiều.
- Trong Gluon, mô-đun ``data`` cung cấp các công cụ để xử lý dữ liệu,
mô-đun ``nn`` định nghĩa một lượng lớn các tầng cho mạng nơ-ron, và
mô-đun ``loss`` cho phép ta thiết lập nhiều hàm mất mát phổ biến.
- Mô-đun ``initializer`` của MXNet cung cấp nhiều phương thức khác nhau
để khởi tạo tham số cho mô hình.
- Kích thước và dung lượng lưu trữ của các tham số sẽ được suy ra một
cách tự động (nhưng nên cẩn thận tránh truy cập các tham số trước khi
chúng được khởi tạo).
.. raw:: html
Bài tập
-------
.. raw:: html
1. Nếu thay thế ``l = loss(output, y)`` bằng
``l = loss(output, y).mean()``, chúng ta cần đổi
``trainer.step(batch_size)`` thành ``trainer.step(1)`` để phần mã
nguồn này hoạt động giống như trước. Tại sao lại thế?
2. Xem lại tài liệu về MXNet để biết các hàm mất mát và các phương thức
khởi tạo được cung cấp trong hai mô-đun ``gluon.loss`` và ``init``.
Hãy thay thế hàm mất mát đang sử dụng bằng hàm mất mát Huber (*Huber
loss*).
3. Làm thế nào để truy cập gradient của ``dense.weight``?
.. raw:: html
.. raw:: html
.. raw:: html
Thảo luận
---------
- `Tiếng Anh `__
- `Tiếng Việt `__
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
- Trần Thị Hồng Hạnh
- Phạm Hồng Vinh
- Vũ Hữu Tiệp
- Lý Phi Long
- Phạm Đăng Khoa
- Lê Khắc Hồng Phúc
- Dương Nhật Tân
- Nguyễn Văn Tâm
- Bùi Nhật Quân
- Nguyễn Mai Hoàng Long