.. raw:: html
.. raw:: html
.. raw:: html
.. _sec_autograd:
Tính vi phân Tự động
====================
.. raw:: html
Như đã giải thích trong :numref:`sec_calculus`, vi phân là phép tính
thiết yếu trong hầu như tất cả mọi thuật toán học sâu. Mặc dù các phép
toán trong việc tính đạo hàm khá trực quan và chỉ yêu cầu một chút kiến
thức giải tích, nhưng với các mô hình phức tạp, việc tự tính rõ ràng
từng bước khá là mệt (và thường rất dễ sai).
.. raw:: html
Gói thư viện ``autograd`` giải quyết vấn đề này một cách nhanh chóng và
hiệu quả bằng cách tự động hoá các phép tính đạo hàm (*automatic
differentiation*). Trong khi nhiều thư viện yêu cầu ta phải biên dịch
một *đồ thị biểu tượng* (*symbolic graph*) để có thể tự động tính đạo
hàm, ``autograd`` cho phép ta tính đạo hàm ngay lập tức thông qua các
dòng lệnh thông thường. Mỗi khi đưa dữ liệu chạy qua mô hình,
``autograd`` xây dựng một đồ thị và theo dõi xem dữ liệu nào kết hợp với
các phép tính nào để tạo ra kết quả. Với đồ thị này ``autograd`` sau đó
có thể lan truyền ngược gradient lại theo ý muốn. *Lan truyền ngược* ở
đây chỉ đơn thuần là truy ngược lại *đồ thị tính toán* và điền vào đó
các giá trị đạo hàm riêng theo từng tham số.
.. code:: python
from mxnet import autograd, np, npx
npx.set_np()
.. raw:: html
.. raw:: html
.. raw:: html
Một ví dụ đơn giản
------------------
.. raw:: html
Lấy ví dụ đơn giản, giả sử chúng ta muốn tính vi phân của hàm số
:math:`y = 2\mathbf{x}^{\top}\mathbf{x}` theo vector cột
:math:`\mathbf{x}`. Để bắt đầu, ta sẽ tạo biến ``x`` và gán cho nó một
giá trị ban đầu.
.. code:: python
x = np.arange(4)
x
.. parsed-literal::
:class: output
array([0., 1., 2., 3.])
.. raw:: html
Lưu ý rằng trước khi có thể tính gradient của :math:`y` theo
:math:`\mathbf{x}`, chúng ta cần một nơi để lưu giữ nó. Điều quan trọng
là ta không được cấp phát thêm bộ nhớ mới mỗi khi tính đạo hàm theo một
biến xác định, vì ta thường cập nhật cùng một tham số hàng ngàn vạn lần
và sẽ nhanh chóng dùng hết bộ nhớ.
.. raw:: html
Cũng lưu ý rằng, bản thân giá trị gradient của hàm số đơn trị theo một
vector :math:`\mathbf{x}` cũng là một vector với cùng kích thước. Do vậy
trong mã nguồn sẽ trực quan hơn nếu chúng ta lưu giá trị gradient tính
theo ``x`` dưới dạng một thuộc tính của chính ``ndarray`` ``x``. Chúng
ta cấp bộ nhớ cho gradient của một ``ndarray`` bằng cách gọi phương thức
``attach_grad``.
.. code:: python
x.attach_grad()
.. raw:: html
.. raw:: html
.. raw:: html
Sau khi đã tính toán gradient theo biến ``x``, ta có thể truy cập nó
thông qua thuộc tính ``grad``. Để an toàn, ``x.grad`` được khởi tạo là
một mảng chứa các giá trị không. Điều này hợp lý vì trong học sâu, việc
lấy gradient thường là để cập nhật các tham số bằng cách cộng (hoặc trừ)
gradient của một hàm để cực đại (hoặc cực tiểu) hóa hàm đó. Bằng cách
khởi tạo gradient bằng mảng chứa giá trị không, ta đảm bảo rằng bất kỳ
cập nhật vô tình nào trước khi gradient được tính toán sẽ không làm thay
đổi giá trị các tham số.
.. code:: python
x.grad
.. parsed-literal::
:class: output
array([0., 0., 0., 0.])
.. raw:: html
Giờ hãy tính :math:`y`. Bởi vì mục đích sau cùng là tính gradient, ta
muốn MXNet tạo đồ thị tính toán một cách nhanh chóng. Ta có thể tưởng
tượng rằng MXNet sẽ bật một thiết bị ghi hình để thu lại chính xác đường
đi mà mỗi biến được tạo.
.. raw:: html
Chú ý rằng ta cần một số lượng phép tính không hề nhỏ để xây dựng đồ thị
tính toán. Vậy nên MXNet sẽ chỉ dựng đồ thị khi được ra lệnh rõ ràng. Ta
có thể thực hiện việc này bằng cách đặt đoạn mã trong phạm vi
``autograd.record``.
.. code:: python
with autograd.record():
y = 2 * np.dot(x, x)
y
.. parsed-literal::
:class: output
array(28.)
.. raw:: html
Bởi vì ``x`` là một ``ndarray`` có độ dài bằng 4, ``np.dot`` sẽ tính
toán tích vô hướng của ``x`` và ``x``, trả về một số vô hướng mà sẽ được
gán cho ``y``. Tiếp theo, ta có thể tính toán gradient của ``y`` theo
mỗi thành phần của ``x`` một cách tự động bằng cách gọi hàm ``backward``
của ``y``.
.. code:: python
y.backward()
.. raw:: html
Nếu kiểm tra lại giá trị của ``x.grad``, ta sẽ thấy nó đã được ghi đè
bằng gradient mới được tính toán.
.. code:: python
x.grad
.. parsed-literal::
:class: output
array([ 0., 4., 8., 12.])
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Gradient của hàm :math:`y = 2\mathbf{x}^{\top}\mathbf{x}` theo
:math:`\mathbf{x}` phải là :math:`4\mathbf{x}`. Hãy kiểm tra một cách
nhanh chóng rằng giá trị gradient mong muốn được tính toán đúng. Nếu hai
``ndarray`` là giống nhau, thì mọi cặp phần tử tương ứng cũng bằng nhau.
.. code:: python
x.grad == 4 * x
.. parsed-literal::
:class: output
array([ True, True, True, True])
.. raw:: html
Nếu ta tiếp tục tính gradient của một biến khác mà giá trị của nó là kết
quả của một hàm theo biến ``x``, thì nội dung trong ``x.grad`` sẽ bị ghi
đè.
.. code:: python
with autograd.record():
y = x.sum()
y.backward()
x.grad
.. parsed-literal::
:class: output
array([1., 1., 1., 1.])
.. raw:: html
Truyền ngược cho các biến không phải Số vô hướng
------------------------------------------------
.. raw:: html
Về mặt kỹ thuật, khi ``y`` không phải một số vô hướng, cách diễn giải tự
nhiên nhất cho vi phân của một vector ``y`` theo vector ``x`` đó là một
ma trận. Với các bậc và chiều cao hơn của ``y`` và ``x``, kết quả của
phép vi phân có thể là một tensor bậc cao.
.. raw:: html
Tuy nhiên, trong khi những đối tượng như trên xuất hiện trong học máy
nâng cao (bao gồm học sâu), thường thì khi ta gọi lan truyền ngược trên
một vector, ta đang cố tính toán đạo hàm của hàm mất mát theo mỗi
*batch* bao gồm một vài mẫu huấn luyện. Ở đây, ý định của ta không phải
là tính toán ma trận vi phân mà là tổng của các đạo hàm riêng được tính
toán một cách độc lập cho mỗi mẫu trong batch.
.. raw:: html
Vậy nên khi ta gọi ``backward`` lên một biến vector ``y`` – là một hàm
của ``x``, MXNet sẽ cho rằng ta muốn tính tổng của các gradient. Nói
ngắn gọn, MXNet sẽ tạo một biến mới có giá trị là số vô hướng bằng cách
cộng lại các phần tử trong ``y`` và tính gradient theo ``x`` của biến
mới này.
.. code:: python
with autograd.record():
y = x * x # y is a vector
y.backward()
u = x.copy()
u.attach_grad()
with autograd.record():
v = (u * u).sum() # v is a scalar
v.backward()
x.grad == u.grad
.. parsed-literal::
:class: output
array([ True, True, True, True])
.. raw:: html
.. raw:: html
.. raw:: html
Tách rời Tính toán
------------------
.. raw:: html
Đôi khi chúng ta muốn chuyển một số phép tính ra khỏi đồ thị tính toán.
Ví dụ, giả sử ``y`` đã được tính như một hàm của ``x``, rồi sau đó ``z``
được tính như một hàm của cả ``y`` và ``x``. Bây giờ, giả sử ta muốn
tính gradient của ``z`` theo ``x``, nhưng vì lý do nào đó ta lại muốn
xem ``y`` như là một hằng số và chỉ xét đến vai trò của ``x`` như là
biến số của ``z`` sau khi giá trị của ``y`` đã được tính.
.. raw:: html
Trong trường hợp này, ta có thể gọi ``u = y.detach()`` để trả về một
biến ``u`` mới có cùng giá trị như ``y`` nhưng không còn chứa các thông
tin về cách mà ``y`` đã được tính trong đồ thị tính toán. Nói cách khác,
gradient sẽ không thể chảy ngược qua ``u`` về ``x`` được. Bằng cách này,
ta đã tính ``u`` như một hàm của ``x`` ở ngoài phạm vi của
``autograd.record``, dẫn đến việc biến ``u`` sẽ được xem như là một hằng
số mỗi khi ta gọi ``backward``. Chính vì vậy, hàm ``backward`` sau đây
sẽ tính đạo hàm riêng của ``z = u * x`` theo ``x`` khi xem ``u`` như là
một hằng số, thay vì đạo hàm riêng của ``z = x * x * x`` theo ``x``.
.. code:: python
with autograd.record():
y = x * x
u = y.detach()
z = u * x
z.backward()
x.grad == u
.. parsed-literal::
:class: output
array([ True, True, True, True])
.. raw:: html
Bởi vì sự tính toán của ``y`` đã được ghi lại, chúng ta có thể gọi
``y.backward()`` sau đó để lấy đạo hàm của ``y = x * x`` theo ``x``, tức
là ``2 * x``.
.. code:: python
y.backward()
x.grad == 2 * x
.. parsed-literal::
:class: output
array([ True, True, True, True])
.. raw:: html
Lưu ý rằng khi ta gắn gradient vào một biến ``x``, ``x = x.detach()`` sẽ
được gọi ngầm. Nếu ``x`` được tính dựa trên các biến khác, phần tính
toán này sẽ không được sử dụng trong hàm ``backward``.
.. code:: python
y = np.ones(4) * 2
y.attach_grad()
with autograd.record():
u = x * y
u.attach_grad() # Implicitly run u = u.detach()
z = 5 * u - x
z.backward()
x.grad, u.grad, y.grad
.. parsed-literal::
:class: output
(array([-1., -1., -1., -1.]), array([5., 5., 5., 5.]), array([0., 0., 0., 0.]))
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Tính gradient của Luồng điều khiển Python
-----------------------------------------
.. raw:: html
Một lợi thế của việc sử dụng vi phân tự động là khi việc xây dựng đồ thị
tính toán đòi hỏi trải qua một loạt các câu lệnh điều khiển luồng
Python, (ví dụ như câu lệnh điều kiện, vòng lặp và các lệnh gọi hàm tùy
ý), ta vẫn có thể tính gradient của biến kết quả. Trong đoạn mã sau, hãy
lưu ý rằng số lần lặp của vòng lặp ``while`` và kết quả của câu lệnh
``if`` đều phụ thuộc vào giá trị của đầu vào ``a``.
.. code:: python
def f(a):
b = a * 2
while np.linalg.norm(b) < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
.. raw:: html
Một lần nữa, để tính gradient ta chỉ cần “ghi lại” các phép tính (bằng
cách gọi hàm ``record``) và sau đó gọi hàm ``backward``.
.. code:: python
a = np.random.normal()
a.attach_grad()
with autograd.record():
d = f(a)
d.backward()
.. raw:: html
Giờ ta có thể phân tích hàm ``f`` được định nghĩa ở phía trên. Hãy để ý
rằng hàm này tuyến tính từng khúc theo đầu vào ``a``. Nói cách khác, với
mọi giá trị của ``a`` tồn tại một hằng số ``k`` sao cho
``f(a) = k * a``, ở đó giá trị của ``k`` phụ thuộc vào đầu vào ``a``. Do
đó, ta có thể kiểm tra giá trị của gradient bằng cách tính ``d / a``.
.. code:: python
a.grad == d / a
.. parsed-literal::
:class: output
array(True)
.. raw:: html
.. raw:: html
.. raw:: html
Chế độ huấn luyện và Chế độ dự đoán
-----------------------------------
.. raw:: html
Như đã thấy, sau khi gọi ``autograd.record``, MXNet sẽ ghi lại những
tính toán xảy ra trong khối mã nguồn theo sau. Có một chi tiết tinh tế
nữa mà ta cần để ý. ``autograd.record`` sẽ thay đổi chế độ chạy từ *chế
độ dự đoán* sang *chế độ huấn luyện*. Ta có thể kiểm chứng hành vi này
bằng cách gọi hàm ``is_training``.
.. code:: python
print(autograd.is_training())
with autograd.record():
print(autograd.is_training())
.. parsed-literal::
:class: output
False
True
.. raw:: html
Khi ta tìm hiểu tới các mô hình học sâu phức tạp, ta sẽ gặp một vài
thuật toán mà mô hình hoạt động khác nhau khi huấn luyện và khi được sử
dụng sau đó để dự đoán. Những khác biệt này sẽ được đề cập chi tiết
trong các chương sau.
.. raw:: html
Tóm tắt
-------
.. raw:: html
- MXNet cung cấp gói ``autograd`` để tự động hóa việc tính toán đạo
hàm. Để sử dụng nó, đầu tiên ta gắn gradient cho các biến mà ta muốn
lấy đạo hàm riêng theo nó. Sau đó ta ghi lại tính toán của giá trị
mục tiêu, thực thi hàm ``backward`` của nó và truy cập kết quả
gradient thông qua thuộc tính ``grad`` của các biến.
- Ta có thể tách rời gradient để kiểm soát những phần tính toán được sử
dụng trong hàm ``backward``.
- Các chế độ chạy của MXNet bao gồm chế độ huấn luyện và chế độ dự
đoán. Ta có thể kiểm tra chế độ đang chạy bằng cách gọi hàm
``is_training``.
.. raw:: html
Bài tập
-------
.. raw:: html
1. Tại sao đạo hàm bậc hai lại mất thêm rất nhiều tài nguyên để tính
toán hơn đạo hàm bậc một?
2. Sau khi chạy ``y.backward()``, lập tức chạy lại lần nữa và xem chuyện
gì sẽ xảy ra.
3. Trong ví dụ về luồng điều khiển khi ta tính toán đạo hàm của ``d``
theo ``a``, điều gì sẽ xảy ra nếu ta thay đổi biến ``a`` thành một
vector hay ma trận ngẫu nhiên. Lúc này, kết quả của tính toán
``f(a)`` sẽ không còn là số vô hướng nữa. Điều gì sẽ xảy ra với kết
quả? Ta có thể phân tích nó như thế nào?
4. Hãy tái thiết kế một ví dụ về việc tìm gradient của luồng điều khiển.
Chạy ví dụ và phân tích kết quả.
5. Cho :math:`f(x) = \sin(x)`. Vẽ đồ thị của :math:`f(x)` và
:math:`\frac{df(x)}{dx}` với điều kiện không được tính trực tiếp đạo
hàm :math:`f'(x) = \cos(x)`.
6. Trong một cuộc đấu giá kín theo giá thứ hai (ví dụ như trong eBay hay
trong quảng cáo điện toán), người thắng cuộc đấu giá chỉ trả mức giá
cao thứ hai. Hãy tính gradient của mức giá cuối cùng theo mức đặt của
người thắng cuộc bằng cách sử dụng ``autograd``. Kết quả cho bạn biết
điều gì về cơ chế đấu giá này? Nếu bạn tò mò muốn tìm hiểu thêm về
các cuộc đấu giá kín theo giá thứ hai, hãy đọc bài báo nghiên cứu của
Edelman et al. :cite:`Edelman.Ostrovsky.Schwarz.2007`.
.. 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
- Lê Khắc Hồng Phúc
- Nguyễn Cảnh Thướng
- Phạm Hồng Vinh
- Vũ Hữu Tiệp
- Tạ H. Duy Nguyên
- Phạm Minh Đức