분류 문제에 있어 Multi-Class와 Multi-Label의 헷갈리는 점이 종종 발생해 정리하려고 한다.
요약
- 멀티클래스: 샘플당 정답 1개 (예: 고양이/개/새).
- 멀티레이블: 샘플당 정답 여러 개 가능 (예: 감정 joy+trust).
- 핵심 차이는 출력층, 활성화 함수, 손실함수, 평가 지표, 임계치(Threshold) 운용에 있다.
- 실무에서는 데이터 라벨 표현, 클래스 불균형, 임계치 튜닝, 캘리브레이션, 리콜/정밀 균형이 성패를 가른다.
1) 문제 정의와 차이 한눈에 보기
항목 멀티클래스 (Multi-Class) 멀티레이블 (Multi-Label)
| 라벨 수 | 1개 고정 | 0~N개 가변 |
| 출력층 | 크기 = 클래스 수 | 크기 = 라벨 수 |
| 활성화 | softmax | sigmoid |
| 손실함수 | CrossEntropyLoss | BCEWithLogitsLoss |
| 예측 | argmax 1개 | 각 라벨별 점수>임계치 |
| 지표 | Acc, Macro/Micro F1 등 | Micro/Macro/Weighted F1, mAP, Subset Acc 등 |
2) 데이터 표현 (y의 형태)
멀티클래스
# 예: 3클래스 (0,1,2)
y = [2, 0, 1, 1, 2] # (N, ) 정수 인덱스
멀티레이블
# 예: 5라벨 (A,B,C,D,E)
# 각 샘플은 라벨 벡터로 표현 (원-핫 다중)
Y = [
[1,0,1,0,0], # A+C
[0,1,0,0,1], # B+E
[0,0,0,1,0], # D
]
# shape: (N, L)
팁: 텍스트 분류라면 sklearn.preprocessing.MultiLabelBinarizer 혹은 직접 사전(mapping)으로 변환.
3) 모델링 차이 (출력/활성/손실)
멀티클래스: Softmax + CrossEntropy
import torch
import torch.nn as nn
class HeadMultiClass(nn.Module):
def __init__(self, hidden_size: int, num_classes: int):
super().__init__()
self.fc = nn.Linear(hidden_size, num_classes)
def forward(self, h): # h: (batch, hidden)
logits = self.fc(h) # (batch, C)
# train: CrossEntropyLoss(logits, target)
# infer: probs = softmax(logits, dim=-1); pred = argmax
return logits
멀티레이블: Sigmoid + BCEWithLogits
class HeadMultiLabel(nn.Module):
def __init__(self, hidden_size: int, num_labels: int):
super().__init__()
self.fc = nn.Linear(hidden_size, num_labels)
def forward(self, h):
logits = self.fc(h) # (batch, L)
# train: BCEWithLogitsLoss(logits, targets)
# infer: probs = sigmoid(logits); pred = probs > thresholds
return logits
BCEWithLogitsLoss는 내부에 시그모이드를 포함하므로, 학습 시 시그모이드를 따로 적용하지 않는다.
4) 평가 지표: 언제 무엇을 쓸까
멀티클래스
- Accuracy: 직관적이지만 클래스 불균형에 취약.
- Macro F1: 클래스별 F1 평균(동일 가중). 소수 클래스까지 균형 있게.
- Weighted F1: 샘플 수로 가중.
멀티레이블
- Micro F1: 전체 TP/FP/FN 집계 후 계산 → 불균형에 상대적으로 강함.
- Macro F1: 라벨별 F1의 평균 → 소수 라벨 민감.
- Subset Accuracy: 모든 라벨을 정확히 맞춘 비율 → 지나치게 보수적(희귀).
- mAP (mean Average Precision), PR-AUC: 랭킹 기반, 임계치 독립.
from sklearn.metrics import f1_score, accuracy_score, average_precision_score
import numpy as np
# y_true, y_pred in {0,1}; y_score in [0,1]
macro_f1 = f1_score(y_true, y_pred, average='macro')
micro_f1 = f1_score(y_true, y_pred, average='micro')
subset_acc = (y_true == y_pred).all(axis=1).mean()
map_score = average_precision_score(y_true, y_score, average='macro')
5) 임계치(Threshold)
멀티레이블은 라벨별 임계치가 핵심. 기본 0.5로 두면 재현율이 과소/과대가 되기 쉽다.
간단한 라벨별 임계치 튜닝 (DEV 기준)
import numpy as np
from sklearn.metrics import f1_score
def search_best_thresholds(y_true, y_score, grid=None):
L = y_true.shape[1]
grid = grid or np.linspace(0.1, 0.9, 17) # 0.1~0.9 by 0.05
best = np.zeros(L)
for l in range(L):
f1_best, th_best = -1, 0.5
for th in grid:
pred_l = (y_score[:, l] >= th).astype(int)
f1_l = f1_score(y_true[:, l], pred_l, zero_division=0)
if f1_l > f1_best:
f1_best, th_best = f1_l, th
best[l] = th_best
return best
# dev에서 thresholds 찾고, test에 적용
# th = search_best_thresholds(Y_dev, S_dev)
# Y_pred = (S_test >= th).astype(int)
6) 클래스 불균형 다루기
- 입력단: 샘플링 (언더/오버/SMOTE)
- 손실단: 가중치(class_weight, pos_weight) / Focal Loss
- 출력단: 라벨별 임계치 조절로 리콜/정밀 균형
import torch
def compute_pos_weight(Y):
# Y: (N, L) in {0,1}
N, L = Y.shape
pos = Y.sum(0)
neg = N - pos
# pos_weight = neg/pos, 0 division 방지
pw = (neg / (pos + 1e-6)).clip(max=100)
return torch.tensor(pw, dtype=torch.float32)
# loss = BCEWithLogitsLoss(pos_weight=pos_weight)
멀티라벨에서 라벨 간 상관 구조가 크면, 샘플링 전략을 라벨 공존(co-occurrence)을 깨지 않도록 신중히 설계.
7) 베이스라인
멀티클래스: 로지스틱 회귀
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
clf = Pipeline([
("tfidf", TfidfVectorizer(max_features=50_000, ngram_range=(1,2))),
("lr", LogisticRegression(max_iter=200, n_jobs=-1))
])
clf.fit(texts_train, y_train) # y_train: (N,)
멀티레이블: One-vs-Rest
from sklearn.multiclass import OneVsRestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import MultiLabelBinarizer
mlb = MultiLabelBinarizer()
y_train_bin = mlb.fit_transform(y_train_multi) # list[set/labels]
ovr = OneVsRestClassifier(LogisticRegression(max_iter=200, n_jobs=-1))
ovr.fit(texts_train_vec, y_train_bin)
# 예측 점수: ovr.decision_function(X)
8) PyTorch 미니멀 레퍼런스 (텍스트 임베딩 이후 헤드만 예시)
import torch, torch.nn as nn
from torch.utils.data import Dataset, DataLoader
class SimpleTextDataset(Dataset):
def __init__(self, X_embed, Y):
self.X = X_embed # (N, H)
self.Y = Y # (N, ) or (N, L)
def __len__(self): return len(self.X)
def __getitem__(self, i): return self.X[i], self.Y[i]
class MultiLabelNet(nn.Module):
def __init__(self, H, L):
super().__init__()
self.fc = nn.Linear(H, L)
def forward(self, x):
return self.fc(x)
H, L = 768, 8
model = MultiLabelNet(H, L)
opt = torch.optim.AdamW(model.parameters(), lr=2e-4)
crit = nn.BCEWithLogitsLoss()
for X, Y in DataLoader(SimpleTextDataset(X_train, Y_train), batch_size=64, shuffle=True):
logits = model(X)
loss = crit(logits, Y)
loss.backward(); opt.step(); opt.zero_grad()
9) 실전 스니펫: 한국어 감정 8라벨(멀티레이블)
라벨: joy, anticipation, trust, surprise, disgust, fear, anger, sadness
(1) 임베딩 모델 + 헤드
import torch
import torch.nn as nn
from transformers import AutoModel, AutoTokenizer
class Emotion8(nn.Module):
def __init__(self, plm="klue/roberta-base", num_labels=8):
super().__init__()
self.backbone = AutoModel.from_pretrained(plm)
hidden = self.backbone.config.hidden_size
self.head = nn.Linear(hidden, num_labels)
def forward(self, input_ids, attention_mask=None):
out = self.backbone(input_ids=input_ids, attention_mask=attention_mask)
h = out.last_hidden_state[:, 0] # [CLS] 또는 pooler 적절히 선택
logits = self.head(h)
return logits
(2) 학습 루프 + 라벨별 임계치 저장
from sklearn.metrics import f1_score
import numpy as np
crit = nn.BCEWithLogitsLoss()
@torch.no_grad()
def eval_dev(model, dev_loader):
model.eval()
scores, truths = [], []
for batch in dev_loader:
logits = model(batch["input_ids"].to(device), batch["attention_mask"].to(device))
probs = torch.sigmoid(logits).cpu().numpy()
scores.append(probs)
truths.append(batch["labels"].numpy())
S = np.vstack(scores); Y = np.vstack(truths)
th = search_best_thresholds(Y, S)
P = (S >= th).astype(int)
return {
"micro_f1": f1_score(Y, P, average="micro"),
"macro_f1": f1_score(Y, P, average="macro"),
"thresholds": th,
}
마무리
멀티클래스와 멀티레이블은 닮았지만, 임계치/지표/불균형에서 실전 난이도가 크게 갈린다. 본문 스니펫을 뼈대로 잡고, Dev 기반으로 라벨별 튜닝을 체계화하면 운영 품질이 안정되는 것같다.
다만,, 현재진행형인 AI 과제에서 Macro F1 score가 잘 나오지 않는 것이 딜레마다.
반응형
'공부하는 습관을 들이자 > Python_ML' 카테고리의 다른 글
| [2-4]타이타닉 생존자 예측 (0) | 2022.01.05 |
|---|---|
| [#3]데이터 전처리 (0) | 2022.01.01 |
| [2. (2) 교차 검증] (0) | 2021.12.30 |
| [2. (1) 붓꽃 품종 예측하기] (0) | 2021.12.29 |
| 부스팅 알고리즘 (0) | 2021.12.29 |