이번에는 [그림 3-14]의 3층 신경망에서 수행되는, 입력부터 출력까지의 처리 (순방향 처리) 를 구현해보자.
이를 위해 앞에서 설명한 넘파이의 다차원 배열을 사용한다. 넘파이 배열을 잘 쓰면 아주 적은 코드만으로도 신경망의 순방향 처리를 완성할 수 있다.
3.4.1 표기법 설명
이번 절에서는 신경망에서의 처리를 설명하며 \(w_{12}^{\left( 1\right) }\) 과 \(a_{1}^{\left( 1\right) }\) 같은 표기법이 나온다.
[그림 3-16]을 보면 입력층의 뉴런 \(x_{2}\)에서 다음 층의 뉴런 \(a_{1}^{\left( 1\right) }\)으로 향하는 선 위에 가중치를 표시하고 있다.
[그림 3-16]과 같이 가중치와 은닉층 뉴런의 오른쪽 위에는 '\(^\left( 1\right)\) '이 붙어 있다. 이는 1층의 가중치, 1층의 뉴런임을 뜻하는 번호이다. 또 가중치 오른쪽 아래의 두 숫자는 차례대로 다음 층 뉴런과 앞 층 뉴런의 인덱스 번호이다.
예를 들어 \(w_{12}^{\left( 1\right) }\)은 앞 층의 2번째 뉴런(\(x_{2}\)에서 다음 층의 1번째 뉴런\(a_{1}^{\left( 1\right) }\)으로 향할 때의 가중치라는 뜻이다. 가중치 오른쪽 아래 인덱스 번호는 '다음 층 번호, 앞 층 번호' 순으로 적는다.
3.4.2 각 층의 신호 전달 구현하기
이번 절에서는 입력층에서 '1층의 첫 번째 뉴런'으로 가는 신호를 살펴본다. [그림 3-17]과 같은 상황이다.
[그림 3-17]에는 편향을 뜻하는 뉴런인 ①이 추가되었다. 편향은 앞 층의 뉴런이 하나뿐이기 때문에 오른쪽 아래 인덱스가 하나밖에 없다.
그럼 \(a_{1}^{\left( 1\right) }\)을 수식으로 나타내보자. \(a_{1}^{\left( 1\right) }\)은 가중치를 곱한 신호 두 개와 편향을 합해서 다음과 같이 계산한다.
여기에서 행렬의 곱을 이용하면 1층의 '가중치 부분'을 다음 식처럼 간소화할 수 있다.
이때 행렬 \(A_{1}^{\left( 1\right) }\), \(X\), \(B_{1}^{\left( 1\right) }\), \(W_{1}^{\left( 1\right) }\)은 각각 다음과 같다.
그럼 넘파이 다차원 배열을 사용해 [식 3.9]를 구현해보자. (입력 신호, 가중치, 편향은 적당한 값을 임의로 설정)
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])
print(W1.shape) # (2, 3)
print(X.shape) # (2,)
print(B1.shape) # (3,)
A1 = np.dot(X, W1) + B1
이 계산은 앞 절에서 한 계산과 같다. 여기서도 역시 W1과 X에 대응하는 차원의 원소 수가 일치한다.
이어서 1층의 활성화 함수에서의 처리를 살펴보자. 이 활성화 함수의 처리를 그림으로 나타내면 [그림 3-18]처럼 된다.
[그림 3-18]과 같이 은닉층에서의 가중치 합을 \(a\)로 표기하고 활성화 함수 \(h\)()로 변환된 신호를 \(z\)로 표기한다.
여기에서는 활성화 함수로 시그모이드 함수를 사용하기도 한다.
이를 파이썬으로 표현하면 다음과 같다.
Z1 = sigmoid(A1)
print(A1) # [0.3, 0.7, 1.1]
print(Z1) # [0.57444252, 0.66818777, 0.75026011]
이어서 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) # (3,)
print(W2.shape) # (3, 2)
print(B2.shape) # (2,)
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)
1층의 출력 Z1이 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) # 혹은 Y = A3
여기에서는 항등 함수인 identity_function()을 정의하고, 이를 출력층의 활성화 함수로 이용했다. 입력 그대로 출력하는 함수이다. 굳이 함수를 정의할 필요는 없다. 또한 [그림 3-20]에서는 출력층의 활성화 함수를 \(\sigma \)로 표시해 은닉층의 활성화 함수 \(h\)()와는 다름을 명시했다.
cf) 출력층의 활성화 함수는 풀고자 하는 문제의 성질에 맞게 정의한다. 예를 들어 회귀에는 항등함수, 2클래스 분류에는 시그모이드 함수, 다중 클래스 분류에는 소프트맥스 함수를 사용하는 것이 일반적이다.
3.4.3 구현 정리
지금까지의 구현을 정리해보자. 신경망 구현 관례에 따라 가중치만 \(W\)1과 같이 대문자로 쓰고, 그 외 편향과 중간 결과 등은 소문자로 썼다.
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']
// 입력층 >> 1층
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
// 1층 >> 2층
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
// 2층 >> 출력층(3층)
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) // 신경망 거쳐 입력 신호를 출력으로 변환, 순방향(forward)
print(y) # [0.31682708 0.69627909]
foward()함수는 신호가 순방향(입력에서 출력 방향)으로 전달됨(순전파)를 알리기 위함이다. 앞으로 신경망 학습을 다룰 때 역방향(backward, 출력에서 입력 방향) 처리에 대해서도 알아볼 예정이다.
'Data Science > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글
손실함수 (0) | 2023.07.25 |
---|---|
데이터에서 학습한다! (0) | 2023.07.15 |
Chapter 3.3 다차원 배열의 계산 (0) | 2023.05.24 |
Chapter 3.2 활성화 함수 (0) | 2023.05.24 |
Chapter 3.1 퍼셉트론에서 신경망으로 (0) | 2023.05.23 |