불균형 데이터 처리(Handling Imbalanced Data)

데이터(data)의 99%가 "정상(normal)"이라면 정확도(accuracy)는 거짓말입니다.

유형: Build 언어: Python 선수 지식: Phase 2, Lessons 01-09, 특히 평가 지표(evaluation metrics) 소요 시간: 약 90분

학습 목표

  • SMOTE를 처음부터 구현하고 합성 오버샘플링(synthetic oversampling)이 무작위 복제(random duplication)와 어떻게 다른지 설명합니다.
  • 불균형 분류기(imbalanced classifier)를 정확도(accuracy) 대신 F1, AUPRC, 매튜스 상관계수(Matthews Correlation Coefficient; MCC)로 평가합니다.
  • 클래스 가중치(class weighting), 임계값 튜닝(threshold tuning), 재샘플링 전략(resampling strategy)을 비교하고 불균형 비율(imbalance ratio)에 맞는 접근을 선택합니다.
  • SMOTE, 클래스 가중치(class weight), 임계값 최적화(threshold optimization)를 결합한 완전한 불균형 데이터 파이프라인(imbalanced data pipeline)을 만듭니다.

문제

사기 탐지 모델(fraud detection model)을 만들었습니다. 정확도(accuracy)가 99.9%입니다. 기뻐하다가 곧 모델이 모든 거래(transaction)를 "사기 아님(not fraud)"으로 예측한다는 사실을 알게 됩니다.

이것은 버그(bug)가 아닙니다. 사기 거래(fraud transaction)가 0.1%뿐이라면 전체 오류(overall error)를 줄이는 가장 합리적인 행동입니다. 모델은 항상 다수 클래스(majority class)를 예측하는 것이 전체 정확도를 높인다고 학습합니다. 기술적으로는 맞지만 실제로는 완전히 쓸모없습니다.

이 문제는 중요한 분류(classification) 문제에서 자주 나타납니다. 질병 진단(disease diagnosis)은 양성률(positive rate)이 1%일 수 있고, 네트워크 침입(network intrusion)은 공격(attack)이 0.01%일 수 있습니다. 제조 결함(manufacturing defect)은 결함품(defective)이 0.5%일 수 있습니다. 스팸 필터링(spam filtering)은 스팸이 20%, 이탈 예측(churn prediction)은 이탈자(churner)가 5%일 수 있습니다. 더 중요한 소수 클래스(minority class)일수록 더 드문 경향이 있습니다.

정확도는 모든 올바른 예측(correct prediction)을 똑같이 취급하므로 실패합니다. 정상 거래(legitimate transaction)를 맞추는 것과 사기를 잡는 것이 정확도 관점에서는 같은 1점입니다. 하지만 사기를 잡는 것이야말로 모델의 존재 이유입니다. 드물지만 중요한 클래스(rare but important class)에 모델이 주의를 기울이도록 만드는 지표(metric), 기법(technique), 학습 전략(training strategy)이 필요합니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.사기 탐지 데이터셋(fraud detection dataset)에 정상 거래(legitimate transaction)가 99.9%, 사기(fraud)가 0.1% 있습니다. 모델이 모든 거래(transaction)를 '정상(legitimate)'으로 예측합니다. 정확도(accuracy)는 얼마인가요?

2.항상 음성(negative)을 예측하는 모델이 쓸모없다는 것을 올바르게 보여주는 지표(metric)는 무엇인가요?

0/2 답변 완료

개념

정확도(Accuracy)가 실패하는 이유

샘플(sample) 1000개 중 990개는 음성(negative), 10개는 양성(positive)인 데이터셋(dataset)을 생각해 봅시다. 항상 음성을 예측하는 모델은 다음과 같은 혼동 행렬(confusion matrix)을 갖습니다.

양성으로 예측음성으로 예측
실제 양성0 (TP)10 (FN)
실제 음성0 (FP)990 (TN)

정확도는 (0 + 990) / 1000 = 99.0%입니다. 모델은 사기(fraud), 질병(disease), 결함(defect)을 하나도 잡지 못하지만 정확도는 99%라고 말합니다. 불균형 문제(imbalanced problem)에서 정확도가 위험한 이유입니다.

더 나은 지표(Better Metrics)

정밀도(Precision) = TP / (TP + FP). 양성이라고 표시(flag)한 것 중 실제 양성이 얼마나 되는지를 나타냅니다. 정밀도가 높으면 거짓 경보(false alarm)가 적습니다.

재현율(Recall) = TP / (TP + FN). 실제 양성 중 얼마나 잡았는지를 나타냅니다. 재현율이 높으면 놓친 양성(missed positive)이 적습니다.

F1 점수(F1 Score) = 2 * precision * recall / (precision + recall). 정밀도와 재현율의 조화평균(harmonic mean)입니다. 둘 중 하나만 극단적으로 높을 때 산술평균(arithmetic mean)보다 더 강하게 벌점(penalty)을 줍니다.

F-beta 점수(F-beta Score) = (1 + beta^2) * precision * recall / (beta^2 * precision + recall). beta가 1보다 크면 재현율을 더 중요하게 보고, 1보다 작으면 정밀도를 더 중요하게 봅니다. 사기 탐지(fraud detection)에서는 사기 누락(missing fraud)이 거짓 경보보다 비싸므로 F2를 자주 사용합니다.

AUPRC(Area Under Precision-Recall Curve; 정밀도-재현율 곡선 아래 면적). ROC AUC보다 불균형 데이터(imbalanced data)에 더 정보가 많습니다. 무작위 분류기(random classifier)의 AUPRC 기준선(baseline)은 양성 클래스 비율(positive class rate)이며, ROC처럼 0.5가 아닙니다.

매튜스 상관계수(Matthews Correlation Coefficient; MCC). (TP * TN - FP * FN) / sqrt((TP+FP)(TP+FN)(TN+FP)(TN+FN))입니다. -1부터 +1까지의 범위를 가지며, 양쪽 클래스(class) 모두에서 잘할 때만 높은 값이 나옵니다. 클래스 크기가 매우 달라도 균형 잡힌 지표로 작동합니다.

항상 음성을 예측하는 모델은 정밀도가 정의되지 않아 보통 0으로 두고, 재현율 0, F1 0, MCC 0이 됩니다. 이러한 지표들은 모델이 쓸모없다는 것을 정확히 드러내 줍니다.

불균형 데이터 파이프라인(Imbalanced Data Pipeline)

flowchart TD
    A[불균형 데이터셋] --> B{불균형 비율?}
    B -->|약함: 80/20| C[클래스 가중치]
    B -->|중간: 95/5| D[SMOTE + 임계값 튜닝]
    B -->|심함: 99/1| E[SMOTE + 클래스 가중치 + 임계값]
    C --> F[모델 학습]
    D --> F
    E --> F
    F --> G[F1 / AUPRC / MCC로 평가]
    G --> H{충분한가?}
    H -->|아니오| I[다른 전략 시도]
    H -->|예| J[모니터링과 함께 배포]
    I --> B

SMOTE: 소수 클래스 합성 오버샘플링 기법(Synthetic Minority Oversampling Technique)

무작위 오버샘플링(random oversampling)은 기존 소수 클래스 샘플(minority sample)을 복제합니다. 작동은 하지만 같은 점(point)을 반복해서 보여주기 때문에 과적합(overfitting) 위험이 있습니다.

SMOTE는 복제가 아니라 그럴듯한 합성 소수 클래스 샘플(synthetic minority sample)을 만들어 냅니다.

  1. 각 소수 클래스 샘플 x에 대해 다른 소수 클래스 샘플 중에서 k개 최근접 이웃(k nearest neighbors)을 찾습니다.
  2. 이웃(neighbor) 하나를 무작위로 고릅니다.
  3. x와 이웃 사이의 선분(line segment) 위에 새 샘플을 만듭니다.

공식은 다음과 같습니다.

new_sample = x + random(0, 1) * (neighbor - x)

기존 점을 복사하지 않고 소수 클래스 영역(minority region) 안에 새로운 점을 보강합니다.

flowchart LR
    subgraph Original["원본 소수 클래스 점"]
        P1["x1 (1.0, 2.0)"]
        P2["x2 (1.5, 2.5)"]
        P3["x3 (2.0, 1.5)"]
    end
    subgraph SMOTE["SMOTE 생성 과정"]
        direction TB
        S1["x1과 이웃 x2 선택"]
        S2["무작위 t = 0.4"]
        S3["new = x1 + 0.4*(x2-x1)"]
        S4["new = (1.2, 2.2)"]
        S1 --> S2 --> S3 --> S4
    end
    Original --> SMOTE
    subgraph Result["증강된 집합"]
        R1["x1 (1.0, 2.0)"]
        R2["x2 (1.5, 2.5)"]
        R3["x3 (2.0, 1.5)"]
        R4["합성 (1.2, 2.2)"]
    end
    SMOTE --> Result

샘플링 전략 비교(Sampling Strategy Comparison)

무작위 오버샘플링(Random Oversampling): 소수 클래스 샘플을 복제해 다수 클래스 개수와 맞춥니다.

  • 장점: 단순하고 정보 손실(information loss)이 없습니다.
  • 단점: 정확한 중복(exact duplicate)이 과적합을 만들고 학습 시간(training time)을 늘립니다.

무작위 언더샘플링(Random Undersampling): 다수 클래스 샘플을 제거해 소수 클래스 개수와 맞춥니다.

  • 장점: 빠르고 단순합니다.
  • 단점: 유용한 다수 클래스 데이터를 버리고 분산(variance)이 커질 수 있습니다.

SMOTE: 보간(interpolation)으로 합성 소수 클래스 샘플을 만듭니다.

  • 장점: 단순 복제보다 과적합이 적습니다.
  • 단점: 결정 경계(decision boundary) 근처에서 잡음 샘플(noisy sample)을 만들 수 있고 다수 클래스 분포(majority distribution)를 고려하지 않습니다.
전략변하는 데이터위험적합한 상황
오버샘플링소수 클래스 복제과적합작은 데이터셋, 중간 불균형
언더샘플링다수 클래스 제거정보 손실큰 데이터셋, 빠른 학습 필요
SMOTE합성 소수 클래스 추가경계 잡음중간 불균형, k-NN에 충분한 소수 클래스 샘플

클래스 가중치(Class Weights)

데이터를 바꾸는 대신 모델이 오차(error)를 다르게 바라보게 합니다. 소수 클래스를 잘못 분류하는 일에 더 큰 가중치(weight)를 줍니다.

이진 분류 문제(binary problem)에서 음성 샘플 950개, 양성 샘플 50개라면 다음과 같이 가중치를 줍니다.

  • 음성 클래스 가중치 = 1000 / (2 * 950) = 0.526
  • 양성 클래스 가중치 = 1000 / (2 * 50) = 10.0

양성 클래스는 약 19배의 가중치를 받습니다. 양성 샘플 하나를 틀리는 비용이 음성 샘플 19개를 틀리는 비용과 비슷해집니다.

로지스틱 회귀(logistic regression)에서는 손실 함수(loss function)가 다음처럼 바뀝니다.

weighted_loss = -sum(w_i * [y_i * log(p_i) + (1-y_i) * log(1-p_i)])

클래스 가중치는 기댓값(expectation) 상으로 오버샘플링과 비슷하지만 새 데이터 점을 만들지 않습니다. 그래서 더 빠르고, 중복된 샘플로 인한 과적합 위험도 없습니다.

임계값 튜닝(Threshold Tuning)

대부분의 분류기(classifier)는 확률(probability)을 출력합니다. 기본 임계값(threshold)은 0.5입니다. P(positive) >= 0.5이면 양성으로 분류합니다. 하지만 0.5는 임의로 정한 값일 뿐입니다. 클래스가 불균형하면 최적 임계값(optimal threshold)은 보통 훨씬 낮습니다.

  1. 모델을 학습합니다.
  2. 검증 집합(validation set)에서 예측 확률(predicted probability)을 얻습니다.
  3. 임계값을 0.0부터 1.0까지 훑습니다(sweep).
  4. 각 임계값에서 F1 또는 선택한 지표를 계산합니다.
  5. 지표를 최대화하는 임계값을 고릅니다.
flowchart LR
    A[모델] --> B[확률 예측]
    B --> C[임계값 0.0~1.0 훑기]
    C --> D[각 임계값에서 F1 계산]
    D --> E[최선 임계값 선택]
    E --> F[운영 환경에서 사용]

사기 거래에 대해 모델이 P(fraud)=0.15를 출력할 수 있습니다. 임계값 0.5에서는 사기가 아닌 것으로 분류되지만, 임계값 0.10에서는 사기로 잡힙니다. 확률 보정(probability calibration)보다 순위(ranking)가 더 중요합니다. 사기 거래가 정상 거래보다 높은 확률을 받기만 하면 둘을 나눌 임계값이 존재합니다.

비용 민감 학습(Cost-Sensitive Learning)

클래스 가중치의 일반화입니다. 일률적인 비용(uniform cost) 대신 오분류 비용(misclassification cost)을 직접 지정합니다.

양성으로 예측음성으로 예측
실제 양성0C_FN = 100
실제 음성C_FP = 10

사기를 놓치는 거짓 음성(FN)은 거짓 경보인 거짓 양성(FP)보다 100배 비쌉니다. 모델은 오류 개수(error count)가 아니라 전체 비용(total cost)을 줄이도록 최적화합니다.

실제 비용(real-world cost)을 추정할 수 있다면 가장 원칙적인 접근입니다. 암 진단을 놓치는 비용과 추가 조직 검사(biopsy)를 유발하는 거짓 경보의 비용은 크게 다릅니다. 비용을 명시하면 올바른 절충(tradeoff)을 강제할 수 있습니다.

의사결정 흐름도(Decision Flowchart)

flowchart TD
    A[시작: 불균형 데이터셋] --> B{얼마나 불균형한가?}
    B -->|"< 70/30"| C["약함: 클래스 가중치 먼저"]
    B -->|"70/30 ~ 95/5"| D["중간: SMOTE + 클래스 가중치"]
    B -->|"> 95/5"| E["심함: 여러 전략 결합"]
    C --> F{데이터가 충분한가?}
    D --> F
    E --> F
    F -->|"< 1000 샘플"| G["오버샘플링 또는 SMOTE, 언더샘플링 피하기"]
    F -->|"1000~10000"| H["SMOTE + 임계값 튜닝"]
    F -->|"> 10000"| I["언더샘플링 가능 또는 클래스 가중치"]
    G --> J[F1/AUPRC로 학습 + 평가]
    H --> J
    I --> J
    J --> K{재현율이 충분한가?}
    K -->|아니오| L[임계값 낮추기]
    K -->|예| M{정밀도가 괜찮은가?}
    M -->|아니오| N[임계값 올리거나 특성 추가]
    M -->|예| O[배포하기]

만들어보기

Step 1: 불균형 데이터셋 생성

import numpy as np

def make_imbalanced_data(n_majority=950, n_minority=50, seed=42):
    rng = np.random.RandomState(seed)
    X_maj = rng.randn(n_majority, 2) * 1.0 + np.array([0.0, 0.0])
    X_min = rng.randn(n_minority, 2) * 0.8 + np.array([2.5, 2.5])
    X = np.vstack([X_maj, X_min])
    y = np.concatenate([np.zeros(n_majority), np.ones(n_minority)])
    shuffle_idx = rng.permutation(len(y))
    return X[shuffle_idx], y[shuffle_idx]

Step 2: SMOTE를 처음부터 구현하기

def smote(X_minority, k=5, n_synthetic=100, seed=42):
    rng = np.random.RandomState(seed)
    n_samples = len(X_minority)
    k = min(k, n_samples - 1)
    synthetic = []

    for _ in range(n_synthetic):
        idx = rng.randint(0, n_samples)
        neighbors = find_k_neighbors(X_minority, idx, k)
        neighbor_idx = neighbors[rng.randint(0, len(neighbors))]
        t = rng.random()
        new_point = X_minority[idx] + t * (X_minority[neighbor_idx] - X_minority[idx])
        synthetic.append(new_point)

    return np.array(synthetic)

Step 3: 무작위 오버샘플링과 언더샘플링

def random_oversample(X, y, seed=42):
    rng = np.random.RandomState(seed)
    classes, counts = np.unique(y, return_counts=True)
    max_count = counts.max()

    X_resampled = list(X)
    y_resampled = list(y)

    for cls, count in zip(classes, counts):
        if count < max_count:
            cls_indices = np.where(y == cls)[0]
            n_needed = max_count - count
            chosen = rng.choice(cls_indices, size=n_needed, replace=True)
            X_resampled.extend(X[chosen])
            y_resampled.extend(y[chosen])

    X_out = np.array(X_resampled)
    y_out = np.array(y_resampled)
    shuffle = rng.permutation(len(y_out))
    return X_out[shuffle], y_out[shuffle]


def random_undersample(X, y, seed=42):
    rng = np.random.RandomState(seed)
    classes, counts = np.unique(y, return_counts=True)
    min_count = counts.min()

    X_resampled = []
    y_resampled = []

    for cls in classes:
        cls_indices = np.where(y == cls)[0]
        chosen = rng.choice(cls_indices, size=min_count, replace=False)
        X_resampled.extend(X[chosen])
        y_resampled.extend(y[chosen])

    X_out = np.array(X_resampled)
    y_out = np.array(y_resampled)
    shuffle = rng.permutation(len(y_out))
    return X_out[shuffle], y_out[shuffle]

random_oversample은 소수 클래스(minority class)를 복제하고, random_undersample은 다수 클래스(majority class)를 줄입니다. 오버샘플링은 정보 손실(information loss)이 없지만 중복으로 인한 과적합 위험이 있고, 언더샘플링은 빠르지만 다수 클래스의 정보를 버립니다.

Step 4: 클래스 가중치를 사용하는 로지스틱 회귀

def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))


def logistic_regression_weighted(X, y, weights, lr=0.01, epochs=200):
    n_samples, n_features = X.shape
    w = np.zeros(n_features)
    b = 0.0

    for _ in range(epochs):
        z = X @ w + b
        pred = sigmoid(z)
        error = pred - y
        weighted_error = error * weights

        gradient_w = (X.T @ weighted_error) / n_samples
        gradient_b = np.mean(weighted_error)

        w -= lr * gradient_w
        b -= lr * gradient_b

    return w, b


def compute_class_weights(y):
    classes, counts = np.unique(y, return_counts=True)
    n_samples = len(y)
    n_classes = len(classes)
    weight_map = {}
    for cls, count in zip(classes, counts):
        weight_map[cls] = n_samples / (n_classes * count)
    return np.array([weight_map[yi] for yi in y])

Step 5: 임계값 튜닝

def find_optimal_threshold(y_true, y_probs, metric="f1"):
    best_threshold = 0.5
    best_score = -1.0

    for threshold in np.arange(0.05, 0.96, 0.01):
        y_pred = (y_probs >= threshold).astype(int)
        tp = np.sum((y_pred == 1) & (y_true == 1))
        fp = np.sum((y_pred == 1) & (y_true == 0))
        fn = np.sum((y_pred == 0) & (y_true == 1))

        if metric == "f1":
            precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
            recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
            score = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
        elif metric == "recall":
            score = tp / (tp + fn) if (tp + fn) > 0 else 0.0
        elif metric == "precision":
            score = tp / (tp + fp) if (tp + fp) > 0 else 0.0

        if score > best_score:
            best_score = score
            best_threshold = threshold

    return best_threshold, best_score

Step 6: 평가 함수

def confusion_matrix_values(y_true, y_pred):
    tp = np.sum((y_pred == 1) & (y_true == 1))
    tn = np.sum((y_pred == 0) & (y_true == 0))
    fp = np.sum((y_pred == 1) & (y_true == 0))
    fn = np.sum((y_pred == 0) & (y_true == 1))
    return tp, tn, fp, fn


def compute_metrics(y_true, y_pred):
    tp, tn, fp, fn = confusion_matrix_values(y_true, y_pred)
    accuracy = (tp + tn) / (tp + tn + fp + fn)
    precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
    recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
    f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0

    denom = np.sqrt(float((tp + fp) * (tp + fn) * (tn + fp) * (tn + fn)))
    mcc = (tp * tn - fp * fn) / denom if denom > 0 else 0.0

    return {
        "accuracy": accuracy,
        "precision": precision,
        "recall": recall,
        "f1": f1,
        "mcc": mcc,
    }

이 코드는 TP, TN, FP, FN을 계산하고 정확도, 정밀도, 재현율, F1, MCC를 반환합니다. 불균형 데이터에서는 정확도만 보지 않고 소수 클래스를 실제로 잡는 지표를 함께 살펴봐야 합니다.

Step 7: 모든 접근 비교

X, y = make_imbalanced_data(950, 50, seed=42)
split = int(0.8 * len(y))
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

# 기준선: 별도 처리 없음
w_base, b_base = logistic_regression_weighted(
    X_train, y_train, np.ones(len(y_train)), lr=0.1, epochs=300
)
probs_base = sigmoid(X_test @ w_base + b_base)
preds_base = (probs_base >= 0.5).astype(int)

# 오버샘플링
X_over, y_over = random_oversample(X_train, y_train)
w_over, b_over = logistic_regression_weighted(
    X_over, y_over, np.ones(len(y_over)), lr=0.1, epochs=300
)
preds_over = (sigmoid(X_test @ w_over + b_over) >= 0.5).astype(int)

# SMOTE
minority_mask = y_train == 1
X_minority = X_train[minority_mask]
synthetic = smote(X_minority, k=5, n_synthetic=len(y_train) - 2 * int(minority_mask.sum()))
X_smote = np.vstack([X_train, synthetic])
y_smote = np.concatenate([y_train, np.ones(len(synthetic))])
w_sm, b_sm = logistic_regression_weighted(
    X_smote, y_smote, np.ones(len(y_smote)), lr=0.1, epochs=300
)
preds_smote = (sigmoid(X_test @ w_sm + b_sm) >= 0.5).astype(int)

# 클래스 가중치
sample_weights = compute_class_weights(y_train)
w_cw, b_cw = logistic_regression_weighted(
    X_train, y_train, sample_weights, lr=0.1, epochs=300
)
probs_cw = sigmoid(X_test @ w_cw + b_cw)
preds_cw = (probs_cw >= 0.5).astype(int)

# 임계값 튜닝. 테스트 집합이 아니라 별도 검증 집합에서 튜닝해야 한다.
probs_val = sigmoid(X_val @ w_cw + b_cw)
best_thresh, best_f1 = find_optimal_threshold(y_val, probs_val, metric="f1")
preds_thresh = (probs_cw >= best_thresh).astype(int)

code/imbalanced.py는 기준선(baseline), 처리 없음, 오버샘플링, 언더샘플링, SMOTE, 클래스 가중치, 임계값 튜닝, 가중 손실(weighted loss)을 한 번에 실행하고 결과를 출력합니다.

사용하기

scikit-learn과 imbalanced-learn을 쓰면 다음처럼 사용할 수 있습니다.

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, f1_score
from sklearn.model_selection import train_test_split
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import RandomUnderSampler
from imblearn.pipeline import Pipeline

X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y)

model_weighted = LogisticRegression(class_weight="balanced")
model_weighted.fit(X_train, y_train)
print(classification_report(y_test, model_weighted.predict(X_test)))

smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
model_smote = LogisticRegression()
model_smote.fit(X_resampled, y_resampled)
print(classification_report(y_test, model_smote.predict(X_test)))

pipeline = Pipeline([
    ("smote", SMOTE()),
    ("model", LogisticRegression(class_weight="balanced")),
])
pipeline.fit(X_train, y_train)
print(classification_report(y_test, pipeline.predict(X_test)))

처음부터 구현한 코드는 각 기법이 정확히 어떤 일을 하는지 보여줍니다. SMOTE는 소수 클래스에서 k-NN 보간(interpolation)을 수행하는 것이고, 클래스 가중치는 손실에 가중치를 곱하는 것이며, 임계값 튜닝은 결정 컷오프(cutoff)를 훑는 반복문(for-loop)입니다. 마법은 없습니다.

산출물 만들기

이 lesson의 최종 산출물은 다음입니다.

  • outputs/skill-imbalanced-data.md: 불균형 분류 문제(imbalanced classification problem)를 처리하기 위한 의사결정 체크리스트(decision checklist)

연습문제

  1. (중간) 경계 기반 SMOTE(Borderline-SMOTE): 클래스가 겹치는 데이터셋에서 결정 경계(decision boundary) 근처 소수 클래스 점에 대해서만 합성 샘플을 만들도록 SMOTE 구현을 수정합니다. k개 최근접 이웃에 다수 클래스 샘플이 포함된 소수 클래스 점을 경계 근처로 봅니다. 표준 SMOTE와 결과를 비교합니다.

  2. (중간) 비용 행렬 최적화(Cost matrix optimization): 비용 행렬(cost matrix)을 매개변수로 받는 비용 민감 학습(cost-sensitive learning)을 구현합니다. 비용 행렬을 받아 기대 비용(expected cost)을 최소화하는 최적 예측(optimal prediction)을 반환하는 함수를 만듭니다. 비용 비율 1:10, 1:100, 1:1000에 따라 정밀도-재현율 절충(precision-recall tradeoff)이 어떻게 달라지는지 그래프로 그립니다.

  3. (어려움) 임계값 보정(Threshold calibration): 플랫 스케일링(Platt scaling)을 구현합니다. 모델의 원시 출력(raw output)에 로지스틱 회귀를 맞춰 보정된 확률(calibrated probability)을 만듭니다. 보정 전후의 정밀도-재현율 곡선을 비교합니다. 보정은 순위를 바꾸지 않아 AUC는 그대로지만 확률값을 더 의미 있게 만들어 줍니다.

  4. (어려움) 균형 배깅 앙상블(Balanced bagging ensemble): 여러 모델을 학습하되, 각 모델을 균형 잡힌 부트스트랩 샘플(balanced bootstrap sample)에서 학습합니다. 각 샘플은 모든 소수 클래스 샘플과 다수 클래스의 무작위 부분 집합으로 구성합니다. 예측을 평균 내고 SMOTE 단일 모델과 비교합니다. 성능과 실행마다의 분산(variance)을 모두 측정합니다.

  5. (중간) 불균형 비율 실험(Imbalance ratio experiment): 균형 잡힌 데이터셋에서 50/50, 70/30, 90/10, 95/5, 99/1로 불균형 비율(imbalance ratio)을 점점 키웁니다. 각 비율마다 SMOTE를 적용하지 않은 경우와 적용한 경우를 학습합니다. 두 접근의 F1과 불균형 비율을 그래프로 그립니다. 어느 비율부터 SMOTE가 의미 있는 차이를 만들기 시작하나요?

핵심 용어

용어흔한 설명실제 의미
클래스 불균형(Class imbalance)"한 클래스가 훨씬 많음"데이터셋의 클래스 분포(class distribution)가 크게 기울어져(skewed) 모델이 다수 클래스를 선호하게 되는 상태
SMOTE"합성 오버샘플링"기존 소수 클래스 샘플과 그 최근접 소수 클래스 이웃 사이를 보간해 새 소수 클래스 샘플을 생성
클래스 가중치(Class weights)"드문 클래스의 오류를 더 비싸게"클래스별 가중치를 손실 함수에 곱해 소수 클래스 오분류에 더 큰 벌점을 부여
임계값 튜닝(Threshold tuning)"결정 경계 옮기기"분류 컷오프(classification cutoff)를 기본 0.5가 아니라 원하는 지표(desired metric)를 최적화하는 값으로 바꾸는 것
정밀도-재현율 절충(Precision-recall tradeoff)"둘 다 가질 수 없음"임계값을 낮추면 재현율은 오르지만 거짓 양성(false positive)이 늘어 정밀도가 떨어지고, 반대로 임계값을 높이면 정밀도는 오르지만 재현율은 떨어지는 관계
AUPRC"정밀도-재현율 곡선 아래 면적"정밀도-재현율 곡선(precision-recall curve)을 하나의 값으로 요약. 심한 불균형에서 AUC-ROC보다 정보가 많음
매튜스 상관계수(Matthews Correlation Coefficient)"균형 잡힌 지표"예측과 실제 레이블 사이의 상관(correlation). 양쪽 클래스 모두에서 잘해야 높음
비용 민감 학습(Cost-sensitive learning)"다른 실수의 비용이 다름"실제 세계의 오분류 비용(misclassification cost)을 학습 목표(training objective)에 반영해 전체 비용을 줄이도록 학습
무작위 오버샘플링(Random oversampling)"소수 클래스 복제"소수 클래스 샘플을 반복해 클래스 개수를 맞추는 방식. 단순하지만 중복으로 인한 과적합 위험이 있음

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

imbalanced
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

skill-imbalanced-data

Decision checklist for handling imbalanced classification problems

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.SMOTE는 합성 소수 클래스 샘플(synthetic minority sample)을 어떻게 생성하나요?

2.불균형 데이터셋(imbalanced dataset)에서 분류 임계값(classification threshold)을 0.5에서 0.3으로 낮추면 정밀도(precision)와 재현율(recall)은 어떻게 되나요?

3.심하게 불균형한 데이터셋(highly imbalanced dataset)에서 AUPRC가 AUC-ROC보다 더 정보가 많은(informative) 이유는 무엇인가요?

0/3 답변 완료