손실 함수 (Loss Functions)¶
모델의 예측과 실제 값의 차이를 정량화하는 함수. 최적화의 목표가 되며, 문제 유형에 따라 적절한 손실 함수 선택이 중요함.
회귀 손실 함수¶
MSE (Mean Squared Error)¶
\[L = \frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2\]
import torch
import torch.nn as nn
import numpy as np
# PyTorch
mse_loss = nn.MSELoss()
loss = mse_loss(predictions, targets)
# 수동 구현
def mse_loss(y_true, y_pred):
return np.mean((y_true - y_pred) ** 2)
특성: - 이상치에 민감 (제곱으로 인한 큰 페널티) - 미분 가능, 부드러운 기울기 - 정규분포 가정 (최대우도 관점)
MAE (Mean Absolute Error)¶
\[L = \frac{1}{n}\sum_{i=1}^{n}|y_i - \hat{y}_i|\]
mae_loss = nn.L1Loss()
loss = mae_loss(predictions, targets)
def mae_loss(y_true, y_pred):
return np.mean(np.abs(y_true - y_pred))
특성: - 이상치에 강건 (robust) - 0에서 미분 불가 (기울기 불연속) - 라플라스 분포 가정
Huber Loss (Smooth L1)¶
MSE와 MAE의 장점 결합.
\[L = \begin{cases} \frac{1}{2}(y - \hat{y})^2 & \text{if } |y - \hat{y}| < \delta \\ \delta(|y - \hat{y}| - \frac{\delta}{2}) & \text{otherwise} \end{cases}\]
huber_loss = nn.SmoothL1Loss(beta=1.0) # beta = delta
loss = huber_loss(predictions, targets)
def huber_loss(y_true, y_pred, delta=1.0):
error = np.abs(y_true - y_pred)
quadratic = np.minimum(error, delta)
linear = error - quadratic
return np.mean(0.5 * quadratic ** 2 + delta * linear)
회귀 손실 비교¶
| 손실 함수 | 이상치 | 기울기 | 사용 사례 |
|---|---|---|---|
| MSE | 민감 | 부드러움 | 일반적 회귀 |
| MAE | 강건 | 상수 | 이상치 있는 데이터 |
| Huber | 중간 | 부드러움 | 혼합 상황 |
분류 손실 함수¶
Binary Cross-Entropy (BCE)¶
이진 분류용.
\[L = -\frac{1}{n}\sum_{i=1}^{n}[y_i \log(\hat{y}_i) + (1-y_i)\log(1-\hat{y}_i)]\]
# logits 입력 (sigmoid 내장)
bce_with_logits = nn.BCEWithLogitsLoss()
loss = bce_with_logits(logits, targets)
# 확률 입력
bce_loss = nn.BCELoss()
loss = bce_loss(torch.sigmoid(logits), targets)
def binary_cross_entropy(y_true, y_pred, eps=1e-7):
y_pred = np.clip(y_pred, eps, 1 - eps)
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
Categorical Cross-Entropy¶
다중 클래스 분류용.
\[L = -\sum_{c=1}^{C}y_c \log(\hat{y}_c)\]
# logits 입력 (softmax 내장)
ce_loss = nn.CrossEntropyLoss()
loss = ce_loss(logits, targets) # targets: 클래스 인덱스
# 확률 입력
nll_loss = nn.NLLLoss()
loss = nll_loss(torch.log_softmax(logits, dim=-1), targets)
def categorical_cross_entropy(y_true, y_pred, eps=1e-7):
"""y_true: one-hot, y_pred: softmax 출력"""
y_pred = np.clip(y_pred, eps, 1 - eps)
return -np.sum(y_true * np.log(y_pred)) / len(y_true)
Focal Loss¶
클래스 불균형 해결. 쉬운 샘플의 가중치 감소.
\[L = -\alpha_t (1-p_t)^\gamma \log(p_t)\]
- \(\alpha\): 클래스 가중치
- \(\gamma\): 포커싱 파라미터 (보통 2)
class FocalLoss(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0):
super().__init__()
self.alpha = alpha
self.gamma = gamma
def forward(self, inputs, targets):
bce_loss = nn.functional.binary_cross_entropy_with_logits(
inputs, targets, reduction='none'
)
probs = torch.sigmoid(inputs)
p_t = probs * targets + (1 - probs) * (1 - targets)
alpha_t = self.alpha * targets + (1 - self.alpha) * (1 - targets)
focal_weight = alpha_t * (1 - p_t) ** self.gamma
return (focal_weight * bce_loss).mean()
# 사용
focal_loss = FocalLoss(alpha=0.25, gamma=2.0)
loss = focal_loss(logits, targets)
Label Smoothing¶
과신(overconfidence) 방지. Hard label을 soft label로 변환.
\[y_{smooth} = (1-\epsilon)y + \frac{\epsilon}{K}\]
# PyTorch CrossEntropyLoss with label smoothing
ce_loss = nn.CrossEntropyLoss(label_smoothing=0.1)
loss = ce_loss(logits, targets)
def label_smoothing_loss(logits, targets, smoothing=0.1):
n_classes = logits.shape[-1]
log_probs = torch.log_softmax(logits, dim=-1)
# Hard label loss
nll_loss = -log_probs.gather(dim=-1, index=targets.unsqueeze(-1)).squeeze(-1)
# Smoothing loss (모든 클래스에 대한 평균)
smooth_loss = -log_probs.mean(dim=-1)
return (1 - smoothing) * nll_loss + smoothing * smooth_loss
LLM 관련 손실 함수¶
Language Modeling Loss¶
다음 토큰 예측의 Cross-Entropy.
def language_modeling_loss(logits, targets, ignore_index=-100):
"""
logits: (batch, seq_len, vocab_size)
targets: (batch, seq_len)
"""
# Shift for next token prediction
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = targets[..., 1:].contiguous()
loss_fct = nn.CrossEntropyLoss(ignore_index=ignore_index)
loss = loss_fct(
shift_logits.view(-1, shift_logits.size(-1)),
shift_labels.view(-1)
)
return loss
# Perplexity
perplexity = torch.exp(loss)
Contrastive Loss¶
임베딩 학습용. 유사한 쌍은 가깝게, 다른 쌍은 멀게.
class InfoNCELoss(nn.Module):
"""Contrastive learning loss (SimCLR, CLIP 등에서 사용)"""
def __init__(self, temperature=0.07):
super().__init__()
self.temperature = temperature
def forward(self, embeddings1, embeddings2):
# 정규화
embeddings1 = nn.functional.normalize(embeddings1, dim=-1)
embeddings2 = nn.functional.normalize(embeddings2, dim=-1)
# 유사도 행렬
similarity = embeddings1 @ embeddings2.T / self.temperature
# 대각선이 positive pairs
labels = torch.arange(len(embeddings1), device=similarity.device)
# 양방향 loss
loss_i2t = nn.functional.cross_entropy(similarity, labels)
loss_t2i = nn.functional.cross_entropy(similarity.T, labels)
return (loss_i2t + loss_t2i) / 2
Triplet Loss¶
앵커, 포지티브, 네거티브 삼중쌍 학습.
\[L = \max(0, d(a, p) - d(a, n) + margin)\]
KL Divergence¶
두 분포의 차이. VAE, 지식 증류에 사용.
\[D_{KL}(P||Q) = \sum P(x) \log\frac{P(x)}{Q(x)}\]
kl_loss = nn.KLDivLoss(reduction='batchmean')
# 학생 모델의 log_softmax와 교사 모델의 softmax
loss = kl_loss(
torch.log_softmax(student_logits / temperature, dim=-1),
torch.softmax(teacher_logits / temperature, dim=-1)
)
RLHF Loss (PPO)¶
강화학습 기반 미세조정.
def ppo_loss(log_probs, old_log_probs, advantages, clip_ratio=0.2):
"""Proximal Policy Optimization loss"""
ratio = torch.exp(log_probs - old_log_probs)
# Clipped surrogate objective
clip_adv = torch.clamp(ratio, 1 - clip_ratio, 1 + clip_ratio) * advantages
loss = -torch.min(ratio * advantages, clip_adv).mean()
return loss
def dpo_loss(policy_logps, ref_logps, yw_idx, yl_idx, beta=0.1):
"""Direct Preference Optimization loss"""
# 선호/비선호 응답의 로그 확률 차이
yw_logps = policy_logps[yw_idx] - ref_logps[yw_idx] # 선호
yl_logps = policy_logps[yl_idx] - ref_logps[yl_idx] # 비선호
loss = -torch.log(torch.sigmoid(beta * (yw_logps - yl_logps))).mean()
return loss
손실 함수 설계 원칙¶
클래스 가중치¶
불균형 데이터 처리.
# 클래스 빈도의 역수로 가중치 설정
class_counts = [1000, 100, 10]
weights = torch.tensor([1/c for c in class_counts])
weights = weights / weights.sum()
ce_loss = nn.CrossEntropyLoss(weight=weights)
다중 손실 결합¶
class CombinedLoss(nn.Module):
def __init__(self, alpha=0.5):
super().__init__()
self.alpha = alpha
self.ce_loss = nn.CrossEntropyLoss()
self.focal_loss = FocalLoss()
def forward(self, logits, targets):
loss1 = self.ce_loss(logits, targets)
loss2 = self.focal_loss(logits, targets)
return self.alpha * loss1 + (1 - self.alpha) * loss2
손실 스케일링¶
# 동적 손실 스케일링 (FP16 학습)
scaler = torch.cuda.amp.GradScaler()
with torch.cuda.amp.autocast():
loss = model(inputs)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
손실 함수 선택 가이드¶
| 문제 유형 | 권장 손실 함수 |
|---|---|
| 회귀 | MSE, Huber |
| 이진 분류 | BCE, Focal Loss |
| 다중 분류 | Cross-Entropy |
| 불균형 분류 | Focal Loss, 가중치 CE |
| 언어 모델링 | Cross-Entropy |
| 임베딩 학습 | Contrastive, Triplet |
| 지식 증류 | KL Divergence |
디버깅 팁¶
# Loss가 NaN인 경우
# 1. 입력 확인
assert not torch.isnan(inputs).any()
assert not torch.isnan(targets).any()
# 2. 로그 확률 클리핑
log_probs = torch.clamp(torch.log(probs), min=-100)
# 3. 수치 안정성
def stable_softmax(x):
x_max = x.max(dim=-1, keepdim=True).values
return torch.exp(x - x_max) / torch.exp(x - x_max).sum(dim=-1, keepdim=True)
Scikit-learn에서의 손실 함수¶
Scikit-learn은 손실 함수를 직접 지정하기보다 모델 선택으로 결정함.
from sklearn.linear_model import (
LinearRegression, # MSE
Ridge, # MSE + L2
Lasso, # MSE + L1
HuberRegressor, # Huber Loss
LogisticRegression, # Cross-Entropy
SGDClassifier, # 다양한 손실 지원
)
# SGDClassifier로 다양한 손실 함수 사용
clf_log = SGDClassifier(loss='log_loss') # Logistic (Cross-Entropy)
clf_hinge = SGDClassifier(loss='hinge') # SVM (Hinge Loss)
clf_huber = SGDClassifier(loss='modified_huber') # Smoothed Hinge
# 회귀에서 손실 함수 선택
from sklearn.linear_model import SGDRegressor
reg_mse = SGDRegressor(loss='squared_error') # MSE
reg_huber = SGDRegressor(loss='huber') # Huber
reg_epsilon = SGDRegressor(loss='epsilon_insensitive') # SVR
하이퍼파라미터 튜닝 팁¶
Focal Loss 파라미터¶
# gamma: 쉬운 샘플 가중치 감소 정도
# gamma=0: 일반 Cross-Entropy와 동일
# gamma=2: 일반적인 시작점
# gamma 높을수록: 어려운 샘플에 더 집중
# alpha: 클래스 가중치
# 양성 클래스 비율이 p일 때, alpha ≈ 1-p 로 시작
Label Smoothing 파라미터¶
# smoothing: 0.0 ~ 0.2 범위
# 0.0: 사용 안 함
# 0.1: 일반적인 값
# 0.2+: 과도한 smoothing, 성능 저하 가능
# 작은 데이터셋/과적합 심할 때: 높은 smoothing
# 큰 데이터셋/일반화 잘 될 때: 낮은 smoothing 또는 사용 안 함
손실 가중치 조합¶
# 여러 손실 결합 시 가중치 튜닝
# 1. 각 손실의 스케일 맞추기
# 2. 검증 세트에서 가중치 탐색
def combined_loss(pred, target, weights):
loss1 = F.cross_entropy(pred, target)
loss2 = focal_loss(pred, target)
loss3 = label_smoothing_loss(pred, target)
# 손실 스케일 정규화
losses = [loss1, loss2, loss3]
normalized = [l / l.detach() for l in losses] # gradient는 유지
return sum(w * l for w, l in zip(weights, normalized))
흔히 하는 실수¶
1. BCE vs BCEWithLogits 혼동¶
# 나쁜 예: sigmoid 두 번 적용
output = torch.sigmoid(logits)
loss = nn.BCEWithLogitsLoss()(output, target) # 내부에서 또 sigmoid
# 좋은 예: 용도에 맞게 선택
# 방법 1: logits 직접 사용
loss = nn.BCEWithLogitsLoss()(logits, target)
# 방법 2: 확률로 변환 후 BCE
probs = torch.sigmoid(logits)
loss = nn.BCELoss()(probs, target)
2. CrossEntropyLoss에 softmax 적용¶
# 나쁜 예: softmax 두 번 적용
probs = F.softmax(logits, dim=-1)
loss = nn.CrossEntropyLoss()(probs, target) # 내부에서 log_softmax
# 좋은 예: logits 직접 사용
loss = nn.CrossEntropyLoss()(logits, target)
3. CrossEntropyLoss에 one-hot 타겟 전달¶
# 나쁜 예: one-hot 인코딩된 타겟
target_onehot = F.one_hot(target, num_classes=10).float()
loss = nn.CrossEntropyLoss()(logits, target_onehot) # 에러!
# 좋은 예: 클래스 인덱스 사용
loss = nn.CrossEntropyLoss()(logits, target) # target: [0, 2, 1, ...]
4. 회귀에 분류 손실 사용 (또는 그 반대)¶
# 나쁜 예: 연속 타겟에 CrossEntropy
loss = nn.CrossEntropyLoss()(output, continuous_target)
# 좋은 예: 문제 유형에 맞는 손실
# 회귀: MSE, MAE, Huber
# 분류: CrossEntropy, BCE
5. 클래스 불균형 무시¶
# 나쁜 예: 불균형 데이터에 기본 CrossEntropy
loss = nn.CrossEntropyLoss()(output, target)
# 좋은 예: 클래스 가중치 또는 Focal Loss
class_weights = torch.tensor([1.0, 10.0]) # 소수 클래스에 높은 가중치
loss = nn.CrossEntropyLoss(weight=class_weights)(output, target)
# 또는 Focal Loss 사용
6. 수치 불안정성 무시¶
# 나쁜 예: log(0) 가능성
loss = -torch.log(probs) # probs가 0이면 inf
# 좋은 예: 클리핑 또는 내장 함수 사용
loss = -torch.log(probs.clamp(min=1e-7))
# 또는
loss = F.cross_entropy(logits, target) # 내부적으로 안전하게 처리
7. 손실 reduction 모드 무시¶
# reduction 옵션: 'none', 'mean', 'sum'
# 기본값: 'mean'
# 샘플별 가중치 적용 시
loss_per_sample = nn.CrossEntropyLoss(reduction='none')(output, target)
weighted_loss = (loss_per_sample * sample_weights).mean()
# 배치 크기가 다를 때 sum 사용 주의
# mean: 배치 크기 무관하게 비교 가능
# sum: 배치 크기에 비례