Multiclass classification with softmax

종속변수 \(Y\)의 class(클래스)가 2개 이상인 분류문제인 다중클래스 분류(또는 다중분류, multiclass classification) 을 살펴보자. 다음과 같은 예를 생각해 볼 수 있다.

  • 강아지, 고양이, 늑대, 여우 이미지 분류 모델
  • 공부 시간과 출석 횟수를 고려한 A,B,C 성적 예측

One vs the rest

앞에서 다루었던 이진분류(binary classification)을 확장하여 다중클래스 분류 문제를 해결하는 one vs the rest를 다뤄보자. 데이터에 \(K\)개의 클래스가 존재할 때 One vs the rest는 각 클래스에 속하는지 아닌지를 이진분류하는 \(K\)개의 독립된 모델을 제작한다. \(i\) 번째 모델은 샘플 데이터가 \(i\) 번째 클래스에 속하는지 아닌지를 판별해주는 모델이다. 즉, 판별하고 싶은 샘플 \(X\)의 결과값을 다음과 같이 나타낼 수 있다.

\[H(X)= (h_1(X), \ldots, h_K(X))\]

여기서 \(h_i\)는 \(X\)가 \(i\) 번째 클래스에 속하는지 아닌지 나타내는 확률이다. 즉, \(h_i\), \((1\leq i \leq K)\) 중 가장 큰 값이 있는 index가 \(X\)의 클래스를 결정한다. 공부 시간과 출석을 독립변수 \(X= (x_1,x_2)\)로 하여 성적(letter grade)을 예측하는 간단한 예제를 살펴보자.

공부 시간 출석 성적
10 5 A
9 5 A
3 2 B
2 4 B
11 2 C

One vs the rest를 이용하면 다음과 같은 3개의 독립된 모델을 제작한다.

  1. A와 나머지 그룹을 분리하는 이진분류 모델 \(H_A\)
  2. B와 나머지 그룹을 분리하는 이진분류 모델 \(H_B\)
  3. C와 나머지 그룹을 분리하는 이진분류 모델 \(H_C\)

3개의 모델은 로지스틱회귀분석이나 MLP 등의 제한이 없지만 설명의 편의를 위해서 hidden layer가 없는 로지스틱회귀분석 모델(single perceptron)로 구성을 하였다. \(H_A\), \(H_B\), \(H_C\)의 예측값(확률)을 각각 \(h_A\), \(h_B\), \(h_C\)라고 하자. 이진분류 모델을 제작했기 때문에 final layer의 activation 함수는 sigmoid이다.
One vs the rest를 이용하여 학생 샘플 \(X= (x_1,x_2)\)의 성적을 예측하는 모델의 output(가설)은 다음과 같이 표현할 수 있다.

\[H(X)=(h_A(X), h_B(X),h_C(X))\]
  • 샘플이 A일 확률: \(h_A = f(XW_A+b_A)\)
  • 샘플이 B일 확률: \(h_B = f(XW_B+b_B)\)
  • 샘플이 C일 확률: \(h_C = f(XW_C+b_C)\)

여기서 \(f\)는 sigmoid이고 \(2\times 1\) 행렬 \(W\)와 실수 \(b\)는 \(H_A\), \(H_B\), \(H_C\) 각각의 모델에서 학습된 weight와 bias들이다.

만약에 \(h_A\)값이 가장 크다면 “샘플 \(X\)의 성적은 A일 것이다” 라고 결론 내릴 수 있다. 하지만 위 3개의 모델은 독립적인 이진분류의 결과값이기 때문에 성적이 A일 가능성이 높아지는 경우(\(h_A\) 증가)에도 \(h_B\)나 \(h_C\)에는 영향을 주지 않는다. 또한 A, B, C 각 클래스에 해당하는 데이터의 갯수가 동일 하더라도, A와 B, C를 구별하는 \(H_A\) 모델의 데이터 비율이 1:2로 구성된다. 즉, 학습데이터의 구성 비율이 이상적인 경우라도 one vs the rest에서 비대칭 데이터(imbalanced data set) 문제가 발생하여 제대로 된 학습 결과가 나오지 않을 수 있다. 분류해야하는 클래스의 갯수가 많아지게 되면 비대칭 비율은 더 커질 것이다(커진다는 표현이 맞는지 작아진다는 비율이 맞는지 애매하지만 여튼 안 좋아진다는 것임).

Softmax

One vs the rest을 이용한 경우에는 각 모델이 독립적으로 학습을 진행했기 때문에 \(H(X)\) 결과가 다음과 같이 나올 수 있다.

\[H(X) =(0.6, 0.5, 0.4)\]

샘플 \(X\)에 대해서 성적 A와 A가 아닌 두 경우로 분류했을 때 A를 받을 확률 0.6이다. 확률값으로만 보면 성적 A를 받을 확률이 가장 크기때문에 A라고 결론 내릴 수 있다. 하지만 각 모델마다 threshold이 상이할 수 있기 때문에 신중하게 접근해야 된다. \(H_A\) 모델에서 0.6의 확률은 A라고 결론 내릴 수 없는데, \(H_C\) 모델에서 0.4 확률은 C라고 결론 내릴 수 있는 경우도 존재할 수 있다는 것이다. 다음 그림을 보면 A와 C를 구별하는 기준이 다른 것을 확인할 수 있다.

정의

이런 문제를 극복하기 위해서 다중클래스 분류 문제에서는 softmax 함수를 사용하여 분류를 한다. 다변수 벡터함수인 softmax의 정의는 다음과 같다. \(Z=(z_1,\ldots,z_d)\in R^d\)에 대해서

\[f(Z) = \left (\frac{e^{z_1}}{\sum_{i=1}^d e^{z_i}}, \ldots, \frac{e^{z_d}}{\sum_{i=1}^d e^{z_i}} \right)\]

으로 정의한다. 정의에 의해서 모든 성분의 합은 1이며 \(i\) 성분을 \(i\) 번째 클래스에 속할 확률로 해석한다. 위의 성적 예측 예제를 적용해보면

\[\begin{equation} Z = (z_1, z_2, z_3) := (XW_A+b_A, XW_B+b_B, XW_C+b_C) \end{equation}\]

에 대해서 다음 softmax 함수값이 각 클래스에 들어갈 확률로 결과가 되도록 weight와 bias를 학습을 진행하는 것이다.

\[\begin{align*} f(Z) &=\left (P(A|X),P(B|X),P(C|X) \right )\\ \\ &=\left (\frac{e^{z_1}} {e^{z_1}+e^{z_2}+e^{z_3}},\frac{e^{z_2}}{e^{z_1}+e^{z_2}+e^{z_3}}, \frac{e^{z_3}}{e^{z_1}+e^{z_2}+e^{z_3}} \right) \end{align*}\]

편의를 위해서 hidden layer가 없는 형태로 \(Z\)를 구성하였으며 Hidden layer가 있는 경우에서는 input 변수 \(X\)대신 마지막 히든레이어(final hidden layer)로 대체하면 된다. 그리고 위 (1)을 간단히 행렬곱으로 다음과 같이 표현할 것이다.

\[Z = (z_1, z_2, z_3) = XW+b\]

여기서 \(X=(x_1,x_2)\), \(W\)는 \(W_A\), \(W_B\), \(W_C\)를 부분행렬로 갖는 \(W = ( W_A, W_B, W_C )\) 모양의 \(2 \times 3\) 행렬이고 \(b= (b_A, b_B, b_C)\)이다. One vs the rest와 기호는 똑같이 사용했지만 다른 weight와 bias로 학습될 것이다. 즉 hidden layer가 없는 softmax를 사용한 다중클래스 분석 모델은 다음과 같다.

\[H(X)=(h_1(X), h_2(X), h_3(X))= f(XW+b)\]

Softmax함수를 이용하면 one vs the rest 처럼 독립적으로 학습을 진행하는 것이 아니라 한번에 모든 클래스를 고려해서 학습을 한다. 그리고 예측 결과도 합이 1이 되도록 normalize 되어 있다. Output의 \(i\) 번째 성분은 \(i\)번째 클래스로 판단할 확률이기 때문에 output 노드의 수(차원)은 종속변수의 클래스의 갯수와 동일하다.

One hot encoding

샘플 \(X\)의 실제 성적이 A라고 한다면 모델의 예측 결과에서 \(h_1\)이 1에 가까워야 한다. 즉, \(H(X)\)는 \((1,0,0)\)과 유사한 값일 것이다. 마찬가지로 실제 성적이 B라면 \(H(X)\sim (0,1,0)\), 실제 성적이 C라면 \(H(X)\sim (0,0,1)\)이어야 한다. 그래서 다중클래스 모델을 구성하고 평가하기 위해서 종속변수를 다음과 같이 one hot encoding으로 클래스를 정의 한다.

공부 시간 출석 성적
10 5 (1,0,0)
9 5 (1,0,0)
3 2 (0,1,0)
2 4 (0,1,0)
11 2 (0,0,1)

Cross entropy for softmax classification

종속변수 \(Y\)의 클래스와 예측한 값 \(H\)와의 차이를 측정하는 cost함수로는 이진분류에서 사용했던 cross entropy를 사용한다. 데이터의 갯수가 \(n\)개 주어진 경우 cost 함수는 다음과 같이 정의한다.

\[Cost(W,b) = -\frac{1}{n} \sum_{i=1}^n \left \langle Y_i , \log H(X_i) \right\rangle\]

여기서 \(\left \langle \cdot , \cdot \right\rangle\)은 두 벡터의 내적, \(Y_i\)는 one hot vector로 표현된 종속변수(실제 라벨)를 의미하고 \(H(X_i)\)는 \(i\) 번째 샘플의 softmax 함수를 이용해서 학습한 모델 결과 벡터이다. 위 예제의 cost를 나타내면 다음과 같다.

\[Cost(W,b) = -\frac{1}{5} \left [ \log h_1(10,5) + \log h_1(9,5) + \log h_2(3,2) + \log h_2(2,4) +\log h_3(11,2) \right ]\]

이진분류: sigmoid vs softmax

종속변수의 클래스가 두 개인 이진분류(binary classification) 모델을 제작할 때 우리는 두가지로 모델을 고려할 수 있다. 간단히, 성공과 실패를 분류한다고 하겠다.

  • 성공 = 1, 실패 = 0 으로 설정: sigmoid를 이용한 이진분류
  • 성공 = (1,0), 실패 = (0,1) 으로 설정: softmax를 이용한 이진분류

결론을 얘기하면 동일한 예측값이 나온다. 즉, 최종 output layer 노드의 갯수만 다르고 동일하게 sigmoid를 사용하는 모델인데(sigmoid의 경우 노드: 1개, softmax의 경우 노드: 2개) 그 이유를 알아보자.

Sigmoid

MLP를 사용하여 모델은 다음과 같이 구성했다고 가정하자.

\(X\)는 input 변수이고 최종 output은 노드를 1개 갖는 \(H\)로 표현하였다. 그리고 최종 layer 바로 전에 있는 hidden layer를 \(H_{-1}\)이라고 하자. 성공을 1로 설정했기 때문에 샘플 \(X\)에 대한 모델의 결과값\(H\)은 성공일 확률을 의미하며 \(H(X)\)는 다음과 같다.

\[\begin{equation} H(X) = g(H_{-1}W +b) = g(z) \end{equation}\]

여기서 \(W\), \(b\)는 마지막 hidden layer에서 output을 연결해주는 weight와 bias들이고 \(g\)는 sigmoid이다.

  • 성공확률: \(H(X)= g(z)\)
  • 실패확률: \(1-H(X) = g(-z)\)

데이터의 갯수는 \(n\)개이고 \(i\)번 째 독립변수와 종속변수는 \(X_i\), \(y_i\)로 표현하였다. 실패확률이 \(g(-z)\)인 이유는 sigmoid의 다음과 같은 성질 때문이다.

\[g(z) + g(-z) = \frac{1}{1+e^{-z}} + \frac{1}{1+e^z} = 1\]

위 모델은 sigmoid를 사용했기 때문에 cost 함수는 다음과 같다.

\[\begin{equation} Cost = -\frac{1}{n}\sum_{i=1}^n \Big( y_i \cdot \log(H(X_i)) + (1 - y_i) \cdot \log(1 - H(X_i)) \Big)\\ \end{equation}\]

Softmax

Softmax를 이용하여 이진분류를 제작해보자. 종속변수가 성공과 실패 두 개이므로 최종 layer에는 노드의 수는 2이다. \(H(X) = (h_1(X), h_2(X))\)에서 첫 번째 성분을 성공확률 두 번째 성분을 실패확률 이라고 하자.

Input \(X\)부터 마지막 hidden layer 까지는 sigmoid 분류 모델과 동일하게 구성(hidden layer의 수와 노드의 수)하자.

Softmax를 사용했기 때문에 샘플 \(X\)에 대한 모델의 결과값 \(H(X)\)는 다음과 같다. 여기서 \(f\)는 softmax 함수다.

\[\begin{align*} H(X) =(h_1(X),h_2(X))= f(H_{-1}W +b) = f(z_1, z_2) &= \left ( \frac{e^{z_1}}{e^{z_1}+e^{z_2}} ,\frac{e^{z_2}}{e^{z_1}+e^{z_2}} \right)\\ &= \left ( \frac{1}{1+e^{z_2-z_1}} ,\frac{1}{1+e^{z_1-z_2}} \right) \end{align*}\]

마지막 output layer를 살펴보면 성공과 실패 확률은 \(z_2- z_1\)에 의해서 결정되는 것을 확인할 수 있다.

\[\text{성공확률} =h_1(X)= g(z_2- z_1)\] \[\text{실패확률} =h_2(X)= g(z_1- z_2)\]

참고로 (2) 식을 보면 sigmoid를 이용한 이진분류에서 성공확률은 \(g(z)\)이었다. 마찬가지로 cost 함수를 써보면

\[\begin{equation} Cost = -\frac{1}{n} \sum_{i=1}^n \left \langle Y_i , (\log h_1(X),\log h_2(X)) \right\rangle \end{equation}\]

이다. \(Y_i\)는 one hot vector로 표현된 종속변수를 의미하며 성공이면 \((1,0)\)이고 실패면 \((0,1)\)을 갖는다.

  • 성공 \(Y_i=(1,0)\), \(y_i=1\) 인 경우: (3), (4) 식이 다음과 같이 같은 값는다.
\[\log g(z_2-z_1) = \log h_1(X) = \log(H(X_i)) = \log g(z)\]
  • 실패 \(Y_i=(0,1)\), \(y_i=\) 인 경우: 마찬가지로 (3), (4) 식이 다음과 같이 같은 값는다.
\[\log g(z_1-z_2)=\log h_2(X) = \log(1-H(X_i)) = \log g(-z)\]

결론적으로 output layer를 제외하고 동일한 MLP 모델로 구성을 했다면 cost 함수도 동일하게 정의되고 예측값도 동일한 값이 나오기 때문에 동일한 모델로 학습이 진행될 것이다. \(z\)의 역할을 \(z_2-z_1\)이 하고 있는 셈이다(\(z\)를 찾는 \(z_2-z_1\)의 솔루션이 유일하지 않을 것이다. 즉, 0.8을 찾는데 차이가 0.8이 나오는 두 실수를 찾는 느낌이다).

결론

이진분류 문제를 해결하는데 softmax를 사용하면 sigmoid를 이용해서 찾아야 하는 weight의 갯수가 두 배로 많아지고 동일한 결과를 도출한다.


TensorFlow code

scikit-learn 라이브러리를 이용해서 one hot encoding 진행

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import OneHotEncoder

params = {'legend.fontsize': 20,
'legend.handlelength': 2}
plt.rcParams.update(params)

%matplotlib inline

# 데이터셋 구성
x_data = np.array([[10,5],
[9,5],
[3,2],
[2,4],
[11,1]])

# A = 0, B = 1, C = 2
y_data = [[0],[0],[1],[1],[2]]

plt.figure(figsize=(12,9))
plt.xlabel("Study time", fontsize=20)
plt.ylabel("Attendance", fontsize=20)
plt.scatter(x_data[0:2,0], x_data[0:2,1], s=500,label="A")
plt.scatter(x_data[2:4,0], x_data[2:4,1], s=500,label="B")
plt.scatter(x_data[4,0], x_data[4,1], s=500, label="C")
plt.legend(loc=3)

# One hot encoding
enc = OneHotEncoder()
enc.fit(y_data)
y_enc = enc.transform(y_data).toarray()

# 모델 구성
X = tf.placeholder(tf.float32, shape=[None,2])
Y = tf.placeholder(tf.float32, shape=[None,3])

W = tf.Variable(tf.random_normal([2,3]), dtype=tf.float32)
b = tf.Variable(tf.random_normal([3]), dtype=tf.float32)

logits = tf.matmul(X,W)+b
H = tf.nn.softmax(logits)

loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=y_enc))
train = tf.train.GradientDescentOptimizer(learning_rate=0.05).minimize(loss)

# Accuracy computation
correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# Launch the graph in a session.
sess = tf.Session()
# Initializes global variables in the graph.
sess.run(tf.global_variables_initializer())

cost_list = []
acc_list = []

iteration = 50
for step in range(iteration):
    acc, cost, _, = sess.run([accuracy, loss,  train], feed_dict={X: x_data, Y: y_enc})
    cost_list.append(cost)
    acc_list.append(acc)
    if (step+1) % (iteration//5) ==0:
        print("Step : %i, Cost : %s  Accuracy : %s" %(step+1, cost, acc))

plt.figure(figsize=(12,9))
plt.subplot(221)
plt.title("Cost", fontsize=20)
plt.xlabel("Steps")
_ = plt.plot(cost_list, "c")
plt.subplot(222)
plt.title("Accuracy", fontsize=20)
plt.xlabel("Steps")
_ = plt.plot(acc_list, "k--")


# X 대입
Hypothesis = sess.run(H, feed_dict={X: x_data})
print(np.roung(Hypothesis,2))

결과

공부 시간 출석 성적 예측값(H)
10 5 A (0.82, 0.06, 0.12)
9 5 A (0.77, 0.13, 0.11)
3 2 B (0.29, 0.48, 0.23)
2 4 B (0.05, 0.95, 0.01)
11 2 C (0.3, 0.0, 0.7 )