Output Data Normalization


서론

로봇의 여러 물리량을 하나의 신경망에서 예측하려고 한다.

정답 데이터가 모두 존재하며, 각 출력에 대해 MSE loss를 사용한다.

데이터는 크게 세 종류이다.

  • 관절 action: 총 12차원이며 각 관절은 서로 다른 평균과 분산을 가진다. 정책은 각 상태에서 Gaussian distribution으로 action을 sampling한 값이다.
  • 로봇의 선형 속도: 3차원 \((v_x,v_y,v_z)\) 이며 단위는 m/s이다.
  • 막대 길이: 1차원이며 단위는 m이고, 값의 범위는 [0.33 ~ 0.45m]이며 균등분포이다.

신경망이 이 세 종류의 값을 동시에 예측하고 각각에 대해

\[L = \lambda_a L_{\text{action}} + \lambda_v L_{\text{velocity}} + \lambda_l L_{\text{length}} \]

MSE loss를 계산하고 이를 바탕으로 backpropagation을 진행한다.

각 loss component들은 scaling이 달라 어느 한쪽의 loss가 크면 gradient udpate를 독식할 수 있다.

문제정의

각 데이터의 단위와 numerical scale이 다름으로 서로 다른 물리량의 MSE numerical scale을 맞춰야 학습 불균형이 일어나지 않는다.

Velocity

축별 Normalize

예를 들어

실제 데이터 통계가 다음과 같다고 하자.

각 x, y, z의 속도 오차(예측 - 정답)을 e라고 한다면 축별 normalization를 적용한 MSE는 다음과 같다.

\[L_v = \frac{1}{3} \left[ \left( \frac{e_x}{\sigma_x} \right)^2 + \left( \frac{e_y}{\sigma_y} \right)^2 + \left( \frac{e_z}{\sigma_z} \right)^2 \right]\]

다시 쓰면

\[L_v = \frac{1}{3} \left[ \frac{1}{\sigma_x^2} e_x^2 + \frac{1}{\sigma_y^2} e_y^2 + \frac{1}{\sigma_z^2} e_z^2 \right]\]

이고 사실상 서로 다른 오차 e에 loss weight를 곱하여 각 축에 주는 것과 같다.

\[w_x = \frac{1}{\sigma_x^2}, \quad w_y = \frac{1}{\sigma_y^2}, \quad w_z = \frac{1}{\sigma_z^2}\]

예를들어

\[ \sigma_x = 0.27,\quad \sigma_y = 0.15,\quad \sigma_z = 0.07 \]

이라면 z와 x의 상대적인 loss weight는

\[\frac{w_z}{w_x} = \frac{1/0.07^2}{1/0.27^2}\approx 14.9\]

이다. 즉 같은 0.1m/s 오차가 발생해도 z오차가 x오차보다 약 15배 강하게 학습에 작용한다.

이게 잘 못된 것은 아니다. 다만 우리의 목표와 맞지 않을 뿐이다.

“z의 분산이 x보다 작으니까 z는 원래 거의 움직이지 않으므로 z에서 0.1 m/s 틀리는 것은 x에서 0.1 m/s 틀리는 것보다 훨씬 심각하다.”

라는 의미를 부여하자고 했다면 축별 std가 맞는 것이다.

그런데 우리의 목적은

\[L = L_{\text{action}} + L_{\text{length}} + L_{\text{velocity}}\]

처럼 여러 종류의 출력 MSE를 동시에 사용할 때, 단위와 numerical scale 차이 때문에 어느 head가 loss를 독점하지 않도록 맞추려는 것이다.

또 하나 중요한 이유는 현재 우리는 로봇이 평지에서 보행함으로 속도 z의 분산 \(\sigma_z\)가 작을 수 있다.

왜냐하면 x, y방향을 많이하지만 위 아래로는 별로 움직이지 않기 때문에 굳이 속도 z에 큰 가중치를 부여할 필요가 없다.

이러한 이유로 축별 std를 쓸 필요가 없다.

Pooled std

따라서 velocity 각 축에 대해서 중요도를 바꾸기 보다는 x, y, z는 하나의 어떤 task로 보고 velocity전체에 하나의 공통 scaleㄴ을 적용하는 pooled std를 사용하는것이 목적에 더 직접적으로 맞는다.

pooled std는

\[\sigma_p = \sqrt{\frac{\sigma_x^2 + \sigma_y^2 + \sigma_z^2}{3}}\]

이다. 각 축의 분산을 구하고 평균낸다음에 루트를 씌운다.

그래서 세 축의 분산을 합쳐

\[\sigma_{pool} = \sqrt{\frac{0.27^2 + 0.15^2 + 0.07^2}{3}} \]

라는 공통 std하나를 만든다.

이것을 일반화해서 보면

\[\begin{align*}\text{MSE}_{\text{Norm}} &= \frac{1}{3} \left[ \left( \frac{e_x}{\sigma_p} \right)^2 + \left( \frac{e_y}{\sigma_p} \right)^2 + \left( \frac{e_z}{\sigma_p} \right)^2 \right] \\[1.5em]&= \frac{1}{3\sigma_p^2} \left( e_x^2 + e_y^2 + e_z^2 \right) = \frac{\text{MSE}_{\text{raw}}}{\sigma_p^2}\end{align*} \]

세 축 동일하게

\[\frac{1}{\sigma_p^2} \]

라는 weight를 받게된다.

Baseline MSE를 1로 맞추자

우리가 만들고자하는 모델의 MSE오차가 가장 안 좋은 성능일 때(평균만 예측했을 때) 대비 얼마나 줄었는지 알고자 한다고하면 수식은 아래와 같다.

\[\frac{\text{MSE}}{\sigma^2_p}\]

분모가 왜 저렇게 되는지 보면

모델은 평균만 예측해야 함으로

\[\hat{y}_x = \bar{y}_x, \quad \hat{y}_y = \bar{y}_y, \quad \hat{y}_z = \bar{y}_z\]

이고 오차는

\[e_x = y_x - \bar{y}_x, \quad e_y = y_y - \bar{y}_y, \quad e_z = y_z - \bar{y}_z\]

각 축에서 오차의 제곱 평균은 분산임으로

\[E[e_x^2] = \sigma_x^2, \quad E[e_y^2] = \sigma_y^2, \quad E[e_z^2] = \sigma_z^2 \]

이다.

따라서 평균만 예측하는 MSE baseline은

\[\text{MSE}_{\text{baseline}} = \frac{1}{3}(E[e^2_x] + E[e^2_y] + E[e^2_z]) \\[1.5em] \frac{1}{3}(\sigma_x^2 + \sigma_y^2 + \sigma_z^2) = \sigma_p^2\]

이 된다.

Pooled std로 Normalize한 MSE는

\[\text{MSE}_{\text{Norm}} = \frac{\text{우리가 만든 모델의 오차}}{\text{평균만 찍는 모델의 오차}} \frac{\text{MSE}}{\sigma_p^2}\]

만약 우리가 열심히 학습시킨 모델의 성능이 매우 안 좋아서, 그냥 평균값을 찍어내는 수준과 오차가 똑같다면 분자와 분모의 값이 같아지게 된다.

\[\text{MSE}_{\text{Norm}} = \frac{\sigma_p^2}{\sigma_p^2} = 1\]

따라서 \(\text{MSE}_{\text{Norm}}\) 수치의 결과는 다음과 같이 해석할 수 있다.

  • \(\text{MSE}_{\text{Norm}} = 1\): 모델이 데이터에서 아무런 패턴도 학습하지 못하고 평균값만 내뱉는 상태
  • ‭\(\text{MSE}_{\text{Norm}} < 1\): 평균을 찍는 것보다 오차가 작으므로, 값이 0에 가까워질수록 모델이 정답을 정확히 예측하고 있다는 뜻. (예: 0.1이라면 기본 오차 대비 10% 수준으로 오차를 줄였다는 의미)
  • ‭\(\text{MSE}_{\text{Norm}} > 1\): 모델이 무언가 단단히 잘못 학습하여, 차라리 평균을 찍는 것보다도 엉망으로 오차를 내고 있다는 상태

따라서 우리는 1이 가장 학습이 되지 않았다는 기준을 줌으로써 MSE의 값의 크기에 의미를 부여하게 된 것이다.

Machine Learning 관점

그렇다면 이 가정은 모델이 학습을 시작할 때부터 정답 데이터의 평균값을 예측하고 있어야 성립한다고 생각할 수 있다.

예를 들어 MLP 기반 회귀 모델을 사용한다고 하면, 초기 가중치는 일반적으로 0에 가까운 작은 값으로 초기화되기 때문에 학습 초기에 모델이 정답 데이터의 평균을 바로 예측하지는 않는다.

그럼으로 학습시 모델이 가장 먼저 배우는 것은 데이터의 복잡한 패턴이 아니라 정답 데이터의 평균(bias) 위치를 먼저 배운다.

가중치가 0에 가깝게 초기화되었더라도, 몇 번의 에폭(Epoch)만 지나면 출력 레이어의 편향(Bias) 파라미터가 빠르게 업데이트되면서 정답의 평균값 근처로 출력을 옮겨놓는다.

따라서 학습 극초반에 \(\text{MSE}_{\text{Norm}}\)은 1보다 훨씬 큰 값에서 시작하지만, 순식간에 1 근처로 뚝 떨어지게 된다

모델이 평균 위치를 대략 파악해서 오차가 1 근처가 되고 나면, 주어진 입력 데이터(피처)들의 패턴을 분석하여 정답 값들이 왜 평균과 다르게 제각각인지 그 원인을 수치적으로 찾아내고 오차를 줄여나간다. 이때부터 오차가 1 밑으로 떨어지기 시작한다.

분산으로 나누어 정규화해 두었기 때문에, '속도'든 '길이'든 이 시점부터는 둘 다 1에서 시작해서 0을 향해가는 동일한 스케일의 학습을 하게 된다. 어느 한쪽의 로스(Loss)가 압도적으로 커서 기울기(Gradient)를 독식하는 현상이 방지되는 것이다.

Target Normalization

추가적으로 여러 물리량을 예측할 때는 Loss 함수를 Normalization수정하는 것에 그치지 않고, 아예 정답 데이터(Target) 자체를 평균이 0이고 분산이 1이 되도록 스케일링(Standardization)을 해버린다.

이렇게 하면 정답의 평균이 0이 되므로, 가중치가 0에 가깝게 초기화된 MLP가 처음부터 정확히 평균(0)을 예측하는 상태에서 학습을 시작하게 된다.

따라서 위 Normalization "가정이 성립하려면 평균부터 시작해야 한다"를 Scaling으로부터 어느정도 가정을 성립하게 만들어준다.

Stick length

속도에 대해서는 3개의 축을 하나의 task로 보고 pooled std를 이용하여

\[\text{MSE}_\text{Norm} = 1\]

이 되도록 numerical scale을 맞췄다.

막대 길이는 1차원 scalar 데이터이며, ‭\([0.33\text{ m}, 0.45\text{ m}]\)‬‭‬‭‬‭‬ ‭‬‭‬‭‬ 범위의 균등분포(Uniform Distribution)를 갖는다.

갖는다.

구하고자 하는 것은 막대 길이 예측 loss 역시 평균만 찍을 때 \(\text{MSE}_{\text{Norm}} = 1\)‬‭‬이 되도록 만들어 다른 물리량과 동등한 출발선을 갖추는 것이다.

막대 길이의 분산 계산

연속형 균등분포 \(U(a, b)\)의 기댓값(평균)과 분산은 다음과 같다.

\[\mu_l = \frac{a + b}{2}, \quad \sigma_l^2 = \frac{(b - a)^2}{12}‭‬\]

문제의 주어진 값인 \(a = 0.33‬‭‬, ‭b = 0.45\)를 대입하면,

\[\mu_l = \frac{0.33 + 0.45}{2} = 0.39\text{ m}\]
\[\sigma_l^2 = \frac{(0.45 - 0.33)^2}{12} = \frac{0.0144}{12} = 0.0012\text{ m}^2\]
\[\sigma_l = \sqrt{0.0012} \approx 0.0346\text{ m} \]

Raw MSE의 문제점

만약 정규화 없이 Raw MSE를 그대로 손실함수로 사용하면, 모델이 아무것도 배우지 않고 항상 평균값 0.39만 출력한 경우 손실값은 아래와 같다.

\[\text{MSE}_{\text{baseline}} = \sigma^2_{\text{l}}=0.0012\]

‬‭‬‭‬‭‬‭‬속도(Velocity)나 관절 액션(Action)의 MSE baseline가 상대적으로 큰 수치(예: ‭\(0.01 \sim 0.1\)‬‭‬ 이상)를 가질 때, 막대 길이의 손실값은 ‭\(0.0012\)‬ 수준에 불과하므로 역전파 시 기울기(Gradient)가 거의 전파되지 않는다. 결과적으로 막대 길이를 예측하는 Head는 네트워크 전체 학습에서 외면받게 된다.

Normalization 적용

\[\mathrm{MSE}_{\mathrm{Norm}}=\frac{(y_i-\hat{y}_i)^2}{\sigma_l^2}=\frac{\mathrm{MSE}_{\mathrm{raw}}}{0.0012}\]

이렇게 설정하면 모델이 평균값(‭\(0.39\text{m}\)‬‭‬)만 예측할 때 정확히 ‭\(\text{MSE}_{\text{Norm}} = 1\)‬‭‬이 된다.

Action

관절 액션(Action)은 총 12차원으로 구성되어 있으며, 각 관절은 서로 다른 평균과 분산을 가진다. 속도(Velocity)에서 3개의 축을 묶어 하나의 Task로 처리했듯이, 12개의 관절 액션 역시 로봇의 '움직임 제어'라는 하나의 통합된 Task로 간주하여 공통된 스케일을 적용한다.

Pooled std

각 관절 ‭\(j\)‬ (‭\(j = 1, 2, \dots, 12\)‬‭‬‭‬ ‭‬‭‬ ‭‬‭‬ ‭‬)의 분산을 ‭\(\sigma_j^2\)‬라고 할 때, 12차원 액션 전체를 대표하는 통합 분산(Pooled variance) ‭\(\sigma_{p}^2\)‬는 모든 관절 분산의 평균으로 계산한다.

\[\sigma_{p}^2 = \frac{1}{12} \sum_{j=1}^{12} \sigma_j^2\]

속도 모델링에서 논의했던 것과 동일한 이유로, 각 관절마다 개별적인 \(\sigma_j^2\)‬로 나누어 가중치를 다르게 주면 상대적으로 움직임(분산)이 적은 특정 관절의 미세한 오차가 전체 Gradient update를 독식하는 문제가 발생할 수 있다. 이를 방지하기 위해 전체 12개 관절에 공통된 분산 \(\sigma_{p}^2\)를 일괄 적용한다.

Baseline MSE 정규화

정규화 전의 Action Raw MSE는 12개 차원에 대한 오차 제곱의 평균이다.

\[\text{MSE}_{\text{raw}} = \frac{1}{12} \sum_{j=1}^{12} (y_j - \hat{y}_j)^2\]

학습 극초반, 모델이 데이터의 복잡한 패턴을 파악하지 못하고 오직 각 관절의 평균값 \(\bar{y}_j\)만 출력하는 상황을 가정해보자. 이때의 예측 오차 제곱의 기댓값은 각 관절의 분산 \(\sigma_j^2\)‬가 되므로, 평균만 예측하는 Baseline 모델의 오차는 다음과 같습니다.

\[\text{MSE}_{\text{baseline}} = \frac{1}{12} \sum_{j=1}^{12} E[(y_j - \bar{y}_j)^2] = \frac{1}{12} \sum_{j=1}^{12} \sigma_j^2 = \sigma_{p}^2\]

Baseline이 1이 되도록 맞추기 위해, MSE를 Pooled variance로 나누어 정규화한다.

\[\text{MSE}_{\text{Norm}} = \frac{\text{MSE}}{\sigma_{p}^2} \]

이렇게 설정하면 모델이 단순히 평균값만 예측할 때 \(\text{MSE}_{\text{Norm}} = 1\)‬‭‬이 성립한다.

이로써 12차원의 Action, 3차원의 Velocity, 1차원의 Stick length 모두 모델이 평균을 예측하는 시점에서 Loss 값이 1이 되는 완벽하게 동일한 출발선을 갖게 된다. 물리량의 차원 수나 원본 데이터의 단위(Scale) 차이에 구애받지 않고, \(\lambda_a, \lambda_v, \lambda_l\) ‭가중치 조절만으로 세 가지 Task의 학습 균형을 직관적이고 안정적으로 제어할 수 있다.

MSE는 정답을 학습하는 걸까?

출력 데이터에 정규화(Normalization)를 적용할 때 흔히 할 수 있는 오해가 있다. 정규화 때문에 원래 정답값이 바뀌면, 모델이 엉뚱한 값을 정답으로 착각하고 학습하는 것은 아닐까? 하는 의문이다.

예를 들어, 어떤 입력 \(x_1\)에 대한 모델의 실제(Raw) 예측값이 0.35이고, 실제 정답(Ground Truth, GT)이 0.5라고 가정해 보자.

그런데 모델 학습을 위해 출력값을 정규화(Normalize)했더니, 예측값은 0.9로, 정답은 1.0으로 스케일이 변환되었다고 해보자. 이 상황만 보면 "우리는 ‭‬\(x_1\)을 넣었을 때 0.5가 나오길 바라는데, 모델은 1.0을 정답으로 알고 학습해버리는 게 아닐까?" 라고 생각하기 쉽다.

그러나 그렇지 않다. 그 이유는 모델이 학습에 사용하는 MSE(Mean Squared Error) Loss의 본질이 정답의 '절대적인 수치'를 외우는 것이 아니라, 예측값과 정답 사이의 '잔차(Residual, 오차)'를 줄이는 데 있기 때문이다.

우리가 출력 정규화를 하는 목적은 각 출력 오차가 Loss에 미치는 수치적 스케일(Numerical Scale)을 일정하게 맞춰주어 Gradient Descent에서 안정적이고 균일한 학습이 이루어지게 하기 위함이다.

따라서 정규화된 상태에서 모델이 학습을 거듭하여 예측값을 정답(1.0)에 가깝게 맞추게 되면(즉, 정규화된 잔차가 0에 수렴하면), 이를 역정규화(Denormalization)하여 원래 스케일로 되돌렸을 때의 잔차 역시 자연스럽게 0에 가까워진다.

평균/분산 Normalize를 쓰지 못하는 이유

1. 공간상의 벡터 방향 왜곡 (기하학적 비틀림)

x, y, z‬는 3차원 공간에서 하나의 Vector를 이룬다. 각 축을 자기 평균을 빼고 분산으로 나누는 순간 공간상에서의 벡터의 원래 방향 정보를 왜곡시킨다.

2. 수치적 폭발 방지 (Division by Zero)

특정 방향으로의 움직임이 거의 없는 데이터(예: 평지에서만 달리는 차라 ‭z‬축 변동이 거의 없는 경우, ‭\((\sigma_x \approx 0)\)가 들어오면, \(\sigma_z\)‬로 나눌 때 값이 무한대로 폭발하여 학습이 불안정해 진다.

3. 동일 물리량의 물리적 에너지 보존

x, y, z‬‭‬는 모두 '속도(m/s)'라는 동일한 단위를 가진다. 축별로 따로 정규화해 버리면, 실제 공간에서 크게 요동치는 축과 거의 정지해 있는 축의 오차 비중이 강제로 똑같아진다.