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

Hồi quy Softmax

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

  • Hiểu vì sao ghép nhiều bộ phân loại nhị phân (one-vs-rest) chưa cho xác suất đúng nghĩa, và hồi quy softmax (softmax regression) giải quyết điều đó thế nào.
  • Tính được hàm softmax bằng tay, giải thích được vì sao nó dùng hàm mũ, và cài đặt được bản ổn định số bằng mẹo trừ logit lớn nhất.
  • Tự dẫn được hàm mất mát cross-entropy với nhãn one-hot và gradient y^−y\hat{\mathbf{y}} - \mathbf{y}, rồi huấn luyện mô hình bằng gradient descent.
  • Nhận ra ranh giới quyết định (decision boundary) của hồi quy softmax là tuyến tính từng đoạn, và biết khi nào không nên dùng softmax.

Bạn có một bông hoa diên vĩ (iris) và đo được cánh hoa (petal) dài 4.9 cm, rộng 1.5 cm. Nó thuộc loài nào trong ba loài: setosa, versicolor hay virginica? Đây là bộ dữ liệu iris, do nhà thống kê Ronald Fisher công bố năm 1936: 150 bông hoa, mỗi loài 50 bông. Bài này chỉ dùng hai số đo cánh hoa, để vẽ được lên mặt phẳng. Bông đầu tiên của mỗi loài trong bộ dữ liệu trông như sau:

LoàiDài cánh hoa (cm)Rộng cánh hoa (cm)
setosa1.40.2
versicolor4.71.4
virginica6.02.5

Bông hoa của bạn nằm giữa versicolor và virginica, nên câu trả lời tốt không chỉ là một tên loài, mà là ba xác suất cộng lại bằng 1, ví dụ “64% versicolor, 35% virginica”.

Cách thứ nhất: ghép ba bộ phân loại nhị phân

Phần tiêu đề “Cách thứ nhất: ghép ba bộ phân loại nhị phân”

Bạn đã biết hồi quy logistic cho hai lớp. Cách đơn giản nhất là huấn luyện ba mô hình riêng, mỗi mô hình trả lời một câu hỏi có/không: “có phải setosa không?”, “có phải versicolor không?”, “có phải virginica không?”. Cách này gọi là one-vs-rest (OvR, một lớp với phần còn lại). Huấn luyện ba mô hình đó bằng LogisticRegression trên 105 bông hoa (70% dữ liệu, đã chuẩn hóa), rồi hỏi về bông hoa của bạn:

Câu hỏiXác suất “có”
Có phải setosa?0.008
Có phải versicolor?0.425
Có phải virginica?0.350
Tổng0.783

Hai điều không ổn:

  • Tổng không bằng 1. Mỗi mô hình được huấn luyện riêng, không mô hình nào biết hai mô hình kia nói gì. Trên cả 150 bông của bộ dữ liệu, tổng này dao động từ 0.405 tới 1.539.
  • Cả ba mô hình đều nói “không”, vì không xác suất nào vượt 0.5. Lấy lớp có xác suất lớn nhất vẫn ra versicolor, nhưng mô hình “có phải versicolor?” gặp khó thật sự: versicolor nằm giữa hai loài kia, nên một đường thẳng không tách được nó khỏi “phần còn lại”.

Thay vì ba câu hỏi rời, hãy cho một mô hình chấm cho mỗi loài một điểm, rồi so các điểm với nhau. Giả sử mô hình chấm cho bông hoa của bạn ba điểm, gọi là logit: setosa −3.0-3.0, versicolor 1.81.8, virginica 1.21.2. Điểm càng cao, loài đó càng hợp.

Đổi ba điểm này thành xác suất thế nào? Chia mỗi điểm cho tổng thì không được: có điểm âm, và tổng −3.0+1.8+1.2-3.0 + 1.8 + 1.2 lại bằng đúng 0. Cách làm của softmax là lấy ee mũ từng điểm trước (kết quả luôn dương), rồi mới chia cho tổng:

LoàiLogit zkz_kezke^{z_k}Xác suất
setosa−3.00.04980.0053
versicolor1.86.04960.6422
virginica1.23.32010.3525
Tổng9.41951.0000

Ví dụ 0.6422=6.0496/9.41950.6422 = 6.0496 / 9.4195. Ba xác suất cộng lại bằng 1, giữ đúng thứ tự của các điểm, và versicolor thắng.

Bài toán của bài này là: cho mỗi lớp một vectơ trọng số để chấm điểm, đổi các điểm thành xác suất cộng lại bằng 1, rồi học các trọng số sao cho lớp đúng của mỗi mẫu nhận xác suất cao.

Có KK lớp, đánh số k=1,…,Kk = 1, \dots, K (với iris, K=3K = 3). Mỗi mẫu có nn đặc trưng x=[x1,…,xn]⊤\mathbf{x} = [x_1, \dots, x_n]^\top và một nhãn là một trong KK lớp. Đây là phân loại nhiều lớp (multiclass classification): mỗi mẫu thuộc đúng một lớp.

Hồi quy softmax cho mỗi lớp kk một vectơ trọng số wk\mathbf{w}_k (có nn phần tử) và một hệ số tự do bkb_k. Điểm của lớp kk là logit:

zk=wk⊤x+bk,k=1,…,Kz_k = \mathbf{w}_k^\top\mathbf{x} + b_k, \qquad k = 1, \dots, K

Mỗi logit là một mô hình tuyến tính như ở bài Hồi quy tuyến tính, chỉ khác là có KK mô hình chạy song song trên cùng một x\mathbf{x}. Với iris dùng 2 đặc trưng, mô hình có 3×2=63 \times 2 = 6 trọng số và 3 hệ số tự do. Gom KK logit thành vectơ z=[z1,…,zK]⊤\mathbf{z} = [z_1, \dots, z_K]^\top.

Softmax đổi vectơ logit thành vectơ xác suất y^=[y^1,…,y^K]⊤\hat{\mathbf{y}} = [\hat{y}_1, \dots, \hat{y}_K]^\top:

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

Tử số là mũ của logit lớp kk; mẫu số là tổng mũ của mọi logit, giống nhau cho mọi lớp. Đọc y^k\hat{y}_k là “xác suất mô hình gán cho lớp kk”. Lớp dự đoán là lớp có y^k\hat{y}_k lớn nhất.

Vì sao lại là hàm mũ? Vì nó cho đúng ba tính chất cần có:

  1. Luôn dương. ez>0e^{z} > 0 với mọi zz, kể cả zz âm, nên mọi xác suất đều dương, và chia cho tổng là ra các số cộng lại bằng 1.
  2. Giữ thứ tự. eze^{z} đồng biến: logit lớn hơn thì xác suất lớn hơn. Lớp có xác suất lớn nhất cũng chính là lớp có logit lớn nhất.
  3. Khuếch đại khác biệt. Chia hai xác suất cho nhau thì mẫu số triệt tiêu, rồi dùng ea/ec=ea−ce^{a}/e^{c} = e^{a - c}:
y^iy^j=eziezj=ezi−zj\frac{\hat{y}_i}{\hat{y}_j} = \frac{e^{z_i}}{e^{z_j}} = e^{z_i - z_j}

Hai logit chênh nhau 1 thì xác suất chênh nhau e≈2.72e \approx 2.72 lần; chênh nhau 2 thì e2≈7.39e^2 \approx 7.39 lần. Trong ví dụ hoa iris, versicolor hơn virginica 0.60.6 điểm, nên 0.6422/0.3525≈e0.6≈1.820.6422 / 0.3525 \approx e^{0.6} \approx 1.82.

Cái tên “softmax” đến từ chỗ nó là một bản “mềm” của việc chọn giá trị lớn nhất (max, hay đúng hơn là argmax): lớp có logit lớn nhất nhận phần lớn xác suất, nhưng các lớp khác vẫn được một phần, và phần đó thay đổi trơn khi logit thay đổi. Tính trơn này là thứ cho phép lấy đạo hàm để huấn luyện.

Cộng hằng số không đổi gì, và mẹo trừ max

Phần tiêu đề “Cộng hằng số không đổi gì, và mẹo trừ max”

Công thức tỉ số ở trên cho thấy chỉ hiệu giữa các logit là quan trọng. Kiểm tra trực tiếp: cộng cùng một số cc vào mọi logit, rồi tách ezk+c=ecezke^{z_k + c} = e^{c}e^{z_k}:

softmax(z+c)k=ecezk∑jecezj=ecezkec∑jezj=softmax(z)k\begin{aligned} \mathrm{softmax}(\mathbf{z} + c)_k &= \frac{e^{c}e^{z_k}}{\sum_{j} e^{c}e^{z_j}} \\ &= \frac{e^{c}e^{z_k}}{e^{c}\sum_{j} e^{z_j}} = \mathrm{softmax}(\mathbf{z})_k \end{aligned}

Dòng thứ hai đặt ece^{c} ra ngoài tổng ở mẫu số, rồi giản ước với ece^{c} ở tử số.

Tính chất này cứu máy tính khỏi tràn số (overflow). Số thực 64 bit lớn nhất khoảng 1.8×103081.8 \times 10^{308}, tức chỉ chứa được eze^{z} khi z<709.78z < 709.78. Nếu logit là (997,1001.8,1001.2)(997, 1001.8, 1001.2), máy tính e1001.8e^{1001.8} ra vô cực, rồi vô cực chia vô cực ra nan (not a number). Mẹo: trừ logit lớn nhất khỏi mọi logit trước khi lấy mũ. Logit thành (−4.8,0,−0.6)(-4.8, 0, -0.6), mũ lớn nhất là e0=1e^{0} = 1, không thể tràn, và theo đẳng thức trên, kết quả vẫn y hệt: đó cũng chính là logit của bông hoa iris cộng thêm 1000, nên xác suất vẫn là (0.0053,0.6422,0.3525)(0.0053, 0.6422, 0.3525). Mọi thư viện đều cài softmax theo cách này. Ô code thứ nhất thử cả hai cách.

Đặt K=2K = 2. Chia cả tử và mẫu của y^1\hat{y}_1 cho ez1e^{z_1}:

y^1=ez1ez1+ez2=11+ez2−z1=11+e−(z1−z2)=σ(z1−z2)\begin{aligned} \hat{y}_1 &= \frac{e^{z_1}}{e^{z_1} + e^{z_2}} \\ &= \frac{1}{1 + e^{z_2 - z_1}} = \frac{1}{1 + e^{-(z_1 - z_2)}} = \sigma(z_1 - z_2) \end{aligned}

Vậy hồi quy softmax hai lớp là hồi quy logistic với điểm z=z1−z2=(w1−w2)⊤x+(b1−b2)z = z_1 - z_2 = (\mathbf{w}_1 - \mathbf{w}_2)^\top\mathbf{x} + (b_1 - b_2), tức w=w1−w2\mathbf{w} = \mathbf{w}_1 - \mathbf{w}_2 và b=b1−b2b = b_1 - b_2. Ví dụ, logit (1.8,1.2)(1.8, 1.2) cho y^1=σ(0.6)≈0.6457\hat{y}_1 = \sigma(0.6) \approx 0.6457. Hồi quy logistic là trường hợp riêng của hồi quy softmax, nên tên chính thức của hồi quy softmax là hồi quy logistic đa thức (multinomial logistic regression).

Phép trừ cũng cho thấy một điều: hai bộ tham số khác nhau có thể cho cùng một mô hình. Cộng cùng một vectơ vào w1\mathbf{w}_1 và w2\mathbf{w}_2 thì hiệu không đổi. Mục huấn luyện ở dưới sẽ quay lại điểm này.

Để so y^\hat{\mathbf{y}} với nhãn thật, viết nhãn cũng thành một vectơ KK phần tử: phần tử của lớp đúng bằng 1, còn lại bằng 0. Cách viết này gọi là mã hóa one-hot (one-hot encoding). Với iris:

Nhãny\mathbf{y}
setosa(1,0,0)(1, 0, 0)
versicolor(0,1,0)(0, 1, 0)
virginica(0,0,1)(0, 0, 1)

Không dùng thẳng nhãn số 0, 1, 2, vì như vậy là ngầm nói virginica “lớn hơn” versicolor và “xa” setosa hơn, điều không có nghĩa với tên loài.

Hàm mất mát của một mẫu là cross-entropy:

ℓ=−∑k=1Kykln⁡y^k\ell = -\sum_{k=1}^{K} y_k \ln\hat{y}_k

Ở đây ln⁡\ln là logarit tự nhiên. Vì y\mathbf{y} là one-hot, mọi số hạng có yk=0y_k = 0 đều biến mất, chỉ còn số hạng của lớp đúng. Gọi lớp đúng là cc:

ℓ=−ln⁡y^c\ell = -\ln\hat{y}_c

Hàm mất mát chỉ đơn giản là trừ logarit của xác suất mà mô hình gán cho lớp đúng. Với bông hoa iris là versicolor: ℓ=−ln⁡0.6422≈0.443\ell = -\ln 0.6422 \approx 0.443. Nếu nó thật ra là virginica thì ℓ=−ln⁡0.3525≈1.043\ell = -\ln 0.3525 \approx 1.043, còn nếu là setosa thì ℓ=−ln⁡0.0053≈5.24\ell = -\ln 0.0053 \approx 5.24: gán xác suất gần 0 cho lớp đúng bị phạt rất nặng. Với K=2K = 2, công thức này chính là log loss của hồi quy logistic, viết gọn lại.

Trên mm mẫu huấn luyện, hàm mất mát là trung bình:

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

Vì sao chọn cross-entropy mà không chọn MSE? Cũng như log loss, làm nhỏ L\mathcal{L} tương đương với làm lớn xác suất mà mô hình gán cho toàn bộ nhãn của tập huấn luyện (cực đại likelihood), và nó cho gradient rất gọn, như mục dưới.

Một mốc để nhớ: lúc mọi tham số bằng 0, mọi logit bằng 0, softmax chia đều 1/K1/K cho mỗi lớp, nên L=−ln⁡(1/K)=ln⁡K\mathcal{L} = -\ln(1/K) = \ln K. Với 3 lớp, đó là ln⁡3≈1.0986\ln 3 \approx 1.0986. Mô hình đã học được gì đó thì L\mathcal{L} phải nhỏ hơn số này.

Gradient: y^−y\hat{\mathbf{y}} - \mathbf{y}

Phần tiêu đề “Gradient: y^−y\hat{\mathbf{y}} - \mathbf{y}y^​−y”

Cần đạo hàm của ℓ\ell theo từng logit zjz_j. Cách gọn nhất là viết ℓ\ell thẳng theo logit.

Bước 1: viết ℓ\ell theo z\mathbf{z}. Thay định nghĩa softmax vào ℓ=−ln⁡y^c\ell = -\ln\hat{y}_c, rồi dùng ln⁡(a/d)=ln⁡a−ln⁡d\ln(a/d) = \ln a - \ln d và ln⁡ezc=zc\ln e^{z_c} = z_c:

ℓ=−ln⁡ezc∑kezk=−zc+ln⁡∑k=1Kezk\ell = -\ln\frac{e^{z_c}}{\sum_{k} e^{z_k}} = -z_c + \ln\sum_{k=1}^{K} e^{z_k}

Bước 2: lấy đạo hàm theo zjz_j. Số hạng đầu −zc-z_c có đạo hàm −1-1 nếu j=cj = c, và 0 nếu j≠cj \ne c. Đó chính là −yj-y_j, vì yj=1y_j = 1 đúng khi j=cj = c. Số hạng thứ hai có dạng ln⁡u\ln u với u=∑kezku = \sum_k e^{z_k}. Đạo hàm của ln⁡u\ln u là 1u\frac{1}{u} nhân đạo hàm của uu, và trong tổng uu chỉ số hạng ezje^{z_j} phụ thuộc zjz_j:

∂∂zjln⁡∑kezk=ezj∑kezk=y^j\frac{\partial}{\partial z_j}\ln\sum_{k} e^{z_k} = \frac{e^{z_j}}{\sum_{k} e^{z_k}} = \hat{y}_j

Bước 3: cộng lại.

∂ℓ∂zj=y^j−yj,tức laˋ∂ℓ∂z=y^−y\frac{\partial\ell}{\partial z_j} = \hat{y}_j - y_j, \qquad\text{tức là}\qquad \frac{\partial\ell}{\partial\mathbf{z}} = \hat{\mathbf{y}} - \mathbf{y}

Đọc bằng lời: gradient theo logit là xác suất dự đoán trừ nhãn one-hot. Với bông hoa iris (lớp đúng versicolor): y^−y=(0.0053,0.6422−1,0.3525)=(0.0053,−0.3578,0.3525)\hat{\mathbf{y}} - \mathbf{y} = (0.0053, 0.6422 - 1, 0.3525) = (0.0053, -0.3578, 0.3525). Gradient descent đi ngược gradient, nên nó tăng logit của lớp đúng và giảm logit của các lớp sai, lớp sai nào đang được nhiều xác suất (virginica) thì bị giảm mạnh hơn. Các thành phần luôn cộng lại bằng 0, vì cả y^\hat{\mathbf{y}} lẫn y\mathbf{y} đều cộng lại bằng 1.

Bước 4: đi tiếp tới trọng số. Logit zj=wj⊤x+bjz_j = \mathbf{w}_j^\top\mathbf{x} + b_j thay đổi theo wj\mathbf{w}_j với tốc độ x\mathbf{x}, và theo bjb_j với tốc độ 1. Theo quy tắc hàm hợp (chain rule), nhân gradient theo logit với hai tốc độ này:

∂ℓ∂wj=(y^j−yj)x,∂ℓ∂bj=y^j−yj\frac{\partial\ell}{\partial\mathbf{w}_j} = \left(\hat{y}_j - y_j\right)\mathbf{x}, \qquad \frac{\partial\ell}{\partial b_j} = \hat{y}_j - y_j

Đúng dạng (y^−y) x(\hat{y} - y)\,\mathbf{x} của hồi quy logistic, nay lặp lại cho mỗi lớp.

Viết cho cả tập huấn luyện. Trong bài này, X\mathbf{X} chỉ gồm các đặc trưng (mm hàng, nn cột, không có cột số 1), còn hệ số tự do viết riêng. Xếp các wk\mathbf{w}_k thành các cột của ma trận W\mathbf{W} (nn hàng, KK cột) và các bkb_k thành vectơ b\mathbf{b}. Khi đó:

  • Z=XW+1b⊤\mathbf{Z} = \mathbf{X}\mathbf{W} + \mathbf{1}\mathbf{b}^\top là ma trận logit, mm hàng KK cột: hàng ii là z(i)\mathbf{z}^{(i)}. Số hạng 1b⊤\mathbf{1}\mathbf{b}^\top chỉ có nghĩa là cộng b\mathbf{b} vào mọi hàng.
  • Y^\hat{\mathbf{Y}} là softmax của từng hàng của Z\mathbf{Z}; Y\mathbf{Y} là ma trận nhãn one-hot, cùng kích thước.

Lấy trung bình gradient của mm mẫu (mỗi mẫu là một hàng của Y^−Y\hat{\mathbf{Y}} - \mathbf{Y}, nhân với x(i)\mathbf{x}^{(i)}) được:

∇WL=1mX⊤(Y^−Y),∇bL=1m∑i=1m(y^(i)−y(i))\nabla_{\mathbf{W}}\mathcal{L} = \frac{1}{m}\mathbf{X}^\top\left(\hat{\mathbf{Y}} - \mathbf{Y}\right), \qquad \nabla_{\mathbf{b}}\mathcal{L} = \frac{1}{m}\sum_{i=1}^{m}\left(\hat{\mathbf{y}}^{(i)} - \mathbf{y}^{(i)}\right)

So với MSE ở bài Gradient Descent, 2mX⊤(Xθ−y)\frac{2}{m}\mathbf{X}^\top(\mathbf{X}\boldsymbol{\theta} - \mathbf{y}): cùng hình dạng “đặc trưng nhân sai lệch”. Quy tắc cập nhật vẫn là W←W−η∇WL\mathbf{W} \leftarrow \mathbf{W} - \eta\nabla_{\mathbf{W}}\mathcal{L}, b←b−η∇bL\mathbf{b} \leftarrow \mathbf{b} - \eta\nabla_{\mathbf{b}}\mathcal{L}. Ô code thứ hai cài đặt đúng hai dòng này.

Ba điều cần biết khi huấn luyện:

  • Không có công thức nghiệm, như hồi quy logistic, nên phải lặp. Tin tốt: L\mathcal{L} là hàm lồi theo (W,b)(\mathbf{W}, \mathbf{b}), không có hố phụ.
  • Nghiệm không duy nhất. Cộng cùng một vectơ v\mathbf{v} vào mọi wk\mathbf{w}_k (và cùng một số vào mọi bkb_k) thì mọi logit cộng thêm v⊤x\mathbf{v}^\top\mathbf{x}, mà softmax bất biến với việc cộng cùng một số. Đáy của L\mathcal{L} là cả một “rãnh” chứ không phải một điểm. Regularization (bài Ridge, Lasso, Elastic Net) chọn ra điểm có trọng số nhỏ nhất trong rãnh; LogisticRegression của scikit-learn có phạt L2 bật sẵn (C=1.0).
  • Lớp tách được hoàn toàn thì trọng số phình mãi. Setosa tách hẳn khỏi hai loài kia bằng một đường thẳng. Không có regularization, mô hình có thể làm xác suất của setosa gần 1 hơn nữa bằng cách tăng trọng số, nên L\mathcal{L} cứ giảm chậm mãi mà không dừng. Ô code thứ hai cho thấy điều này.

Từ 3 lớp trở lên, LogisticRegression của scikit-learn (bộ giải mặc định lbfgs) huấn luyện đúng mô hình softmax này. Tham số multi_class trong các hướng dẫn cũ đã bị bỏ ở các bản scikit-learn mới, đừng truyền nó.

Mô hình dự đoán lớp có y^k\hat{y}_k lớn nhất, tức lớp có logit zkz_k lớn nhất (softmax giữ thứ tự). Ranh giới giữa lớp kk và lớp jj là nơi hai logit bằng nhau:

zk=zj  ⟺  (wk−wj)⊤x+(bk−bj)=0z_k = z_j \iff \left(\mathbf{w}_k - \mathbf{w}_j\right)^\top\mathbf{x} + \left(b_k - b_j\right) = 0

Đây là một phương trình tuyến tính theo x\mathbf{x}: với 2 đặc trưng, nó là một đường thẳng. Vùng của mỗi lớp là nơi logit của nó thắng mọi logit khác, tức giao của các nửa mặt phẳng, nên là một vùng lồi. Ghép các vùng lại, ranh giới giữa chúng là tuyến tính từng đoạn (piecewise linear): các đoạn thẳng gặp nhau tại những điểm mà ba logit bằng nhau. Ô code thứ ba vẽ ranh giới này cho iris.

Lớp cuối của hầu hết mạng nơ-ron phân loại chính là một hồi quy softmax: các lớp phía trước biến đổi x\mathbf{x} thành đặc trưng mới, còn lớp cuối tính KK logit, áp softmax, và huấn luyện bằng cross-entropy với gradient y^−y\hat{\mathbf{y}} - \mathbf{y} như trên. Mô hình ngôn ngữ lớn cũng vậy, với KK là số token trong từ vựng (hàng chục nghìn): mỗi bước, nó cho một logit cho mỗi token rồi softmax chọn token kế tiếp. Tham số temperature mà bạn chỉnh khi gọi API chỉ là chia mọi logit cho một số TT trước softmax, như bài Sử dụng API văn bản. Mục Minh họa cho thấy việc co giãn logit làm gì.

  • Ranh giới luôn tuyến tính từng đoạn. Lớp có hình dạng phức tạp (ví dụ một lớp nằm trong vòng tròn của lớp khác) cần đặc trưng mới, hoặc mô hình khác như bài Decision Tree và Random Forest hay mạng nơ-ron.
  • Mỗi mẫu đúng một lớp. Bài toán một mẫu có nhiều nhãn cùng lúc không dùng softmax (hộp thứ hai ở mục Lỗi hay gặp).
  • Xác suất chưa chắc đáng tin. Hai mô hình có cùng độ chính xác vẫn có thể cho xác suất khác nhau khá xa (hộp thứ ba ở mục Lỗi hay gặp).

Bảng dưới lấy logit của bông hoa iris, z=(−3.0,1.8,1.2)\mathbf{z} = (-3.0, 1.8, 1.2), nhân cả ba với cùng một số cc, rồi tính softmax. Nhân với cc cũng là chia cho nhiệt độ T=1/cT = 1/c.

Logit dùngsetosaversicolorvirginica
0⋅z=(0,0,0)0 \cdot \mathbf{z} = (0, 0, 0)0.33330.33330.3333
0.5 z0.5\,\mathbf{z}0.04950.54600.4045
1⋅z1 \cdot \mathbf{z} (gốc)0.00530.64220.3525
z+5\mathbf{z} + 50.00530.64220.3525
2 z2\,\mathbf{z}0.00010.76850.2315
10 z10\,\mathbf{z}0.00000.99750.0025

Đọc bảng từ trên xuống:

  • Logit bằng nhau thì xác suất chia đều. Hàng đầu là mô hình lúc chưa học gì: mỗi lớp 1/31/3, và cross-entropy là ln⁡3\ln 3.
  • Cộng thêm một hằng số không đổi gì. Hàng z+5\mathbf{z} + 5 giống hệt hàng gốc: đây là tính chất cho phép mẹo trừ max.
  • Nhân với số lớn làm phân phối nhọn. Thứ tự ba lớp không bao giờ đổi, nhưng với 10 z10\,\mathbf{z}, versicolor nhận gần hết xác suất: softmax tiến tới argmax, chọn hẳn một lớp. Nhân với số nhỏ thì ngược lại, phân phối phẳng dần về 1/31/3.

Điều này cũng giải thích vì sao trọng số phình to làm mô hình “tự tin” hơn: nhân mọi wk\mathbf{w}_k, bkb_k với 2 thì mọi logit nhân với 2, ranh giới không đổi chỗ, nhưng xác suất bị đẩy về gần 0 và 1.

Ba ô dưới đây chạy độc lập. Ô thứ nhất chỉ dùng NumPy. Ô thứ hai và thứ ba lần đầu chạy lâu hơn, vì trình duyệt phải tải thêm scikit-learn (và Matplotlib cho ô thứ ba).

Ô đầu tiên cài đặt softmax theo hai cách, thử với logit của bông hoa iris, với logit cộng thêm 1000, và kiểm tra trường hợp K=2K = 2.

Softmax ổn định số bằng NumPy

import numpy as np
def softmax_naive(z):
# Cách ngây thơ: lấy mũ thẳng từng logit.
e = np.exp(z)
return e / e.sum()
def softmax(z):
# Trừ logit lớn nhất trước: kết quả không đổi, mũ lớn nhất là e^0 = 1.
e = np.exp(z - z.max())
return e / e.sum()
# Ba logit của một bông hoa: setosa, versicolor, virginica.
z = np.array([-3.0, 1.8, 1.2])
p = softmax(z)
print("p =", p.round(4), f" sum = {p.sum():.4f}")
# Cộng cùng một hằng số vào mọi logit: xác suất giữ nguyên.
print("z + 5 =", softmax(z + 5).round(4))
# Logit lớn: e^1001.8 vượt quá số thực lớn nhất (khoảng 1.8e308).
big = z + 1000
with np.errstate(over="ignore", invalid="ignore"):
print("naive =", softmax_naive(big))
print("stable =", softmax(big).round(4))
# K = 2: softmax của (z1, z2) bằng sigmoid của z1 - z2.
z2 = np.array([1.8, 1.2])
sigmoid = 1 / (1 + np.exp(-(z2[0] - z2[1])))
print("K=2 =", softmax(z2).round(4), f" sigmoid = {sigmoid:.4f}")

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

  • Logit (−3.0,1.8,1.2)(-3.0, 1.8, 1.2) cho (0.0053,0.6422,0.3525)(0.0053, 0.6422, 0.3525), tổng bằng 1, đúng như bảng ở mục Trực giác. Cộng 5 vào mọi logit không đổi gì.
  • Cộng 1000: cách ngây thơ cho [nan nan nan] vì e1001.8e^{1001.8} tràn thành vô cực. Cách trừ max cho đúng xác suất cũ.
  • Dòng cuối: softmax của (1.8,1.2)(1.8, 1.2) cho lớp đầu 0.64570.6457, đúng bằng σ(1.8−1.2)=σ(0.6)\sigma(1.8 - 1.2) = \sigma(0.6).

Ô thứ hai huấn luyện hồi quy softmax bằng batch gradient descent trên iris với hai đặc trưng cánh hoa. Dữ liệu được chia 105 bông để huấn luyện, 45 bông để kiểm tra (stratify=y giữ mỗi loài 15 bông trong tập test), rồi chuẩn hóa z-score bằng StandardScaler học từ tập train. Mô hình xuất phát từ W=0\mathbf{W} = \mathbf{0}, b=0\mathbf{b} = \mathbf{0}, η=0.5\eta = 0.5, chạy 1000 epoch, rồi so với LogisticRegression của scikit-learn.

Hồi quy softmax bằng gradient descent

import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X, y = load_iris(return_X_y=True)
X = X[:, 2:] # chiều dài và chiều rộng cánh hoa (cm)
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.3, random_state=0, stratify=y)
scaler = StandardScaler().fit(X_tr) # chỉ học từ tập train
A_tr, A_te = scaler.transform(X_tr), scaler.transform(X_te)
m, K = len(y_tr), 3
Y = np.eye(K)[y_tr] # nhãn one-hot: m hàng, K cột
def softmax(Z):
E = np.exp(Z - Z.max(axis=1, keepdims=True)) # trừ max của từng hàng
return E / E.sum(axis=1, keepdims=True)
# Mỗi cột của W là vectơ trọng số w_k của một lớp.
W, b, eta = np.zeros((2, K)), np.zeros(K), 0.5
for epoch in range(1001):
P = softmax(A_tr @ W + b)
if epoch in (0, 1, 10, 100, 1000):
loss = -np.mean(np.log(P[np.arange(m), y_tr]))
print(f"epoch {epoch:4d}: loss = {loss:.4f}")
G = P - Y # gradient theo logit: y_hat - y
W -= eta * A_tr.T @ G / m
b -= eta * G.mean(axis=0)
acc = np.mean(np.argmax(A_te @ W + b, axis=1) == y_te)
print(f"GD test acc = {acc:.4f}")
# Từ 3 lớp trở lên, LogisticRegression mặc định dùng softmax.
clf = LogisticRegression().fit(A_tr, y_tr)
print(f"sklearn test acc = {clf.score(A_te, y_te):.4f}")
flower = scaler.transform([[4.9, 1.5]]) # cánh hoa 4.9 cm x 1.5 cm
print("GD p =", softmax(flower @ W + b)[0].round(3))
print("sklearn p =", clf.predict_proba(flower)[0].round(3))

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

  • Epoch 0, loss là 1.0986, đúng ln⁡3\ln 3: mọi lớp đang được 1/31/3. Sau 10 epoch còn 0.4657, sau 100 epoch 0.1836, sau 1000 epoch 0.0976.
  • Loss vẫn đang giảm ở epoch 1000. Nếu cho chạy tới epoch 5000 (sửa cả range(1001) lẫn danh sách epoch được in), nó chỉ xuống 0.0891, còn trọng số thì tiếp tục lớn dần. Đây là hiện tượng setosa tách được hoàn toàn đã nói ở mục Lý thuyết.
  • Hai mô hình có cùng độ chính xác trên tập test, 0.9778, tức 44 trên 45 bông.
  • Với bông hoa 4.9 cm × 1.5 cm, scikit-learn cho (0.006,0.654,0.340)(0.006, 0.654, 0.340), rất gần bảng ở mục Trực giác: logit chưa làm tròn của nó là (−2.95,1.80,1.15)(-2.95, 1.80, 1.15). Mô hình GD tự viết cho versicolor 0.771, tự tin hơn hẳn, vì nó không có phạt L2 nên trọng số lớn hơn.

Ô thứ ba dùng LogisticRegression (sau StandardScaler, trong một pipeline) để tô màu mặt phẳng theo lớp dự đoán, rồi đánh giá trên tập test bằng ma trận nhầm lẫn (confusion matrix) và classification_report.

Ranh giới quyết định và đánh giá trên tập test

import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
iris = load_iris()
X, y = iris.data[:, 2:], iris.target # hai đặc trưng cánh hoa (cm)
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.3, random_state=0, stratify=y)
model = make_pipeline(StandardScaler(), LogisticRegression())
model.fit(X_tr, y_tr)
# Tô màu mỗi điểm của lưới theo lớp có xác suất lớn nhất.
g1, g2 = np.meshgrid(np.linspace(0.5, 7.5, 300), np.linspace(0, 2.8, 300))
grid = np.c_[g1.ravel(), g2.ravel()]
region = model.predict(grid).reshape(g1.shape)
fig, ax = plt.subplots(figsize=(6, 4.5))
ax.contourf(g1, g2, region, levels=[-0.5, 0.5, 1.5, 2.5],
colors=["#e8d5b7", "#c9d9c3", "#c7cfe0"])
for k, marker in enumerate(["o", "s", "^"]):
pts = X_tr[y_tr == k]
ax.scatter(pts[:, 0], pts[:, 1], marker=marker, edgecolor="k",
facecolor="white", label=iris.target_names[k])
ax.set_xlabel("petal length (cm)")
ax.set_ylabel("petal width (cm)")
ax.legend(loc="upper left")
plt.show()
# Đánh giá trên tập test: hàng là lớp thật, cột là lớp dự đoán.
y_pred = model.predict(X_te)
print(confusion_matrix(y_te, y_pred))
print(classification_report(y_te, y_pred, digits=3,
target_names=iris.target_names))

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

Hình cho thấy ranh giới tuyến tính từng đoạn: hai đoạn thẳng chia mặt phẳng thành ba vùng. Ba đường z1=z2z_1 = z_2, z2=z3z_2 = z_3, z1=z3z_1 = z_3 gặp nhau ở một điểm nằm ngoài khung hình (chiều dài cánh hoa âm), nên trong khung bạn chỉ thấy hai đoạn. Ranh giới setosa nằm giữa khoảng trống rộng, còn ranh giới versicolor–virginica phải đi qua chỗ hai nhóm chồng lên nhau.

Ma trận nhầm lẫn (hàng là lớp thật, cột là lớp dự đoán) cho thấy lỗi duy nhất: một bông virginica bị đoán là versicolor. Đó là bông có cánh hoa 5.6 cm × 1.4 cm, hẹp như versicolor; mô hình cho nó versicolor 0.593, virginica 0.405. Báo cáo có precision, recall và F1 cho từng lớp, rồi hai dòng gộp: macro avg lấy trung bình cộng của ba lớp, mỗi lớp nặng như nhau, còn weighted avg lấy trung bình theo số mẫu của mỗi lớp. Ở đây mỗi lớp có đúng 15 mẫu nên hai dòng bằng nhau: precision 0.979, recall và F1 0.978. Ý nghĩa của các chỉ số này và khi nào chọn cách gộp nào nằm ở bài Đánh giá mô hình.

Câu 1/4 Một mô hình cho ba logit z=(4,2,2)\mathbf{z} = (4, 2, 2). Xác suất softmax của lớp thứ nhất gần nhất với số nào?
Câu 2/4 Bông hoa thật là virginica (lớp thứ ba). Mô hình đang dự đoán y^=(0.1,0.6,0.3)\hat{\mathbf{y}} = (0.1, 0.6, 0.3). Sau một bước gradient descent tính trên riêng bông hoa này, logit nào thay đổi thế nào?
Câu 3/4 Bạn gắn nhãn ảnh: một ảnh có thể chứa cùng lúc mèo, chó và người. Đầu ra nào hợp lý nhất?
Câu 4/4 Hồi quy softmax với K=2K = 2 lớp có w1=(2,−1)\mathbf{w}_1 = (2, -1), b1=0.5b_1 = 0.5 và w2=(0.5,1)\mathbf{w}_2 = (0.5, 1), b2=1b_2 = 1. Hồi quy logistic σ(w⊤x+b)\sigma(\mathbf{w}^\top\mathbf{x} + b) cho đúng P(lớp 1)P(\text{lớp 1}) có tham số nào?

Ý chính:

  • Hồi quy softmax cho mỗi lớp một logit zk=wk⊤x+bkz_k = \mathbf{w}_k^\top\mathbf{x} + b_k, rồi y^k=ezk/∑jezj\hat{y}_k = e^{z_k}/\sum_j e^{z_j}. Khác ba bộ phân loại one-vs-rest rời nhau, các xác suất luôn dương, giữ thứ tự của logit và cộng lại bằng 1.
  • Softmax chỉ phụ thuộc hiệu giữa các logit: cộng cùng một hằng số không đổi gì, nên trừ logit lớn nhất trước khi lấy mũ để tránh tràn số. Với K=2K = 2, softmax là σ(z1−z2)\sigma(z_1 - z_2), tức hồi quy logistic.
  • Với nhãn one-hot, cross-entropy −∑kykln⁡y^k-\sum_k y_k\ln\hat{y}_k thu về −ln⁡y^lớp đuˊng-\ln\hat{y}_{\text{lớp đúng}}, bắt đầu từ ln⁡K\ln K khi mô hình chưa học gì. Gradient theo logit là y^−y\hat{\mathbf{y}} - \mathbf{y}, nên ∇WL=1mX⊤(Y^−Y)\nabla_{\mathbf{W}}\mathcal{L} = \frac{1}{m}\mathbf{X}^\top(\hat{\mathbf{Y}} - \mathbf{Y}), và huấn luyện bằng gradient descent.
  • Ranh giới quyết định là tuyến tính từng đoạn. Softmax cũng là lớp cuối của mạng nơ-ron phân loại và của LLM; nó chỉ dùng cho bài toán mỗi mẫu đúng một lớp.

Đọ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