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)