퍼셉트론에서 신경망으로
활성화 함수의 등장
편향을 명시한 퍼셉트론과 그에 따른 수식은 다음과 같다.
이 퍼셉트론은 x1, x2, 1이라는 3개의 신호가 뉴런에 입력되어, 각 신호에 가중치를 곱한 후 다음 뉴런에 전달된다. 다음 뉴런에서는 이 신호들의 값을 더하여 그 합이 0을 넘으면 1을, 그렇지 않으면 0을 출력한다. 편향의 입력 신호는 항상 1이기 때문에 다른 뉴런들과 구별했다.
수식에서는 조건 분기의 동작, 즉 0을 넘으면 1을 출력하고 0을 넘지 않으면 0을 출력하는 동작을 하나의 함수 h(x)로 나타냈다. 입력 신호의 총합이 h(x)라는 함수를 거쳐 변환되어 그 변환된 값이 y의 출력이 된다. 이러한 함수를 `활성화 함수(activation function)`이라고 하며 이는 입력 신호의 총합이 활성화를 일으키는지를 정하는 역할을 수행한다.
위의 식에서 가중치가 달린 입력 신호와 편향의 총합을 계산하고 이를 a라 표현하고, a를 함수 h()에 넣어 y를 출력하는 과정을 표현한 그림이다. 즉, 가중치 신호를 조합한 결과가 a라는 노드가 되고, 활성화 함수 h()를 통과하여 y라는 노드로 변환되는 과정이다.
활성화 함수(Activation Function)
계단 함수(step function)
위에서 확인한 함수 h(x)는 임계값을 경계로 출력이 바뀌는 `계단 함수(step function)`이다. 단순 퍼셉트론에서는 활성화 함수로 계단 함수를 이용한다.
# step_function
def step_function(x):
if x>0:
return 1
else:
return 0
해당 함수는 단순하게 입력이 0을 넘으면 1을 출력하고, 그 외에는 0을 출력하는 함수이다. 그러나 이 함수에서 인수 x는 실수만 받아들인다. 즉, 넘파이 배열은 인수로 넣을 수 없다. 이를 위해서 다음과 같이 함수를 수정한다.
def step_function(x):
y = x > 0
return y.astype(np.int)
넘파이 배열에 부등호 연산을 수행하여 bool 배열을 새로 생성하고 각 원소가 0보다 크면 True, 0 이하면 False로 변환한 새로운 배열 y를 생성한다. 이후 `astype()` 함수를 통해 넘파이 배열의 자료형을 bool에서 int형으로 변환하였다. 계단 함수를 그래프로 그려보면 다음과 같다.
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)
plt.show()
이렇듯 계단 함수는 0을 경계로 출력이 0에서 1 또는 1에서 0으로 변화한다.
시그모이드 함수(sigmoid function)
다음은 신경망에서 자주 이용하는 활성화 함수인 `시그모이드 함수(sigmoid function)`을 나타낸 수식이다.
이를 코드로 구현하고, 그래프를 확인해보면 다음과 같다.
def sigmoid(x):
return 1 / (1 + np.exp(-x))
x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x,y)
plt.ylim(-0.1, 1.1)
plt.show()
시그모이드 함수와 계단 함수 비교
시그모이드 함수와 계단 함수의 가장 큰 차이점은 '매끄러움'이다. 시그모이드 함수는 부드러운 곡선이며 입력에 따라 출력이 연속적으로 변화한다. 이로 인해 계단 함수는 0과 1만 출력하며, 시그모이드 함수는 0과 1 사이의 모든 실수값을 출력한다.
그러나 두 함수에는 공통점도 존재한다. 두 함수 모두 입력이 작을 때의 출력은 0 혹은 0에 가까우며, 입력이 커지면 출력이 1 혹은 1에 가까워지는 구조이다. 즉, 입력이 중요하면 큰 값을 출력하고 입력이 중요하지 않으면 작은 값을 출력한다. 또한 입력이 아무리 작거나 커도 출력은 0에서 1 사이라는 것 또한 공통점이다.
비선형 함수
함수란 어떤 값을 입력하면 그에 따른 값을 돌려주는 변환기의 역할을 한다. 이 변환기에 무언가를 입력했을 때 출력이 입력의 상수배만큼 변하는 함수를 `선형 함수`라고 한다. 따라서 선형 함수는 곧은 1개의 직선이 된다. 반대로 직선 1개로 그릴 수 없는 함수, 즉 선형이 아닌 함수를 `비선형 함수`라고 한다. 시그모이드 함수는 곡선, 계단 함수는 계단처럼 구부러진 직선으로 나타나기 때문에 두 함수 모두 비선형 함수로 분류된다.
신경망에서는 반드시 활성화 함수로 비선형 함수를 사용해야 한다. 선형 함수를 신경망의 활성화 함수로 이용하면, 층을 깊게 하는 의미가 없기 때문이다. 즉, 선형 함수를 사용하면 층을 아무리 깊게 해도 은닉층이 없는 네트워크로도 똑같은 기능을 할 수 있다.
ReLU 함수(ReLU function)
지금까지 신경망의 활성화 함수로 시그모이드 함수가 많이 사용되었지만, 최근에는 `ReLU(Rectified Linear Unit)` 함수를 주로 이용한다. ReLU는 입력이 0을 넘으면 그 입력을 그대로 출력하고, 0 이하이면 0을 출력하는 함수이다. 그래프와 수식은 다음과 같다.
ReLU 함수를 코드로 구현하면 다음과 같다.
def relu(x):
return np.maximum(0, x)
3층 신경망 구현하기
3층 신경망에서 수행되는 입력부터 출력까지의 처리(순방향 처리)를 구현한다. 위 그림은 3층 신경망의 예시로, 입력층(0층)은 2개, 첫 번째 은닉층(1층)은 3개, 두 번째 은닉층(2층)은 2개, 출력층(3층)은 3개의 노드로 구성된다.
표기법 설명
신경망 구조를 이해하는 데 사용되는 표기법은 다음과 같다. 신경망에서의 계산을 행렬 계산으로 정리할 수 있다는 것이 핵심적인 내용이기 때문에 세세한 표기 규칙이 중요하진 않다.
입력층(0층)의 두 번째 노드(x2)에서 첫 번째 은닉층(1층)의 첫 번째 노드로 향하는 선 위에 가중치를 표시했다.
- 가중치 오른쪽 아래의 두 숫자는 순서대로 다음 층 뉴런과 앞 층 뉴런의 인덱스 번호
- 오른쪽 위의 숫자는 1층의 가중치
- 노드에서의 오른쪽 위의 숫자 또한 1층의 노드임을 의미
각 층의 신호 전달 입력하기
입력층에서 1층의 첫 번째 노드로 가는 신호를 나타냈다. 이전 그림에는 없던 편향을 뜻하는 노드가 추가되었다. 편향은 앞 층의 노드가 하나뿐이기 때문에 오른쪽 아래 인덱스가 1개밖에 없다. 오른쪽 수식에서는 첫 번째 층의 노드를 가중치를 곱한 신호 두 개와 편향을 합해서 표현했고, 이를 행렬의 곱을 이용하여 간소화했다. 이를 넘파이의 다차원 배열을 사용해서 구현하면 다음과 같다.
# 입력층에서 1층으로 신호 전달
X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])
A1 = np.dot(X, W1) + B1
은닉층에서의 가중치 합(가중 신호와 편햐으이 총합)을 a로 표기하고 활성화 함수 h()로 변환된 신호를 z로 표기한다. 여기에 활성화 함수는 앞서본 시그모이드 함수를 사용하면 다음과 같이 구성된다.
이를 코드로 구현하면 다음과 같다.
# 활성화 함수
Z1 = sigmoid(A1)
print(A1)
print(Z1)
출력 결과에서 확인할 수 있듯이, 시그모이드 함수를 통해 기존에 a값과 결과값이 달라지며 같은 수의 원소로 구성된 넘파이 배열을 반환한다.
다음은 1층에서 2층으로 가는 과정과 코드 구현이다.
# 1층에서 2층으로의 신호 전달
W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
B2 = np.array([0.1, 0.2])
print(Z1.shape)
print(W2.shape)
print(B2.shape)
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)
print(A2, Z2)
코드의 경우 1층의 출력 Z1이 2층의 입력으로 변경된다는 점 외에는 전의 코드와 동일하다. 마지막은 2층에서 출력층으로의 신호 전달이다. 이 또한 거의 동일하지만, 활성화 함수만 다르다. 출력층의 활성화 함수는 회귀에서는 항등 함수를, 이진 분류에서는 시그모이드 함수를, 다중 분류에서는 소프트맥스 함수를 사용하는 것이 일반적이다.
# 2층에서 출력층으로의 신호 전달
def identity_function(x):
return x
W3 = np.array([[0.1, 0.3], [0.2, 0.4]])
B3 = np.array([0.1, 0.2])
A3 = np.dot(Z2, W3) + B3
Y = identity_function(A3)
print(A3, Y)
3층 신경망 구현 정리
def init_network():
network = {}
network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
network['b1'] = np.array([0.1, 0.2, 0.3])
network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
network['b2'] = np.array([0.1, 0.2])
network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
network['b3'] = np.array([0.1, 0.2])
return network
def forward(network, x):
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = identity_function(a3)
return y
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y)
신경망 구현의 관례에 따라 가중치만 대문자로 쓰고, 편향이나 중간 결과 등은 모두 소문자로 썼다.
- `init_network()` : 가중치와 편향을 초기화하고 이들을 딕셔너리 변수인 network에 저장
- `forward()` : 입력 신호를 출력으로 변환하는 처리 과정을 구현
출력층 설계하기
항등 함수와 소프트맥스 함수 구현하기
신경망은 분류와 회귀 모두에 이용가능하다. 일반적으로 회귀에는 `항등 함수(identity function)`를, 분류에는 `소프트맥스 함수(softmax function)`를 사용한다. 항등 함수는 입력을 그대로 출력한다. 따라서 출력층에서 항등 함수를 사용하면 입력 신호가 그대로 출력 신호가 된다. 이를 신경망 그림으로 표현하면 다음과 같다.
분류에서 사용하는 소프트맥스 함수의 식은 다음과 같다.
- n은 출력층의 뉴런 수, y_k는 그중 k번째 출력
- 분자는 입력 신호의 지수 함수
- 분모는 모든 입력 신호의 지수 함수의 합
이를 신경망 그림으로 표현하면 다음과 같다.
소프트맥스 함수를 코드로 구현하면 다음과 같다.
# softmax 함수 구현
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
그러나 해당 코드에서는 `오버플로(overflow)` 문제가 발생한다. 이는 지수 함수의 값이 쉽게 매우 커질 수 있기 때문에 이를 컴퓨터가 표현할 수 없는 문제이다. 따라서 오버플로 문제를 개선하기 위해 소프트맥스 수식을 새롭게 수정하면 다음과 같다.
해당 식은 소프트맥스의 지수 함수를 계산할 때 어떤 정수를 더해거나 빼도 결과는 바뀌지 않는다는 것이다. 그러나 일반적으로 입력 신호 중 최댓값을 이용하여 대입한다. 이를 코드로 구현하면 다음과 같다.
# 수정된 소프트맥스 함수
def softmax(a):
c = np.max(a)
exp_a = np.exp(c - a) # 오버플로 대책
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
소프트맥스 함수의 특징
- 소프트맥스 함수의 출력은 0에서 1.0 사이의 실수
- 소프트맥스 함수의 출력의 총합은 1 → 확률로 해석
- 소프트맥스 함수를 이용함으로써 문제를 확률적(통계적)으로 대응
- 소프트맥스 함수를 적용해도 각 원소의 대소 관계는 변화 x
- 신경망을 이용한 분류에서는 일반적으로 가장 큰 출력을 내는 뉴런에 해당하는 클래스로만 인식
- 소프트맥스 함수를 적용해도 출력이 가장 큰 뉴런의 위치는 변화 x
- 기계 학습의 문제 풀이는 학습과 추론의 단계를 거치는데, 신경망의 학습 과정에선 출력층에서 소프트맥스 함수를 사용하며 추론 단계에서는 출력층의 소프트맥스 함수를 생략하는 것이 일반적
이 포스팅은 '밑바닥부터 시작하는 딥러닝'을 공부하고 작성한 글입니다. 해당 포스팅에 포함된 코드는 깃허브에서 확인 가능합니다.