AIFFEL(220214)_활성화 함수의 이해

NODE : 26. 활성화 함수의 이해

📖 활성화 함수

활성화 함수란 무엇일까? "어떤 것이 활성화(activated)되었다" 라는 것을 들으면 어떤 것이 떠오르나?
넌 나의 함정 카드를 발동(활성화) 시켰다! 라는, 게임에서 종종 나오는 아래 사진과 같은 표현이 있다.

특정 행동을 함 → 특정 조건을 만족 → 함정 카드 발동 이라는 일련의 사건을 뜻하는데, 이처럼 활성화(activated) or 비활성화(deactivated)라는 것은 '어떤 조건을 만족 or 불만족했다'라는 것과 긴밀한 연관이 있다.

우리가 지금까지 써왔던 신경망 속의 퍼셉트론(perceptron) 혹은 노드(node)도 '특정 조건'이 만족하면 '활성화' 되도록 디자인되어 있다. 노드에 입력으로 들어오는 값이 어떤 '임계치'를 넘어가면 "활성화(activated)"되고, 넘어가지 않으면 "비활성화(deactivated)"되게끔 코딩이 되어있는 것이다. 익숙하게 써왔던 ReLU 함수는 이러한 관점에서 어떻게 작용하고 있을까?

ReLU는 입력값이 음수라면, 즉 0 미만이라는 조건을 만족한다면 0을 출력하고, 입력값이 0이상이면 입력값 그대로를 출력하도록 디자인되어 있다. 이를 "활성화 or 비활성화"로 표현하려면 우선 활성화되는 기준을 정해야 한다. 만일 출력값이 0보다 큰 경우를 활성화되었다고 정의한다면, ReLU함수란, "0 미만인 경우는 비활성화가 되고 0 이상인 경우는 활성화되는 함수"라고 말할 수 있다.

Q. 활성화 함수 중엔 시그모이드라는 함수가 있다. 출력값이 0.5 이상이면 활성화가 된 것으로 보고, 0.5 미만이면 비활성화된 것으로 정의를 한다면, 시그모이드 함수는 입력값의 어떤 조건으로 활성화되는가?
A. 시그모이드의 입력값이 0 이상일 때 활성화가 되고, 0보다 작을 때 비활성화된다.

시그모이드는 ReLU처럼 명확하고 간단하게 글로 표현하긴 힘들지만, 입력값이 -\infty로 갈수록 0을 출력하고 ++\infty로 갈수록 1을 출력하며, 0일 때는 1/2을 출력하는 함수이다.
수식적으로는 xx를 입력값이라고 할 때, f(x)=11+exp(x)f(x)=\dfrac{1}{1+exp(-x)}

또한 활성화와 비활성화의 조건을 0.5로 잡았으니, 시그모이드는 0 이상일 때 활성화가 되고, 0 보다 작을 때 비활성화된다고 말할 수 있다.

활성화 함수의 기본적 정의는 위와 같지만, 실제로 딥러닝에서 활성화 함수를 쓰는 결정적 이유는 따로 있다. 바로 딥러닝 모델의 표현력을 향상시켜주기 위해서이다. 전문적인 용어로는 모델의 representation capacity 또는 expressivity를 향상시킨다라고도 말한다.

활성화 함수는 모델의 표현력을 왜 향상시켜줄까? 답은 간단하다. 만일 어떤 모델이 w1w_1

📖 퍼셉트론

활성화 함수라는 아이디어는 어디서부터 비롯되었는지 알아보자.

퍼셉트론


익히 알고 있는 딥러닝 모델은 보통 여러 개의 층으로 이루어져 있다. 그중에 하나의 층을 가져와 다시 쪼갠다면 보통 '노드'라고 불리는 것으로 쪼개지게 되는데, 이것이 바로 퍼셉트론(Perceptron)이다. 반대로 퍼셉트론을 쌓으면 단층 퍼셉트론이 되고, 이 단층 퍼셉트론을 쌓으면 다층 퍼셉트론이 된다. 즉, 우리가 알고 있는 딥러닝 모델은 사실 퍼셉트론들이 다양한 구조로 쌓인 것이다.

그렇다면 퍼셉트론은 어떻게 나온 것일까?

우리의 목표는 컴퓨터를 학습시키는 것이다. 그런데 어떻게 컴퓨터를 학습시킬 수 있을까? 과학자들은 방법을 생각하다가 이미 잘 짜인 머신이라고도 할 수 있는 동물의 학습 방법을 모방하기로 한다. 이를 위해서 학습시킬 머신 자체를 동물의 신경세포와 유사하게 설계해 나오게 된 것이 바로 최초의 퍼셉트론이다.

신경세포의 구조


그럼 퍼셉트론의 구조를 보기 전에 간단히 신경세포(Nueron)에 대해 잠시 살펴보자.

신경세포는 크게 세포체(Soma), 가지돌기(Dendrite), 축삭돌기(Axon), 시냅스(Synapse)로 구성되어 있다. (물론 해부학적으로 더 세분화해서 나눌 수 있겠지만 여기서는 퍼셉트론과 대응되는 구성요소만 다룬다.)

  • 가지돌기 : 세포로 전달되는 신호를 받아들이는 부분
  • 축삭돌기 : 세포에서 다른 세포로 신호를 전달하는 부분
  • 시냅스 : 가지돌기와 축삭돌기 사이에 있는 부분으로 신호 전달의 세기를 담당하며 시냅스의 발달 정도에 따라 같은 신호도 강하게 전달되거나 약하게 전달된다. 시냅스는 사용 빈도에 따라 커지거나 작아지며 심지어 시냅스 자체가 사라지기도 한다.
  • 세포체 : 세포체는 각 가지돌기로부터 들어온 신호들로부터 자신의 출력 신호를 만들고 이를 다음 세포에 전송한다. 이때의 출력 신호는 단순히 입력신호들의 합이 아닌 비선형 방식으로 결정된다. 즉, 합이 일정 수준 이하이면 비활성 상태가 되어 신호를 무시하고, 일정 수준을 넘게 되면 활성 상태가 되어 신호를 다음 세포로 전송한다.

퍼셉트론의 구조



앞에서 소개한 신경세포의 구조와 퍼셉트론의 구조를 대응 시켜 소개한다.

퍼셉트론에 주어지는 입력은 신경세포에서 가지돌기로 받아들이는 신호에 해당합한다. 각 입력 항에 곱해지는 가중치는 신경 세포들의 연결 부위에 형성된 시냅스의 발달 정도에 해당한다. xiwix_i w_i

(요즘 사용되는 퍼셉트론의 구조는 처음 발표된 것과 좀 달라졌지만 이름은 '퍼셉트론' 그대로 사용하고 있다.)

활성화 함수


앞서 확인할 수 있듯이 활성화 함수는 신경 세포로 치면 세포체에서 일어나는 일을 맡고 있다. 따라서 하는 일도 비슷하다. 들어온 신호가 특정 임계점을 넘으면 출력을 하고, 넘지 못하면 무시를 한다. 활성화 함수는 신호를 '전달' 해주기 때문에 Transfer function으로도 알려져 있다.

활성화 함수는 그 표현에 따라

  • 선형 활성화 함수(Linear activation function)
  • 비선형 활성화 함수(Non-linear activation function)

로 나눌 수 있다.

딥러닝에서는 일반적으로 비선형 활성화 함수를 사용한다고 한다. 그럼 선형 활성화 함수는 왜 딥러닝에서 사용되지 않는 걸까?

📖 선형과 비선형

선형(Linear)


영상에서 알 수 있듯이 선형 변환이란 '선형'이라는 규칙을 지키며 VV 공간상의 벡터를 WW 공간상의 벡터로 바꿔주는 역할을 한다.


먼저 선형 변환(linear transformation)이 어떤 것인지 정의하도록 한다.

VVWW가 어떤 (1)^{(1)}

간단히 'T\mathcal{T}선형(linear)이다.' 라고 하기도 한다.
T\mathcal{T}가 선형이라면 다음과 같은 성질을 가진다.

T(0)=0T(0)=0

📖 비선형 함수를 쓰는 이유_(1)

비선형(Non-linear)


그렇다면 비선형은 뭘까? 선형이 아닌 함수를 비선형(Non-linear) 함수라고 한다.

아래 함수 f(x)f(x)들을 살펴보고, 다음 질문에 답해 보자.

1) f(x)=3xf(x)=3x

Q. 위 함수 3가지는 각각 선형함수인가, 비선형함수인가?
A. 1) 선형 2) 비선형 3) 선형

퀴즈 상세 설명


1) 어떤 실수 xx,yy,cc가 있다고 할 때, f(cx+y)=3(cx+y)f(cx+y)=3(cx+y)

2) 어떤 실수 xx,yy,cc가 있다고 할 때, f(cx+y)=(cx+y)2f(cx+y)=(cx+y)^2

3) 주어진 식을 벡터의 형태로 다음과 같이 표현할 수 있다.

f(x)=θ0x0+θ1x1=[θ0θ1][x0x1]=θxf(x)=θ_0x_0+θ_1x_1 =[θ_0θ_1] * [x_0x_1] = θx

어떤 벡터 x,yR2x,y∈R^2

f(cx+y)=f([cx0+y0cx1+y1])=[θ0θ1][cx0+y0cx1+y1]=θ0(cx0+y0)+θ1(cx1+y1)f(cx+y) = f([cx_0+y_0 cx_1+y_1]) = [θ_0 θ_1] * [cx_0+y_0 cx_1+y_1] = θ_0(cx_0+y_0)+θ_1(cx_1+y_1)

이므로 ff는 선형이다.

비선형 함수를 쓰는 이유


그렇다면 왜 딥러닝에서는 비선형 활성화 함수를 주로 사용할까?

앞서 Introduction에서 잠시 언급했듯, 한 문장으로 요약하자면, "딥러닝 모델의 표현력을 향상시키기 위해서"이다.

  • 그럼 선형 활성화 함수를 사용하면 왜 표현력이 떨어지게 되는 걸까?
  • 레이어를 충분히 쌓는다면 선형 활성화 함수를 사용한 모델의 표현력을 향상시킬 수 있지 않을까?

간단한 예시를 통해 알아가보도록 하자.

위 그림과 같이 퍼셉트론 3개로 구성된 모델이 있다고 가정한다.

입력값 xx가 모델을 거치면 출력값 yy가 된다. (여기서 입력값 xx와 출력값 yy는 스칼라값이고
ff는 활성화 함수이다.)

수식으로 표현하면 다음과 같습니다.

y=f(w3f(w2f(w1x)))y=f(w_3f(w_2f(w_1x)))

여기서 wiw_i

이때 만약 ff가 선형이라고 한다면 무슨 일이 일어날까?

ff가 선형이기 때문에 선형 함수의 정의에 의해 f(w1x)=w1f(x)f(w_1x) = w_1f(x)

y=f(w3f(w2f(w1x)))=f(w3f(f(w1w2x)))=f(f(f(w1w2w3x)))=f(f(f(Wx)))y = f(w_3f(w_2f(w_1x))) = f(w_3f(f(w_1w_2x))) = f(f(f(w_1w_2w_3x))) = f(f(f(Wx)))

여기서 W=w1w2w3W=w_1w_2w_3

간단하게 예를 들어 본다.

w1w2w3w_1w_2w_3

y=f(w3f(w2f(w1x)))y=f(w′_3f(w′_2f(w′_1x)))

함수 ff가 선형인 것을 이용해 식을 다음과 같이 바꾸어 본다.

y=f(f(f(Wx)))y=f(f(f(Wx)))

여기서 W=w1w2w3W = w′_1w′_2w′_3

즉, w2w_2

그럼 나가아서 f(f(f(Wx)))f(f(f(Wx)))f(Wx)f*(Wx)

이렇게 하기 위해선 그냥 ff함수 3개를 하나의 합성함수로 만들어 주면 된다. 그런데 선형 함수들의 합성함수도 선형일까?

선형이다.

선형 변환의 합성함수에 관한 정리


V,WV, W그리고 ZZ가 실수 공간상의 벡터 공간이고, 함수 T:VW\mathcal{T} : V \rightarrow W

증명)

x,yVx, y \in V

UT(ax+y)=U(T(ax+y))=U(aT(x)+T(y))=aU(T(x))+U(T(y))=aUT(x)+UT(y)\begin{aligned} \mathcal{UT}(ax+y) & = \mathcal{U}(\mathcal{T}(ax+y)) \\ & = \mathcal{U}(a\mathcal{T}(x)+\mathcal{T}(y)) \\ & = a\mathcal{U}(\mathcal{T}(x)) + \mathcal{U}(\mathcal{T}(y)) \\ & = a\mathcal{U}\mathcal{T}(x) + \mathcal{U}\mathcal{T}(y) \end{aligned}

이므로, 선형의 성질에 의해 UT\mathcal{UT}도 선형입니다.

이제 선형함수의 합성함수 또한 선형이라는 것을 알았다. 이 정리에 의해 이제 f(f(f(Wx)))f(f(f(W x)))f(Wx)f^\star(W x)로 표현할 수 있다.

이것의 의미는 무엇일까?

바로 3개의 노드를 1개로 줄여서 표현을 해도 결과가 달라지지 않는다는 것이다.

📖 비선형 함수를 쓰는 이유_(2)

결론적으로, 선형 활성화 함수를 사용한다면, 노드의 개수를 아무리 많이 붙여도 결국 하나의 노드를 사용하는 것과 차이가 없다.

그럼 위에서 노드 3개를 사용한 모델은 결국 다음과 같이 단순한 식으로 표현이 된다.

y=model(x)=f(wx)=awxy=model(x)=f(wx)=awx

다시 말해, 선형 활성화 함수를 사용한다면, 모델의 표현력이 떨어지게 된다. 그럼 단일 노드가 아닌 layer들로 구성되어 있는 모델에서도 위와 같을까?

지금부터는 Layer들을 쌓아도 활성화 함수가 선형이라면 모델의 표현력은 증가하지 않는다는 것을 증명해 본다. 아래 그림은 input layer가 각각 1개, hidden layer는 2개(왼쪽 그림) or 1개(오른쪽)로 구성된 임의의 모델이다. 빨간색으로 표현된 부분은 활성화 함수이며, 두 모델 모두 선형 활성화 함수라고 본다.

먼저, 각 노드와 연결된 weight와 노드에서 연산된 값(=activation value)을 다음과 같이 표현한다.

위와 같이 하나의 weight에는 양옆에 있는 layer와 연결이 되어있으므로, 3개의 index가 필요하다.

wnl,mw_n^{l,m}

l:layer indexl : layer~index

그리고, 노드에서 연산된 activation값은 2개의 index가 필요하다.

ajl  or  ajl1a_j^l ~~ or ~~ a_j^{l-1}

l:layer indexl : layer~index

그러면 지금부터 모델에서 임의의 노드를 선택하여 연산을 비교해 보겠습니다.

먼저, 왼쪽 모델에서 수행되는 연산은 다음과 같습니다.

2번째 layer의 3번째 node 값 : a32=f(Σixiwi2,3)a_3^2=f(\Sigma_ix_iw_i^{2,3})

(2)번 식의 aj2a_j^2

a13=f(Σjaj2wj3,1)(2)a_1^3=f(\Sigma_ja_j^2w_j^{3,1}) - (2)

aj2=f(Σixiwi2,j)a_j^2=f(\Sigma_ix_iw_i^{2,j})

를 대입하면 다음과 같고,

a13=f(Σjf(Σixiwi2,j)wj3,1)(3)a_1^3=f(\Sigma_jf(\Sigma_ix_iw_i^{2,j})w_j^{3,1}) - (3)

여기서, 이전에 다루었던 "선형 함수의 성질을 이용"하면 (3)식은 다음과 같이 정리된다.

a13=f(f(x1))(Σjw12,jwj3,1)+f(f(x2))(Σjw22,jwj3,1)+...(3)a_1^3=f(f(x_1))(\Sigma_jw_1^{2,j}w_j^{3,1})+f(f(x_2))(\Sigma_jw_2^{2,j}w_j^{3,1})+... - (3)

오른쪽 모델은 1줄이면 끝난다. 오른쪽은 1개의 hidden layer이므로 다음과 같이 표현된다.

a22=f(ΣixiwI2,3)=f(x1)w12,2+f(x2)w22,2+...(4)\begin{aligned} a_2^2&=f(\Sigma_ix_iw_I^{2,3}) \\ &=f(x_1)w_1^{2,2}+f(x_2)w_2^{2,2}+...-(4) \end{aligned}

혹시 식 (3)과 (4)의 공통점이 있다면 무엇이 있을까?

바로 input data의 각 x1,x2,x3,...x_1,x_2,x_3,...

식 (3)과 (4)에서 x1x_1

(3)에 있는 f(f(x1))f(f(x_1))

한편, 식 (3)의 f(f(x1))f(f(x_1))

이러한 결과는 "선형 함수의 성질"로 식 (3)의 각 항들을 풀면서 나타난 성질이다.

이제 앞서 던졌던 질문에 대한 답을 할 수 있게 되었다.

그렇다면..


반면에 다음과 같이 각 layer 마다 비선형 함수가 있다면 어떻게 될까?

기본적인 연산은 위에서 했던 것과 같으므로, 선형 변환하기 전의 과정은 생략하고 다음 그림과 함께 살펴보도록 하자.

📖 활성화 함수의 종류_(1) 이진 계단 함수

활성화 함수는 크게 3가지 종류의 함수로 나눌 수 있다.

  1. 이진 계단 함수(Binary step function)
  2. 선형 활성화 함수(Linear activation function)
  3. 비선형 활성화 함수(Non-linear activation function)

이진 계단 함수


이진 계단 함수(Binary step function)라는 이름이 붙은 이유는 간단하다. 이 함수로 들어온 입력이 특정 임계점을 넘으면 1(혹은 True)를 출력하고 그렇지 않을 때는 0을 출력하기 때문이다. (그래프를 보면 더 명확히 알 수 있다.) 이러한 특성 때문에 이 활성화 함수는 간단한 이진 분류 문제에서 꽤 유용하게 쓰인다. 아래의 식은 임계점이 0일 때의 이진 계단 함수식이다.

f(x)={0  for  x < 01  for  x  0f(x) = \begin{cases} 0~~for~~x~<~0\\ 1~~for~~x~≥~0 \end{cases}
def binary_step(x, threshold=0): 
# threshold가 있는 함수를 쓰면 꼭 defualt 값을 설정해주세요
    return 0 if x<threshold else 1
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np

def plot_and_visulize(image_url, function, derivative=False):
    X = [-10 + x/100 for x in range(2000)]
    y = [function(y) for y in X]
    
    plt.figure(figsize=(12,12))
    
    # 함수 그래프
    plt.subplot(3,2,1)
    plt.title('function')
    plt.plot(X,y)
    
    # 함수의 미분 그래프
    plt.subplot(3,2,2)
    plt.title('derivative')
    if derivative:
        dev_y = [derivative(y) for y in X]
        plt.plot(X,dev_y)
    
    # 무작위 샘플들 분포
    samples = np.random.rand(1000)
    samples -= np.mean(samples)
    plt.subplot(3,2,3)
    plt.title('samples')
    plt.hist(samples,100)
    
    # 활성화 함수를 통과한 샘플들 분포
    act_values = [function(y) for y in samples]
    plt.subplot(3,2,4)
    plt.title('activation values')
    plt.hist(act_values,100)
    
    # 원본 이미지
    image = np.array(Image.open(image_url), dtype=np.float64)[:,:,0]/255. # 구분을 위해 gray-scale해서 확인
    image -= np.median(image)
    plt.subplot(3,2,5)
    plt.title('origin image')
    plt.imshow(image, cmap='gray')
    
    # 활성화 함수를 통과한 이미지
    activation_image = np.zeros(image.shape)
    h, w = image.shape
    for i in range(w):
        for j in range(h):
            activation_image[j][i] += function(image[j][i])
    plt.subplot(3,2,6)
    plt.title('activation results')
    plt.imshow(activation_image, cmap='gray')
    
    return plt
import os
img_path = os.getenv('HOME')+'/aiffel/activation/jindo_dog.jpg'
ax = plot_and_visulize(img_path, binary_step)
ax.show()


이진 계단 함수의 치역(range)은 0,1 (0과 1만 나온다는 뜻)이 된다.

이진 계단 함수는 단층 퍼셉트론(single layer perceptrons)라는 초기의 신경망에서 자주 사용되었다.

# 퍼셉트론
class Perceptron(object):
    def __init__(self, input_size, activation_ftn, threshold=0, learning_rate=0.01):
        self.weights = np.random.randn(input_size)
        self.bias = np.random.randn(1)
        self.activation_ftn = np.vectorize(activation_ftn)
        self.learning_rate = learning_rate
        self.threshold = threshold

    def train(self, training_inputs, labels, epochs=100, verbose=1):
        '''
        verbose : 1-매 에포크 결과 출력, 
                  0-마지막 결과만 출력 
        '''
        for epoch in range(epochs):
            for inputs, label in zip(training_inputs, labels):
                prediction = self.__call__(inputs)
                self.weights += self.learning_rate * (label - prediction) * inputs
                self.bias += self.learning_rate * (label - prediction)
            if verbose == 1:
                pred = self.__call__(training_inputs)
                accuracy = np.sum(pred==labels)/len(pred)
                print(f'{epoch}th epoch, accuracy : {accuracy}')
        if verbose == 0:
            pred = self.__call__(training_inputs)
            accuracy = np.sum(pred==labels)/len(pred)
            print(f'{epoch}th epoch, accuracy : {accuracy}')
    
    def get_weights(self):
        return self.weights, self.bias
                
    def __call__(self, inputs):
        summation = np.dot(inputs, self.weights) + self.bias
        return self.activation_ftn(summation, self.threshold)

이러한 신경망은 선형적으로 구분 가능한(linearly separable) 문제(예를 들면, AND gate 또는 OR gate)를 구현할 수 있다. 다시 말해 하나의 선으로 구분할 수 있는 문제를 풀 수 있다.

def scatter_plot(plt, X, y, threshold = 0, three_d=False):
    ax = plt
    if not three_d:
        area1 = np.ma.masked_where(y <= threshold, y)
        area2 = np.ma.masked_where(y > threshold, y+1)
        ax.scatter(X[:,0], X[:,1], s = area1*10, label='True')
        ax.scatter(X[:,0], X[:,1], s = area2*10, label='False')
        ax.legend()
    else:
        area1 = np.ma.masked_where(y <= threshold, y)
        area2 = np.ma.masked_where(y > threshold, y+1)
        ax.scatter(X[:,0], X[:,1], y-threshold, s = area1, label='True')
        ax.scatter(X[:,0], X[:,1], y-threshold, s = area2, label='False')
        ax.scatter(X[:,0], X[:,1], 0, s = 0.05, label='zero', c='gray')
        ax.legend()
    return ax
# AND gate, OR gate
X = np.array([[0,0], [1,0], [0,1], [1,1]])

plt.figure(figsize=(10,5))
# OR gate
or_y = np.array([x1 | x2 for x1,x2 in X])
ax1 = plt.subplot(1,2,1)
ax1.set_title('OR gate ' + str(or_y))
ax1 = scatter_plot(ax1, X, or_y)

# AND gate
and_y = np.array([x1 & x2 for x1,x2 in X])
ax2 = plt.subplot(1,2,2)
ax2.set_title('AND gate ' + str(and_y))
ax2 = scatter_plot(ax2, X, and_y)

plt.show()


이진 계단 함수의 임계점이 0이라고 하면 위의 gate들은 다음과 같이 단층 퍼셉트론으로 구현할 수 있다.

# OR gate
or_p = Perceptron(input_size=2, activation_ftn=binary_step)
or_p.train(X, or_y, epochs=1000, verbose=0)
print(or_p.get_weights()) # 가중치와 편향값은 훈련마다 달라질 수 있습니다.

# AND gate
and_p = Perceptron(input_size=2, activation_ftn=binary_step)
and_p.train(X, and_y, epochs=1000, verbose=0)
print(and_p.get_weights()) # 가중치와 편향값은 훈련마다 달라질 수 있습니다.
# 실행결과
999th epoch, accuracy : 1.0
(array([0.22882678, 2.55064811]), array([-0.0052415]))
999th epoch, accuracy : 1.0
(array([0.33919254, 0.97595424]), array([-0.98097147]))

OR gate와 AND gate 를 구현한 단층 퍼셉트론 모델의 accuracy가 1.0을 기록하고 있다. 이 정도면 단층 퍼셉트론으로 OR gate와 AND gate 를 충분히 구현해 낼 수 있음을 확인할 수 있다.

그럼 단층 퍼셉트론 모델의 추론 결과를 그래프로 그려 본다. 위에서 그려보았던 그래프가 4개의 점으로 표시된 것에 비해, 아래 그려질 그래프는 x, y축를 100등분한 결과를 모델에 대입하여 True와 False의 경계선이 선형적으로 드러나도록 그려질 것이다. 위에서 언급한 것처럼 퍼셉트론이 하나의 선으로 구분할 수 있는 문제를 풀 수 있다는 것을 시각적으로 확인하기 위해서이다.

from itertools import product

# 그래프로 그려보기
test_X = np.array([[x/100,y/100] for (x,y) in product(range(101),range(101))])
pred_or_y = or_p(test_X)
pred_and_y = and_p(test_X)

plt.figure(figsize=(10,10))
ax1 = plt.subplot(2,2,1)
ax1.set_title('predict OR gate')
ax1 = scatter_plot(ax1, test_X, pred_or_y)

ax2 = plt.subplot(2,2,2, projection='3d')
ax2.set_title('predict OR gate 3D')
ax2 = scatter_plot(ax2, test_X, pred_or_y, three_d=True)

ax3 = plt.subplot(2,2,3)
ax3.set_title('predict AND gate')
ax3 = scatter_plot(ax3, test_X, pred_and_y)

ax4 = plt.subplot(2,2,4, projection='3d')
ax4.set_title('predict AND gate 3D')
ax4 = scatter_plot(ax4, test_X, pred_and_y, three_d=True)

plt.show()

당초 예상했던 것과 그래프의 모양이 다소 다르게 나오는가? 이미 눈치챘겠지만, 이 그래프의 모양은 단층 퍼셉트론을 훈련(train)할 때마다 다르게 그려질 것이다. OR gate와 AND gate의 accuracy를 1.0으로 만드는 퍼셉트론의 최적 가중치 값은 하나로 정해져 있지 않으므로, 따라서 이 가중치에 의해 결정되는 그래프의 모양도 계속 달라진다. 그러나 중요한 것은 퍼셉트론의 가중치가 결정짓는 경계선을 기준으로 [[0,0], [1,0], [0,1], [1,1]] 이상 4개의 점에서의 OR gate, AND gate의 진릿값이 정확한가 하는 것이다.

이진 계단 함수의 한계 1


하지만, XOR gate도 구현할 수 있을까?

# XOR gate
threshold = 0
X = np.array([[0,0], [1,0], [0,1], [1,1]])

plt.figure(figsize=(5,5))
xor_y = np.array([x1 ^ x2 for x1,x2 in X])
plt.title('XOR gate '+ str(xor_y))
plt = scatter_plot(plt, X, xor_y)
plt.show()

# XOR gate가 풀릴까?
xor_p = Perceptron(input_size=2, activation_ftn=binary_step, threshold=threshold)
xor_p.train(X, xor_y, epochs=1000, verbose=0)
print(xor_p.get_weights())

# 그래프로 그려보기
test_X = np.array([[x/100,y/100] for (x,y) in product(range(101),range(101))])
pred_xor_y = xor_p(test_X)

plt.figure(figsize=(10,5))
ax1 = plt.subplot(1,2,1)
ax1.set_title('predict XOR gate?')
ax1 = scatter_plot(ax1, test_X, pred_xor_y)

ax2 = plt.subplot(1,2,2, projection='3d')
ax2.set_title('predict XOR gate 3D?')
ax2 = scatter_plot(ax2, test_X, pred_xor_y, three_d=True)

plt.show()
# 실행결과
999th epoch, accuracy : 0.5
(array([-0.00539602, -0.00795063]), array([0.00542861]))


이번에 나온 accuracy는 무려.... 0.25밖에 안된다. 왜 그럴까?

단층 퍼셉트론은 이 XOR gate를 구현할 수 없다. 왜냐하면 XOR gate의 진릿값 그래프를 하나의 선으로 구분을 할 수 없기 때문이다. 하지만 이를 해결할 수 있는 방법이 있다. 바로 여러 층을 쌓는 것이다. 이렇게 여러 층을 쌓은 모델을 다층 퍼셉트론(multi-layer perceptron, MLP) 이라고 한다.

(여기서 말하는 single layer, multi-layer라고 하는 것은 퍼셉트론을 하나를 쓰냐, 여러 개를 쓰냐를 뜻하는 것으로 현대 인공신경망에서 쓰이고 있는 layer와는 의미가 다르다. 현대 인공신경망에서 쓰이는 단어대로 표현을 하자면 single layer는 노드의 개수가 1개인 layer 하나를 뜻하고, multi-layer는 노드의 개수가 2개 이상인 layer하나를 뜻한다.)

이진 계단 함수의 한계 2


이처럼 층만 쌓으면 이진 계단 함수를 사용한 모델은 비선형적 데이터도 예측할 수 있다. 하지만 이진 계단 함수는 몇 가지 큰 단점이 있다.

바로 역전파 알고리즘(backpropagation algorithm)을 사용하지 못하는 것이다. 이진 계단 함수는 0에서는 미분이 안 될뿐더러 0인 부분을 제외하고 미분을 한다고 해도 미분 값이 전부 0이 나온다. 때문에 역전파에서 가중치들이 업데이트되지 않는다.

현실의 대부분 문제들에서 다층 인공신경망을 사용하고 있고 이런 인공신경망을 훈련시킬 때 역전파 알고리즘을 사용하고 있다. 이진 계단 함수를 사용한다면 역전파 알고리즘을 사용하지 못하기 때문에 상대적으로 비효율적인 업데이트 방법을 사용해야 한다. 즉, 현실의 복잡한 문제는 사실상 해결하기 어렵다는 것이다.

또한 다중 출력은 할 수 없다는 단점이 있다. 이진 계단 함수는 출력을 1 또는 0으로 밖에 주지 못하기 때문에 다양한 클래스를 구분해야 하는 문제는 해결할 수 없다.

📖 활성화 함수의 종류_(2) 선형 활성화 함수

선형 활성화 함수


선형 활성화 함수(linear activation function)은 말 그대로 '선형'인 활성화 함수이다.

선형 활성화 함수를 사용한 모델은 이진 계단 함수를 사용한 모델과 다르게 다중 출력이 가능하다. 때문에 이진 분류는 물론이고 간단한 다중 분류 문제까지도 해결할 수 있다.

또한 미분이 가능해서 역전파 알고리즘 또한 사용할 수 있다.

표적인 선형 함수로는 f(x)=xf(x) = x

import os
img_path = os.getenv('HOME')+'/aiffel/activation/jindo_dog.jpg'

# 선형 함수
def linear(x):
    return x

def dev_linear(x):
    return 1

# 시각화
ax = plot_and_visulize(img_path, linear, dev_linear)
ax.show()


선형 활성화 함수의 치역은 실수 전체이다.

선형 활성화 함수를 사용한 모델은 그 이름에 걸맞게 선형적으로 구분 가능한 문제를 해결할 수 있다. 조금 전에 보았던 AND gate와 OR gate를 가져와 본다.

# AND gate, OR gate
threshold = 0
X = np.array([[0,0], [1,0], [0,1], [1,1]])

plt.figure(figsize=(10,5))
# OR gate
or_y = np.array([x1 | x2 for x1,x2 in X])
ax1 = plt.subplot(1,2,1)
ax1.set_title('OR gate ' + str(or_y))
ax1 = scatter_plot(ax1, X, or_y)

# AND gate
and_y = np.array([x1 & x2 for x1,x2 in X])
ax2 = plt.subplot(1,2,2)
ax2.set_title('AND gate ' + str(and_y))
ax2 = scatter_plot(ax2, X, and_y)

plt.show()


선형 활성화 함수를 사용한 단층 퍼셉트론을 이용해 해당 gate를 다음과 같이 구현할 수 있다.

import tensorflow as tf

# OR gate model
or_linear_model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(2,), dtype='float64'),
    tf.keras.layers.Dense(1, activation='linear')
])
or_linear_model.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(learning_rate=0.01), metrics=['accuracy'])
or_linear_model.summary()

# AND gate model
and_linear_model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(2,), dtype='float64'),
    tf.keras.layers.Dense(1, activation='linear')
])
and_linear_model.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(learning_rate=0.01), metrics=['accuracy'])
and_linear_model.summary()
# 실행결과
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 1)                 3         
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 1)                 3         
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
or_linear_model.fit(X, or_y, epochs=1000, verbose=0)
and_linear_model.fit(X, and_y, epochs=1000, verbose=0)
print('done')
# 그래프로 그려보기
test_X = np.array([[x/100,y/100] for (x,y) in product(range(101),range(101))])
pred_or_y = or_linear_model(test_X)
pred_and_y = and_linear_model(test_X)

plt.figure(figsize=(10,10))
ax1 = plt.subplot(2,2,1)
ax1.set_title('predict OR gate')
ax1 = scatter_plot(ax1, test_X, pred_or_y, threshold=0.5)

ax2 = plt.subplot(2,2,2, projection='3d')
ax2.set_title('predict OR gate 3D')
ax2 = scatter_plot(ax2, test_X, pred_or_y, threshold=0.5, three_d=True)

ax3 = plt.subplot(2,2,3)
ax3.set_title('predict AND gate')
ax3 = scatter_plot(ax3, test_X, pred_and_y, threshold=0.5)

ax4 = plt.subplot(2,2,4, projection='3d')
ax4.set_title('predict AND gate 3D')
ax4 = scatter_plot(ax4, test_X, pred_and_y, threshold=0.5, three_d=True)

plt.show()


물론 선형 활성화를 사용한 단층 퍼셉트론이 정확한 값을 예측하는 것은 아니다. 위의 예시를 보면 첫 번째 AND gate를 구현하기 위한 모델에 [[0,0],[0,1],[1,0],[1,1]]을 넣으면 나오는 출력값은 [[-0.25],[0.25],[0.25],[0.75]]이며, 두 번째 OR gate를 구현하기 위한 모델에 [[0,0],[0,1],[1,0],[1,1]]를 넣으면 출력값은 [[0.25],[0.75],[0.75],[1.25]]가 된다. 따라서 그림에도 결과가 유사하다는 표현을 해놓았다. 하지만, 만약 0.5를 기준으로 기준보다 높으면 1이고 낮으면 0을 출력하게 한다면, 이 모델들은 AND gate와 OR gate를 완벽히 구현한다고 말할 수 있다.

그럼 이 모델로 XOR gate를 구현할 수 있을까? 정답은 '불가능하다' 이다.

# XOR gate
xor_linear_model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(2,), dtype='float64'),
    tf.keras.layers.Dense(1, activation='linear')
])
xor_linear_model.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(learning_rate=0.01), metrics=['accuracy'])
xor_linear_model.fit(X, xor_y, epochs=1000, verbose=0)

# 그래프로 그려보기
test_X = np.array([[x/100,y/100] for (x,y) in product(range(101),range(101))])
pred_xor_y = xor_linear_model(test_X)

plt.figure(figsize=(10,5))
ax1 = plt.subplot(1,2,1)
ax1.set_title('predict XOR gate')
ax1 = scatter_plot(ax1, test_X, pred_xor_y, threshold=0.5)

ax2 = plt.subplot(1,2,2, projection='3d')
ax2.set_title('predict XOR gate 3D')
ax2 = scatter_plot(ax2, test_X, pred_xor_y, threshold=0.5, three_d=True)

plt.show()


마찬가지로 선 하나로는 나눌 수 없기 때문이다.

Q. 그렇다면 층을 쌓는다면 가능할까
A. 불가능하다. 위에서도 다루었듯이 선형 활성화 함수를 사용하면 아무리 층을 쌓아도 결국 하나의 노드를 쓰는 것과 같다. 따라서 층을 쌓는다고 해서 크게 달라지는 것이 없다.
(위 코드에서 'xor_linear_model'의 linear layer를 추가해 확인해볼 수 있다.)

선형 활성화 함수의 한계


선형 활성화 함수의 한계는 명확하다. 바로 모델에 선형 활성화 함수를 사용한다면 비선형적 특성을 지닌 데이터를 예측하지 못한다는 것이다.

📖 활성화 함수의 종류_(3) 비선형 활성화 함수-시그모이드, Softmax

비선형 활성화 함수(non-linear activation function)는 '비선형'인 활성화 함수로써 앞에서 나왔던 활성화 함수들의 문제점을 해결한 함수이다. 따라서 비선형 활성화 함수를 사용한 모델은 역전파 알고리즘을 사용할 수 있으며, 다중 출력도 가능하고 비선형적 특성을 지닌 데이터도 예측할 수 있다.

시그모이드 / 로지스틱


첫 번째로 소개해 드릴 비선형 활성화 함수는 시그모이드(sigmoid 혹은 로지스틱 logistic) 활성화 함수이다. 시그모이드 함수는 다음과 같이 표현된다.

σ(x)=11+ex\sigma(x) = \frac{1}{1+e^{-x}}

시그모이드 함수의 치역은 (0,1), 즉, 0<σ(x)<10<\sigma(x)<1

시그모이드 함수를 쓰는 가장 주된 이유가 바로 치역이 0과 1사이라는 것이다. 특히 확률을 예측해야 하는 모델에서 자주 사용된다.

시그모이드 함수의 미분은 다음과 같다.

dσ(x)dx=ex(1+ex)2=11+exex1+ex=11+ex(1+ex1+ex11+ex)=σ(x)(1σ(x))\begin{aligned} \frac{d \sigma(x)}{dx} &= \frac{e^{-x}}{(1+e^{-x})^2}\\ & = \frac{1}{1+e^{-x}}\frac{e^{-x}}{1+e^{-x}} \\ & = \frac{1}{1+e^{-x}}\biggl(\frac{1+e^{-x}}{1+e^{-x}}-\frac{1}{1+e^{-x}}\biggr)\\ & =\sigma(x)(1-\sigma(x)) \end{aligned}
import os
img_path = os.getenv('HOME')+'/aiffel/activation/jindo_dog.jpg'

# 시그모이드 함수
def sigmoid(x):
    return 1/(1+np.exp(-x).astype(np.float64))

def dev_sigmoid(x):
    return sigmoid(x)*(1-sigmoid(x))

# 시각화
ax = plot_and_visulize(img_path, sigmoid, dev_sigmoid)
ax.show()

수치 미분과 해석 미분의 차이


프로그램을 이용해서는 해석 미분을 구할 수 없기 때문에 꽤 많은 분들이 모르는 함수의 미분을 구할 때 수치 미분(numerical differentiation)을 구한다. 어떤 함수 f(x)f(x)가 있을 때 이것의 미분 f(x)f'(x)은 다음과 같이 표현할 수 있다.

f(x)=limh0f(x+h)f(x)hf'(x) = \lim_{h \rightarrow 0} \frac{f(x+h)-f(x)}{h}

여기서 수치 미분이라고 하면 hh의 값을 최대한 0에 가까운 값으로 잡아서 실제 미분값에 근사하게 만들어주는 것이라고 할 수 있다.

f(x+h)f(x)hf(x)\frac{f(x+h)-f(x)}{h} \approx f'(x)

hh의 값을 0에 가까운 아주 작은 수로 잡으면 수치 미분을 통해 계산한 값과 실제 미분을 통해 계산한 값은 차이가 없을 것으로 생각된다. 하지만 적당한 hh를 주지 않는다면 생각보다 큰 차이가 있을 수 있다.

  • (아주 작은 hh를 주려고 h=1e20h=1e-20

이는 직접 그래프를 그려보는 것으로 확인할 수 있다.

# 수치 미분
def num_derivative(x, function):
    h = 1e-15 # 이 값을 바꾸어 가며 그래프를 확인해 보세요
    numerator = function(x+h)-function(x)
    return numerator/h

# 두 그래프의 차이
diff_X = [-5+x/100 for x in range(1001)]
dev_y = np.array([dev_sigmoid(x) for x in diff_X])
num_dev_y = np.array([num_derivative(x, sigmoid) for x in diff_X])

diff_y = dev_y - num_dev_y
plt.plot(diff_X, num_dev_y, label='numerical')
plt.plot(diff_X, dev_y, label='analytic')
plt.plot(diff_X, diff_y, label='differnce')
plt.legend()

plt.show()

시그모이드 함수의 단점


시그모이드 함수에는 두 가지 큰 단점이 있다.

시그모이드 함수는 0 또는 1에서 포화(saturate)된다. 이 말은 입력값이 아무리 커져도 함수의 출력은 1에 더 가까워져 갈 뿐 1 이상으로 높아지지 않고, 입력값이 아무리 작아져도 함수의 출력은 0에 더 가까워져 갈 뿐 0 이하로 떨어지지 않는다는 의미이다. 이렇게 포화가 되면 발생하는 일이 바로 그래디언트가 0과 아주 가까워지는 것이다. 역전파에서 이 0과 가까워진 그래디언트는 앞에서 온 역전파 값에 곱해지게 되는데 그렇게 되면 그 이후로 전파되는 모든 역전파 값이 0에 근접하게 되어 사실상 가중치 업데이트가 일어나지 않게 된다. 이것을 '그래디언트를 죽인다(kill the gradient)' 라고 표현한다. 극단적인 예로 만약 어떤 모델의 초기 가중치 값들을 아주 크게 잡아 포화상태를 만들면 역전파 때 그래디언트가 죽기 때문에 아무리 많이 에포크를 돌려도 훈련이 거의 되지 않는다.

시그모이드 함수의 출력은 0이 중심(zero-centered)이 아니다. 여기서 발생하는 문제는 훈련의 시간이 오래 걸리게 된다는 것이다. 만약 입력값으로 들어오는 데이터의 값이 모두 양수라고 가정하고 모델에 있는 어떤 한 노드의 출력값이 σ(iwixi+b)\sigma(\sum_i w_i x_i + b)

Lw= w(σ(iwixi+b))×upstream gradient= σ(iwixi+b)(1σ(iwixi+b))w(iwixi+b)×upstream gradients= σ(iwixi+b)(1σ(iwixi+b))x×upstream gradient\begin{aligned} \frac{\partial L}{\partial w} = ~& \frac{\partial}{\partial w}\biggl(\sigma(\sum_i w_i x_i +b)\biggr) \times \text{upstream gradient} \\ =~& \sigma(\sum_i w_i x_i +b)(1-\sigma(\sum_i w_i x_i +b))\frac{\partial}{\partial w}\biggl(\sum_i w_i x_i +b\biggr) \\ & \times \text{upstream gradients}\\ =~& \sigma(\sum_i w_i x_i +b)(1-\sigma(\sum_i w_i x_i +b))x \times \text{upstream gradient} \end{aligned}

여기서 LLlossloss이고 ww는 노드 하나의 가중치이다. 위 식에서, σ(iwixi+b)(1σ(iwixi+b))x\sigma(\sum_i w_i x_i +b)(1-\sigma(\sum_i w_i x_i +b))x

비선형 활성화 함수를 사용해 AND gate, OR gate, XOR gate 구현


비선형 활성화 함수인 시그모이드 함수를 이용해 AND gate, OR gate, XOR gate를 구현해 본다.

# OR gate
or_sigmoid_model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(2,)),
    tf.keras.layers.Dense(1, activation='sigmoid')
])
or_sigmoid_model.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(learning_rate=0.01), metrics=['accuracy'])
or_sigmoid_model.fit(X, or_y, epochs=1000, verbose=0)

# AND gate
and_sigmoid_model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(2,)),
    tf.keras.layers.Dense(1, activation='sigmoid')
])
and_sigmoid_model.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(learning_rate=0.01), metrics=['accuracy'])
and_sigmoid_model.fit(X, and_y, epochs=1000, verbose=0)

# XOR gate
xor_sigmoid_model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(2,)),
    tf.keras.layers.Dense(1, activation='sigmoid')
])
xor_sigmoid_model.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(learning_rate=0.01), metrics=['accuracy'])
xor_sigmoid_model.fit(X, xor_y, epochs=1000, verbose=0)

# 그래프로 그려보기
test_X = np.array([[x/100,y/100] for (x,y) in product(range(101),range(101))])
pred_or_y = or_sigmoid_model(test_X)
pred_and_y = and_sigmoid_model(test_X)
pred_xor_y = xor_sigmoid_model(test_X)

plt.figure(figsize=(10,15))
ax1 = plt.subplot(3,2,1)
ax1.set_title('predict OR gate')
ax1 = scatter_plot(ax1, test_X, pred_or_y, threshold=0.5)

ax2 = plt.subplot(3,2,2, projection='3d')
ax2.set_title('predict OR gate 3D')
ax2 = scatter_plot(ax2, test_X, pred_or_y, threshold=0.5, three_d=True)

ax3 = plt.subplot(3,2,3)
ax3.set_title('predict AND gate')
ax3 = scatter_plot(ax3, test_X, pred_and_y, threshold=0.5)

ax4 = plt.subplot(3,2,4, projection='3d')
ax4.set_title('predict AND gate 3D')
ax4 = scatter_plot(ax4, test_X, pred_and_y, threshold=0.5, three_d=True)

ax5 = plt.subplot(3,2,5)
ax5.set_title('predict XOR gate')
ax5 = scatter_plot(ax5, test_X, pred_xor_y, threshold=0.5)

ax6 = plt.subplot(3,2,6, projection='3d')
ax6.set_title('predict XOR gate 3D')
ax6 = scatter_plot(ax6, test_X, pred_xor_y, threshold=0.5, three_d=True)

plt.show()


XOR gate는 제대로 구현하지 못한 것을 확인할 수 있다. 하지만 이것이 비선형 함수들도 XOR gate를 구현하지 못한다는 것은 아니다. 다만, 좀 더 특수한 비선형 함수가 필요하다.

참고 자료에 나와 있듯이 2차 다항식(quadratic polynomial)을 추가한 시그모이드 함수를 사용한다면 XOR gate를 구현할 수 있다.

이 밖에도 layer를 추가해 준다면 XOR gate를 무리 없이 구현하는 것을 확인할 수 있다.
(하지만 선형 함수를 사용했을 때는 어떻게 해도 안 된다.)

# 레이어를 추가했을 때
# XOR gate
xor_sigmoid_model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(2,)),
    tf.keras.layers.Dense(2, activation='sigmoid'), # 2 nodes로 변경
    tf.keras.layers.Dense(1)
])
xor_sigmoid_model.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(learning_rate=0.01), metrics=['accuracy'])
xor_sigmoid_model.fit(X, xor_y, epochs=1000, verbose=0)

plt.figure(figsize=(10,5))
pred_xor_y = xor_sigmoid_model(test_X)

ax1 = plt.subplot(1,2,1)
ax1.set_title('predict XOR gate')
ax1 = scatter_plot(ax1, test_X, pred_xor_y, threshold=0.5)

ax2 = plt.subplot(1,2,2, projection='3d')
ax2.set_title('predict XOR gate 3D')
ax2 = scatter_plot(ax2, test_X, pred_xor_y, threshold=0.5, three_d=True)

plt.show()

Softmax는 뭔가요?


Softmax는 강아지와 고양이 사진을 구분하는 분류 문제에서 가장 많이 접한 함수이다. 시그모이드는 Binary Classification, 즉 참(1) or 거짓(0) 등 2가지 class를 분류할 때 많이 쓰인다. 왜냐하면 시그모이드는 0 과 1 사이의 출력값을 가지고 있기 때문이다.

한편, Softmax는 10가지, 100가지 class 등 class의 수에 제한 없이 "각 class의 확률"을 구할 때 쓰입니다. 예컨대, 가위, 바위, 보 사진 분류 문제는 3개 class 분류 문제이고, softmax는 각 class의 확률값, 즉 (0.2, 0.5, 0.3) 이렇게 출력값을 준다. Softmax의 가장 큰 특징은, 확률의 성질인 모든 경우의 수(=모든 class)의 확률을 더하면 1이 되는 성질을 가지고 있다. 그래서 Softmax는 모델의 마지막 layer에서 활용이 된다.

📖 활성화 함수의 종류_(4) 비선형 활성화 함수-하이퍼볼릭 탄젠트

하이퍼볼릭 탄젠트


하이퍼볼릭 탄젠트(tanh, Hyperbolic tangent) 함수는 쌍곡선 함수 중 하나이다.

하이퍼볼릭 탄젠트 함수는 다음과 같이 표현된다.

tanh(x)=exexex+ex\tanh(x) = \frac{e^x-e^{-x}}{e^x+e^{-x}}

시그모이드 함수의 치역은 (-1,1), 즉, 1<σ(x)<1-1<\sigma(x)<1

이 하이퍼볼릭 탄젠트 함수는 시그모이드 함수를 이용해서도 표현할 수 있다.

tanh(x)=exexex+ex=1e2x1+e2x=21+e2x1+e2x1+e2x=21+e2x1=2σ(2x)1\begin{aligned} \tanh(x) & = \frac{e^x-e^{-x}}{e^x+e^{-x}}\\ & = \frac{1-e^{-2x}}{1+e^{-2x}}\\ & = \frac{2}{1+e^{-2x}}-\frac{1+e^{-2x}}{1+e^{-2x}} \\ & = \frac{2}{1+e^{-2x}}-1 \\ & = 2\sigma(2x)-1 \end{aligned}

하이퍼볼릭 탄젠트 함수의 미분은 다음과 같다.

ddxtanh(x)=ddx(exexex+ex)=(exex)2(ex+ex)2(ex+ex)2=1tanh2(x)\begin{aligned} \frac{d}{dx}\tanh(x) & = \frac{d}{dx} \biggl(\frac{e^x-e^{-x}}{e^x+e^{-x}}\biggr) \\ & = -\frac{(e^x-e^{-x})^2 - (e^x+e^{-x})^2}{(e^x+e^{-x})^2} \\ & = 1 - \tanh^2(x) \end{aligned}
import os
img_path = os.getenv('HOME')+'/aiffel/activation/jindo_dog.jpg'

# 하이퍼볼릭 탄젠트 함수
def tanh(x):
    return (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))

def dev_tanh(x):
    return 1-tanh(x)**2

# 시각화
ax = plot_and_visulize(img_path, tanh, dev_tanh)
ax.show()

하이퍼볼릭 탄젠트 함수의 단점


하이퍼볼릭 탄젠트 함수는 그래프에서도 알 수 있듯 -1 또는 1에서 포화된다.

📖 활성화 함수의 종류_(5) 비선형 활성화 함수-ReLU

ReLU


ReLU(rectified linear unit) 함수는 최근 가장 많이 사용되고 있는 활성화 함수이다. ReLU의 식은 다음과 같다.

f(x)=max(0,x)f(x) = \max(0,x)
import os
img_path = os.getenv('HOME')+'/aiffel/activation/jindo_dog.jpg'

# relu 함수
def relu(x):
    return max(0,x)

# 시각화
ax = plot_and_visulize(img_path, relu)
ax.show()


ReLU함수의 치역은 [0,)[0, \infty)이다. ReLU를 사용한 모델은 하이퍼볼릭 탄젠트를 사용한 모델보다 몇 배 더 빠르게 훈련이 된다. 이는 아래의 그래프를 통해서 확인할 수 있다.

위 그래프는 에포크당 훈련 에러 비율을 나타내고 있다. 실선이 ReLU 함수를 사용한 모델의 에러 비율이고, 점선이 하이퍼볼릭 탄젠트 함수를 사용한 모델의 에러 비율이다. ReLU를 사용한 모델이 약 7배 정도 빠르게 에러 비율이 감소한 것을 볼 수 있다. 또한 ReLU 함수는 시그모이드나 하이퍼볼릭 탄젠트처럼 비용이 높은 (예를 들면, exponential와 같은) 연산을 사용하지 않기 때문에 처리 속도가 빠르다.

ReLU는 0을 제외한 구간에서 미분이 가능하다. 이는 다음과 같이 표현할 수 있다.

f(x)={0if x<01if x>0 , undefined at x=0f'(x)= \begin{cases} 0 & \text{if }x<0 \\ 1 & \text{if }x>0 \end{cases} \text{ , undefined at x=0}

x=0x=0

  • 우미분

    f+(x)=limh0+f(x+h)f(x)hf'_+(x) = \lim_{h\rightarrow0+}\frac{f(x+h)-f(x)}{h}
  • 좌미분

    f(x)=limh0f(x+h)f(x)hf'_-(x) = \lim_{h\rightarrow0-}\frac{f(x+h)-f(x)}{h}

이때, 어떤 값 x=ax=a

f(a)=limh0+f(a+h)f(a)h=limh0f(a+h)f(a)hf'(a) = \lim_{h\rightarrow0+}\frac{f(a+h)-f(a)}{h} = \lim_{h\rightarrow0-}\frac{f(a+h)-f(a)}{h}

따라서 만약 어떤 값에 대해 함수의 좌미분 값과 우미분 값이 다르다면, 그 함수는 그 값에 대해 미분이 불가능하다고 표현할 수 있습니다.

그럼 x=0x=0

f(x)=max(0,x)f(x)=max(0,x)f+(0)=limh0+f(0+h)f(0)h=limh0+hh=1f(0)=limh0f(0+h)f(0)h=limh00h=0f(x) = max(0,x) f(x)=max(0,x) f'_+(0) = \lim_{h\rightarrow0+}\frac{f(0+h)-f(0)}{h} = \lim_{h\rightarrow0+}\frac{h}{h} = 1 f'_-(0) = \lim_{h\rightarrow0-}\frac{f(0+h)-f(0)}{h} = \lim_{h\rightarrow0-}\frac{0}{h} = 0

즉, x=0x=0

ReLU 함수를 이용한 모델은 비선형적 데이터의 특징을 잘 잡아낼까?


ReLU 함수를 보고 있으면 이러한 의문이 든다.

'시그모이드 함수처럼 곡선이 포함되어 있지 않은데 어떻게 비선형적 데이터의 특징을 잡아낼까?'

과연 ReLU 함수로 f(x)=x2f(x) = x^2

q_X = np.array([-10+x/100 for x in range(2001)])
q_y = np.array([(x)**2 + np.random.randn(1)*10 for x in q_X])
plt.scatter(q_X, q_y, s=0.5)

approx_relu_model_p = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(1,)),
    tf.keras.layers.Dense(6, activation='relu'), # 6 nodes 병렬 연결
    tf.keras.layers.Dense(1)
])
approx_relu_model_p.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(learning_rate=0.005), metrics=['accuracy'])
approx_relu_model_p.fit(q_X, q_y, batch_size=32, epochs=100, verbose=0)

approx_relu_model_s = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(1,)),
    tf.keras.layers.Dense(2, activation='relu'),# 2 nodes 직렬로 3번 연결
    tf.keras.layers.Dense(2, activation='relu'),
    tf.keras.layers.Dense(2, activation='relu'),
    tf.keras.layers.Dense(1)
])
approx_relu_model_s.compile(loss='mse', optimizer=tf.keras.optimizers.Adam(learning_rate=0.005), metrics=['accuracy'])
approx_relu_model_s.fit(q_X, q_y, batch_size=32, epochs=100, verbose=0)

approx_relu_model_p.summary()
approx_relu_model_s.summary()
# 실행결과
Model: "sequential_7"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_8 (Dense)              (None, 6)                 12        
_________________________________________________________________
dense_9 (Dense)              (None, 1)                 7         
=================================================================
Total params: 19
Trainable params: 19
Non-trainable params: 0
_________________________________________________________________
Model: "sequential_8"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_10 (Dense)             (None, 2)                 4         
_________________________________________________________________
dense_11 (Dense)             (None, 2)                 6         
_________________________________________________________________
dense_12 (Dense)             (None, 2)                 6         
_________________________________________________________________
dense_13 (Dense)             (None, 1)                 3         
=================================================================
Total params: 19
Trainable params: 19
Non-trainable params: 0
_________________________________________________________________
q_test_X = q_X.reshape((*q_X.shape,1))
plt.figure(figsize=(10,5))

ax1 = plt.subplot(1,2,1)
ax1.set_title('parallel')
pred_y_p = approx_relu_model_p(q_test_X)
ax1.plot(q_X, pred_y_p)

ax2 = plt.subplot(1,2,2)
ax2.set_title('serial')
pred_y_s = approx_relu_model_s(q_test_X)
ax2.plot(q_X, pred_y_s)

plt.show()


실제로 x2x^2의 그래프를 근사할 수 있는지 확인해 봄과 동시에 노드를 병렬로 쌓는 것과 직렬로 쌓는 것 중에 어느 것이 더 좋은지도 비교해 보았다. 이 결과로 우리는 ReLU함수를 사용해도 충분히 x2x^2와 같은 비선형적 데이터를 예측해낼 수 있다는 것을 알 수 있었고, 파라미터 수가 같음에도 불구하고 노드를 병렬로 쌓은 것이 직렬로 쌓은 것보다 더 좋은 결과를 낸 것을 확인할 수 있었다.
(병렬 쌓기와 직렬 쌓기가 각각이 어떤 의미를 담고 있는지 좀 더 연구해 보면 좋을 듯하다.)

ReLU 함수의 단점


ReLU 함수의 출력값이 0이 중심이 아니다. 따라서 위에서 언급했던 문제가 발생할 수 있다. 또 하나의 단점은 Dying ReLU이다. 이것의 의미는 모델에서 ReLU를 사용한 노드가 비활성화되며 출력을 0으로만 하게 되는 것이다. 왜 이런 상황이 발생하는지 알아보자. 어떤 모델에 있는 한 노드의 출력값이 max(o,iwixi+b)\max(o, \sum_i w_i x_i + b)

Lw= w(max(0,iwixi+b))×upstream gradient= {0if iwixi+b<0upstream gradientif iwixi+b>0\begin{aligned} \frac{\partial L}{\partial w} = ~& \frac{\partial}{\partial w}\biggl(\max(0, \sum_i w_i x_i + b)\biggr) \times \text{upstream gradient} \\ =~& \begin{cases} 0 & \text{if }\sum_i w_i x_i + b<0 \\ \text{upstream gradient}& \text{if }\sum_i w_i x_i + b>0 \end{cases} \end{aligned}

만약, 이전 훈련 스텝에서 이 노드의 가중치가 업데이트되어 iwixi+b\sum_i w_i x_i + b

📖 활성화 함수의 종류_(6) 비선형 활성화 함수-ReLU의 단점을 극복하기 위한 시도들

Leaky ReLU


Leaky ReLU함수는 ReLU 함수의 'Dying ReLU'를 해결하기 위한 시도 중 하나이다. 식을 먼저 보자.

f(x)=max(0.01x,x)f(x)=max(0.01x,x)
import os
img_path = os.getenv('HOME')+'/aiffel/activation/jindo_dog.jpg'

# leaky relu 함수
def leaky_relu(x):
    return max(0.01*x,x)

# 시각화
ax = plot_and_visulize(img_path, leaky_relu)
ax.show()


'Dying ReLU'를 발생시켰던 0을 출력하던 부분을 아주 작은 음수값을 출력하게 만들어 주어 해당 문제를 해결하였다.

이 부분을 제외하고는 ReLU와 같은 특성을 가진다.

PReLU


PReLU(parametric ReLU)는 Leaky ReLU와 유사하지만 새로운 파라미터를 추가하여 0 미만일 때의 '기울기'가 훈련되게 했다. 식으로 표현하면 다음과 같다.

f(x)=max(αx,x)f(x) = \max(\alpha x,x)

여기서 α\alpha가 훈련과정에서 업데이트된다.

# PReLU 함수
def prelu(x, alpha):
    return max(alpha*x,x)

# 시각화
ax = plot_and_visulize(img_path, lambda x: prelu(x, 0.1)) # parameter alpha=0.1일 때

ax.show()

ELU


ELU(exponential linear unit)은 ReLU의 모든 장점을 포함하며, 0이 중심점이 아니었던 단점과, 'Dying ReLU'문제를 해결한 활성화 함수이다. 식은 다음과 같다.

f(x)={xif x>0α(ex1)if x0f(x)= \begin{cases} x & \text{if }x>0 \\ \alpha(e^x-1) & \text{if }x\leq0 \end{cases}

ELU의 미분은 논문에서 다음과 같이 정의하고 있다.

f(x)={1if x>0f(x)+αif x0f'(x)= \begin{cases} 1 & \text{if }x>0 \\ f(x)+\alpha & \text{if }x\leq0 \end{cases}
# elu 함수
def elu(x, alpha):
    return x if x > 0 else alpha*(np.exp(x)-1)

def dev_elu(x, alpha):
    return 1 if x > 0 else elu(x, alpha) + alpha

# 시각화
ax = plot_and_visulize(img_path, lambda x: elu(x, 1), lambda x: dev_elu(x, 1)) # alpha가 1일 때
ax.show()


이 함수의 단점은 exponential 연산이 들어가서 계산 비용이 높아졌다는 점이 있다.

끝으로...


이 밖에도 다양한 시도들이 존재하며 계속 연구가 이루어지고 있다.

좋은 웹페이지 즐겨찾기