다중 레이블 분류 성능 평가 방법
들어가며
모델을 훈련했다면 이제 그 성능을 정확히 평가해야 합니다. 하지만 다중 레이블 분류는 일반적인 단일 분류와 달리 여러 개의 레이블이 동시에 예측되기 때문에, 평가 방법도 더욱 복잡하고 다양한 관점에서 접근해야 합니다.
이번 글에서는 편향성 감지 모델의 성능을 종합적으로 평가할 수 있는 다양한 지표들과 그 해석 방법을 자세히 알아보겠습니다.

다중 레이블 분류의 복잡성
단일 분류 vs 다중 레이블 분류
단일 분류의 경우:
- 하나의 정답 클래스만 존재
- 정확도(Accuracy) 하나로 성능 요약 가능
- 혼동 행렬(Confusion Matrix)이 직관적
다중 레이블 분류의 경우:
- 여러 레이블이 동시에 참일 수 있음
- 레이블 간 상관관계 고려 필요
- 부분적으로 맞는 예측의 처리 문제
예시로 이해하는 복잡성
실제 레이블: [1, 0, 1, 0, 0, 0, 0, 0, 0] # 성별, 인종 편향
예측 레이블: [1, 0, 0, 0, 0, 1, 0, 0, 0] # 성별, 세대 편향
이 경우 성별 편향은 맞췄지만 인종 편향은 놓치고, 세대 편향은 잘못 예측했습니다. 이를 어떻게 평가할 것인가가 핵심입니다.
기본 성능 지표들
1. 정확도 (Accuracy)
Exact Match Accuracy
모든 레이블을 정확히 맞춘 샘플의 비율:
def exact_match_accuracy(y_true, y_pred):
"""모든 레이블이 정확히 일치하는 비율"""
return np.mean(np.all(y_true == y_pred, axis=1))
# 예시
y_true = np.array([[1, 0, 1], [0, 1, 0], [1, 1, 0]])
y_pred = np.array([[1, 0, 1], [0, 1, 1], [1, 1, 0]])
print(f"Exact Match Accuracy: {exact_match_accuracy(y_true, y_pred):.3f}")
# 결과: 0.667 (3개 중 2개만 완전히 일치)
Hamming Loss
레이블별 오분류율의 평균:
from sklearn.metrics import hamming_loss
def hamming_accuracy(y_true, y_pred):
"""햄밍 거리 기반 정확도"""
return 1 - hamming_loss(y_true, y_pred)
print(f"Hamming Accuracy: {hamming_accuracy(y_true, y_pred):.3f}")
# 결과: 0.889 (9개 레이블 중 8개가 맞음)
2. 정밀도, 재현율, F1 스코어
다중 레이블에서는 평균화 방법에 따라 다양한 계산이 가능합니다.
Macro Average (매크로 평균)
각 레이블별로 계산한 후 단순 평균:
from sklearn.metrics import precision_score, recall_score, f1_score
def calculate_macro_metrics(y_true, y_pred):
"""레이블별 지표의 단순 평균"""
precision = precision_score(y_true, y_pred, average='macro', zero_division=0)
recall = recall_score(y_true, y_pred, average='macro', zero_division=0)
f1 = f1_score(y_true, y_pred, average='macro', zero_division=0)
return {
'macro_precision': precision,
'macro_recall': recall,
'macro_f1': f1
}
매크로 평균의 특징:
- 모든 레이블을 동등하게 취급
- 희귀한 편향 유형도 중요하게 평가
- 클래스 불균형에 민감
Micro Average (마이크로 평균)
전체 예측을 하나로 합쳐서 계산:
def calculate_micro_metrics(y_true, y_pred):
"""전체 예측의 종합 지표"""
precision = precision_score(y_true, y_pred, average='micro')
recall = recall_score(y_true, y_pred, average='micro')
f1 = f1_score(y_true, y_pred, average='micro')
return {
'micro_precision': precision,
'micro_recall': recall,
'micro_f1': f1
}
마이크로 평균의 특징:
- 빈도가 높은 레이블의 영향이 큼
- 전체적인 성능을 잘 반영
- 클래스 불균형에 덜 민감
Weighted Average (가중 평균)
레이블의 빈도에 따라 가중치를 부여:
def calculate_weighted_metrics(y_true, y_pred):
"""빈도 가중 평균 지표"""
precision = precision_score(y_true, y_pred, average='weighted', zero_division=0)
recall = recall_score(y_true, y_pred, average='weighted', zero_division=0)
f1 = f1_score(y_true, y_pred, average='weighted', zero_division=0)
return {
'weighted_precision': precision,
'weighted_recall': recall,
'weighted_f1': f1
}
3. ROC AUC와 PR AUC
ROC AUC (Receiver Operating Characteristic)
from sklearn.metrics import roc_auc_score
import matplotlib.pyplot as plt
def calculate_roc_auc(y_true, y_pred_probs):
"""ROC AUC 계산 (확률 점수 필요)"""
try:
# 매크로 평균 ROC AUC
macro_auc = roc_auc_score(y_true, y_pred_probs, average='macro')
# 레이블별 ROC AUC
label_aucs = []
for i in range(y_true.shape[1]):
if len(np.unique(y_true[:, i])) > 1: # 양/음성 모두 존재하는 경우만
auc = roc_auc_score(y_true[:, i], y_pred_probs[:, i])
label_aucs.append(auc)
else:
label_aucs.append(np.nan)
return macro_auc, label_aucs
except ValueError as e:
print(f"ROC AUC 계산 오류: {e}")
return None, None
PR AUC (Precision-Recall AUC)
클래스 불균형 상황에서 더 적합한 지표:
from sklearn.metrics import average_precision_score
def calculate_pr_auc(y_true, y_pred_probs):
"""PR AUC 계산"""
macro_pr_auc = average_precision_score(y_true, y_pred_probs, average='macro')
label_pr_aucs = []
for i in range(y_true.shape[1]):
pr_auc = average_precision_score(y_true[:, i], y_pred_probs[:, i])
label_pr_aucs.append(pr_auc)
return macro_pr_auc, label_pr_aucs
종합 평가 함수 구현
완전한 평가 메트릭 계산
import torch
import numpy as np
from sklearn.metrics import classification_report
def compute_metrics(pred):
"""Trainer에서 사용할 종합 평가 함수"""
labels = pred.label_ids
predictions = pred.predictions
# 로짓을 확률로 변환
probs = torch.sigmoid(torch.tensor(predictions))
# 이진 예측으로 변환 (임계값 0.5)
pred_labels = (probs > 0.5).int().numpy()
# 기본 지표들
exact_match = np.mean(np.all(labels == pred_labels, axis=1))
hamming_acc = 1 - hamming_loss(labels, pred_labels)
# 정밀도, 재현율, F1
macro_precision = precision_score(labels, pred_labels, average='macro', zero_division=0)
macro_recall = recall_score(labels, pred_labels, average='macro', zero_division=0)
macro_f1 = f1_score(labels, pred_labels, average='macro', zero_division=0)
micro_precision = precision_score(labels, pred_labels, average='micro')
micro_recall = recall_score(labels, pred_labels, average='micro')
micro_f1 = f1_score(labels, pred_labels, average='micro')
# ROC AUC
try:
macro_auc = roc_auc_score(labels, probs.numpy(), average='macro')
except ValueError:
macro_auc = 0.0
# PR AUC
macro_pr_auc = average_precision_score(labels, probs.numpy(), average='macro')
return {
'exact_match_accuracy': exact_match,
'hamming_accuracy': hamming_acc,
'macro_precision': macro_precision,
'macro_recall': macro_recall,
'macro_f1': macro_f1,
'micro_precision': micro_precision,
'micro_recall': micro_recall,
'micro_f1': micro_f1,
'macro_roc_auc': macro_auc,
'macro_pr_auc': macro_pr_auc
}
레이블별 상세 분석
혼동 행렬과 분류 리포트
def detailed_evaluation(y_true, y_pred, y_pred_probs, label_names):
"""레이블별 상세 분석"""
print("=== 전체 성능 요약 ===")
print(f"Exact Match Accuracy: {np.mean(np.all(y_true == y_pred, axis=1)):.3f}")
print(f"Hamming Accuracy: {1 - hamming_loss(y_true, y_pred):.3f}")
print(f"Macro F1: {f1_score(y_true, y_pred, average='macro', zero_division=0):.3f}")
print(f"Micro F1: {f1_score(y_true, y_pred, average='micro'):.3f}")
print("\n=== 레이블별 상세 분석 ===")
report = classification_report(
y_true, y_pred,
target_names=label_names,
output_dict=True,
zero_division=0
)
for i, label_name in enumerate(label_names):
if label_name in report:
metrics = report[label_name]
print(f"\n{label_name}:")
print(f" 정밀도: {metrics['precision']:.3f}")
print(f" 재현율: {metrics['recall']:.3f}")
print(f" F1 스코어: {metrics['f1-score']:.3f}")
print(f" 지원 샘플: {metrics['support']}")
# ROC AUC (이진 분류가 가능한 경우)
if len(np.unique(y_true[:, i])) > 1:
auc = roc_auc_score(y_true[:, i], y_pred_probs[:, i])
print(f" ROC AUC: {auc:.3f}")
임계값 최적화
from sklearn.metrics import precision_recall_curve
def optimize_thresholds(y_true, y_pred_probs, label_names):
"""레이블별 최적 임계값 찾기"""
optimal_thresholds = []
for i, label_name in enumerate(label_names):
if len(np.unique(y_true[:, i])) > 1:
# Precision-Recall 곡선 계산
precision, recall, thresholds = precision_recall_curve(
y_true[:, i], y_pred_probs[:, i]
)
# F1 스코어가 최대인 임계값 찾기
f1_scores = 2 * (precision * recall) / (precision + recall + 1e-8)
optimal_idx = np.argmax(f1_scores)
optimal_threshold = thresholds[optimal_idx] if optimal_idx < len(thresholds) else 0.5
optimal_thresholds.append(optimal_threshold)
print(f"{label_name}: 최적 임계값 = {optimal_threshold:.3f}, F1 = {f1_scores[optimal_idx]:.3f}")
else:
optimal_thresholds.append(0.5)
print(f"{label_name}: 데이터 부족으로 기본값 0.5 사용")
return optimal_thresholds
시각화를 통한 성능 분석
성능 지표 히트맵
import matplotlib.pyplot as plt
import seaborn as sns
def plot_performance_heatmap(y_true, y_pred_probs, label_names, thresholds=None):
"""레이블별 성능 지표 히트맵"""
if thresholds is None:
thresholds = [0.5] * len(label_names)
metrics_data = []
for i, (label_name, threshold) in enumerate(zip(label_names, thresholds)):
pred_binary = (y_pred_probs[:, i] > threshold).astype(int)
if len(np.unique(y_true[:, i])) > 1:
precision = precision_score(y_true[:, i], pred_binary, zero_division=0)
recall = recall_score(y_true[:, i], pred_binary, zero_division=0)
f1 = f1_score(y_true[:, i], pred_binary, zero_division=0)
auc = roc_auc_score(y_true[:, i], y_pred_probs[:, i])
else:
precision = recall = f1 = auc = 0.0
metrics_data.append([precision, recall, f1, auc])
# 히트맵 생성
metrics_df = pd.DataFrame(
metrics_data,
index=label_names,
columns=['Precision', 'Recall', 'F1', 'ROC AUC']
)
plt.figure(figsize=(8, 6))
sns.heatmap(metrics_df, annot=True, cmap='Blues', fmt='.3f')
plt.title('레이블별 성능 지표')
plt.tight_layout()
plt.show()
return metrics_df
ROC 곡선과 PR 곡선 시각화
from sklearn.metrics import roc_curve
def plot_roc_pr_curves(y_true, y_pred_probs, label_names):
"""ROC와 PR 곡선 시각화"""
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# ROC 곡선
ax1 = axes[0, 0]
for i, label_name in enumerate(label_names[:4]): # 처음 4개만 표시
if len(np.unique(y_true[:, i])) > 1:
fpr, tpr, _ = roc_curve(y_true[:, i], y_pred_probs[:, i])
auc = roc_auc_score(y_true[:, i], y_pred_probs[:, i])
ax1.plot(fpr, tpr, label=f'{label_name} (AUC={auc:.3f})')
ax1.plot([0, 1], [0, 1], 'k--', alpha=0.5)
ax1.set_xlabel('False Positive Rate')
ax1.set_ylabel('True Positive Rate')
ax1.set_title('ROC Curves')
ax1.legend()
# PR 곡선
ax2 = axes[0, 1]
for i, label_name in enumerate(label_names[:4]):
if len(np.unique(y_true[:, i])) > 1:
precision, recall, _ = precision_recall_curve(y_true[:, i], y_pred_probs[:, i])
pr_auc = average_precision_score(y_true[:, i], y_pred_probs[:, i])
ax2.plot(recall, precision, label=f'{label_name} (AP={pr_auc:.3f})')
ax2.set_xlabel('Recall')
ax2.set_ylabel('Precision')
ax2.set_title('Precision-Recall Curves')
ax2.legend()
plt.tight_layout()
plt.show()
실제 평가 실행
테스트 데이터 평가
# 훈련된 모델로 테스트 데이터 예측
test_results = trainer.predict(test)
test_predictions = test_results.predictions
test_labels = test_results.label_ids
# 확률로 변환
test_probs = torch.sigmoid(torch.tensor(test_predictions)).numpy()
# 레이블 이름 정의
label_names = ['성별', '정치', '인종', '국가', '지역', '세대', '사회계층', '외모', '기타']
# 기본 임계값(0.5)으로 이진 예측
test_pred_binary = (test_probs > 0.5).astype(int)
# 종합 평가 실행
print("=== 최종 테스트 결과 ===")
detailed_evaluation(test_labels, test_pred_binary, test_probs, label_names)
# 최적 임계값 찾기
print("\n=== 임계값 최적화 ===")
optimal_thresholds = optimize_thresholds(test_labels, test_probs, label_names)
# 최적화된 임계값으로 재평가
optimized_predictions = np.zeros_like(test_pred_binary)
for i, threshold in enumerate(optimal_thresholds):
optimized_predictions[:, i] = (test_probs[:, i] > threshold).astype(int)
print("\n=== 최적화된 임계값 결과 ===")
detailed_evaluation(test_labels, optimized_predictions, test_probs, label_names)
결과 해석과 개선 방향
성능 지표 해석 가이드
높은 정밀도, 낮은 재현율:
- 모델이 보수적으로 예측 (확실한 경우만 양성 예측)
- 거짓 긍정(False Positive)을 줄이고 싶은 경우에 적합
- 임계값을 낮춰서 재현율 개선 가능
낮은 정밀도, 높은 재현율:
- 모델이 적극적으로 예측 (의심스러운 경우도 양성 예측)
- 거짓 부정(False Negative)을 줄이고 싶은 경우에 적합
- 임계값을 높여서 정밀도 개선 가능
편향 유형별 특성 고려
def analyze_label_characteristics(y_true, label_names):
"""레이블별 특성 분석"""
print("=== 레이블별 데이터 특성 ===")
for i, label_name in enumerate(label_names):
positive_count = np.sum(y_true[:, i])
total_count = len(y_true)
positive_ratio = positive_count / total_count
print(f"{label_name}:")
print(f" 양성 샘플: {positive_count} / {total_count} ({positive_ratio:.3f})")
if positive_ratio < 0.01:
print(" → 매우 희귀한 편향 유형, 데이터 증강 고려")
elif positive_ratio < 0.05:
print(" → 희귀한 편향 유형, 가중치 조정 고려")
elif positive_ratio > 0.3:
print(" → 빈번한 편향 유형, 임계값 조정 고려")
다음 단계 미리보기
성능 평가가 완료되면 다음과 같은 인사이트를 얻을 수 있습니다:
- 전체적인 모델 성능: 편향 감지 시스템의 실용성 판단
- 레이블별 강약점: 어떤 편향 유형을 잘/못 감지하는지 파악
- 최적 임계값: 실제 서비스에 적용할 예측 기준점
- 개선 방향: 데이터 보강, 모델 조정 등의 다음 단계
다음 글에서는 이렇게 평가된 모델을 실제 서비스에 배포하고 활용하는 방법을 다뤄보겠습니다.
마무리
다중 레이블 분류의 성능 평가는 단일 지표로는 완전히 설명할 수 없는 복잡한 과정입니다. 다양한 관점에서의 종합적인 평가를 통해 모델의 실제 성능과 한계를 정확히 파악할 수 있으며, 이는 실무 적용에서 매우 중요한 정보가 됩니다.
특히 편향 감지와 같은 민감한 주제에서는 거짓 긍정과 거짓 부정의 비용을 신중히 고려하여 최적의 임계값을 설정하는 것이 핵심입니다.
'프로그래밍 > 딥러닝 (완)' 카테고리의 다른 글
편향성 감지 시스템의 사회적 의미와 활용 방안 (99) (3) | 2025.06.29 |
---|---|
실제 서비스 적용을 위한 모델 배포 (98) (2) | 2025.06.29 |
BERT 모델 훈련 및 하이퍼파라미터 튜닝 (96) (4) | 2025.06.28 |
한국어 텍스트 분류를 위한 데이터 전처리 (95) (1) | 2025.06.28 |
BERT와 딥러닝을 활용한 자연어 처리 입문 (94) (0) | 2025.06.28 |