- Published on
Các hàm kích hoạt (activation function) trong neural network
- Authors
- Name
- Viet Anh
- @vietanhdev
Hàm kích hoạt (activation function) mô phỏng tỷ lệ truyền xung qua axon của một neuron thần kinh. Trong một mạng nơ-ron nhân tạo, hàm kích hoạt đóng vai trò là thành phần phi tuyến tại output của các nơ-ron. Trong bài viết này, chúng ta sẽ cùng tìm hiểu các hàm kích hoạt phổ biến nhất và các ưu, nhược điểm của chúng.
Tại sao lại cần các hàm kích hoạt phi tuyến?
Câu trả lời là nếu không có các hàm kích hoạt phi tuyến, thì mạng nơ-ron của chúng ta dù có nhiều lớp vẫn sẽ có hiệu quả như một lớp tuyến tính mà thôi. Để hiểu rõ hơn và trực quan hơn, ta sẽ xem xét một mạng nơ-ron nhỏ gồm 2 lớp (có các hàm kích hoạt tuyến tính tương ứng). Mạng này sẽ nhận vào input là X và trả ra output Y:
Trong lớp thứ nhất, ta có trọng số , hệ số bias B^1. Output Z^1 được tính như sau:
Sau đó, output được đẩy qua hàm kích hoạt tuyến tính g() thu được kết quả là đầu ra của lớp thứ nhất:
Tương tự như vậy, output lại trở thành đầu vào của lớp thứ 2:
Vì ta đang giả sử hàm kích hoạt g() là tuyến tính nên (c là một số thực).
Để cho đơn giản, ta giả sử c = 1. Sử dụng các công thức bên trên, ta có:
Công thức này có thể được viết dưới dạng:
thực chất vẫn chỉ là một hàm tuyến tính của X ban đầu. Vì thế, việc xếp chồng các lớp nơ-ron lên nhau là vô nghĩa.
Vậy trên thực tế có bao giờ dùng các hàm kích hoạt tuyến tính không? Vẫn có một số trường hợp các hàm kích hoạt tuyến tính được sử dụng. Ví dụ trong bài toán regression, kết quả đầu ra Y là một số thực, hàm kích hoạt ngay phía trước Y có thể là một hàm tuyến tính. Dù thế, các hàm kích hoạt ở các lớp ẩn (hidden layer) bắt buộc phải có các yếu tố phi tuyến.
Ở các phần dưới đây, tôi xin giới thiệu đến các bạn các hàm kích hoạt thường dùng kèm theo giải thích về ưu và nhược điểm của chúng.
1. Sigmoid
Công thức
Phân tích
Hàm Sigmoid nhận đầu vào là một số thực và chuyển thành một giá trị trong khoảng (0;1) (xem đồ thị phía trên). Đầu vào là số thực âm rất nhỏ sẽ cho đầu ra tiệm cận với 0, ngược lại, nếu đầu vào là một số thực dương lớn sẽ cho đầu ra là một số tiệm cận với 1. Trong quá khứ hàm Sigmoid hay được dùng vì có đạo hàm rất đẹp. Tuy nhiên hiện nay hàm Sigmoid rất ít được dùng vì những nhược điểm sau:
1. Hàm Sigmoid bão hào và triệt tiêu gradient: Một nhược điểm dễ nhận thấy là khi đầu vào có trị tuyệt đối lớn (rất âm hoặc rất dương), gradient của hàm số này sẽ rất gần với 0. Điều này đồng nghĩa với việc các hệ số tương ứng với unit đang xét sẽ gần như không được cập nhật (còn được gọi là vanishing gradient).
2. Hàm Sigmoid không có trung tâm là 0 gây khó khăn cho việc hội tụ.
Tại sao lại như vậy?
Ta có
Lại có
Vì nên cùng dấu với .
Để trực quan hơn, hãy xét trường hợp có 2 trọng số và cần được tối ưu. Khi gradient luôn có cùng dấu, chúng ta sẽ chỉ có thể di chuyển hướng đông bắc hoặc hướng tây nam trong đồ thị phía dưới để đến được điểm tối ưu. Nếu điểm tối ưu nằm ở phía đông nam như hình dưới, chúng ta sẽ cần đi đường zig zag để đến đó.

Rất may chúng ta có thể giải quyết vấn đề này bằng cách chuẩn hoá dữ liệu về dạng có trung tâm là 0 (zero-centered) với các thuật toán batch/layer normalization.
Cài đặt hàm Sigmoid bằng Python
- Sigmoid cho một số thực:
import math
def sigmoid(x):
return 1 / (1 + math.exp(-x))
print(sigmoid(0.5))
Output:
0.6224593312018546
- Sigmoid cho một ma trận (dùng numpy):
import numpy as np
import math
x = np.array([-3, -2, -1, 1, 2, 3])
z = 1/(1 + np.exp(-x))
print(z)
Output:
[0.04742587 0.11920292 0.26894142 0.73105858 0.88079708 0.95257413]
2. Tanh
Công thức
Phân tích
Hàm nhận đầu vào là một số thực và chuyển thành một giá trị trong khoảng (-1; 1). Cũng như Sigmoid, hàm Tanh bị bão hoà ở 2 đầu (gradient thay đổi rất ít ở 2 đầu). Tuy nhiên hàm Tanh lại đối xứng qua 0 nên khắc phục được một nhược điểm của Sigmoid.
Hàm còn có thể được biểu diễn bằng hàm sigmoid như sau:
Cài đặt trong Python
- Tanh cho một số thực:
import math
x = 0.7
z = math.tanh(x)
print(z)
Output:
0.6043677771171636
- Tanh cho một ma trận (dùng numpy):
import numpy as np
import math
x = np.array([-3, -2, -1, 1, 2, 3])
z = np.tanh(x)
print(z)
Output:
[-0.99505475 -0.96402758 -0.76159416 0.76159416 0.96402758 0.99505475]
3. ReLU
Công thức
Phân tích
Hàm ReLU đang được sử dụng khá nhiều trong những năm gần đây khi huấn luyện các mạng neuron. ReLU đơn giản lọc các giá trị < 0. Nhìn vào công thức chúng ta dễ dàng hiểu được cách hoạt động của nó. Một số ưu điểm khá vượt trội của nó so với Sigmoid và Tanh:
(+) Tốc độ hội tụ nhanh hơn hẳn. ReLU có tốc độ hội tụ nhanh gấp 6 lần Tanh (Krizhevsky et al.). Điều này có thể do ReLU không bị bão hoà ở 2 đầu như Sigmoid và Tanh.
(+) Tính toán nhanh hơn. Tanh và Sigmoid sử dụng hàm
exp
và công thức phức tạp hơn ReLU rất nhiều do vậy sẽ tốn nhiều chi phí hơn để tính toán.(-) Tuy nhiên ReLU cũng có một nhược điểm: Với các node có giá trị nhỏ hơn 0, qua ReLU activation sẽ thành 0, hiện tượng đấy gọi là “Dying ReLU“. Nếu các node bị chuyển thành 0 thì sẽ không có ý nghĩa với bước linear activation ở lớp tiếp theo và các hệ số tương ứng từ node đấy cũng không được cập nhật với gradient descent. => Leaky ReLU ra đời.
(-) Khi learning rate lớn, các trọng số (weights) có thể thay đổi theo cách làm tất cả neuron dừng việc cập nhật.
Cài đặt trong Python
- ReLU cho một ma trận (dùng numpy): Cách cài đặt hàm này hết sức đơn giản như dưới
import numpy as np
x = [[-0.48017645, -0.44408717],
[-0.1838917 , -0.24481959],
[ 0.33408084, 0.42567599]]
z = np.maximum(x, 0)
print(z)
Output:
[[0. 0. ]
[0. 0. ]
[0.33408084 0.42567599]]
4. Leaky ReLU
Công thức
với là hằng số nhỏ.
Phân tích
Leaky ReLU là một cố gắng trong việc loại bỏ "dying ReLU". Thay vì trả về giá trị 0 với các đầu vào < 0 thì Leaky ReLU tạo ra một đường xiên có độ dốc nhỏ (xem đồ thị). Có nhiều báo cáo về việc hiệu Leaky ReLU có hiệu quả tốt hơn ReLU, nhưng hiệu quả này vẫn chưa rõ ràng và nhất quán.
Ngoài Leaky ReLU có một biến thể cũng khá nổi tiếng của ReLU là PReLU. PReLU tương tự Leaky ReLU nhưng cho phép neuron tự động chọn hệ số tốt nhất.
Cài đặt trong Python
- PReLU cho một ma trận (dùng numpy):
import numpy as np
x = np.array([[-0.48017645, -0.44408717],
[-0.1838917 , -0.24481959],
[ 0.33408084, 0.42567599]])
alpha = 0.01
z = np.where(x > 0, x, x * alpha)
print(z)
Output:
[[-0.00480176 -0.00444087]
[-0.00183892 -0.0024482 ]
[ 0.33408084 0.42567599]]
5. Maxout
Khi đến với Maxout, chúng ta sẽ không sử dụng công thức dạng nữa. Một dạng khá phổ biến là Maxout neuron (giới thiệu bởi Goodfellow et al.)) được tính bằng công thức: . Leaky ReLU và ReLU là các dạng đặc biệt của công thức này (Với ReLU, áp dụng công thức với ). Maxout có tất cả ưu điểm của Leaky ReLU, ReLU. Tuy vậy, nó khiến mạng phải sử dụng gấp đôi số tham số (parameter) cho mỗi neuron, vì thế làm tăng đáng kể chi phí cả về bộ nhớ và tính toán - một điều cần suy xét khi huấn luyện mạng deep learning ở hiện tại.
6. Vậy lựa chọn thế nào giữa các hàm kích hoạt?
Câu trả lời là tuỳ bài toán. Khi các bạn tìm hiểu về các cấu trúc mạng cụ thể, các activation khác nhau sẽ được sử dụng, tuỳ vào độ sâu của mạng, output mong muốn, thậm chí là dữ liệu của bài toán. Chúng ta không thể nói hàm nào tốt hơn khi chưa xét đên điều kiện cụ thể. Để bắt đầu, khi các bạn học về cấu trúc mạng nào, hãy quan sát thiết kế và mã nguồn của mạng được viết bởi phần đông mọi người. Việc này thường cho kết quả tương đối tốt.