Loss vs Metric

머신러닝에서는 훈련을 통해 목표를 잘 달성했는지를 나타내는 값을 잡고, 그 값을 기준으로 훈련을 시행합니다. 하나의 실험이더라도 이런 값은 여러 가지가 있을 수 있습니다. 이 중 학습을 통해 직접적으로 줄이고자 하는 값을 손실(loss), 에러(error), 혹은 코스트(cost)라고 합니다. 한편, 학습을 통해 목표를 얼마나 잘(못) 달성했는지를 나타내는 값을 척도(metric)라고 합니다. 머신러닝의 최종 목표는 척도로 달성률을 표시하지만, 직접 척도를 낮추도록 훈련하는 것은 여러 가지 이유로 힘들기 때문에 손실을 줄이는 방향으로 훈련합니다. 손실과 에러 모두 손실 함수(loss function), 에러 함수(error function)이라고 이야기하기도 합니다.

손실은 다양한 방법으로 구할 수 있습니다. 지도학습(supervised learning)의 경우 데이터를 넣어서 얻은 결과물(model output)과 실제 얻어야 할 결과물(true output, 혹은 label)을 비교해 손실을 계산하곤 합니다. 손실 또한 미니 배치(mini-batch)에 대한 합 혹은 평균으로 구할 수 있습니다. 보통 미니 배치의 크기에 상관없이 훈련하기 위해 평균을 사용합니다. 간략한 표시를 위해서 아래 손실 함수들의 식에는 더하고 갯수로 나누는 과정을 뺐습니다.

1-hot encoding

One-hot 인코딩은 주로 분류(classification) 문제에서 정답(label)을 나타내기 위해 사용합니다. 지도학습의 결과물은 가능한 클래스들에 해당될 확률로 나오게 되는데, 이 확률 벡터와 차원을 맞춰주기 위해 주로 사용합니다. 예를 들어, A, B, C, D의 4개 클래스가 있고 이 중 어느 것인지 학습하는 상황을 생각해 보겠습니다. 실제 답이 C라고 하면, 인코딩된 답은 [0, 0, 1, 0]이 됩니다. 한편, 지금 모델의 출력은 [0.1 0.1 0.6 0.2] 일 수 있습니다. 차원을 맞춰야 TF에서 제대로 수행될 수 있습니다.

가능한 클래스의 수가 10개~100개 정도로 별로 없을 때는 미리 one-hot 인코딩한 데이터를 들고 있는 것이 번거롭지 않지만, 클래스가 수백 ~ 수천 개가 되는 경우 정답을 미리 인코딩하면 메모리가 많이 부족해 질 수 있습니다. 이 경우 그냥 정수로만 정답을 저장하고 있다가, 미니 배치를 그래프에 넣기 직전에 잠깐 인코딩하는 방법으로 구현합니다.

def to_onehot(y, num_class):
   num_data = y.shape[0]  # len(y)
    one_hot = np.zeros((num_data, num_class))
    one_hot[np.arange(num_data), y] = 1
    # assign 1 to [0,2], [1,7], [2,3], [3,6], ...
    return one_hot

Various Losses

여기서는 3가지 손실 함수를 다룹니다.

Square Loss

제곱차 손실(square loss)은 두 벡터간의 유클리드 거리(Euclidian distance)를 측정합니다. 두 벡터가 가까워질수록 제곱차 손실은 줄어듭니다. 제곱차 손실은 \(y\)에 대한 볼록함수이기 때문에 손실 함수로 사용하기 좋습니다. 하지만 훈련 속도가 느리고 성능이 다른 손실 함수들보다 많이 떨어져 지금은 잘 사용하지 않습니다. 평균이라는 점 때문에 평균 제곱차 손실(mean-squared error, MSE)이라고 하기도 합니다.

def square_loss(y, t):
    return 0.5 * tf.reduce_mean(tf.square(y - t))

Entropy Loss

정보이론(information theory)에서 만들어진 엔트로피 기반 손실 함수들은 기본적으로 Kullback-Leibler 발산(KL divergence)에 기반을 두고 있습니다. KL은 두 확률분포 사이의 거리(distance)를 측정합니다. 수학적 의미에서의 거리는 아니지만, 거리와 유사하게 생각할 수 있습니다. 즉, 두 확률분포가 얼마나 유사한지를 나타내 줍니다. 이산확률변수(discrete random variable)의 경우에 대한 식은 아래와 같습니다. 여기서 \(p, q\)는 확률분포를 나타내기 때문에 모든 원소가 0 이상 1 이하의 값이며 총 합은 1입니다.

위 식에서 \(p, q\)가 완전히 동일하다면 \(D = 0\)이 됩니다. 즉, 우리는 정답의 확률분포와 예측한 확률분포가 같도록, KL이 0이 되도록 훈련해야 합니다. KL은 \(p,q\)를 바꾸면 값이 달라져 대칭성(symmetry)을 만족하지 못하기 때문에, 실제로 손실 함수로 사용할 때는 정답을 두 인자(argument) 중 어디에 넣어야 하는지 잘 확인해야 합니다. \(p\)가 정답입니다!

머신러닝에선 \(p\)를 정답 확률분포로, \(q\)를 모델이 예측한 확률분포로 사용합니다. 엔트로피 손실 함수들의 경우, 수학적으로 안정하지(numerical stability) 않기 때문에 약간의 손질이 들어가야 합니다. \(q\)가 0이 될 경우 분모에 0이 들어가게 되는데, 보통은 softmax의 결과물이기 때문에 0은 아닙니다. 그래도 엄청 많은 클래스가 사용되면 거의 0에 가까워지고, 라이브러리에 따라서는 빠른 계산을 위해 아예 0으로 만들어버리기도 합니다. 안정성을 위해서는 미리 만들어진 함수(built-in function)을 사용하는 것이 좋습니다.

  • \(\log(x)\) 안에는 늘 아주 작은 \(\epsilon=10^{-8}\) 을 더해 주자.
  • 뭔가를 나눌 때는 늘 분모에 아주 작은 \(\epsilon=10^{-8}\) 을 더해 주자.
  • \(\sqrt{x}\) 안에도 늘 아주 작은 \(\epsilon=10^{-8}\) 을 더해 주자.

만약 훈련 중에 NaN, Inf 같은 값이 나온다면 높은 확률로 손실 함수 때문입니다. 위의 \(\epsilon\) 관련 항목만 기억해도 훨씬 안정적인 훈련을 할 수 있습니다!

KL 발산 또한 \(q\)에 대한 볼록 함수이기 때문에 SGD 알고리즘을 적용하기 좋습니다. 위 식에서 \(p\)는 외부에서 주어지는 고정된 값이라는 것을 생각하면, KL 발산을 줄인다는 것은 다음과 같이 바꿀 수 있습니다.

가장 마지막 식을 교차 엔트로피(cross entropy)라고 하며, 요즘 대부분의 손실함수로는 이 값을 사용합니다.

교차 엔트로피는 음수 로그 가능도(negative log-likelihood)라고도 하며, 가능도(likelihood)에서 시작해도 같은 식을 얻을 수 있습니다.

Binary Cross Entropy

\(y\)가 0 혹은 1만 되는 경우(즉, 이진 분류 문제인 경우) 위 교차 엔트로피 식은 다음과 같이 바꿀 수 있습니다.

def binary_crossentropy(y, t):
    return -tf.reduce_mean(t * tf.log(y + 1e-7) - (1-t) * tf.log(1 - y + 1e-7))

Categorical Cross Entropy

\(y\)가 여러 클래스를 갖는 경우, 위 교차 엔트로피 식은 다음과 같습니다.

def square_loss(y, t):
    return -tf.reduce_mean(t * log(y + 1e-7))

Various Metrics

여기서는 1가지 손실 함수만을 다룹니다. 훈련을 계속하다 보면 손실값이 줄어들면 척도값도 줄어들지만, 정말 수렴할 때 즈음에 이르러서는 손실값이 줄어도 척도값이 줄지 않기도 합니다. 둘의 계산식이 다르다 보니 발생하는 문제입니다. 그래서, 훈련 막바지에는 검증 데이터(validation data)에서 손실값이 줄어드는 것을 모니터링하지 않고 척도값이 줄어드는(늘어나는) 것을 모니터링하기도 합니다.

Accuracy

정확도(accuracy)는 1 - 에러율(error rate)입니다. 분류 문제에서는 가장 확률이 높게 예측한 클래스가 실제 정답이면 맞은 것, 아니면 틀린 것으로 생각합니다. Top-k 정확도의 경우 확률이 높은 순서대로 k 개 안에 정답이 있으면 그 데이터에 대해서는 맞은 것으로 세는 방식입니다. 일반적인 정확도는 Top-1이라고 생각할 수도 있습니다.

def categorical_accuracy(t, y):
    return tf.reduce_mean(tf.equal(tf.argmax(t, axis=-1), tf.argmax(y, axis=-1)))