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

Hồi quy Logistic

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

  • Hiểu vì sao bài toán phân loại nhị phân (binary classification) cần một mô hình cho ra xác suất, và vì sao hồi quy tuyến tính rồi cắt ngưỡng không ổn.
  • Đọc được mô hình hồi quy logistic (logistic regression) theo ba cách: xác suất qua hàm sigmoid, log-odds tuyến tính, và ranh giới quyết định (decision boundary) w⊤x+b=0\mathbf{w}^\top\mathbf{x} + b = 0.
  • Tự dẫn được hàm mất mát log loss từ hợp lý cực đại (maximum likelihood), và gradient 1m∑i(y^(i)−y(i))xj(i)\frac{1}{m}\sum_i\left(\hat{y}^{(i)} - y^{(i)}\right)x^{(i)}_j của nó.
  • Cài đặt được hồi quy logistic bằng gradient descent và bằng scikit-learn, hiểu tham số C, và diễn giải hệ số qua odds.

Thầy chủ nhiệm ghi lại số giờ ôn thi trong tuần trước kỳ thi của 18 sinh viên, và kết quả: đậu hay trượt. Số liệu dưới đây là dữ liệu tự tạo cho bài học.

Kết quảSố giờ ôn thi
Trượt (9 bạn)0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4.5, 6
Đậu (9 bạn)4, 5, 5.5, 6.5, 7, 7.5, 8, 9, 10

Nhìn qua là thấy: ôn ít thì hay trượt, ôn nhiều thì hay đậu, ở giữa có vùng lẫn lộn (bạn 4 giờ đậu, bạn 4.5 giờ và 6 giờ trượt). Thầy muốn trả lời: một bạn ôn 5 giờ thì khả năng đậu là bao nhiêu?

Đây là bài toán phân loại (classification): nhãn yy chỉ có hai giá trị, 1 (đậu) hoặc 0 (trượt). Thứ thầy cần không chỉ là “đậu hay trượt”, mà là một xác suất: 90% và 51% đều là “đoán đậu”, nhưng ý nghĩa khác hẳn nhau.

Thử hồi quy tuyến tính rồi cắt ngưỡng

Phần tiêu đề “Thử hồi quy tuyến tính rồi cắt ngưỡng”

Cách đầu tiên ai cũng nghĩ tới: coi 0 và 1 là số, khớp một đường thẳng như bài Hồi quy tuyến tính, rồi đoán “đậu” khi giá trị dự đoán từ 0.5 trở lên. Với 18 bạn, đường bình phương tối thiểu là y^=0.1413x−0.1828\hat{y} = 0.1413x - 0.1828. Ngưỡng 0.5 rơi vào x=4.83x = 4.83 giờ, và 16/18 bạn được đoán đúng. Nghe có vẻ ổn, nhưng có hai chỗ hỏng.

Thứ nhất, con số dự đoán không phải xác suất. Bạn ôn 0 giờ được y^=−0.183\hat{y} = -0.183, bạn ôn 10 giờ được y^=1.230\hat{y} = 1.230. Xác suất âm hay lớn hơn 1 đều vô nghĩa.

Thứ hai, đường thẳng bị kéo bởi những điểm “quá đúng”. Thêm một bạn ôn 30 giờ, và tất nhiên đậu. Bạn này chẳng làm bài toán khó thêm chút nào. Nhưng đường thẳng phải với tới điểm (30,1)(30, 1), nên nó nằm nghiêng hẳn xuống: y^=0.0421x+0.2671\hat{y} = 0.0421x + 0.2671. Ngưỡng dời từ 4.83 sang 5.53 giờ, và hai bạn ôn 5 giờ và 5.5 giờ, vốn đã đậu, giờ bị đoán trượt: trên 18 bạn cũ chỉ còn đúng 14. Nguyên nhân là bình phương sai số phạt bạn 30 giờ vì y^\hat{y} chưa bằng đúng 1, dù với phân loại, đoán bạn ấy “rất chắc đậu” là đủ.

Thứ cần là một đường cong: đi gần 0 khi ôn rất ít, gần 1 khi ôn rất nhiều, và chuyển từ 0 sang 1 ở vùng lẫn lộn. Ở hai đầu, đường phải nằm ngang, để bạn 30 giờ đã ở sát 1 thì không kéo gì thêm. Đó là đường chữ S, và hồi quy logistic chính là cách khớp đường chữ S này.

Bài toán của bài là: từ các đặc trưng của một mẫu, đoán xác suất mẫu đó thuộc lớp 1, bằng một đường cong chữ S, và chọn đường cong làm cho dữ liệu đã thấy có khả năng xảy ra cao nhất.

Có mm mẫu. Mẫu thứ ii có vectơ đặc trưng x(i)\mathbf{x}^{(i)} gồm nn số và nhãn y(i)∈{0,1}y^{(i)} \in \{0, 1\}. Lớp 1 gọi là lớp dương (positive class), lớp 0 là lớp âm (negative class). Với 18 sinh viên, m=18m = 18, n=1n = 1, xx là số giờ ôn.

Hồi quy logistic làm hai bước. Bước một giống hệt hồi quy tuyến tính: tính một điểm số

z=w⊤x+bz = \mathbf{w}^\top\mathbf{x} + b

với w\mathbf{w} là trọng số, bb là hệ số tự do. zz là một số thực bất kỳ, gọi là logit. Bước hai đưa zz qua hàm sigmoid σ\sigma để được một xác suất:

y^=σ(z)=11+e−z,y^=P(y=1∣x)\hat{y} = \sigma(z) = \frac{1}{1 + e^{-z}}, \qquad \hat{y} = P(y = 1 \mid \mathbf{x})

Vậy y^\hat{y} ở bài này không còn là “giá trị dự đoán của yy” như hồi quy tuyến tính, mà là xác suất mô hình gán cho lớp 1. Xác suất của lớp 0 là 1−y^1 - \hat{y}.

Nhìn vào công thức để thấy vì sao nó cho đường chữ S:

  • zz rất lớn: e−ze^{-z} gần 0, nên σ(z)\sigma(z) gần 1/1=11/1 = 1.
  • zz rất âm: e−ze^{-z} rất lớn, nên σ(z)\sigma(z) gần 0.
  • z=0z = 0: e0=1e^{0} = 1, nên σ(0)=1/2\sigma(0) = 1/2.

Vì e−z>0e^{-z} > 0 với mọi zz, mẫu số luôn lớn hơn 1, nên σ(z)\sigma(z) luôn nằm trong khoảng (0,1)(0, 1), không bao giờ chạm đúng 0 hay 1. Đường còn đối xứng quanh điểm (0,0.5)(0, 0.5): σ(−z)=1−σ(z)\sigma(-z) = 1 - \sigma(z). Vài giá trị:

zz−6-6−4-4−2-200224466
σ(z)\sigma(z)0.00250.0180.11920.50.88080.9820.9975

Đường dốc nhất quanh z=0z = 0, và gần như nằm ngang khi ∣z∣\lvert z\rvert vượt quá 5: đúng hình dạng mà mục Trực giác cần. Hình ở mục Minh họa vẽ đường này.

Odds và log-odds: đọc mô hình như một đường thẳng

Phần tiêu đề “Odds và log-odds: đọc mô hình như một đường thẳng”

Odds của một xác suất pp là tỉ số p/(1−p)p/(1 - p): khả năng xảy ra chia cho khả năng không xảy ra. Xác suất 0.8 có odds là 0.8/0.2=40.8/0.2 = 4, hay “4 ăn 1”. Xác suất 0.5 có odds 1. Odds đi từ 0 tới vô cùng.

Hãy giải ngược y^=σ(z)\hat{y} = \sigma(z) để tìm zz. Lấy nghịch đảo hai vế, trừ 1, rồi lấy ln⁡\ln:

1y^=1+e−z1−y^y^=e−zln⁡y^1−y^=z=w⊤x+b\begin{aligned} \frac{1}{\hat{y}} &= 1 + e^{-z} \\ \frac{1 - \hat{y}}{\hat{y}} &= e^{-z} \\ \ln\frac{\hat{y}}{1 - \hat{y}} &= z = \mathbf{w}^\top\mathbf{x} + b \end{aligned}

Dòng thứ hai trừ 1 ở hai vế và quy đồng 1y^−1=1−y^y^\frac{1}{\hat{y}} - 1 = \frac{1 - \hat{y}}{\hat{y}}. Dòng thứ ba lật phân số (nên e−ze^{-z} thành eze^{z}), rồi lấy ln⁡\ln hai vế. Vế trái là log-odds, cũng gọi là logit của y^\hat{y}. Vậy:

Điều này cho cách đọc hệ số. Tăng xjx_j thêm 1 đơn vị, giữ các đặc trưng khác nguyên, thì log-odds tăng đúng wjw_j, nên odds nhân với ewje^{w_j}:

odds(xj+1)=ez+wj=ez⋅ewj=odds(xj)⋅ewj\text{odds}(x_j + 1) = e^{z + w_j} = e^{z} \cdot e^{w_j} = \text{odds}(x_j) \cdot e^{w_j}

Ở ô code đầu tiên, mô hình của 18 sinh viên có w=1.263w = 1.263, b=−6.006b = -6.006, nên mỗi giờ ôn thêm nhân odds đậu với e1.263≈3.54e^{1.263} \approx 3.54. Bạn ôn 4 giờ có y^=0.278\hat{y} = 0.278, odds 0.386; bạn ôn 5 giờ có y^=0.577\hat{y} = 0.577, odds 1.365, đúng gấp 3.54 lần. Để ý: xác suất không tăng một lượng cố định. Từ 4 lên 5 giờ, xác suất tăng 0.3; từ 9 lên 10 giờ, nó gần như đứng yên vì đã sát 1. Hệ số wjw_j âm thì ewj<1e^{w_j} < 1, odds giảm.

Muốn ra một lớp cụ thể, ta so y^\hat{y} với một ngưỡng (threshold), mặc định 0.5: đoán lớp 1 khi y^≥0.5\hat{y} \ge 0.5. Vì σ\sigma là hàm tăng và σ(0)=0.5\sigma(0) = 0.5:

y^≥0.5  ⟺  z≥0  ⟺  w⊤x+b≥0\hat{y} \ge 0.5 \iff z \ge 0 \iff \mathbf{w}^\top\mathbf{x} + b \ge 0

Tập các điểm có w⊤x+b=0\mathbf{w}^\top\mathbf{x} + b = 0 là ranh giới quyết định. Với một đặc trưng, đó là một điểm: x=−b/w=6.006/1.263≈4.75x = -b/w = 6.006/1.263 \approx 4.75 giờ. Với hai đặc trưng, đó là đường thẳng w1x1+w2x2+b=0w_1x_1 + w_2x_2 + b = 0; với nn đặc trưng, là một siêu phẳng (hyperplane). Đường cong chữ S chỉ nằm ở khâu đổi điểm số thành xác suất, còn ranh giới luôn phẳng. Vì vậy hồi quy logistic là một bộ phân loại tuyến tính (linear classifier).

Ngưỡng 0.5 chỉ là mặc định. Khi bỏ sót lớp dương nguy hiểm hơn báo nhầm (ví dụ sàng lọc bệnh), người ta hạ ngưỡng. Chọn ngưỡng và đánh giá bộ phân loại kỹ hơn độ chính xác là nội dung bài Đánh giá mô hình: Accuracy, Precision, Recall, F1.

Hàm mất mát: từ hợp lý cực đại tới log loss

Phần tiêu đề “Hàm mất mát: từ hợp lý cực đại tới log loss”

Cần một con số đo mô hình tốt hay tệ. Ý tưởng của hợp lý cực đại: chọn tham số làm cho dữ liệu đã quan sát có xác suất xảy ra cao nhất theo mô hình.

Bước 1: xác suất của một mẫu. Mô hình nói P(y=1∣x)=y^P(y = 1 \mid \mathbf{x}) = \hat{y} và P(y=0∣x)=1−y^P(y = 0 \mid \mathbf{x}) = 1 - \hat{y}. Gộp hai trường hợp vào một công thức:

P(y∣x)=y^ y (1−y^)1−yP(y \mid \mathbf{x}) = \hat{y}^{\,y}\,(1 - \hat{y})^{1 - y}

Thử lại: y=1y = 1 thì thừa số thứ hai là (1−y^)0=1(1 - \hat{y})^0 = 1, còn lại y^\hat{y}. y=0y = 0 thì thừa số thứ nhất là y^0=1\hat{y}^0 = 1, còn lại 1−y^1 - \hat{y}. Đây là phân phối Bernoulli.

Bước 2: xác suất của cả tập dữ liệu. Coi các mẫu độc lập với nhau, xác suất để cả mm nhãn xảy ra đúng như đã thấy là tích:

Lik(w,b)=∏i=1m(y^(i))y(i)(1−y^(i))1−y(i)\text{Lik}(\mathbf{w}, b) = \prod_{i=1}^{m}\left(\hat{y}^{(i)}\right)^{y^{(i)}}\left(1 - \hat{y}^{(i)}\right)^{1 - y^{(i)}}

Đại lượng này, xem như hàm của tham số, gọi là likelihood. Ta muốn làm nó lớn nhất.

Bước 3: lấy ln⁡\ln. Tích của nhiều số nhỏ hơn 1 rất nhanh tiến về 0. Với mô hình tốt nhất cho 18 sinh viên, likelihood chỉ là 0.0062; với vài nghìn mẫu, máy tính làm tròn nó thành 0. Vì ln⁡\ln là hàm tăng, làm lớn ln⁡Lik\ln\text{Lik} cũng là làm lớn Lik\text{Lik}. Dùng ln⁡(ab)=ln⁡a+ln⁡b\ln(ab) = \ln a + \ln b và ln⁡ak=kln⁡a\ln a^k = k\ln a:

ln⁡Lik(w,b)=∑i=1m[y(i)ln⁡y^(i)+(1−y(i))ln⁡(1−y^(i))]\ln\text{Lik}(\mathbf{w}, b) = \sum_{i=1}^{m}\left[y^{(i)}\ln\hat{y}^{(i)} + \left(1 - y^{(i)}\right)\ln\left(1 - \hat{y}^{(i)}\right)\right]

Bước 4: đổi thành bài toán làm nhỏ. Quen với việc làm nhỏ hàm mất mát, ta đổi dấu và chia cho mm để lấy trung bình. Được log loss, còn gọi là binary cross-entropy:

L(w,b)=−1m∑i=1m[y(i)ln⁡y^(i)+(1−y(i))ln⁡(1−y^(i))]\mathcal{L}(\mathbf{w}, b) = -\frac{1}{m}\sum_{i=1}^{m}\left[y^{(i)}\ln\hat{y}^{(i)} + \left(1 - y^{(i)}\right)\ln\left(1 - \hat{y}^{(i)}\right)\right]

Làm nhỏ L\mathcal{L} đúng là làm lớn likelihood, vì L=−1mln⁡Lik\mathcal{L} = -\frac{1}{m}\ln\text{Lik}.

Đọc từng mẫu: mỗi mẫu chỉ đóng góp −ln⁡-\ln của xác suất mô hình gán cho nhãn đúng. Gán 0.9 cho nhãn đúng thì mất 0.105; gán 0.5 thì mất 0.693; gán 0.1 thì mất 2.303; gán gần 0 thì mất gần vô cùng. Log loss phạt rất nặng những lần sai mà tự tin. Trong 18 sinh viên, hai mẫu bị phạt nặng nhất là hai bạn bị đoán sai: bạn ôn 4 giờ mà đậu (y^=0.278\hat{y} = 0.278, mất −ln⁡0.278=1.28-\ln 0.278 = 1.28) và bạn ôn 6 giờ mà trượt (y^=0.828\hat{y} = 0.828, mất −ln⁡0.172=1.76-\ln 0.172 = 1.76). Trung bình trên 18 bạn, L=0.2826\mathcal{L} = 0.2826. Mô hình “không biết gì”, w=0\mathbf{w} = \mathbf{0}, b=0b = 0, cho mọi bạn 0.5, có L=ln⁡2≈0.6931\mathcal{L} = \ln 2 \approx 0.6931.

Sao không giữ MSE 1m∑i(y^(i)−y(i))2\frac{1}{m}\sum_i\left(\hat{y}^{(i)} - y^{(i)}\right)^2 với y^=σ(z)\hat{y} = \sigma(z)? Xét một mẫu có y=1y = 1 và nhìn mất mát theo zz:

  • MSE: (σ(z)−1)2\left(\sigma(z) - 1\right)^2. Khi zz rất âm, nó nằm ngang ở gần 1; khi zz rất dương, nằm ngang ở gần 0. Một đường đi từ nằm ngang, xuống dốc, rồi lại nằm ngang có dạng chữ S, không phải cái bát: nó không lồi. Cộng nhiều mẫu lại, mặt mất mát có những vùng gần như phẳng, nơi gradient descent bò rất chậm.
  • Log loss: −ln⁡σ(z)-\ln\sigma(z). Khi zz rất âm, nó tăng gần như theo đường thẳng −z-z, không hề nằm ngang. Sai càng nặng, dốc càng rõ.

Bằng số: với z=−6z = -6 (y^=0.0025\hat{y} = 0.0025, sai rất tự tin), đạo hàm theo zz của MSE chỉ là −0.0049-0.0049, của log loss là −0.9975-0.9975, mạnh hơn khoảng 200 lần. Mục sau cho thấy vì sao.

Gradient descent cần đạo hàm riêng của L\mathcal{L} theo từng wjw_j và bb. Xét một mẫu, bỏ chỉ số (i)(i): ℓ=−[yln⁡y^+(1−y)ln⁡(1−y^)]\ell = -\left[y\ln\hat{y} + (1 - y)\ln(1 - \hat{y})\right], với y^=σ(z)\hat{y} = \sigma(z) và z=w⊤x+bz = \mathbf{w}^\top\mathbf{x} + b. Đây là hàm hợp ba tầng, nên dùng quy tắc hàm hợp (chain rule):

∂ℓ∂wj=∂ℓ∂y^⋅dy^dz⋅∂z∂wj\frac{\partial\ell}{\partial w_j} = \frac{\partial\ell}{\partial\hat{y}} \cdot \frac{d\hat{y}}{dz} \cdot \frac{\partial z}{\partial w_j}

Bước 1: đạo hàm của sigmoid. Viết σ(z)=(1+e−z)−1\sigma(z) = \left(1 + e^{-z}\right)^{-1}. Đạo hàm của u−1u^{-1} là −u−2-u^{-2}, nhân với đạo hàm của 1+e−z1 + e^{-z} là −e−z-e^{-z}:

σ′(z)=e−z(1+e−z)2=11+e−z⋅e−z1+e−z=σ(z)(1−σ(z))\begin{aligned} \sigma'(z) &= \frac{e^{-z}}{\left(1 + e^{-z}\right)^2} \\ &= \frac{1}{1 + e^{-z}} \cdot \frac{e^{-z}}{1 + e^{-z}} \\ &= \sigma(z)\left(1 - \sigma(z)\right) \end{aligned}

Dòng thứ hai tách phân số thành tích. Dòng thứ ba dùng e−z1+e−z=(1+e−z)−11+e−z=1−σ(z)\frac{e^{-z}}{1 + e^{-z}} = \frac{(1 + e^{-z}) - 1}{1 + e^{-z}} = 1 - \sigma(z). Vậy dy^dz=y^(1−y^)\frac{d\hat{y}}{dz} = \hat{y}(1 - \hat{y}). Thừa số này lớn nhất (0.25) ở z=0z = 0 và gần 0 ở hai đầu: đó là lý do MSE qua sigmoid bò chậm.

Bước 2: đạo hàm của ℓ\ell theo y^\hat{y}. Đạo hàm của ln⁡u\ln u là 1/u1/u; đạo hàm của ln⁡(1−y^)\ln(1 - \hat{y}) theo y^\hat{y} là −11−y^-\frac{1}{1 - \hat{y}}:

∂ℓ∂y^=−yy^+1−y1−y^=−y(1−y^)+(1−y)y^y^(1−y^)=y^−yy^(1−y^)\begin{aligned} \frac{\partial\ell}{\partial\hat{y}} &= -\frac{y}{\hat{y}} + \frac{1 - y}{1 - \hat{y}} \\ &= \frac{-y(1 - \hat{y}) + (1 - y)\hat{y}}{\hat{y}(1 - \hat{y})} \\ &= \frac{\hat{y} - y}{\hat{y}(1 - \hat{y})} \end{aligned}

Dòng thứ hai quy đồng mẫu số. Dòng thứ ba phá ngoặc tử số: −y+yy^+y^−yy^=y^−y-y + y\hat{y} + \hat{y} - y\hat{y} = \hat{y} - y.

Bước 3: ghép lại. z=w1x1+⋯+wnxn+bz = w_1x_1 + \dots + w_nx_n + b, nên ∂z∂wj=xj\frac{\partial z}{\partial w_j} = x_j và ∂z∂b=1\frac{\partial z}{\partial b} = 1. Nhân ba thừa số:

∂ℓ∂wj=y^−yy^(1−y^)⋅y^(1−y^)⋅xj=(y^−y) xj\frac{\partial\ell}{\partial w_j} = \frac{\hat{y} - y}{\hat{y}(1 - \hat{y})} \cdot \hat{y}(1 - \hat{y}) \cdot x_j = (\hat{y} - y)\,x_j

Thừa số y^(1−y^)\hat{y}(1 - \hat{y}) ở mẫu và ở tử triệt tiêu nhau. Đó là vì sao log loss không bò chậm khi sai tự tin: đạo hàm theo zz chỉ còn y^−y\hat{y} - y.

Bước 4: trung bình trên mm mẫu. L\mathcal{L} là trung bình của các ℓ(i)\ell^{(i)}, nên đạo hàm của nó là trung bình các đạo hàm:

∂L∂wj=1m∑i=1m(y^(i)−y(i))xj(i),∂L∂b=1m∑i=1m(y^(i)−y(i))\frac{\partial\mathcal{L}}{\partial w_j} = \frac{1}{m}\sum_{i=1}^{m}\left(\hat{y}^{(i)} - y^{(i)}\right)x^{(i)}_j, \qquad \frac{\partial\mathcal{L}}{\partial b} = \frac{1}{m}\sum_{i=1}^{m}\left(\hat{y}^{(i)} - y^{(i)}\right)

Viết gọn bằng ma trận X\mathbf{X} (mm hàng, mỗi hàng một mẫu, chưa có cột số 1) và vectơ y^\hat{\mathbf{y}} chứa các y^(i)\hat{y}^{(i)}: ∇wL=1mX⊤(y^−y)\nabla_{\mathbf{w}}\mathcal{L} = \frac{1}{m}\mathbf{X}^\top\left(\hat{\mathbf{y}} - \mathbf{y}\right).

Cho gradient bằng 0 thì được các phương trình có σ\sigma bên trong, không giải ra công thức được như phương trình chuẩn. Nên ta dùng gradient descent:

wj←wj−η 1m∑i=1m(y^(i)−y(i))xj(i),b←b−η 1m∑i=1m(y^(i)−y(i))w_j \leftarrow w_j - \eta\,\frac{1}{m}\sum_{i=1}^{m}\left(\hat{y}^{(i)} - y^{(i)}\right)x^{(i)}_j, \qquad b \leftarrow b - \eta\,\frac{1}{m}\sum_{i=1}^{m}\left(\hat{y}^{(i)} - y^{(i)}\right)

Tin tốt: log loss là hàm lồi theo (w,b)(\mathbf{w}, b). Theo zz, mất mát của một mẫu có đạo hàm σ(z)−y\sigma(z) - y và đạo hàm bậc hai σ(z)(1−σ(z))>0\sigma(z)\left(1 - \sigma(z)\right) > 0, tức cong lên như cái bát; mà zz lại tuyến tính theo tham số, nên tổng các mẫu vẫn lồi. Không có cực tiểu cục bộ nào để mắc kẹt. scikit-learn mặc định không dùng gradient descent thường mà dùng L-BFGS, một thuật toán dùng thêm thông tin về độ cong để đi ít bước hơn, nhưng đáy nó tìm tới là cùng một đáy.

LogisticRegression của scikit-learn mặc định có phạt L2, như Ridge ở bài Ridge, Lasso, Elastic Net. Từ bản 1.8, tài liệu viết hàm mục tiêu (khi các mẫu có trọng số bằng nhau) là:

J(w,b)=L(w,b)+12mC∥w∥22J(\mathbf{w}, b) = \mathcal{L}(\mathbf{w}, b) + \frac{1}{2mC}\lVert\mathbf{w}\rVert_2^2
  • C là nghịch đảo của độ mạnh phạt. So với dạng L+λ∥w∥22\mathcal{L} + \lambda\lVert\mathbf{w}\rVert_2^2 của bài Regularization, λ=12mC\lambda = \frac{1}{2mC}. C nhỏ là phạt mạnh, C lớn là phạt yếu. Mặc định C=1.
  • bb không bị phạt (với solver mặc định lbfgs).
  • Muốn tắt phạt, tài liệu bản 1.8 khuyên C=np.inf (tham số penalty đã bị đánh dấu sắp bỏ). Bản 1.8.0 chạy trên trang lại in một cảnh báo thừa khi gặp C=np.inf, nên các ô code dùng C=1e10: phạt nhỏ tới mức không đổi kết quả.
  • Nên chuẩn hóa đặc trưng, vì cùng lý do như Ridge: phạt đối xử mọi wjw_j như nhau.

Phạt còn cứu một tình huống mà không phạt thì hỏng: khi có một siêu phẳng tách hẳn hai lớp (linearly separable). Lúc đó, nhân w\mathbf{w} và bb lên gấp đôi thì ranh giới giữ nguyên, nhưng mọi y^\hat{y} bị đẩy sát 0 hoặc 1 hơn, nên log loss lại giảm. Không có đáy: các hệ số phình ra mãi. Ô code thứ ba gặp đúng chuyện này.

  • Ranh giới luôn phẳng. Hai lớp xếp thành vòng tròn lồng nhau thì không đường thẳng nào tách được. Có thể thêm đặc trưng như x12x_1^2, x1x2x_1x_2 (giống hồi quy đa thức), hoặc dùng mô hình khác như ở bài K-Nearest Neighbors và Decision Tree và Random Forest.
  • Chỉ hai lớp. Ba lớp trở lên thì dùng softmax thay cho sigmoid, ở bài Hồi quy Softmax.
  • Xác suất chỉ đáng tin khi mô hình đúng dạng. Log-odds thật không tuyến tính theo đặc trưng thì xác suất mô hình đưa ra có thể lệch, dù độ chính xác vẫn cao.
  • Hệ số khó đọc khi đặc trưng tương quan mạnh (hộp thứ ba ở mục Lỗi hay gặp).

Hình dưới vẽ hàm sigmoid theo zz. Hãy nhìn vào hai đầu đường cong, nơi nó nằm ngang sát hai đường tiệm cận 0 và 1, và vào điểm giữa σ(0)=0.5\sigma(0) = 0.5. Các chấm là 18 sinh viên, đặt tại z(i)=1.263x(i)−6.006z^{(i)} = 1.263x^{(i)} - 6.006 của mô hình khớp ở ô code đầu tiên, ở độ cao 1 nếu đậu, 0 nếu trượt. Đường dọc z=0z = 0 chính là ranh giới quyết định, ứng với 4.75 giờ ôn.

Ba ô dưới đây chạy độc lập, ô nào cũng tự tạo dữ liệu. Ô chạy đầu tiên lâu hơn một chút, vì trình duyệt phải tải thêm scikit-learn; ô thứ hai còn tải thêm Matplotlib.

Ô đầu tiên cài đặt đúng phần Lý thuyết: sigmoid, log loss, gradient 1m∑i(y^(i)−y(i))x(i)\frac{1}{m}\sum_i\left(\hat{y}^{(i)} - y^{(i)}\right)x^{(i)}, xuất phát từ w=b=0w = b = 0 với η=0.2\eta = 0.2, dừng khi độ lớn gradient dưới 10−610^{-6}. Cuối ô là LogisticRegression của scikit-learn để đối chiếu.

Hồi quy logistic bằng gradient descent

import numpy as np
from sklearn.linear_model import LogisticRegression
# 18 sinh viên (dữ liệu tự tạo): số giờ ôn thi và kết quả (1 = đậu).
hours = np.array([0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5,
5, 5.5, 6, 6.5, 7, 7.5, 8, 9, 10])
passed = np.array([0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1])
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def log_loss(p, y): # binary cross-entropy, trung bình trên m mẫu
return -np.mean(y * np.log(p) + (1 - y) * np.log(1 - p))
w, b, eta = 0.0, 0.0, 0.2 # xuất phát từ 0, tốc độ học 0.2
for step in range(100_000):
p = sigmoid(w * hours + b) # y^ = P(đậu | số giờ)
grad_w = np.mean((p - passed) * hours) # dL/dw
grad_b = np.mean(p - passed) # dL/db
if np.hypot(grad_w, grad_b) < 1e-6: # gradient đủ nhỏ thì dừng
break
w, b = w - eta * grad_w, b - eta * grad_b
print(f"GD stopped after {step} steps")
print(f"GD: w = {w:.3f}, b = {b:.3f}, log loss = {log_loss(p, passed):.4f}")
print(f"threshold -b/w = {-b / w:.3f} h, odds ratio e^w = {np.exp(w):.2f}")
for h in [2, 4, 6, 8]:
print(f"P(pass | {h} h) = {sigmoid(w * h + b):.3f}")
# scikit-learn: C rất lớn nghĩa là gần như không phạt (xem bài).
model = LogisticRegression(C=1e10).fit(hours.reshape(-1, 1), passed)
w_sk, b_sk = model.coef_[0, 0], model.intercept_[0]
print(f"sklearn: w = {w_sk:.3f}, b = {b_sk:.3f}")
print(f"sklearn P(pass | 4 h) = {model.predict_proba([[4]])[0, 1]:.3f}")

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

GD dừng sau 7540 bước với w=1.263w = 1.263, b=−6.006b = -6.006, log loss 0.2826, khớp scikit-learn tới ba chữ số. Ngưỡng −b/w=4.754-b/w = 4.754 giờ, mỗi giờ ôn thêm nhân odds đậu với 3.54. Xác suất đậu đi 0.030, 0.278, 0.828, 0.984 khi số giờ đi 2, 4, 6, 8: tăng nhanh nhất quanh ngưỡng, chậm dần ở hai đầu, đúng hình chữ S.

Hơn 7500 bước là nhiều, vì số giờ chưa chuẩn hóa: như bài Gradient Descent, đặc trưng nằm xa 0 làm mặt mất mát hẹp và dài. Hãy thử đổi eta thành 0.05: GD vẫn tới cùng đáp án, nhưng mất khoảng 30 000 bước.

Ô thứ hai kiểm lại các số ở mục Trực giác: khớp hồi quy tuyến tính và hồi quy logistic trên 18 sinh viên, rồi trên 19 sinh viên (thêm bạn ôn 30 giờ), in ngưỡng và độ chính xác trên 18 bạn đầu, và vẽ bốn đường.

Hồi quy tuyến tính và hồi quy logistic

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression, LogisticRegression
# 18 sinh viên như ô trước, rồi thêm một bạn ôn 30 giờ và đậu.
hours = np.array([0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5,
5, 5.5, 6, 6.5, 7, 7.5, 8, 9, 10])
passed = np.array([0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1])
X, X30 = hours.reshape(-1, 1), np.append(hours, 30).reshape(-1, 1)
y30 = np.append(passed, 1)
grid = np.linspace(0, 32, 300).reshape(-1, 1)
fig, ax = plt.subplots(figsize=(7, 4))
ax.scatter(X30[:, 0], y30, color="black", zorder=3, label="students")
for name, Xt, yt, style in [("18", X, passed, "-"), ("19", X30, y30, "--")]:
lin = LinearRegression().fit(Xt, yt)
log = LogisticRegression(C=1e10).fit(Xt, yt) # gần như không phạt
a, c = lin.coef_[0], lin.intercept_
w, b = log.coef_[0, 0], log.intercept_[0]
acc = np.mean((lin.predict(X) >= 0.5) == passed) # trên 18 bạn đầu
print(f"{name} students: line y = {a:.4f} x + {c:.4f}")
print(f" line: y^(0) = {c:.3f}, y^(10) = {a * 10 + c:.3f}, "
f"y^(30) = {a * 30 + c:.3f}")
print(f" line threshold = {(0.5 - c) / a:.2f} h, accuracy = {acc:.3f}")
print(f" logistic threshold = {-b / w:.2f} h, "
f"accuracy = {log.score(X, passed):.3f}")
ax.plot(grid, lin.predict(grid), "tab:orange", ls=style,
label=f"linear, {name} students")
ax.plot(grid, log.predict_proba(grid)[:, 1], "tab:blue", ls=style,
label=f"logistic, {name} students")
ax.axhline(0.5, color="gray", lw=0.8)
ax.set_xlabel("hours of study")
ax.set_ylabel("prediction")
ax.set_ylim(-0.3, 1.5)
ax.legend(fontsize=8)
plt.show()

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

Với 18 bạn, hai cách cho ngưỡng gần nhau (4.83 và 4.75 giờ) và cùng đoán đúng 0.889, tức 16/18. Nhưng đường thẳng cho −0.183-0.183 ở 0 giờ, 1.2301.230 ở 10 giờ và 4.0554.055 ở 30 giờ: không đọc như xác suất được. Thêm bạn 30 giờ, đường thẳng nghiêng xuống, ngưỡng dời sang 5.53 giờ, độ chính xác trên 18 bạn cũ tụt còn 0.778. Đường chữ S thì gần như không động đậy: ngưỡng vẫn 4.75 giờ. Lý do nằm ở gradient: bạn 30 giờ có y^\hat{y} sát 1, nên y^−y\hat{y} - y gần 0, gần như không đẩy tham số.

Ô thứ ba dùng bộ load_breast_cancer có sẵn trong scikit-learn: 569 khối u, 30 đặc trưng đo từ ảnh chụp nhân tế bào (bán kính, độ sần bề mặt, độ lõm…). Nhãn 1 là lành tính (benign), nhãn 0 là ác tính (malignant). Ô chia 75% train, 25% test giữ đúng tỉ lệ hai lớp (stratify=y), đặt StandardScaler trong pipeline, khớp với C=1 (mặc định) và C=1e10 (gần như không phạt), rồi đọc ba hệ số lớn nhất của mô hình mặc định.

Hồi quy logistic trên bộ dữ liệu u vú

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
# 569 khối u, 30 đặc trưng. Nhãn: 0 = ác tính, 1 = lành tính.
data = load_breast_cancer()
X, y = data.data, data.target
print(f"m = {X.shape[0]}, n = {X.shape[1]}, benign = {y.mean():.3f}")
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, stratify=y, random_state=0)
for C in [1.0, 1e10]: # mặc định (phạt L2, C = 1) và gần như không phạt
model = make_pipeline(StandardScaler(),
LogisticRegression(C=C, max_iter=10_000))
model.fit(X_train, y_train)
w = model[-1].coef_[0]
print(f"C = {C:g}: train acc = {model.score(X_train, y_train):.3f}, "
f"test acc = {model.score(X_test, y_test):.3f}, "
f"max |w| = {np.abs(w).max():.1f}")
# Mô hình mặc định: 3 hệ số lớn nhất về trị tuyệt đối.
model = make_pipeline(StandardScaler(), LogisticRegression())
model.fit(X_train, y_train)
w = model[-1].coef_[0]
for j in np.argsort(-np.abs(w))[:3]:
print(f"{data.feature_names[j]:>19}: w = {w[j]:+.3f}, "
f"e^w = {np.exp(w[j]):.3f}")
# Xác suất lành tính của 5 khối u đầu tiên trong tập test.
proba = model.predict_proba(X_test[:5])[:, 1]
print("P(benign):", " ".join(f"{p:.3f}" for p in proba))
print("predicted:", model.predict(X_test[:5]), " true:", y_test[:5])

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

  • Mặc định C=1: độ chính xác test 0.958, tức 137/143 khối u, hệ số lớn nhất về trị tuyệt đối chỉ 1.2.
  • C=1e10: train đúng 1.000, tức có một siêu phẳng tách hẳn hai lớp trên tập train, và hệ số lớn nhất phình tới 205.9, đúng như mục Regularization. Test tụt còn 0.944: mô hình overfit.
  • Ba hệ số lớn nhất đều âm: radius error (độ dao động bán kính giữa các nhân tế bào, w=−1.213w = -1.213), worst texture (độ sần ở vùng tệ nhất, −1.107-1.107), mean concave points (số điểm lõm trung bình, −1.106-1.106). Đặc trưng đã chuẩn hóa, nên đọc là: tăng một độ lệch chuẩn radius error, giữ các đặc trưng khác nguyên, thì odds lành tính nhân với e−1.213=0.297e^{-1.213} = 0.297, tức còn chưa tới một phần ba. Hệ số âm ở đây nghĩa là nghiêng về ác tính, vì lớp 1 là lành tính.
  • Năm khối u đầu tiên của tập test có xác suất lành tính 0.996, 0.000, 0.000, 1.000, 0.003, và cả năm được đoán đúng. “0.000” chỉ là làm tròn: sigmoid không bao giờ cho đúng 0.
Câu 1/4 Một mô hình hồi quy logistic có hệ số w=0.7w = 0.7 cho đặc trưng "số giờ ôn thi". Một bạn đang có xác suất đậu là 0.5. Nếu bạn ấy ôn thêm đúng 1 giờ (các đặc trưng khác giữ nguyên), xác suất đậu mới khoảng bao nhiêu?
Câu 2/4 Mô hình có hai đặc trưng, w1=2w_1 = 2, w2=−1w_2 = -1, b=−3b = -3. Với ngưỡng 0.5, mô hình đoán gì cho điểm x=[3,2]⊤\mathbf{x} = [3, 2]^\top?
Câu 3/4 Một bạn đậu, nhưng mô hình đang cho y^=0.01\hat{y} = 0.01. Xét đạo hàm của mất mát của riêng mẫu này theo zz. Vì sao huấn luyện bằng log loss sửa sai cho mẫu này nhanh hơn bằng bình phương sai số (y^−y)2(\hat{y} - y)^2?
Câu 4/4 Bạn khớp LogisticRegression(C=1e10) trên dữ liệu đã chuẩn hóa. Độ chính xác trên tập train là 1.000, hệ số lớn nhất có trị tuyệt đối khoảng 200, và độ chính xác trên tập test thấp hơn mô hình mặc định. Nên chỉnh thế nào?

Ý chính:

  • Hồi quy logistic tính z=w⊤x+bz = \mathbf{w}^\top\mathbf{x} + b rồi đưa qua sigmoid: y^=σ(z)=1/(1+e−z)=P(y=1∣x)\hat{y} = \sigma(z) = 1/(1 + e^{-z}) = P(y = 1 \mid \mathbf{x}). Khác đường thẳng, đầu ra luôn trong (0,1)(0, 1) và không bị kéo bởi những điểm “quá đúng”.
  • Log-odds tuyến tính: ln⁡y^1−y^=w⊤x+b\ln\frac{\hat{y}}{1 - \hat{y}} = \mathbf{w}^\top\mathbf{x} + b. Tăng xjx_j một đơn vị thì odds nhân ewje^{w_j}. Ranh giới quyết định ở ngưỡng 0.5 là siêu phẳng w⊤x+b=0\mathbf{w}^\top\mathbf{x} + b = 0.
  • Log loss là −1m-\frac{1}{m} lần log-likelihood: L(w,b)=−1m∑i=1m[y(i)ln⁡y^(i)+(1−y(i))ln⁡(1−y^(i))]\mathcal{L}(\mathbf{w}, b) = -\frac{1}{m}\sum_{i=1}^{m}\left[y^{(i)}\ln\hat{y}^{(i)} + \left(1 - y^{(i)}\right)\ln\left(1 - \hat{y}^{(i)}\right)\right] Hàm mất mát này lồi, phạt nặng lỗi sai tự tin, và có gradient 1m∑i(y^(i)−y(i))xj(i)\frac{1}{m}\sum_i\left(\hat{y}^{(i)} - y^{(i)}\right)x^{(i)}_j, cùng dạng với hồi quy tuyến tính. Không có nghiệm dạng đóng, nên huấn luyện bằng gradient descent hoặc L-BFGS.
  • LogisticRegression mặc định phạt L2 với C=1, λ=12mC\lambda = \frac{1}{2mC}: C nhỏ là phạt mạnh. Phạt giữ hệ số hữu hạn khi dữ liệu tách được hoàn toàn.

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