03.2 활성화 함수

[식 3.3]과 같은 활성화 함수는 임계값을 경계로 출력이 바뀌는데, 이런 함수를 계단 함수 step function라 합니다. 그래서 “퍼셉트론에서는 활성화 함수로 계단 함수를 이용한다”라 할 수 있습니다. 즉, 활성화 함수로 쓸 수 있는 여러 후보 중에서 퍼셉트론은 계단 함수를 채용하고 있습니다. 그렇다면 계단 함수 이외의 함수를 사용하면 어떻게 될까요? 실은 활성화 함수를 계단 함수에서 다른 함수로 변경하는 것이 신경망의 세계로 나아가는 열쇠입니다! 그럼 어서 신경망에서 이용하는 활성화 함수를 소개하겠습니다.

3.2.1 시그모이드 함수

다음은 신경망에서 자주 이용하는 활성화 함수인 시그모이드 함수 sigmoid function를 나타낸 식입니다.

\[h(x) = \frac{1}{1 + \exp(-x)} \hfill \text{[식 3.6]}\]

[식 3.6]에서 $\exp(-x)$는 $e^{-x}$를 뜻하며, $e$는 자연상수로 2.7182…의 값을 갖는 실수입니다. [식 3.6]으로 나타나는 시그모이드 함수는 얼핏 복잡해 보이지만 이 역시 단순한 ‘함수’일 뿐입니다. 함수는 입력을 주면 출력을 돌려주는 변환기죠. 예를 들어 시그모이드 함수에 1.0과 2.0을 입력하면 $h(1.0) = 0.731\dots$, $h(2.0) = 0.880\dots$ 처럼 특정 값을 출력합니다.

신경망에서는 활성화 함수로 시그모이드 함수를 이용하여 신호를 변환하고, 그 변환된 신호를 다음 뉴런에 전달합니다. 사실 앞 장에서 본 퍼셉트론과 앞으로 볼 신경망의 주된 차이는 이 활성화 함수뿐입니다. 그 외에 뉴런이 여러 층으로 이어지는 구조와 신호를 전달하는 방법은 기본적으로 앞에서 살펴본 퍼셉트론과 같습니다. 그러면 활성화 함수로 이용되는 시그모이드 함수를 계단 함수와 비교하면서 자세히 살펴보겠습니다.

3.2.2 계단 함수 구현하기

이번 절과 다음 절에서는 파이썬으로 계단 함수를 그려보겠습니다(함수의 형태를 눈으로 확인해보면 그 함수를 이해하는 데 큰 도움이 됩니다). 계단 함수는 [식 3.3]과 같이 입력이 0을 넘으면 1을 출력하고, 그 외에는 0을 출력하는 함수입니다. 다음은 이러한 계단 함수를 단순하게 구현한 것입니다.

def step_function(x):
    if x > 0:
        return 1
    else:
        return 0

이 구현은 단순하고 쉽지만, 인수 x는 실수(부동소수점)만 받아들입니까. 즉, step_function(3.0)은 되지만 넘파이 배열을 인수로 넣을 수는 없습니다. 가령 step_function(np.array([1.0, 2.0]))는 안 됩니다. 우리는 앞으로를 위해 넘파이 배열도 지원하도록 수정하고 싶습니다. 그러기 위해서는 가령 다음과 같은 구현을 생각할 수 있겠죠.

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

겨우 두 줄이라 엉성해 보이겠지만, 이는 넘파이의 편리한 트릭을 사용한 덕분이죠. 어떤 트릭을 썼는지는 다음 파이썬 인터프리터의 예를 보면서 설명해보죠. 다음 예에서는 x라는 넘파이 배열을 준비하고 그 넘파이 배열에 부등호 연산을 수행합니다.

>>> import numpy as np
>>> x = np.array([-1.0, 1.0, 2.0])
>>> x
array([-1.,  1.,  2.])
>>> y = x > 0
>>> y
array([False,  True,  True], dtype=bool)

넘파이 배열에 부등호 연산을 수행하면 배열의 원소 각각에 부등호 연산을 수행한 bool 배열이 생성됩니다. 이 예에서는 배열 x의 원소 각각이 0보다 크면 True로, 0 이하면 False로 변환한 새로운 배열 y가 생성됩니다.

y는 bool 배열입니다. 그런데 우리가 원하는 계단 함수는 0이나 1의 ‘int형’을 출력하는 함수죠. 그래서 배열 y의 원소를 bool에서 int형으로 바꿔줍니다.

>>> y = y.astype(np.int)
>>> y
array([0, 1, 1])

이처럼 넘파이 배열의 자료형을 변환할 때는 astype() 메서드를 이용합니다. 원하는 자료형(이 예에서는 np.int)을 인수로 지정하면 되죠. 그리고 파이썬에서는 bool을 int로 변환하면 True는 1로, False는 0으로 변환됩니다. 이상이 계단 함수 구현에서 사용한 넘파이의 ‘트릭’이었습니다.

3.2.3 계단 함수의 그래프

이제 앞에서 정의한 계단 함수를 그래프로 그려봅시다. 이를 위해 matplotlib 라이브러리를 사용합니다.

import numpy as np
import matplotlib.pylab as plt

def step_function(x):
    return np.array(x > 0, dtype=np.int)

x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # y축의 범위 지정
plt.show()

np.arange(-5.0, 5.0, 0.1)은 -5.0에서 5.0 전까지 0.1 간격의 넘파이 배열을 생성합니다. 즉, [-5.0, -4.9, ..., 4.9]를 생성합니다. step_function()은 인수로 받은 넘파이 배열의 원소 각각을 인수로 계단 함수를 실행해, 그 결과를 다시 배열로 만들어 돌려줍니다. 이 x, y 배열을 그래프로 그리면(plot) 결과는 [그림 3-6]처럼 됩니다.

그림 3-6 계단 함수의 그래프

그림 3-6

[그림 3-6]에서 보듯 계단 함수는 0을 경계로 출력이 0에서 1(또는 1에서 0)로 바뀝니다. 이제 ‘계단’ 함수로 불리는 이유를 아셨겠죠? 바로 이 그림처럼 값이 바뀌는 형태가 계단처럼 생겼기 때문이랍니다.

지니의 마법 설명

  • 지니: “도로시, 계단 함수는 0이라는 장벽을 넘는 순간 갑자기 키가 1로 쑥 커버리는 딱딱한 요술 계단이야!”
  • 도로시: “맞아, 중간이 없이 오직 0과 1만 있으니까 모 아니면 도 같아. 조금 더 부드러운 함수는 없을까?”
  • 토토: “멍멍! (계단 아래에서 위를 쳐다보며) 올라가기 벅차다 멍!”

계단 함수의 특징 지니가 중간값이 없는 딱딱한 계단 함수를 도로시와 토토에게 설명하고 있어요.

3.2.4 시그모이드 함수 구현하기

이어서 시그모이드 함수를 구현합시다. [식 3.6]의 시그모이드 함수는 파이썬으로 다음과 같이 작성할 수 있습니다.

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

여기서 np.exp(-x)는 $\exp(-x)$ 수식에 해당합니다. 이 구현에서 특별히 어려운 건 없고, 인수 x가 넘파이 배열이어도 올바른 결과가 나온다는 정도만 기억해둡시다. 실제로 넘파이 배열을 제대로 처리하는지 실험해보죠.

>>> x = np.array([-1.0, 1.0, 2.0])
>>> sigmoid(x)
array([ 0.26894142,  0.73105858,  0.88079708])

이 함수가 넘파이 배열도 훌륭히 처리해줄 수 있는 비밀은 넘파이의 브로드캐스트에 있습니다(“1.5.5 브로드캐스트” 참고). 브로드캐스트 기능이란 넘파이 배열과 스칼라값의 연산을 넘파이 배열의 원소 각각과 스칼라값의 연산으로 바꿔 수행하는 것입니다. 복습 겸 구체적인 예를 하나 보시죠.

>>> t = np.array([1.0, 2.0, 3.0])
>>> 1.0 + t
array([ 2.,  3.,  4.])
>>> 1.0 / t
array([ 1.        ,  0.5       ,  0.33333333])

이 예에서는 스칼라값 1.0과 넘파이 배열 사이에서 수치 연산(+/)을 해보았습니다. 결과적으로 스칼라값과 넘파이 배열의 각 원소 사이에서 연산이 이뤄지고, 연산 결과가 넘파이 배열로 출력되었습니다. 앞에서 구현한 sigmoid 함수에서도 np.exp(-x)가 넘파이 배열을 반환하기 때문에 1 / (1 + np.exp(-x))도 넘파이 배열의 각 원소에 연산을 수행한 결과를 내어줍니다.

그럼, 시그모이드 함수를 그래프로 그려볼까요? 그래프를 그리는 코드는 앞 절의 계단 함수 그리기 코드와 거의 같습니다. 유일하게 다른 부분은 y를 출력하는 함수를 sigmoid 함수로 변경한 곳입니다.

x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # y축 범위 지정
plt.show()

이 코드를 실행하면 [그림 3-7]의 그래프를 보실 수 있습니다.

그림 3-7 시그모이드 함수의 그래프*

그림 3-7

* 옮긴이_ 시그모이드(sigmoid)란 ‘S자 모양’이라는 뜻입니다. 계단 함수처럼 그 모양을 따 이름 지은 것이죠. 그래서 ‘시그모이드 함수’보다는 ‘S자 모양 함수’란 이름이 그 성질을 더 직관적으로 내비치지만, 이 책에서는 시그모이드란 이름으로 옮겼습니다. 첫 번째 이유는 실제 현장에서 많이 쓰이는 이름이고, 두 번째는 소스 코드와의 일관성 때문입니다. 소스 코드에서 이 기능을 담당하는 함수 이름이 ‘sigmoid’이고 책 전반에 자주 등장합니다. ‘시그모이드 = S자 모양’이란 점만 확실히 기억해주세요.

3.2.5 시그모이드 함수와 계단 함수 비교

시그모이드 함수와 계단 함수를 비교해봅시다. 이 두 함수를 [그림 3-8]에 함께 그려봤습니다. 무엇이 다르고, 또 공통되는 성질이라 할 만한 것은 무엇인가요? 곰곰이 생각해보세요.

그림 3-8 계단 함수(점선)와 시그모이드 함수(실선)

그림 3-8

[그림 3-8]을 보고 가장 먼저 느껴지는 점은 ‘매끄러움’의 차이일 것입니다. 시그모이드 함수는 부드러운 곡선이며 입력에 따라 출력이 연속적으로 변화합니다. 한편, 계단 함수는 0을 경계로 출력이 갑자기 바뀌고 있습니다. 시그모이드 함수의 이 매끈함이 신경망 학습에서 아주 중요한 역할을 하게 됩니다.

지니의 마법 설명

  • 지니: “시그모이드 함수는 둥글고 매끄러운 ‘S자 모양 미끄럼틀’이야. 경사가 완만해서 도로시가 타고 내려올 때 부드러운 속도 변화(연속적인 실수 값)를 느낄 수 있단다!”
  • 도로시: “와! 갑자기 떨어지는 계단보다 훨씬 안전하고 부드럽게 출력이 변화하니까, 다음 뉴런으로 신호를 전달하기도 훨씬 자연스럽겠네!”
  • 토토: “낑낑! (미끄럼틀 밑에서 꼬리를 흔들며) 멍멍! 나도 타고 싶다 멍!”

시그모이드 함수 미끄럼틀 도로시가 시그모이드 미끄럼틀을 부드럽게 타며 연속적인 실수의 흐름을 체감하고 있어요.

(역시 매끈함과 관련되지만) 계단 함수가 0과 1 중 하나의 값만 돌려주는 반면 시그모이드 함수는 실수(0.731…, 0.880… 등)를 돌려준다는 점도 다릅니다. 다시 말해 퍼셉트론에서는 뉴런 사이에 0 혹은 1이 흘렀다면 신경망에서는 연속적인 실수가 흐릅니다.

비유하자면, 계단 함수는 ‘시시오도시’*이고 시그모이드 함수는 ‘물레방아’와 비슷하죠. 계단 함수는 시시오도시처럼 물을 쏟아내거나 쏟아내지 않는(0 또는 1) 두 가지 움직임을 보여주며, 시그모이드 함수는 물레방아처럼 흘러온 물의 양에 비례해 흐르는 물의 양을 조절합니다.

* 옮긴이_ 대나무 물레방아라고도 부르는, 일본 전통 정원에서 흔히 볼 수 있는 장식입니다. 물이 떨어지는 곳에 한쪽을 자른 대나무 통을 시소처럼 비스듬히 설치해두면, 통 안에 물이 차 쏟아내고 원래대로 돌아가는 동작을 반복합니다. 이때 바닥을 때리는 소리로 운치를 더합니다.

두 함수의 공통점도 살펴볼까요? 두 함수는 ‘매끄러움’이라는 점에서는 다르지만, [그림 3-8]을 큰 관점에서 보면 둘은 같은 모양을 하고 있습니다. 둘 다 입력이 작을 때의 출력은 0에 가깝고(혹은 0이고), 입력이 커지면 출력이 1에 가까워지는(혹은 1이 되는) 구조인 것이죠. 즉, 계단 함수와 시그모이드 함수는 입력이 중요하면 큰 값을 출력하고 입력이 중요하지 않으면 작은 값을 출력합니다. 그리고 입력이 아무리 작거나 커도 출력은 0에서 1사이라는 것도 둘의 공통점입니다.

3.2.6 비선형 함수

계단 함수와 시그모이드 함수의 공통점은 그 밖에도 있습니다. 중요한 공통점으로, 둘 모두는 비선형 함수입니다. 시그모이드 함수는 곡선, 계단 함수는 계단처럼 구부러진 직선으로 나타나며, 동시에 비선형 함수로 분류됩니다.

NOTE_ 활성화 함수를 설명할 때 비선형 함수와 선형 함수라는 용어가 자주 등장합니다. 함수란 어떤 값을 입력하면 그에 따른 값을 돌려주는 ‘변환기’입니다. 이 변환기에 무언가 입력했을 때 출력이 입력의 상수배만큼 변하는 함수를 선형 함수라고 합니다. 수식으로는 $f(x) = ax + b$이고, 이때 $a$와 $b$는 상수입니다. 그래서 선형 함수는 곧은 1개의 직선이 됩니다. 한편, 비선형 함수는 문자 그대로 ‘선형이 아닌’ 함수입니다. 즉, 직선 1개로는 그릴 수 없는 함수를 말합니다.

신경망에서는 활성화 함수로 비선형 함수를 사용해야 합니다. 달리 말하면 선형 함수를 사용해서는 안 됩니다. 왜 선형 함수는 안 되는 걸까요? 그 이유는 바로 선형 함수를 이용하면 신경망의 층을 깊게 하는 의미가 없어지기 때문입니다.

선형 함수의 문제는 층을 아무리 깊게 해도 ‘은닉층이 없는 네트워크’로도 똑같은 기능을 할 수 있다는 데 있습니다. 구체적으로 (약간 직관적으로) 설명해주는 간단한 예를 생각해봤습니다. 선형 함수인 $h(x) = cx$를 활성화 함수로 사용한 3층 네트워크를 떠올려보세요. 이를 식으로 나타내면 $y(x) = h(h(h(x)))$가 됩니다. 이 계산은 $y(x) = c * c * c * x$처럼 세 번의 곱셈을 수행하지만, 실은 $y(x) = ax$와 똑같은 식입니다. $a = c^3$이라고만 하면 끝이죠. 즉, 은닉층이 없는 네트워크로 표현할 수 있습니다. 이 예처럼 선형 함수를 이용해서는 여러 층으로 구성하는 이점을 살릴 수 없습니다. 그래서 층을 쌓는 혜택을 얻고 싶다면 활성화 함수로는 반드시 비선형 함수를 사용해야 합니다.

3.2.7 ReLU 함수

지금까지 활성화 함수로서 계단 함수와 시그모이드 함수를 소개했습니다. 시그모이드 함수는 신경망 분야에서 오래전부터 이용해왔으나, 최근에는 ReLU Rectified Linear Unit, 렐루 함수를 주로 이용합니다.

ReLU는 입력이 0을 넘으면 그 입력을 그대로 출력하고, 0 이하이면 0을 출력하는 함수입니다(그림 3-9).

그림 3-9 ReLU 함수의 그래프*

그림 3-9

지니의 마법 설명

  • 지니: “ReLU(렐루) 함수는 음수 신호(-값)가 오면 문을 딱 걸어 잠그고 0만 보내주지만, 양수 신호(+값)가 들어오면 차단 장치와 포탈이 열리면서 본래 신호 크기를 그대로 통과시켜주는 ‘일방통행 램프’란다!”
  • 도로시: “아하! 음수는 0으로 차단하고, 양수는 모양 그대로 직진하니까 연산이 매우 간단해서 신경망의 속도가 아주 빨라지겠어!”
  • 토토: “(신나게 램프 위를 뛰어가며) 멍멍! 양수 구역은 오르막길이라 신난다 멍!”

ReLU 함수와 경사로 도로시와 토토가 음수 구역은 평평하고 양수 구역은 쭉 뻗어 있는 ReLU 경사로를 신나게 걷고 있어요.

ReLU 함수를 수식으로 나타내면 [식 3.7]처럼 쓸 수 있습니다.

\[h(x) = \begin{cases} x & (x > 0) \\ 0 & (x \le 0) \end{cases} \hfill \text{[식 3.7]}\]

그래프와 수식에서 보듯 ReLU는 간단한 함수입니다. 그래서 다음과 같이 쉽게 구현해 쓸 수 있습니다.

* 옮긴이_ ReLU에서 Rectified란 ‘정류된’이란 뜻입니다. 정류(整流)는 전기회로 쪽 용어로, 예를 들어 반파정류회로(half-wave rectification circuit)는 +/-가 반복되는 교류에서 - 흐름을 차단하는 회로입니다. [그림 3-9]와 비교하면 x가 0 이하일 때를 차단하여 아무 값도 출력하지 않는(0을 출력하는) 것이죠. 그래서 ReLU 함수를 ‘정류된 선형 함수’ 정도로 옮길 수 있겠으나, 간단한 개념을 어려운 용어로 바꾼다는 느낌이라 그만두었습니다.

def relu(x):
    return np.maximum(0, x)

여기에서는 넘파이의 maximum 함수를 사용했습니다. maximum은 두 입력 중 큰 값을 선택해 반환하는 함수입니다.

이번 장에서는 앞으로 시그모이드 함수를 활성화 함수로 사용합니다만, 이 책 후반부는 주로 ReLU 함수를 사용합니다.

서브목차