Danh sách bài viết

Bài 5: Activation function: Sigmoid và vấn đề saturation

Activation function mang phi tuyến cho neural network. Sigmoid logistic: công thức, tính chất, đạo hàm, saturation và vanishing gradient. Vì sao sigmoid không dùng cho hidden layer của deep network. Numerical stability. Code Python và PyTorch.

24/05/2026
12 phút đọc
1 lượt xem
1

Mục tiêu bài học

Sau bài học, bạn sẽ:

  • Giải thích được vì sao neural network cần activation function (phi tuyến).
  • Viết được công thức sigmoid \( \sigma(z) = \frac{1}{1 + e^{-z}} \) và đạo hàm \( \sigma'(z) = \sigma(z)(1 - \sigma(z)) \).
  • Hiểu hiện tượng saturation: khi \( |z| \) lớn thì \( \sigma'(z) \to 0 \).
  • Giải thích vì sao saturation dẫn đến vanishing gradient trong deep network và vì sao sigmoid bị thay thế ở hidden layer.
  • Biết khi nào sigmoid vẫn hữu ích: output layer cho binary classification, gate của LSTM/GRU.
  • Viết được implementation sigmoid numerically stable bằng NumPy và dùng torch.sigmoid() trong PyTorch.

Bài này kế tiếp Bài 4 — Forward Propagation và mở đầu chuỗi 4 bài về activation function (B5-B8).

2

Vì sao cần activation function

Một layer fully-connected (không activation) thực hiện phép biến đổi affine:

\[ \mathbf{h} = W \mathbf{x} + \mathbf{b} \]

Stack 2 layer kiểu này:

\[ \mathbf{h}_2 = W_2 \big( W_1 \mathbf{x} + \mathbf{b}_1 \big) + \mathbf{b}_2 = (W_2 W_1) \mathbf{x} + (W_2 \mathbf{b}_1 + \mathbf{b}_2) \]

Đặt \( W' = W_2 W_1 \) và \( \mathbf{b}' = W_2 \mathbf{b}_1 + \mathbf{b}_2 \), ta thu được:

\[ \mathbf{h}_2 = W' \mathbf{x} + \mathbf{b}' \]

Tức là tương đương 1 layer duy nhất. Chồng bao nhiêu layer linear thì kết quả vẫn là một phép biến đổi affine — chỉ học được hàm tuyến tính. Không thể tách 2 lớp dữ liệu sắp xếp dạng vòng tròn, không thể fit hàm XOR.

Activation function chèn một bước phi tuyến giữa các layer:

\[ \mathbf{h}_1 = \phi(W_1 \mathbf{x} + \mathbf{b}_1), \quad \mathbf{h}_2 = \phi(W_2 \mathbf{h}_1 + \mathbf{b}_2) \]

Khi \( \phi \) phi tuyến, mạng nhiều layer biểu diễn được lớp hàm rộng hơn hẳn — định lý universal approximation (Hornik 1991, Cybenko 1989) phát biểu một MLP đủ rộng với 1 hidden layer và activation phi tuyến phù hợp xấp xỉ được mọi hàm liên tục trên tập compact với độ chính xác tuỳ ý.

3

4 activation phổ biến — bản đồ Bài 5-8

4 bài sắp tới đi sâu vào 4 activation hay gặp nhất:

  • Bài 5 — Sigmoid: nén về \( (0, 1) \). Lịch sử lâu đời, giờ chủ yếu dùng ở output layer cho binary classification và gate của LSTM/GRU.
  • Bài 6 — ReLU: \( \max(0, z) \). Mặc định cho hidden layer của deep network từ 2010.
  • Bài 7 — Tanh: nén về \( (-1, 1) \), zero-centered. Vẫn xuất hiện trong RNN cổ điển.
  • Bài 8 — Softmax: chuyển vector logit thành phân phối xác suất, dùng ở output layer cho multi-class classification.

Bài này tập trung sigmoid và phân tích kỹ vì sao nó không còn là lựa chọn mặc định cho hidden layer — câu chuyện minh hoạ tốt cho cơ chế gradient trong deep network.

4

Sigmoid — công thức

Sigmoid (còn gọi là logistic function) định nghĩa:

\[ \sigma(z) = \frac{1}{1 + e^{-z}} \]

Đường cong hình S đặc trưng:

z σ(z) 1 0.5 0 saturate σ'(z) ≈ 0 σ'(0) = 0.25 (max) saturate σ'(z) ≈ 0

Một số giá trị tham chiếu (làm tròn 4 chữ số):

  • \( \sigma(-5) \approx 0.0067 \) — gần 0.
  • \( \sigma(-1) \approx 0.2689 \).
  • \( \sigma(0) = 0.5 \) — chính giữa.
  • \( \sigma(1) \approx 0.7311 \).
  • \( \sigma(5) \approx 0.9933 \) — gần 1.
5

Tính chất của sigmoid

  • Range: \( \sigma(z) \in (0, 1) \) với mọi \( z \in \mathbb{R} \). Cận không bao giờ đạt được nhưng tiến tới khi \( z \to \pm\infty \).
  • Tâm: \( \sigma(0) = 0.5 \). Đầu vào dương cho output \( > 0.5 \), đầu vào âm cho \( < 0.5 \).
  • Smooth, monotonic, khả vi mọi nơi: thuận tiện cho gradient-based optimization.
  • Đối xứng quanh \( (0, 0.5) \): \( \sigma(-z) = 1 - \sigma(z) \). Lật ngang quanh \( z = 0 \) và lật dọc quanh \( y = 0.5 \) thì khớp lại chính nó.
  • Asymptote: tiệm cận \( y = 0 \) khi \( z \to -\infty \) và \( y = 1 \) khi \( z \to +\infty \).

Range \( (0, 1) \) là lý do sigmoid được dùng để biểu diễn probability: output có thể đọc trực tiếp như xác suất class dương trong bài toán binary classification.

6

Đạo hàm sigmoid

Backpropagation cần \( \sigma'(z) \). Lấy đạo hàm bằng chain rule với \( u = 1 + e^{-z} \), \( \sigma = u^{-1} \):

\[ \sigma'(z) = -u^{-2} \cdot \frac{du}{dz} = -\frac{1}{(1 + e^{-z})^2} \cdot (-e^{-z}) = \frac{e^{-z}}{(1 + e^{-z})^2} \]

Biến đổi tiếp: viết \( \frac{e^{-z}}{1 + e^{-z}} = 1 - \sigma(z) \), thu được dạng quen thuộc:

\[ \sigma'(z) = \sigma(z) \big( 1 - \sigma(z) \big) \]

Một số giá trị đạo hàm:

  • Tại \( z = 0 \): \( \sigma(0) = 0.5 \Rightarrow \sigma'(0) = 0.5 \cdot 0.5 = 0.25 \) — giá trị tối đa của \( \sigma'(z) \) trên toàn miền.
  • Tại \( z = \pm 1 \): \( \sigma'(z) \approx 0.7311 \cdot 0.2689 \approx 0.1966 \).
  • Tại \( z = \pm 5 \): \( \sigma'(z) \approx 0.9933 \cdot 0.0067 \approx 0.0066 \) — đã rất nhỏ.
  • Khi \( \sigma(z) \to 0 \) hoặc \( \to 1 \): \( \sigma'(z) \to 0 \).

Điểm cần nhớ: đạo hàm sigmoid không bao giờ vượt quá 0.25. Mỗi lần backprop qua một sigmoid layer, gradient bị nhân với một số \( \le 0.25 \).

7

Saturation problem

Saturation chỉ vùng mà sigmoid "no" — đầu ra gần sát cận 0 hoặc 1, và đạo hàm gần 0.

  • Khi \( z > 5 \): \( \sigma(z) > 0.993 \), \( \sigma'(z) < 0.007 \).
  • Khi \( z < -5 \): \( \sigma(z) < 0.007 \), \( \sigma'(z) < 0.007 \).
  • Khi \( |z| > 10 \): đạo hàm thực tế bằng 0 trong float32.

Trong backpropagation, gradient cập nhật weight ở layer \( \ell \) chứa thừa số \( \sigma'(z^{(\ell)}) \). Nếu pre-activation \( z^{(\ell)} \) rơi vào vùng saturate, thừa số đó gần 0 → weight gần như không được cập nhật. Neuron đó "kẹt".

Nguyên nhân pre-activation lớn: weight khởi tạo quá lớn, hoặc đầu vào có scale lớn (chưa normalize), hoặc bias drift theo thời gian. Đây là một trong những lý do weight initialization (B20) và batch normalization (B23) trở thành chủ đề riêng.

8

Vanishing gradient trong deep network

Trong một network có \( L \) layer dùng sigmoid, gradient của loss theo weight ở layer đầu tiên (theo backprop — chi tiết B11) có dạng tích các thừa số kiểu:

\[ \frac{\partial L}{\partial W^{(1)}} \;\propto\; \prod_{\ell=1}^{L} \sigma'(z^{(\ell)}) \cdot W^{(\ell)} \]

Nếu mỗi \( \sigma'(z^{(\ell)}) \le 0.25 \) (trường hợp tốt nhất, không saturate), tích cả chuỗi đã bị chặn trên:

\[ 0.25^{L} \]

Vài giá trị minh hoạ:

  • \( L = 10 \): \( 0.25^{10} \approx 9.5 \times 10^{-7} \).
  • \( L = 20 \): \( 0.25^{20} \approx 9.1 \times 10^{-13} \).
  • \( L = 100 \): \( 0.25^{100} \approx 6.2 \times 10^{-61} \) — bằng 0 trong float32.

Đó là vanishing gradient: gradient lan ngược về các layer đầu nhỏ tới mức weight ở đó không cập nhật. Network sâu dùng sigmoid học rất chậm hoặc đứng yên. Hochreiter (1991, luận văn) phân tích lần đầu hiện tượng này cho RNN; Bengio và cộng sự (1994) trình bày lại trong bối cảnh deep network. Cùng cơ chế làm RNN vanilla "quên" thông tin dài hạn (B36).

Lưu ý thực tế còn xấu hơn: ngay khi vài layer saturate, \( \sigma'(z) \) tụt xuống dưới 0.01 chứ không phải 0.25, và tích càng nhỏ đi.

9

Khi nào vẫn dùng sigmoid

Sigmoid không dùng cho hidden layer của deep network, nhưng vẫn cần thiết trong vài vai trò chuyên biệt:

  • Output layer cho binary classification: cần một số \( p \in (0, 1) \) đọc như xác suất class dương. Kết hợp với binary cross-entropy loss (B10). Đây là use case kinh điển và vẫn là mặc định.
  • Multi-label classification: mỗi label độc lập, dùng sigmoid trên từng output thay vì softmax (B8).
  • Gate trong LSTM / GRU (B37-38): cổng input / forget / output trong LSTM, cổng update / reset trong GRU dùng sigmoid để cho ra giá trị \( \in (0, 1) \) — giải thích như "mở bao nhiêu phần trăm".
  • Attention variants: một số biến thể attention dùng sigmoid thay softmax khi muốn nhiều vị trí được "bật" độc lập.

Điểm chung của các use case này: sigmoid được dùng ở một layer cụ thể, không phải xếp chồng hàng chục layer sigmoid liên tiếp — nên vanishing gradient không phải vấn đề.

Khuyến nghị thực tế ngắn gọn: không dùng sigmoid cho hidden layer của feed-forward deep network. Mặc định là ReLU (B6) hoặc các biến thể.

10

Not zero-centered

Vấn đề thứ hai của sigmoid (ngoài saturation) là output luôn dương — không zero-centered.

Xét gradient của loss theo weight \( w_i \) ở một neuron:

\[ \frac{\partial L}{\partial w_i} = \frac{\partial L}{\partial z} \cdot x_i \]

Nếu \( x_i \) là output của một sigmoid layer trước đó, thì \( x_i > 0 \) luôn. Dấu của gradient theo từng \( w_i \) chỉ phụ thuộc dấu của \( \frac{\partial L}{\partial z} \) — chung cho mọi \( w_i \) của neuron đó. Nói cách khác, mọi weight của một neuron cùng tăng hoặc cùng giảm trong một bước update.

Hệ quả: trajectory cập nhật weight đi zigzag — chỉ được di chuyển trong các hướng có dấu nhất quán, không thể đi thẳng theo hướng tối ưu. Hội tụ chậm. Tanh (B7) sửa được vấn đề này nhờ output \( \in (-1, 1) \) zero-centered.

11

Computational cost

Tính \( \sigma(z) \) yêu cầu \( \exp(-z) \) — phép toán mất khoảng 10-20 cycles trên CPU hiện đại, đắt hơn nhiều so với \( \max(0, z) \) của ReLU (1-2 cycles).

Trên GPU, \( \exp \) vẫn được tối ưu nhưng tổng cost layer-wise đáng kể với network rộng. Với batch size lớn và hàng triệu activation mỗi forward pass, chênh lệch tích luỹ ảnh hưởng throughput training rõ rệt — đặc biệt khi network sâu hàng chục layer.

Đó cũng là một trong những lý do thực dụng làm ReLU trở thành mặc định: rẻ hơn và tránh được vanishing gradient cùng lúc.

12

Numerical stability

Implementation ngây thơ 1 / (1 + exp(-z)) bị tràn số khi \( z \) âm rất lớn:

  • \( z = 1000 \): \( \exp(-1000) \approx 0 \), \( \sigma(z) \approx 1 \) — không vấn đề.
  • \( z = -1000 \): \( \exp(-(-1000)) = \exp(1000) \) overflow → inf → \( \sigma(z) = 0 \) (đúng kết quả) nhưng đã raise RuntimeWarning: overflow.

Cách viết stable: dùng dạng thay thế tương đương khi \( z < 0 \). Để ý:

\[ \sigma(z) = \frac{1}{1 + e^{-z}} = \frac{e^{z}}{1 + e^{z}} \]

Dạng bên phải an toàn khi \( z < 0 \) (vì \( e^z \) nhỏ); dạng bên trái an toàn khi \( z \ge 0 \) (vì \( e^{-z} \) nhỏ). Chọn nhánh theo dấu \( z \):

import numpy as np


def sigmoid_stable(z):
    return np.where(
        z >= 0,
        1.0 / (1.0 + np.exp(-z)),
        np.exp(z) / (1.0 + np.exp(z)),
    )

PyTorch (torch.sigmoid), TensorFlow (tf.nn.sigmoid) và scipy.special.expit đều dùng implementation stable kiểu này. Khi viết code production, gọi thẳng các API có sẵn — không tự nhân chia với np.exp.

13

Code Python

Implement sigmoid stable, đạo hàm, và quan sát saturate:

import numpy as np


def sigmoid(z):
    return np.where(
        z >= 0,
        1.0 / (1.0 + np.exp(-z)),
        np.exp(z) / (1.0 + np.exp(z)),
    )


def sigmoid_derivative(z):
    s = sigmoid(z)
    return s * (1 - s)


# Giá trị sigma(z) và sigma'(z) tại vài điểm
zs = np.array([-10, -5, -1, 0, 1, 5, 10])
for z in zs:
    s = sigmoid(z)
    dsdz = sigmoid_derivative(z)
    print(f"z = {z:4d}  sigma(z) = {s:.6f}  sigma'(z) = {dsdz:.6f}")

Output (đã làm tròn, có thể khác chữ số cuối tùy float precision):

z =  -10  sigma(z) = 0.000045  sigma'(z) = 0.000045
z =   -5  sigma(z) = 0.006693  sigma'(z) = 0.006648
z =   -1  sigma(z) = 0.268941  sigma'(z) = 0.196612
z =    0  sigma(z) = 0.500000  sigma'(z) = 0.250000
z =    1  sigma(z) = 0.731059  sigma'(z) = 0.196612
z =    5  sigma(z) = 0.993307  sigma'(z) = 0.006648
z =   10  sigma(z) = 0.999955  sigma'(z) = 0.000045

Quan sát đáng nhớ:

  • \( \sigma'(z) \) max là 0.25 tại \( z = 0 \), đối xứng quanh \( z = 0 \).
  • Tại \( |z| = 5 \) đạo hàm còn 0.66%; tại \( |z| = 10 \) còn 0.0045%.
  • Test stability: sigmoid(np.array([1000.0, -1000.0])) trả về [1.0, 0.0] không warning. Phiên bản ngây thơ 1/(1+np.exp(-z)) sẽ raise RuntimeWarning ở phần \( z = -1000 \).
14

Sigmoid trong PyTorch

PyTorch 2.x cung cấp 2 dạng tương đương — functional và module:

import torch
import torch.nn as nn

z = torch.tensor([-1000.0, -1.0, 0.0, 1.0, 1000.0])

# Dạng functional
print(torch.sigmoid(z))
# tensor([0.0000, 0.2689, 0.5000, 0.7311, 1.0000])

# Dạng module — dùng khi build nn.Sequential
act = nn.Sigmoid()
print(act(z))

Khi dùng cho output binary classification, khuyến nghị gộp sigmoid + loss thành nn.BCEWithLogitsLoss thay vì nn.Sigmoid() rồi nn.BCELoss(). Lý do: BCEWithLogitsLoss dùng log-sum-exp trick nội bộ, tránh tính \( \log \) của \( \sigma(z) \) khi \( \sigma(z) \) bị làm tròn về 0 — chi tiết B10.

logits = model(x)               # output thô, KHÔNG qua sigmoid
loss = nn.BCEWithLogitsLoss()(logits, target)

Lúc inference, sigmoid vẫn cần để chuyển logit → probability cho user đọc:

with torch.no_grad():
    prob = torch.sigmoid(model(x))    # probability cho class dương
    pred = (prob > 0.5).long()         # ngưỡng 0.5 → nhãn 0/1
15

Lịch sử ngắn

  • 1844: Pierre François Verhulst đặt tên "logistic curve" trong mô hình tăng trưởng dân số.
  • 1986: Rumelhart, Hinton, Williams công bố backpropagation với sigmoid là activation chính cho hidden layer. Sigmoid thành mặc định cho neural network thời kỳ này.
  • 1991: Sepp Hochreiter trong luận văn Diplom phân tích vanishing gradient trong RNN; Bengio và cộng sự (1994) tổng quát hoá cho deep network.
  • 2010: Glorot và Bengio chỉ ra Xavier initialization giảm vấn đề saturation trong các network sigmoid / tanh.
  • 2011: Glorot, Bordes, Bengio đề xuất ReLU làm activation cho deep network — học nhanh hơn nhiều.
  • 2012: AlexNet (Krizhevsky, Sutskever, Hinton) thắng ImageNet với ReLU, đánh dấu chuyển dịch khỏi sigmoid ở hidden layer.
  • 1997 → nay: LSTM (Hochreiter và Schmidhuber) dùng sigmoid cho cổng — vai trò này vẫn giữ vì gate cần \( (0, 1) \).
16

Bài tập

  1. Tính tay \( \sigma(z) \) và \( \sigma'(z) \) tại \( z = -5, 0, 5 \). Đối chiếu với bảng ở mục 13.
  2. Chứng minh \( \sigma'(z) = \sigma(z)(1 - \sigma(z)) \) bằng cách lấy đạo hàm trực tiếp công thức \( \sigma(z) = (1 + e^{-z})^{-1} \).
  3. Chứng minh tính đối xứng: \( \sigma(-z) = 1 - \sigma(z) \) với mọi \( z \).
  4. Tìm \( z \) tại đó \( \sigma'(z) = 0.1 \). Có bao nhiêu nghiệm? Gợi ý: giải phương trình bậc 2 cho \( s = \sigma(z) \).
  5. Implement sigmoid_naive(z) = 1 / (1 + np.exp(-z))sigmoid_stable như mục 12. Test cả hai với z = np.array([1000.0, -1000.0, 0.0]) và in ra warning nào xuất hiện.
  6. Với network 10 layer toàn sigmoid, weight được khởi tạo sao cho mỗi \( \sigma'(z^{(\ell)}) \approx 0.2 \). Ước tính bậc độ lớn của gradient ở layer đầu so với gradient ở layer cuối.
  7. So sánh thời gian chạy torch.sigmoid(x)torch.relu(x) trên tensor kích thước \( 10^7 \) bằng %timeit. Quan sát chênh lệch.
Đáp án ngắn
  1. \( \sigma(-5) \approx 0.0067 \), \( \sigma'(-5) \approx 0.0066 \). \( \sigma(0) = 0.5 \), \( \sigma'(0) = 0.25 \). \( \sigma(5) \approx 0.9933 \), \( \sigma'(5) \approx 0.0066 \).
  2. Xem mục 6 — chain rule với \( u = 1 + e^{-z} \).
  3. \( \sigma(-z) = \frac{1}{1 + e^{z}} \). Nhân tử và mẫu với \( e^{-z} \): \( = \frac{e^{-z}}{1 + e^{-z}} = 1 - \sigma(z) \).
  4. \( s(1-s) = 0.1 \Rightarrow s^2 - s + 0.1 = 0 \Rightarrow s = (1 \pm \sqrt{0.6})/2 \approx 0.887 \) hoặc \( 0.113 \). Tương ứng \( z = \sigma^{-1}(s) = \ln(s/(1-s)) \approx \pm 2.06 \). Hai nghiệm đối xứng.
  5. Naive raise RuntimeWarning: overflow in exp tại \( z = -1000 \); stable không warning, kết quả \( [1.0, 0.0, 0.5] \) cho cả hai.
  6. Tích \( 0.2^{10} \approx 10^{-7} \). Gradient layer đầu nhỏ hơn layer cuối khoảng 7 bậc độ lớn (chưa kể yếu tố \( W \)).
  7. ReLU thường nhanh hơn sigmoid 2-4 lần trên CPU, chênh lệch nhỏ hơn trên GPU nhưng vẫn rõ.
17

Tóm tắt

  • Activation phi tuyến là điều kiện cần để stack nhiều layer học được hàm phức tạp; không có nó, MLP suy biến thành 1 layer linear.
  • Sigmoid \( \sigma(z) = \frac{1}{1 + e^{-z}} \) nén về \( (0, 1) \), smooth, khả vi mọi nơi, \( \sigma(0) = 0.5 \).
  • Đạo hàm: \( \sigma'(z) = \sigma(z)(1 - \sigma(z)) \), max bằng 0.25 tại \( z = 0 \).
  • Saturation: \( |z| \) lớn → \( \sigma'(z) \to 0 \). Backprop nhân nhiều thừa số nhỏ → vanishing gradient. Layer đầu của deep network gần như không học được.
  • Không zero-centered → hướng update zigzag.
  • Tính \( \exp \) đắt hơn ReLU.
  • Vẫn dùng ở: output layer cho binary / multi-label classification, gate trong LSTM / GRU, một vài attention variant. Không dùng cho hidden layer của deep network.
  • Implementation phải stable cho \( z \) âm lớn — chọn nhánh theo dấu hoặc dùng torch.sigmoid / scipy.special.expit.
  • Trong PyTorch, kết hợp binary classification thì dùng BCEWithLogitsLoss thay vì Sigmoid + BCELoss.