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

Hồi quy tuyến tính

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

  • Hiểu hồi quy tuyến tính (linear regression) dự đoán thế nào, và vì sao đường tốt nhất là đường có sai số bình phương trung bình (mean squared error, MSE) nhỏ nhất.
  • Tự dẫn được công thức của ww và bb khi có một đặc trưng (feature), và đọc được phương trình chuẩn (normal equation) X⊤Xθ=X⊤y\mathbf{X}^\top\mathbf{X}\boldsymbol{\theta} = \mathbf{X}^\top\mathbf{y}.
  • Cài đặt bằng NumPy và scikit-learn, và giải thích được ý nghĩa của ww, bb.
  • Nhận ra các giới hạn chính, như điểm ngoại lai (outlier), ngoại suy (extrapolation), và lúc phương trình chuẩn có vô số nghiệm.

Bạn đang tìm thuê căn hộ. Bạn hỏi giá 10 căn quanh khu mình muốn ở và ghi lại thành bảng dưới đây. Cả bài sẽ dùng bảng này.

Giá thuê theo diện tích của 10 căn hộ. Số liệu tự tạo cho bài học, không phải giá thị trường.
Diện tích (m²)Giá thuê (triệu đồng/tháng)
256.7
306.5
368.8
428.8
489.4
5512.2
6012.0
6814.1
7513.9
8516.8

Rồi một người quen báo có căn 65 m² sắp trống. Giá bao nhiêu thì hợp lý?

Nhìn bảng, bạn thấy ngay căn càng rộng thì càng đắt. Nhưng không đều: căn 30 m² lại rẻ hơn căn 25 m², căn 75 m² rẻ hơn căn 68 m². Giá còn phụ thuộc vị trí, tầng, nội thất, những thứ bảng không ghi. Vì vậy không đường thẳng nào đi qua đủ 10 điểm.

Điều bạn làm được là tìm một đường đi gần các điểm nhất, rồi dùng nó để đoán giá căn 65 m². Đó là toàn bộ ý tưởng của hồi quy tuyến tính.

Hãy thử hai cách đoán đơn giản:

  • Đường A: bỏ qua diện tích, căn nào cũng đoán bằng giá trung bình của bảng, 10.92 triệu. Đây là một đường nằm ngang.
  • Đường B: “mỗi m² 200 nghìn đồng”. Căn 25 m² đoán 5.0 triệu, căn 85 m² đoán 17.0 triệu.

Với mỗi căn, lấy giá thật trừ giá mà đường đoán. Hiệu này gọi là phần dư (residual).

Với đường B, căn 25 m² có phần dư 6.7 − 5.0 = 1.7: điểm nằm trên đường. Căn 75 m² có phần dư 13.9 − 15.0 = −1.1: điểm nằm dưới đường. Trên đồ thị, phần dư là đoạn thẳng đứng nối điểm dữ liệu với đường. Muốn so hai đường, bạn cần một con số gom phần dư của cả 10 căn lại.

Cách nghĩ đầu tiên là cộng các phần dư lại. Nhưng tổng phần dư của đường A đúng bằng 0, vì phần dư dương và âm quanh giá trung bình bù trừ nhau vừa khít. Vậy mà đường A rất tệ: nó đoán căn 25 m² và căn 85 m² cùng một giá.

Cách sửa là bình phương từng phần dư, rồi lấy trung bình. Con số này chính là sai số bình phương trung bình, viết tắt MSE. Có ba lý do để chọn bình phương:

  1. Không triệt tiêu. Bình phương luôn không âm, nên sai về phía nào cũng bị tính.
  2. Phạt nặng sai số lớn. Lệch 1 triệu tính 1 điểm, lệch 2 triệu tính 4 điểm. Nên hai căn cùng lệch 1 triệu (2 điểm) tốt hơn một căn đúng hẳn và một căn lệch 2 triệu (4 điểm). Mặt trái: một điểm nằm rất xa sẽ kéo cả đường về phía nó, như bạn sẽ thấy ở ô code thứ ba.
  3. Lấy đạo hàm được. Hàm bình phương trơn, còn hàm trị tuyệt đối có góc nhọn tại 0, ở đó không có đạo hàm. Nhờ đạo hàm, bạn tìm được đường tốt nhất bằng đại số thay vì thử mò.

MSE của các đường, tính bằng Python trên bảng trên:

ĐườngMSE
A: nằm ngang ở giá trung bình 10.9210.50
B: mỗi m² 200 nghìn đồng0.88
Đường tốt nhất, sẽ tìm ở phần sau0.36

Đường B tốt hơn đường A rất nhiều, nhưng vẫn có đường tốt hơn nữa. Bài toán giờ đã rõ: trong mọi đường thẳng, tìm đường có MSE nhỏ nhất.

Bảng có mm mẫu (sample), mỗi mẫu là một căn hộ, nên m=10m = 10. Mẫu thứ ii gồm đặc trưng x(i)x^{(i)} là diện tích, và nhãn (label) y(i)y^{(i)} là giá thuê thật. Chỉ số (i)(i) viết ở trên nghĩa là “căn thứ ii”, không phải số mũ.

Mô hình đoán giá bằng một đường thẳng:

y^=wx+b\hat{y} = wx + b
  • y^\hat{y} (đọc là “y mũ”) là giá dự đoán, để phân biệt với giá thật yy.
  • ww là trọng số (weight). Khi chỉ có một đặc trưng, ww chính là độ dốc (slope) của đường: diện tích tăng 1 m² thì giá dự đoán tăng ww triệu.
  • bb là hệ số tự do (bias, còn gọi là intercept), chỗ đường thẳng cắt trục tung.

Phần lớn bài dùng một đặc trưng cho dễ nhìn. Mục dạng ma trận sẽ mở rộng cho nhiều đặc trưng.

Với căn thứ ii, giá dự đoán là y^(i)=wx(i)+b\hat{y}^{(i)} = wx^{(i)} + b, và phần dư là

e(i)=y(i)−y^(i)e^{(i)} = y^{(i)} - \hat{y}^{(i)}

Hàm mất mát (loss function) L\mathcal{L} là MSE, trung bình bình phương phần dư trên cả mm căn:

L(w,b)=1m∑i=1m(y^(i)−y(i))2=1m∑i=1m(wx(i)+b−y(i))2\begin{aligned} \mathcal{L}(w, b) &= \frac{1}{m}\sum_{i=1}^{m}\left(\hat{y}^{(i)} - y^{(i)}\right)^2 \\ &= \frac{1}{m}\sum_{i=1}^{m}\left(wx^{(i)} + b - y^{(i)}\right)^2 \end{aligned}

Vài điều cần để ý:

  • y^(i)−y(i)\hat{y}^{(i)} - y^{(i)} chỉ là phần dư đổi dấu. Bình phương lên thì như nhau.
  • Dữ liệu đã cố định, nên L\mathcal{L} chỉ còn là hàm của hai tham số (parameter) ww và bb.
  • Nhiều sách dùng 12m\frac{1}{2m} để số 2 triệt tiêu khi lấy đạo hàm. Nhân L\mathcal{L} với một hằng số dương không dời điểm thấp nhất, nên nghiệm không đổi. Bài này dùng 1m\frac{1}{m} cho khớp với MSE của scikit-learn.

Bài toán là tìm ww và bb làm L(w,b)\mathcal{L}(w, b) nhỏ nhất. Lời giải gọi là nghiệm bình phương tối thiểu (least squares).

L\mathcal{L} là tổng bình phương của các biểu thức bậc nhất theo ww và bb, nên đồ thị của nó theo (w,b)(w, b) là một cái bát ngửa lên. Chỗ duy nhất mặt bát nằm phẳng (mọi đạo hàm riêng bằng 0) là đáy bát, nên chỉ cần cho hai đạo hàm riêng bằng 0 rồi giải. Điều này cần các căn không cùng một diện tích (xem mục “Khi nào nghiệm không duy nhất”).

Gọi xˉ=1m∑i=1mx(i)\bar{x} = \frac{1}{m}\sum_{i=1}^{m} x^{(i)} là diện tích trung bình và yˉ=1m∑i=1my(i)\bar{y} = \frac{1}{m}\sum_{i=1}^{m} y^{(i)} là giá trung bình.

Bước 1: đạo hàm riêng theo bb. Theo quy tắc hàm hợp (chain rule), đạo hàm của u2u^2 bằng 2u2u nhân đạo hàm của uu. Ở đây u=wx(i)+b−y(i)u = wx^{(i)} + b - y^{(i)} có đạo hàm theo bb bằng 1. Vậy:

∂L∂b=2m∑i=1m(wx(i)+b−y(i))\frac{\partial\mathcal{L}}{\partial b} = \frac{2}{m}\sum_{i=1}^{m}\left(wx^{(i)} + b - y^{(i)}\right)

Cho đạo hàm này bằng 0 rồi chia hai vế cho 2. Tổng trong ngoặc tách thành w∑ix(i)+mb−∑iy(i)w\sum_{i} x^{(i)} + mb - \sum_{i} y^{(i)}. Chia cho mm, ba phần lần lượt thành wxˉw\bar{x}, bb và yˉ\bar{y}:

0=1m∑i=1m(wx(i)+b−y(i))=wxˉ+b−yˉ\begin{aligned} 0 &= \frac{1}{m}\sum_{i=1}^{m}\left(wx^{(i)} + b - y^{(i)}\right) \\ &= w\bar{x} + b - \bar{y} \end{aligned}

Chuyển vế:

b=yˉ−wxˉb = \bar{y} - w\bar{x}

Đọc bằng lời: yˉ=wxˉ+b\bar{y} = w\bar{x} + b, tức đường tốt nhất luôn đi qua điểm trung bình (mean point) (xˉ,yˉ)(\bar{x}, \bar{y}), và trung bình phần dư bằng 0. Đường A cũng đi qua điểm này (nên tổng phần dư bằng 0) mà vẫn tệ: điều kiện này cần nhưng chưa đủ, còn phải chọn đúng độ dốc.

Bước 2: đạo hàm riêng theo ww. Lần này uu có đạo hàm theo ww là x(i)x^{(i)}:

∂L∂w=2m∑i=1mx(i)(wx(i)+b−y(i))\frac{\partial\mathcal{L}}{\partial w} = \frac{2}{m}\sum_{i=1}^{m}x^{(i)}\left(wx^{(i)} + b - y^{(i)}\right)

Cho đạo hàm này bằng 0, rồi bỏ hệ số 2m\frac{2}{m} vì nó khác 0:

∑i=1mx(i)(wx(i)+b−y(i))=0\sum_{i=1}^{m}x^{(i)}\left(wx^{(i)} + b - y^{(i)}\right) = 0

Bước 3: thay bb từ bước 1 vào. Gọi độ lệch (deviation) so với trung bình là x~(i)=x(i)−xˉ\tilde{x}^{(i)} = x^{(i)} - \bar{x} và y~(i)=y(i)−yˉ\tilde{y}^{(i)} = y^{(i)} - \bar{y}. Biểu thức trong ngoặc trở thành:

wx(i)+b−y(i)=wx(i)+(yˉ−wxˉ)−y(i)=w(x(i)−xˉ)−(y(i)−yˉ)=wx~(i)−y~(i)\begin{aligned} & wx^{(i)} + b - y^{(i)} \\ &= wx^{(i)} + \left(\bar{y} - w\bar{x}\right) - y^{(i)} \\ &= w\left(x^{(i)} - \bar{x}\right) - \left(y^{(i)} - \bar{y}\right) \\ &= w\tilde{x}^{(i)} - \tilde{y}^{(i)} \end{aligned}

Dòng thứ ba gom riêng hai số hạng có ww và hai số hạng còn lại. Phương trình ở bước 2 thành:

∑i=1mx(i)(wx~(i)−y~(i))=0\sum_{i=1}^{m}x^{(i)}\left(w\tilde{x}^{(i)} - \tilde{y}^{(i)}\right) = 0

Bước 4: đổi x(i)x^{(i)} đứng trước ngoặc thành x~(i)\tilde{x}^{(i)}. Các độ lệch khỏi trung bình luôn có tổng bằng 0: ∑ix~(i)=∑ix(i)−mxˉ=0\sum_{i} \tilde{x}^{(i)} = \sum_{i} x^{(i)} - m\bar{x} = 0, tương tự ∑iy~(i)=0\sum_{i} \tilde{y}^{(i)} = 0. Do đó:

xˉ∑i=1m(wx~(i)−y~(i))=xˉ (w⋅0−0)=0\begin{aligned} & \bar{x}\sum_{i=1}^{m}\left(w\tilde{x}^{(i)} - \tilde{y}^{(i)}\right) \\ &= \bar{x}\,(w \cdot 0 - 0) = 0 \end{aligned}

Lấy phương trình ở bước 3 trừ đẳng thức này, mỗi x(i)x^{(i)} trước ngoặc thành x(i)−xˉx^{(i)} - \bar{x}, tức x~(i)\tilde{x}^{(i)}:

∑i=1mx~(i)(wx~(i)−y~(i))=0\sum_{i=1}^{m}\tilde{x}^{(i)}\left(w\tilde{x}^{(i)} - \tilde{y}^{(i)}\right) = 0

Bước 5: rút ww. Phá ngoặc, phương trình tách thành hai tổng:

w∑i=1m(x~(i))2−∑i=1mx~(i)y~(i)=0w\sum_{i=1}^{m}\left(\tilde{x}^{(i)}\right)^2 - \sum_{i=1}^{m}\tilde{x}^{(i)}\tilde{y}^{(i)} = 0

Chuyển tổng thứ hai sang vế phải, chia cho tổng thứ nhất, rồi viết lại theo xx, yy ban đầu:

w=∑i=1m(x(i)−xˉ)(y(i)−yˉ)∑i=1m(x(i)−xˉ)2w = \frac{\sum_{i=1}^{m}\left(x^{(i)} - \bar{x}\right)\left(y^{(i)} - \bar{y}\right)}{\sum_{i=1}^{m}\left(x^{(i)} - \bar{x}\right)^2}

Có ww rồi thì b=yˉ−wxˉb = \bar{y} - w\bar{x} theo bước 1.

Đọc công thức. Căn có hai độ lệch cùng dấu (rộng hơn và đắt hơn trung bình, hoặc nhỏ hơn và rẻ hơn) góp vào tử số một số dương, trái dấu thì góp số âm. Khi các số dương lấn át số âm thì w>0w > 0 và đường dốc lên. Mẫu số đo các diện tích trải rộng tới đâu, và chỉ bằng 0 khi mọi căn cùng diện tích.

Áp vào bảng căn hộ. Ở đây xˉ=52.4\bar{x} = 52.4, yˉ=10.92\bar{y} = 10.92, tử số bằng 598.32 và mẫu số bằng 3530.4. Do đó:

w=598.323530.4≈0.1695b≈10.92−0.1695×52.4≈2.04\begin{aligned} w &= \frac{598.32}{3530.4} \approx 0.1695 \\ b &\approx 10.92 - 0.1695 \times 52.4 \approx 2.04 \end{aligned}

Đường tốt nhất là y^=0.1695x+2.04\hat{y} = 0.1695x + 2.04, với MSE khoảng 0.3615. Mỗi m² thêm vào làm giá dự đoán tăng khoảng 0.17 triệu (170 nghìn đồng). Còn b≈2.04b \approx 2.04 chỉ là chỗ đường cắt trục tung, không phải “giá thuê căn 0 m²”: dữ liệu không có căn nào nhỏ như vậy.

Với nhiều đặc trưng, ví dụ thêm số phòng ngủ, mỗi căn là một vectơ x=[x1,…,xn]⊤\mathbf{x} = [x_1, \dots, x_n]^\top, mỗi xjx_j có trọng số wjw_j riêng, và dự đoán là tích vô hướng cộng bb:

y^=w⊤x+b\hat{y} = \mathbf{w}^\top\mathbf{x} + b

Ký hiệu ma trận giúp viết gọn bài toán cho mọi nn. Xếp dữ liệu thành ma trận thiết kế (design matrix) X\mathbf{X}: mỗi hàng là một căn, mỗi cột là một đặc trưng, thêm một cột toàn số 1 ở cuối. Giá thật xếp thành vectơ y\mathbf{y}. Với bảng căn hộ:

X=[251301⋮⋮851],y=[6.76.5⋮16.8]\mathbf{X} = \begin{bmatrix} 25 & 1 \\ 30 & 1 \\ \vdots & \vdots \\ 85 & 1 \end{bmatrix}, \qquad \mathbf{y} = \begin{bmatrix} 6.7 \\ 6.5 \\ \vdots \\ 16.8 \end{bmatrix}

Tham số gộp vào vectơ θ=[w,b]⊤\boldsymbol{\theta} = [w, b]^\top. Với nn đặc trưng, hàng ii của X\mathbf{X} là [ x1(i),…,xn(i),1 ][\,x^{(i)}_1, \dots, x^{(i)}_n, 1\,] (xj(i)x^{(i)}_j là đặc trưng thứ jj của căn thứ ii) và θ=[w1,…,wn,b]⊤\boldsymbol{\theta} = [w_1, \dots, w_n, b]^\top, nên X\mathbf{X} có mm hàng và n+1n + 1 cột.

Vì sao cần cột số 1? Tích vô hướng của hàng ii với θ\boldsymbol{\theta} là x(i)w+1⋅b=y^(i)x^{(i)}w + 1 \cdot b = \hat{y}^{(i)}: cột số 1 để bb được cộng vào mọi dự đoán. Vậy Xθ\mathbf{X}\boldsymbol{\theta} chứa cả mm giá dự đoán, và Xθ−y\mathbf{X}\boldsymbol{\theta} - \mathbf{y} chứa các y^(i)−y(i)\hat{y}^{(i)} - y^{(i)}. Bình phương độ dài của một vectơ, ∥v∥2=v12+⋯+vm2\lVert\mathbf{v}\rVert^2 = v_1^2 + \dots + v_m^2, là tổng bình phương các phần tử, nên MSE viết gọn thành:

L(θ)=1m∥Xθ−y∥2\mathcal{L}(\boldsymbol{\theta}) = \frac{1}{m}\left\lVert\mathbf{X}\boldsymbol{\theta} - \mathbf{y}\right\rVert^2

Cho mọi đạo hàm riêng của hàm này (theo w1,…,wnw_1, \dots, w_n và bb) bằng 0, bạn được n+1n + 1 phương trình bậc nhất. Viết bằng ma trận, cả hệ chỉ còn một dòng, gọi là phương trình chuẩn:

X⊤X θ=X⊤y\mathbf{X}^\top\mathbf{X}\,\boldsymbol{\theta} = \mathbf{X}^\top\mathbf{y}

Không cần đạo hàm ma trận để hiểu nó. Chuyển vế phải sang vế trái rồi đặt X⊤\mathbf{X}^\top ra ngoài, phương trình thành X⊤(Xθ−y)=0\mathbf{X}^\top\left(\mathbf{X}\boldsymbol{\theta} - \mathbf{y}\right) = \mathbf{0}. Hàng jj của vế trái là tích vô hướng giữa cột jj của X\mathbf{X} và Xθ−y\mathbf{X}\boldsymbol{\theta} - \mathbf{y}, tức ∑ixj(i)(y^(i)−y(i))\sum_{i} x^{(i)}_j\left(\hat{y}^{(i)} - y^{(i)}\right). Nhân với 2m\frac{2}{m}, hàng jj chính là ∂L/∂wj\partial\mathcal{L}/\partial w_j (quy tắc hàm hợp như bước 2), còn hàng cuối, ứng với cột số 1, là ∂L/∂b\partial\mathcal{L}/\partial b. Vậy phương trình chuẩn chỉ là các phương trình “đạo hàm bằng 0” xếp vào ma trận. Với bảng căn hộ, nó là hệ hai ẩn:

30988 w+524 b=6320.4524 w+10 b=109.2\begin{aligned} 30988\,w + 524\,b &= 6320.4 \\ 524\,w + 10\,b &= 109.2 \end{aligned}

Ở vế trái, 30988, 524 và 10 lần lượt là ∑i(x(i))2\sum_{i} \left(x^{(i)}\right)^2, ∑ix(i)\sum_{i} x^{(i)} và mm, các phần tử của X⊤X\mathbf{X}^\top\mathbf{X}. Ở vế phải, 6320.4 là ∑ix(i)y(i)\sum_{i} x^{(i)}y^{(i)} và 109.2 là ∑iy(i)\sum_{i} y^{(i)}, các phần tử của X⊤y\mathbf{X}^\top\mathbf{y}. Chia phương trình thứ hai cho 10 được 52.4 w+b=10.9252.4\,w + b = 10.92, đúng là xˉw+b=yˉ\bar{x}w + b = \bar{y} của bước 1.

Nếu X⊤X\mathbf{X}^\top\mathbf{X} khả nghịch (invertible), nghiệm viết được thành:

θ=(X⊤X)−1X⊤y\boldsymbol{\theta} = \left(\mathbf{X}^\top\mathbf{X}\right)^{-1}\mathbf{X}^\top\mathbf{y}

Đây là cách viết nghiệm, không phải cách nên tính. Trong code, hãy dùng np.linalg.solve(X.T @ X, X.T @ y), hoặc tốt hơn là np.linalg.lstsq(X, y), vì nó giải trực tiếp trên X\mathbf{X} mà không lập X⊤X\mathbf{X}^\top\mathbf{X} (LinearRegression của scikit-learn cũng vậy): lập ma trận này sẽ bình phương số điều kiện (condition number), mức mà sai số tương đối ở đầu vào, kể cả sai số làm tròn, có thể bị phóng đại ở kết quả. Với bảng căn hộ, số điều kiện theo chuẩn 2 (2-norm, mặc định của np.linalg.cond) tăng từ khoảng 165 lên khoảng 165² ≈ 27 nghìn, nhưng tính bằng ma trận nghịch đảo (inverse), bằng solve hay bằng lstsq thì vẫn cùng kết quả tới khoảng 14 chữ số.

Phương trình chuẩn luôn có nghiệm. Nghiệm là duy nhất khi X⊤X\mathbf{X}^\top\mathbf{X} khả nghịch, tức là khi các cột của X\mathbf{X} độc lập tuyến tính (linearly independent): không cột nào viết được thành tổng có hệ số của các cột khác. Ba trường hợp hay làm hỏng điều này:

  • Mọi mẫu có cùng giá trị của một đặc trưng. Nếu căn nào cũng 40 m², cột diện tích bằng 40 lần cột số 1.
  • Đặc trưng trùng lặp. Ví dụ có cả diện tích theo m² lẫn theo ft²: cột ft² bằng cột m² nhân khoảng 10.76. Các cột phụ thuộc tuyến tính như vậy, hoặc gần như vậy, gọi là cộng tuyến (collinearity).
  • Ít mẫu hơn tham số, tức m<n+1m < n + 1, ví dụ 5 căn hộ nhưng 8 đặc trưng.

Khi đó X⊤X\mathbf{X}^\top\mathbf{X} suy biến (singular), và có vô số θ\boldsymbol{\theta} cho cùng một dự đoán trên tập huấn luyện, với cùng MSE nhỏ nhất.

Với một đặc trưng, trường hợp đầu hiện rõ qua định thức (determinant). Dùng các phần tử của X⊤X\mathbf{X}^\top\mathbf{X} trong hệ ở trên:

det⁡(X⊤X)=m∑i(x(i))2−(∑ix(i))2=m(∑i(x(i))2−mxˉ2)=m∑i(x(i)−xˉ)2\begin{aligned} \det\left(\mathbf{X}^\top\mathbf{X}\right) &= m\sum_{i}\left(x^{(i)}\right)^2 - \Big(\sum_{i} x^{(i)}\Big)^2 \\ &= m\Big(\sum_{i}\left(x^{(i)}\right)^2 - m\bar{x}^2\Big) \\ &= m\sum_{i}\left(x^{(i)} - \bar{x}\right)^2 \end{aligned}

Dòng thứ hai thay ∑ix(i)=mxˉ\sum_{i} x^{(i)} = m\bar{x} rồi đặt mm ra ngoài. Dòng thứ ba đúng vì khai triển ∑i(x(i)−xˉ)2\sum_{i}\left(x^{(i)} - \bar{x}\right)^2 được ∑i(x(i))2−2xˉ⋅mxˉ+mxˉ2\sum_{i}\left(x^{(i)}\right)^2 - 2\bar{x} \cdot m\bar{x} + m\bar{x}^2, tức ∑i(x(i))2−mxˉ2\sum_{i}\left(x^{(i)}\right)^2 - m\bar{x}^2, đúng bằng biểu thức trong ngoặc. Định thức bằng 0 đúng khi mọi x(i)x^{(i)} bằng nhau, cũng là lúc mẫu số trong công thức của ww bằng 0. Hậu quả trong code và cách xử lý: xem hộp thứ ba ở mục Lỗi hay gặp.

Hồi quy tuyến tính đơn giản, nhanh, dễ giải thích, nhưng có bốn giới hạn:

  • Chỉ tuyến tính theo các đặc trưng bạn đưa vào. Muốn bám đường cong thì thêm đặc trưng như x2x^2 (hộp đầu ở mục Lỗi hay gặp). Thêm bao nhiêu là vừa thì xem bài Overfitting và Underfitting.
  • Nhạy với điểm ngoại lai. Một điểm lạ có thể kéo lệch cả đường (ô code thứ ba).
  • Ngoại suy dễ sai. Dữ liệu chỉ có từ 25 đến 85 m² (hộp thứ hai ở mục Lỗi hay gặp).
  • Tốn kém khi nn lớn. Lập X⊤X\mathbf{X}^\top\mathbf{X} cỡ (n+1)×(n+1)(n + 1) \times (n + 1) tốn cỡ mn2mn^2 phép tính, giải hệ thêm cỡ n3n^3. Bài Gradient Descent tìm đáy của cùng hàm L\mathcal{L} bằng từng bước nhỏ.

Giờ bạn tự tìm đường tốt nhất bằng tay. Hình dưới là biểu đồ phân tán (scatter plot) của 10 căn hộ. Thanh trượt ww làm đường xoay, thanh trượt bb nâng hoặc hạ cả đường. Các đoạn thẳng đứng là phần dư, MSE cập nhật ngay khi bạn kéo. Dấu × là điểm trung bình (52.4,10.92)(52.4, 10.92). Hãy thử:

  1. Lúc đầu w=0w = 0, b=10.9b = 10.9: đường nằm ngang, gần như đường A, MSE 10.5020.
  2. Tăng ww một chút, rồi hạ bb cho đường đi lại qua dấu ×. MSE giảm. Lặp lại vài lần.
  3. Đừng tăng ww mãi. Ở w=0.4w = 0.4, b=−10.0b = -10.0, đường vẫn đi sát dấu × nhưng quá dốc, MSE vọt lên 19.1240. Đi qua điểm trung bình là cần, nhưng chưa đủ.
  4. Khi MSE đủ nhỏ, hình hiện dòng “Rất gần đường tốt nhất”. Bấm Hiện đường tốt nhất để so với y^=0.1695x+2.04\hat{y} = 0.1695x + 2.04, MSE 0.3615. Thanh trượt đi theo bước 0.005 và 0.1, nên điểm gần nhất bạn chạm được là w=0.17w = 0.17, b=2.0b = 2.0, MSE 0.3617.
Kéo hai thanh trượt để đặt đường thẳng. Bạn thử tìm đường có MSE nhỏ nhất.
  • Một căn hộ
  • Điểm trung bình (52.4, 10.92)
  • Phần dư: đoạn thẳng đứng từ một điểm tới đường của bạn
Đường của bạny^\hat{y} = 0.0000xx + 10.90MSE = 10.5020
Đường tốt nhất (bình phương tối thiểu)y^\hat{y} = 0.1695xx + 2.04MSE = 0.3615

Mười điểm đi lên từ trái sang phải, từ căn 25 m² giá 6.7 triệu tới căn 85 m² giá 16.8 triệu. Căn rộng hơn thường đắt hơn, nhưng các điểm không nằm trên cùng một đường thẳng. Dấu hình chữ X đánh dấu điểm trung bình (52.4, 10.92). Đường tốt nhất luôn đi qua điểm này.

Ba ô code dưới đây làm lại các bước trên bằng Python. Ô nào cũng tự tạo bảng căn hộ, nên chạy ô nào trước cũng được. Ô thứ hai lần đầu chạy lâu hơn, vì trình duyệt phải tải thêm scikit-learn.

Ô đầu tiên lập ma trận thiết kế, giải phương trình chuẩn bằng np.linalg.solve, rồi tính MSE.

Phương trình chuẩn bằng NumPy

import numpy as np
# Bảng 10 căn hộ (dữ liệu minh họa, tự tạo cho bài học).
# area: diện tích (m²), rent: giá thuê (triệu đồng/tháng).
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
# Ma trận thiết kế X: cột diện tích, thêm một cột toàn số 1 cho hệ số tự do b.
m = len(area)
X = np.column_stack([area, np.ones(m)])
# Phương trình chuẩn: (X^T X) theta = X^T y.
# np.linalg.solve giải thẳng hệ phương trình, không tính ma trận nghịch đảo.
theta = np.linalg.solve(X.T @ X, X.T @ rent)
w, b = theta # thứ tự khớp với thứ tự cột của X
# MSE: trung bình bình phương phần dư.
y_hat = X @ theta
mse = np.mean((y_hat - rent) ** 2)
print(f"b = {b:.4f}")
print(f"w = {w:.4f}")
print(f"MSE = {mse:.4f}")

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

Kết quả là b=2.0394b = 2.0394, w=0.1695w = 0.1695 và MSE =0.3615= 0.3615, khớp với công thức một đặc trưng và với đường tốt nhất trong phần Minh họa.

Ô thứ hai dùng LinearRegression của scikit-learn, lớp giải đúng bài toán bình phương tối thiểu thông thường (ordinary least squares, OLS) ở trên. Ô này đối chiếu với công thức tay, dự đoán giá hai căn mới, rồi vẽ đường.

scikit-learn và dự đoán căn mới

import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
# Bảng 10 căn hộ (dữ liệu minh họa, tự tạo cho bài học).
# area: diện tích (m²), rent: giá thuê (triệu đồng/tháng).
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
# scikit-learn nhận X dạng bảng: m hàng (mẫu), n cột (đặc trưng). Ở đây n = 1.
X = area.reshape(-1, 1)
model = LinearRegression().fit(X, rent) # tự thêm hệ số tự do b
print(f"sklearn: w = {model.coef_[0]:.4f}, b = {model.intercept_:.4f}")
# Đối chiếu với công thức một đặc trưng ở phần Lý thuyết.
dx = area - area.mean()
dy = rent - rent.mean()
w = np.sum(dx * dy) / np.sum(dx**2)
b = rent.mean() - w * area.mean()
print(f"formula: w = {w:.4f}, b = {b:.4f}")
# Dự đoán hai căn mới: 65 m² (trong vùng dữ liệu) và 300 m² (xa ngoài vùng).
new_area = np.array([[65], [300]])
for a, p in zip(new_area[:, 0], model.predict(new_area)):
print(f"{a} m² -> {p:.2f}")
# Vẽ dữ liệu và đường tốt nhất, chỉ trong vùng có dữ liệu.
xs = np.array([[20], [90]])
plt.scatter(area, rent, label="apartments")
plt.plot(xs, model.predict(xs), color="C1", label="best line")
plt.xlabel("Area (m²)")
plt.ylabel("Rent (million VND/month)")
plt.legend()
plt.show()

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

Hai dòng đầu khớp nhau: scikit-learn và công thức tay cho cùng ww, bb. Hai điều cần nhớ khi dùng scikit-learn:

  • X phải là bảng hai chiều, mm hàng và nn cột, kể cả khi chỉ có một đặc trưng. Đó là lý do có reshape(-1, 1).
  • Trọng số nằm trong coef_, còn bb nằm trong intercept_. Bạn không cần tự thêm cột số 1.

Căn 65 m² được đoán 13.06 triệu đồng/tháng, nằm giữa giá căn 60 m² (12.0) và căn 68 m² (14.1). Nghe hợp lý. Căn 300 m² được đoán 52.88 triệu, một con số đáng ngờ (xem mục Lỗi hay gặp).

Ô thứ ba thử một điểm ngoại lai: một căn 110 m² bị gõ nhầm giá 19.0 thành 9.0. Ô này dùng np.linalg.lstsq để khớp đường trước và sau khi thêm điểm đó.

Một điểm ngoại lai

import matplotlib.pyplot as plt
import numpy as np
# Bảng 10 căn hộ (dữ liệu minh họa, tự tạo cho bài học).
# area: diện tích (m²), rent: giá thuê (triệu đồng/tháng).
# dataset: apartments
area = np.array([25, 30, 36, 42, 48, 55, 60, 68, 75, 85])
rent = np.array([6.7, 6.5, 8.8, 8.8, 9.4, 12.2, 12.0, 14.1, 13.9, 16.8])
def fit(x, y):
# np.linalg.lstsq giải bình phương tối thiểu thẳng trên X, không lập X^T X.
X = np.column_stack([x, np.ones(len(x))])
w, b = np.linalg.lstsq(X, y)[0]
return w, b
# Thêm căn 110 m², lớn hơn mọi căn khác, nhưng giá bị gõ nhầm 19.0 thành 9.0.
area_out = np.append(area, 110)
rent_out = np.append(rent, 9.0)
w1, b1 = fit(area, rent)
w2, b2 = fit(area_out, rent_out)
print(f"without outlier: w = {w1:.4f}, b = {b1:.2f}")
print(f"with outlier: w = {w2:.4f}, b = {b2:.2f}")
# Vẽ hai đường để so sánh.
xs = np.array([20, 115])
plt.scatter(area, rent, label="apartments")
plt.scatter([110], [9.0], color="C3", marker="x", s=80, label="outlier")
plt.plot(xs, w1 * xs + b1, color="C1", label="without outlier")
plt.plot(xs, w2 * xs + b2, color="C3", linestyle="--", label="with outlier")
plt.xlabel("Area (m²)")
plt.ylabel("Rent (million VND/month)")
plt.legend()
plt.show()

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

Chỉ thêm một điểm trong 11, độ dốc đã giảm hơn một nửa: ww từ 0.1695 xuống 0.0760 (từ khoảng 170 nghìn còn 76 nghìn đồng mỗi m²). Còn bb tăng từ 2.04 lên 6.36. Vì sao một điểm lại mạnh đến vậy?

  • So với đường cũ, điểm mới lệch gần 12 triệu. Bình phương lên được hơn 130, trong khi tổng bình phương phần dư của cả 10 căn kia chỉ khoảng 3.6. Muốn MSE nhỏ, đường buộc phải nghiêng về phía điểm này.
  • Điểm nằm ở mép phải, xa điểm trung bình theo chiều ngang, như người ngồi ở đầu bập bênh: càng xa trục, càng dễ làm tấm ván nghiêng. Điểm như vậy gọi là điểm đòn bẩy cao (high-leverage point).

Vì vậy, hãy vẽ dữ liệu ra xem và kiểm tra các điểm nằm xa đám đông trước khi khớp mô hình.

Câu 1/4 Vì sao MSE dùng bình phương phần dư, thay vì cộng thẳng các phần dư lại?
Câu 2/4 Mô hình học được y^=0.17x+2.04\hat{y} = 0.17x + 2.04, với xx là diện tích (m²) và y^\hat{y} là giá thuê (triệu đồng/tháng). Hai căn hơn kém nhau 10 m² thì giá dự đoán chênh nhau bao nhiêu?
Câu 3/4 Thay cho căn 110 m² ở ô code thứ ba, bạn thêm vào bảng gốc một căn 52 m², rất gần diện tích trung bình xˉ=52.4\bar{x} = 52.4, với giá gõ nhầm 10.0 thành 1.0. Đường bình phương tối thiểu thay đổi thế nào?
Câu 4/4 Trường hợp nào làm X⊤X\mathbf{X}^\top\mathbf{X} không khả nghịch?

Ý chính:

  • Hồi quy tuyến tính dự đoán y^=w⊤x+b\hat{y} = \mathbf{w}^\top\mathbf{x} + b và chọn tham số làm MSE nhỏ nhất. Bình phương để sai số không triệt tiêu, phạt nặng sai số lớn, và lấy đạo hàm được.
  • Với một đặc trưng: b=yˉ−wxˉb = \bar{y} - w\bar{x}, còn ww bằng tổng tích các độ lệch chia cho tổng bình phương độ lệch của xx. Đường tốt nhất đi qua điểm trung bình.
  • Dạng tổng quát là phương trình chuẩn X⊤Xθ=X⊤y\mathbf{X}^\top\mathbf{X}\boldsymbol{\theta} = \mathbf{X}^\top\mathbf{y}, với X\mathbf{X} có thêm cột số 1. Nghiệm duy nhất khi các cột của X\mathbf{X} độc lập tuyến tính. Trong code, dùng lstsq hoặc LinearRegression, không tự tính nghịch đảo.
  • wjw_j cho biết dự đoán đổi bao nhiêu khi xjx_j tăng 1 đơn vị và các đặc trưng khác giữ nguyên. Cẩn thận với ngoại suy và điểm ngoại lai.

Đọc thêm:

  • Vũ Hữu Tiệp (2016). Machine Learning cơ bản, Bài 3: Linear Regression. Tiếng Việt, có ví dụ NumPy và scikit-learn. Ký hiệu hơi khác: NN là số mẫu, cột số 1 đặt ở đầu.
  • James, G., Witten, D., Hastie, T., Tibshirani, R. (2013). An Introduction to Statistical Learning (bản 1), chương 3: Linear Regression. Mục 3.1: lời giải một đặc trưng. Mục 3.3.3: điểm ngoại lai, điểm đòn bẩy cao, cộng tuyến.
  • Ng, A. (2003). CS229 Lecture notes (PDF), Part I: Linear Regression (bản cũ, Stanford). Dẫn phương trình chuẩn bằng đạo hàm ma trận.
  • Deisenroth, M. P., Faisal, A. A., Ong, C. S. (2020). Mathematics for Machine Learning, chương 9: Linear Regression. Mục 9.4 giải thích phương trình chuẩn bằng hình chiếu vuông góc (orthogonal projection).
  • scikit-learn developers (2007–2026). scikit-learn 1.8 User Guide, mục 1.1.1 Ordinary Least Squares. Tài liệu chính thức của LinearRegression.

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