이상치에 덜민감한 L1+L2 = Huber Loss

이상치에 덜민감한 L1+L2 = Huber Loss
Photo by Anne Nygård / Unsplash

Why Huber Loss

  • 주요 모델링 업무 중 하나로 배달시간을 예측하다보면, Long Tail Distribution 형태의 모습을 자주 보게 된다. 이 부분에 대응하기 위해 Doordash도 비슷한 고민을 하는 과정에서 Loss Function을 수정하는 모습을 보여주었는데, 그외 Alternative로서 Huber Loss에 대해서 공부하고 적용해본 기억이 있다. 이에 대해서 정리해본다

Definition

  • Huber Loss는 평균 제곱 오차(MSE)와 평균 절대 오차(MAE)의 장점을 결합한 손실 함수
  • 작은 오차에 대해서는 MSE처럼 동작하고, 큰 오차에 대해서는 MAE처럼 동작하여 이상치에 덜 민감하게 설계되었습니다.
  • Huber Loss는 다음과 같이 정의됨, 여기서 𝑎는 실제 값, 𝑓(𝑥)는 예측 값, 𝛿는 임계값

Motivation

  • 이상치에 대한 민감도 감소
    • MSE는 이상치에 매우 민감하여 큰 오차가 있을 경우 손실 값이 크게 증가
    • 반면, MAE는 이상치에 덜 민감하지만, 작은 오차에 대해서는 미분이 불연속적
    • Huber Loss는 이 두 가지 문제를 모두 해결할 수 있음
  • 안정적인 학습: 작은 오차에 대해서는 MSE처럼 미분이 연속적이고, 큰 오차에 대해서는 MAE처럼 이상치에 민감하지 않음

Pros & Cons

Pros

  • 이상치에 대한 강건성: Huber Loss는 이상치에 덜 민감
  • 연속적 미분 가능: 작은 오차에 대해서는 MSE처럼 동작하여 미분이 연속적이므로 경사 하강법을 사용한 최적화에 유리
  • 하이퍼파라미터 𝛿: 임계값 𝛿를 조정하여 모델의 민감도를 제어할 수 있음(비즈니스 로직으로 활용 가능)

Cons

  • 하이퍼파라미터 선택: 적절한 𝛿 값을 선택하는 것이 중요하며, 이는 데이터셋에 따라 다를 수 있
  • 계산 비용: Huber Loss는 MAE보다 계산 비용이 더 많이 소요

Alternative

  • 평균 제곱 오차(MSE): 이상치에 매우 민감하지만, 작은 오차에 대해서는 좋은 성능을 보입니다.
  • 평균 절대 오차(MAE): 이상치에 덜 민감하지만, 작은 오차에 대해서는 미분이 불연속적
    • 절대값 함수의 미분은 𝑥=0에서 정의되지 않기 때문에, MAE의 경우 실제 값과 예측 값이 같을 때(즉, 오차가 0일 때) 미분이 불연속적임
      • 불연속적이다 → 함수의 특정 점에서 좌측 미분 값과 우측 미분 값이 서로 다르거나, 특정 점에서 미분 값이 정의되지 않는 경우
  • Log-Cosh Loss: Huber Loss와 비슷하지만, 미분이 항상 연속적임 $$ L(y, \hat{y}) = \sum_{i} \log(\cosh(\hat{y}_i - y_i))$$$$cosh(x) = \frac{e^x + e^{-x}}{2}$$

Sample

import torch  
import torch.nn as nn  
import torch.optim as optim  
from sklearn.datasets import make_regression  
from sklearn.model_selection import train_test_split  
import matplotlib.pyplot as plt  
  
# 데이터 생성  
X, y = make_regression(n_samples=1000, n_features=1, noise=0.1)  
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)  
  
# PyTorch 텐서로 변환  
X_train = torch.FloatTensor(X_train)  
X_test = torch.FloatTensor(X_test)  
y_train = torch.FloatTensor(y_train)  
y_test = torch.FloatTensor(y_test)  
  
# 모델 정의  
class SimpleModel(nn.Module):  
    def __init__(self):  
        super(SimpleModel, self).__init__()  
        self.fc1 = nn.Linear(1, 64)  
        self.fc2 = nn.Linear(64, 64)  
        self.fc3 = nn.Linear(64, 1)  
  
    def forward(self, x):  
        x = torch.relu(self.fc1(x))  
        x = torch.relu(self.fc2(x))  
        x = self.fc3(x)  
        return x  
  
model = SimpleModel()  
  
# Huber Loss와 옵티마이저 정의  
delta = 1.0  # Huber Loss의 임계값  
criterion = nn.SmoothL1Loss(beta=delta)  
optimizer = optim.Adam(model.parameters(), lr=0.01)  
  
# 모델 학습  
train_losses = []  
for epoch in range(100):  
    model.train()  
    optimizer.zero_grad()  
    outputs = model(X_train)  
    loss = criterion(outputs.squeeze(), y_train)  
    loss.backward()  
    optimizer.step()  
    train_losses.append(loss.item())  
  
# 학습 곡선 시각화  
plt.plot(train_losses, label='Train Loss')  
plt.legend()  
plt.title('Huber Loss Training Curve')  
plt.xlabel('Epochs')  
plt.ylabel('Loss')  
plt.show()  
  
# 모델 평가  
model.eval()  
with torch.no_grad():  
    outputs = model(X_test)