중간 정리 : 신경망의 순전파

1. 신경망의 순전파

1-1. 신경망과 퍼셉트론

1-1-1. 신경망과 퍼셉트론의 공통점

신경망에서, 각 뉴런은 다음 층의 뉴런으로 신호를 전달한다는 측면에서 퍼셉트론과 유사한 부분이 있음을 알았다.

1-1-2. 신경망과 퍼셉트론의 차이점

그러나 퍼셉트론에서는 활성화 함수로 계단 함수를 사용했다. 두 개의 입력이 퍼셉트론에 가해진다고 가정했을 때, 수식은 아래와 같았다.

y=h(W1x1+W2x2+b)h(x)={0(x0)1(x>0)y = h(W_1x_1+W_2x_2+b)\\ h(x)=\begin{cases} 0(x\le0)\\ 1(x>0) \end{cases}

[h(x)h(x)는 계단 함수(Step Function)]
그러나 신경망에서는 다른 비선형 함수들도 활성화 함수로 사용한다.

h(x)=11+exh(x)={1 \over {1+e^{-x}}}

[Sigmoid Function]

h(x)={0(x0)x(x>0)h(x)=\begin{cases} 0(x\le0)\\ x(x>0) \end{cases}

[ReLU Function]

1-2. 신경망

1-2-1. 활성화 함수로서의 선형 함수와 비선형 함수

선형 함수를 활성화 함수로 사용하면 층을 깊게 하는 이점을 살릴 수 없다. 아래와 같은 함수가 있다고 가정해보자.

h(x)=axh(x)=ax

위 함수를 활성화 함수로 하여 3층 네트워크를 구성하면, 아래와 같은 식이 될 것이다.

h(h(h(x)))=a3xh(h(h(x))) = a^3x

이것은 본래의 형태인 h(x)=axh(x)=ax

h(x)=cxh(x)=cx

즉, 선형 함수를 활성화 함수로 사용한 네트워크는 단층으로도 표현할 수 있다. 다시 말해, 층을 쌓는 이점을 살리려면 비선형 함수를 사용해야 한다는 것이다.

1-2-2. 행렬을 이용한 신경망 구현

참고: https://velog.io/@developerkerry/3층-신경망-구현하기


참고 포스트에서 3층 신경망을 구현한 적 있다. 행렬 없이 위와 같은 신경망을 구현하려면 엄청난 노가다가 필요할 것이다. 하지만, np.array로 행렬을 만들고, np.dot() 함수로 행렬곱을 수행하면 위 그림과 같은 신경망을 코드 몇 줄로 구현할 수 있다.

입력은 xnx_n

여기서 a1(1)a_1^{(1)}

a1(1)=w11(1)x1+w12(1)x2+b1(1)a_1^{(1)} = w_{11}^{(1)}x_1+w_{12}^{(1)}x_2+b_1^{(1)}

여기에서, 행렬곱을 도입하면, 0층에서 1층으로 향하는 모든 신호를 다음 식처럼 간소화할 수 있다.

A(1)=XW(1)+B(1)A^{(1)}=XW^{(1)}+B^{(1)}

이때 행렬 A(1)A^{(1)}, XX, B(1)B^{(1)}, W(1)W^{(1)}는 아래와 같이 나타내면 끝이다.

A(1)=[a1(1)a2(1)a3(1)]A^{(1)}=\left[ \begin{matrix} a^{(1)}_1&a^{(1)}_2&a^{(1)}_3 \end{matrix} \right]
X=[x1x2]X=\left[\begin{matrix} x_1 & x_2 \end{matrix} \right]
B(1)=[b1(1)b2(1)b3(1)]B^{(1)}=\left[\begin{matrix} b^{(1)}_1&b^{(1)}_2&b^{(1)}_3 \end{matrix} \right]
W(1)=[w11(1)w21(1)w31(1)w12(1)w22(1)w32(1)]W^{(1)}=\left[\begin{matrix} w^{(1)}_{11}&w^{(1)}_{21}&w^{(1)}_{31}\\ w^{(1)}_{12}&w^{(1)}_{22}&w^{(1)}_{32} \end{matrix} \right]

위 식의 결과를 활성화 함수에 통과시키면...

Z(1)=h(XW(1)+B(1))Z^{(1)}=h(XW^{(1)}+B^{(1)})

1층에서 2층으로 넘어가는 신호는 아래처럼 나타낼 수 있다.

W(2)=[w11(2)w21(2)w12(2)w22(2)w13(2)w23(2)]W^{(2)}=\left[\begin{matrix}w^{(2)}_{11}&w^{(2)}_{21}\\w^{(2)}_{12}&w^{(2)}_{22}\\w^{(2)}_{13}&w^{(2)}_{23} \end{matrix}\right]
B(2)=[b1(2)b2(2)]B^{(2)}=\left[\begin{matrix}b^{(2)}_1&b^{(2)}_2 \end{matrix}\right]
A(2)=Z(1)W(2)+B(2)Z(2)=h(A(2))A^{(2)}=Z^{(1)}W^{(2)}+B^{(2)}\\ Z^{(2)}=h(A^{(2)})

마지막으로 2층에서 3층으로 가는 신호는...

W(3)=[w11(3)w21(3)w12(3)w22(3)]W^{(3)}=\left[\begin{matrix}w^{(3)}_{11}&w^{(3)}_{21}\\w^{(3)}_{12}&w^{(3)}_{22}\end{matrix}\right]
B(3)=[b1(3)b2(3)]B^{(3)}=\left[\begin{matrix}b^{(3)}_1&b^{(3)}_2\end{matrix}\right]
A(3)=Z(2)W(3)+B(3)y=h(A(3))A^{(3)}=Z^{(2)}W^{(3)}+B^{(3)}\\ y=h(A^{(3)})

1-2-3. 신경망 입력층 설계

신경망에서의 입력층의 뉴런 개수는 입력 데이터의 원소 수만큼 만들어줘야 한다. 예를 들어, MNIST 데이터셋의 손글씨 이미지의 경우 28px * 28px이므로, 28 * 28 = 784개의 입력층 뉴런이 필요하다.

1-2-4. 신경망 출력층 설계

출력층의 뉴런 개수는 입력에 대한 출력의 개수만큼으로 설정해 줘야 한다.

예를 들어, 주어진 입력에 따라 어떤 하나의 값을 예측하는 회귀 모델인 경우 출력층에 한 개의 뉴런을 둬야 한다. 만약 어떤 이미지에 쓰인 글씨가 0~9중 어떤 숫자인지 분류하는 모델이라면 출력층에 10개의 뉴런을 둬야 한다. 즉, 분류하려는 Class의 수와 출력층 뉴런의 수를 같게 설정해야 한다.


출력층의 뉴런에는 은닉층과는 다른 활성화 함수를 이용하는 것이 일반적이다.

회귀 모델의 출력층에는 항등 함수를 쓴다. 항등 함수란, 입력을 그대로 출력으로 내놓는 함수이다.

의사 코드로 나타내자면, 아래와 같다.

y = predict(network, x)

혹은

def identity_func(x):
    return x
...
y = identity_func(predict(network, x))

항등 함수는 아래 수식처럼 나타낼 수 있다.

h(x)=xh(x)=x

만약 2 Class 분류 문제라면 Sigmoid 함수를 출력층의 활성화 함수로 사용한다.

h(k)=eaki=1neaih(k)={e^{a_k}\over{\displaystyle\sum^n_{i=1}e^{a_i}}}
def softmax_func(x):
    exp_x = np.exp(x)
    sum_exp_x = np.sum(exp_x)
    return exp_x / sum_exp_x

[Softmax Function]

다중 클래스 분류 문제라면 Softmax 함수를 출력층의 활성화 함수로 사용한다.

1-2-5. Softmax Function

Softmax 함수는 다음과 같은 특징이 있다.

  • 출력층에 적용된 Softmax 함수의 출력은 출력층 전체에 입력되는 모든 값에 영향을 받는다.
  • Softmax 함수가 적용된 Array는 모든 원소의 합이 1이다.
  • 따라서 각 원소의 값을 '입력이 해당 인덱스에 대응하는 클래스일 확률'로 해석할 수 있다.
  • 입력의 대소관계가 곧 출력의 대소관계이다.

그러나 Softmax 함수에는 치명적인 단점이 있는데, 바로 지수 함수라는 점이다. 지수 함수는 입력에 따른 결괏값이 기하급수적으로 커진다. 즉, 컴퓨터로 계산 시에 오버플로우가 날 가능성이 크다. 그래서 원래의 Softmax 함수를 약간 변형해서 사용하곤 한다.

h(k)=eaki=1naih(k)={e^{a_k}\over\displaystyle\sum^n_{i=1}a^i}
=CeakCi=1nai={Ce^{a_k}\over{C\displaystyle\sum^n_{i=1}a^i}}
=eak+logCi=1nai+logC={e^{a_k+\log C}\over\displaystyle\sum^n_{i=1}a^{i+\log C}}
=eak+Ci=1nei+C={e^{a_k+C'}\over{\displaystyle\sum_{i=1}^ne^{i+C'}}}

위 식을 통해 ee의 지수, ii, kk에 임의의 수를 더하거나 빼도 결과는 바뀌지 않는다는 사실을 알 수 있다. 따라서, 오버플로우를 막기 위해 보통적으로 입력 배열의 최댓값을 입력 배열의 각 원소에서 빼서 사용을 한다.

이를 Python으로 구현해보면 아래와 같다.

def softmax_func(x):
    max_x = np.max(x)
    exp_x = np.exp(x - max_x)
    sum_exp_x = np.sum(exp_x)
    
    return exp_x / sum_exp_x

1-2-6. 배치 처리

참고: https://velog.io/@developerkerry/배치-처리

처음으로 구현한 MNIST 손글씨 숫자 인식 모델의 경우, 아래와 같이 행렬곱이 수행되었다.

(1,784)(784,50)(50,100)(100,10)=(1,10)(1, 784) * (784, 50) * (50, 100) * (100, 10) = (1, 10)

그러나 이런 방식을 그대로 코드로 구현해 사용하면 이미지 데이터 한 장을 불러와 CPU에 넘겨주고, 연산을 수행하는 일련의 과정이 반복된다.

MNIST 손글씨 숫자 인식 모델의 경우에는 1) 데이터 수가 그리 많지 않고, 2) 신경망의 크기가 크지 않아 데이터 I/O Latency에 의한 유휴 시간이 있어도 금방 1만 장의 Test data prediction이 완료되었다.

그러나 모델이 크고, 복잡하고, 데이터의 수가 많은 경우 I/O Latency가 쌓여서 학습 속도나 추론 속도가 크게 느려지는 일이 발생한다. 그래서 한 번에 여러 개의 데이터를 묶어서 신경망에 넘겨서 학습/추론을 하도록 하는데, 이때 이 데이터의 묶음을 배치(batch)라고 한다.

배치의 크기, 즉 배치 사이즈는 Hyper Parameter로서 사람이 직접 적절한 값을 줘야 한다. 배치를 적절히 활용하면 I/O Latency를 줄여 신경망의 학습 및 추론 속도를 크게 높일 수 있다.

1-2-7. 정규화와 전처리

참고: https://velog.io/@developerkerry/신경망으로-손글씨-숫자-인식하기

정규화(normalization)란 어떤 데이터의 요소들을 일정 범위의 값으로 변환하는 것을 말한다. 참고 포스트에서는 원래 0~255 범위이던 MNIST 손글씨 숫자 이미지의 각 픽셀 값을 0.0~1.0 사이로 정규화 했다.

이처럼 데이터를 정규화 하는 등 신경망 학습/추론에 앞서 데이터를 적절히 변환하거나 어떤 처리를 가하는 것을 데이터 전처리(Data Preprocessing)라고 한다. 참고 포스트에서는 데이터 전처리로 정규화를 수행한 것이다.

좋은 웹페이지 즐겨찾기