sử dụng hàm mất mát tùy chỉnh cho dữ liệu mất cân bằng

Trong bài toán học máy, khi dữ liệu bị mất cân bằng (imbalanced data), mô hình có xu hướng thiên lệch về phía lớp chiếm đa số. Một cách phổ biến để giải quyết vấn đề này là sử dụng hàm mất mát (loss function) tùy chỉnh, nhằm phạt nặng hơn những lỗi thuộc lớp thiểu số.

Dưới đây là một số cách phổ biến để xây dựng hàm mất mát tùy chỉnh cho dữ liệu mất cân bằng:


✅ 1. Weighted Cross-Entropy Loss

Gán trọng số lớn hơn cho lớp thiểu số.

Ví dụ với PyTorch:

import torch
import torch.nn as nn

# Giả sử có 2 lớp: lớp 0 chiếm 90%, lớp 1 chiếm 10%
class_weights = torch.tensor([0.1, 0.9])  # Trọng số nghịch đảo tỷ lệ
criterion = nn.CrossEntropyLoss(weight=class_weights)

Lưu ý: Cần tính tỷ lệ xuất hiện của từng lớp từ dữ liệu huấn luyện để chọn trọng số hợp lý.


✅ 2. Focal Loss

Được thiết kế để tập trung hơn vào các ví dụ khó học (thường là lớp thiểu số).

Công thức:

Trong các bài toán phân loại thường gặp, CrossEntropy Loss sẽ đối xử các mẫu như nhau. Nhưng khi có quá nhiều mẫu dễ (ví dụ lớp chiếm đa số), mô hình có xu hướng học tốt lớp đó và bỏ qua lớp thiểu số. Focal Loss xử lý điều này bằng cách:

  • Giảm ảnh hưởng của các mẫu dễ phân loại.
  • Tăng mức độ “tập trung” vào các mẫu khó hoặc lớp hiếm.

Công thức của Focal Loss dựa trên CrossEntropy Loss:

FL(p_t) = - \alpha_t (1 - p_t)^\gamma \log(p_t)

Trong đó:

  • p_t: độ tin cậy của mô hình cho lớp đúng (được tính từ output của softmax)
  • \alpha_t: trọng số của lớp (để xử lý mất cân bằng)
  • \gamma: tham số điều chỉnh (gamma càng lớn → mô hình càng tập trung vào các mẫu khó)

Ví dụ cài đặt PyTorch:

class FocalLoss(nn.Module):
    def __init__(self, alpha=1, gamma=2, reduction='mean'):
        super(FocalLoss, self).__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.reduction = reduction

    def forward(self, inputs, targets):
        BCE_loss = nn.functional.cross_entropy(inputs, targets, reduction='none')
        pt = torch.exp(-BCE_loss)
        focal_loss = self.alpha * (1 - pt) ** self.gamma * BCE_loss

        if self.reduction == 'mean':
            return focal_loss.mean()
        elif self.reduction == 'sum':
            return focal_loss.sum()
        else:
            return focal_loss

✅ 3. Label Smoothing + Weighting

Kết hợp label smoothing (giảm overconfidence) với trọng số lớp.


✅ 4. Custom Class-Balanced Loss

Theo công thức trong bài báo “Class-Balanced Loss Based on Effective Number of Samples” (CVPR 2019)

Công thức trọng số:

weight_c = \frac{1 - \beta}{1 - \beta^{n_c}}

với
n_c=số mẫu trong lớp c
\beta: giá trị gần 1 (ví dụ 0.999)


✅ Gợi ý lựa chọn:

Tình huốngHàm mất mát đề xuất
Mất cân bằng nhẹWeighted CrossEntropy
Mất cân bằng nặngFocal Loss hoặc Class-Balanced Loss
Dữ liệu nhiễuFocal Loss với label smoothing

Tuyệt vời, Kurious! Dưới đây là phần giải thích chi tiết được đặt ngay trước mỗi đoạn mã để giúp bạn hiểu rõ cách hoạt động của chương trình huấn luyện mạng nơ-ron với dữ liệu bệnh Parkinson và so sánh các kỹ thuật xử lý bất cân bằng lớp:


📦 Nhập thư viện cần thiết

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import TensorDataset, DataLoader

Sử dụng pandas, numpy, scikit-learn cho tiền xử lý và đánh giá mô hình. torch cung cấp các công cụ cần thiết để xây dựng và huấn luyện mạng nơ-ron với PyTorch.


📁 Tải và xử lý dữ liệu Parkinson

data = pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/parkinsons/parkinsons.data')
data = data.drop(['name'], axis=1)
X = data.drop(['status'], axis=1).values
y = data['status'].values

Tải dữ liệu từ UCI ML Repository, loại bỏ cột ‘name’ (không dùng cho huấn luyện), tách đặc trưng (X) và nhãn (y), với status là 0 (bình thường) hoặc 1 (bệnh Parkinson).


🧪 Chia dữ liệu thành tập huấn luyện và kiểm tra

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)

Chia dữ liệu thành 80% huấn luyện và 20% kiểm tra. Dùng stratify để đảm bảo tỷ lệ lớp được giữ nguyên.


🔄 Chuyển dữ liệu thành Tensor để dùng trong PyTorch

X_train_tensor = torch.tensor(X_train, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train, dtype=torch.long)
X_test_tensor = torch.tensor(X_test, dtype=torch.float32)
y_test_tensor = torch.tensor(y_test, dtype=torch.long)

Chuyển dữ liệu từ NumPy array sang PyTorch tensors để tương thích với mô hình.


🧺 Tạo DataLoader để hỗ trợ việc huấn luyện theo mini-batch

train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)

Gói dữ liệu huấn luyện thành DataLoader để lặp qua các mini-batch và shuffle nhằm tăng chất lượng huấn luyện.


🧠 Định nghĩa mô hình mạng nơ-ron đơn giản

class SimpleNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(X.shape[1], 64),
            nn.ReLU(),
            nn.Linear(64, 2)
        )

    def forward(self, x):
        return self.fc(x)

Mạng gồm 1 lớp ẩn với 64 neurons và hàm ReLU, đầu ra là softmax gồm 2 lớp (tương ứng bệnh và không bệnh).


🔥 Xây dựng Focal Loss để xử lý mất cân bằng lớp

class FocalLoss(nn.Module):
    def __init__(self, alpha=[1.0, 3.0], gamma=2):
        super().__init__()
        self.alpha = torch.tensor(alpha, dtype=torch.float32)
        self.gamma = gamma

    def forward(self, inputs, targets):
        ce_loss = F.cross_entropy(inputs, targets, reduction='none')
        pt = torch.exp(-ce_loss)
        at = self.alpha[targets]
        focal_loss = at * (1 - pt) ** self.gamma * ce_loss
        return focal_loss.mean()

Focal Loss giảm trọng số của các mẫu dễ phân loại và tăng cho các mẫu khó, hỗ trợ lớp hiếm. alpha điều chỉnh trọng số từng lớp, gamma kiểm soát độ nhấn mạnh.


🏋️ Hàm huấn luyện và đánh giá mô hình

def train_and_evaluate(loss_fn, description):
    model = SimpleNN()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    for epoch in range(20):
        model.train()
        for xb, yb in train_loader:
            optimizer.zero_grad()
            output = model(xb)
            loss = loss_fn(output, yb)
            loss.backward()
            optimizer.step()

    # Evaluation
    model.eval()
    with torch.no_grad():
        logits = model(X_test_tensor)
        preds = torch.argmax(logits, dim=1).numpy()
        print(f"\n=== {description} ===")
        print(classification_report(y_test, preds))

Huấn luyện mô hình trong 20 epochs, tính loss và cập nhật trọng số. Sau huấn luyện, dùng tập kiểm tra để đánh giá hiệu suất bằng classification_report.


📊 Chạy thử với các phương pháp loss khác nhau

CrossEntropy bình thường:

train_and_evaluate(nn.CrossEntropyLoss(), "CrossEntropy (No Weight)")

Không xử lý mất cân bằng lớp.

CrossEntropy có trọng số theo tần suất lớp:

num_0, num_1 = sum(y_train == 0), sum(y_train == 1)
weights = torch.tensor([1.0 / num_0, 1.0 / num_1], dtype=torch.float32)
train_and_evaluate(nn.CrossEntropyLoss(weight=weights), "Weighted CrossEntropy")

Tính trọng số ngược với số lượng của từng lớp.

Focal Loss:

train_and_evaluate(FocalLoss(alpha=[3.0, 1.0]), "Focal Loss")

Ưu tiên lớp hiếm (đặt alpha lớn hơn).


Toàn bộ code

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import TensorDataset, DataLoader

# Load and prepare data
data = pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/parkinsons/parkinsons.data')
data = data.drop(['name'], axis=1)
X = data.drop(['status'], axis=1).values
y = data['status'].values

# Train-test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)

# Convert to tensors
X_train_tensor = torch.tensor(X_train, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train, dtype=torch.long)
X_test_tensor = torch.tensor(X_test, dtype=torch.float32)
y_test_tensor = torch.tensor(y_test, dtype=torch.long)

train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)

# Model
class SimpleNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(X.shape[1], 64),
            nn.ReLU(),
            nn.Linear(64, 2)
        )

    def forward(self, x):
        return self.fc(x)

# Focal Loss
class FocalLoss(nn.Module):
    def __init__(self, alpha=[1.0, 3.0], gamma=2):
        super().__init__()
        self.alpha = torch.tensor(alpha, dtype=torch.float32)
        self.gamma = gamma

    def forward(self, inputs, targets):
        ce_loss = F.cross_entropy(inputs, targets, reduction='none')
        pt = torch.exp(-ce_loss)
        at = self.alpha[targets]
        focal_loss = at * (1 - pt) ** self.gamma * ce_loss
        return focal_loss.mean()

# Training and evaluation
def train_and_evaluate(loss_fn, description):
    model = SimpleNN()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    for epoch in range(20):
        model.train()
        for xb, yb in train_loader:
            optimizer.zero_grad()
            output = model(xb)
            loss = loss_fn(output, yb)
            loss.backward()
            optimizer.step()

    # Evaluation
    model.eval()
    with torch.no_grad():
        logits = model(X_test_tensor)
        preds = torch.argmax(logits, dim=1).numpy()
        print(f"\n=== {description} ===")
        print(classification_report(y_test, preds))

# No weighting
train_and_evaluate(nn.CrossEntropyLoss(), "CrossEntropy (No Weight)")

# Weighted CrossEntropy
num_0, num_1 = sum(y_train == 0), sum(y_train == 1)
weights = torch.tensor([1.0 / num_0, 1.0 / num_1], dtype=torch.float32)
train_and_evaluate(nn.CrossEntropyLoss(weight=weights), "Weighted CrossEntropy")

# Focal Loss
train_and_evaluate(FocalLoss(alpha=[3.0, 1.0]), "Focal Loss")

Chạy trên Colab

Output:

=== CrossEntropy (No Weight) ===
              precision    recall  f1-score   support

           0       0.38      0.50      0.43        10
           1       0.81      0.72      0.76        29

    accuracy                           0.67        39
   macro avg       0.60      0.61      0.60        39
weighted avg       0.70      0.67      0.68        39


=== Weighted CrossEntropy ===
              precision    recall  f1-score   support

           0       0.67      0.40      0.50        10
           1       0.82      0.93      0.87        29

    accuracy                           0.79        39
   macro avg       0.74      0.67      0.69        39
weighted avg       0.78      0.79      0.78        39


=== Focal Loss ===
              precision    recall  f1-score   support

           0       0.42      0.80      0.55        10
           1       0.90      0.62      0.73        29

    accuracy                           0.67        39
   macro avg       0.66      0.71      0.64        39
weighted avg       0.78      0.67      0.69        39

⚖️ CrossEntropy Loss (No Weight)

  • Lớp 0 (bình thường):
    • Precision = 0.38 → Khá thấp, nhiều dự đoán sai về lớp này.
    • Recall = 0.50 → Chỉ nhận diện được một nửa số mẫu thực sự thuộc lớp 0.
    • F1-score = 0.43 → Hiệu suất kém.
  • Lớp 1 (Parkinson):
    • Precision = 0.81 → Đa phần dự đoán lớp 1 là đúng.
    • Recall = 0.72 → Nhận diện lớp 1 tốt hơn lớp 0.

➡️ Mô hình thiên vị lớp 1 vì đây là lớp chiếm đa số. Mặc định loss không xử lý mất cân bằng lớp nên lớp 0 bị “ngó lơ”.


⚖️ Weighted CrossEntropy

  • Lớp 0:
    • Precision = 0.67 → Khả năng phân biệt được cải thiện đáng kể.
    • Recall = 0.40 → Tuy chưa cao nhưng giảm nhầm lẫn.
  • Lớp 1:
    • Precision = 0.82, Recall = 0.93 → Hiệu suất rất mạnh với lớp chính.

➡️ Accuracy tăng lên 79%F1 macro cũng cao hơn. Việc thêm trọng số theo tần suất lớp giúp mô hình chú ý hơn đến lớp thiểu số, dù vẫn còn bỏ sót một số mẫu lớp 0.


🔥 Focal Loss

  • Lớp 0:
    • Precision = 0.42 → Vẫn khá thấp nhưng…
    • Recall = 0.80 → Rất tốt, tức mô hình nhận diện được hầu hết các mẫu thuộc lớp 0.
  • Lớp 1:
    • Precision = 0.90 → Dự đoán chính xác lớp 1.
    • Recall = 0.62 → Có xu hướng bỏ sót một số mẫu lớp 1.

➡️ Focal Loss tập trung mạnh vào các mẫu khó phân loại, dẫn đến việc ưu tiên lớp 0 (lớp thiểu số). Tuy accuracy không tăng, nhưng khả năng phát hiện lớp 0 đã cải thiện rõ rệt.


🧠 Tổng kết

Loss FunctionAccuracyLớp 0 RecallLớp 1 RecallChú thích
CrossEntropy (No Weight)67%50%72%Thiên vị lớp đa số
Weighted CrossEntropy79%40%93%Cân bằng tốt hơn, ưu lớp 1
Focal Loss67%80%62%Tăng cường lớp hiếm nhưng giảm lớp chính

Nếu mục tiêu của bạn là phát hiện chính xác các ca bệnh Parkinson, thì Weighted CrossEntropy là lựa chọn tốt. Còn nếu bạn muốn giảm thiểu bỏ sót lớp bình thường, Focal Loss lại là phương án đáng cân nhắc.

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

error: Content is protected !!