인공 신경망의 학습 과정(Learning Process of ANN)

(인공 신경망을 모르신다면 이 링크 먼저 보시는게 좋아요! 인공 신경망)

인공 신경망은 어떻게 학습할까?

인공 신경망 학습 과정

위 이미지(속칭 움짤)는 인공 신경망의 학습 과정을 나타낸 것이다.
먼저 움짤이 보여주는 순서대로 인공 신경망의 학습 과정을 말로 풀어보자.

① : 데이터 입력(입력층) -> 가중치-편향 연산 & 활성화 함수 연산 반복 수행(은닉층)
② : ①의 연산 결과(예측값) 출력(출력층)
③ : 예측값과 실제 값의 차이 계산 by 손실 함수
④ : ③의 계산 결과에 따라 가중치 갱신 by 경사 하강법
⑤ : 기준 만족할 때까지 ①~④ 과정 반복
(기준 : 학습 횟수, 성능 평가 점수 등 사용자 지정 요소)

여기서 ①, ②가 이루어지는 과정을 순전파(Forward Propagation),
③은 손실 계산 (또는 그냥 손실 함수 그 자체),
④는 역전파(Backward Propagation)라고 한다.

그리고 ①부터 ④까지 한 번 수행된 것을 두고 iteration 또는 epoch라고 하는데, 이는 학습시키는 데이터의 단위(=배치 사이즈(batch size))에 따라 다르다.
만약 데이터가 잘게 나뉘어서 조각조각 학습되었다면 하나의 조각이 위 과정을 한 번 수행했을 때 이를 1 iteration이라고 한다.
그리고 잘게 나뉘었든 통짜든 전체 데이터 뭉치가 한 번 위 과정을 거쳤다면 이를 1 epoch라고 한다.
자세한 내용은 아래에서 다루겠다.

※ 참고 : iteration과 epoch의 의미

iteration
the process of doing something again and again, usually to improve it, or one of the times you do it

epoch
a long period of time, especially one in which there are new developments and great change

- Cambridge English Dictionary

그럼 구체적으로 인공 신경망의 각 학습 과정에서 무슨 일이 벌어지고 있는지 들여다 보자.

순전파(Forward Propagation; Feed Forward)

순전파와 역전파를 순(順)과 역(逆), 전파(傳播)로 나누어서 보자.
전파(傳播)는 무언가를 널리 전달하여 퍼지게 하는 것이다.
순(順)은 순한 또는 순응한다는 뜻이고 역은 거꾸로, 거스른다는 뜻이다.

그럼 무엇을 전파하는 것인가? 간단하다. 입력받은 값이다.
자, 무언가가 들어왔다. 그럼 나갈 곳도 있어야 하는게 순리이다(월급이 들어오면 기다렸다는 듯이 카드값이 나가듯...).
따라서 순전파는 들어온 값이 나가는 방향으로 자연스럽게 퍼져가는 것이다.
반대로 역전파는 나갈 곳에서 나가지 않고 거꾸로 들어온 방향으로 거슬러 가는 것이다.

인공 신경망에서의 순전파도 똑같은 이치이다.

1) 이전 단계(입력층 또는 이전 은닉층)에서 값이 들어온다.
2) 이에 대해 가중치-편향 연산을 수행한다.
3) 그리고 가중합으로 얻은 값을 활성화 함수를 통해 다음 층으로 전달한다.

손실 함수(Loss Function)

연습생(입력층)부터 시작해 하드 트레이닝(순전파)을 거쳐온 $x_1$(입력값)이 마침내 최종 오디션(출력층)에 도달하였다.
이제 세상에 나를 드러내기만 하면 된...다고 생각하던 찰나, 대표님(손실 함수 J)이 말한다.
"응 아직 아니야, 돌아가~ 돌아가는 길에 스타일이랑 컨셉 싹 고쳐서 다시 트레이닝 해달라고 해~ 너 아직 데뷔 못해~"

손실 함수는 순전파를 통해 출력층에 도달한 값, 즉 신경망의 예측값을 실제 데이터의 타겟 값과 비교하여 그 차이를 계산한다.
차이를 계산하는 방법은 다양한데, Keras 기준으로 자주 사용되는 몇 가지는 다음과 같다.

  • binary_crossentropy
  • categorical_crossentropy$^*$$^1$
  • sparse_categorical_crossentropy$^*$$^1$
  • 그 외 mean_squared_error, mean_absolute_error 등

차이 계산을 마쳤으면 그 차이를 줄이기 위해서 먼 길을 험난하게 거쳐온 출력값들을 가차없이 되돌려보낸다.

그냥 보내는가? 아니다. 왔던 길 되돌아가면서 각 길목 담당자(노드)한테 다음 번에 더 잘 할 수 있게 개선 좀 하라고 하며 돌려보낸다.

역전파(Backward Propagation; Backpropagation)

"트레이너(노래 담당 - 노드)님, 대표님이 저 다시 하래요. 근데 스타일이랑 컨셉 싹 다 고치래요."
"그래? 그럼 창법을 두성으로 바꾸자!"
"트레이너(댄스 담당 - 노드)님, 대표님이 저 다시 하래요. 근데 스타일이랑 컨셉 싹 다 고치래요."
"그래? 그럼 힙합 스타일로 가자!"
"트레이너님", "트레이너님", "트레이너님" ...
... 모든 트레이너와 스타일 및 컨셉 논의를 마치고서 $x_1$은 다시 연습생이 되어야 한다.

자, 손실 함수한테 퇴짜맞고서 이제 왔던 길을 되돌아가려고 한다.
그런데 그냥 터덜터덜 가면 안 되고, 가는 길목 길목 담당자마다 만나서 개선해야 한다 말하고 실제로 개선까지 같이 해야 되돌아갈 수 있다!

역전파는 손실 함수를 통해서 얻은 손실 정보를 출력층부터 입력층까지 전달하여 매 단계 가중치가 어떻게 조정되어야 할지 구하는 과정이다.

그럼 이 가중치는 어떻게 조정을 하는가?
이때 사용되는 방법이 바로 경사 하강법(GD; Gradient Descent)이다.

경사 하강법(GD; Gradient Descent)

"$x_1$. 어디 가니?"
"앗, GD 선배님! 저 대표님에게 퇴짜맞아서 다시 처음으로 되돌아가는 중입니다..."
"그렇군. 다음 번에 또 퇴짜맞으면 안 되잖아, 그렇지?"
"네 맞아요! 근데 저 어떻게 해야할지 모르겠어요..."
"걱정마! 내가 너에게 또 퇴짜맞지 않도록 가이드를 해주지!"
"!!!"

경사 하강법은 역전파 과정에서 손실 함수의 값을 줄이기 위한 방법 중의 하나이다.
우선 손실 함수를 표현한 그래프를 보자.

가로축은 가중치, 세로축은 손실 함수 값이다.
손실 함수의 값을 줄이기 위해서는 맨 아래에 위치한 최소값을 향해서 가야 할텐데, 어떻게 도달할 수 있을까?
간단하다. 손실 함수의 기울기가 작아지는 방향으로 가중치를 갱신하는 것이다.

기울기는 어떻게 구하지?
간단하다. 손실 함수를 미분한 함수, 즉 손실 함수의 도함수가 바로 기울기이다.
따라서 손실 함수의 도함수를 계산하여 이 값이 작아지도록 가중치를 조정한다.

그럼 구체적으로 가중치 갱신 과정을 알아보자.


가중치 갱신 과정


위 이미지는 $i$번째 가중치인 $\theta_i$가 갱신되는 모습을 수식으로 표현한 것이다.
$\eta$는 경사를 얼마나 큰 보폭으로 내려갈지 결정하는 것이고,
$\theta_{i,j}$의 $j$는 iteration을 의미한다.
$J(\theta)$는 가중치 $\theta$에 대한 손실 함수이며,
$\partial$는 편미분 기호이다.

해당 가중치에 대한 손실 함수의 기울기는 손실 함수를 해당 가중치로 편미분한 $\frac{\partial J(\theta)}{\partial \theta_{i, j}}$로 표현된다.

기울기가 작아지는 방향으로 가중치를 움직여야 하므로 기울기 값에 음의 부호(-)를 붙인다. 이러면 양수 기울기는 음수로, 음수 기울기는 양수가 되어 매 갱신마다 가중치가 손실 함수의 최소값에 가까워지도록 만든다.

가중치 조정 결과


역전파 편미분 계산 과정 with 연쇄 법칙(Chain Rule)

위에서 가중치 $\theta_{i, j}$에 대한 손실 함수의 기울기는 $\frac{\partial J(\theta)}{\partial \theta_{i, j}}$인 것을 확인했다. 근데 이건 또 어떻게 구하지?
여기서 편미분과 함께 연쇄 법칙이 사용된다.

먼저 가중치, 이전 입력값, 편향과 현재 입력값, 그리고 출력값의 손실 함수로 이루어진 그래프를 그려보자.

$wx+b$가 활성화 함수 $Z$를 거쳐서 출력값 $y$가 되었고, y는 손실 함수에 전해져 손실 함수값 J가 나왔다.
$\partial w$, $\partial Z$, $\partial y$, $\partial J$는 $w$, $Z$, $y$, $J$ 각각을 미분한 것을 의미한다.

여기서 가중치에 대한 손실 함수의 미분값(기울기)는 $\frac{\partial J}{\partial w}$로 나타낼 수 있는데, 이를 한 번에 계산할 수가 없다.
왜냐하면 $J$와 $w$ 사이에 여러 가지 계산 과정이 자리를 잡고 있기 때문이다. 따라서 중간 보스 격파 후에 최종 보스를 깨듯이 이 각각의 단계를 순차적으로 깨야 한다. 이 과정은 아래와 같이 표현된다.

$$\frac{\partial J}{\partial w} = \frac{\partial J}{\partial y} \cdot \frac{\partial y}{\partial Z} \cdot \frac{\partial Z}{\partial w}$$

이런 식으로 $\partial J$와 $\partial w$의 중간에 위치한 노드도 미분 과정에 포함시켜 이어붙이는 것이다. 이러한 계산 방식을 연쇄 법칙(Chain Rule)이라고 한다.

※ 손실 함수를 왜 J라고 표시할까?

명확한 출처는 찾지 못했으나, 인터넷 상에 올라온 의견들을 보면 오래 전부터 미적분학에서 사용했다거나, 'Jacobian'에서 따왔다거나, Loss에서 L을 거울에 비추듯 뒤집은 것이라는 등의 내용이 있었다.
(확실한 내용을 아시는 분께서는 코멘트 남겨주시면 감사하겠습니다!)


옵티마이저(Optimizer)

"감사합니다, GD 선배님! 덕분에 제 스타일이랑 컨셉도 저에게 딱 맞게 다 바꿨어요! 근데 저뿐만 아니라 저랑 비슷한 처지인 친구들이 산더미인데, 혹시..."
"아, 그러면 나 말고 나보다 빠릿빠릿한 친구가 있거든? 그 친구한테 가봐. 이름은 SGD야."

위에서 경사 하강법을 통해서 손실 함수의 기울기를 계산하여 가중치를 갱신한다는 것을 알았다.
허나 경사 하강법은 모든 입력 데이터에 대해서 손실 함수의 기울기를 계산하기 때문에, 데이터가 커질 경우에는 계산 과정이 상당히 오래 걸리게 된다.
이 문제를 각자 나름의 방법으로 최적화하는 친구들이 있으니, 이 친구들을 묶어서 옵티마이저라고 한다.

단순하게 말하자면, 가파른 경사길을 어떤 방식으로 내려올 것인지 지정하는 것이다.
후딱 빠르게 내려갈지, 천천히 주변을 살필지, 보폭을 크게 혹은 작게 할지 등등.

우선 너무 꼼꼼한 나머지 대량의 데이터에는 굼떠버리는 GD를 대신하여 신속함(달리 말하면 성급함)을 미덕으로 삼는 SGD(Stochastic Gradient Descent)를 먼저 만나보자.

1) 확률적 경사 하강법(SGD; Stochastic Gradient Descent)

"(연습생 그룹) 안녕하세요 SGD 선배님, 잘 부탁드립니다!"
"왔구나? 오케이, 일단 너 하나 먼저 가자!"
"에?"
(잠시 뒤)
"우선 한 번 됐고, 이런 식으로 하나씩 돌아가면서 다들 어떻게 할지 각을 재보자!"
'(연습생 그룹) 뭔가... 불안하다... !'

확률적 경사 하강법은 전체 데이터에서 하나의 데이터만 뽑아서 손실 계산 및 역전파 과정을 수행하는 방식이다.
즉, 한 번의 iteration마다 1개의 데이터만 사용하는 것이다.

데이터를 1개만 쓰다 보니 가중치를 업데이트하는 속도는 매우 빠르다.
문제는 1개만 쓰기 때문에 학습 과정에서 경사 하강이 이리 갔다 저리 갔다 불안불안 불안정한 모습을 보인다.

전부 다 투입시키자니 한 세월이 가고,
그렇다고 하나씩 보내서 빨리 빨리 하려니 가는 애들마다 이랬다 저랬다 난리라서 불안하고...
그러면 적당히 작은 그룹으로 나눠서 보내면 전부 다 보내는 것보다는 빠르고, 하나만 보내는 것보다는 덜 왔다 갔다 하지 않을까?

그래서 나온 방법이 미니 배치 경사 하강법(Mini-batch GD)이다.

2) 미니 배치 경사 하강법(Mini-batch GD)

"한 명씩 보내니까 너무 이랬다 저랬다 하네?
그럼 이제부터는 그룹 단위로 가서 맞춰보자!"

전체 데이터를 작게 쪼개어서 학습 과정에 보내는 방식이다.
어느 정도의 크기로 나누는가에 따라 학습 속도나 가중치 갱신의 최적점 도달 등의 결과가 달라진다.
여기서 데이터를 나누어주는 크기를 배치 사이즈(Batch size) 라고 한다.

  • 배치 사이즈(Batch size)
    • 미니 배치 경사 하강법에서 사용하는 미니 배치의 크기
    • 일반적으로 2의 배수로 설정
    • 메모리 용량에 여유가 있다면 가능한 한 큰 배치 사이즈를 쓰는 것이 안정적인 학습 진행에 유리함

배치 사이즈가 작을 때

배치 사이즈가 작아질수록 경사 하강법의 가중치 갱신이 불안정해진다. 이로 인해 최적점 도달에 많은 iteration이 필요해진다.

하지만 동시에 불안정, 즉 노이즈(noise)가 많기 때문에 지역 최적점(Local Minima)에서 빠져나와 전역 최적점(Global Minima)에 도달할 확률이 높아진다는 장점도 있다.

배치 사이즈가 클 때

배치 사이즈가 커질수록 가중치 갱신이 안정적이므로 최적점 도달에 비교적 적은 iteration만 필요해진다.

그러나 배치 사이즈를 너무 크게 설정하면 메모리 용량을 초과해버리는 Out-of-Memory 문제가 발생할 수도 있으므로 적당한 배치 사이즈를 설정해야 한다.

데이터 수와 배치 사이즈, iteration의 관계
$$
\text{# of Data = Batch size} \times \text{Iteration}
$$
순전파부터 역전파까지, 즉 가중치를 한 번 갱신하는 단위가 iteration이다. 그리고 미니 배치 경사 하강법에서는 배치 사이즈 단위로 iteration을 진행한다.

따라서 전체 데이터의 수는 배치 사이즈와 iteration을 곱한 값이라고 할 수 있다.

3) 그 외 옵티마이저들

GD와 SGD, Mini-batch GD 이후로 아래와 같이 다양한 옵티마이저들이 나왔다.

각각이 가진 특징을 간단한 비유로 표현해보겠다.

  • Momentum : 가던 방향으로 더 빨리 잘 가게 밀어주기
  • AdaGrad : 모르는 곳은 휙휙 둘러보고, 아는 곳은 자세하게 들여다 보기
  • RMSProp : 매 발걸음마다 내가 가는 이 길이 맞는지 아닌지 점검하며 가기
  • AdaDelta : AdaGrad의 개선 버전. 자세히 보는건 좋은데 멈추진 말고 계속 가면서 보기
  • Adam : AdaGrad와 RMSProp의 퓨-전(fusion). AdaGrad처럼 상황에 따라 보폭 조절하고, RMSProp처럼 제대로 가고 있는지 봐가면서 움직이기

이 중에서 Adam이 현재 가장 널리 사용되는 옵티마이저이다.
그렇다고 모든 상황에서 만능은 아니므로, 다루는 데이터의 성질과 해결할 문제 등을 고려하여 옵티마이저를 선택해야 할 것이다.


*1 categorical_crossentropy VS sparse_categorical_crossentropy

categorical_crossentropy와 sparse_categorical_crossentropy 모두 이름에서 알 수 있듯이 분류 문제를 위한 손실 함수로 쓰인다.
그럼 저 sparse가 붙고 안 붙고의 차이는 무엇인가?

우선 TensorFlow API 문서에 나온 예시를 보자.

# categorical_crossentropy
y_true = [[0, 1, 0], [0, 0, 1]]
y_pred = [[0.05, 0.95, 0], [0.1, 0.8, 0.1]]
loss = tf.keras.losses.categorical_crossentropy(y_true, y_pred)
assert loss.shape == (2,)
loss.numpy()

array([0.0513, 2.303], dtype=float32)

# sparse_categorical_crossentropy
y_true = [1, 2]
y_pred = [[0.05, 0.95, 0], [0.1, 0.8, 0.1]]
loss = tf.keras.losses.sparse_categorical_crossentropy(y_true, y_pred)
assert loss.shape == (2,)
loss.numpy()

array([0.0513, 2.303], dtype=float32)

위 예시 모두 3개의 클래스가 있고, 실제 타겟값은 1번 클래스와 2번 클래스를 의미한다.
여기서 둘의 표현 방식이 달라진다. categorical_crossentropy는 타겟값이 One-hot Encoding 되어 있는 반면에 sparse_categorical_crossentropy는 정수값이 그대로 있다.

그럼 각각 어떤 때에 사용해야 할까?

우선 sparse_categorical_crossentropy는 하나의 타겟 특성만 있는 경우 사용하면 될 것이다.
한 개의 타겟 특성 내에 여러 개의 클래스가 있는 경우이다.
예) Fashion MNIST

categorical_crossentropy는 타겟 특성이 One-hot Encoding되어 여러 개로 나누어졌거나 타겟 값이 여러 특성에 동시에 속할 수 있는 혹은 타겟 값이 여러 특성에 걸친 확률값인 경우 사용하는게 좋을 것이다.
예) One-hot Encoding : [1,0,0], [0,1,0], [0,0,1]
여러 특성에 걸친 경우 : [0.5, 0.3, 0.2]


<참고 자료>

활성화 함수(Activation Function)

(인공 신경망을 모르신다면 이 링크 먼저 보시는게 좋아요! 인공 신경망)

활성화 함수가 뭐지?

In artificial neural networks, the activation function of a node defines the output of that node given an input or set of inputs. A standard integrated circuit can be seen as a digital network of activation functions that can be "ON" (1) or "OFF" (0), depending on input. This is similar to the linear perceptron in neural networks. However, only nonlinear activation functions allow such networks to compute nontrivial problems using only a small number of nodes, and such activation functions are called nonlinearities.

Wikipedia (Activation function)

활성화 함수는 각 노드에서 가중치-편향 연산을 거친 입력값을 다음 단계로 줄지 말지, 주면 어떻게 줄지 결정하는 일종의 문지기 역할을 한다.

활성화 함수의 역할은 두 가지가 있다.

1) 가중치-편향 연산의 결과값(가중합)을 그대로 내보내면 너무 크거나 작을 수 있다.
활성화 함수는 이를 0에서 1 또는 -1에서 1 사이의 값 등으로 바꿔준다(활성화 함수 유형에 따라 다름. 전부 다 상하한의 제한이 있는 것은 아님).

2) 가중치-편향 연산 결과에 비선형성을 부여한다(비선형적인 활성화 함수인 경우. 특별한 경우가 아니라면 비선형성을 가진 활성화 함수만 사용함$^*$$^1$).

활성화 함수에는 여러 유형이 있는데, 그중에서 많이 알려졌거나 주로 쓰이는 것들만 들여다 보려고 한다.


활성화 함수의 유형

1) 계단 함수(Step function)

$$\begin{cases}
0 & \text{if } x < 0\\
1 & \text{if } x \geq 0
\end{cases}
$$

이름대로 생긴 함수다.
0보다 작으면 0, 0 이상이면 1인 모 아니면 도 방식의 가장 간단한 활성화 함수이다.

문제는 인공 신경망의 학습 과정 중 역전파(backpropagation)라는 것이 있는데, 이때 활성화 함수의 미분이 필요하다.
그런데 계단 함수를 미분하니 임계값(0)에서는 미분값이 무한대가 되고 나머지는 0이다. 이는 역전파 과정에서 무의미하므로 계단 함수는 쓰이지 않는다.


2) 시그모이드 함수(Sigmoid function)

Sigmoid


$$\sigma(x) = \frac{1}{1+e^{-x}}$$

로지스틱 회귀에서도 사용되는 함수이다.
받아들인 값을 0 ~ 1 사이의 값으로 바꿔준다.

위의 계단 함수는 무의미한 미분 결과를 가진 것에 반해, 시그모이드 함수는 모든 $x$ 값에 대하여 미분이 가능하다(즉, 미분값(기울기)이 존재한다).

허나 이 친구도 계단 함수처럼 역전파 과정에서 문제점이 있다.
시그모이드 함수를 미분했을 때 미분값은 최대 0.25이다. 그런데 층이 여러 개라면?
2개 층이면 0.250.25=0.0625,
3개 층이면 0.250.250.25=0.015625 ...
매번 최대치로 잡는다고 해도 미분값이 훅훅 작아져버린다.
즉, 시그모이드 함수로 구성된 신경망에서 역전파 과정을 거치면 층이 갈수록 미분값이 작아져서 결국 학습이 제대로 되질 않는다(이를 기울기 소실$^
$$^2$이라고 함).

이런 이유로 신경망의 은닉층에서 활성화 함수로 시그모이드를 사용하지 않는다.
대신 이진 분류 문제의 출력층에서 사용된다(예측 결과가 1일 확률을 0~1 사이의 확률값으로 표현).


3) ReLU 함수(Rectified Linear Unit function)

ReLU


$$\begin{cases}0 & \text{if } x \leq 0 \\ x & \text{if } x > 0\end{cases}$$

앞의 두 친구가 역전파에서 불합격 통보를 받고 탈락했다.
그래서 나온 친구가 바로 ReLU다.

이미지에서 보이는 것처럼 0 이하의 값은 모두 0으로, 그 외에는 들어온 값 그대로 보내주는 방식이다.
미분을 해도 양수 쪽에서는 기울기가 1이므로 역전파 과정에서 시그모이드 함수처럼 신경망의 층이 갈수록 값이 소실되는 문제도 없다.
그래서 신경망의 은닉층에서 주로 사용되는 활성화 함수이다.

그러나 이 친구도 완벽하진 않다. 그래프에서 볼 수 있듯이 ReLU 함수는 신경망 학습 과정에서 양수만 보내주고 음수는 전부 0으로 처리한다. 이로 인해 음수 가중치를 가진 노드들은 싹 다 무시해버리는, 이른바 dying ReLU 문제가 있다.

그래서 이 죽어버린 아이들을 살리기 위해 아래와 같은 ReLU 변종들이 나왔다.


3-1) Leaky ReLU 함수(Leaky ReLU function)

Leaky ReLU


$$\begin{cases}0.01x & \text{if } x < 0 \\ x & \text{if } x \geq 0\end{cases}$$

leaky, 즉 라면 냄비 바닥이 살짝 구멍 뚫려 국물이 조금 샌 것처럼 0 이하의 값에도 아주 약간의 기울기를 부여한 것이다.
보통의 ReLU에서 다 죽던 음수 가중치들에게 산소 호흡기를 던져준 느낌으로 보면 된다.


3-2) PReLU 함수(Parametric ReLU function)

PReLU


$$\begin{cases}\alpha x & \text{if } x < 0 \\ x & \text{if } x \geq 0\end{cases}\\ \text{with parameter }\alpha \text{ }(\alpha \leq 1)$$
($\alpha$는 학습되는 값으로, 계속해서 갱신됨)

Parametric이라는 말처럼 $\alpha$라는 값을 통해 0 이하의 값에 기울기를 부여하는 것이다.
Keras에서 PReLU를 사용할 경우, $\alpha$는 스스로 학습되는 값이다. 여기에 특정한 제약 등을 적용할 수 있다.


3-3) ELU 함수(Exponential Linear Unit)

ELU


$$\begin{cases}\alpha (e^x -1) & \text{if } x \leq 0 \\ x & \text{if } x > 0\end{cases}\\ \text{with parameter } \alpha \text{ }(\alpha \geq 0)$$
($\alpha$는 학습되는 값으로, 계속해서 갱신됨)

0 이하 부분이 지수함수 형태로 되어 있는 ReLU 함수다.
PReLU와 마찬가지로 $\alpha$가 있다.


4) Softmax 함수(Softmax function)

$$
\begin{bmatrix}z_1 \\ z_2 \\ z_3 \\ \cdots \\ z_k \end{bmatrix}=>
S(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{k}{e^{z_j}}} =>
\begin{bmatrix}p_1 \\ p_2 \\ p_3 \\ \cdots \\ p_k \end{bmatrix}\\
(\text{for } i = 1, \cdots, k)
$$

여자친구가 물어본다. 오늘 저녁 뭐 먹지? 메뉴 세 가지만 말해봐 !
그럼 나는 대답한다. 치킨, 초밥, 김밥 !
그럼 여자친구는 답한다. 오늘은 김밥이다 !

소프트맥스 함수는 선택 가능한 옵션 여러 개를 받아서 각각이 최종 정답일 확률값을 내보낸다.
조금 더 구체적으로 말하면, k개의 원소를 가진 벡터(k차원 벡터)를 입력받은 후 각각에 대한 확률값을 가진 벡터를 출력한다.
확률이므로 출력된 벡터의 원소를 합하면 1이다.

위 이야기에서 치킨, 초밥, 김밥을 말했을 때 여자친구가 머릿속에서 "치킨은 30%, 초밥은 10%, 김밥은 60%"라고 생각했다고 해보자.
이를 신경망 모델에 대입해보면 각 클래스(입력 - [치킨, 초밥, 김밥])에 대한 확률값(출력)은 0.3, 0.1, 0.6이고 모델은 최종 결과로 '김밥'이라는 결과를 내보내는 것이라고 할 수 있다.

물론 신경망이 자기 스스로 생각해서 저녁에 뭐 먹을지 답을 내는 것은 아니고, 위의 소프트맥스 함수의 수식에 따라 계산값을 도출하여 답을 낸다.

이러한 특징을 갖고 있기에 소프트맥스 함수는 다중 분류 문제를 위한 신경망 학습 과정 중 출력층에서 사용된다.


*1 왜 비선형적인 활성화 함수만 사용되나?

인공 신경망의 학습 과정에서 가중치-편향 연산만 수행하게 될 경우 각각의 노드의 결과값은 Wx+b 형태가 된다(W : 가중치 행렬 / x : 입력값 / b : 편향).
만약 신경망의 층이 아무리 여러 겹으로 쌓인다 하여도 활성화 함수에서 비선형성을 부여하지 않으면 결국은 직선 형태에서 벗어나질 못한다.

$l(x) = ax+b$ 라고 하면
$$l(l(l(x))) = l^3(x) \\= a(a(ax+b)+b)+b \\= a^3x+a^2b+ab+b$$
$a^3=c$, $a^2b+ab+b=d$라고 하면
$l^3(x) = cx+d$, 직선이다. 3겹이든, 페스츄리 겹겹이 만큼이든.

이렇게 되면 여러 층을 쌓는 이점도 상실될 뿐만 아니라 XOR 등의 복잡한 문제 해결이 불가능해진다.
이런 이유로 인해 비선형적인 활성화 함수를 통해서 가중치-편향 연산 결과에 비선형성을 부여하는 것이다.


*2 기울기 소실

인공 신경망 학습 과정 중 역전파에서는 활성화 함수의 미분값을 매 층마다 곱하게 된다. 그런데 이 미분값이 1 미만의 소수인 경우, 여러 번 곱해지다보면 0으로 수렴하여 신경망의 학습이 제대로 이루어질 수가 없다.

시그모이드 도함수 그래프


위 그래프는 시그모이드 함수를 미분한 함수의 그래프이다. 보이는 것처럼 최댓값이 0.25이다.
이 상태로는 층이 많아지면 많아질수록 역전파를 거치면서 기울기 값이 0을 향해 낙하하므로 시그모이드 함수를 은닉층의 활성화 함수로는 사용할 수가 없는 것이다.

(참고 : 시그모이드 함수를 $f(x)$라고 하면
$$f'(x) = f(x)(1-f(x))$$이다.
$x=0$일 때 기울기가 최대이므로 $$f'(0) = \frac{1}{2}*(1-\frac{1}{2}) = 0.25$$이다.)


<참고 자료>

인공 신경망(ANN; Artificial Neural Network)

인공 신경망이 뭐지?

인공신경망(人工神經網, 영어: artificial neural network, ANN)은 기계학습과 인지과학에서 생물학의 신경망(동물의 중추신경계중 특히 뇌)에서 영감을 얻은 통계학적 학습 알고리즘이다. 인공신경망은 시냅스의 결합으로 네트워크를 형성한 인공 뉴런(노드)이 학습을 통해 시냅스의 결합 세기를 변화시켜, 문제 해결 능력을 가지는 모델 전반을 가리킨다. 좁은 의미에서는 오차역전파법을 이용한 다층 퍼셉트론을 가리키는 경우도 있지만, 이것은 잘못된 용법으로, 인공신경망은 이에 국한되지 않는다.

- 위키백과 (인공 신경망)

인공 신경망은 생물의 신경계가 작동하는 방식을 모방하여 만든 머신 러닝 모델이라고 볼 수 있다.

우리의 신경계는 수많은 신경 세포, 즉 뉴런(neuron)으로 이루어져 있다. 일반적인 뉴런의 구조는 아래와 같이 생겼다.

일반적인 뉴런의 구조


간단하게 설명하자면, 뉴런은 수상돌기를 통해 신호를 받아들인 후 신경세포체와 축삭을 거쳐서 다른 뉴런으로 신호를 전달한다.

우리 몸의 신경계가 뉴런으로 구성되듯이 인공 신경망을 이루는 가장 작은 단위가 있는데, 이를 퍼셉트론(perceptron)이라고 한다.

퍼셉트론(perceptron)

위에서 뉴런이 '수상돌기 -> 신경세포체/축삭 -> 다음 뉴런의 수상돌기'라는 과정을 거쳐서 신호를 전달하는 것을 보았다. 퍼셉트론도 이와 비슷한 구조를 갖고 있다.

먼저 여러 개의 입력값을 받는다. 각각의 입력값에는 신호의 세기, 즉 가중치가 부여되어 있다. 각각의 입력값과 가중치가 곱해진 값을 합한 후, 이에 편향을 더해준다(가중치-편향 연산). 이를 식으로 나타내면 아래와 같다.
$$w_1x_1 + w_2x_2+ b$$
위에서는 입력값이 2개뿐이다. 만약 입력값이 n개라고 하면 식이 어떻게 될까?
$$\sum_{k=1}^{n}w_kx_k + b\\
= (w_1x_1+w_2x_2+...+w_nx_n) + b\\
= \begin{bmatrix}w_1&w_2&\cdots&w_n\end{bmatrix}\begin{bmatrix}x_1\\x_2\\...\\x_n\end{bmatrix} + b\\
=Wx + b$$
각각의 입력값과 가중치가 곱해지는 연산을 벡터로 표현하였고, 그 결과 $Wx+b$와 같은 형태가 되었다(여기서 $W$는 가중치 행렬$^*$$^1$이라고 함).
어디서 많이 보던 모양이다. 그렇다. 일차 함수, 직선이다!

그렇다. 직선이다. 그게 뭐 특별한건가?
특별하다. 왜냐하면 이대로는 복잡한 문제 해결이 불가능하다.
어떤 복잡한 문제인가? XOR 이라고 하는 문제가 있다.

XOR (배타적 논리합)

XOR을 알기 위해서는 먼저 AND, NAND, OR와 같은 논리 게이트가 어떻게 작동하는 것들인지 이해해야 한다.

1) AND
두 명의 사람이 있고, 저녁에 치킨을 먹을지 말지 고민 중이다.
둘 모두 치킨을 먹는다고 할 때에만 먹는(1) 경우다.

AND GATE

2) NAND
이는 AND의 반대, Not AND라서 NAND이다.

NAND GATE

2) OR
이는 둘 중 한 명이라도 치킨 먹자! 라고 하면 치킨을 먹는 경우다.

OR GATE

3) XOR
둘 다 먹자고 하면 안 먹고, 둘 다 안 먹자고 하면 안 먹는데 둘 중 하나만 먹자고 하면 먹는 이상한 경우다. 둘 모두 먹으면 둘 다 살이 찌니까 한 명만 먹기로 하는 결정인걸까? 확실히 단순하지는 않다.

XOR GATE

XOR이 어떤 식으로 작동하는지 보았다. 이번에는 위 논리 게이트의 Output들이 직선을 통해 어떤 식으로 분류되는지 보자.

논리 게이트 Output 분류


AND와 OR의 경우 하나의 직선으로 0과 1의 Output들이 깔끔하게 분류가 된다. 그런데 XOR은? 이래서 아까 위에서 나온 Wx+b의 직선 형태로는 XOR 문제를 해결할 수가 없다.

자, 직선으로는 답이 안나온다. 그럼 어떻게 하지?
일단 휘게 만든다. 뭘로? 활성화 함수로.

활성화 함수(Activation Function)

활성화 함수. 이름대로 자기가 받은 신호를 잘 살려서 내보내주는 역할을 한다. 마치 아이돌 연습생을 잘 트레이닝해서 데뷔를 시키는 프로듀서와도 같다.

잘 살려서 내보내주는 역할이란 무엇인가? 여기에는 두 가지가 있다.

1) 가중합(가중치*입력값 들의 합, Wx)이 계산되고 나면 값이 너무 커지거나 작아질 수가 있다. 이때 활성화 함수를 통해서 이 값을 0에서 1 또는 -1에서 1 사이의 값 등으로 바꿔준다(활성화 함수 유형에 따라 다름. 전부 다 상하한의 제한이 있는 것은 아님).
아이돌 연습생의 과도한 똘끼(?)를 진정시켜 마음을 가다듬게 해준다고 보면 되겠다.

2) 비선형성을 가진 활성화 함수를 통해서 Wx+b를 휘게 만들어준다.
연습생이 이상한 고집 부리지 않게끔 유연한 사고방식을 심어준다고 생각하면 될 것 같다.

(구체적인 활성화 함수 내용은 다음 링크 참고 : 활성화 함수)

자, 우리 연습생의 똘끼도 진정시켰고, 이상한 고집도 안 부리게끔 만들었다. 이제 세상으로 내보내보려고 한다.
잠깐, 그 전에 잘 하는지 한번 보자.

### XOR 문제 구성 ###
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(0)

x11 = np.random.uniform(low=0, high=5, size=(50,))
x12 = np.random.uniform(low=10, high=15, size=(50,))
x21 = np.random.uniform(low=0, high=5, size=(50,))
x22 = np.random.uniform(low=10, high=15, size=(50,))


x1 = np.append(x11, x12)
x2 = np.append(x21, x22)

y11 = np.random.uniform(low=10, high=15, size=(50,))
y12 = np.random.uniform(low=0, high=5, size=(50,))
y21 = np.random.uniform(low=0, high=5, size=(50,))
y22 = np.random.uniform(low=10, high=15, size=(50,))

y1 = np.append(y11, y12)
y2 = np.append(y21, y22)

x_1 = np.vstack([x1, y1]).T
x_2 = np.vstack([x2, y2]).T
y_1 = np.ones_like(x_1[:, 0])
y_2 = np.zeros_like(x_2[:, 0])
x = np.vstack([x_1, x_2])
y = np.hstack([y_1, y_2])


fig, ax = plt.subplots(figsize = (12,5))
ax.plot(x_1[:, 0], x_1[:,1], 'bo')
ax.plot(x_2[:,0], x_2[:,1], 'ro')
ax.grid()

XOR problem

### XOR 문제 해결시켜보기(단층 퍼셉트론) ###
import tensorflow as tf

model = tf.keras.models.Sequential([
    # 한 개의 층으로만 구성
    tf.keras.layers.Dense(1, input_dim=2, activation='sigmoid')
])

model.compile(optimizer='sgd',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(x, y, epochs=1000, verbose=0)

preds = model.predict(x)
preds_1d = preds.flatten()
pred_class = np.where(preds_1d > 0.5, 1 , 0)

y_true = x[pred_class==1]
y_false = x[pred_class==0]

fig, ax = plt.subplots(figsize = (12,5))
ax.plot(y_true[:, 0], y_true[:,1], 'bo')
ax.plot(y_false[:,0], y_false[:,1], 'ro')
ax.grid()

XOR problem solution failed

큰일이다. 이 친구가 혼자서 해내지 못한다.
솔로 데뷔가 힘들 것 같은데... 그렇다면 다른 연습생들과 묶어서 그룹으로 데뷔를 시켜야 할까?

### XOR 문제 해결시켜보기(다층 퍼셉트론) ###
model = tf.keras.models.Sequential([
    # 여러 개의 층으로 구성
    tf.keras.layers.Dense(8, input_dim=2, activation='sigmoid'),
    tf.keras.layers.Dense(1, activation='sigmoid')
])

model.compile(optimizer='sgd',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(x, y, epochs=1000, verbose=0)

preds = model.predict(x)
preds_1d = preds.flatten()
pred_class = np.where(preds_1d > 0.5, 1 , 0)

y_true = x[pred_class==1]
y_false = x[pred_class==0]

fig, ax = plt.subplots(figsize = (12,5))
ax.plot(y_true[:, 0], y_true[:,1], 'bo')
ax.plot(y_false[:,0], y_false[:,1], 'ro')
ax.grid()

XOR problem solution successed

다른 연습생들과 함께 그룹으로 묶으니 이제 복잡한 문제도 자기들끼리 척척 잘 해결한다!
이제는 믿고 데뷔를 시킬 수 있을 것 같다.

이렇게 여러 층의 퍼셉트론으로 구축한 신경망을 다층 퍼셉트론 신경망(MLP; Multi Layer Perceptron)이라고 한다.


인공 신경망은 어떻게 생긴 것이지?

인공 신경망의 가장 기본이 되는 단위인 퍼셉트론 하나부터 시작해서 퍼셉트론 여러 개가 여러 층으로 모인 다층 퍼셉트론 신경망까지 살펴봤다.
이번에는 인공 신경망의 전체적인 그림을 보자.

인공 신경망

인공 신경망은 위 그림처럼 생겼다. 그림 속 각각의 원은 노드(node)라고 하며, 전체는 크게 세 부분 - 입력층(Input Layer), 은닉층(Hidden Layers), 출력층(Output Layer)으로 나누어진다.

1) 입력층(Input Layer)

  • 데이터셋이 입력되는 층
  • 데이터셋의 특성(feature) 개수에 맞춰 입력층의 노드 수가 결정됨
  • 어떤 계산 없이 입력값의 전달만 수행 -> 신경망 층수(깊이, depth)에 포함되지 않음

2) 은닉층(Hidden Layers)

  • 입력층에서 들어온 값이 가중치-편향 연산 및 활성화 함수를 거쳐가는 층
  • 일반적으로 입력층과 출력층 사이에 있는 층임
  • 사용자가 계산 결과를 볼 수 없으므로 은닉(hidden)층이라 함
  • 입력 데이터셋의 특성 수와 관계없이 노드 수 구성 가능
    ※ 딥 러닝 : 2개 이상의 은닉층을 가진 신경망

3) 출력층(Output Layer)

  • 은닉층의 연산을 마친 값이 출력되는 층
  • 해결할 문제에 따라 출력층의 노드 수가 결정됨
  노드 수 결과 값 활성화 함수
이진 분류 1
(∵0 또는 1의
값 1개)
0~1 사이의 확률값 Sigmoid
다중 분류 레이블(타겟)
클래스 수
각 클래스별 0~1
사이의 확률값
Softmax
회귀 출력값의
특성(타겟) 수
타겟 값 일반적으로는
지정 X

 

인공 신경망 구현 예시(Tensorflow & Keras)

1) 데이터 불러오기

입력 데이터 샘플과 Features : 1077 샘플 x 69 Features (변수)
데이터 label: 다운증후군 (1), 정상군 (2)

(데이터는 다운증후군과 정상군 마우스 피질의 핵 분획에서 검출 가능한 신호를 생성하는 69 개 단백질의 발현 수준으로 구성되어 있습니다.
라벨로는 다운증후군 1, 정상군 2로 할당되어 있습니다.)

import pandas as pd
df = pd.read_excel("https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/MouseProtein/mouse_protein_X.xls", header=None)
df_label = pd.read_excel("https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/MouseProtein/mouse_protein_label.xls", header=None)

2) 라벨 값 변경

# 기존에 다운증후군(1), 정상군(2)였던 값을 정상군(0), 다운증후군(1)로 변경
df_label = df_label.replace(2, 0).iloc[:, 0].values
df_label.astype(object)

3) 훈련 / 테스트 데이터셋 나누기

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(df, df_label, test_size=0.2, random_state=42)

4) 신경망 모델 구성

# 모델 초기화
model = tf.keras.models.Sequential([
    tf.keras.layers.Dense(128, input_dim=69, activation='relu'),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dense(1, activation='sigmoid')
])
# Dense : 각각의 신경망 층을 나타냄
# 맨 처음과 마지막이 입력층과 출력층, 그 사이는 은닉층
# 각 층 맨 앞의 숫자(128, 128, 1)는 해당 층의 노드 수임
# input_dim : 입력 데이터 특성 수(input_shape=(69,) 이렇게도 입력 가능)
# activation : 해당 층의 노드들이 사용할 활성화 함수 지정
# keras 모델 초기화 후에는 compile 과정을 거쳐야 함
model.compile(optimizer='sgd',
              loss='binary_crossentropy',
              metrics=['accuracy'])
# optimizer : 손실 함수의 최소값을 찾는 방법 지정
# loss : 손실 함수 종류 지정
# metrics : 훈련 시 평가 지표 지정
# 모델 훈련시키기
model.fit(X_train, y_train, epochs=500)
# epochs : 전체 데이터셋을 한 번 훈련한 것이 1 epoch임
# epochs=500 이라는 것은 전체 데이터셋에 대해 훈련을 500번 진행한다는 의미

Epoch 1/500
27/27 [==============================] - 0s 876us/step - loss: 0.6981 - accuracy: 0.5017
Epoch 2/500
27/27 [==============================] - 0s 778us/step - loss: 0.6906 - accuracy: 0.5296
Epoch 3/500
27/27 [==============================] - 0s 815us/step - loss: 0.6804 - accuracy: 0.5935
...
Epoch 499/500
27/27 [==============================] - 0s 852us/step - loss: 0.0252 - accuracy: 0.9988
Epoch 500/500
27/27 [==============================] - 0s 852us/step - loss: 0.0287 - accuracy: 0.9954

# 테스트셋을 통한 모델 평가
model.evaluate(X_test, y_test, verbose=2)
# verbose : 결과 출력의 단계 설정
# auto - 대부분 1로 지정됨
# 0 - 출력 없음
# 1 - 진행 상황 출력(프로그레스바 포함)
# 2 - 진행 상황 출력(프로그레스바 제외, 1에 비해 간소화)

7/7 - 0s - loss: 0.0478 - accuracy: 0.9861
[0.04781070724129677, 0.9861111044883728]


*1 가중치 행렬(Weight Matrix)
위에서는 퍼셉트론 하나라서 행렬이라기보다는 벡터였다.
하지만 아래와 같이 퍼셉트론 여러 개로 구성된 신경망이라면?

가중치 행렬


이렇게 되면 가중치 연산은 (편향은 없다 하면)
$$(w_1x_1+w_3x_1+w_5x_1+w_2x_2+w_4x_2+w_6x_2)\\
= \begin{bmatrix}x_1&x_2\end{bmatrix}\begin{bmatrix}w_1&w_3&w_5\\w_2&w_4&w_6\end{bmatrix} = \begin{bmatrix}y_1&y_2&y_3\end{bmatrix}$$
이렇게 되고, 결과를 정리해서 표현하면 $y = Wx$ 라고 할 수 있다.
여기서 입력값의 벡터 $x$와 출력값의 벡터 $y$를 이어주는 행렬 $W$를 가중치 행렬이라고 한다.

코드 상에서 가중치 행렬의 형태는 어떻게 알 수 있을까?

model = tf.keras.models.Sequential([
    tf.keras.layers.Dense(10, activation='relu', input_shape=100), # 은닉층
    tf.keras.layers.Dense(1, activation='sigmoid') # 출력층
])

입력 데이터의 특성이 100개이므로 입력층 노드 수는 100, 은닉층의 노드 수는 10이므로 둘 사이의 가중치 행렬 형태는 (100, 10)이다.
같은 원리로 은닉층과 출력층 사이에서는 (10, 1)이 된다.


<참고 자료>

+ Recent posts