콘텐츠로 이동
Data Prep
상세

손실 함수 (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)\]
triplet_loss = nn.TripletMarginLoss(margin=1.0)
loss = triplet_loss(anchor, positive, negative)

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: 배치 크기에 비례

참고 자료