Programming & Machine Learning/풀어쓰는 머신러닝

Softmax Classifier의 이해 & Python으로 구현하기

Yamarae 2018. 7. 8. 19:58

이번 포스팅은 Softmax 함수를 이용하여 다중 클래스를 분류하는 방법과 모델 학습에 대해 개인적으로 이해한 내용을 바탕으로 작성한 것이다. Softmax Regression, 혹은 Softmax Classifier를 이해하기 위해서는 Regression, Logistic Regression에 대한 이해가 선행되어 있어야 한다.









1. Softmax 알고리즘의 이해



회귀분석의 개념을 이진 분류문제로 확장한 Logistic Regression(Logistic Regression 혹은 Linear Regression에 대한 설명 참고)의 원리를 생각해보자. input에 대한 연산 결과를 0~1 사이의 확률값으로 표현하고, 이를 통해 두 가지 중에 하나로 결론을 내리는 방법이 바로 로지스틱 회귀이다.


소프트맥스 회귀는 이를 확장한 것이다. 이진 분류문제가 아닌, 다중 분류를 해결하기 위한 모델을 제안한 것이 바로 소프트맥스 함수이다. 소프트맥스 함수는 여러 개의 연산 결과를 정규화하여 모든 클래스의 확률값의 합을 1로 만들자는 간단한 아이디어다. 정규화 함수에 자연상수를 한 번 씌워주는데, cost function의 미분값을 convex하게 만들어주기 위한 것이고 이것이 응용의 전부이다. 그래서 Logistic Regression과 Cross-entropy에 대해 제대로 이해했다면, 바로 연관지어서 생각하기 쉬운 함수이다.







Neural Network에서의 결과값의 class가 3개인 학습을 진행할 때의 Network 구조를 생각해보자. 보통 이런 식의 구조에서 Softmax classifier를 사용한다. input layer와 hidden layer를 거쳐서 최종적으로 3개의 output layer를 만든다. 이 때, 위 그림에서처럼 (2.0, 1.0, 0.1) 이라는 결과값이 나온다고 가정하자. 이 3개의 값들을 정규화하여 총 합이 1이 되는 3개의 확률을 만들어 내는 함수가 바로 softmax 함수이다. softmax 함수의 수식은 다음과 같다.





일반적인 Neural Network의 activation function 에서 유닛 k의 출력 Zk는 입력 Uk로부터만 결정되는 것과 대조적으로, 소프트맥수 함수를 activation function으로 사용할 때는 유닛 k의 출력 Zk는 Sum(Uk)로 결정된다는 차이점을 아는 것이 중요하다.









2. Softmax Classification의 Cost function



Logistic function과 마찬가지로, input parameter에 대한 우도를 평가하고 최대화하는 방법으로 cost function을 만들어내야 한다. 모든 Machine Learning 문제와 마찬가지로, 입력 x와 그 정답 클래스의 쌍을 학습 데이터로 사용한다. 그리고 일반적으로 Softmax classifier를 만들어내는 경우에는 정답 클래스를 one-hot encoding의 방법으로 학습시킨다.




결과적으로 Softmax의 Cost function은 위 이미지(항상 그렇듯이 저 강의의 이미지가 가장 직관적인듯)의 수식으로 정의한다. Machine Learning에 대한 해외의 좋은 강의들을 찾아 듣다 보면, 수식을 먼저 설명해서 이해시키기 보다는 일일히 값들을 넣어보고 '발견적' 학습을 하는 것을 좋아하는 것 같다. 과학이라는 것이 발견적 학문이라는 점을 생각해 보았을 때, 사실 이런 학습법이 가장 이해에 도움을 주는 것이 맞다. 그래서 어릴때 부터 주입식 수학 교육에 신물이 났던 나로써는 이 방법이 굉장히 신선하고 이해하기에 편했다. 아무튼 결론은, 위 이미지에서 나온 숫자들을 수식에 넣어보라는 것이다. (0.7, 0.2, 0.1)의 비율을 바꿔도 보고, 정답 데이터인 L의 값을 바꿔도 보자. 그럼 이 cross-entropy가 왜 cost function으로써 올바른지 직관적으로 이해하게 될 것이다. 물론 cross-entropy를 cost function으로 사용하는 수식적인 이유는 '미분의 편의성' 때문이다.









3. Softmax Regression의 Learning Problem



소프트맥스 함수는 함수의 input값의 변화에 매우 둔감한 학습능력을 보인다. 예를 들어, 함수의 Input값 Uk들 모두에 정수 U0를 더한다고 가정해보자.






그러면 위와 같은 수식의 유도 과정을 얻을 수 있다. 이 가정은 모든 결과값에 동일한 상수를 더한다는 가정이 있지만, 함수의 잉여성(Okatani Takayuki의 '딥러닝 제대로 시작하기' 참고)이 발생한다고 볼 수 있다. 이와 같은 잉여성이 발생할 때 학습 모델의 가중치 W들이 제대로 학습되지 않는 단점이 있다. 


Regression 계열의 Machine Learning에서 이러한 문제점을 해결하는 가장 일반적인 해결방법은 피처 정규화(Feature Regularization), 혹은 다른 말로 가중치 감쇠(Weight Decay)를 이용하는 것이다. (피처 정규화, regression에서의 여러 문제점과 관련된 포스팅 참고) 이는 가중치에 제약을 걸어준다는 것인데, 개별 W에 대해 음의 방향 혹은 양의 방향으로 가중치를 업데이트 할 때, 자신의 크기 만큼의 학습 벌점을 부과한다는 것이다. 마치 Gradient Descent 에서 Learning Rate를 높이는 것과 비슷한 효과이다. 이처럼 가중치에 제약이 가해지게 되면, 학습이 더디게 진행되는 현상이 해결된다. 수식은 다음과 같다.






다른 방법은 Dropout과 개념적으로 유사한 방법을 활용하는 것인데, Softmax 함수의 input 중 하나인 출력 유닛 Uk의 값을 0으로 바꾸는 방법이다. 이를 각 epoch마다 random하게 변경해주면 학습의 우연성, Serendipity(적당한 용어가 생각나지 않아 이 단어를 사용했다)를 올려주는 효과를 보일 것이다.






4. Python 코드로 구현하기



지금까지의 내용을 Python Code로 구현하였다. Cost function과 이에 대한 Gradient 부분의 설명이 다소 비약되거나 수식을 생략한 부분이 있는데, 코드로 짜보면 명확하게 이해가 간다.


import numpy as np ''' - GD를 이용하여 Softmax regression 학습 - 참고 : https://deepnotes.io/softmax-crossentropy ''' class SoftmaxRegression: def __init__(self, learning_rate=0.01, threshold=0.01, max_iterations=100000, verbose=False, reg_strength=1e-5): self._learning_rate = learning_rate # 학습 계수 self._max_iterations = max_iterations # 반복 횟수 self._threshold = threshold # 학습 중단 계수 self._verbose = verbose # 중간 진행사항 출력 여부 self._reg_strength = reg_strength # 정규화 파라미터 계수 # theta(W) 계수들 return def get_coeff(self): return self._W # softmax function def softmax_func(self, x_data): predictions = x_data - (x_data.max(axis=1).reshape([-1, 1])) softmax = np.exp(predictions) softmax /= softmax.sum(axis=1).reshape([-1, 1]) return softmax # prediction result example # [[0.01821127 0.24519181 0.73659691] # [0.87279747 0.0791784 0.04802413] # [0.05280815 0.86841135 0.0787805 ]] # cost function 정의 def cost_func(self, softmax, y_data): sample_size = y_data.shape[0] # softmax[np.arange(len(softmax)), np.argmax(y_data, axis=1)] # --> 해당 one-hot 의 class index * 해당 유닛의 출력을 각 row(1개의 input row)에 대해 계산 # --> (n, 1) 의 shape cost = -np.log(softmax[np.arange(len(softmax)), np.argmax(y_data, axis=1)]).sum() cost /= sample_size cost += (self._reg_strength * (self._W**2).sum()) / 2 return cost # gradient 계산 (regularized) def gradient_func(self, softmax, x_data, y_data): sample_size = y.shape[0] # softmax cost function의 미분 결과는 pi−yi 이므로, # softmax가 계산된 matrix에서, (해당 one-hot 의 class index * 해당 유닛)에 해당하는 유닛 위치에 -1을 더해줌. softmax[np.arange(len(softmax)), np.argmax(y_data, axis=1)] -= 1 gradient = np.dot(x_data.transpose(), softmax) / sample_size gradient += self._reg_strength * self._W return gradient # learning def fit(self, x_data, y_data): num_examples, num_features = np.shape(x_data) num_classes = y.shape[1] # 가중계수 초기화 self._W = np.random.randn(num_features, num_classes) / np.sqrt(num_features / 2) for i in range(self._max_iterations): # y^ 계산 z = np.dot(x_data, self._W) softmax = self.softmax_func(z) # cost 함수 cost = self.cost_func(softmax, y_data) # softmax 함수의 gradient (regularized) gradient = self.gradient_func(softmax, x_data, y_data) # gradient에 따라 theta 업데이트 self._W -= self._learning_rate * gradient # 판정 임계값에 다다르면 학습 중단 if cost < self._threshold: return False # 100 iter 마다 cost 출력 if (self._verbose == True and i % 100 == 0): print ("Iter(Epoch): %s, Loss: %s" % (i, cost)) # prediction def predict(self, x_data): return np.argmax(x_data.dot(self._W), 1)



코드의 실행 및 실제 학습진행 까지의 전체 코드는 아래 깃헙 링크에 있음.

https://github.com/yoonkt200/ml-theory-python/tree/master/03-softmax-regression



수식 및 구현 참고한 곳 (링크)