🔬 Bài 32: Dữ liệu phải xếp hình gì — X và y
Chặng 8 · Học máy thực chiến (Bài 31 → 42) · đây là bài 2/12 của chặng.
🎯 Mục tiêu: xếp dữ liệu đúng hình dạng mà scikit-learn đòi hỏi, và không bao giờ dính lại lỗi Expected 2D array nữa.
👥 Yêu cầu: Cần biết trước: Bài 31 (ba động tác fit, predict).
📖 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.
Sau bài này bạn sẽ:
- Giải thích được vì sao
Xbắt buộc phải là bảng hai chiều, cònythì không. - Nhìn
.shapelà biết ngay dữ liệu đang xếp đúng hay sai. - Phân biệt được
reshape(-1, 1)vàreshape(1, -1)— hai thứ trông giống nhau mà ý nghĩa ngược nhau. - Đọc hiểu và tự sửa được lỗi
Expected 2D array, thay vì chép đại một dòng từ trên mạng.
Bài 31 có một dòng ta lướt qua và hẹn giải thích sau:
X = x.reshape(-1, 1) # Bai 32 se giai thich dong nay
Hôm nay trả nợ. Đây là bài ngắn về lý thuyết nhưng đáng giá bậc nhất chặng: thiếu nó thì mọi bài sau đều vướng, và Expected 2D array là lỗi mà gần như ai học scikit-learn cũng gặp trong tuần đầu tiên.
Chỉ có một điều mới phải nhớ: scikit-learn luôn muốn dữ liệu xếp thành bảng. Hiểu được vì sao, mọi thứ còn lại tự sáng ra.
1. Vì sao bắt buộc phải là bảng?
Ở Bài 31 ta chỉ dùng một thứ để đoán điểm: số giờ tự học. Nhưng đời thực đâu chỉ có một thứ. Điểm thi còn phụ thuộc số buổi vắng, số bài tập đã làm, số giờ ngủ…
scikit-learn được viết sẵn cho trường hợp nhiều thứ. Nên nó đòi dữ liệu ở dạng một cuốn sổ điểm quen thuộc:
- Mỗi dòng là một học sinh — thuật ngữ gọi là một mẫu (sample).
- Mỗi cột là một thứ ta đo được — thuật ngữ gọi là một đặc trưng (feature).
Nhìn theo cách đó thì bài 31 không phải ngoại lệ, nó chỉ là cuốn sổ có đúng một cột. Và một cột vẫn là bảng — đó là lý do phải reshape.
Câu hỏi tự nhiên: sao thư viện không tự đoán hộ? Vì nó không đoán được. Đưa vào sáu con số, thư viện không có cách nào biết đó là sáu học sinh mỗi bạn một phép đo, hay một học sinh với sáu phép đo. Hai cách hiểu cho ra hai bài toán khác hẳn nhau. Nên nó dừng lại và hỏi bạn, thay vì đoán bừa.
Hình 1 — Sáu học sinh, hai thứ được đo. X là bảng nên có hai chiều; y chỉ là một dãy số nên có một chiều. Toàn bộ bài học nằm gọn trong hai dòng shape ở khung vàng.
Dựng bảng bằng np.column_stack — đúng như tên gọi: xếp các cột cạnh nhau.
import numpy as np from sklearn.linear_model import LinearRegression gio = np.array([1, 2, 3, 4, 5, 6]) # cot 1 vang = np.array([0, 2, 1, 3, 0, 1]) # cot 2 y = np.array([4.5, 4.6, 6.7, 6.5, 10.5, 11.3]) X = np.column_stack([gio, vang]) print(X.shape, y.shape) # (6, 2) (6,) mo_hinh = LinearRegression().fit(X, y) print(mo_hinh.coef_) # [ 1.4944 -0.8045] print(mo_hinh.intercept_) # 3.0581
Để ý: coef_ giờ có hai số, mỗi cột một số. Bài 31 đã báo trước điều này khi bắt ta viết coef_[0].
Và hai con số ấy đọc được thành lời:
+1,4944— học thêm 1 giờ thì được thêm khoảng 1,5 điểm.−0,8045— vắng thêm 1 buổi thì mất khoảng 0,8 điểm. Dấu âm nói lên chiều tác động.
Bộ số liệu này tôi tạo ra từ một quy luật giấu sẵn: điểm = 1,5 × giờ − 0,8 × vắng + 3, rồi thêm chút nhiễu. Máy tìm lại được 1,4944 và −0,8045 — gần như đúng y quy luật thật mà nó chưa từng được cho biết.
Quy ước (n_samples, n_features) — số mẫu trước, số đặc trưng sau — không phải sở thích riêng của scikit-learn. Nó là chuẩn chung của gần như cả ngành: pandas, PyTorch, TensorFlow đều xếp dữ liệu bảng theo thứ tự này. Học một lần dùng được khắp nơi.
Sau khi fit, mô hình ghi nhớ số cột nó đã thấy và sẽ từ chối nếu lần sau bạn đưa số cột khác:
print(mo_hinh.n_features_in_) # 2 <- da hoc tren 2 cot mo_hinh.predict([[7]]) # loi: dua vao 1 cot, doi 2
Đây lại là một ví dụ của tinh thần đã gặp ở Bài 31: thà báo lỗi còn hơn trả về con số vô nghĩa. Nhớ dấu gạch dưới cuối tên n_features_in_ — nó nói rằng thuộc tính này chỉ có sau khi fit.
Thêm cột có ích thì mô hình khá lên thật. Trên bộ số liệu này:
# Chi dung cot 'gio' : MSE = 0.7443 # Dung ca 'gio' va 'vang' : MSE = 0.0088
Giảm hơn 85 lần. Nhưng đừng vội kết luận “cứ thêm cột là tốt”. Thêm cột vô nghĩa vào cũng làm Loss giảm trên chính dữ liệu đã học, mà mô hình lại tệ đi khi gặp dữ liệu mới. Đó chính là cái bẫy Bài 35 sẽ mổ xẻ.
2. Vì sao X viết hoa mà y viết thường?
Không phải ngẫu nhiên, cũng không phải ai đó gõ nhầm. Đây là quy ước mượn từ toán học:
- Chữ hoa cho thứ có hai chiều — bảng, ma trận. Nên
X. - Chữ thường cho thứ có một chiều — dãy, vector. Nên
y.
Cái lợi rất thực tế: nhìn tên biến là đoán được hình dạng. Đọc code người khác thấy X_train, y_test là biết ngay cái nào là bảng, cái nào là dãy, chưa cần chạy thử.
Còn vì sao y không cần là bảng? Vì mỗi học sinh chỉ có một đáp số duy nhất — một điểm thi. Sáu học sinh thì sáu con số, xếp thành một dãy là đủ. Không có gì để làm thành cột thứ hai.
.shape là công cụ kiểm tra nhanh nhất. Nó trả về hình dạng dưới dạng một bộ số:
print(X.shape) # (6, 2) -> 2 so = 2 chieu = bang print(y.shape) # (6,) -> 1 so = 1 chieu = day print(X.ndim, y.ndim) # 2 1
Dấu phẩy lẻ loi trong (6,) hay làm người mới bối rối. Nó chỉ là cách Python viết một bộ có đúng một phần tử, để phân biệt với dấu ngoặc thường. (6,) là “một chiều, dài 6”; còn (6, 1) là “hai chiều: 6 dòng, 1 cột”. Hai thứ khác nhau.
Thói quen nên tập: trước mỗi lần fit, in X.shape và y.shape ra xem. Số đầu tiên của cả hai phải bằng nhau — vì mỗi dòng dữ liệu phải có đúng một đáp số đi kèm.
Có trường hợp y cũng là bảng hai chiều: khi mỗi mẫu cần dự đoán nhiều đáp số cùng lúc. Ví dụ từ chiều cao và cân nặng của một người, dự đoán đồng thời cả huyết áp tâm thu lẫn tâm trương — hai số cho mỗi người, nên y.shape = (n, 2). Người ta gọi là bài toán nhiều đầu ra (multi-output), và tên biến lúc đó thường được viết hoa thành Y cho đúng quy ước.
Trường hợp đó hiếm, và ta sẽ không gặp trong chặng này. Nhưng biết nó tồn tại giúp bạn hiểu vì sao quy ước chữ hoa – chữ thường lại đáng giữ: nó mang thông tin thật, không phải trang trí.
3. reshape(−1, 1) — và cách không dính lỗi nữa
reshape nghĩa là xếp lại cùng những con số ấy sang một hình dạng khác. Không thêm, không bớt, chỉ đổi cách bày.
Còn số -1 là một mẹo rất tiện: nó có nghĩa “chỗ này tự tính hộ tôi”. Bạn chỉ cần nói rõ một chiều, chiều còn lại NumPy tự suy ra.
| Câu lệnh | Đọc thành lời | Kết quả với 6 số |
|---|---|---|
reshape(-1, 1) | “Cho tôi 1 cột, bao nhiêu dòng tùy” | 6 dòng × 1 cột — 6 học sinh, mỗi bạn 1 phép đo |
reshape(1, -1) | “Cho tôi 1 dòng, bao nhiêu cột tùy” | 1 dòng × 6 cột — 1 học sinh với 6 phép đo |
Hai dòng lệnh trông na ná nhau, chỉ đảo chỗ hai con số, nhưng ý nghĩa ngược hẳn. Chọn nhầm là sai cả bài toán.
Cách nhớ chắc nhất, không cần thuộc lòng: đếm xem bạn có bao nhiêu học sinh. Sáu học sinh thì bảng phải có sáu dòng. Chỉ có reshape(-1, 1) cho ra sáu dòng.
Hình 2 — Số -1 nghĩa là “tự tính hộ tôi”. Bạn chỉ định một chiều, NumPy suy ra chiều kia. Đảo chỗ hai con số là đảo luôn ý nghĩa của cả bộ dữ liệu.
Đây là nguyên văn lỗi khi đưa mảng một chiều vào fit. Đáng đọc kỹ, vì bản thân nó đã nói ra cách sửa:
ValueError: Expected 2D array, got 1D array instead: array=[1 2 3 4 5 6]. Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.
Tạm dịch: “Tôi cần mảng 2 chiều, bạn lại đưa mảng 1 chiều. Hãy xếp lại bằng reshape(-1, 1) nếu dữ liệu của bạn có một đặc trưng, hoặc reshape(1, -1) nếu nó là một mẫu.”
Thư viện không thể chọn hộ, vì chỉ bạn mới biết sáu con số đó là gì. Nhưng nó đã đặt sẵn hai đáp án lên bàn.
Nếu chọn nhầm, bạn sẽ gặp lỗi thứ hai — và lỗi này còn dễ hiểu hơn:
X_sai = gio.reshape(1, -1) # shape (1, 6) LinearRegression().fit(X_sai, y) ValueError: Found input variables with inconsistent numbers of samples: [1, 6]
“Số mẫu không khớp: 1 và 6”. Nghĩa là X bảo có 1 học sinh còn y có 6 điểm thi. Đúng cái quy tắc ở Lớp B phần 2: số đầu tiên của hai shape phải bằng nhau.
Quy tắc “phải là bảng” áp dụng cho cả predict, và đây là chỗ hay vấp nhất. Ba cách viết, chỉ một cách chạy:
mo_hinh.predict(7) # loi: Expected 2D array, got scalar array mo_hinh.predict([7]) # loi: Expected 2D array, got 1D array mo_hinh.predict([[7]]) # chay duoc -> [13.58]
Hai lớp ngoặc không phải để cho đẹp. Chúng nói đúng cấu trúc bảng: ngoặc ngoài là cả bảng, ngoặc trong là một dòng. Nên [[7]] nghĩa là “một bảng, gồm một dòng, dòng ấy có một cột”.
Hiểu thế thì việc dự đoán cho nhiều người cùng lúc trở nên hiển nhiên — cứ thêm dòng vào:
mo_hinh.predict([[7, 0], [7, 3], [2, 0]]) # [13.51899441 11.10558659 6.04692737]
Ba dòng vào, ba số ra. Bạn học 7 giờ mà không vắng buổi nào được 13,52; cũng học 7 giờ nhưng vắng 3 buổi chỉ còn 11,11. Chênh lệch là 2,4134 điểm — đúng bằng 3 × 0,8045, tức ba buổi vắng nhân với mức trừ của mỗi buổi.
⚠️ Ba câu tự hỏi trước mỗi lần fit — tập thành phản xạ thì sẽ không bao giờ dính lỗi hình dạng nữa:
Xcó phải bảng không? InX.shape, phải thấy hai con số.- Số dòng có bằng số đáp số không? Số đầu của
X.shapephải bằng số đầu củay.shape. - Số cột có đúng bằng số thứ mình đo không? Đo một thứ thì phải là 1, không phải 6.
Ba dòng print tốn hai giây, tiết kiệm cho bạn hàng giờ đi tìm lỗi.
📝 Bảng ghi nhớ
| Điều cần nhớ | Cụ thể | Ghi chú |
|---|---|---|
| Một dòng | Một mẫu (sample) | Một học sinh, một căn nhà, một bệnh nhân… |
| Một cột | Một đặc trưng (feature) | Một thứ ta đo được |
| Hình dạng của X | (n_mẫu, n_đặc_trưng) | Luôn hai con số |
| Hình dạng của y | (n_mẫu,) | Một con số và một dấu phẩy |
| X viết hoa | Vì là bảng, hai chiều | Quy ước mượn từ toán |
| y viết thường | Vì là dãy, một chiều | Mỗi mẫu chỉ một đáp số |
| Có 1 đặc trưng | x.reshape(-1, 1) | Nhiều dòng, 1 cột |
| Có 1 mẫu | x.reshape(1, -1) | 1 dòng, nhiều cột |
| Ghép nhiều cột | np.column_stack([a, b]) | Xếp các cột cạnh nhau |
| Dự đoán 1 trường hợp | predict([[7, 0]]) | Hai lớp ngoặc: bảng, rồi dòng |
| Kiểm tra nhanh | print(X.shape, y.shape) | Số đầu của hai bên phải bằng nhau |
✎ Kiểm tra nhanh
Câu 1. Trong bảng dữ liệu của scikit-learn, một cột tương ứng với:
- Một học sinh
- Một lần dự đoán
- Một đặc trưng — một thứ ta đo được
- Một đáp số
Câu 2. Bạn có 100 bệnh nhân, mỗi người đo 5 chỉ số. X.shape phải là:
(5, 100)(100, 5)(100,)(500,)
(số mẫu, số đặc trưng) — số mẫu đứng trước. 100 bệnh nhân là 100 dòng, 5 chỉ số là 5 cột.Câu 3. Vì sao y chỉ cần một chiều trong khi X phải hai chiều?
- Vì
yluôn ít số hơnX - Vì scikit-learn chưa hỗ trợ
yhai chiều - Vì
ylà số thực cònXlà số nguyên - Vì mỗi mẫu chỉ có một đáp số, nên không có gì để làm thành cột thứ hai
y cũng thành bảng, nhưng đó là trường hợp hiếm, và lúc ấy người ta viết hoa thành Y.Câu 4. Bạn có 6 con số, là số giờ tự học của 6 học sinh. Phải xếp lại thế nào?
reshape(-1, 1)— cho ra 6 dòng, 1 cộtreshape(1, -1)— cho ra 1 dòng, 6 cột- Không cần xếp lại, đưa thẳng vào
fit reshape(2, 3)— cho ra 2 dòng, 3 cột
reshape(-1, 1). Cứ đếm số học sinh là ra ngay, không cần thuộc lòng.Câu 5. Chạy fit(X, y) và nhận lỗi Found input variables with inconsistent numbers of samples: [1, 6]. Chuyện gì đã xảy ra?
- Dữ liệu có giá trị bị thiếu
ychưa được đổi sang mảng NumPyXđang có 1 dòng trong khiycó 6 đáp số — nhiều khả năng đã dùng nhầmreshape(1, -1)- Mô hình chưa được tạo trước khi gọi
fit
X và y khai báo. Chúng phải bằng nhau. Thấy [1, 6] là dấu hiệu kinh điển của việc đảo nhầm hai số trong reshape.Câu 6. Mô hình đã học trên 2 cột. Muốn dự đoán cho một bạn học 7 giờ và vắng 1 buổi, viết thế nào?
predict(7, 1)predict([[7, 1]])predict([7, 1])predict([[7], [1]])
[[7, 1]] nghĩa là một bảng gồm một dòng, dòng đó có hai cột. Còn [[7], [1]] lại là hai dòng mỗi dòng một cột — sai hẳn ý.💻 Thực hành: nhìn tận mắt các hình dạng
▶ Cách dùng: bấm Khởi động Python một lần (khoảng 15 giây — bài này không cần Matplotlib nên nhẹ hơn Bài 31), đợi báo sẵn sàng rồi bấm Chạy code. Phần giữa cố tình gây lỗi để bạn đọc nguyên văn thông báo.
💡 Thử thách (làm được cả ba là bạn đã nắm chắc bài):
- Thêm một cột thứ ba tên
bai_tap = np.array([5, 3, 6, 2, 8, 7])vàocolumn_stack. Trước khi chạy, hãy đoán trướcX.shapesẽ là gì vàcoef_sẽ có mấy số. Chạy rồi đối chiếu. - Bỏ một lớp ngoặc trong
predict: đổi[[7, 0], [7, 3], [2, 0]]thành[7, 0]. Bạn sẽ gặp lại đúng lỗiExpected 2D arrayvới hai đề nghị quen thuộc. Lần này[7, 0]là dữ liệu của một bạn duy nhất với hai đặc trưng — vậy trong hai đề nghị đó phải chọn cái nào? Đây chính là trường hợp hiếm hoi màreshape(1, -1)mới là đáp án đúng. - Dùng nhầm có chủ đích: sau khi
m2đã học trên 2 cột, thửm2.predict([[7]]). Lỗi lần này khác hẳn hai lỗi trên — nó không nói gì về chuyện hai chiều nữa. Nó nói gì? Và vì sao mô hình lại cần nhớ số cột nó từng học, thay vì cứ thế tính đại?
Bài tiếp theo: hai bài vừa rồi đã cho bạn đủ đồ nghề — fit, predict, và cách xếp dữ liệu. Bài 33 là bài luyện tập, không có khái niệm nào mới. Ta sẽ đem đúng những thứ này chạy trên ba bộ dữ liệu khác nhau, mỗi bộ hé lộ một điều mà bộ trước chưa cho thấy. Cứ thong thả, đây là chặng nghỉ trước khi vào phần khó của chặng.