LOSS & ACCURACY
Tính toán Sai số Mạng nơ-ron với Hàm mất mát (Loss Function)¶
Chào mừng các bạn đến với bài học về cách chúng ta "đo lường sự sai lầm" của một mạng nơ-ron. Trong các chương trước, chúng ta đã xây dựng được một mạng có khả năng nhận đầu vào và đưa ra dự đoán. Nhưng làm thế nào để biết dự đoán đó "tốt" hay "tệ" đến mức nào? Và "tệ" hơn bao nhiêu so với một dự đoán khác? Đây chính là lúc Hàm mất mát (Loss Function) vào cuộc.
1. Tại sao Độ chính xác (Accuracy) là không đủ?¶
Câu hỏi đầu tiên bạn có thể đặt ra là: "Tại sao không dùng luôn độ chính xác để đo lường? Cứ xem mạng dự đoán đúng bao nhiêu phần trăm là được."
Hãy xem ví dụ kinh điển trong sách: Giả sử kết quả đúng là lớp ở giữa (index 1). Chúng ta có hai dự đoán từ mô hình:
- Dự đoán A:
[0.22, 0.6, 0.18]
- Dự đoán B:
[0.32, 0.36, 0.32]
Nếu chúng ta chỉ dùng độ chính xác, ta sẽ làm như sau:
1. Dùng hàm argmax
để tìm chỉ số của giá trị lớn nhất.
2. argmax
của cả A và B đều là 1
.
3. So sánh với kết quả đúng là 1
. Cả hai đều đúng. Độ chính xác là 100% cho cả hai trường hợp.
Nhưng hãy nhìn kỹ hơn. Đầu ra của mạng nơ-ron (sau lớp Softmax) thể hiện mức độ tự tin (confidence). * Ở dự đoán A, mô hình rất tự tin (60%) rằng đó là lớp 1. * Ở dự đoán B, mô hình chỉ hơi nhỉnh hơn một chút (36%) và khá phân vân giữa cả ba lớp.
Rõ ràng, dự đoán A "tốt hơn" nhiều so với dự đoán B. Chúng ta cần một thước đo có thể phản ánh được sự khác biệt về độ tự tin này. Độ chính xác là một thước đo rời rạc (đúng hoặc sai), trong khi chúng ta cần một thước đo liên tục, chi tiết hơn.
Đây chính là mục đích của Loss (Mất mát). Loss là một con số duy nhất cho biết mô hình đã sai lầm nhiều như thế nào. Mục tiêu của việc huấn luyện là điều chỉnh trọng số (weights) và độ lệch (biases) để giá trị Loss này càng gần 0 càng tốt.
2. Categorical Cross-Entropy: Hàm mất mát cho bài toán phân loại¶
Trong bài toán của chúng ta (phân loại dữ liệu xoắn ốc), lớp đầu ra sử dụng hàm kích hoạt Softmax, biến các con số (logits) thành một phân phối xác suất (tổng các giá trị bằng 1). Để so sánh hai phân phối xác suất (một là dự đoán của mô hình, một là sự thật), người ta thường dùng một công cụ toán học gọi là Cross-Entropy.
Info
Kiến thức bên lề: Cross-Entropy là gì? Trong lý thuyết thông tin, cross-entropy đo lường sự khác biệt giữa hai phân phối xác suất. Nếu bạn dùng một bộ mã hóa tối ưu cho phân phối P để mã hóa dữ liệu từ phân phối Q, cross-entropy H(Q, P) cho bạn biết số bit trung bình cần thiết. Khi P và Q càng giống nhau, giá trị này càng nhỏ. Trong Machine Learning, chúng ta coi "sự thật" (ground-truth) là phân phối P và "dự đoán" của mô hình (predictions) là phân phối Q. Mục tiêu là làm cho Q càng giống P càng tốt, tức là giảm thiểu cross-entropy. (Nguồn: Deep Learning Book, Chapter 3.13)
Khi áp dụng cho bài toán phân loại với nhiều lớp, nó được gọi là Categorical Cross-Entropy.
Công thức toán học¶
Công thức đầy đủ để tính loss cho một mẫu (sample) i
là:
L_i = - Σ_j ( y_i,j * log(ŷ_i,j) )
Trong đó:
L_i
: Giá trị loss của mẫu thứi
.j
: Chỉ số của từng lớp đầu ra (ví dụ: chó, mèo, người).Σ_j
: Ký hiệu lấy tổng theo tất cả các lớpj
.y_i,j
: Sự thật (ground-truth). Là 1 nếu mẫui
thực sự thuộc lớpj
, và là 0 cho các lớp còn lại.ŷ_i,j
(đọc là "y-hat"): Dự đoán của mô hình. Là xác suất mà mô hình dự đoán mẫui
thuộc về lớpj
(giá trị từ Softmax).log
: Là logarit tự nhiên (cơ số e), trong Python làmath.log()
hoặcnp.log()
.
Sự kỳ diệu của One-Hot Encoding¶
Hãy xem công thức này hoạt động như thế nào trong thực tế. Giả sử ta có 3 lớp và sự thật là lớp đầu tiên (index 0).
* Dự đoán của mô hình (ŷ): [0.7, 0.1, 0.2]
* Sự thật (y): [1, 0, 0]
Vector [1, 0, 0]
được gọi là one-hot encoding. "Hot" là 1, "cold" là 0.
Áp dụng công thức:
L = - ( y_0*log(ŷ_0) + y_1*log(ŷ_1) + y_2*log(ŷ_2) )
L = - ( 1*log(0.7) + 0*log(0.1) + 0*log(0.2) )
L = - ( 1*log(0.7) + 0 + 0 )
L = -log(0.7)
Công thức đồ sộ ban đầu đã được rút gọn thành một phép tính vô cùng đơn giản: chỉ cần lấy logarit tự nhiên của xác suất dự đoán cho lớp đúng, rồi đổi dấu.
Đây là một insight cực kỳ quan trọng! Nó giúp việc tính toán trở nên hiệu quả hơn rất nhiều.
3. Triển khai bằng Python (Step-by-Step)¶
3.1. Tính Loss cho một Batch¶
Trong thực tế, chúng ta không xử lý từng mẫu một mà xử lý theo batch (lô) để tăng tốc độ.
import numpy as np
# Đầu ra Softmax cho một batch 3 mẫu, mỗi mẫu 3 lớp
softmax_outputs = np.array([[0.7, 0.1, 0.2], # Mẫu 1
[0.1, 0.5, 0.4], # Mẫu 2
[0.02, 0.9, 0.08]]) # Mẫu 3
# Nhãn thật (target labels). Đây là dạng "sparse" (thưa)
# Mẫu 1 đúng là lớp 0, mẫu 2 là lớp 1, mẫu 3 là lớp 1
class_targets = [0, 1, 1]
Làm thế nào để lấy ra các xác suất đúng từ softmax_outputs
? Chúng ta cần:
- Từ hàng 0, lấy phần tử ở cột 0 (
0.7
) - Từ hàng 1, lấy phần tử ở cột 1 (
0.5
) - Từ hàng 2, lấy phần tử ở cột 1 (
0.9
)
NumPy cung cấp một cách rất thanh lịch để làm điều này, gọi là advanced indexing:
# Lấy ra các xác suất của các lớp đúng
correct_confidences = softmax_outputs[
range(len(softmax_outputs)), # Chỉ số hàng: [0, 1, 2]
class_targets # Chỉ số cột: [0, 1, 1]
]
print(correct_confidences)
# Kết quả: [0.7 0.5 0.9]
Bây giờ, chúng ta chỉ cần áp dụng công thức -log
cho từng giá trị này:
# Tính loss cho từng mẫu
sample_losses = -np.log(correct_confidences)
print(sample_losses)
# Kết quả: [0.35667494 0.69314718 0.10536052]
Cuối cùng, loss của cả batch thường được tính bằng trung bình cộng (mean) của các loss riêng lẻ:
# Tính loss trung bình cho cả batch
average_loss = np.mean(sample_losses)
print(average_loss)
# Kết quả: 0.38506088...
3.2. Vấn đề nan giải: log(0)
¶
Hàm log(x)
chỉ xác định khi x > 0
. Chuyện gì sẽ xảy ra nếu mô hình dự đoán xác suất của lớp đúng là 0?
log(0)
là âm vô cùng. Điều này sẽ tạo ra giá trị loss là inf
(vô cùng) trong Python, và một khi inf
xuất hiện, nó sẽ "lây nhiễm" cho các phép tính sau đó (ví dụ, trung bình của một dãy số có inf
cũng là inf
). Điều này sẽ làm hỏng toàn bộ quá trình huấn luyện.
Tệ hơn nữa, nếu mô hình quá tự tin và dự đoán xác suất là 1.0 cho lớp đúng, thì log(1.0) = 0
, loss sẽ bằng 0. Nhưng do sai số làm tròn của số thực trong máy tính, giá trị có thể là 1.0000001
. log(1.0000001)
là một số dương rất nhỏ, khiến loss trở thành một số âm rất nhỏ. Loss âm là điều vô lý.
Giải pháp: Clipping (Cắt xén)
Để giải quyết triệt để, chúng ta sẽ "cắt xén" các giá trị dự đoán để chúng không bao giờ chạm tới 0 hoặc 1. Ta sẽ ép chúng vào một khoảng rất nhỏ, ví dụ [1e-7, 1 - 1e-7]
.
1e-7
là cách viết khoa học của0.0000001
.- Bất kỳ giá trị nào nhỏ hơn
1e-7
sẽ được gán bằng1e-7
. - Bất kỳ giá trị nào lớn hơn
1 - 1e-7
sẽ được gán bằng1 - 1e-7
.
Trong NumPy, ta dùng hàm np.clip()
:
log
của 0 hoặc của một số lớn hơn 1, giúp quá trình tính toán ổn định.
3.3. Xử lý cả hai định dạng Nhãn (One-Hot và Sparse)¶
Nhãn thật (targets) có thể ở hai dạng:
- Sparse (thưa): Một mảng 1 chiều chứa các chỉ số của lớp đúng. Ví dụ:
[0, 1, 1]
. - One-Hot: Một mảng 2 chiều. Ví dụ:
[[1,0,0], [0,1,0], [0,1,0]]
.
Ta có thể kiểm tra số chiều của mảng nhãn (len(y_true.shape)
) để biết nó thuộc dạng nào và xử lý tương ứng.
- Nếu là Sparse (shape=1): Dùng phương pháp indexing như đã làm ở trên.
- Nếu là One-Hot (shape=2): Ta quay lại công thức gốc
Σ (y * log(ŷ))
. Doy
là one-hot, phép nhâny * log(ŷ)
sẽ biến tất cả các giá trị của lớp sai thành 0, chỉ giữ lại giá trị của lớp đúng. Sau đó ta chỉ cần lấy tổng theo hàng (axis=1
).
# Giả sử y_pred_clipped đã được tính
# Và y_true là nhãn one-hot
if len(y_true.shape) == 2:
correct_confidences = np.sum(
y_pred_clipped * y_true,
axis=1
)
4. Xây dựng Class Loss hoàn chỉnh¶
Để mã nguồn có tổ chức và dễ tái sử dụng, chúng ta sẽ tạo các class cho Loss.
Class Loss
cơ sở¶
Đây là một class cha trừu tượng. Nó có một phương thức calculate
chung cho tất cả các loại loss: tính loss của từng mẫu (qua hàm forward
) rồi lấy trung bình.
# Common loss class
class Loss:
# Tính toán loss dữ liệu
def calculate(self, output, y):
# Tính loss của từng mẫu
sample_losses = self.forward(output, y)
# Tính loss trung bình
data_loss = np.mean(sample_losses)
return data_loss
Class Loss_CategoricalCrossentropy
¶
Class này kế thừa từ Loss
và triển khai logic tính toán cụ thể cho Categorical Cross-Entropy.
# Cross-entropy loss
class Loss_CategoricalCrossentropy(Loss):
# Phương thức forward
def forward(self, y_pred, y_true):
samples = len(y_pred)
# 1. Clipping dữ liệu để tránh log(0)
y_pred_clipped = np.clip(y_pred, 1e-7, 1 - 1e-7)
# 2. Kiểm tra định dạng nhãn và tính toán
# Nếu là nhãn sparse [0, 1, 1]
if len(y_true.shape) == 1:
correct_confidences = y_pred_clipped[
range(samples),
y_true
]
# Nếu là nhãn one-hot [[1,0,0], [0,1,0], ...]
elif len(y_true.shape) == 2:
correct_confidences = np.sum(
y_pred_clipped * y_true,
axis=1
)
# 3. Tính negative log likelihoods (loss cho từng mẫu)
negative_log_likelihoods = -np.log(correct_confidences)
return negative_log_likelihoods
5. Tính thêm Độ chính xác (Accuracy)¶
Mặc dù không dùng để tối ưu hóa, độ chính xác vẫn là một thước đo rất trực quan để con người đánh giá hiệu suất của mô hình. Chúng ta sẽ tính nó song song với loss.
Cách tính accuracy rất đơn giản:
- Dùng
np.argmax(softmax_outputs, axis=1)
để tìm ra lớp được dự đoán có xác suất cao nhất cho mỗi mẫu.axis=1
nghĩa là tìm argmax trên mỗi hàng (mỗi mẫu). - So sánh mảng dự đoán này với mảng nhãn thật. Phép so sánh
predictions == class_targets
sẽ trả về một mảng các giá trịTrue
(nếu đúng) vàFalse
(nếu sai). - Lấy trung bình của mảng boolean này.
np.mean()
sẽ tự động coiTrue
là 1 vàFalse
là 0.
# Lấy dự đoán từ đầu ra softmax
predictions = np.argmax(softmax_outputs, axis=1)
# Nếu nhãn là one-hot, chuyển về sparse
if len(class_targets.shape) == 2:
class_targets = np.argmax(class_targets, axis=1)
# So sánh và tính trung bình
accuracy = np.mean(predictions == class_targets)
print('acc:', accuracy)
Tổng kết¶
Qua chương này, chúng ta đã học được những điều cốt lõi:
- Tại sao cần Loss: Loss là một thước đo chi tiết, liên tục về "mức độ sai lầm" của mô hình, tốt hơn nhiều so với độ chính xác (Accuracy) cho việc huấn luyện.
- Categorical Cross-Entropy: Là hàm mất mát tiêu chuẩn cho bài toán phân loại đa lớp, đo lường sự khác biệt giữa phân phối xác suất dự đoán và phân phối xác suất thật.
- Công thức rút gọn: Khi nhãn là one-hot, công thức phức tạp được rút gọn thành
-log(xác suất của lớp đúng)
. -
Triển khai thực tế:
- Sử dụng NumPy indexing để tính toán hiệu quả trên cả batch.
- Giải quyết vấn đề
log(0)
bằng kỹ thuật clipping. - Xây dựng mã nguồn theo hướng đối tượng (OOP) với các class
Loss
để dễ quản lý và mở rộng. - Accuracy: Vẫn là một thước đo quan trọng để con người theo dõi và được tính toán song song với loss.
Với việc tính được Loss, chúng ta đã có một "tín hiệu" để biết mô hình cần cải thiện ở đâu và như thế nào. Chương tiếp theo sẽ chỉ cho chúng ta cách sử dụng tín hiệu này để thực sự "dạy" cho mạng nơ-ron thông qua quá trình tối ưu hóa và lan truyền ngược (backpropagation).