.. raw:: html
.. raw:: html
.. raw:: html
.. _sec_numerical_stability:
Ổn định Số học và Khởi tạo
==========================
.. raw:: html
Cho đến nay, đối với mọi mô hình mà ta đã lập trình, ta đều phải khởi
tạo các tham số theo một phân phối cụ thể nào đó. Tuy nhiên, ta mới chỉ
lướt qua các chi tiết thực hiện mà không để tâm lắm tới việc tại sao lại
khởi tạo tham số như vậy. Bạn thậm chí có thể nghĩ rằng các lựa chọn này
không đặc biệt quan trọng. Tuy nhiên, việc lựa chọn cơ chế khởi tạo đóng
vai trò rất lớn trong quá trình học của mạng nơ-ron và có thể là yếu tố
quyết định để duy trì sự ổn định số học. Hơn nữa, các phương pháp khởi
tạo cũng có thể bị ràng buộc bởi các hàm kích hoạt phi tuyến theo những
cách thú vị. Việc lựa chọn hàm kích hoạt và cách khởi tạo tham số có thể
ảnh hưởng tới tốc độ hội tụ của thuật toán tối ưu. Nếu ta lựa chọn không
hợp lý, việc bùng nổ hoặc tiêu biến gradient có thể sẽ xảy ra. Trong
phần này, ta sẽ đi sâu hơn vào các chi tiết của chủ đề trên và thảo luận
một số phương pháp thực nghiệm hữu ích mà bạn có thể sẽ sử dụng thường
xuyên trong suốt sự nghiệp học sâu.
.. raw:: html
Tiêu biến và Bùng nổ Gradient
-----------------------------
.. raw:: html
Xét một mạng nơ-ron sâu với :math:`L` tầng, đầu vào :math:`\mathbf{x}`
và đầu ra :math:`\mathbf{o}`. Mỗi tầng :math:`l` được định nghĩa bởi một
phép biến đổi :math:`f_l` với tham số là trọng số :math:`\mathbf{W}_l`.
Mạng nơ-ron này có thể được biểu diễn như sau:
.. math:: \mathbf{h}^{l+1} = f_l (\mathbf{h}^l) \text{ và vì vậy } \mathbf{o} = f_L \circ \ldots, \circ f_1(\mathbf{x}).
.. raw:: html
Nếu tất cả giá trị kích hoạt và đầu vào là vector, ta có thể viết lại
gradient của :math:`\mathbf{o}` theo một tập tham số
:math:`\mathbf{W}_l` bất kỳ như sau:
.. math:: \partial_{\mathbf{W}_l} \mathbf{o} = \underbrace{\partial_{\mathbf{h}^{L-1}} \mathbf{h}^L}_{:= \mathbf{M}_L} \cdot \ldots \cdot \underbrace{\partial_{\mathbf{h}^{l}} \mathbf{h}^{l+1}}_{:= \mathbf{M}_l} \underbrace{\partial_{\mathbf{W}_l} \mathbf{h}^l}_{:= \mathbf{v}_l}.
.. raw:: html
Nói cách khác, gradient này là tích của :math:`L-l` ma trận
:math:`\mathbf{M}_L \cdot \ldots, \cdot \mathbf{M}_l` với vector
gradient :math:`\mathbf{v}_l`. Vì vậy ta sẽ dễ gặp phải vấn đề tràn số
dưới, một hiện tượng thường xảy ra khi nhân quá nhiều giá trị xác suất
lại với nhau. Khi làm việc với các xác suất, một mánh phổ biến là chuyển
về làm việc với giá trị log của nó. Nếu nhìn từ góc độ biểu diễn số học,
điều này đồng nghĩa với việc chuyển trọng tâm biểu diễn của các bit từ
phần định trị (*mantissa*) sang phần mũ (*exponent*). Thật không may,
bài toán trên lại nghiêm trọng hơn nhiều: các ma trận :math:`M_l` ban
đầu có thể có nhiều trị riêng với độ lớn rất khác nhau. Các trị riêng có
thể nhỏ hoặc lớn và do đó tích của chúng có thể *rất lớn* hoặc *rất
nhỏ*. Rủi ro của việc gradient bất ổn không chỉ dừng lại ở vấn đề biểu
diễn số học. Nếu ta không kiểm soát được độ lớn của gradient, sự ổn định
của các thuật toán tối ưu cũng không được đảm bảo. Lúc đó ta sẽ quan sát
được các bước cập nhật hoặc (i) quá lớn và phá hỏng mô hình (vấn đề
*bùng nổ* gradient); hoặc (ii) quá nhỏ (vấn đề *tiêu biến* gradient),
khiến việc học trở nên bất khả thi, khi mà các tham số hầu như không
thay đổi ở mỗi bước cập nhật.
.. raw:: html
.. raw:: html
.. raw:: html
Tiêu biến Gradient
~~~~~~~~~~~~~~~~~~
.. raw:: html
Thông thường, thủ phạm gây ra vấn đề tiêu biến gradient này là hàm kích
hoạt :math:`\sigma` được chọn để đặt nối tiếp phép toán tuyến tính tại
mỗi tầng. Trước đây, hàm kích hoạt sigmoid :math:`(1 + \exp(-x))` (đã
giới thiệu trong :numref:`sec_mlp`) là lựa chọn phổ biến bởi nó hoạt
động giống với một hàm lấy ngưỡng. Bởi các mạng nơ-ron nhân tạo thời kỳ
đầu lấy cảm hứng từ mạng nơ-ron sinh học, ý tưởng rằng các nơ-ron được
kích hoạt *hoàn toàn* hoặc *không hề* kích hoạt (giống như nơ-ron sinh
học) có vẻ rất hấp dẫn. Hãy cùng xem xét hàm sigmoid kỹ lưỡng hơn để
thấy tại sao nó có thể gây ra vấn đề tiêu biến gradient.
.. code:: python
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import autograd, np, npx
npx.set_np()
x = np.arange(-8.0, 8.0, 0.1)
x.attach_grad()
with autograd.record():
y = npx.sigmoid(x)
y.backward()
d2l.plot(x, [y, x.grad], legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
.. figure:: output_numerical-stability-and-init_vn_b0fd33_1_0.svg
.. raw:: html
Như ta có thể thấy, gradient của hàm sigmoid tiêu biến khi đầu vào của
nó quá lớn hoặc quá nhỏ. Hơn nữa, khi thực hiện lan truyền ngược qua
nhiều tầng, trừ khi giá trị nằm trong vùng Goldilocks, tại đó đầu vào
của hầu hết các hàm sigmoid có giá trị xấp xỉ không, gradient của cả
phép nhân có thể bị tiêu biến. Khi mạng nơ-ron có nhiều tầng, trừ khi ta
cẩn trọng, nhiều khả năng luồng gradient sẽ bị ngắt tại *một* tầng nào
đó. Vấn đề này đã từng gây nhiều khó khăn cho quá trình huấn luyện mạng
nơ-ron sâu. Do đó, ReLU, một hàm số ổn định hơn (nhưng lại không hợp lý
lắm từ khía cạnh khoa học thần kinh) đã và đang dần trở thành lựa chọn
mặc định của những người làm học sâu.
.. raw:: html
.. raw:: html
.. raw:: html
Bùng nổ Gradient
~~~~~~~~~~~~~~~~
.. raw:: html
Một vấn đề đối lập là bùng nổ gradient cũng có thể gây phiền toái không
kém. Để giải thích việc này rõ hơn, chúng ta lấy :math:`100` ma trận
ngẫu nhiên Gauss và nhân chúng với một ma trận ban đầu nào đó. Với
khoảng giá trị mà ta đã chọn (phương sai :math:`\sigma^2=1`), tích các
ma trận bị bùng nổ số học. Khi khởi tạo các mạng nơ-ron sâu một cách
không hợp lý, các bộ tối ưu dựa trên hạ gradient sẽ không thể hội tụ
được.
.. code:: python
M = np.random.normal(size=(4, 4))
print('A single matrix', M)
for i in range(100):
M = np.dot(M, np.random.normal(size=(4, 4)))
print('After multiplying 100 matrices', M)
.. parsed-literal::
:class: output
A single matrix [[ 2.2122064 1.1630787 0.7740038 0.4838046 ]
[ 1.0434405 0.29956347 1.1839255 0.15302546]
[ 1.8917114 -1.1688148 -1.2347414 1.5580711 ]
[-1.771029 -0.5459446 -0.45138445 -2.3556297 ]]
After multiplying 100 matrices [[ 3.4459714e+23 -7.8040680e+23 5.9973287e+23 4.5229990e+23]
[ 2.5275089e+23 -5.7240326e+23 4.3988473e+23 3.3174740e+23]
[ 1.3731286e+24 -3.1097155e+24 2.3897773e+24 1.8022959e+24]
[-4.4951040e+23 1.0180033e+24 -7.8232281e+23 -5.9000354e+23]]
.. raw:: html
.. raw:: html
.. raw:: html
Tính Đối xứng
~~~~~~~~~~~~~
.. raw:: html
Một vấn đề khác trong việc thiết kế mạng nơ-ron sâu là tính đối xứng
hiện hữu trong quá trình tham số hóa. Giả sử ta có một mạng nơ-ron sâu
với một tầng ẩn gồm hai nút :math:`h_1` và :math:`h_2`. Trong trường hợp
này, ta có thể hoán vị trọng số :math:`\mathbf{W}_1` của tầng đầu tiên,
rồi làm điều tương tự với các trọng số của tầng đầu ra để thu được một
hàm giống hệt ban đầu. Ta có thể thấy rằng không có sự khác biệt nào
giữa nút ẩn đầu tiên với nút ẩn thứ hai. Nói cách khác, ta có tính đối
xứng hoán vị giữa các nút ẩn của từng tầng.
.. raw:: html
Đây không chỉ là phiền toái về mặt lý thuyết. Thử hình dung xem điều gì
sẽ xảy ra nếu ta khởi tạo giá trị của mọi tham số ở các tầng như sau:
:math:`\mathbf{W}_l = c` với hằng số :math:`c` nào đó. Trong trường hợp
này thì các gradient cho tất cả các chiều là giống hệt nhau, nên mỗi nút
không chỉ có cùng giá trị mà chúng còn có bước cập nhật giống nhau. Bản
thân phương pháp hạ gradient ngẫu nhiên không thể phá vỡ tính đối xứng
này và ta sẽ không hiện thực hóa được sức mạnh biểu diễn của mạng. Tầng
ẩn sẽ hoạt động như thể nó chỉ có một nút duy nhất. Nhưng hãy lưu ý rằng
dù hạ gradient ngẫu nhiên không thể phá vỡ được tính đối xứng, kỹ thuật
điều chuẩn dropout lại hoàn toàn có thể!
.. raw:: html
Khởi tạo Tham số
----------------
.. raw:: html
Một cách giải quyết, hay ít nhất giảm thiểu các vấn đề được nêu ở trên
là khởi tạo tham số một cách cẩn thận. Chỉ cần cẩn trọng một chút trong
quá trình tối ưu hóa và điều chuẩn mô hình phù hợp, ta có thể cải thiện
tính ổn định của quá trình học.
.. raw:: html
Khởi tạo Mặc định
~~~~~~~~~~~~~~~~~
.. raw:: html
Trong các phần trước, ví dụ như trong :numref:`sec_linear_gluon`, ta
đã sử dụng ``net.initialize(init.Normal(sigma=0.01))`` để khởi tạo các
giá trị cho trọng số. Nếu ta không chỉ định sẵn một phương thức khởi tạo
như ``net.initialize()``, MXNet sẽ sử dụng phương thức khởi tạo ngẫu
nhiên mặc định: các trọng số được lấy mẫu ngẫu nhiên từ phân phối đều
:math:`U[-0.07, 0.07]`, còn các hệ số điều chỉnh đều được đưa về giá trị
:math:`0`. Cả hai lựa chọn đều hoạt động tốt với các bài toán cỡ trung
trong thực tiễn.
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Khởi tạo Xavier
~~~~~~~~~~~~~~~
.. raw:: html
Hãy cùng nhìn vào phân phối khoảng giá trị kích hoạt của các nút ẩn
:math:`h_{i}` ở một tầng nào đó:
.. math:: h_{i} = \sum_{j=1}^{n_\mathrm{in}} W_{ij} x_j.
.. raw:: html
Các trọng số :math:`W_{ij}` đều được lấy mẫu độc lập từ cùng một phân
phối. Hơn nữa, ta giả sử rằng phân phối này có trung bình bằng không và
phương sai :math:`\sigma^2` (đây không bắt buộc phải là phân phối Gauss,
chỉ là ta cần phải cho trước trung bình và phương sai). Tạm thời hãy giả
sử rằng đầu vào của tầng :math:`x_j` cũng có trung bình bằng không và
phương sai :math:`\gamma^2`, độc lập với :math:`\mathbf{W}`. Trong
trường hợp này, ta có thể tính được trung bình và phương sai của
:math:`h_i` như sau:
.. math::
\begin{aligned}
E[h_i] & = \sum_{j=1}^{n_\mathrm{in}} E[W_{ij} x_j] = 0, \\
E[h_i^2] & = \sum_{j=1}^{n_\mathrm{in}} E[W^2_{ij} x^2_j] \\
& = \sum_{j=1}^{n_\mathrm{in}} E[W^2_{ij}] E[x^2_j] \\
& = n_\mathrm{in} \sigma^2 \gamma^2.
\end{aligned}
.. raw:: html
Một cách để giữ phương sai cố định là đặt
:math:`n_\mathrm{in} \sigma^2 = 1`. Bây giờ hãy xem xét lan truyền
ngược. Ở đó ta phải đối mặt với vấn đề tương tự, mặc dù gradient được
truyền từ các tầng trên cùng. Tức thay vì :math:`\mathbf{W} \mathbf{w}`,
ta cần đối phó với :math:`\mathbf{W}^\top \mathbf{g}`, trong đó
:math:`\mathbf{g}` là gradient đến từ lớp phía trên. Sử dụng lý luận
tương tự với lan truyền xuôi, ta có thể thấy phương sai của các gradient
sẽ bùng nổ trừ khi :math:`n_\mathrm{out} \sigma^2 = 1`. Điều này khiến
ta rơi vào một tình huống khó xử: ta không thể thỏa mãn cả hai điều kiện
cùng một lúc. Thay vào đó, ta cố thỏa mãn điều kiện sau:
.. math::
\begin{aligned}
\frac{1}{2} (n_\mathrm{in} + n_\mathrm{out}) \sigma^2 = 1 \text{ hoặc tương đương }
\sigma = \sqrt{\frac{2}{n_\mathrm{in} + n_\mathrm{out}}}.
\end{aligned}
.. raw:: html
Đây là lý luận đằng sau phương thức khởi tạo *Xavier*, được đặt tên theo
người đã tạo ra nó :cite:`Glorot.Bengio.2010`. Bây giờ nó đã trở thành
phương thức tiêu chuẩn và rất hữu dụng trong thực tiễn. Thông thường,
phương thức này lấy mẫu cho trọng số từ phân phối Gauss với trung bình
bằng không và phương sai
:math:`\sigma^2 = 2/(n_\mathrm{in} + n_\mathrm{out})`. Ta cũng có thể
tận dụng cách hiểu trực quan của Xavier để chọn phương sai khi lấy mẫu
từ một phân phối đều. Chú ý rằng phân phối :math:`U[-a, a]` có phương
sai là :math:`a^2/3`. Thay :math:`\sigma^2` bằng :math:`a^2/3` vào điều
kiện trên, ta biết được rằng ta nên khởi tạo theo phân phối đều
:math:`U\left[-\sqrt{6/(n_\mathrm{in} + n_\mathrm{out})}, \sqrt{6/(n_\mathrm{in} + n_\mathrm{out})}\right]`.
.. raw:: html
.. raw:: html
.. raw:: html
Sâu xa hơn nữa
~~~~~~~~~~~~~~
.. raw:: html
Các lập luận đưa ra ở trên mới chỉ chạm tới bề mặt của những kỹ thuật
khởi tạo tham số hiện đại. Trên thực tế, MXNet có nguyên một mô-đun
```mxnet.initializer`` `__
với hàng chục các phương pháp khởi tạo dựa theo thực nghiệm khác nhau đã
được lập trình sẵn. Hơn nữa, các phương pháp khởi tạo vẫn đang là một
chủ đề nghiên cứu căn bản rất được quan tâm trong học sâu. Trong số đó
là những phương pháp dựa trên thực nghiệm dành riêng cho trường hợp tham
số bị trói buộc (được chia sẻ), cho bài toán siêu phân giải, mô hình
chuỗi và nhiều trường hợp khác. Nếu có hứng thú, chúng tôi khuyên bạn
nên đào sâu hơn vào mô-đun này, đọc các bài báo mà có đề xuất và phân
tích các phương pháp thực nghiệm, và rồi tự khám phá các bài báo mới
nhất về chủ đề này. Có lẽ bạn sẽ gặp (hay thậm chí phát minh ra) một ý
tưởng thông minh và lập trình nó để đóng góp cho MXNet.
.. raw:: html
Tóm tắt
-------
.. raw:: html
- Tiêu biến hay bùng nổ gradient đều là những vấn đề phổ biến trong
những mạng nơ-ron sâu. Việc khởi tạo tham số cẩn thận là rất cần
thiết để đảm bảo gradient và các tham số được kiểm soát tốt.
- Các kĩ thuật khởi tạo tham số dựa trên thực nghiệm là cần thiết để
đảm bảo rằng gradient ban đầu không quá lớn hay quá nhỏ.
- Hàm kích hoạt ReLU giải quyết được vấn đề tiêu biến gradient. Điều
này có thể làm tăng tốc độ hội tụ.
- Khởi tạo ngẫu nhiên là chìa khóa để đảm bảo tính đối xứng bị phá vỡ
trước khi tối ưu hóa.
.. raw:: html
Bài tập
-------
.. raw:: html
1. Ngoài tính đối xứng hoán vị giữa các tầng, bạn có thể nghĩ ra các
trường hợp mà mạng nơ-ron thể hiện tính đối xứng khác cần được phá vỡ
không?
2. Ta có thể khởi tạo tất cả trọng số trong hồi quy tuyến tính hoặc
trong hồi quy softmax với cùng một giá trị hay không?
3. Hãy tra cứu cận chính xác của trị riêng cho tích hai ma trận. Nó cho
ta biết gì về việc đảm bảo rằng gradient hợp lý?
4. Nếu biết rằng một vài số hạng sẽ phân kỳ, bạn có thể khắc phục vấn đề
này không? Bạn có thể tìm cảm hứng từ bài báo LARS
:cite:`You.Gitman.Ginsburg.2017`.
.. 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ý Phi Long
- Lê Khắc Hồng Phúc
- Phạm Minh Đức
- Nguyễn Văn Tâm
- Trần Yến Thy
- Bùi Chí Minh
- Phạm Hồng Vinh