🔬 Bài 28: Hàm mất mát — Cách máy tự chấm điểm chính mình
Chặng 7 · Toán & Tư duy ML (Bài 26 → 30) · đây là bài 3/5 của chặng.
🎯 Mục tiêu: hiểu vì sao máy bắt buộc phải đo được mình đang sai bao nhiêu, tự viết được hàm MSE, và nhìn thấy “chiếc bát sai số” mà mọi mô hình đều phải lăn xuống đáy.
👥 Đối tượng: học sinh cấp 2–3, sinh viên, người mới học Python. Cần biết trước: mảng NumPy (Bài 21, 26) và đạo hàm (Bài 27).
📖 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.
Sau bài này bạn sẽ:
- Giải thích được vì sao cộng thẳng các sai số lại cho kết quả vô nghĩa.
- Tự viết được hàm MSE và tính tay kiểm chứng lại kết quả của NumPy.
- Nói được hai lý do người ta chuộng bình phương hơn trị tuyệt đối — và cả cái giá phải trả.
- Nhìn ra mối nối giữa Bài 27 và Bài 29: đạo hàm là la bàn, hàm mất mát là địa hình.
Bài 27 cho ta một chiếc la bàn: đạo hàm chỉ hướng dốc lên hay dốc xuống. Nhưng la bàn chỉ có ích khi biết mình đang đứng trên địa hình nào. Bài này dựng ra địa hình đó.
Ý tưởng cốt lõi đơn giản đến bất ngờ: muốn máy tự sửa mình thì trước hết máy phải tự chấm điểm được mình. Con số điểm ấy gọi là hàm mất mát (loss function). Không có nó, mô hình không khác gì học sinh làm bài mà không bao giờ được biết mình sai chỗ nào.
Hình 1 — Chấm xanh là dữ liệu thật, đường xanh dương là dự đoán của mô hình. Đoạn đỏ là sai số; ô vuông dựng trên đoạn đó chính là bình phương sai số. Sai gấp 3 thì diện tích ô vuông gấp 9 — đó là toàn bộ lý do người ta bình phương.
1. Cộng thẳng sai số: cái bẫy đầu tiên
Bạn đoán giá một căn nhà là 3 tỉ, thực tế 5 tỉ — đoán hụt 2 tỉ. Căn tiếp theo bạn đoán 7 tỉ, thực tế 5 tỉ — đoán lố 2 tỉ.
Cộng hai sai số lại: (−2) + (+2) = 0. Theo con số này thì bạn đoán hoàn hảo. Trong khi thực tế bạn sai bét cả hai lần.
Đây không phải chuyện vặt. Với hàng nghìn dự đoán, chỗ lố và chỗ hụt sẽ triệt tiêu nhau và cho ra một con số đẹp giả tạo. Muốn đo cho thật, phải gom mọi sai số về số dương trước khi cộng.
Có hai cách gom: bỏ dấu đi (trị tuyệt đối), hoặc đem bình phương. Cả hai đều cho số dương, nhưng ngành ML gần như luôn chọn bình phương — phần Lớp C giải thích vì sao.
Nhờ NumPy, cả nghìn dự đoán được xử lý cùng lúc mà không cần vòng lặp — đúng thứ đã học ở Bài 26.
import numpy as np y_that = np.array([5, 2, 9]) # gia tri dung y_doan = np.array([3, 3, 8]) # may du doan sai_so = y_doan - y_that print(sai_so) # [-2 1 -1] print(sai_so.sum()) # -2 <- cong thang: gan bang 0, danh lua ta print(abs(sai_so).sum()) # 4 <- bo dau roi cong print((sai_so ** 2).sum()) # 6 <- binh phuong roi cong
Ba con số −2, 4 và 6 cùng mô tả một tình huống nhưng kể ba câu chuyện khác hẳn nhau. Chọn cách đo là một quyết định thật sự, không phải chi tiết kỹ thuật.
Vì sao ML chuộng bình phương hơn trị tuyệt đối? Hai lý do, và lý do thứ hai mới là lý do thật.
- Phạt nặng lỗi lớn. Sai 1 thì bị tính 1; sai 10 thì bị tính 100. Mô hình buộc phải ưu tiên dập những chỗ sai thảm hại trước, thay vì rải đều sai sót.
- Đạo hàm tồn tại ở mọi điểm. Đồ thị trị tuyệt đối là hình chữ V, gãy nhọn ngay tại đáy — đúng chỗ ta cần đến nhất thì lại không có độ dốc xác định. Đồ thị bình phương là chữ U cong mượt, lấy đạo hàm được ở mọi nơi. Nói cách khác: bình phương được chọn vì nó hợp với chiếc la bàn ở Bài 27.
Điểm 2 mới là điều quyết định. Bài 29 sẽ cho máy bước theo đạo hàm; nếu hàm mất mát có chỗ không lấy đạo hàm được thì thuật toán đứng hình đúng chỗ quan trọng nhất.
2. MSE — gộp mọi sai sót thành một con số
Bình phương từng sai số rồi lấy trung bình cộng, ta được một con số duy nhất tóm tắt toàn bộ mức độ sai của mô hình. Tên nó là MSE — Mean Squared Error, trung bình bình phương sai số.
Vì sao lấy trung bình chứ không lấy tổng? Để con số không phụ thuộc vào việc bạn có bao nhiêu dữ liệu. Mô hình chấm 10 bài và mô hình chấm 10.000 bài phải so sánh được với nhau.
Từ đây, mục tiêu của việc huấn luyện gói gọn trong một câu: vặn các tham số sao cho MSE nhỏ nhất có thể.
Cả công thức nằm gọn trong một dòng.
def mse(y_that, y_doan): return np.mean((y_doan - y_that) ** 2) print(mse(y_that, y_doan)) # (4 + 1 + 1) / 3 = 2.0
Điều đáng chú ý: trong thực tế y_doan không rơi từ trên trời xuống, nó do mô hình sinh ra từ tham số. Với mô hình đơn giản nhất y = w × x, đổi w là đổi y_doan, mà đổi y_doan thì MSE đổi theo.
x = np.array([1, 2, 3]) y_that = np.array([2, 4, 6]) # quy luat that: y = 2x for w in [0.0, 1.0, 2.0, 3.0, 4.0]: print(w, mse(y_that, w * x)) # 0.0 18.67 # 1.0 4.67 # 2.0 0.00 <- day bat # 3.0 4.67 # 4.0 18.67
Nhìn cột bên phải: nhỏ nhất tại w = 2, và càng đi xa khỏi 2 về bất kỳ phía nào thì càng lớn. Nối các điểm ấy lại chính là chiếc bát ở Hình 2.
MSE chỉ là một lựa chọn trong nhiều. Với bài toán phân loại — ảnh này là chó hay mèo — người ta dùng Cross-Entropy, vì đầu ra là xác suất chứ không phải một con số đo được. Nhưng triết lý không đổi: nén toàn bộ sai lệch thành đúng một số thực để có thứ mà tối thiểu hóa. Trong paper nó thường ký hiệu L (loss) hoặc J (cost).
Một chi tiết bạn sẽ gặp khi đọc tài liệu: nhiều nơi viết MSE với 1/(2n) thay vì 1/n. Số 2 ở mẫu không có ý nghĩa thống kê nào cả — nó chỉ để khi lấy đạo hàm, số 2 sinh ra từ lũy thừa sẽ triệt tiêu, cho công thức gọn hơn. Vị trí đáy bát không đổi, nên đừng bối rối khi thấy hai cách viết.
Với mô hình y = w × x, có thể lấy đạo hàm bằng tay: dL/dw = (2/n) · Σ xᵢ(w·xᵢ − yᵢ). Bài 29 sẽ dùng đúng công thức này, nhưng bạn cũng có thể tính bằng số theo cách của Bài 27 mà không cần thuộc nó.
Hình 2 — Trục ngang là tham số w, trục dọc là mức sai. Đổi w thì sai số vẽ ra một đường cong hình bát. Viên bi đỏ là mô hình lúc mới khởi tạo; đường tím là độ dốc tại đó, chỉ cho biết phải lăn về phía nào.
3. Ghép hai mảnh: đó chính là “học”
Giờ hãy nhìn toàn cảnh. Một vòng học của máy diễn ra đúng bốn nhịp:
- Mô hình nhận dữ liệu vào và đoán bừa một kết quả.
- Hàm mất mát chấm điểm: sai bao nhiêu. Đây là Bài 28.
- Đạo hàm đo độ dốc ngay tại chỗ đang đứng: nên chỉnh tham số về phía nào. Đây là Bài 27.
- Chỉnh tham số một bước nhỏ theo hướng đó. Rồi quay lại nhịp 1.
Lặp vài nghìn lần, viên bi trôi dần xuống đáy bát. Không có phép màu nào ở đây cả — chỉ là chấm điểm, xem dốc, bước một bước, làm lại.
Ta chưa dựng vòng lặp học ở bài này, nhưng có thể kiểm chứng ngay rằng đạo hàm thật sự chỉ đúng hướng. Dùng lại đúng hàm đạo hàm bằng số của Bài 27:
def dao_ham(f, w, h=1e-6): return (f(w + h) - f(w - h)) / (2*h) def loss_theo_w(w): return mse(y_that, w * x) print(dao_ham(loss_theo_w, 0.5)) # -14.0 <- am: tang w thi bot sai print(dao_ham(loss_theo_w, 2.0)) # 0.0 <- bang 0: dang o day bat print(dao_ham(loss_theo_w, 3.5)) # 14.0 <- duong: giam w thi bot sai
Ba con số này là toàn bộ thông tin máy cần. Dấu cho biết đi hướng nào, độ lớn cho biết còn cách đáy bao xa. Ở đúng đáy, đạo hàm bằng 0 — máy tự biết là đã tới nơi.
Chiếc bát ở Hình 2 đẹp vì mô hình chỉ có một tham số. Với hai tham số, địa hình thành một mặt cong trong không gian ba chiều — vẫn hình dung được. Với một mạng nơ-ron có 10 triệu tham số, đó là một mặt cong trong không gian 10 triệu chiều, không ai vẽ nổi.
Và điều quan trọng: khi ấy địa hình không còn là cái bát trơn tru. Nó lồi lõm, có nhiều hố trũng, có những vùng phẳng lì. Viên bi có thể mắc kẹt ở một hố cạn thay vì hố sâu nhất. Đây là vấn đề cực tiểu địa phương, và phần lớn các kỹ thuật huấn luyện hiện đại sinh ra để đối phó với nó.
Điều đáng kinh ngạc là dù địa hình phức tạp đến vậy, cách làm vẫn y hệt bốn nhịp ở Lớp A. Cả ngành deep learning được xây trên đúng một ý tưởng đơn giản này, lặp đi lặp lại hàng tỉ lần.
⚠️ Hai điều dễ hiểu nhầm về MSE
Một: đơn vị bị bình phương. Nếu y tính bằng triệu đồng thì MSE có đơn vị là triệu đồng bình phương — một thứ chẳng có ý nghĩa gì trong đời thực. Vì vậy khi báo cáo kết quả người ta thường lấy căn bậc hai và gọi là RMSE, để con số quay về đúng đơn vị ban đầu và đọc được.
Hai: rất nhạy với dữ liệu bất thường. Đặc tính “phạt nặng lỗi lớn” là con dao hai lưỡi. Chỉ cần một điểm dữ liệu ghi sai — ví dụ gõ nhầm 50 thành 500 — là điểm đó một mình chi phối cả MSE, kéo mô hình lệch hẳn đi để chiều lòng một con số rác. Đó là lý do Bài 23 dạy làm sạch dữ liệu trước khi huấn luyện, chứ không phải sau.
📝 Bảng ghi nhớ
| Khái niệm | Ý nghĩa | Trong Python |
|---|---|---|
| Sai số | Dự đoán trừ giá trị thật, có thể âm hoặc dương | y_doan - y_that |
| Vì sao không cộng thẳng | Lố và hụt triệt tiêu nhau, cho số đẹp giả tạo | sai_so.sum() → gần 0 |
| Hàm mất mát | Gộp mọi sai lệch thành đúng một con số | ký hiệu L hoặc J |
| MSE | Trung bình bình phương sai số | np.mean((y_doan - y_that)**2) |
| RMSE | Căn của MSE, đưa về đúng đơn vị gốc | np.sqrt(mse(...)) |
| MAE | Trung bình trị tuyệt đối, ít nhạy ngoại lai hơn | np.mean(np.abs(y_doan - y_that)) |
| Đáy bát | Bộ tham số cho sai số nhỏ nhất | chỗ đạo hàm bằng 0 |
| Đạo hàm của Loss | Dấu chỉ hướng chỉnh, độ lớn chỉ mức gấp | dao_ham(loss_theo_w, w) |
✎ Kiểm tra nhanh
Câu 1. Vì sao không thể chỉ cộng thẳng các sai số lại với nhau?
- Vì phép cộng chạy chậm hơn phép nhân
- Vì NumPy không cộng được số âm
- Vì sai số âm và sai số dương triệt tiêu nhau, cho ra con số nhỏ giả tạo
- Vì kết quả sẽ luôn bằng 0 với mọi mô hình
Câu 2. Với y_that = [10, 20] và y_doan = [12, 16], giá trị MSE bằng bao nhiêu?
- 10
- −2
- 4
- 20
12−10 = 2 và 16−20 = −4. Bình phương: 4 và 16. Trung bình: (4+16)/2 = 10. Phương án −2 là kết quả của việc cộng thẳng — đúng cái bẫy ở Câu 1.Câu 3. Mục tiêu cuối cùng của việc huấn luyện một mô hình là gì?
- Làm cho các tham số càng lớn càng tốt
- Tìm bộ tham số làm cho hàm mất mát nhỏ nhất
- Tính được đạo hàm phức tạp nhất có thể
- Chạy qua dữ liệu càng nhiều vòng càng tốt
Câu 4. Với x = [1, 2, 3], y_that = [2, 4, 6] và mô hình y = w × x, giá trị Loss tại w = 1 và tại w = 3 so với nhau thế nào?
- Tại
w = 1nhỏ hơn, vì số nhỏ hơn - Tại
w = 3nhỏ hơn, vì gần với 4 hơn - Không so sánh được nếu chưa chạy code
- Bằng nhau, vì hai giá trị cách đều đáy bát
w = 2. Cả 1 và 3 đều cách đáy đúng 1 đơn vị về hai phía, mà chiếc bát thì đối xứng, nên Loss bằng nhau — cùng bằng 4,67. Đây là lý do hình dạng chiếc bát đáng nhớ hơn là con số cụ thể.Câu 5. Vì sao hàm trị tuyệt đối (MAE) khó dùng cho thuật toán đi theo độ dốc hơn MSE?
- Vì MAE luôn cho kết quả âm
- Vì đồ thị MAE gãy nhọn ngay tại đáy, chỗ đó không có độ dốc xác định
- Vì MAE tính chậm hơn nhiều lần
- Vì MAE không phạt được lỗi lớn
Câu 6. Nếu y được tính bằng đơn vị triệu đồng, thì MSE có đơn vị là gì?
- Triệu đồng
- Không có đơn vị, vì đã lấy trung bình
- Triệu đồng bình phương — nên người ta hay lấy căn để về lại triệu đồng
- Phần trăm
💻 Thực hành: tự tay dựng chiếc bát
▶ Cách dùng: bấm Khởi động Python một lần (lần đầu mất khoảng 10–20 giây), đợ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):
- Đổi
y_thatthành[3, 6, 9, 12]rồi chạy lại. Đáy bát dịch tới giá trịwbằng bao nhiêu? Vì sao lại là con số đó? - Sửa một điểm dữ liệu thành số rác:
y_that = [2, 4, 60, 8]. Đáy bát dịch đi đâu? So sánh mức dịch của đường MSE với đường MAE — kết quả này minh họa điều gì trong hộp cảnh báo phía trên? - Phóng to phần đáy bằng cách đổi
ws = np.linspace(1.8, 2.2, 300). Ở gần đáy, đường MSE trông gần như thẳng hay vẫn cong? Điều đó gợi ý gì về việc máy sẽ bước nhanh hay chậm khi tới gần đích?
Bài tiếp theo: đã có địa hình (Bài 28) và có la bàn (Bài 27), giờ chỉ còn thiếu đôi chân. Bài 29 dựng gradient descent — vòng lặp cho máy tự bước từng bước xuống đáy bát, và bạn sẽ thấy tận mắt con số w bò dần về đúng giá trị đúng mà không ai mách nó cả.