🔬 Bài 31: scikit-learn — Ba dòng thay cho cả Chặng 7
Chặng 8 · Học máy thực chiến (Bài 31 → 42) · đây là bài 1/12, bài mở chặng.
🎯 Mục tiêu: huấn luyện được một mô hình học máy bằng đúng ba dòng lệnh, và hiểu rõ ba dòng ấy làm gì bên trong.
👥 Đối tượng: học sinh cấp 2–3, sinh viên. Cần biết trước: chỉ cần NumPy cơ bản. Nếu đã học Bài 30 thì bài này sẽ đặc biệt thú vị, nhưng chưa học vẫn theo được.
📖 Cách đọc 3 lớp: mỗi phần có ba khối màu song song — 🟢 Lớp A trực giác, 🔵 Lớp B cơ chế, 🟣 Lớp C nghiên cứu. Đọc hết Lớp A trước rồi quay lại B và C cũng hoàn toàn ổn.
🤝 Một lời trước khi bắt đầu. Chặng 7 vừa rồi là chặng dốc nhất từ đầu khóa tới giờ. Nếu bạn thấy đuối, thấy có chỗ chưa thông, thấy hơi nản — đó là phản ứng bình thường, không phải dấu hiệu bạn không hợp với môn này.
Chặng 8 được chia nhỏ hẳn ra: mỗi bài chỉ có một ý mới, và cứ vài bài lại có một bài luyện tập không thêm gì mới để bạn kịp thở.
Riêng bài hôm nay chỉ có đúng một điều mới phải nhớ. Phần còn lại là nhìn lại thứ bạn đã tự làm được rồi — xem như một phần thưởng cho quãng đường vừa qua.
Sau bài này bạn sẽ:
- Huấn luyện được một mô hình bằng
fitvà dự đoán bằngpredict. - Mở được “nắp” mô hình để xem máy đã học ra chính xác những con số nào.
- Giải thích được vì sao kết quả của scikit-learn hơi lệch so với kết quả bạn tự tính ở Bài 30 — và vì sao con số của scikit-learn mới là con số đúng.
- Trả lời được một câu hỏi rất hay: nếu máy giải thẳng ra đáp án được, tại sao cả thế giới vẫn dùng gradient descent?
Ở Bài 30, bạn đã tự tay dựng một mô hình học máy hoàn chỉnh: viết hàm mất mát, viết hàm gradient, cho máy lặp 2000 vòng, và cuối cùng nó tìm ra đường thẳng y = 1,509·x + 3,018.
Hôm nay bạn sẽ làm lại đúng bài toán đó bằng ba dòng lệnh.
Và đây mới là phần thú vị: kết quả không chỉ giống. Nó còn chính xác hơn kết quả bạn vất vả tính ra. Phần 3 sẽ giải thích vì sao — và vì sao công sức ở Chặng 7 không hề phí một giây nào.
🔄 Bạn đang ở đâu — tóm tắt Chặng 7 trong năm dòng, phòng khi bạn quay lại sau một thời gian nghỉ:
| Bài 26 | Mọi dữ liệu đều quy về vector và ma trận — nhờ đó một dòng lệnh xử lý được cả nghìn điểm. |
| Bài 27 | Đạo hàm là chiếc la bàn: nó cho biết nhích tham số lên hay xuống thì sai số giảm. |
| Bài 28 | Hàm mất mát là địa hình: một con số duy nhất tóm tắt mô hình đang sai nhiều hay ít. |
| Bài 29 | Gradient descent là cách đi: xem dốc, bước một bước nhỏ, lặp lại rất nhiều lần. |
| Bài 30 | Ráp cả bốn thứ lại thành một mô hình hai tham số, tự viết từ con số không. |
1. Ba dòng đó là gì
scikit-learn (đọc là “sai-kít-lơn”, viết tắt quen thuộc là sklearn) là thư viện học máy phổ biến nhất thế giới cho những mô hình cỡ vừa. Nó miễn phí, và nó đã đóng gói sẵn đúng những thứ bạn tự viết ở Chặng 7.
Cách dùng nó gói gọn trong ba động tác, và ba động tác này giống hệt cách bạn dạy một đứa em nhỏ:
- Lấy ra một cuốn vở trắng. Bạn tạo một mô hình mới. Lúc này nó chưa biết gì cả — hoàn toàn rỗng.
- Cho em xem bài mẫu. Bạn đưa dữ liệu vào và bảo mô hình học. Đây là lệnh
fit. - Ra đề mới hỏi em. Bạn đưa một tình huống chưa từng thấy và hỏi kết quả. Đây là lệnh
predict.
Điều quan trọng nhất cần nhớ: thứ tự này không đảo được. Chưa fit mà đã predict thì cũng như bắt đứa em làm bài khi chưa dạy nó chữ nào — và scikit-learn sẽ báo lỗi thẳng chứ không đoán bừa.
Hình 1 — Ba động tác của scikit-learn. Trước khi fit, mô hình là một cuốn vở trắng. Sau khi fit, nó chứa đúng hai con số — và chỉ cần hai con số đó là dự đoán được mọi trường hợp mới.
Đây là toàn bộ chương trình. Ba dòng có dấu sao chính là ba động tác vừa nói:
import numpy as np from sklearn.linear_model import LinearRegression x = np.array([1, 2, 3, 4, 5, 6]) # gio tu hoc y = np.array([4.5, 6.2, 7.4, 9.1, 10.4, 12.2]) # diem thi X = x.reshape(-1, 1) # Bai 32 se giai thich dong nay mo_hinh = LinearRegression() # (*) 1 - chon mo hinh mo_hinh.fit(X, y) # (*) 2 - cho may hoc print(mo_hinh.predict([[7]])) # (*) 3 - hoi may -> [13.58]
Bảy dòng, trong đó bốn dòng đầu chỉ là chuẩn bị dữ liệu và khai báo. Phần “học máy” thật sự đúng bằng ba dòng.
Hãy so với Bài 30: ở đó bạn phải tự viết hàm mat_mat, hàm gradient, rồi một vòng for 2000 lượt với hai phép cập nhật bên trong. Tất cả những thứ ấy giờ nằm gọn trong chữ fit.
Điểm khiến scikit-learn thắng các thư viện khác không phải là thuật toán — mà là sự nhất quán của giao diện. Mọi mô hình trong thư viện, từ đơn giản nhất tới phức tạp nhất, đều dùng đúng ba động tác ấy.
# Doi mo hinh chi phai sua DUNG MOT DONG: from sklearn.tree import DecisionTreeRegressor mo_hinh = DecisionTreeRegressor() # <- chi dong nay doi mo_hinh.fit(X, y) # y nguyen mo_hinh.predict([[7]]) # y nguyen
Hệ quả rất lớn trong thực tế: muốn thử mười mô hình khác nhau trên cùng một bài toán, bạn viết một vòng lặp qua mười cái tên, phần còn lại của chương trình không đổi một chữ. Người ta gọi quy ước này là estimator API, và nhiều thư viện ra sau đã bắt chước lại nó.
Cũng cần biết ranh giới: scikit-learn mạnh với dữ liệu dạng bảng cỡ vừa — từ vài chục tới vài triệu dòng, chạy trên một máy tính thường. Với ảnh, âm thanh hay ngôn ngữ, người ta dùng PyTorch hoặc TensorFlow. Nhưng ngay cả ở đó, ba động tác này vẫn là khuôn mẫu tư duy.
2. Mở nắp mô hình: máy đã học được gì?
Sau khi fit chạy xong, có một câu hỏi rất đáng hỏi: “học” xong rồi thì kiến thức nằm ở đâu?
Câu trả lời gọn đến mức hơi bất ngờ: nằm trong hai con số. Đúng hai con số w và b của Bài 30, không hơn.
Bạn có thể vứt bỏ toàn bộ dữ liệu của 6 học sinh đi. Chỉ cần giữ lại w = 1,5086 và b = 3,0200 là dự đoán được cho bất kỳ ai. Đó chính là ý nghĩa của việc “học”: nén một đống dữ liệu thành một nhúm con số dùng được.
Ý này nhỏ nhưng đi rất xa. Một mô hình ngôn ngữ khổng lồ cũng làm đúng việc đó — chỉ khác là thay vì hai con số, nó giữ lại hàng trăm tỉ con số.
Hai con số đó nằm ở coef_ (độ dốc) và intercept_ (điểm cắt trục dọc):
mo_hinh.fit(X, y) print(mo_hinh.coef_) # [1.50857143] <- do doc w print(mo_hinh.intercept_) # 3.0199999999999987 <- diem cat b # Tu hai con so nay, tu tinh lay khong can predict: w = mo_hinh.coef_[0] b = mo_hinh.intercept_ print(w*7 + b) # 13.579999999999998 print(mo_hinh.predict([[7]])) # [13.58] -> cung mot ket qua
Chú ý coef_ là một mảng chứ không phải một số, nên phải lấy coef_[0]. Lý do: bài này chỉ có một đặc trưng đầu vào, nhưng thư viện được viết cho trường hợp tổng quát có nhiều đặc trưng — khi đó mỗi đặc trưng có một độ dốc riêng, và coef_ là cả danh sách.
Đừng hoảng khi thấy 3.0199999999999987. Máy tính lưu số thập phân dưới dạng nhị phân, và 3,02 không biểu diễn được chính xác bằng nhị phân — y như 1/3 không viết hết được bằng số thập phân. Sai lệch nằm ở chữ số thứ mười lăm, hoàn toàn vô hại. Muốn nhìn cho gọn thì dùng round(b, 4), sẽ ra 3.02.
Để ý dấu gạch dưới ở cuối tên: coef_, intercept_. Đây không phải lỗi gõ mà là một quy ước xuyên suốt scikit-learn: tên kết thúc bằng dấu gạch dưới nghĩa là thuộc tính chỉ tồn tại sau khi đã fit.
Nhờ quy ước này, chỉ cần liếc tên là biết cái gì do bạn đặt và cái gì do máy học ra:
| Tên | Ai quyết định | Có từ lúc nào |
|---|---|---|
fit_intercept | Bạn đặt, khi tạo mô hình | Ngay từ đầu |
coef_ | Máy học ra từ dữ liệu | Chỉ sau khi fit |
intercept_ | Máy học ra từ dữ liệu | Chỉ sau khi fit |
n_features_in_ | Máy ghi nhận từ dữ liệu | Chỉ sau khi fit |
Nhóm thứ nhất gọi là siêu tham số (hyperparameter) — do con người chọn. Nhóm thứ hai gọi là tham số (parameter) — do máy tự tìm. Bạn đã gặp cả hai ở Bài 29 rồi: tốc độ học là siêu tham số, còn w là tham số. Phân biệt được hai nhóm này là điều kiện để hiểu Bài 36.
3. Vì sao con số hơi khác — và con số nào mới đúng?
Đặt hai kết quả cạnh nhau, bạn sẽ thấy chúng không trùng khít:
| Cách làm | w | b |
|---|---|---|
| Bài 30 — gradient descent 2000 vòng | 1,5090 | 3,0183 |
| Bài 31 — scikit-learn | 1,5086 | 3,0200 |
Chênh nhau ở chữ số thứ tư. Vậy ai đúng?
scikit-learn đúng. Và lý do rất dễ hình dung.
Gradient descent giống một người bịt mắt lần xuống lòng chảo. Mỗi bước anh ta sờ xem chỗ nào thấp hơn rồi nhích tới. Càng gần đáy, mặt chảo càng phẳng, bước chân càng ngắn lại. Đi 2000 bước thì đã rất gần đáy — nhưng gần, không phải đúng. Đi thêm 2000 bước nữa sẽ gần hơn chút nữa, mãi mãi không chạm hẳn.
scikit-learn thì không lần mò. Với riêng bài toán đường thẳng này, toán học cho sẵn một công thức tính thẳng ra đáy. Nó thay số vào công thức và đáp xuống đúng điểm thấp nhất ngay lập tức, không bước nào cả.
Hình 2 — Chấm đỏ là các bước của gradient descent: càng gần đáy bước càng ngắn, nên nó tiến mãi mà không chạm hẳn. Mũi tên xanh là scikit-learn: tính thẳng ra đáy. Chênh lệch Loss chỉ ở chữ số thứ sáu — nhưng scikit-learn vẫn thấp hơn, và luôn luôn thấp hơn.
Có một cách kiểm tra dứt điểm xem ai đúng: đo Loss của cả hai. Đáy chảo là chỗ Loss nhỏ nhất, nên bộ tham số nào cho Loss thấp hơn thì bộ đó gần đáy hơn.
def mse(w, b): return ((w*x + b - y) ** 2).mean() print(mse(1.5090, 3.0183)) # 0.015620 <- Bai 30 print(mse(1.5086, 3.0200)) # 0.015619 <- sklearn, thap hon
scikit-learn thấp hơn. Không phải chút xíu do may mắn — mà vì không tồn tại cặp (w, b) nào cho Loss thấp hơn con số đó. Đó là định nghĩa của đáy.
Nhân đây, để ý một điều: chênh lệch giữa hai kết quả nhỏ tới mức không ảnh hưởng gì tới dự đoán thực tế. Học 7 giờ, Bài 30 đoán 13,58 điểm, scikit-learn cũng đoán 13,58 điểm. Trong đời thực, sai số của phép đo dữ liệu lớn hơn khoảng chênh này cả nghìn lần. Biết mình đang chính xác tới mức nào — và mức đó có đáng bận tâm không — là một kỹ năng riêng.
Giờ là câu hỏi hay nhất của bài, và bạn nên tự hỏi nó trước khi đọc tiếp:
Nếu tính thẳng ra đáy được, tại sao cả ngành học máy vẫn dùng gradient descent?
Vì cái công thức thần kỳ ấy hầu như không bao giờ tồn tại. Nó chỉ có với một nhóm rất hẹp các mô hình đơn giản mà hồi quy tuyến tính là đại diện. Hai lý do:
- Đa số mô hình không có công thức nào cả. Với một mạng nơ-ron, phương trình “chỗ nào đạo hàm bằng 0” không giải ra được bằng bút giấy, cũng không bằng máy. Không có đường tắt nào để nhảy thẳng tới đáy — chỉ còn cách bò từng bước.
- Ngay cả khi có công thức, nó cũng có thể quá đắt. Công thức đóng cho hồi quy tuyến tính đòi hỏi nghịch đảo một ma trận vuông có cạnh bằng số đặc trưng. Với 20 đặc trưng thì tức thì; với một triệu đặc trưng thì không máy nào kham nổi. Lúc đó gradient descent lại nhanh hơn.
Nói cách khác: hôm nay bạn vừa gặp trường hợp ngoại lệ may mắn. Bài toán đường thẳng nhỏ tới mức có đường tắt. Ra khỏi trường hợp đó — tức là gần như toàn bộ học máy hiện đại, từ nhận dạng ảnh tới các mô hình ngôn ngữ — con đường duy nhất vẫn là đúng vòng lặp bạn tự viết ở Bài 29 và 30.
Chặng 7 không hề phí. Nó là thứ đang chạy bên trong chữ fit, và là thứ duy nhất còn lại khi bài toán lớn lên.
⚠️ Về dòng X = x.reshape(-1, 1): bạn sẽ thấy nó ở mọi ví dụ trong bài, và có thể thắc mắc vì sao phải viết thêm. Câu trả lời ngắn: scikit-learn bắt dữ liệu đầu vào phải xếp theo dạng bảng — mỗi dòng là một mẫu, mỗi cột là một đặc trưng — nên một dãy số phẳng phải được dựng thành bảng một cột.
Bỏ dòng này đi, chương trình sẽ báo lỗi Expected 2D array, got 1D array instead. Đây là lỗi phổ biến nhất của người mới dùng scikit-learn. Tạm thời cứ coi nó là thủ tục bắt buộc; Bài 32 dành trọn vẹn để nói cho kỹ, kể cả vì sao y thì lại không cần.
📝 Bảng ghi nhớ
| Việc cần làm | Câu lệnh | Ghi chú |
|---|---|---|
| Nhập mô hình | from sklearn.linear_model import LinearRegression | Mỗi loại mô hình ở một ngăn khác nhau |
| Tạo mô hình rỗng | mo_hinh = LinearRegression() | Chưa biết gì cả |
| Cho máy học | mo_hinh.fit(X, y) | Đây là toàn bộ Chặng 7 gói lại |
| Dự đoán | mo_hinh.predict([[7]]) | Hai lớp ngoặc: một bảng, một dòng |
| Xem độ dốc | mo_hinh.coef_[0] | Là mảng, vì có thể có nhiều đặc trưng |
| Xem điểm cắt | mo_hinh.intercept_ | Là một số |
| Dựng bảng một cột | X = x.reshape(-1, 1) | Bài 32 giải thích kỹ |
| Dấu gạch dưới cuối tên | coef_, intercept_ | Nghĩa là “chỉ có sau khi fit” |
✎ Kiểm tra nhanh
Câu 1. Ba động tác của scikit-learn theo đúng thứ tự là:
- predict → fit → chọn mô hình
- chọn mô hình → fit → predict
- fit → chọn mô hình → predict
- chọn mô hình → predict → fit
fit), rồi mới hỏi được (predict). Giống hệt việc dạy một đứa em: lấy vở ra, dạy, rồi mới ra đề.Câu 2. Lệnh mo_hinh.fit(X, y) làm việc gì?
- Vẽ đồ thị của dữ liệu
- Dự đoán kết quả cho những trường hợp mới
- Dò tìm bộ tham số làm mô hình sai ít nhất trên dữ liệu được đưa vào
- Chia dữ liệu thành hai phần để kiểm tra
fit chính là toàn bộ Chặng 7 đóng gói lại: đo sai số, tìm hướng chỉnh, chỉnh tham số cho tới khi sai số nhỏ nhất. Việc chia dữ liệu ở phương án cuối là chuyện của Bài 34, và nó không tự động xảy ra ở đây — một điểm rất quan trọng sẽ nói kỹ sau.Câu 3. Sau khi fit, muốn xem độ dốc mà máy học được thì đọc thuộc tính nào?
mo_hinh.wmo_hinh.coef_[0]mo_hinh.predictmo_hinh.slope
coef_ là một mảng các độ dốc — mỗi đặc trưng đầu vào một cái. Bài này chỉ có một đặc trưng nên lấy phần tử đầu tiên: coef_[0]. Dấu gạch dưới ở cuối nhắc rằng thuộc tính này chỉ tồn tại sau khi đã fit.Câu 4. Gọi predict khi chưa hề gọi fit thì chuyện gì xảy ra?
- Trả về giá trị 0
- Trả về một dự đoán ngẫu nhiên
- Báo lỗi, vì mô hình chưa học gì cả
- Tự động chạy
fitrồi mới dự đoán
NotFittedError. Đây là thiết kế có chủ ý và rất đáng quý: thư viện thà dừng hẳn còn hơn trả về một con số trông có vẻ hợp lý nhưng vô nghĩa. Trong học máy, kết quả sai mà im lặng nguy hiểm hơn nhiều so với một thông báo lỗi.Câu 5. Bài 30 cho w = 1,5090, scikit-learn cho w = 1,5086. Kết luận đúng là:
- scikit-learn sai, vì nó không chạy gradient descent
- Bài 30 sai hoàn toàn, phải làm lại
- scikit-learn tính thẳng ra đáy nên đúng; gradient descent chỉ tiến gần tới đáy, 2000 bước vẫn còn lệch một chút
- Hai kết quả khác nhau vì dùng dữ liệu khác nhau
Câu 6. Nếu máy tính thẳng ra đáp án được như vậy, tại sao cả ngành học máy vẫn dùng gradient descent?
- Vì gradient descent luôn cho kết quả chính xác hơn
- Vì công thức tính thẳng chỉ tồn tại với vài mô hình đơn giản; mạng nơ-ron và các mô hình lớn không có công thức nào như thế
- Vì scikit-learn chỉ chạy được trên máy tính mạnh
- Vì gradient descent tốn ít bộ nhớ hơn trong mọi trường hợp
💻 Thực hành: ba dòng, chạy thật
▶ Cách dùng: bấm Khởi động Python một lần (lần đầu mất khoảng 20–30 giây vì phải tải cả NumPy, scikit-learn và Matplotlib), đợi báo sẵn sàng rồi bấm Chạy code. Sửa thoải mái trong ô đen và chạy lại bao nhiêu lần tùy thích.
💡 Thử thách (làm được cả ba là bạn đã nắm chắc bài):
- Bỏ chữ
.reshape(-1, 1)đi, tức là viếtmo_hinh.fit(x, y)với chữxthường. Chạy lại và đọc kỹ thông báo lỗi. Nó nhắc bạn làm gì? Đọc quen thông báo lỗi là kỹ năng đáng giá hơn nhiều người tưởng. - Xóa dòng
mo_hinh.fit(X, y)rồi chạy. Lỗi lần này tên là gì, và vì sao thư viện thà báo lỗi chứ không chịu trả về một con số? - Đổi một điểm dữ liệu thành số vô lý — ví dụ đổi
12.2thành40(một bạn học 6 giờ mà được 40 điểm). Chạy lại và xemw,bnhảy đi bao xa. Chỉ một điểm hỏng đã kéo cả đường thẳng lệch đi — nhớ lại Bài 28: bình phương sai số phạt rất nặng những điểm ở xa. Đây cũng là lý do dữ liệu bẩn nguy hiểm đến thế.
Bài tiếp theo: hôm nay ta đã lướt qua dòng X = x.reshape(-1, 1) và hẹn giải thích sau. Bài 32 sẽ trả món nợ đó: vì sao scikit-learn bắt dữ liệu phải xếp thành bảng, vì sao X viết hoa còn y viết thường, và làm sao để không bao giờ dính lại cái lỗi Expected 2D array nữa. Chỉ một ý, nhưng là ý mà thiếu nó thì mọi bài sau đều vướng.