728x90
반응형

인공신경망(Artificial Neural Network, ANN)

 지금까지 퍼셉트론의 개념과 노드에 전달되어 합쳐진 신호들이 다음 노드로 전달될 때, 값이 어떤 방법으로 전달될지를 결정하는 활성화 함수에 대해 학습해보았다.

 앞서 학습했던, 퍼셉트론은 계단 함수를 이용해서 신호를 전달할지(출력값 = 1), 전달하지 않을지(출력 값 = 0)를 정하였으며, XOR 게이트 실현에서 층을 여러 개 쌓자 단일층으로 해결하지 못했던 문제를 해결할 수 있었다.

 여기서, 활성화 함수의 존재와 층을 여러 개 쌓는다. 이 부분에 초점을 맞추면, 인공신경망을 만들 수 있고, 이 인공신경망을 구현하고, 가장 적합한 가중치를 알아서 찾아내는 것이 바로 딥러닝(Deep Learning)이다.

 본격적으로 신경망을 공부하기 전에 단층 퍼셉트론의 연산이 어떻게 이루어지는지 확인해보도록 하자. 단층 퍼셉트론의 연산 방법을 알게 되면, 다층 퍼셉트론의 구현은 이를 쌓아가기만 하면 된다.

 

 

 

1. m : 1 단층 퍼셉트론의 연산

  • 위 그림에서 정보가 전달되는 방식을 수식으로 적어보면 다음과 같다.

$$ Z = w_1*x_1 + w_2*x_2 +b*1 $$

$$ y = h(Z) $$

  • 여기서, $x_1=2, x_2=5, w_1 = 0.4, w_2 = 0.2, b = 0.7$이라고 가정해보자
  • 위 수식에서 $Z$를 가장 빠르게 출력할 수 있는 방법은 백터 연산이다.
  • 활성화 함수를 시그모이드 함수로 해서 구현해보자.
>>> import numpy as np

>>> def sigmoid(x):
>>>     return 1 / (1 + np.exp(-x))

>>> x = np.array([2, 5, 1])
>>> w = np.array([0.4, 0.2, 0.7])
>>> Z = np.sum(x*w)
>>> sigmoid(Z)

0.9241418199787566
  • 도착점이 1개만 있는 경우, 입력층의 노드 수와 곱해지는 가중치 엣지의 수가 서로 동일하기 때문에, 길이가 동일한 벡터가 2개 나온다.
  • 때문에, 벡터연산으로 쉽게 해결할 수 있다.
    (numpy의 벡터 연산은 길이가 같은 벡터끼리 연산 시, 동일한 위치의 원소끼리 연산이 이루어지는 방식이다.)
  • m : 1 퍼셉트론은 이처럼 쉽게 연산이 가능했다. 그렇다면 입력층 노드의 수와 가중치 엣지의 수가 다른 m : n은 어떨까?

 

 

 

 

2. m : n 단층 퍼셉트론의 연산

  • 참고로 위에서 각 엣지(Edge)별 가중치에 써놓은 숫자들이 무슨 뜻인지 이해가 안 갈 수 있으니, 이를 간략히 설명해보겠다.

  • 가중치에서 위 괄호 안에 들어있는 값은 몇 번째 층(Layer)인지를 의미한다.
  • 아래 숫자는 앞은 다음 층, 뒤는 앞 층을 이야기한다.
  • 입력 노드의 값은 위에서부터 순서대로 1, 3, 5 라고 가정하자.
  • 가중치 엣지의 값은 위에서부터 순서대로 0.3, 0.5, 0.4, 0.2, 0.7, 0.3이라고 가정하자.
  • 편향 엣지의 값은 위에서부터 순서대로 0.2, 0.3이라 가정하자.
  • 이를, 벡터 연산으로 구하고자 한다면, 각 벡터의 길이가 다르고, 이를 잘라서 $y_1$, $y_2$를 따로따로 연산하기엔 시간도 많이 걸리고 공식도 지저분해진다.

 

 

 

 

3. 행렬 곱

  • 위와 같은 m : n 퍼셉트론은 행렬 곱을 사용한다면, 한방에 계산을 할 수 있다.
  • 위 퍼셉트론을 수식으로 간소화하면 다음과 같다.

$$ Y = WX + B$$

$$ X = (x_1, x_2, x_3) = (1, 3, 5)$$

$$ B = (b_{1}^{(1)}, b_{2}^{(1)}) = (0.2, 0.3)$$

$$ W=
\begin{pmatrix}
w_{11}^{(1)} & w_{21}^{(1)}\\ 
w_{12}^{(1)} & w_{22}^{(1)}\\ 
w_{13}^{(1)} & w_{23}^{(1)}
\end{pmatrix}
=
\begin{pmatrix}
0.3 & 0.5\\ 
0.4 & 0.2\\ 
0.7 & 0.3
\end{pmatrix} $$

  • 행렬 연산을 하기 위해선, 가장 먼저 배열이 어떻게 생겼는지를 확인해야 한다.
>>> X = np.array([1, 3, 5])
>>> B = np.array([0.2, 0.3])
>>> W = np.array([[0.3, 0.5],[0.4, 0.2],[0.7, 0.3]])

>>> print("X shape:", X.shape)
>>> print("B shape:", B.shape)
>>> print("W shape:", W.shape)

X shape: (3,)
B shape: (2,)
W shape: (3, 2)
  • 여기서 우리는 X와 W를 행렬곱할 것이다.
  • 행렬곱을 간단하게 짚고 가자면 다음과 같다.

  • 위 행렬 곱 방법을 보면, 서로 곱해지는 행렬에서 빨간 부분이 일치해야만 곱해지며, 출력되는 행렬의 녹색 부분이 행의 수, 파란색이 열의 수를 결정한다.
  • 여기서 지금까지의 행렬에 대한 인식을 조금만 틀어보자.
  • 행 = 데이터의 수
  • 열 = 변수의 수
  • 앞으로 이 인식을 하고 행렬 곱을 생각하게 된다면, 뒤에서 나올 n-차원 텐서의 연산에 대해서도 쉽게 이해할 수 있을 것이다(이에 대한 상세한 내용은 나중에 이야기하겠다.)
  • 자, 위 수식을 함수로 구현해보자.
# 단층 퍼셉트론 연산
>>> Z = np.dot(X, W) + B
>>> Z
array([5.2, 2.9])
  • np.dot(A,B): 행렬 A와 행렬 B를 행렬 곱한다.
  • 편향인 B는 행렬 X와 W의 곱과 길이가 동일한 벡터이므로(다음 노드의 수와 동일하다), 쉽게 벡터 합이 된다.
  • 신경망에서 신호가 흘러가는 것은 행렬 연산을 통해 진행되며, 때문에 딥러닝에서 행렬 연산에 매우 유리한 GPU가 사용되는 것이다.
  • 각 노드별 합산된 결과를 시그모이드 함수를 통해서 출력해보자.
# 시그모이드 함수를 활성화 함수로 사용
>>> sigmoid(Z)
array([0.9945137 , 0.94784644])

 

 

 

 

 지금까지 단층 퍼셉트론(SLP)을 이용해서 신경망에서 연산이 어떻게 이루어지는지 확인해보았다. 다음 포스트에서는 다층 퍼셉트론(MLP)을 이용해서 신경망 연산을 학습해보도록 하자.

728x90
반응형
728x90
반응형

앞서 은닉층에서 자주 사용되는 함수인 렐루(ReLU)에 대해 알아보았다. 그러나, 렐루 함수는 음수인 값들을 모두 0으로 만들어 Dying ReLU를 만드는 문제점이 있다고 하였다.

 렐루 함수는 아주 단순하지만, 그 단순함을 무기로 딥러닝의 길을 연 활성화 함수라 할 수 있고, 이러한 렐루 함수의 한계점을 보완하기 위해 다양한 활성화 함수들이 만들어졌으며, 지금도 만들어지고 있다.

 이번 포스트에서는 렐루 함수의 단점을 보완하기 위해 만들어진 활성화 함수들에 대해 알아보겠다.

 

 

 

1. 리키 렐루(Leaky ReLU, LReLU)

  • 렐루 함수의 한계점의 원인은 음수 값들이 모두 0이 된다는 것이었다.
  • 이를 해결하기 위해, 음수를 일부 반영해주는 함수인 리키 렐루가 등장하게 되었다.
  • 기존 렐루 함수는 음수를 모두 0으로 해주었다면,
  • 리키 렐루는 음수를 0.01배 한다는 특징이 있다.
>>> import numpy as np
>>> import matplotlib.pyplot as plt

# Leaky ReLU를 만들어보자
>>> def Leaky_ReLU(x):
    
>>>     return np.maximum(0.01*x, x)
>>> x = np.arange(-100.0, 100.0, 0.1)
>>> y = Leaky_ReLU(x)

>>> fig = plt.figure(figsize=(8,6))
>>> fig.set_facecolor('white')

>>> plt.ylim(-5, 20)
>>> plt.title("Leaky ReLU", fontsize=30)
>>> plt.xlabel('x', fontsize = 15)
>>> plt.ylabel('y', fontsize = 15, rotation = 0)
>>> plt.axvline(0.0, color='gray', linestyle="--", alpha=0.8)
>>> plt.axhline(0.0, color='gray', linestyle="--", alpha=0.8)
>>> plt.plot(x, y)
>>> plt.show()

  • 보시다시피 Leaky ReLU는 음수에 아주 미미한 값(0.01)을 곱하여, Dying ReLU를 막고자 하였다.
  • 그러나, 음수에서 선형성이 생기게 되고, 그로 인해 복잡한 분류에서 사용할 수 없다는 한계가 생긴다. 
  • 도리어 일부 사례에서 Sigmoid 함수나 Tanh 함수보다도 성능이 떨어진다는 이야기가 나올 때도 있다.
  • 만약, 음수가 아주 중요한 상황이라면 제한적으로 사용하는 것을 추천한다.

 

 

파라미터 렐루(Parameter ReLU, PReLU)

  • 렐루 함수가 0.01이라는 고정된 값을 음수에 곱해준다면, 파라미터 렐루는 이 값을 $\alpha$로 하여, 하이퍼 파라미터로써 내가 원하는 값을 줄 수 있도록 만든 활성화 함수다.
# Leaky ReLU를 만들어보자
>>> def Leaky_ReLU(x):
    
>>>     return np.maximum(0.01*x, x)


# PReLU를 만들어보자
>>> def PReLU(x, a):
    
>>>     return np.maximum(a*x, x)
>>> x = np.arange(-100.0, 100.0, 0.1)
>>> y1 = Leaky_ReLU(x)
>>> y2 = PReLU(x,0.05)

>>> fig = plt.figure(figsize=(8,6))
>>> fig.set_facecolor('white')

>>> plt.ylim(-5, 20)
>>> plt.title("Leaky ReLU & PReLU", fontsize=30)
>>> plt.xlabel('x', fontsize = 15)
>>> plt.ylabel('y', fontsize = 15, rotation = 0)
>>> plt.axvline(0.0, color='gray', linestyle="--", alpha=0.8)
>>> plt.axhline(0.0, color='gray', linestyle="--", alpha=0.8)
>>> plt.plot(x, y1, c = "blue", linestyle = "--", label = "Leaky ReLU")
>>> plt.plot(x, y2, c = "green", label = "PReLU")
>>> plt.legend(loc="upper right")
>>> plt.show()

  • PReLU는 Leaky ReLU와 그 성격이 상당히 비슷하지만, 음수의 계수인 $\alpha$를 가중치 매개변수처럼 학습되도록 역전파에 $\alpha$의 값이 변경되기 때문에, 대규모 이미지 데이터셋에서는 ReLU보다 성능이 좋다는 이야기가 있으나, 소규모 데이터셋에서는 과적합(Over fitting)될 위험이 있다.
  • 또한, PReLU 역시 선형성을 띄기 때문에 복잡한 분류에서 사용하지 못할 수 있으므로, 주의해서 사용하는 것이 좋다.
  • LReLU, PReLU 모두 기본으로는 ReLU 함수를 사용하고, 성능 개선 시, 활성화 함수를 해당 활성화 함수로 바꿔가며 실험해보고 사용하는 것을 추천한다.

 

 

 

 

2. ELU(Exponential Linear Unit)

  • E렐루는 2015년에 나온 비교적 최근 방법으로, 각져 있는 ReLU를 exp를 사용해, 부드럽게(Smooth) 만든 것이다.

$$f(x) = \begin{cases}
x \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \  (x > 0) \\ 
\alpha(e^x - 1) \ \  (x\leq 0)
\end{cases}$$

# ELU 함수를 만들어보자
>>> def ELU(x, a=1):
    
>>>     return (x>0)*x + (x<=0)*(a*(np.exp(x) - 1))
  • 위 코드에서 (x>0)은 x가 0보다 큰 값만 True로 하여 1로 나머지 값은 0으로 만든다.
  • (x <=0)은 0 이하인 값만 True로 하여 1로 하고 나머지 값은 0으로 만든다.
  • 0보다 큰 경우 x가 곱해지고, 0 이하는 $exp(x) - 1$이 곱해진다.
  • 각 반대되는 영역은 0이므로, 두 array을 합치면, 의도한 양수는 본래의 값, 나머지 값은 $exp(x) - 1$이 곱해져서 더해진다.
  • 이러한 Masking을 이용한 Numpy 연산은 코드를 단순하게 하고, 연산 시간을 크게 줄이므로, 꼭 익히도록 하자.
>>> x = np.arange(-30.0, 30.0, 0.1)
>>> a = 1
>>> y = ELU(x,a)

>>> fig = plt.figure(figsize=(8,7))
>>> fig.set_facecolor('white')

>>> plt.ylim(-3, 25)
>>> plt.xlim(-20, 20)
>>> plt.title("ELU", fontsize=30)
>>> plt.xlabel('x', fontsize = 15)
>>> plt.ylabel('y', fontsize = 15, rotation = 0)
>>> plt.axvline(0.0, color='gray', linestyle="--", alpha=0.8)
>>> plt.axhline(0.0, color='gray', linestyle="--", alpha=0.8)
>>> plt.plot(x, y)
>>> plt.show()

  • ELU는 ReLU와 유사하게 생겼으면서도, exp를 이용해 그래프를 부드럽게 만들고, 그로 인해 미분 시, 0에서 끊어지는 ReLU와 달리 ELU는 미분해도 부드럽게 이어진다.
  • ERU는 ReLU의 대표적인 대안 방법 중 하나다.
  • ERU는 ReLU와 달리 음의 출력을 생성할 수 있다.
  • ERU는 LReLU나 PReLU와 달리 음에서도 비선형적이기 때문에 복잡한 분류에서도 사용할 수 있다.
  • $\alpha$는 일반적으로 1로 설정하며, 이 경우 $x=0$에서 급격하게 변하지 않고, 모든 구간에서 매끄럽게 변하므로, 경사 하강법에서 수렴 속도가 빠르다고 한다.
  • 그러나, ReLU에 비해 성능이 크게 증가한다는 이슈가 없으며, 되려 exp의 존재로 연산량이 늘어났기에 학습 속도가 ReLU에 비해 느려서 잘 사용하지 않는다.

 

 

SELU(Scaled Exponential Linear Unit)

  • 일반적으로 ELU는 $\alpha$에 1을 넣으므로, PReLU처럼 $\alpha$를 하이퍼 파라미터 값으로 넣어 학습시킬 수 있게, ELU를 수정한 것이다.
  • $\alpha$ 2개를 파라미터로 넣어 학습시키면, 활성화 함수의 분산이 일정하게 나와 성능이 좋아진다고 한다.
  • 그러나 알파 값에 따라 활성화 함수의 결괏값이 일정하지 않아 층을 많이 쌓을 수 없다고 한다.
  • SELU 역시 ReLU에 비해 성능이 그리 뛰어나지 않고, 연산만 늘어나므로, 정 쓰고자 한다면 SELU보다 ELU를 추천한다.

 

 

 

 지금까지 ReLU와 유사한 ReLU의 형제들에 대해 알아보았다. LReLU, PReLU, ELU, SELU 중에 개인적으로는 ELU를 추천하지만, 소모되는 시간에 비해 성능 향상이 미비하거나 되려 성능이 내려가기도 하니 주의해서 쓰도록 하자.

 cs231n 강의에서는 ReLU > LReLU or ELU 순으로 사용할 것을 이야기하였으며, 가능한 sigmoid는 사용하지 말라고 하였다.

 이외에도 tanh함수를 대체하기 위해 고안된 softsign, ReLU를 부드럽게 꺾은 듯한 softplus와 Google이 최근 발표하였고, 높은 성능이 기대되는 Swish, ReLU와 LReLU를 일반화한 Maxout 등 다양한 활성화 함수가 존재하고, 지금도 새로 만들어지고 있다. 

 이러한 새로운 활성화 함수 중 성능이 괜찮다는 이슈가 있는 것은 추후 포스팅을 하도록 하고, 퍼셉트론에 기존의 계단 함수가 아닌 지금까지 학습해왔던 활성화 함수가 들어가 다층을 쌓아 만들어내는 신경망에 대해 차근차근 학습해보도록 하겠다.

 혹여 활성화함수에 대해 더 관심이 있다면 아래 홈페이지를 참고하기 바란다.

www.tensorflow.org/api_docs/python/tf/keras/activations

 

Module: tf.keras.activations  |  TensorFlow Core v2.4.1

Built-in activation functions.

www.tensorflow.org

 

728x90
반응형
728x90
반응형

 지금까지 계단 함수, 선형 함수, 시그모이드 함수, 소프트맥스 함수, 하이퍼볼릭 탄젠트 함수에 대해 다뤄보았다. 이들은 은닉층에서 사용해서는 안되거나, 사용할 수 있더라도 제한적으로 사용해야 하는 활성화 함수들이었다. 이번 포스트에서는 은닉층에서 많이 사용되는 렐루 함수에 대해 학습해 보겠다.

 

 

 

렐루 함수(Rectified Linear Unit, ReLU)

  • 렐루 함수는 딥러닝 역사에 있어 한 획을 그은 활성화 함수인데, 렐루 함수가 등장하기 이전엔 시그모이드 함수를 활성화 함수로 사용해서 딥러닝을 수행했다.
  • 그러나, 이전 포스트에서 언급했듯 시그모이드 함수는 출력하는 값의 범위가 0에서 1사이므로, 레이어를 거치면 거칠수록 값이 현저하게 작아지게 되어 기울기 소실(Vanishing gradient) 현상이 발생한다고 하였다.
    gooopy.tistory.com/52?category=824281
 

머신러닝-3.1. 활성화함수(2)-시그모이드 함수

 지난 포스트에서 퍼셉트론의 가장 기본이 되는 활성화 함수인 계단 함수(Step Function)를 학습하였으며, 선형 함수(Linear Function)의 한계점에 대해서도 학습해보았다.  선형 함수는 층을 쌓는 것이

gooopy.tistory.com

  • 이 문제는 1986년부터 2006년까지 해결되지 않았으나, 제프리 힌튼 교수가 제안한 렐루 함수로 인해, 시그모이드의 기울기 소실 문제가 해결되게 되었다.
  • 렐루 함수는 우리 말로, 정류된 선형 함수라고 하는데, 간단하게 말해서 +/-가 반복되는 신호에서 -흐름을 차단한다는 의미다.
  • 렐루 함수는 은닉층에서 굉장히 많이 사용되는데, 별생각 없이 다층 신경망을 쌓고, 은닉층에 어떤 활성화 함수를 써야 할지 모르겠다 싶으면, 그냥 렐루 함수를 쓰라고 할 정도로, 아주 많이 사용되는 활성화 함수이다(물론 신경망을 의도를 가지고 써보고 싶다면, 그래선 안된다.).

 

 

 

 

1. 렐루 함수의 생김새

  • 렐루 함수는 +신호는 그대로 -신호는 차단하는 함수라고 하였는데, 그 생김새는 아래와 같다.

$$ h(x) = \begin{cases}
 x \ \ \ (x>0) \\ 
 0 \ \ \ (x\leq 0) 
\end{cases} $$

  • 말 그대로, 양수면 자기 자신을 반환하고, 음수면 0을 반환한다.
  • 이번에는 이를 구현해보고, 어떻게 생겼는지 확인해보자.
>>> import numpy as np

# ReLU 함수를 구현해보자
>>> def ReLU(x):
    
>>>     return np.maximum(0, x)
  • 단순하게 최댓값 함수를 사용하여 지금 들어온 값(원소별 연산이 된다!)이 0보다 크면 자기 자신을 반환하고, 그렇지 않으면, 최댓값인 0을 반환하는 함수를 이용해서 구현하였다.
>>> import matplotlib.pyplot as plt

>>> x = np.arange(-5.0, 5.0, 0.1)
>>> y = ReLU(x)

>>> fig = plt.figure(figsize=(8,6))
>>> fig.set_facecolor('white')

>>> plt.title("ReLU", fontsize=30)
>>> plt.xlabel('x', fontsize = 15)
>>> plt.ylabel('y', fontsize = 15, rotation = 0)
>>> plt.axvline(0.0, color='gray', linestyle="--", alpha=0.8)
>>> plt.axhline(0.0, color='gray', linestyle="--", alpha=0.8)
>>> plt.plot(x, y)
>>> plt.show()

  • 가장 많이 사용되는 활성화함수라기엔 지금까지 보아왔던 시그모이드, 소프트맥스, 하이퍼볼릭 탄젠트 등에 비해 너무 단순하게 생겼다는 생각이 들 것이다.
  • 그렇다면 왜 렐루 함수를 은닉층에서 많이 사용할까?

 

 

 

 

2. 렐루 함수를 은닉층에서 많이 사용하는 이유

 

기울기 소실(Vanishing Gradient) 문제가 발생하지 않는다.

  • 렐루 함수는 양수는 그대로, 음수는 0으로 반환하는데, 그러다 보니 특정 양수 값에 수렴하지 않는다. 
  • 즉, 출력값의 범위가 넓고, 양수인 경우 자기 자신을 그대로 반환하기 때문에, 심층 신경망인 딥러닝에서 시그모이드 함수를 활성화 함수로 사용해 발생한 문제였던 기울기 소실(Vanishing Gradient) 문제가 발생하지 않는다.

 

기존 활성화 함수에 비해 속도가 매우 빠르다

  • 동시에 렐루 함수의 공식은 음수면 0, 양수면 자기 자신을 반환하는 아주 단순한 공식이다 보니, 경사 하강 시 다른 활성화 함수에 비해 학습 속도가 매우 빠르다!
  • 확률적 경사하강법(SGD)을 쓴다고 할 때, 시그모이드 함수나 하이퍼볼릭 탄젠트 함수에 비해 수렴하는 속도가 약 6배 가까이 빠르다고 한다!
  • ReLU가 나오기 전에는 활성화 함수가 부드러워야(Smooth) 가중치 업데이트가 잘된다고 생각하여 exp 연산이 들어간 시그모이드나, 하이퍼볼릭 탄젠트 함수를 사용하여쓰나, 활성화 함수가 부드러운(Smooth)한 구간에 도달하는 순간 가중치 업데이트 속도가 매우 느려진다.
  • ReLU는 편미분(기울기) 시 1로 일정하므로, 가중치 업데이트 속도가 매우 빠르다.

 

 

 

 

3. 렐루 함수의 한계점

  • 렐루 함수의 그래프를 보면, 음수 값이 들어오는 경우 모두 0으로 반환하는 문제가 있다보니, 입력값이 음수인 경우 기울기도 모조리 0으로 나오게 된다.
  • 입력값이 음수인 경우에 한정되긴 하지만, 기울기가 0이 되어 가중치 업데이트가 안되는 현상이 발생할 수 있다.
  • 즉, 가중치가 업데이트 되는 과정에서 가중치 합이 음수가 되는 순간 ReLU는 0을 반환하기 때문에 해당 뉴런은 그 이후로 0만 반환하는 아무것도 변하지 않는 현상이 발생할 수 있다.
  • 이러한 죽은 뉴런(Dead Neuron)을 초래하는 현상을 죽어가는 렐루(Dying ReLU) 현상이라고 한다.
  • 또한 렐루 함수는 기울기 소실 문제 방지를 위해 사용하는 활성화 함수이기 때문에 은닉층에서만 사용하는 것을 추천한다.
  • ReLU의 출력값은 0 또는 양수이며, ReLU의 기울기도 0 또는 1이므로, 둘 다 양수이다. 이로 인해 시그모이드 함수처럼 가중치 업데이트 시 지그제그로 최적의 가중치를 찾아가는 지그재그 현상이 발생한다.
  • 또, ReLU의 미분은 0 초과 시, 1 0은 0으로 끊긴다는 문제가 있다. 즉, ReLU는 0에서 미분이 불가능하다.
    (이에 대해 활성화 함수로는 미분 불가능 하다할지라도, 출력값 문제는 아니고, 0에 걸릴 확률이 적으니, 이를 무시하고 사용한다.)

 

 

 

 지금까지 은닉층에서 주로 사용되는 활성화 함수인 렐루 함수에 대해 학습해보았다. 비록 렐루 함수가 입력값이 0일 때, 기울기가 0에 수렴해 가중치 업데이트가 안 되는 현상이 발생한다고는 하지만, 성능상 큰 문제가 없으며, 도리어 이를 해결하기 위해 만든 활성화 함수의 성능이 보다 안 나오는 경우도 있다고 한다.

 때문에 기본적으로 은닉층에서는 렐루 함수를 사용하지만, 때에 따라 렐루 함수의 단점이 두드러지는 경우도 존재하므로, 렐루 함수의 한계점을 보완하기 위한 렐루 함수의 형제 함수들이 있다. 

 다음 포스트에서는 렐루 함수의 한계점을 극복하기 위해 만들어진 다양한 활성화 함수에 대해 학습해보도록 하곘다.

728x90
반응형
728x90
반응형

 지난 포스트에서는 시그모이드 함수에서 발전한 소프트맥스 함수에 대해 학습해보았다. 이번 포스트에서는 시그모이드 함수와 꽤 유사하면서, 시그모이드 함수의 단점을 보완한 하이퍼볼릭 탄젠트 함수에 대해 학습해보겠다.

 

 

하이퍼볼릭 탄젠트(Hyperbolic Tangent, tanh)

 우리말로 쌍곡선 탄젠트 함수라고 말하는 하이퍼볼릭 탄젠트는 수학이나 물리학을 전공한 사람이 아니라면, 영 볼 일이 없는 함수다.

 시그모이드, 소프트맥스 함수는 워낙 자주 사용되고, 신경망의 핵심 알고리즘인 로지스틱 회귀 모형에서 유래되었으므로, 공식까지 세세하게 파고 들어갔으나, 하이퍼볼릭 탄젠트 함수는 그 정도까지 설명할 필요는 없다고 생각한다.

 가볍게 하이퍼볼릭 탄젠트가 어떻게 생겼고, 왜 시그모이드 함수의 단점을 보완했다는지만 알아보도록 하자.

 

 

 

1. 하이퍼볼릭 탄젠트란?

  • 하이퍼볼릭 함수는 우리말로 쌍곡선 함수라고도 하며, 삼각함수는 단위원 그래프를 매개변수로 표시할 때, 나오지만, 쌍곡선 함수는 표준 쌍곡선을 매개변수로 표시할 때 나온다는 특징이 있다.
  • 삼각함수에서 $tanx$ = $sinx$/$cosx$로 나왔듯, 쌍곡선 함수에서 쌍곡탄젠트(Hyperbolic tangent)는 $tanhx$ = $sinhx$/$coshx$를 통해서 구한다.
  • 공식은 다음과 같다.

$$ sinhx = sinhx = \frac{e^x - e^{-x}}{2} $$

$$ coshx = \frac{e^x + e^{-x}}{2} $$

$$ tanhx = \frac{sinhx}{coshx} = \frac{e^x - e^{-x}}{e^x + e^{-x}} $$

  • 이들을 명명하는 방식은 다음과 같다.
    • $sinhx$: 신치, 쌍곡 샤인, 하이퍼볼릭 샤인
    • $coshx$: 코시, 쌍곡 코샤인, 하이퍼볼릭 코샤인
    • $tanhx$: 텐치, 쌍곡 탄젠트, 하이퍼볼릭 탄젠트

 

 

 

 

2. 하이퍼볼릭 탄젠트의 구현.

  • 위 공식을 그대로 구현해보면 다음 코드와 같다.
>>> import numpy as np

# 하이퍼볼릭 탄젠트
>>> def tanh(x):
>>>     p_exp_x = np.exp(x)
>>>     m_exp_x = np.exp(-x)
    
>>>     y = (p_exp_x - m_exp_x)/(p_exp_x + m_exp_x)
    
>>>     return y

 

  • 시각화해보자
>>> import matplotlib.pyplot as plt

>>> x = np.arange(-5.0, 5.0, 0.1)
>>> y = tanh(x)

# 캔버스 설정
>>> fig = plt.figure(figsize=(10,7)) # 캔버스 생성
>>> fig.set_facecolor('white')      # 캔버스 색상 설정

>>> plt.plot(x, y)
>>> plt.title("Hyperbolic Tangent", fontsize=30)
>>> plt.xlabel('x', fontsize=20)
>>> plt.ylabel('y', fontsize=20, rotation=0)

>>> plt.yticks([-1.0, 0.0, 1.0]) # 특정 축에서 특정 값만 나오게
>>> plt.axvline(0.0, color='k')
>>> ax = plt.gca()
>>> ax.yaxis.grid(True) # y축에 있는 모든 숫자에 회색 점근선을 그음

>>> plt.show()

  • 위 그림을 보면, 어째서 하이퍼볼릭 탄젠트 함수가 시그모이드 함수를 일부 보완했다고 하였는지, 이해할 수 있겠는가?
  • 시그모이드 함수와 하이퍼볼릭 탄젠트 함수의 가장 큰 차이는 출력값의 범위로, 하이퍼볼릭 탄젠트 함수는 -1에서 1 사이의 값을 출력하며, 중앙값도 0이다!
  • 이를 정리해보면 다음과 같다.
  시그모이드 함수 하이퍼볼릭 탄젠트 함수
범위 0 ~ 1 -1 ~ 1
중앙값 0.5 0
미분 최댓값 0.3 1

 

 

 

 

3. 하이퍼볼릭 탄젠트와 시그모이드 함수

  • 하이퍼볼릭 탄젠트는 중앙값이 0이기 때문에, 경사하강법 사용 시 시그모이드 함수에서 발생하는 편향 이동이 발생하지 않는다.
  • 즉, 기울기가 양수 음수 모두 나올 수 있기 때문에 시그모이드 함수보다 학습 효율성이 뛰어나다.
  • 또한, 시그모이드 함수보다 범위가 넓기 때문에 출력값의 변화폭이 더 크고, 그로 인해 기울기 소실(Gradient Vanishing) 증상이 더 적은 편이다.
    (※ 기울기 소실(Gradient Vanishing): 미분 함수에 대하여, 값이 일정 이상 커지는 경우 미분값이 소실되는 현상)
  • 때문에 은닉층에서 시그모이드 함수와 같은 역할을 하는 레이어를 쌓고자 한다면, 하이퍼볼릭 탄젠트를 사용하는 것이 효과적이다.
  • 그러나, 시그모이드 함수보다 범위가 넓다 뿐이지 하이퍼볼릭 탄젠트 역시 그 구간이 그리 크지는 않은 편이므로, $x$가 -5보다 작고 5보다 큰 경우, 기울기(Gradient)가 0으로 작아져 소실되는 기울기 소실 현상 문제는 여전히 존재한다.
# 시그모이드 함수의 미분
def diff_sigmoid(x):
    
    return 1/(1+np.exp(-x)) * (1 - (1/(1+np.exp(-x))))

# 하이퍼볼릭 탄젠트의 미분
def diff_tanh(x):
    
    return 4 / (np.exp(2*x) + 2 + np.exp(-2*x))
>>> import matplotlib.pyplot as plt

>>> x = np.arange(-10.0, 10.0, 0.1)
>>> y1 = diff_sigmoid(x)
>>> y2 = diff_tanh(x)

>>> fig = plt.figure(figsize=(10,5))

>>> plt.plot(x, y1, c = 'blue', linestyle = "--", label = "diff_sigmoid")
>>> plt.plot(x, y2, c = 'green', label = "diff_tanh")

>>> plt.title("Sigmoid VS tanh", fontsize=30)
>>> plt.xlabel('x', fontsize=20)
>>> plt.ylabel('y', fontsize=20, rotation=0)

>>> plt.ylim(-0.5, 2)
>>> plt.xlim(-7, 7)

>>> plt.legend(loc = "upper right")

>>> plt.axvline(0.0, color='k')
>>> ax = plt.gca()
>>> ax.yaxis.grid(True)
>>> ax.xaxis.grid(True)

>>> plt.show()

 

  • 위 그래프는 시그모이드의 도함수(파랑)와 하이퍼볼릭 탄젠트(녹색)의 미분 함수를 비교한 것으로, 시그모이드 함수의 미분보다 하이퍼볼릭 탄젠트의 미분이 상황이 더 낫긴 하지만, 하이퍼볼릭 탄젠트의 미분 역시 ±5부터 0이 되어버리므로, 기울기 소실 문제에서 안전하지 않다는 것을 알 수 있다.

 

 

 

 지금까지 하이퍼볼릭 탄젠트에 대해 알아보았다. 시그모이드 함수의 단점을 많이 보완한 활성화 함수이긴 하지만, 여전히 기울기 소실 문제가 발생할 가능성이 있으므로, 은닉층에서 쓰고자 하면, 쓰되 조심히 쓰기를 바란다.

 다음 포스트에서는 은닉층에서 가장 많이 사용되는 렐루(ReLU) 함수에 대해 알아보도록 하겠다.

728x90
반응형
728x90
반응형

 지난 포스트에선 활성화 함수에서 자주 사용되는 시그모이드 함수(Sigmoid Function)에 대해 학습해 보았다. 시그모이드 함수는 이진 분류에서 주로 사용되며, 보통 출력층에서만 사용된다. 은닉층에서 소프트맥스 함수가 사용되는 경우, 이전 포스트에서도 말했듯, 기울기 소실 문제 등 기울기를 제대로 찾지 못해, 학습 효율성이 감소한다는 단점이 있다.

 이번 포스트에서는 이전에 학습했던 이진 분류 활성화 함수인 시그모이드가 아닌 다중 분류에 주로 사용되는 활성화 함수인 소프트맥스(Softmax) 활성화 함수에 대해 살펴보도록 하겠다.

 

 

소프트맥스 함수(Softmax Function)

  • 소프트맥스는 세 개 이상으로 분류하는 다중 클래스 분류에서 사용되는 활성화 함수다.
  • 소프트맥스 함수는 분류될 클래스가 n개라 할 때, n차원의 벡터를 입력받아, 각 클래스에 속할 확률을 추정한다.

 

 

 

 

1. 소프트맥스 함수 공식

$$ y_k = \frac{e^{a_k}}{\sum_{i=1}^{n}e^{a_i}} $$

  • $n$ = 출력층의 뉴런 수(총 클래스의 수), $k$ = $k$번째 클래스
  • 만약, 총 클래스의 수가 3개라고 한다면 다음과 같은 결과가 나오게 된다.

$$softmax(z) = [\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}}] = [p_1, p_2, p_3]$$

  • 위 공식을 보면, 소프트맥스 함수의 생김세는 "k번일 확률 / 전체 확률"로 꽤나 단순하다는 것을 알 수 있다.

 

 

 

 

2. 소프트맥스 함수에서 $e^x$를 사용하여, 확률을 계산하는 이유

 위 소프트맥스 함수의 공식을 보면, 각 클래스에 속할 확률을 구하는 것은 알겠는데, 대체 왜 자연로그의 밑인 상수 e에 대한 지수함수를 사용하여 확률을 나타내는 것일까?

 

 이는 소프트맥스 함수는 시그모이드 함수로부터 유도된 것이기 때문이다. 

  • 시그모이드 함수를 $S$라고 가정하고 $e^{f(x)}$에 대한 공식으로 변환해보자.

$$S = \frac{1}{e^{-t}+1}, \ \ \  \frac{S}{1-S} = e^{t}$$

  • $\frac{S}{1-S}$에서 $S$는 "전체에서 $S$할 확률"이고 $1-S$는 "전체에서 $1-S$할 확률"이다. 즉, 앞서 봤던 오즈와 같다. 이 식은 각 집단이 독립이라는 가정하에 다음과 같이 변화시킬 수 있다.

$$ \frac{P(C_1)}{P(C_2)} = \frac{\frac{P(C_1)*P(X)}{P(X)}}{\frac{P(C_2)*P(X)}{P(X)}} = \frac{P(C_1|X)}{P(C_2|X)} = e^t$$

  • 위 식은 클래스가 2개일 때의 확률을 이야기하는 것이다. 그렇다면 클래스가 K개라면 어떨까?

$$ \frac{P(C_i|X)}{P(C_K|X)} = e^{t_i} $$

  • 이 식에 대하여 양변을 i = 1 부터 i = K-1까지 더해보자.

$$\sum_{i=1}^{K-1}\frac{P(C_i|X)}{P(C_K|X)} = \frac{1}{P(C_K|X)}\sum_{i=1}^{K-1}P(C_i|X) = \sum_{i=1}^{K-1}e^{t_i} $$

  • 위 식에서 일부분을 이렇게 바꿀 수 있다.

$$ \sum_{i=1}^{K-1}P(C_i|X) = 1-P(C_K|X) $$

  • 이를 식에 반영해보면 이렇게 된다.

$$ \frac{1-P(C_K|X)}{P(C_K|X)} = \sum_{i=1}^{K-1}e^{t_i}, \ \ \ \frac{P(C_K|X)}{1-P(C_K|X)} = \frac{1}{\sum_{i=1}^{K-1}e^{t_i}} $$

$$ P(C_K|X)\sum_{i=1}^{K-1}e^{t_i}= 1-P(C_K|X), \ \ \ P(C_K|X)(\sum_{i=1}^{K-1}e^{t_i} + 1) = 1 $$

$$ P(C_K|X) = \frac{1}{\sum_{i=1}^{K-1}e^{t_i} + 1} $$

  • 위 식을 통해서 $P(C_K|X)$를 유도하였으며, 이제 처음에 만들었던 식을 이용해서 P(C_i|X)를 유도해보자.

$$ \frac{P(C_i|X)}{P(C_K|X)} = e^{t_i}, \ \ \ P(C_i|X) = e^{t_i}P(C_K|X) = \frac{e^{t_i}}{\sum_{i=1}^{K-1}e^{t_i} + 1} $$

  • 분모에 있는 1은 다음과 같은 방법으로 제거한다.

$$ \frac{P(C_i|X)}{P(C_K|X)} = e^{t_i} $$

  • $i = K$ 이라면

$$ \frac{P(C_K|X)}{P(C_K|X)} = 1 = e^{t_K} $$

  • 위 식을 분모의 1에 넣어주자.

$$ P(C_i|X) = \frac{e^{t_i}}{\sum_{i=1}^{K-1}e^{t_i} + e^{t_K}} = \frac{e^{t_i}}{\sum_{i=1}^{K}e^{t_i}} $$

  • 시그모이드에서부터 지금까지의 과정을 보면, "로짓(Logit) > 시그모이드 함수 > 소프트맥스 함수" 순으로 유도되는 것을 알 수 있다.
  • 애초에 분류라는 것은 로지스틱 회귀 모델에 의해 파생되는 것이므로, 기계 학습에 대해 자세히 알기 위해선 로지스틱 회귀 모델에 대해 자세히 알 필요가 있다.

 

 

 

 

3. 소프트맥스 함수에서 $e^x$를 사용하여 얻어지는 장점

  1. 지수함수 단조 증가함수(계속 증가하는 함수)이기 때문에 소프트맥스에 들어가는 인자들의 대소 관계는 변하지 않는다.
  2. 지수함수를 적용하면, 아무리 작은 값의 차이라도 확실히 구별될 정도로 커진다.
  3. $e^x$의 미분은 원래 값과 동일하기 때문에, 미분을 하기 좋다.

$$\frac{d}{dx}e^x = e^x $$

# 가볍게 지수함수를 그려보자
import numpy as np
import matplotlib.pyplot as plt

x = np.arange(-5, 5, 0.1)
y = np.exp(x)

# 캔버스 설정
fig = plt.figure(figsize=(8,6)) # 캔버스 생성
fig.set_facecolor('white')      # 캔버스 색상 설정

plt.plot(x, y)
plt.title("Exponential Function", fontsize = 25)
plt.xlabel('x', fontsize = 15)
plt.ylabel('y', fontsize = 15, rotation = 0)
plt.show()

 

 

 

 

4. 소프트맥스 함수의 구현

  • 위 공식을 그대로 구현해보면 다음 코드와 같다.
# 소프트맥스
>>> def softmax(x):
    
>>>     exp_x = np.exp(x)
>>>     result = exp_x / np.sum(exp_x)
    
>>>     return result
  • 그러나 위 코드에는 상상치 못한 문제점이 하나 숨어있다.
  • 그것은 바로, 코드 내에 지수함수가 포함되어 있다는 것인데, 지수함수는 값을 더욱 확대한다는 특징을 가지고 있으며, 만약 지나치게 큰 값이 원소로 들어가게 된다면, 값이 너무 커서 연산이 되지 않는 오버플로 문제를 일으킬 위험이 있다.
  • 지수함수의 문제가 어떠한지 보기 위해 아래 코드를 보자.
>>> print(np.exp(10))
>>> print(np.exp(100))
>>> print(np.exp(1000))

22026.465794806718
2.6881171418161356e+43
inf
<ipython-input-15-2a14d587d35d>:3: RuntimeWarning: overflow encountered in exp
  print(np.exp(1000))
  • 여기서 np.exp(x)는 $e^x$를 의미하는 함수이다.
  • 고작 $e^1000$만 했을 뿐인데, 값이 무한대로 나와, RuntimeWarning이 뜨는 것을 볼 수 있다.
  • 이는 softmax에 들어가는 원소들에 대하여, 그 원소들의 최댓값을 빼는 것으로 쉽게 해결할 수 있다.
>>> def softmax(x):
>>>     """ 소프트맥스 함수
>>>     Input: array
>>>     Output: array
>>>     """
>>>     # Input 값에 Input 값의 최댓값을 뺀다.
>>>     array_x = x - np.max(x)
    
>>>     exp_x = np.(array_x)
>>>     result = exp_x / np.sum(exp_x)
    
>>>     return result
  • 위 방법이 가능한 것을 증명해보면 다음과 같다.

$$ y_k = \frac{e^{a_k}}{\sum_{i=1}^{n}e^{a_i}} = \frac{Ce^{a_k}}{C\sum_{i=1}^{n}e^{a_i}} = \frac{e^{a_k + lnC}}{\sum_{i=1}^{n}e^{a_i + lnC}} =  \frac{e^{a_k + C'}}{C\sum_{i=1}^{n}e^{a_i + C'}} $$

  • 위 코드를 보면 """ 주석 """를 만들어주었으며, Input, Output을 써주었다. 보다 자세히 코드에 대한 설명을 써주면 좋지만, 최소한 Input, Output되는 Data가 어떠한 형태인지는 써줄 필요가 있다.
  • 위 함수에 값을 넣어 그 효과와 형태를 보자.
# 소프트맥스 함수에 임의의 값을 넣어보자
>>> x = np.array([15, 10, 20, 30, 60])
>>> softmax(x)
array([2.86251858e-20, 1.92874985e-22, 4.24835426e-18, 9.35762297e-14,
       1.00000000e+00])
  • 가장 끝에 있는 5번째 값이 가장 크게 나오는 것을 볼 수 있다.
  • 이를 그래프로 그려보자.
# 소프트맥스 함수로 그래프를 그려보자.
>>> x = np.arange(-5.0, 5.0, 0.1)
>>> y = softmax(x)

>>> fig = plt.figure(figsize=(10,7)) # 캔버스 생성
>>> fig.set_facecolor('white')      # 캔버스 색상 설정

>>> plt.plot(x, y)
>>> plt.ylim(0, 0.1)
>>> plt.title("Softmax", fontsize=30)
>>> plt.xlabel('x', fontsize=20)
>>> plt.ylabel('y', fontsize=20, rotation=0)
>>> plt.show()

 

 

 

 소프트맥스 함수는 시그모이드 함수처럼 출력층에서 주로 사용되며, 이진 분류에서만 사용되는 시그모이드 함수와 달리 다중 분류에서 주로 사용된다. 

 무엇보다도 소프트맥스 함수의 큰 장점은 확률의 총합이 1이므로, 어떤 분류에 속할 확률이 가장 높을지를 쉽게 인지할 수 있다. 

 다음 포스트에서는 시그모이드 함수의 대체제로 사용되는 활성화 함수인 하이퍼볼릭 탄젠트 함수(tanh)에 대해 학습해 보겠다.

728x90
반응형
728x90
반응형

 지난 포스트에서 퍼셉트론의 가장 기본이 되는 활성화 함수인 계단 함수(Step Function)를 학습하였으며, 선형 함수(Linear Function)의 한계점에 대해서도 학습해보았다.

 선형 함수는 층을 쌓는 것이 무의미해진다는 단점이 있고, 비선형함수 중 하나인 계단 함수는 값의 크기에 대한 정보가 소실된다는 단점이 있다.

 이번 포스트에서는 이 두 단점이 해결된 비선형 함수 중 하나인 시그모이드 함수에 대해 학습해보겠다.

 

 

시그모이드 함수(Sigmoid Function)

계단 함수는 출력을 0과 1로 이진 값만 반환하며, 그 사이에 있는 값은 무시한다는 단점이 있었다. 그렇다면, 앞서 봤던 계단 함수의 각진 부분이 매끄러워진다면 어떨까?

 

 

 

1. 로지스틱 회귀 모델과 오즈(Odds)

  • 통계학을 조금이라도 공부해봤거나, 분석에 관심 있는 사람이라면, 이진 분류의 대표적인 모델 중 하나인 로지스틱 회귀(Logistic regression)에 대해 들어봤거나, 알고 있을 것이다.
  • 시그모이드 함수를 설명하기 앞서 로지스틱 회귀 모델의 오즈(Odds)를 이야기 해보겠다.

 

 

오즈(Odds)

  • 오즈는 성공과 실패의 비율이다.
  • 확률(Probability)과 뉘앙스가 꽤 다른 확률로
  • "사건 A가 일어날 확률 / 사건 A가 일어나지 않을 확률"을 말한다.

$$ Odds = \frac{P}{1-P} $$

 

 

오즈비(Odds ratio)

  • 참고로 오즈비(Odds ratio)와 오즈(Odds)를 헷갈리는 경우가 종종 있는데, 오즈비는 우리말로 교차비라고 하며, 서로 다른 집단의 오즈를 비교할 때 사용된다.
  • 예를 들어 약품의 성능 대한 오즈비는, "약품 A를 먹어서 호전될 오즈 / 위약을 먹어서 호전될 오즈"이다.
  Favorable UnFavorable Total
Test 60 40 100
Control 20 80 100

$$ Test\ favorable\ Odds = \frac{Test\ favorable\ ratio}{Test\ unfavorable\ ratio} = \frac{\frac{60}{100}}{\frac{40}{100}} = 1.5$$

$$ Control\ favorable\ Odds = \frac{Control\ favorable\ ratio}{Control\ unfavorable\ ratio} = \frac{\frac{20}{100}}{\frac{80}{100}}=0.25$$

$$ OddsRatio = \frac{Test\ favorable\ Odds}{Control\ favorable\ Odds} = \frac{1.5}{0.25} = 6.0 $$

  • 위 예시를 보면, 약품A의 효과는 위약의 효과의 6.0배임을 알 수 있다.

 

 

로짓 변환.

  • 오즈에 자연로그를 취해서 로짓(Logit) 함수를 만들어보자.

$$ logit(P) = ln\frac{p}{1-p} = f(x) $$

$$ \frac{p}{1-p} = e^{f(x)},\ \  p=e^{f(x)}(1-p),\ \  p=e^{f(x)} - pe^{f(x)}, \ \  p(1+e^{f(x)})=e^{f(x)} $$

$$ p = \frac{e^{f(x)}}{1+e^{f(x)}} = \frac{1}{1+e^{-{f(x)}}} $$

  • 위 함수를 로지스틱 시그모이드 함수(Logistic sigmoid function)이라고 하며, 줄여서 시그모이드 함수(Sigmoid function)이라고 한다.
  • 여기서 $f(x)$에 회귀 분석과 같은 함수 식을 넣으면, 로지스틱 회귀 모델(Logistic Regression model)이 된다.

$$ f(x) = w^Tx = w_0x_0 + w_1x_1 + w_2x_2 +\ ... + w_mx_m  $$

 

 

 

 

2. 시그모이드 함수(Sigmoid Function)

  • 시그모이드 함수를 파이썬에서 구현해보자.
# 시그모이드 함수
>>> def sigmoid(x):
>>>     return 1 / (1 + np.exp(-x))
>>> x = np.arange(-10.0, 10.0, 0.1)
>>> y = sigmoid(x)

# 캔버스 설정
>>> fig = plt.figure(figsize=(8,7)) # 캔버스 생성
>>> fig.set_facecolor('white')      # 캔버스 색상 설정

>>> plt.plot(x, y)
>>> plt.ylim(-0.1, 1.1)
>>> plt.xlim(-10, 10)
>>> plt.title("Sigmoid", fontsize=30)
>>> plt.xlabel('x', fontsize=20)
>>> plt.ylabel('y', rotation=0, fontsize=20)

>>> plt.yticks([0.0, 0.5, 1.0]) # 특정 축에서 특정 값만 나오게
>>> plt.axvline(0.0, color='k')
>>> ax = plt.gca()
>>> ax.yaxis.grid(True) # y축에 있는 모든 숫자에 회색 점근선을 그음

>>> plt.show()

  • 시그모이드 함수는 0에서 1 사이의 함수이며, 값이 들어왔을 때, 0~1 사이의 값을 반환한다.
  • 연속형 데이터이기 때문에 계단 함수가 끊기지 않는 매끄러운 모양으로 바뀐 것을 알 수 있다.
  • 동시에 이상치가 들어온다 할지라도, 시그모이드 함수는 0과 1에 수렴하므로, 이상치 문제도 해결하면서, 연속된 값을 전달할 수 있다.
  • 시그모이드 함수를 활성화 함수로 사용하면, 0과 1에 가까운 값을 통해 이진 분류를 할 수 있다.

 

 

 

 

3. 시그모이드 함수의 장점과 단점

A. 장점

  • 출력 값의 범위가 0 ~ 1 사이이며, 매우 매끄러운 곡선을 가지므로, 후술 할 경사하강법을 시행할 때, 기울기가 급격하게 변해서 발산하는, 기울기 폭주(Gradient Exploding)가 발생하지 않는다.
  • 분류는 0과 1로 나뉘며, 출력 값이 어느 값에 가까운지를 통해 어느 분류에 속하는지 쉽게 알 수 있다.

 

B. 단점

  • 입력값이 아무리 크더라도, 출력되는 값의 범위가 매우 좁기 때문에 경사하강법 수행 시에 범위가 너무 좁아, 0에 수렴하는 기울기 소실(Gradient Vanishing)이 발생할 수 있다.

  B.1. 기울기 소실(Gradient Vanishing) 문제

  • 시그모이드 함수는 아무리 큰 값이 들어온다 할지라도 0~1사이의 값만 반환하므로, 값이 일정 비율로 줄어들어 값의 왜곡이라 할 수는 없으나, 값이 현저하게 줄어들게 된다. 
  • 또한, 출력 값의 중앙값이 0이 아닌 0.5이며, 모두 양수기 때문에 출력의 가중치 합이 입력의 가중치 합보다 커지게 된다.
  • 이를 편향 이동(Bias Gradient)라 하고, 신호가 각 레이어를 통과할 때마다 분산이 계속 커지게 되어, 활성화 함수의 출력이 최댓값과 최솟값인 0과 1에 수렴하게 된다.
  • 시그모이드 함수의 도함수는 $\sigma(1-\sigma)$인데, 도함수에 들어가는 함수의 값이 0이나 1에 가까울수록 당연히 출력되는 값이 0에 가까워지게 된다.
  • 이로 인해 수렴되는 뉴련의 기울기(Gradient) 값이 0이 되고, 역전파 시 0이 곱해져서 기울기가 소멸(kill)되는 현상이 발생해버린다! 즉, 역전파가 진행될수록 아래 층(Layer)에 아무런 신호가 전달되지 않는 것이다!
  • 이를 기울기 소실(Gradient Vanishing)이라 하며, 렐루 함수가 등장하기 전까지인 1986년부터 2006년까지 해결되지 않은 문제다.

  B.2. 학습 속도 저하 문제

  • 시그모이드 함수의 출력값은 모두 양수기 때문에 경사하강법을 진행할 때, 그 기울기가 모두 양수거나 음수가 된다. 이는 기울기 업데이트가 지그재그로 변동하는 결과를 가지고 오고, 학습 효율성을 감소시켜 학습에 더 많은 시간이 들어가게 만든다.

 

 

 

 위 시그모이드 함수의 장단점을 간추려보면, 출력값이 너무 작아 제대로 학습이 안되는데다가 시간도 많이 잡아먹는다는 소리다.

 이는, 출력층에서 시그모이드 함수를 사용하는 것은 상관 없으나, 아래로 정보가 계속 흘러가는 은닉층(Hidden Layer)에서는 시그모이드 함수를 활성화 함수로 사용해서는 안된다는 소리다.

  • 은닉층(Hidden Layer)은 입력층(시작), 출력층(끝) 사이에 있는 부분이다.
  • 즉, 은닉층에는 앞서 말했던 선형 함수와 시그모이드 함수는 사용하지 않는 것이 좋다.
  • 시그모이드 함수는 이진 분류를 하고자 하는 경우 출력층에서만 사용하는 것을 권고한다.
  • 만약, 입력층에서 시그모이드 함수를 쓰고자 한다면, 이의 발전형인 하이퍼볼릭 탄젠트 함수를 사용하는 것을 추천한다.

 

 

 

 

 

 이번 포스트에서는 시그모이드 함수에 대해 간략하게 알아보았다. 시그모이드 함수는 이진 분류가 목적인 학습 모델에서 출력층에서 사용하는 것을 추천한다. 다음 포스트에서는 또 다른 활성화 함수인 소프트맥스 함수에 대해 학습해보겠다.

728x90
반응형
728x90
반응형

 

활성화 함수(Activation Function)

 이전 퍼셉트론에서 학습했던 내용을 보면, 입력층(Input layer)에서 전달된 정보(값)는 가중치를 받아 값이 변하고, 가중치를 받아 합산된 값($w_1x_1 + x_2x_2$)이 편향($b=-\theta$)보다 크거나 작다에 의해 정보가 전달(0 또는 1) 된다고 하였다.

 이번 포스트에서는 이 정보가 전달되는지, 즉 정보가 활성화 되는지 혹은 정보가 활성화된다면, 어떻게 활성화되어 출력 값을 생성해내는지를 결정하는 활성화 함수(Activation Function)에 대해 학습해보겠다.

 

 

 

1. 퍼셉트론에서 활성화 함수가 적용된 방법

  • 우리가 퍼셉트론에서 사용했던 "임계값을 넘으면 정보가 전달되고, 임계값을 넘지 않으면 정보가 전달되지 않는다. "는 말을 활성화 함수에 초점을 맞춰서 보다 단순화된 공식으로 만들어보자.

 

$$ X = w_1x_1 + w_2x_2 + b $$

$$ h(x) = \begin{cases}
 0 \ \ (x \leq 0) \\ 
 1 \ \ (x >0) 
\end{cases} $$

 

  • 위 공식에서 퍼셉트론 수식의 결과인 $X$는 활성화 함수 $h(x)$에 들어가게 되고, 그 결과는 출력 값 $y$로 나오게 된다. 이를 이해하기 쉽게 그림으로 그려보자.

  • 이전에 봤던 퍼셉트론의 그림과 달리 파란색 노드가 새로 추가되지 않았는가?
  • 이는 편향값이던 $b$를 노드와 동일한 형태로 만든 것이다. 이로써 $w_1, w_2, b$모두 단순하게 가중치라고 생각해도 충분한 상황이 만들어졌다.
  • 앞에서도 설명하긴 했지만, 다시 한번 설명해보자면, 각 노드 1, $x_1, x_2$는 각각 $b, w_1, w_2$를 가중치로 받아 곱해지고, $X$로 합산되어 나온다. 합산된 $X$는 활성화 함수 $h(X)$가 되어 출력 값인 $y$가 최종적으로 도출되게 된다.

 

 

 

 

2. 계단 함수(Step Function)

  • 이번엔 위 퍼셉트론에서 활성화 함수로 사용된, 계단 함수(Step Function)에 대해 초점을 맞춰보자.

$$ h(x) = \begin{cases}
 0 \ \ (x \leq 0) \\ 
 1 \ \ (x >0) 
\end{cases} $$

  • 계단 함수라고 하니, 대체 무슨 소리인가 싶을 텐데, 위 공식을 그래프로 그려보면 쉽게 이해할 수 있다.
>>> import numpy as np
>>> import matplotlib.pyplot as plt

# 계단 함수1
>>> def step_function1(x):
>>>     if x <= 0:
>>>         return 0
>>>     else x>0:
>>>         return 1


# 계단 함수2
>>> def step_function2(x):
>>>     y = x > 0 
>>>     return y.astype(np.int)
  • 위 코드를 보면, 위 코드는 단순히 "만약 x가 0 이하면 0을 반환하고, 0 초과이면 1을 반환하라"라는 의미로 단순하게 받아들여질 수 있는데, 아래 코드는 무슨 의미인지 잘 와 닿지 않을 수 있다.
  • x.astype(np.int)는 데이터 x를 정수(int) 타입으로 바꿔준다는 것인데, x > 0의 결과는 x로 주어진 인자들이 0보다 큰지 작은지를 진리 값을 반환하며, True는 1, False는 0이므로, 위 step_function1과 같은 기능을 갖는 것이다.
  • 코드를 쉽고 다른 사람도 이해하기 쉽게 짜려면 위 코드도 괜찮은 선택이다. 다만 상황에 따라 데이터의 양이 너무 많이 성능을 따져야한다면, 이런 식으로 같은 결과를 가지고 오지만, 다른 방법으로 돌아가는 코드도 짜서 성능 비교를 해볼 필요는 있다.
# 계단 함수를 그려보자
>>> np.arange(-5.0, 5.0, 0.1)
>>> y = step_function2(x)

# 캔버스 설정
>>> fig = plt.figure(figsize=(8,8)) # 캔버스 생성
>>> fig.set_facecolor('white')      # 캔버스 색상 설정

>>> plt.plot(x, y)
>>> plt.ylim(-0.5, 1.5)
>>> plt.xlim(-5, 5)
>>> plt.ylabel('y', fontsize = 20, rotation = 0)
>>> plt.xlabel('x', fontsize = 20)
>>> plt.title("Step Function", fontsize = 30)
>>> plt.show()

  • 위 그래프를 보면, 마치 계단을 올라가듯이, x가 0을 기준으로 크게 변하는 것을 알 수 있다.
  • 위 함수의 의미는 아주 순수하게 신경 세포의 전달 방법을 묘사한 기법으로, 출력되는 결과값이 갖는 정보가 너무 희석된다는 단점이 있다.
  • 예를 들어, 합산된 값이 0.1인 경우와 1.0인 경우는 단순 산술적으로 10배 정도 차이가 있으나, 이를 모두 무시하고 단순하게 1로 전달하므로, 합산된 값의 강도에 대한 의미가 부여되지 않는다.

 

 

 

 

3. 선형 함수(Linear Function)

  • 앞서 본 계단 함수는 합산된 값의 크기를 완전히 무시한다는 단점이 있다고 했다.
  • 그렇다면, 정직하게 자신의 값을 나타내는 선형 함수(일차 함수)는 어떨까?

$$ y = kx (k: 상수)$$

# 선형 함수
>>> def Linear_Function(x, k):
    
>>>     return k*x


>>> x = np.arange(-5.0, 5.0, 0.1)
>>> y = Linear_Function(x, 0.7)

# 캔버스 설정
>>> fig = plt.figure(figsize=(8,7)) # 캔버스 생성
>>> fig.set_facecolor('white')      # 캔버스 색상 설정

>>> plt.plot(x, y)
>>> plt.ylim(-1, 4)
>>> plt.xlim(-1, 4)
>>> plt.axhline(c="black")
>>> plt.axvline(c="black")
>>> plt.ylabel('y', fontsize = 20, rotation = 0)
>>> plt.xlabel('x', fontsize = 20)
>>> plt.title("Linear Function", fontsize = 30)
>>> plt.show()

  • 자 아주 단순한 일차 함수를 그려보았다.
  • 그러나, 선형 함수는 활성화 함수로 사용할 수 없는 치명적인 단점을 2가지 가지고 있다.

 

선형 함수를 활성화 함수로 사용하지 못하는 이유

 1. 선형 함수는 층을 쌓는 의미가 없게 만든다.

  • 예를 들어 3개 층으로 구성된 신경망이 있다고 가정해보자.

  • 만약, 활성화 함수가 선형 함수 $h(x) = cx$라면, 각 노드에서 합쳐져 출력된 값들은 계수 c만큼 계속 곱해져 가며 커지기만 할 뿐이다.
  • 즉, $h(x) = cx$를 쓰는 것과 $h(x) = ax \ \ (a = c^3)$를 쓰는 것과 큰 차이가 없는 형태가 된다.
  • 물론, 이는 선형 함수를 절대 써선 안된다는 의미가 아니다. 선형 함수는 녹색 부분인 출력층에서 사용하는 것엔 문제가 없으나, 은닉층(파란색)에서 사용할 경우, 층이 쌓이는 것에 의미가 사라지기 때문에 사용하지 않는 것이 좋다.

 

2. 입력치에 이상치가 존재하는 경우, 분류를 불가능하게 만든다.

  • 예를 들어, 학습 기간에 따른 합격 여부를 나눈다고 해보자.
  • 공부 기간이 4일 이하인 경우, 불합격이 차지하는 비중이 많았고, 5일 이상에서 합격이 차지하는 비중이 훨씬 많았다. 그렇다면, 신경망은 4일을 기준으로 해서 합격 불합격 여부를 나누려고 학습을 할 것이다.
  • 그러나, 만약, 누군가가 지나치게 공부를 오래하여, 1달 동안 공부를 해버렸다고 해보자. 이 경우 신경망은 어디를 기준으로 분류를 해야 할지 헷갈리게 된다.

 

 반대로 말하자면, 활성화 함수로 선형 함수를 사용하는 것은, 만약 다층 신경망이 아니거나, 입력치에 이상치가 없거나, 이상치를 조정하여 데이터에서 이상치가 존재하지 않는 형태로 만들었다면, 선형 함수를 쓰는 것에 큰 문제가 없다.

 

 

 

 

 지금까지 활성화 함수에서 가장 기초가 되는 계단 함수와 선형 함수에 대해 알아보았다. 선형 함수도 사용은 가능하나, 제한적으로 사용 가능하며, 다층 신경망에서 사용 시, 은닉층에서 사용해서는 안되므로, 주의해서 사용하길 바란다.

 다음 포스트에서는 분류를 할 때, 가장 많이 사용되는 두 활성화 함수인 Sigmoid, Softmax에 대해 알아보겠다.

728x90
반응형
728x90
반응형

 지난 포스트에서 퍼셉트론을 이용해 대표적인 논리 게이트인 AND 게이트, NAND 게이트, OR 게이트를 구현해보았다. 이번 포스트에선 또 다른 대표적인 논리 게이트인 XOR 게이트를 구현해보자.

 

 

1. XOR 게이트

  • XOR 게이트는 배타적(자기 자신을 제외하고 나머지는 거부한다.) 논리합이라는 회로로, 변수 중 단 하나만 True(참 = 1) 일 때, True(=1)을 반환한다.
  • XOR 게이트의 진리표는 다음과 같다.
0 0 0
0 1 1
1 0 1
1 1 0
  • 자, 위 진리표를 구현할 수 있는 가중치($w_1, w_2, b$)를 찾을 수 있는가?

$$ y = \begin{cases}
 0, \ \ (w_1*0 + w_2*0 + b \leq 0) \\ 
 1, \ \ (w_1*0 + w_2*1 + b \leq 0) \\ 
 1, \ \ (w_1*1 + w_2*0 + b \leq 0) \\ 
 0, \ \ (w_1*1 + w_2*1 + b > 0) 
\end{cases} $$

  • 위 공식에 들어 맞는 가중치를 찾는 것은 불가능하다.
  • 그 이유는 앞서 말한 퍼셉트론 역시 회귀분석과 마찬가지로 선형성을 이용해서 0보다 크고, 작은 지를 구분해내기 때문이다.
  • 위 말을 이해하기 쉽도록 위 진리표를 시각적으로 보자.
>>> import matplotlib.pyplot as plt

# 캔버스 설정
>>> fig = plt.figure(figsize=(8,8)) # 캔버스 생성
>>> fig.set_facecolor('white')      # 캔버스 색상 설정

>>> plt.title('XOR Gate, Visualization of True table', fontsize = 25)
>>> plt.ylim(-1, 4)
>>> plt.xlim(-1, 4)
>>> plt.axhline(color = 'k', alpha = 0.5)
>>> plt.axvline(color = 'k', alpha = 0.5)
>>> plt.xlabel("x1", fontsize = 20)
>>> plt.ylabel("x2", fontsize = 20, rotation = 0)

>>> plt.scatter([0, 1], [0, 1], s = 200, c = "green", marker="v")
>>> plt.scatter([0, 1], [1, 0], s = 200, c = "blue", marker="s")

>>> plt.show()

  • 위에서 퍼셉트론의 구분 방법은 선형성을 이용한다고 했다.
  • 선형성을 이용한다는 소리는 위 그래프에서 선 하나(퍼셉트론)로 파란 네모 점과 녹색 세모 점을 구분할 수 있어야 한다는 소리다.
  • 직선이 아닌 곡선을 사용하지 않는한, 그 어떠한 방법으로도 하나의 직선으로는 위 점을 서로 다른 2개의 집단으로 구분해낼 수 없다.
  • 즉, 퍼셉트론 하나로는 논리 게이트의 구현이 불가능하다는 것이고, 이는 퍼셉트론 하나로는 컴퓨터 같은 고등 연산을 수행할 수 없다는 것이다!

 

 

 

 

2. XOR 게이트의 해결 방법

 머리가 비상하게 좋은 친구라면(블로그 주인장은 그렇지 못하지만...), "앞서 만들었던 논리 게이트를 조합해서 이 문제를 해결할 수 있지 않을까?"라는 생각이 들지도 모른다.

 우리는 앞에서 AND, NAND, OR 게이트를 만들어보았고, 이들을 조합해서 문제를 해결할 수는 없을까?

  • 위 그림을 보면, NAND, OR, AND 논리 게이트를 조합하면 우리가 위에서 만들었던 XOR 진리표의 결과를 출력하는 것을 알 수 있다.
  • $x_1, x_2$는 일정하며, 어떤 논리 게이트를 결정하느냐에 따라 가중치($w_1, w_2, b$)만 바뀐다.
  • 반대로 말하면, 가중치만 다른 퍼셉트론을 조합해서 내가 원하는 기능을 얻을 수 있다는 것이다.
  • 자, 위 그림을 코드로 구현해보자. 이번에는 조금 더 코드 친화적으로 Numpy의 array를 이용해서 짜 보겠다.
>>> import numpy as np

>>> def step_function(x):
    
>>>     y = x > 0
    
>>>     return y.astype(np.int) 
        

>>> def Perceptron(x, dict_name, gate_name):

>>>     weight = dict_name[gate_name]
>>>     y = np.sum(x*weight['w']) + weight["b"]

>>>     return step_function(y)


>>> def XOR_gate(x):

>>>     array_x = np.array(x)
>>>     dict_W = {"AND":{"w":[0.5,0.5], "b":-0.7},
>>>               "NAND":{"w":[-0.5,-0.5], "b":0.7},
>>>               "OR":{"w":[0.5,0.5], "b":-0.2}}

>>>     y1 = Perceptron(array_x, dict_W, "NAND")
>>>     y2 = Perceptron(array_x, dict_W, "OR")
    
>>>     X = np.array([y1, y2])
>>>     Y = Perceptron(X, dict_W, "AND")
    
>>>     return step_function(Y)
>>> print("XOR Gate")
>>> print("----"*20)
>>> print("(0,0):", XOR_gate([0,0]))
>>> print("(0,1):", XOR_gate([0,1]))
>>> print("(1,0):", XOR_gate([1,0]))
>>> print("(1,1):", XOR_gate([1,1]))

XOR Gate
--------------------------------------------------------------------------------
(0,0): 0
(0,1): 1
(1,0): 1
(1,1): 0
  • 위 코드에서 새롭게 ster_function()이라는 함수가 생겼는데, 이는 계단 함수로, 앞서 우리가 if문을 이용해서 합산된 결과가 0보다 클 때, 1을 반환하고, 0보다 작거나 같으면, 0을 반환하던 부분이다.
  • 이를 활성화 함수(Activation Function)라 하는데, 활성화 함수를 통해 합산된 값을 출력할지, 출력한다면 어떻게 출력할지를 결정한다.
  • 계단 함수는 이후 활성화 함수를 자세히 다룰 때, 다시 이야기하도록 하겠다.
  • 위 코드를 보면, 오로지 가중치만 다른 퍼셉트론을 겹쳐서 사용했는데, XOR 게이트 문제를 해결한 것을 알 수 있다.
  • 이렇게 2개 이상의 퍼셉트론을 쌓는 것을 다층 퍼셉트론이라고 한다.

 

 

 

 

3. 논리를 비약시켜보자.

  • 위에서 우리는 2개 이상의 층(Layer)을 쌓아, 우리가 원하는 문제를 해결하였다.
  • 이 곳에 사용된 층들은 오로지 가중치만 다르며, 이 가중치가 갖는 의미도 꽤 다르다.
  • $w_1, w_2$ 같은 가중치는, 각 입력 신호에 부여되는 영향력(중요도)을 조절한다.
  • $\theta$는 임계점이라 하였는데, 좌변으로 이항 시켜, $b$로 만들어주었고, 이는 편향(bias)라고 한다. 편향은 뉴런이 얼마나 쉽게 활성화되느냐에 영향을 미치며, $b$의 크기에 따라 활성화 함수가 작동하는지, 마는지가 결정된다.
  • 지금까지 우리가 학습한 내용을 단순화시켜보면, 층을 많이 쌓고, 가중치를 맞게 설정해주면, 내가 원하는 결과를 얻을 수 있다는 것이다.
  • 층을 많이 쌓는다는 것이 바로 우리가 아는 딥러닝(Deep Learning)의 딥(Deep)을 의미하며, 층이 많이 쌓인 신경망은 각 노드에서 다음 노드로 이동하는 신호에 부여하는 가중치를 다르게만 한다면, 구체적으로 함수 식을 모른다 할지라도 우리가 원하는 결과를 알 수 있다는 것이다.
  • 그런데, 만약 그 가중치를 컴퓨터가 알아서 찾을 수 있다면 어떨까?
  • 여기서 조금 어려워질지도 모르겠는데, 기계가 어떠한 방법(손실 함수)을 이용해서 가장 적합한 가중치를 찾아낸다면, 우리가 스스로 공부를 해서 어떤 결과를 도출하는 것처럼 컴퓨터도 적합한 가중치를 찾기 위한 활동, 즉! 학습과 동일하게 보이는 행동을 통해 데이터만 가지고 원하는 결과를 도출할 수 있다는 것이다.
  • 여기서 "기계가 학습한다(Machine Learning)"이라는 말이 나오게 되는 것이며, 학습을 하되(가중치를 찾는 과정) 그것을 다층 레이어를 통해 찾아내는 것을 딥러닝(Deep Learning)이라 하게 되는 것이다.

 

 

 

 이번 포스트에선 퍼셉트론을 이용해 머신러닝이라는 단어와 딥러닝이라는 단어가 어떻게 만들어졌는지를 학습해보았다. 다음 포스트에서는 짧게 짚고 넘어갔던 활성화 함수에 대해 학습해보자.

728x90
반응형
728x90
반응형

 지난 포스트에서 기초적인 파이썬 코드를 사용하여, 퍼셉트론을 구현해보았다. 이번 포스트에서는 지난번에 생성한 퍼셉트론을 사용해서 논리 회로를 적용해보겠다.

 

 

논리회로(Logical circuit)

 1937년 클로드 섀넌(Claude Shannon)이 개발한 논리 회로(Logical Circuit)는 불 대수(Boolean algebra)를 물리적으로 구현한 것으로, 하나 이상의 논리적 입력값(True / False)이 들어가면, 그에 맞는 논리 연산(And / Or 등)을 수행하여 하나의 논리적 출력 값(True / False)을 얻는 전자 회로다.

 논리 회로는 다양한 불 대수의 조합을 통해 다양한 기능을 수행할 수 있는데, 이를 이용해서 컴퓨터 같은 고등 연산이 가능한 기계를 만들어 낼 수도 있다.

 여기서 AND, NOT, OR, XOR 등과 같은 기본이 되는 논리 연산을 수행하는 것을 논리 게이트(Logical gate)라 한다.

 만약, 앞에서 만들었던 퍼셉트론이 논리 게이트에 대해서도 적용 가능하다면, 퍼셉트론을 이용해서 컴퓨터도 만들 수 있지 않을까?

 

 

 

논리 게이트

1. AND 게이트

  • AND 게이트는 우리가 코드를 짤 때, 익숙한 논리 연산자 AND와 같으며, 이는 집합에서 교집합에 해당한다. 교집합은 두 집합이 모두 참(True)일 경우, 참(True)을 반환하는 것이다.
  • 두 집합에 대한 진리표를 만들어, 이를 보다 쉽게 이해해보자.
$x_1$ $x_2$ $y$
0 0 0
0 1 0
1 0 0
1 1 1
  • 위 표를 볼 때, 1은 True(참)이라 생각하고, 0은 False(거짓)이라고 생각해보자.
  • 위 표를 퍼셉트론의 공식에 맞게 고쳐보자.

$$ y = \begin{cases}
 0, \ \ \ w_1*0 + w_2*0 \leq \theta \\ 
 0, \ \ \ w_1*0 + w_2*1 \leq \theta \\ 
 0, \ \ \ w_1*1 + w_2*0 \leq \theta \\ 
 1, \ \ \ w_1*1 + w_2*1 > \theta  
\end{cases} $$

  • 위 공식을 보면, 변수인 가중치($w_1, w_2$)와 임계값($\theta$)을 어떻게 설정하느냐로 AND 게이트를 구현할 수 있다는 것을 알 수 있다.
  • 위 공식을 참으로 만드는 가중치와 임계값을 설정하여, AND 게이트를 구현해보자
# Perceptron
>>> def Perceptron(x1, x2, w1, w2, theta):
    
>>>     y = w1*x1 + w2*x2
    
>>>     if y <= theta:
>>>         return 0
>>>     elif y > theta:
>>>         return 1
    
    
# AND Gate를 구현해보자.
>>> w1, w2, theta = 0.5, 0.5, 0.8

>>> print("AND Gate")
>>> print("----"*20)
>>> print("(0, 0):", Perceptron(x1=0, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(0, 1):", Perceptron(x1=0, x2=1, w1=w1, w2=w2, theta=theta))
>>> print("(1, 0):", Perceptron(x1=1, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(1, 1):", Perceptron(x1=1, x2=1, w1=w1, w2=w2, theta=theta))

AND Gate
--------------------------------------------------------------------------------
(0, 0): 0
(0, 1): 0
(1, 0): 0
(1, 1): 1
  • 위 코드를 보면, 가중치와 임계값을 0.5로 정했을 뿐인데, AND Gate와 동일한 결과가 나온 것을 알 수 있다.

 

 

 

2. NAND 게이트

  • 이번에는 AND 게이트의 반대인 NOT AND 게이트인 NAND 게이트를 구현해보자.
  • NAND 게이트는 AND 게이트의 반대이므로, 진리표는 다음과 같다.
$x_1$ $x_2$ $y$
0 0 1
0 1 1
1 0 1
1 1 0
# NAND Gate를 구현해보자.
>>> w1, w2, theta = -0.5, -0.5, -0.8

>>> print("NAND Gate")
>>> print("----"*20)
>>> print("(0, 0):", Perceptron(x1=0, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(0, 1):", Perceptron(x1=0, x2=1, w1=w1, w2=w2, theta=theta))
>>> print("(1, 0):", Perceptron(x1=1, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(1, 1):", Perceptron(x1=1, x2=1, w1=w1, w2=w2, theta=theta))

NAND Gate
--------------------------------------------------------------------------------
(0, 0): 1
(0, 1): 1
(1, 0): 1
(1, 1): 0
  • NAND Gate는 AND Gate의 반대이므로, 가중치와 임계값을 모두 역수로 만들어주었다.

 

 

 

3. OR 게이트

  • 다음은 또 다른 기본 논리 게이트 중 하나인 OR 게이트를 구현해보자.
  • OR은 집합의 합집합에 해당하며, 둘 중 하나라도 True인 경우 True를 반환한다.
$x_1$ $x_2$ $y$
0 0 0
0 1 1
1 0 1
1 1 1
# OR Gate를 구현해보자.
>>> w1, w2, theta = 0.5, 0.5, 0.2

>>> print("OR Gate")
>>> print("----"*20)
>>> print("(0, 0):", Perceptron(x1=0, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(0, 1):", Perceptron(x1=0, x2=1, w1=w1, w2=w2, theta=theta))
>>> print("(1, 0):", Perceptron(x1=1, x2=0, w1=w1, w2=w2, theta=theta))
>>> print("(1, 1):", Perceptron(x1=1, x2=1, w1=w1, w2=w2, theta=theta))

OR Gate
--------------------------------------------------------------------------------
(0, 0): 0
(0, 1): 1
(1, 0): 1
(1, 1): 1
  • OR 게이트도 오로지 가중치만 바꾸었는데, 우리가 원하는 값을 반환한 것을 볼 수 있다!

 

 

 

4. 퍼셉트론 공식 정리 및 코드 체계화

  • 지금까지 퍼셉트론 코드를 짜보며, 이 코드를 보다 쉽게 바꿀 수 있다는 생각이 들지 않는가?
  • 먼저 퍼셉트론에 사용된 공식에서 임계값 $\theta$를 왼쪽으로 이동시켜, 절편으로 만들면 어떨까?

$$ y = \begin{cases}
 0, \ \ \  (w_1x_1 + w_2x_2 + b \leq 0)  \\ 
 1, \ \ \  (w_1x_1 + w_2x_2 + b > 0) 
\end{cases} $$

  • 좌변으로 이동하면서 $-\theta$로 부호가 음수로 바뀌었는데, 보기에 깔끔하지 않으니 부호가 양수인 $b$라는 절편으로 만들어보았다.
  • 그런데 위 공식을 보면, 어디서 많이 본 공식과 굉장히 유사하지 않은가? 그렇다. 회귀식과 퍼셉트론의 공식은 동일하며, 이로써 퍼셉트론도 선형성을 따지는 것임을 알 수 있다.
  • 위 내용을 코드에 반영하면서, 동시에 퍼셉트론으로 AND 게이트, NAND 게이트, OR 게이트를 구현할 때, 오로지 가중치만을 바꿨는데, 이를 보다 깔끔한 코드로 구현해보자.
>>> class Logical_gate:
    
>>>     def __init__(self, weight_dict):
        
>>>         self.weight = weight_dict


>>>     def Perceptron(self, x1, x2, key):

>>>         weight = self.weight[key]
>>>         w = weight["w"]
>>>         b = weight["b"]

>>>         y = w[0]*x1 + w[1]*x2 + b

>>>         if y <= 0:
>>>             return 0
>>>         elif y > 0:
>>>             return 1
        
>>>     def Run_Gate(self, key):

>>>         print(key + " Gate")
>>>         print("----"*20)
>>>         print("(0, 0):", self.Perceptron(0, 0, key))
>>>         print("(0, 1):", self.Perceptron(0, 1, key))
>>>         print("(1, 0):", self.Perceptron(1, 0, key))
>>>         print("(1, 1):", self.Perceptron(1, 1, key))
>>>         print("----"*20)
>>>         print("\n")
>>> weight_dict = {"AND":{"w":[0.5,0.5], "b":-0.5},
>>>                     "NAND":{"w":[-0.5,-0.5], "b":0.5},
>>>                     "OR":{"w":[0.5,0.5], "b":-0.2}}

>>> LG = Logical_gate()

>>> LG.Run_Gate("AND")
>>> LG.Run_Gate("NAND")
>>> LG.Run_Gate("OR")

AND Gate
--------------------------------------------------------------------------------
(0, 0): 0
(0, 1): 0
(1, 0): 0
(1, 1): 1
--------------------------------------------------------------------------------


NAND Gate
--------------------------------------------------------------------------------
(0, 0): 1
(0, 1): 0
(1, 0): 0
(1, 1): 0
--------------------------------------------------------------------------------


OR Gate
--------------------------------------------------------------------------------
(0, 0): 0
(0, 1): 1
(1, 0): 1
(1, 1): 1
--------------------------------------------------------------------------------
  • 위 코드는 기존의 퍼셉트론 코드에 비해 더 어려워 보이긴 하지만, 일단 만들어 놓으면, 사용하기는 훨씬 쉽다.
  • 새로운 논리 게이트를 구현하고자 하면, 딕셔너리인 weight_dict에 새로운 가중치($w_1, w_2, b$ 모두를 앞으로 단순하게 가중치로 부르겠다.)만 담으면 끝이다.

 

 

 

 지금까지 퍼셉트론을 이용해서 AND, NAND, OR 게이트를 가중치만 바꿔서 구현한 것을 살펴보았는데, 지금까지만 보면, 퍼셉트론이 논리 게이트에서도 잘 작동하는 것으로 보일 것이다.

 그러나, 앞서 말했듯 퍼셉트론도 선형성을 기반으로 결과를 도출하고, 그 결과가 활성화 함수인 계단 함수(이 부분은 뒤에서 자세히 다루겠다.)를 통해 출력되었는데, 이에 대한 반례가 존재한다.

 다음 포스트에서는 퍼셉트론의 논리 게이트 적용에서 반례인 XOR 게이트에 대해 학습해보고, 어째서 그런 문제가 발생하는지, 그리고 해결 방안은 무엇인지에 대해 학습해 보겠다.

728x90
반응형
728x90
반응형

 지난 포스트의 머신러닝, 딥러닝에 대한 설명이 잘 와 닿지 않았을 수 있다. 그러나 퍼셉트론(Perceptron)에 대해 학습해보면, 어떤 과정을 통해서 머신러닝이 이루어지고, 기계 학습이라는 단어의 학습이 정확히 무엇을 의미하는지 알 수 있을 것이다.

 

 

1. 신경 세포

 머신러닝 공부를 해보면 신경망(Neural Network)라는 단어를 종종 볼 수 있었을 것이다. 그리고 머신러닝에 사용되는 인공신경망이 사람의 신경을 흉내 내어 만들어졌다는 글도 나오는데, 대체 이게 무슨 소리일까?

ZUM 학습백과. 뉴런의 구조(http://study.zum.com/book/11779)

  • 위 그림은 인간의 신경 세포인 뉴런이다.
  • 뉴런은 가지돌기(수상돌기)에 여러 신호가 도착하면 신경 세포체에서 이를 합치고, 합쳐진 신호가 특정 임계 값을 넘으면, 출력 신호가 생성되어 축삭 돌기를 통해 다음 뉴런으로 신호가 전달된다.
  • 여기서 중요한 포인트는 다음과 같다.
    • A. 여러 신호가 신경 세포체로 전달된다.
    • B. 전달된 신호는 하나로 합쳐진다.
    • C. 합쳐진 신호가 특정 임계값을 넘으면, 전달된다.
  • 나중에 다룰 이야기지만, 위 이야기에서 정보가 하나로 합쳐져서 전달되는 과정이 활성화함수(Activation function)를 나타내는 부분이라고 생각하자(이 건 나중에 다시 이야기하니깐 대충 넘기자)

 

 

 

 

2. 퍼셉트론(Perceptron)

  • 퍼셉트론은 위에서 나온 신경 세포를 알고리즘화한 것으로, 딥러닝의 기본이 되는 개념이다.
  • 퍼셉트론은 N개의 입력 받아 1개의 신호를 출력한다.
  • 퍼셉트론은 받아들여 합친 신호가 임계값보다 크면 1을 출력하고, 임계값보다 작으면 0을 출력하여, 정보를 전달하거나 전달하지 않는다.

입력이 2개인 퍼셉트론

  • 위 퍼셉트론 그림에서 원은 노드(Node) 혹은 뉴런(Neuron)이라고 부르며, 선은 엣지(Edge)라고 부른다.
  • 노드에서 다음 노드로 정보가 전해질 때, 가중치($w_1$, $w_2$)가 곱해져 전달된다.
  • 전달되는 값과 가중치가 곱해진 값($w_1x_1$)의 합($w_1x_1 + w_2x_2$)이 임계값 $\theta$(theta)보다 크면 1이, 작으면 0이 출력되는데, 이는 위에서 언급한 신경세포에서 신호가 전달되는 과정을 그대로 따라한 것과 같다.
  • 이를 수식으로 나타내면 다음과 같다.

$$y = \begin{cases}
 0 \ \ \ (w_1x_1 + w_2x_2 \leq \theta) \\ 
 1 \ \ \ (w_1x_1 + w_2x_2 > \theta)
\end{cases}$$

  • 위 함수를 보면, 위 신경세포에서 설명한 신호가 움직이는 과정을 쉽게 표현한 것임을 알 수 있다.

 

 

 

3. 퍼셉트론의 구현

  • 앞서 입력 노드가 2개인 퍼셉트론의 함수를 적어보았다. 만약, 위 함수를 파이썬 코드로 구현한다면, 그것이 바로 퍼셉트론을 구현하는 것이 아니겠는가. 한 번 만들어보도록 하자.
# Perceptron 함수를 코드로 구현해보자
>>> def Perceptron(x1, x2, w1, w2, theta):

>>>     y = w1*x1 + w2*x2
            
>>>     if y <= theta:
>>>         return 0
                
>>>     elif y > theta:
>>>         return 1
  • $x_1$과 $x_2$는 퍼셉트론의 노드이므로, 0과 1의 값만 나올 수 있다.
  • $x_1=0,\ x_2=1,\ w_1=0.1,\ w_2=0.3,\ theta=0.6$으로 함수를 실행해보자 
>>> Perceptron(x1=0, x2=1, w1=0.1, w2=0.3, theta=0.6)
0
  • 위 결과를 보면, 0*0.1 + 1*0.3 <= 0.6으로 나와 0이 반환된 것을 알 수 있다.
  • 이를 보면, 가중치는 해당 노드에서 전달되는 정보의 중요도를 어느 정도나 강하게 줄 것인지를 따지는 것이고, 임계값은 받아들인 신호를 얼마나 타이트하게 평가할 것인가를 판단하는 수단이라는 것을 알 수 있다.

 

 

 지금까지 신경망의 기본이 되는 신경 세포, 퍼셉트론의 기본 개념에 대해 학습해보았다. 다음 학습에선 퍼셉트론을 이용하여 단순한 논리 회로를 적용하는 방법에 대해 학습해보도록 하겠다.

 

 

 해당 포스팅은 사이토 고키의 "밑바닥부터 시작하는 딥러닝"책을 참고하여 작성하였다. 해당 책은 꽤 얇고 내용도 이해하기 쉽게 써놨으므로, 머신러닝을 시작하는 초보가 읽기에 괜찮은 책이다. 시간 날 때, 한 번 읽어보기 바란다(참고로 PPL이 아니다! 내 돈 주고 내가 산 괜찮은 책 소개다!).

728x90
반응형

+ Recent posts