Bỏ qua để đến nội dung

Mạng nơ-ron nhân tạo: perceptron, MLP, lan truyền xuôi, hàm kích hoạt

Sau bài này, bạn sẽ:

  • Hiểu một nơ-ron nhân tạo (artificial neuron) tính gì: cộng có trọng số, cộng hệ số tự do, rồi qua hàm kích hoạt (activation function); và perceptron học bằng luật học perceptron (perceptron learning rule) ra sao.
  • Tự chứng minh được vì sao một perceptron không giải được bài XOR, và vì sao thêm một lớp ẩn (hidden layer) thì giải được.
  • Viết được lan truyền xuôi (forward propagation) của perceptron nhiều lớp (multilayer perceptron, MLP) ở dạng ma trận, đếm được số tham số, và cài đặt nó bằng NumPy.
  • Biết các hàm kích hoạt hay dùng, vì sao chúng phải phi tuyến, cách chọn lớp ra cùng hàm mất mát cross-entropy, và định lý xấp xỉ phổ quát (universal approximation theorem) nói gì, không nói gì.

Cầu thang nhà bạn có hai công tắc, một ở chân cầu thang, một ở đầu cầu thang. Gạt công tắc nào cũng đổi trạng thái của đèn: đi lên thì bật ở dưới, lên tới nơi thì tắt ở trên. Ghi vị trí mỗi công tắc là 0 hoặc 1, lắp sao cho cả hai ở 0 thì đèn tắt. Bảng của đèn là:

Công tắc dưới x1x_1Công tắc trên x2x_2Đèn yy
000 (tắt)
011 (sáng)
101 (sáng)
110 (tắt)

Đèn sáng khi đúng một công tắc ở vị trí 1. Phép toán logic này gọi là XOR (exclusive or, “hoặc loại trừ”).

Thử một quy tắc “cộng điểm rồi so ngưỡng”

Phần tiêu đề “Thử một quy tắc “cộng điểm rồi so ngưỡng””

Cách đơn giản nhất để máy ra quyết định từ hai số x1x_1, x2x_2: cho mỗi số một trọng số, cộng lại, rồi so với một ngưỡng. Điểm đạt ngưỡng thì đèn sáng. Với vài cổng logic quen thuộc, cách này chạy tốt:

  • AND (sáng khi cả hai cùng bật): trọng số 1 và 1, ngưỡng 1.5. Chỉ (1,1)(1, 1) có điểm 2 đạt ngưỡng.
  • OR (sáng khi ít nhất một bật): trọng số 1 và 1, ngưỡng 0.5.

Với XOR thì loay hoay mãi không ra. Muốn (1,0)(1, 0) và (0,1)(0, 1) sáng, mỗi trọng số phải đạt ngưỡng. Muốn (0,0)(0, 0) tắt, ngưỡng phải dương. Nhưng khi đó (1,1)(1, 1) có điểm bằng tổng hai trọng số, còn lớn hơn nữa, nên cũng sáng. Nhìn bằng hình: đặt bốn điểm ở bốn góc một hình vuông, hai điểm “sáng” nằm trên một đường chéo, hai điểm “tắt” nằm trên đường chéo kia. Quy tắc “cộng điểm rồi so ngưỡng” chỉ chia mặt phẳng bằng một đường thẳng, mà không đường thẳng nào tách được hai đường chéo.

Nói bằng lời thì XOR rất dễ: đèn sáng khi “ít nhất một công tắc bật” và “không phải cả hai cùng bật”. Vế đầu là OR, vế sau là “không phải AND”. Mỗi vế là một quy tắc cộng điểm rồi so ngưỡng. Thêm một quy tắc thứ ba nhận kết quả của hai quy tắc đầu, và bạn có XOR. Đó là ý tưởng của mạng nơ-ron nhiều lớp: những đơn vị đơn giản, xếp thành lớp, lớp sau dùng kết quả của lớp trước.

Bài toán của bài này là: ghép những “nơ-ron” cộng điểm rồi quyết định thành nhiều lớp, để cả mạng biểu diễn được những quy tắc mà một nơ-ron không làm nổi, như đèn cầu thang; và viết phép tính của cả mạng thật gọn, để dùng được cho cả những bài lớn như đọc chữ số viết tay.

Nơ-ron sinh học nhận tín hiệu từ nhiều nơ-ron khác. Khi tổng kích thích đủ mạnh, nó “phát” một xung truyền đi. McCulloch và Pitts (1943) mô hình hóa việc này rất thô: cộng có trọng số các tín hiệu vào, rồi so với ngưỡng. Mạng nơ-ron (neural network) ngày nay chỉ giữ lại ý tưởng đó, không mô phỏng bộ não.

Một nơ-ron nhân tạo nhận vectơ x=[x1,…,xn]⊤\mathbf{x} = [x_1, \dots, x_n]^\top gồm nn đầu vào và tính hai bước:

z=w⊤x+b=∑j=1nwjxj+b,a=g(z)z = \mathbf{w}^\top\mathbf{x} + b = \sum_{j=1}^{n} w_j x_j + b, \qquad a = g(z)
  • w\mathbf{w} là vectơ trọng số, bb là hệ số tự do. Hệ số tự do đóng vai “trừ đi ngưỡng”: “điểm ≥\geq ngưỡng θ\theta” chính là "z≥0z \geq 0" với b=−θb = -\theta.
  • zz là tổng có trọng số, còn gọi là tiền kích hoạt (pre-activation).
  • gg là hàm kích hoạt, và aa là đầu ra của nơ-ron, gọi là kích hoạt (activation).

Bạn đã gặp nơ-ron này rồi. Với g(z)=zg(z) = z, nó là hồi quy tuyến tính. Với g=σg = \sigma, nó là hồi quy logistic. Tập các điểm có z=0z = 0 là một đường thẳng khi n=2n = 2, một siêu phẳng (hyperplane) khi nn lớn hơn: đó là biên quyết định (decision boundary) của nơ-ron.

Perceptron (Rosenblatt, 1958) là nơ-ron có hàm kích hoạt là hàm bậc thang (step function):

step(z)={1neˆˊu z≥00neˆˊu z<0y^=step(w⊤x+b)\mathrm{step}(z) = \begin{cases} 1 & \text{nếu } z \geq 0 \\ 0 & \text{nếu } z < 0 \end{cases} \qquad\qquad \hat{y} = \mathrm{step}(\mathbf{w}^\top\mathbf{x} + b)

Rosenblatt còn đưa ra một cách học w\mathbf{w}, bb từ dữ liệu có nhãn y∈{0,1}y \in \{0, 1\}. Duyệt từng mẫu, tính y^\hat{y}, rồi sửa:

w←w+η (y−y^) x,b←b+η (y−y^)\mathbf{w} \leftarrow \mathbf{w} + \eta\,(y - \hat{y})\,\mathbf{x}, \qquad b \leftarrow b + \eta\,(y - \hat{y})

Hiệu y−y^y - \hat{y} chỉ có ba giá trị, nên luật này đọc thành ba trường hợp:

  • Đoán đúng (y−y^=0y - \hat{y} = 0): không sửa gì.
  • Đoán 0 nhưng đúng ra là 1 (y−y^=1y - \hat{y} = 1): cộng ηx\eta\mathbf{x} vào w\mathbf{w} và η\eta vào bb. Tính lại zz của chính mẫu này với tham số mới: (w+ηx)⊤x+(b+η)=z+η(∥x∥2+1)(\mathbf{w} + \eta\mathbf{x})^\top\mathbf{x} + (b + \eta) = z + \eta\left(\lVert\mathbf{x}\rVert^2 + 1\right). Vậy zz tăng, mẫu bị kéo về phía “1”.
  • Đoán 1 nhưng đúng ra là 0 (y−y^=−1y - \hat{y} = -1): trừ đi, zz của mẫu giảm cùng một lượng, mẫu bị kéo về phía “0”.

Ví dụ với AND, η=1\eta = 1, xuất phát từ w=[0,0]⊤\mathbf{w} = [0, 0]^\top, b=0b = 0. Epoch đầu tiên:

Mẫu (x1,x2)(x_1, x_2)yyzzy^\hat{y}Sửa thành
(0,0)(0, 0)001sai: w=[0,0]⊤\mathbf{w} = [0, 0]^\top, b=−1b = -1
(0,1)(0, 1)0−1-10đúng, giữ nguyên
(1,0)(1, 0)0−1-10đúng, giữ nguyên
(1,1)(1, 1)1−1-10sai: w=[1,1]⊤\mathbf{w} = [1, 1]^\top, b=0b = 0

Epoch này sai 2 mẫu. Lặp lại tới khi có một epoch không sai mẫu nào. Ô code đầu tiên chạy tiếp và dừng ở epoch thứ 6.

Khi nào luật học dừng? Dữ liệu gọi là tách được tuyến tính (linearly separable) nếu có một siêu phẳng chia đúng mọi mẫu của hai lớp. Định lý hội tụ perceptron (Novikoff, 1962) nói: nếu dữ liệu tách được tuyến tính, luật học chỉ sửa một số hữu hạn lần rồi dừng, với mọi thứ tự duyệt mẫu. Nếu dữ liệu không tách được, nó sửa mãi, và các trọng số cứ đổi qua đổi lại.

Vì sao một perceptron không giải được XOR

Phần tiêu đề “Vì sao một perceptron không giải được XOR”

Phần Trực giác đã nói bằng hình. Giờ chứng minh chặt. Giả sử có w1,w2,bw_1, w_2, b để perceptron đúng cả bốn mẫu XOR. Với hàm bậc thang, y^=1\hat{y} = 1 khi z≥0z \geq 0, y^=0\hat{y} = 0 khi z<0z < 0. Bốn mẫu cho bốn điều kiện:

(0,0)→0:b<0(0,1)→1:w2+b≥0(1,0)→1:w1+b≥0(1,1)→0:w1+w2+b<0\begin{aligned} (0, 0) \to 0:&\quad b < 0 \\ (0, 1) \to 1:&\quad w_2 + b \geq 0 \\ (1, 0) \to 1:&\quad w_1 + b \geq 0 \\ (1, 1) \to 0:&\quad w_1 + w_2 + b < 0 \end{aligned}

Cộng điều kiện thứ hai và thứ ba được w1+w2+2b≥0w_1 + w_2 + 2b \geq 0. Chuyển một bb sang vế phải: w1+w2+b≥−bw_1 + w_2 + b \geq -b. Theo điều kiện đầu, b<0b < 0 nên −b>0-b > 0. Vậy w1+w2+b>0w_1 + w_2 + b > 0, trái với điều kiện cuối. Không có bộ tham số nào thỏa cả bốn: XOR không tách được tuyến tính.

Thay hàm bậc thang bằng sigmoid rồi quyết định “lớp 1 khi σ(z)≥0.5\sigma(z) \geq 0.5” cũng không cứu được, vì σ(z)≥0.5\sigma(z) \geq 0.5 đúng khi z≥0z \geq 0: biên quyết định vẫn là đường thẳng z=0z = 0. Năm 1969, cuốn Perceptrons của Minsky và Papert phân tích kỹ những giới hạn kiểu này của perceptron một lớp, và góp phần làm giới nghiên cứu nguội đi với mạng nơ-ron trong khoảng một thập kỷ.

Trở lại ý “ghép hai quy tắc”. Dùng hai perceptron ở lớp đầu và một perceptron ở lớp sau:

  • h1=step(x1+x2−0.5)h_1 = \mathrm{step}(x_1 + x_2 - 0.5): bằng 1 khi ít nhất một công tắc bật (OR).
  • h2=step(x1+x2−1.5)h_2 = \mathrm{step}(x_1 + x_2 - 1.5): bằng 1 khi cả hai cùng bật (AND).
  • y^=step(h1−h2−0.5)\hat{y} = \mathrm{step}(h_1 - h_2 - 0.5): bằng 1 khi h1=1h_1 = 1 và h2=0h_2 = 0.

Kiểm tra cả bốn mẫu:

(x1,x2)(x_1, x_2)(h1,h2)(h_1, h_2)h1−h2−0.5h_1 - h_2 - 0.5y^\hat{y}XOR
(0,0)(0, 0)(0,0)(0, 0)−0.5-0.500
(0,1)(0, 1)(1,0)(1, 0)0.50.511
(1,0)(1, 0)(1,0)(1, 0)0.50.511
(1,1)(1, 1)(1,1)(1, 1)−0.5-0.500

Để ý cột (h1,h2)(h_1, h_2). Lớp đầu đã đổi cách biểu diễn dữ liệu: hai mẫu “sáng” cùng thành điểm (1,0)(1, 0), hai mẫu “tắt” thành (0,0)(0, 0) và (1,1)(1, 1). Trong mặt phẳng (h1,h2)(h_1, h_2), ba điểm này tách được bằng một đường thẳng, và nơ-ron cuối chỉ việc vẽ đường thẳng đó. Đây là điều mạng nhiều lớp làm được mà một nơ-ron không làm được: các lớp đầu học ra đặc trưng mới, lớp cuối giải bài toán dễ hơn trên đặc trưng đó.

Mạng như thế gọi là perceptron nhiều lớp:

  • Lớp vào (input layer) chỉ là các đầu vào x1,…,xnx_1, \dots, x_n, không tính gì.
  • Lớp ẩn là các lớp ở giữa. “Ẩn” vì dữ liệu không cho biết chúng phải ra giá trị gì.
  • Lớp ra (output layer) cho dự đoán y^\hat{y}.

Mỗi nơ-ron nhận đầu vào từ mọi nơ-ron của lớp ngay trước, nên mỗi lớp như vậy gọi là lớp kết nối đầy đủ (fully connected layer, hay dense layer). Thông tin chỉ đi một chiều từ lớp vào tới lớp ra, không quay vòng, nên MLP còn gọi là mạng truyền thẳng (feed-forward network). Mạng ở trên viết tắt là 2-2-1 (số nơ-ron mỗi lớp) và được gọi là mạng 2 lớp: theo quy ước thường gặp, không đếm lớp vào.

Tên “perceptron nhiều lớp” là di sản lịch sử. MLP ngày nay không dùng hàm bậc thang mà dùng các hàm kích hoạt trơn hơn, lý do ở mục Hàm kích hoạt.

Với 2 đầu vào thì viết từng nơ-ron được. Bài đọc chữ số ở ô code thứ ba có 64 đầu vào và 64 nơ-ron ẩn: cần một cách viết gọn hơn.

Ký hiệu. Đánh số lớp l=0,1,…,NLl = 0, 1, \dots, N_L, với lớp 0 là lớp vào và lớp NLN_L là lớp ra. Lớp ll có n[l]n^{[l]} nơ-ron; lớp vào có n[0]=nn^{[0]} = n, bằng số đặc trưng. Chỉ số trong ngoặc vuông [l][l] là số thứ tự lớp, khác với (i)(i) là số thứ tự mẫu.

Một lớp. Nơ-ron thứ kk của lớp ll có vectơ trọng số wk[l]\mathbf{w}^{[l]}_k (dài n[l−1]n^{[l-1]}) và hệ số tự do bk[l]b^{[l]}_k. Nó nhận vectơ kích hoạt a[l−1]\mathbf{a}^{[l-1]} của lớp trước và tính zk[l]=(wk[l])⊤a[l−1]+bk[l]z^{[l]}_k = \left(\mathbf{w}^{[l]}_k\right)^\top\mathbf{a}^{[l-1]} + b^{[l]}_k. Xếp các vectơ (wk[l])⊤\left(\mathbf{w}^{[l]}_k\right)^\top thành các hàng của một ma trận W[l]\mathbf{W}^{[l]}, và các bk[l]b^{[l]}_k thành vectơ cột b[l]\mathbf{b}^{[l]}. Phép nhân ma trận với vectơ tính một lúc mọi tích vô hướng, mỗi hàng một tích, nên cả lớp viết được trong hai dòng:

z[l]=W[l]a[l−1]+b[l]a[l]=g[l] ⁣(z[l])\begin{aligned} \mathbf{z}^{[l]} &= \mathbf{W}^{[l]}\mathbf{a}^{[l-1]} + \mathbf{b}^{[l]} \\ \mathbf{a}^{[l]} &= g^{[l]}\!\left(\mathbf{z}^{[l]}\right) \end{aligned}

Hàm kích hoạt g[l]g^{[l]} áp lên từng phần tử của z[l]\mathbf{z}^{[l]}. Các lớp ẩn thường dùng chung một hàm gg; lớp ra có thể dùng hàm khác (mục Lớp ra).

Kiểm tra kích thước. W[l]\mathbf{W}^{[l]} có n[l]n^{[l]} hàng (mỗi nơ-ron của lớp ll một hàng) và n[l−1]n^{[l-1]} cột (mỗi đầu vào một cột):

z[l]⏟n[l]×1=W[l]⏟n[l]×n[l−1] a[l−1]⏟n[l−1]×1+b[l]⏟n[l]×1\underbrace{\mathbf{z}^{[l]}}_{n^{[l]} \times 1} = \underbrace{\mathbf{W}^{[l]}}_{n^{[l]} \times n^{[l-1]}}\ \underbrace{\mathbf{a}^{[l-1]}}_{n^{[l-1]} \times 1} + \underbrace{\mathbf{b}^{[l]}}_{n^{[l]} \times 1}

Cả mạng. Bắt đầu với a[0]=x\mathbf{a}^{[0]} = \mathbf{x}, áp hai dòng trên cho l=1,2,…,NLl = 1, 2, \dots, N_L, và đầu ra là y^=a[NL]\hat{\mathbf{y}} = \mathbf{a}^{[N_L]}. Tính lần lượt từ lớp vào tới lớp ra như vậy gọi là lan truyền xuôi.

Mạng XOR ở trên viết thành:

W[1]=[1111],b[1]=[−0.5−1.5],W[2]=[1−1],b[2]=[−0.5]\mathbf{W}^{[1]} = \begin{bmatrix} 1 & 1 \\ 1 & 1 \end{bmatrix}, \quad \mathbf{b}^{[1]} = \begin{bmatrix} -0.5 \\ -1.5 \end{bmatrix}, \quad \mathbf{W}^{[2]} = \begin{bmatrix} 1 & -1 \end{bmatrix}, \quad \mathbf{b}^{[2]} = \begin{bmatrix} -0.5 \end{bmatrix}

Với x=[1,0]⊤\mathbf{x} = [1, 0]^\top: z[1]=[1−0.5, 1−1.5]⊤=[0.5,−0.5]⊤\mathbf{z}^{[1]} = [1 - 0.5,\ 1 - 1.5]^\top = [0.5, -0.5]^\top, qua hàm bậc thang được a[1]=[1,0]⊤\mathbf{a}^{[1]} = [1, 0]^\top. Tiếp theo z[2]=1⋅1−1⋅0−0.5=0.5z^{[2]} = 1 \cdot 1 - 1 \cdot 0 - 0.5 = 0.5, nên y^=1\hat{y} = 1. Đúng với dòng thứ ba của bảng.

Nhiều mẫu một lúc. Xếp mm mẫu thành mm cột của ma trận A[0]=[x(1),…,x(m)]\mathbf{A}^{[0]} = \left[\mathbf{x}^{(1)}, \dots, \mathbf{x}^{(m)}\right], kích thước n×mn \times m. Cột thứ ii của W[l]A[l−1]\mathbf{W}^{[l]}\mathbf{A}^{[l-1]} chính là W[l]\mathbf{W}^{[l]} nhân với cột thứ ii của A[l−1]\mathbf{A}^{[l-1]}, nên một phép nhân ma trận làm lan truyền xuôi cho cả mm mẫu:

Z[l]=W[l]A[l−1]+b[l],A[l]=g[l] ⁣(Z[l])\mathbf{Z}^{[l]} = \mathbf{W}^{[l]}\mathbf{A}^{[l-1]} + \mathbf{b}^{[l]}, \qquad \mathbf{A}^{[l]} = g^{[l]}\!\left(\mathbf{Z}^{[l]}\right)

Ở đây b[l]\mathbf{b}^{[l]} được cộng vào mọi cột; NumPy tự làm việc này (gọi là broadcasting). Ô code thứ hai làm đúng phép tính này. Máy tính nhân ma trận rất nhanh, nhất là trên GPU, nên viết dạng ma trận không chỉ gọn mà còn là cách mạng nơ-ron thật sự được tính.

Số tham số. Lớp ll có n[l]×n[l−1]n^{[l]} \times n^{[l-1]} trọng số và n[l]n^{[l]} hệ số tự do, tức n[l](n[l−1]+1)n^{[l]}\left(n^{[l-1]} + 1\right) tham số. MLP 64-64-10 ở ô code thứ ba có 64×65+10×65=4160+650=481064 \times 65 + 10 \times 65 = 4160 + 650 = 4810 tham số.

Vì sao phải phi tuyến. Thử bỏ hàm kích hoạt ở lớp ẩn, tức g(z)=zg(z) = z. Thay a[1]=W[1]x+b[1]\mathbf{a}^{[1]} = \mathbf{W}^{[1]}\mathbf{x} + \mathbf{b}^{[1]} vào lớp thứ hai:

z[2]=W[2](W[1]x+b[1])+b[2]=(W[2]W[1])x+(W[2]b[1]+b[2])\begin{aligned} \mathbf{z}^{[2]} &= \mathbf{W}^{[2]}\left(\mathbf{W}^{[1]}\mathbf{x} + \mathbf{b}^{[1]}\right) + \mathbf{b}^{[2]} \\ &= \left(\mathbf{W}^{[2]}\mathbf{W}^{[1]}\right)\mathbf{x} + \left(\mathbf{W}^{[2]}\mathbf{b}^{[1]} + \mathbf{b}^{[2]}\right) \end{aligned}

Dòng thứ hai nhân phân phối W[2]\mathbf{W}^{[2]} vào trong ngoặc rồi gom lại. Đặt W=W[2]W[1]\mathbf{W} = \mathbf{W}^{[2]}\mathbf{W}^{[1]} và b=W[2]b[1]+b[2]\mathbf{b} = \mathbf{W}^{[2]}\mathbf{b}^{[1]} + \mathbf{b}^{[2]}: hai lớp gộp thành một lớp Wx+b\mathbf{W}\mathbf{x} + \mathbf{b}. Lặp lại lập luận này, xếp bao nhiêu lớp tuyến tính cũng chỉ bằng một lớp tuyến tính, và vẫn không giải được XOR. Chính hàm kích hoạt phi tuyến cho phép các lớp ẩn bẻ cong không gian, như cách lớp ẩn của mạng XOR gộp hai điểm “sáng” làm một.

Các hàm hay gặp. Bảng giá trị ở một vài điểm (Leaky ReLU với α=0.01\alpha = 0.01):

Hàmz=−2z = -2z=−0.5z = -0.5z=0z = 0z=0.5z = 0.5z=2z = 2
Bậc thang00111
Sigmoid σ\sigma0.1190.3780.50.6220.881
tanh−0.964−0.46200.4620.964
ReLU0000.52
Leaky ReLU−0.02−0.00500.52
GELU−0.046−0.15400.3461.954
  • Bậc thang. Đồ thị nằm ngang ở mọi nơi trừ chỗ nhảy tại 0, nên đạo hàm bằng 0 ở mọi chỗ có đạo hàm. Gradient descent cần độ dốc để biết nên sửa tham số về phía nào, nên mạng nhiều lớp với hàm bậc thang không học bằng gradient được. Luật học perceptron chỉ dùng được cho một lớp, vì chỉ lớp ra mới có nhãn để so.
  • Sigmoid σ(z)=11+e−z\sigma(z) = \dfrac{1}{1 + e^{-z}}, giá trị trong (0,1)(0, 1). Trơn, nhưng bão hòa (saturate) khi ∣z∣\lvert z\rvert lớn: đồ thị gần như nằm ngang, đạo hàm gần 0. Ngày nay sigmoid chủ yếu dùng ở lớp ra của bài phân loại hai lớp, và trong các cổng của GRU, LSTM.
  • tanh tanh⁡(z)=ez−e−zez+e−z=2σ(2z)−1\tanh(z) = \dfrac{e^{z} - e^{-z}}{e^{z} + e^{-z}} = 2\sigma(2z) - 1, giá trị trong (−1,1)(-1, 1). Là sigmoid co giãn lại cho đối xứng quanh 0, nên đầu ra có trung bình gần 0 hơn. Cũng bão hòa ở hai đầu. Hay dùng trong mạng hồi tiếp.
  • ReLU (rectified linear unit) ReLU(z)=max⁡(0,z)\mathrm{ReLU}(z) = \max(0, z). Tính rất rẻ, không bão hòa ở phía dương, và thành lựa chọn mặc định cho lớp ẩn từ đầu thập niên 2010 (Nair và Hinton, 2010; Glorot và cộng sự, 2011). Điểm yếu: với z<0z < 0, đầu ra và đạo hàm đều bằng 0. Nếu một nơ-ron rơi vào cảnh z<0z < 0 với mọi mẫu, nó không bao giờ học được nữa, gọi là nơ-ron chết (dying ReLU).
  • Leaky ReLU max⁡(αz,z)\max(\alpha z, z) với α\alpha nhỏ, thường 0.010.01 (Maas và cộng sự, 2013). Phía âm vẫn có một độ dốc nhỏ, nên nơ-ron không chết hẳn.
  • GELU (Gaussian error linear unit, Hendrycks và Gimpel, 2016) GELU(z)=z Φ(z)\mathrm{GELU}(z) = z\,\Phi(z), với Φ\Phi là hàm phân phối tích lũy của phân phối chuẩn tắc: xác suất một biến chuẩn tắc nhỏ hơn zz. Khi zz lớn, Φ(z)≈1\Phi(z) \approx 1 nên GELU gần bằng zz; khi zz rất âm, Φ(z)≈0\Phi(z) \approx 0 nên GELU gần 0. Ở giữa, đồ thị trơn và hơi trũng xuống dưới 0, thấp nhất khoảng −0.17-0.17 tại z≈−0.75z \approx -0.75. GELU là hàm kích hoạt của BERT và GPT; bài Xây dựng LLM đầu tiên và bài Họ LLaMA gặp lại nó trong khối MLP của Transformer.

Không có hàm nào tốt nhất cho mọi bài. Điểm khởi đầu an toàn: ReLU cho lớp ẩn của MLP và CNN, GELU cho Transformer, tanh và sigmoid trong mạng hồi tiếp. Bài Huấn luyện mạng nơ-ron giải thích vì sao sự bão hòa của sigmoid làm mạng sâu khó học.

Lớp ẩn học đặc trưng; lớp ra biến đặc trưng đó thành đúng loại dự đoán mà bài toán cần. Ba trường hợp hay gặp:

Bài toánLớp raHàm mất mát
Hồi quy (dự đoán một số)1 nơ-ron, g(z)=zg(z) = zMSE
Phân loại hai lớp1 nơ-ron, sigmoidBinary cross-entropy
Phân loại KK lớpKK nơ-ron, softmaxCross-entropy

Softmax. Lớp ra có KK nơ-ron, cho KK logit z1,…,zKz_1, \dots, z_K. Softmax đổi chúng thành phân phối xác suất:

y^k=softmax(z)k=ezk∑j=1Kezj\hat{y}_k = \mathrm{softmax}(\mathbf{z})_k = \frac{e^{z_k}}{\sum_{j=1}^{K} e^{z_j}}

Hàm mũ làm mọi số thành dương, phép chia làm tổng bằng 1, và logit lớn hơn thì xác suất lớn hơn. Khác các hàm kích hoạt ở trên, softmax không tính từng phần tử riêng: mỗi y^k\hat{y}_k phụ thuộc vào mọi logit. Ví dụ z=[2.0,1.0,0.1]⊤\mathbf{z} = [2.0, 1.0, 0.1]^\top cho y^≈[0.659,0.242,0.099]⊤\hat{\mathbf{y}} \approx [0.659, 0.242, 0.099]^\top.

Cross-entropy. Với một mẫu có nhãn đúng là lớp cc, mất mát là −ln⁡-\ln xác suất mà mạng gán cho lớp cc. Viết nhãn thành vectơ one-hot y\mathbf{y} (phần tử thứ cc bằng 1, còn lại bằng 0) thì công thức trung bình trên mm mẫu là:

L=−1m∑i=1m∑k=1Kyk(i)ln⁡y^k(i)\mathcal{L} = -\frac{1}{m}\sum_{i=1}^{m}\sum_{k=1}^{K} y^{(i)}_k \ln \hat{y}^{(i)}_k

Trong tổng theo kk, chỉ số hạng của lớp đúng còn lại (các yk(i)y^{(i)}_k khác bằng 0), nên mỗi mẫu góp đúng −ln⁡y^c(i)-\ln \hat{y}^{(i)}_{c}. Với ví dụ trên: nếu lớp đúng là lớp 1 thì mất mát là −ln⁡0.659≈0.417-\ln 0.659 \approx 0.417; nếu là lớp 3 thì −ln⁡0.099≈2.317-\ln 0.099 \approx 2.317. Đoán đúng và tự tin thì mất mát gần 0; tự tin mà sai thì mất mát rất lớn. Với K=2K = 2 và một nơ-ron sigmoid cho y^=P(y=1)\hat{y} = P(y = 1), công thức thu về binary cross-entropy −1m∑i[y(i)ln⁡y^(i)+(1−y(i))ln⁡(1−y^(i))]-\frac{1}{m}\sum_i\left[y^{(i)}\ln\hat{y}^{(i)} + \left(1 - y^{(i)}\right)\ln\left(1 - \hat{y}^{(i)}\right)\right].

Nhìn lại cả mạng: lớp ra softmax cộng cross-entropy chính là hồi quy softmax, chỉ khác ở chỗ đầu vào của nó không phải x\mathbf{x} mà là a[NL−1]\mathbf{a}^{[N_L - 1]}, các đặc trưng do lớp ẩn học ra.

Mạng XOR cần đúng một lớp ẩn. Vậy một lớp ẩn làm được tới đâu? Định lý xấp xỉ phổ quát trả lời: với một hàm kích hoạt phi tuyến phù hợp, một MLP một lớp ẩn, đủ nhiều nơ-ron, xấp xỉ được mọi hàm liên tục trên một miền bị chặn với sai số nhỏ tùy ý. Cybenko (1989) chứng minh cho sigmoid; Hornik, Stinchcombe và White (1989) cho một lớp hàm rộng hơn; Leshno và cộng sự (1993) chỉ ra mọi hàm kích hoạt liên tục không phải đa thức đều dùng được, gồm cả ReLU.

Bài này không chứng minh, nhưng ý chính dễ thấy với ReLU. Mỗi nơ-ron ReLU là một đường gãy khúc tại một điểm. Hiệu ReLU(z)−ReLU(z−1)\mathrm{ReLU}(z) - \mathrm{ReLU}(z - 1) là một con dốc đi từ 0 lên 1 rồi nằm ngang. Cộng nhiều con dốc như vậy, mỗi cái đặt ở một chỗ với một độ cao, bạn ghép được một đường gấp khúc bám theo đường cong bất kỳ. Càng nhiều nơ-ron, càng nhiều khúc, càng bám sát.

Đọc kỹ những gì định lý không nói:

  • Không nói cần bao nhiêu nơ-ron. Với nhiều hàm, số nơ-ron cần có thể rất lớn. Trên thực tế, mạng sâu thường biểu diễn cùng một hàm với ít tham số hơn nhiều so với mạng nông và rộng.
  • Không nói tìm ra trọng số bằng cách nào. Định lý chỉ đảm bảo bộ trọng số tốt tồn tại. Gradient descent có tìm được nó không là chuyện khác.
  • Không nói gì về dữ liệu mới. Khớp tốt tập huấn luyện vẫn có thể là overfitting.

Trọng số của mạng XOR được đặt bằng tay. Mạng chữ số có 4810 tham số, mạng thật có hàng triệu tới hàng tỉ: không thể đặt tay. Hướng đi đã rõ: chọn hàm mất mát L\mathcal{L} (cross-entropy), rồi dùng gradient descent để giảm nó. Muốn vậy cần ∂L/∂W[l]\partial\mathcal{L}/\partial\mathbf{W}^{[l]} và ∂L/∂b[l]\partial\mathcal{L}/\partial\mathbf{b}^{[l]} cho mọi lớp, mà mỗi tham số ảnh hưởng tới L\mathcal{L} qua tất cả các lớp sau nó. Cách tính các gradient này hiệu quả là lan truyền ngược (backpropagation), nội dung của bài Huấn luyện mạng nơ-ron.

Ngoài ra, MLP coi mỗi đầu vào là một con số độc lập: nó không biết hai điểm ảnh nằm cạnh nhau hay hai từ đứng liền nhau. Bài Mạng tích chập (CNN) và bài Mạng nơ-ron hồi tiếp (RNN) đưa cấu trúc của ảnh và chuỗi vào mạng.

Hình dưới đặt hai thứ cạnh nhau. Bên trái là một nơ-ron: nhìn theo mũi tên, các đầu vào xjx_j được nhân với trọng số wjw_j, cộng với bb thành zz, rồi qua gg thành aa. Bên phải là một MLP 2-3-1: mỗi nơ-ron của lớp ẩn chính là nơ-ron bên trái, và mỗi mũi tên giữa hai lớp là một trọng số, tức một phần tử của W[l]\mathbf{W}^{[l]}.

Bảng dưới là kết quả lan truyền xuôi của ô code thứ hai: mạng XOR 2-2-1, lần này dùng sigmoid thay cho hàm bậc thang, với trọng số lớn hơn để sigmoid gần giống bậc thang. Hãy nhìn cột lớp ẩn: hai mẫu “sáng” (0,1)(0, 1) và (1,0)(1, 0) cho cùng một điểm (0.953,0.047)(0.953, 0.047). Lớp ẩn đã gộp chúng lại, nên lớp ra chỉ còn phải tách ba điểm bằng một đường thẳng.

(x1,x2)(x_1, x_2)Lớp ẩn a[1]\mathbf{a}^{[1]}Lớp ra y^\hat{y}XOR
(0,0)(0, 0)(0.047,0.000)(0.047, 0.000)0.0110
(0,1)(0, 1)(0.953,0.047)(0.953, 0.047)0.9831
(1,0)(1, 0)(0.953,0.047)(0.953, 0.047)0.9831
(1,1)(1, 1)(1.000,0.953)(1.000, 0.953)0.0110

Các ô dưới đây chạy độc lập, ô nào cũng tự tạo dữ liệu. Hai ô đầu chỉ dùng NumPy và chạy ngay trên trang. Ô thứ ba cần PyTorch nên chạy trên máy bạn.

Ô thứ nhất cài đặt perceptron và luật học perceptron, rồi cho nó học ba cổng AND, OR, XOR, mỗi cổng tối đa 20 epoch. Mỗi cổng in số epoch đã chạy, w\mathbf{w}, bb, số mẫu sai trong từng epoch, và dự đoán cuối cùng.

Perceptron học AND, OR và XOR

import numpy as np
# Bốn đầu vào của một cổng logic hai đầu vào: mỗi hàng là một mẫu.
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
gates = {
"AND": np.array([0, 0, 0, 1]),
"OR": np.array([0, 1, 1, 1]),
"XOR": np.array([0, 1, 1, 0]),
}
def step(z):
return (z >= 0).astype(int) # hàm bậc thang: 1 nếu z >= 0
def train_perceptron(X, y, eta=1.0, max_epochs=20):
w, b = np.zeros(X.shape[1]), 0.0 # xuất phát từ 0
history = [] # số mẫu đoán sai trong mỗi epoch
for epoch in range(max_epochs):
errors = 0
for x_i, y_i in zip(X, y):
y_hat = step(w @ x_i + b)
# Luật học perceptron: chỉ sửa khi đoán sai.
w = w + eta * (y_i - y_hat) * x_i
b = b + eta * (y_i - y_hat)
errors += int(y_hat != y_i)
history.append(errors)
if errors == 0: # một lượt không sai mẫu nào: đã hội tụ
break
return w, b, history
for name, y in gates.items():
w, b, history = train_perceptron(X, y)
print(f"{name}: epochs = {len(history)}, w = {w}, b = {b}")
print(f" errors per epoch: {history}")
print(f" predictions: {step(X @ w + b)}, targets: {y}")

Lần chạy đầu cần tải Python, khoảng 10 MB.

  • AND hội tụ sau 6 epoch (số mẫu sai 2, 3, 3, 2, 1, 0), dừng ở w=[2,1]⊤\mathbf{w} = [2, 1]^\top, b=−3b = -3. Bộ số này không đối xứng như “trọng số 1 và 1, ngưỡng 1.5” của mục Trực giác, nhưng cũng đúng: chỉ (1,1)(1, 1) có z=0≥0z = 0 \geq 0. Dữ liệu tách được tuyến tính thì có vô số đường thẳng tách đúng, và perceptron dừng ở đường đầu tiên nó gặp.
  • OR hội tụ sau 4 epoch, ở w=[1,1]⊤\mathbf{w} = [1, 1]^\top, b=−1b = -1.
  • XOR sai 3, 3, rồi 4 mẫu ở mọi epoch cho tới epoch thứ 20. Các trọng số cứ đổi qua đổi lại; lúc dừng là w=[−1,0]⊤\mathbf{w} = [-1, 0]^\top, b=0b = 0, dự đoán [1,1,0,0][1, 1, 0, 0], chỉ đúng 2 trên 4 mẫu, không hơn đoán bừa. Đúng như chứng minh: không có bộ tham số nào để hội tụ tới.

Ô thứ hai là mạng 2-2-1 giải XOR với trọng số đặt tay, dạng sigmoid: lớp ẩn có W[1]=[6666]\mathbf{W}^{[1]} = \begin{bmatrix} 6 & 6 \\ 6 & 6 \end{bmatrix}, b[1]=[−3,−9]⊤\mathbf{b}^{[1]} = [-3, -9]^\top; lớp ra có W[2]=[10,−10]\mathbf{W}^{[2]} = [10, -10], b[2]=−5b^{[2]} = -5. Bốn mẫu được xếp thành bốn cột của A[0]\mathbf{A}^{[0]}, và lan truyền xuôi chạy đúng hai dòng Z=WA+b\mathbf{Z} = \mathbf{W}\mathbf{A} + \mathbf{b}, A=σ(Z)\mathbf{A} = \sigma(\mathbf{Z}) cho mỗi lớp. Phần cuối bỏ hàm kích hoạt và kiểm tra rằng hai lớp tuyến tính bằng đúng một lớp W[2]W[1]\mathbf{W}^{[2]}\mathbf{W}^{[1]}.

MLP 2-2-1 giải XOR, lan truyền xuôi dạng ma trận

import numpy as np
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# Bốn mẫu XOR, xếp thành CỘT: A0 có 2 hàng (đặc trưng), 4 cột (mẫu).
A0 = np.array([[0, 0, 1, 1],
[0, 1, 0, 1]])
y = np.array([0, 1, 1, 0])
# Lớp ẩn: 2 nơ-ron. Hàng 1 gần với OR, hàng 2 gần với AND.
W1 = np.array([[6.0, 6.0],
[6.0, 6.0]])
b1 = np.array([[-3.0],
[-9.0]]) # vectơ cột, cộng vào mọi cột của Z1
# Lớp ra: 1 nơ-ron, tính "OR và không AND".
W2 = np.array([[10.0, -10.0]])
b2 = np.array([[-5.0]])
# Lan truyền xuôi: z = W a + b, rồi a = g(z), lớp nối lớp.
Z1 = W1 @ A0 + b1
A1 = sigmoid(Z1)
Z2 = W2 @ A1 + b2
A2 = sigmoid(Z2) # xác suất lớp 1, shape (1, 4)
print("hidden A1:\n", A1.round(3))
print("output A2:", A2.round(3), " target:", y)
# Bỏ hàm kích hoạt (g(z) = z): hai lớp gộp lại thành một lớp tuyến tính.
W = W2 @ W1
b = W2 @ b1 + b2
two_layers = W2 @ (W1 @ A0 + b1) + b2
one_layer = W @ A0 + b
print("linear W =", W, " b =", b.ravel())
print("same output:", np.allclose(two_layers, one_layer))

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đầu ra là [0.011,0.983,0.983,0.011][0.011, 0.983, 0.983, 0.011]: lấy ngưỡng 0.5 thì khớp XOR. Hàng đầu của A1 gần với OR, hàng thứ hai gần với AND, như thiết kế. Phần bỏ hàm kích hoạt in same output: True: hai lớp tuyến tính cho đúng kết quả của một lớp với W=[0,0]\mathbf{W} = [0, 0] và b=55b = 55. Ở đây W\mathbf{W} bằng 0 vì hai hàng của W[1]\mathbf{W}^{[1]} giống nhau; với trọng số khác thì W\mathbf{W} khác 0, nhưng vẫn chỉ là một lớp tuyến tính, nên vẫn không giải được XOR.

Ô thứ ba huấn luyện một MLP 64-64-10 có ReLU trên bộ chữ số viết tay load_digits của scikit-learn: 1797 ảnh xám 8×8, tức 64 đầu vào, nhãn từ 0 tới 9. Mạng được viết bằng nn.Sequential: nn.Linear(64, 64) là một lớp W[1]a+b[1]\mathbf{W}^{[1]}\mathbf{a} + \mathbf{b}^{[1]}, nn.ReLU() là gg. Hàm mất mát nn.CrossEntropyLoss nhận thẳng logit, và bộ tối ưu là gradient descent mini-batch (torch.optim.SGD, cỡ batch 32, η=0.2\eta = 0.2, 30 epoch). Dòng loss.backward() tính gradient của L\mathcal{L} theo mọi tham số: tạm coi nó là một hộp đen, bài sau sẽ mở hộp này ra.

Chạy trên máy bạn

MLP bằng PyTorch đọc chữ số viết tay

Cần cài
pip install torch scikit-learn
Phần cứng
CPU là đủ, không cần GPU. Khoảng 30 giây tới 3 phút trên máy phát triển, tùy máy đang bận cỡ nào
import torch
from torch import nn
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
torch.manual_seed(0)
# 1797 ảnh chữ số viết tay 8x8, mỗi điểm ảnh từ 0 tới 16.
X, y = load_digits(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.25, random_state=0, stratify=y)
X_tr = torch.tensor(X_tr / 16, dtype=torch.float32) # đưa về [0, 1]
X_te = torch.tensor(X_te / 16, dtype=torch.float32)
y_tr = torch.tensor(y_tr)
y_te = torch.tensor(y_te)
# MLP 64 -> 64 -> 10. Lớp ra trả về logit, KHÔNG có softmax:
# nn.CrossEntropyLoss tự áp softmax rồi lấy -log xác suất lớp đúng.
model = nn.Sequential(
nn.Linear(64, 64),
nn.ReLU(),
nn.Linear(64, 10),
)
n_params = sum(p.numel() for p in model.parameters())
print(f"parameters: {n_params}")
loss_fn = nn.CrossEntropyLoss()
opt = torch.optim.SGD(model.parameters(), lr=0.2) # mini-batch GD
for epoch in range(1, 31):
perm = torch.randperm(len(X_tr)) # xáo trộn mỗi epoch
for i in range(0, len(X_tr), 32): # mini-batch cỡ 32
idx = perm[i:i + 32]
logits = model(X_tr[idx]) # lan truyền xuôi
loss = loss_fn(logits, y_tr[idx])
opt.zero_grad()
loss.backward() # tính gradient: bài sau giải thích
opt.step() # w <- w - lr * gradient
if epoch in (1, 5, 10, 20, 30):
with torch.no_grad():
full = loss_fn(model(X_tr), y_tr).item()
print(f"epoch {epoch:2d} train loss {full:.4f}")
with torch.no_grad():
pred = model(X_te).argmax(dim=1) # lớp có logit lớn nhất
acc = (pred == y_te).float().mean().item()
print(f"test accuracy: {acc:.4f} ({len(y_te)} images)")

Kết quả mẫu

Lấy từ một lần chạy thật.

parameters: 4810
epoch 1 train loss 1.4863
epoch 5 train loss 0.2110
epoch 10 train loss 0.2626
epoch 20 train loss 0.0631
epoch 30 train loss 0.0416
test accuracy: 0.9756 (450 images)

Đọc kết quả:

  • Số tham số 4810, khớp phép đếm 64×65+10×6564 \times 65 + 10 \times 65 ở mục Lý thuyết.
  • Loss trên tập huấn luyện đi từ 1.4863 sau epoch đầu xuống 0.0416 sau epoch 30. Mốc so sánh: mạng chưa học, đoán đều 10 lớp, có loss ln⁡10≈2.303\ln 10 \approx 2.303. Loss ở epoch 10 (0.2626) cao hơn ở epoch 5 (0.2110): mỗi bước chỉ dùng 32 mẫu nên đường đi có nhiễu, như SGD ở bài Gradient Descent.
  • Độ chính xác trên tập kiểm tra là 0.9756, tức 439 trên 450 ảnh chưa thấy lúc huấn luyện. Để so, hồi quy softmax (LogisticRegression của scikit-learn) trên cùng cách chia đạt 0.9689 trong lần chạy thử của bài: bộ dữ liệu nhỏ và dễ, nên lớp ẩn chỉ giúp thêm một chút.

Seed cố định nên mỗi lần chạy trên cùng máy cho cùng kết quả; trên máy khác hoặc bản PyTorch khác, các số có thể lệch đôi chút. Hãy thử đổi nn.ReLU() thành nn.Tanh() hoặc nn.GELU(), hoặc đổi số nơ-ron ẩn, rồi chạy lại.

Câu 1/5 Bạn xếp ba lớp nn.Linear liên tiếp, 10 → 50 → 50 → 1, không có hàm kích hoạt nào ở giữa. Mạng này biểu diễn được loại hàm nào của x\mathbf{x}?
Câu 2/5 Bạn chạy luật học perceptron trên 100 điểm thuộc hai lớp, xuất phát từ w=0\mathbf{w} = \mathbf{0}, b=0b = 0. Sau 1000 epoch, số mẫu đoán sai trong mỗi epoch vẫn chưa lần nào về 0. Kết luận hợp lý nhất là gì?
Câu 3/5 Một lớp ẩn có 32 nơ-ron, nhận đầu vào từ lớp trước có 64 nơ-ron. Theo quy ước z[l]=W[l]a[l−1]+b[l]\mathbf{z}^{[l]} = \mathbf{W}^{[l]}\mathbf{a}^{[l-1]} + \mathbf{b}^{[l]}, W[l]\mathbf{W}^{[l]} có kích thước nào, và lớp này có bao nhiêu tham số?
Câu 4/5 Bài toán phân loại 3 lớp, lớp ra cho logit z=[2.0,1.0,0.1]⊤\mathbf{z} = [2.0, 1.0, 0.1]^\top. Nếu cộng 5 vào cả ba logit, xác suất softmax và cross-entropy thay đổi thế nào?
Câu 5/5 Định lý xấp xỉ phổ quát đảm bảo điều gì?

Ý chính:

  • Một nơ-ron tính z=w⊤x+bz = \mathbf{w}^\top\mathbf{x} + b rồi a=g(z)a = g(z). Perceptron dùng hàm bậc thang và luật học w←w+η(y−y^)x\mathbf{w} \leftarrow \mathbf{w} + \eta(y - \hat{y})\mathbf{x}; luật này chỉ hội tụ khi dữ liệu tách được tuyến tính, nên không học được XOR.
  • MLP xếp nơ-ron thành các lớp kết nối đầy đủ. Lan truyền xuôi: a[0]=x\mathbf{a}^{[0]} = \mathbf{x}, rồi z[l]=W[l]a[l−1]+b[l]\mathbf{z}^{[l]} = \mathbf{W}^{[l]}\mathbf{a}^{[l-1]} + \mathbf{b}^{[l]}, a[l]=g[l](z[l])\mathbf{a}^{[l]} = g^{[l]}(\mathbf{z}^{[l]}), với W[l]\mathbf{W}^{[l]} kích thước n[l]×n[l−1]n^{[l]} \times n^{[l-1]}. Lớp ẩn học ra cách biểu diễn mới, trong đó bài toán dễ hơn.
  • Hàm kích hoạt phải phi tuyến, nếu không cả mạng chỉ là một lớp tuyến tính. ReLU là mặc định cho lớp ẩn; sigmoid và tanh bão hòa; Leaky ReLU tránh nơ-ron chết; GELU dùng trong Transformer.
  • Phân loại KK lớp dùng KK nơ-ron ra, softmax và cross-entropy −ln⁡y^c-\ln\hat{y}_c. Định lý xấp xỉ phổ quát nói MLP một lớp ẩn đủ rộng xấp xỉ được mọi hàm liên tục, nhưng không nói cách tìm trọng số: đó là việc của lan truyền ngược, ở bài sau.

Đọc thêm:

Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu