03.4 3층 신경망 구현하기
이제 더 그럴싸한 신경망을 구현해보죠. 이번에는 [그림 3-15]의 3층 신경망에서 수행되는, 입력부터 출력까지의 처리(순방향 처리)를 구현하겠습니다. 이를 위해 앞에서 설명한 넘파이의 다차원 배열을 사용합니다. 넘파이 배열을 잘 쓰면 아주 적은 코드만으로 신경망의 순방향 처리를 완성할 수 있습니다.
그림 3-15 3층 신경망 : 입력층(0층)은 2개, 첫 번째 은닉층(1층)은 3개, 두 번째 은닉층(2층)은 2개, 출력층(3층)은 2개의 뉴런으로 구성된다.

지니의 모험 준비
- 지니: “도로시, 토토! 드디어 은닉층이 2개나 숨겨진 본격적인 ‘3층 신경망 성’에 도달했어! 0층(입력층), 1층(은닉층1), 2층(은닉층2), 3층(출력층)으로 이루어진 높은 성이야.”
- 도로시: “와! 층이 많아져서 더 깊고 복잡한 계산을 할 수 있겠네. 넘파이 행렬 연산을 활용해서 이 성의 꼭대기(출력층)까지 신호를 빠르게 통과시켜보자!”
- 토토: “왈왈! (성을 올려다보며) 높은 성이지만 지니의 지도와 넘파이 부스터가 있다면 금방 정복할 수 있다 멍!”
3층 신경망 성을 정복하기 위해 힘차게 출발하는 도로시, 지니, 그리고 토토
3.4.1 표기법 설명
이번 절에서는 신경망에서의 처리를 설명하며 $w_{12}^{(1)}$과 $a_1^{(1)}$ 같은 표기법을 선보입니다. 조금 복잡해 보일 수 있는데, 이번 절에서만 사용하는 표기이니 가볍게 건너뛰고 읽어도 문제는 없습니다.
NOTE_ 이번 절의 핵심은 신경망에서의 계산을 행렬 계산으로 정리할 수 있다는 것입니다. 신경망 각 층의 계산은 행렬의 내적으로 처리할 수(더 큰 관점에서 생각할 수) 있으니, 세세한 표기 규칙은 잊어버려도 앞으로의 설명을 이해하는 데 전혀 지장이 없습니다.
그럼 하나씩 정의해봅시다. [그림 3-16]을 보세요. [그림 3-16]은 입력층의 뉴런 $x_2$에서 다음 층의 뉴런 $a_1^{(1)}$으로 향하는 선 위에 가중치를 표시하고 있습니다.
그림 3-16 중요한 표기

[그림 3-16]과 같이 가중치와 은닉층 뉴런의 오른쪽 위에는 ‘$(1)$’이 붙어 있습니다. 이는 1층의 가중치, 1층의 뉴런임을 뜻하는 번호입니다. 또, 가중치의 오른쪽 아래의 두 숫자는 차례로 다음 층 뉴런과 앞 층 뉴런의 인덱스 번호입니다. 가령 $w_{12}^{(1)}$은 앞 층의 2번째 뉴런($x_2$)에서 다음 층의 첫 번째 뉴런($a_1^{(1)}$)으로 향할 때의 가중치라는 뜻입니다. 가중치 오른쪽 아래의 인덱스 번호는 ‘다음 층 번호, 앞 층 번호’ 순으로 적습니다.*
* 옮긴이_ 이 순서를 반대로 표기하는 경우도 많으니 다른 자료를 볼 때는 순서를 한 번씩 확인해보세요.
3.4.2 각 층의 신호 전달 구현하기
이번 절에서는 입력층에서 ‘1층의 첫 번째 뉴런’으로 가는 신호를 살펴보겠습니다. [그림 3-17]과 같은 상황이죠.
그림 3-17 입력층에서 1층으로 신호 전달

[그림 3-17]과 같이 편향을 뜻하는 뉴런인 ①이 추가되었습니다. 편향은 오른쪽 아래 인덱스가 하나밖에 없다는 것에 주의하세요. 이는 앞 층의 편향 뉴런(뉴런 ①)이 하나뿐이기 때문입니다.
그럼 지금까지 확인할 것을 반영하여 $a_1^{(1)}$을 수식으로 나타내봅시다. $a_1^{(1)}$은 가중치를 곱한 신호 두 개와 편향을 합해서 다음과 같이 계산합니다.
\[a_1^{(1)} = w_{11}^{(1)}x_1 + w_{12}^{(1)}x_2 + b_1^{(1)} \hfill \text{[식 3.8]}\]여기에서 행렬의 내적을 이용하면 1층의 ‘가중치 부분’을 다음 식처럼 간소화할 수 있습니다.
\[\mathbf{A}^{(1)} = \mathbf{XW}^{(1)} + \mathbf{B}^{(1)} \hfill \text{[식 3.9]}\]이때 행렬 $\mathbf{A}^{(1)}$, $\mathbf{X}$, $\mathbf{B}^{(1)}$, $\mathbf{W}^{(1)}$은 각각 다음과 같습니다.
\[\mathbf{A}^{(1)} = \begin{pmatrix} a_1^{(1)} & a_2^{(1)} & a_3^{(1)} \end{pmatrix}, \quad \mathbf{X} = \begin{pmatrix} x_1 & x_2 \end{pmatrix}, \quad \mathbf{B}^{(1)} = \begin{pmatrix} b_1^{(1)} & b_2^{(1)} & b_3^{(1)} \end{pmatrix}\] \[\mathbf{W}^{(1)} = \begin{pmatrix} w_{11}^{(1)} & w_{21}^{(1)} & w_{31}^{(1)} \\ w_{12}^{(1)} & w_{22}^{(1)} & w_{32}^{(1)} \end{pmatrix}\]그럼 넘파이의 다차원 배열을 사용해서 [식 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은 $2 \times 3$ 행렬, X는 원소가 2개인 1차원 배열입니다. 여기에서도 역시 W1과 X의 대응하는 차원의 원소 수가 일치하고 있군요.
이어서 1층의 활성화 함수에서의 처리를 살펴보겠습니다. 이 활성화 함수의 처리를 그림으로 나타내면 [그림 3-18]처럼 됩니다.
그림 3-18 입력층에서 1층으로의 신호 전달

[그림 3-18]과 같이 은닉층에서의 가중치 합(가중 신호와 편향의 총합)을 $a$로 표기하고 활성화 함수 $h()$로 변환된 신호를 $z$로 표기합니다. 여기에서는 활성화 함수로 시그모이드 함수를 사용하기로 합니다. 이를 파이썬으로 구현하면 다음과 같습니다.
Z1 = sigmoid(A1)
print(A1) # [0.3, 0.7, 1.1]
print(Z1) # [0.57444252, 0.66818777, 0.75026011]
이 sigmoid() 함수는 앞에서 정의한 함수입니다. 이 함수는 넘파이 배열을 받아 같은 수의 원소로 구성된 넘파이 배열을 반환합니다.
이어서 1층에서 2층으로 가는 과정([그림 3-19])과 그 구현을 살펴보죠.
그림 3-19 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층에서 출력층으로의 신호 전달입니다([그림 3-20]). 출력층의 구현도 그동안의 구현과 거의 같습니다. 딱 하나, 활성화 함수만 지금까지의 은닉층과 다릅니다.
그림 3-20 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()을 정의하고, 이를 출력층의 활성화 함수로 이용했습니다. 항등 함수는 입력을 그대로 출력하는 함수입니다. 그래서 이 예에서는 identity_function()을 굳이 정의할 필요는 없지만, 그동안의 흐름과 통일하기 위해 이렇게 구현했습니다. 또한 [그림 3-20]에서는 출력층의 활성화 함수를 $\sigma()$로 표시하여 은닉층의 활성화 함수 $h()$와는 다름을 명시했습니다($\sigma$는 ‘시그마’라고 읽습니다).
NOTE_ 출력층의 활성화 함수는 풀고자 하는 문제의 성질에 맞게 정합니다. 예를 들어 회귀에는 항등 함수를, 2클래스 분류에는 시그모이드 함수를, 다중 클래스 분류에는 소프트맥스 함수를 사용하는 것이 일반적입니다. 출력층의 활성화 함수에 대해서는 다음에 다시 자세히 설명합니다.
3.4.3 구현 정리
이로써 3층 신경망에 대한 설명은 끝입니다. 그럼 지금까지의 구현을 정리해보도록 하죠. 신경망 구현의 관례에 따라 가중치만 W1과 같이 대문자로 쓰고, 그 외 편향과 중간 결과 등은 모두 소문자로 썼습니다.
도로시의 파이썬 코딩 일지
- 도로시: “지니가 칠판에 적어준 복잡한 신경망 수식들을 파이썬 코드로 깔끔하게 합쳐놓으니 한눈에 들어와! 가중치 매개변수를 딕셔너리(
network)에 넣어두니까 관리하기도 정말 편하다.”- 지니: “그렇단다! 코드로 변환할 때는 가중치 매개변수(
W1,W2,W3)는 행렬 모양이니까 대문자로 쓰고, 편향(b1,b2,b3)이나 입력(x), 신호(a,z)는 소문자로 구별하는 게 파이썬 딥러닝 구현의 관례이자 좋은 코딩 습관이야.”- 토토: “왈왈! (노트북 화면의 초록색 코드 글씨들을 신나게 보며) 딕셔너리로 마법의 상자를 만들었다 멍!”
도로시가 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) # [ 0.31682708 0.69627909]
여기에서는 init_network()와 forward()라는 함수를 정의했습니다. init_network() 함수는 가중치와 편향을 초기화하고 이들을 딕셔너리 변수인 network에 저장합니다. 이 딕셔너리 변수 network에는 각 층에 필요한 매개변수(가중치와 편향)를 저장합니다. 그리고 forward() 함수는 입력 신호를 출력으로 변환하는 처리 과정을 모두 구현하고 있습니다.
함수 이름을 forward라 한 것은 신호가 순방향(입력에서 출력 방향)으로 전달됨(순전파)을 알리기 위함입니다. 앞으로 신경망 학습을 다룰 때 역방향(backward, 출력에서 입력 방향) 처리에 대해서도 살펴볼 예정입니다.
이로써 신경망의 순방향 구현은 끝입니다. 보신 것처럼 넘파이의 다차원 배열을 잘 사용하면 신경망을 효율적으로 구현할 수 있습니다!
지니의 마법 정리
- 지니: “도로시, 입력값
x에서 출발하여 출력값y까지 물 흐르듯 순서대로 나아가는 이 과정을 순방향 처리(Forward propagation)라고 한단다. 다음 장에서는 오차를 거꾸로 전파하여 가중치들을 스스로 고치게 하는 역방향 처리(Backward)도 배우게 될 거야!”- 도로시: “와! 신호를 앞으로 던져 전달하는 순전파 흐름이 정말 상쾌해! 역방향으로 배우는 자동 수정 마법도 얼른 해보고 싶어!”
- 토토: “멍멍! (앞발로 앞쪽을 가리키며) 앞으로 가고, 뒤로 가고! 다 정복할 수 있다 멍!”
도로시가 던진 마법 신호(입력)를 토토가 물어 지니에게 전하는 릴레이를 통해, 입력에서 출력으로 나아가는 순방향 처리 과정을 묘사하고 있어요.