선형 회귀는 가장 단순하고 유용한 모델 중 하나다. 입력 $x$로부터 목표값 $y$를 다음과 같은 직선으로 예측한다.
$$\hat y = mx+b$$ 여기서 $m$은 직선의 기울기, $b$는 절편이며, 이들이 학습을 통해 결정해야 할 모델의 파라미터(parameter)이다. 예측값과 관측값의 차이인 residual은 다음과 같이 정의한다.
$$r= y - \hat y$$
이제 주어진 residual이 모델의 예측 오차를 얼마나 크게 나타내는지를 정량화할 필요가 있다. 이를 위해 손실함수(loss function)를 정의한다. 손실함수는 하나의 데이터에 대해 예측이 관측값에서 얼마나 벗어났는지를 수치로 나타낸다. 여러 데이터에 대한 손실을 종합하면 모델 전체의 예측 성능을 나타내는 비용함수(cost function)를 정의할 수 있다. 모델을 학습한다는 것은 결국 이 비용함수의 값을 가능한 한 작게 만드는 파라미터 $m$과 $b$를 찾는 과정이라고 할 수 있다.
이를 위해 널리 사용되는 방법이 경사하강법(gradient descent method)이다. 파라미터 공간에서 비용함수의 gradient를 구하면 비용함수가 가장 빠르게 증가하는 방향을 알 수 있다. 따라서 그 반대 방향으로 파라미터를 조금씩 이동시키면 비용함수를 점차 감소시킬 수 있다. 이 과정을 반복함으로써 더 좋은 예측을 하는 $m$과 $b$를 찾아간다.
선형 회귀에서 가장 널리 사용되는 비용함수 중 하나가 평균제곱오차(Mean Squared Error, MSE)이다. $N$개의 데이터에 대해 residual을
$$r_i=y_i-\hat y_i$$라고 하면 MSE 비용함수는 각각의 residual을 제곱한 평균인
$$J=\frac{1}{N}\sum_{i}r_i^2$$로 정의된다. Residual을 단순히 더하지 않고 제곱하는 데에는 두 가지 중요한 이유가 있다. 우선 양의 residual과 음의 residual이 서로 상쇄되는 것을 막을 수 있다. 또한 residual의 크기가 커질수록 훨씬 큰 손실을 부여하므로, 크게 잘못된 예측을 적극적으로 교정하도록 모델을 학습시킬 수 있다.
MSE는 미분 가능한 매끄러운 함수이기 때문에 gradient descent를 적용하기에도 매우 편리하다. 제곱 손실을 residual에 대한 미분은$$\frac{d}{dr}r^2=2r$$이다. 따라서 residual의 크기가 클수록 해당 데이터가 gradient에 미치는 영향도 커진다.
선형 회귀 $\hat{y}_i = mx_i + b$에 대해서는 $r_i=y_i-(mx_i+b)$이므로 비용함수는
$$J(m,b)=\frac{1}{N}\sum_{i}\left[y_i-(mx_i+b)\right]^2$$이므로 $m$과 $b$에 대한 gradient는
$$\nabla J = \left(-\frac{2}{N} \sum_i x_i r_i, -\frac{2}{N}\sum_{i}r_i \right)$$가 된다. Gradient descent에서는 이 gradient의 반대 방향으로
$$m \leftarrow m-\eta\frac{\partial J}{\partial m},\qquad b\leftarrow b-\eta\frac{\partial J}{\partial b}$$와 같이 파라미터를 조금씩 변화시킨다. 여기서 $\eta$는 한 번에 얼마나 이동할지를 결정하는 learning rate이다.
MSE가 큰 오차를 강하게 교정한다는 성질은 일반적으로는 장점이지만, 데이터에 이상치(outlier)가 존재하면 문제가 될 수 있다. 예를 들어 대부분의 데이터에서 residual의 크기가 $1$ 정도인데 하나의 이상치에서 residual이 $10$이라고 하자. MSE에서 각각의 손실은 $1^2=1$, $10^2=100$이 된다. 따라서 단 하나의 이상치가 정상적인 데이터 하나보다 $100$배나 큰 손실을 만들어낸다.
Gradient의 관점에서 보면 문제가 더욱 분명해진다. 제곱 손실의 gradient는 $L'(r)=2r$이므로 residual이 커질수록 gradient의 크기도 제한 없이 증가한다. 결국 소수의 매우 큰 residual이 전체 gradient를 지배할 수 있다. 그 결과 gradient descent는 대부분의 데이터를 잘 설명하는 방향보다 몇 개의 이상치의 오차를 줄이는 방향으로 파라미터가 지나치게 움직일 수 있다. 즉, MSE를 이용한 line fitting에서는 이상치 때문에 회귀 직선 자체가 이상치 쪽으로 끌려갈 수 있다.
이 문제를 줄이는 가장 단순한 방법은 residual을 제곱하지 않고 절댓값을 사용하는 것이다.
$$L(r)=|r|$$이를 절대오차(absolute error)라고 한다. Residual이 $1$이면 손실은 $1$, residual이 $10$이면 손실은 $10$이다. MSE처럼 $1$과 $100$으로 차이가 증폭되지 않는다. 더 중요한 차이는 gradient에 있다. $r \neq 0$에서 $L' (r) = \text{sign}(r)$이므로 residual이 아무리 커져도 gradient의 크기는 $1$을 넘지 않는다. 따라서 하나의 outlier가 전체 gradient를 압도하는 현상을 크게 줄일 수 있다.
하지만 절대오차에도 문제가 있다. $|r|$은 $r=0$에서 뾰족한 모양을 가지므로 그 지점에서 미분 가능하지 않다. 또한 $r$이 매우 작아져도 gradient의 크기가 계속 $1$로 유지되기 때문에 최소점 근처에서 MSE처럼 부드럽게 감소하지 않는다.
따라서 다음 두 성질을 동시에 가지는 손실함수를 고려하는 것이 바람직하다.
- residual이 작을 때는 MSE처럼 제곱함수를 사용하여 최소점 근처에서 부드럽게 학습한다.
- residual이 클 때는 절댓값 함수처럼 선형적으로 증가시켜 이상치가 gradient를 지나치게 지배하지 못하게 한다.
이 두 가지 성질을 결합한 것이 Huber loss이다. Huber loss는 residual의 크기가 어떤 기준값 $\delta$보다 작을 때는 제곱함수를 사용하고, 그보다 클 때는 절댓값에 비례하는 선형함수 전환한다.
$$ L_\delta(r) = \begin{cases} \frac{1}{2} r^2 & |r| \le \delta \\ \delta \left( |r|- \frac{1}{2}\delta \right) & |r| > \delta \end{cases} $$여기서 $\delta$는 제곱 영역과 선형 영역을 나누는 기준값이다. 앞에서 사용한 제곱 손실 $r^2$과 비교하면 작은 residual 영역에서 $1/2$이라는 상수배의 차이가 있지만, 이는 gradient의 전체 크기만 바꿀 뿐 제곱 손실의 본질적인 성질에는 영향을 주지 않는다.
Huber loss를 residual에 대해 미분하면
$$\frac{dL_\delta}{dr} = \begin{cases} -\delta,&r<-\delta,\\ r,&|r|\le\delta,\\ \delta,&r>\delta \end{cases}$$가 된다. 작은 residual 영역에서는 미분값이 $r$에 비례하므로 예측값이 실제값에 가까워질수록 자연스럽게 0으로 감소한다. 반면 $|r|>\delta$가 되면 미분값의 크기는 $\delta$로 제한된다. 따라서 residual이 아무리 커지더라도 하나의 outlier가 gradient에 미치는 영향은 일정 수준 이상으로 증가하지 않는다.

이러한 특성은 Huber loss의 주요 장점 중 하나이다. MSE에서는 residual이 커질수록 gradient도 함께 증가하므로, 소수의 큰 outlier가 전체 gradient descent의 방향에 과도한 영향을 줄 수 있다. 반면 Huber loss에서는 residual이 일정 수준을 넘으면 gradient의 크기가 제한되므로, 큰 이상치가 최적화 과정을 지나치게 좌우하는 현상을 완화할 수 있다.
Huber loss는 작은 residual 영역에서는 MSE처럼 동작하고, 큰 residual 영역에서는 absolute error와 유사하게 동작하는 비용함수이다. 따라서 작은 오차에 대해서는 MSE의 부드러운 최적화 특성을 활용하면서, 큰 오차에 대해서는 영향력을 제한하여 이상치에 대한 민감도를 낮출 수 있다. 이러한 점에서 Huber loss는 MSE와 absolute error의 장점을 적절히 결합한 비용함수라고 볼 수 있다.
// line fitting code with Huber loss;
std::vector<double> train_huber(const std::vector<double>& xdata,
const std::vector<double>& ydata,
double delta, double learn_rate, int iters) {
double m = 0.0; // initialize slope
double b = 0.0; // initialize intercept
const double tol = 1e-8;
while (iters--) {
double grad_m = 0.0, grad_b = 0.0;
for (int i = xdata.size(); i-->0; ) {
double prediction = m * xdata[i] + b;
double residual = ydata[i] - prediction;
// if residual is within the L2 threshold
double dL_dr;
if (fabs(residual) <= delta) dL_dr = residual;
else dL_dr = (residual > 0) ? delta : -delta;
// chain rule: dr/dm = -x, dr/db = -1
// dL/dm = (dL/dr) * (dr/dm) = -dL_dr * x
grad_m += -dL_dr * xdata[i];
grad_b += -dL_dr;
}
// average_gradient;
grad_m /= xdata.size();
grad_b /= xdata.size();
// gradient descent update
m -= learn_rate * grad_m;
b -= learn_rate * grad_b;
if (sqrt(grad_m * grad_m + grad_b * grad_b) < tol) break;
}
std::vector<double> line(2);
line[0] = m; line[1] = b;
return line;
}

'Image Recognition > Fundamental' 카테고리의 다른 글
| FFT를 이용한 inverse FFT (0) | 2024.07.31 |
|---|---|
| FFT 구현 (0) | 2024.07.31 |
| CLAHE (2) (1) | 2024.06.26 |
| Approximate Distance Transform (0) | 2024.06.02 |
| Graph-based Segmentation (1) | 2024.05.26 |


