Skip to content

Latest commit

 

History

History
415 lines (323 loc) · 27.1 KB

numerical-stability-and-init_vn.md

File metadata and controls

415 lines (323 loc) · 27.1 KB

Ổn định Số học và Khởi tạo

🏷️sec_numerical_stability

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.

Tiêu biến và Bùng nổ Gradient

Xét một mạng nơ-ron sâu với $L$ tầng, đầu vào $\mathbf{x}$ và đầu ra $\mathbf{o}$. Mỗi tầng $l$ được định nghĩa bởi một phép biến đổi $f_l$ với tham số là trọng số $\mathbf{W}_l$. Mạng nơ-ron này có thể được biểu diễn như sau:

$$\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}).$$

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 $\mathbf{o}$ theo một tập tham số $\mathbf{W}_l$ bất kỳ như sau:

$$\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}.$$

Nói cách khác, gradient này là tích của $L-l$ ma trận $\mathbf{M}_L \cdot \ldots, \cdot \mathbf{M}_l$ với vector gradient $\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 $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.

Tiêu biến Gradient

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 $\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 $(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.

%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))

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.

Bùng nổ Gradient

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 $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 $\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.

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)

Tính Đối xứng

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 $h_1$$h_2$. Trong trường hợp này, ta có thể hoán vị trọng số $\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.

Đâ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: $\mathbf{W}_l = c$ với hằng số $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ể!

Khởi tạo Tham số

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.

Khởi tạo Mặc định

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 $U[-0.07, 0.07]$, còn các hệ số điều chỉnh đều được đưa về giá trị $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.

Khởi tạo Xavier

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 $h_{i}$ ở một tầng nào đó:

$$h_{i} = \sum_{j=1}^{n_\mathrm{in}} W_{ij} x_j.$$

Các trọng số $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 $\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 $x_j$ cũng có trung bình bằng không và phương sai $\gamma^2$, độc lập với $\mathbf{W}$. Trong trường hợp này, ta có thể tính được trung bình và phương sai của $h_i$ như sau:

$$ \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} $$

Một cách để giữ phương sai cố định là đặt $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ì $\mathbf{W} \mathbf{w}$, ta cần đối phó với $\mathbf{W}^\top \mathbf{g}$, trong đó $\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 $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:

$$ \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} $$

Đâ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 $\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 $U[-a, a]$ có phương sai là $a^2/3$. Thay $\sigma^2$ bằng $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 $U\left[-\sqrt{6/(n_\mathrm{in} + n_\mathrm{out})}, \sqrt{6/(n_\mathrm{in} + n_\mathrm{out})}\right]$.

Sâu xa hơn nữa

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.

Tóm tắt

  • 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.

Bài tập

  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.

Thảo luận

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