이번 주제부터는 이론 편과 실전 편으로 나눠서 글을 작성하겠다. 파이썬과 numpy 라이브러리를 사용한다 가정하고 딥러닝에 대한 개념을 조금 잡고 지나가 보자.
- 인공신경망. neural network
- 활성화함수. activation function
- 손실함수. loss function
- 역전파. backpropagation
- 최적화함수. optimzier function
딥러닝에서 사용하는 요소들에 대해 알아보자. 설명하는 데에 있어 input x는 인공신경망에 들어가는 값이다. 이 x는 이미지 사진이 될 수도 있고, '공룡이 사과를 먹는다'같은 자연어 데이터가 될 수도 있다.
인공신경망
딥러닝은 다층의 인공신경망(Neural Network)으로 이루어져 있다. 인공신경망이란 무엇일까? 이는 퍼셉트론에서 시작했는데, 간단하게 아래와 같은 식을 의미한다.
perceptron
out = x * w + b
x는 input값, w는 가중치, b는 편향을 의미한다. 퍼셉트론 식을 보니 1차 함수, 직선이다. 인공신경망은 이런 선형에서 벗어나 비선형함수로 표현력을 높이고자 하였다. 그래서 퍼셉트론에 활성화함수를 추가하여 비선형함수를 만들었다.
활성화함수 (activation function)
act_fn : 활성화함수
out = act_fn( x * w + b )
활성화함수는 선형을 비선형으로 만들기 위해 도입한 함수다. 퍼셉트론의 출력값이 활성화함수를 통과하는 형태다. 이런 활성화함수는 여러 개가 있는데, 그중 자주 쓰이는 함수를 살펴보면 다음과 같다.
1. Sigmoid
주로 이진 분류 문제를 풀 때 마지막 층에 추가하는 활성화함수다.
def sigmoid(arr):
exps = np.exp(arr)
return exps / (1 + exps)
2. Softmax
클래스가 10개라면 10개의 확률 값을 반환한다. 10개를 다 더하면 1이 된다. 주로 다중 분류 문제에서 마지막 층에 추가하는 활성화함수다.
def softmax(arr):
exps = np.exp(arr)
return exps / np.sum(exps)
3. ReLU
값이 양수면 그대로, 그렇지 않다면 0을 반환하는 함수다. 함수를 그려보면 으잉 직선인데? 하겠지만 미분이 안되기 때문에 비선형이다. (0에서 왼쪽과 오른쪽 미분 값이 다르다.) 신경망을 여러 개를 쌓을 때 신경망과 신경망 사이에 주로 쓰인다.
def relu(arr):
return np.max(arr, 0)
4. tanh
이전 포스트인 2022.03.27 - [머신러닝 with 파이썬] - 파이썬으로 기초 RNN 구현하기 에 쓰였던 활성화함수다. 시계열 데이터를 다룰 때 주로 쓰인다.
def tanh(x):
exp_minus = np.exp(-1 * x)
exp_plus = np.exp(x)
return (exp_plus + exp_minus) / (exp_plus + exp_minus)
loss function (손실 함수)
퍼셉트론과 활성화함수를 통과시켜 출력 값이 나왔다. 자 이제 어떡한담? 내가 원하는 출력 값과 비슷한지를 판별해야 한다. 손실함수를 사용해서 예측값 out과 실제값 y의 loss를 구하면 된다. 그리고 우리는 이 loss값이 작아지길 원할 것이다.
loss_fcn : 손실함수
loss = loss_fcn(out, y)
자주 쓰이는 loss 함수는 생각보다 많지 않다. 직관적으로 생각했을 때 out과 y 의 손실 값을 어떻게 구하면 좋을까? 단순하게 그 차를 구하면 되지 않을까?
1. MSELoss (Mean Square Error)
비교적 직관적으로 loss를 구하는 함수다.
def MSELoss(preds, labels):
matrix = np.square(preds - labels)
return np.mean(matrix)
예측값과 실제값의 차에 √ 를 취한다. 그러고 평균을 구하면 끝이다.
2. Cross Entropy Loss
이름에 엔트로피가 들어가는 손실함수다. 엔트로피가 무엇이냐? 불확실성이다. 예측값 out이 과연 내가 원하는 결괏값 y와 얼마나 같을까? out과 y값은 비슷할수록 불확실성이 작아질 것이다. 엔트로피는 보통 아래와 같이 표현할 수 있다. 여기서 p_i는 i번째 데이터의 확률(probability)이 된다.
entropy
H = ∑ p_i * log(1/p_i)
보통 분류 문제에서 우리는 결과값 y의 값이 0 또는 1의 값을 갖게 됨을 알고 있다. 예를 들어 예측 클래스가 10개 정도 있고, 내 결괏값은 8을 의미하고 있다면, 이때 y는 one-hot 벡터로 [0,0,0,0,0,0,0,1,0,0] 와 같이 표현될 수 있다. 만약 내 out값이 [0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.91, 0.01, 0.01]의 값으로 나타났다고 하자. 이때 엔트로피는 어떻게 구할 수 있을까?
cross entropy
H(p, q) = ∑ p_i * log(1/q_i)
이때 q는 결괏값 y의 one-hot 벡터가 되겠다. 이를 코드로 풀면 다음과 같이 표현할 수 있다.
def CrossEntropyLoss(preds, labels):
batch_size , num_classes = preds.shape
loss = 0
for i in range(num_classes):
loss += np.sum(preds[:, i] * np.log(labels[:,i]))
return loss
지금은 분류 문제를 예시로 들었지만 위 수식은 로지스틱 회귀 문제에서도 통용된다. 이 부분은 따로 글을 적도록 하겠다.
backpropagation (역전파)
위에서 loss를 구했다. 이 손실 값을 어떡하면 좋을까? 컨셉 자체가 예측값과 실제값의 차이니 이 값이 작으면 좋겠다. 학습을 반복할수록 loss값이 더 작아지면 좋겠다! 그럼 우리가 지금 손볼 수 있는 파라미터가 무엇이 있는지 살펴보자. 입력값과 실제값 x, y는 주어진 값이다. 활성화함수도 우리가 어떻게 임의로 값을 바꿀 수 있는 친구가 아니다. 그렇다면 남은 것은? 퍼셉트론에서 살펴봤던 가중치 w와 편향 b다.
그렇다. 딥러닝은 결국 w와 b가 최적 값을 갖게끔 튜닝하는 과정이다.
어떻게 하면 w, b를 우리가 원하는 방향으로 잘 튜닝할 수 있을까? 딥러닝을 공부하신 분들이 우리 대신 이미 고민을 해주셨고, 그 답을 찾았다. 바로 역전파다.
지금까지의 인공신경망 학습 과정을 생각해보자.
out = act_fn (w * x + b)
loss = loss_fn(out, y)
loss가 작아지면 좋겠다!
역전파란 이 순서를 반대로 따라가는 것이다. 여기서 미분이 들어가게 된다. loss를 w로 미분한 값을 통해 가중치 w를 업데이트하고, 마찬가지로 loss를 b로 미분한 값을 통해 가중치 b를 업데이트한다. 여기서 학습률(learning rate)라는 개념이 등장하게 되는데, 이는 업데이트할 때 미분 값을 얼마나 사용할지 정하는 파라미터다.
w = w + learning_rate * dloss/dw
b = b + learning_rate * dloss/db
여기까지는 뭐 그래 알겠다. 근데 dloss/dw와 dloss/db는 어떻게 구한담? 바로 chain rule을 사용하면 된다.
chain rule을 이해해보자
dx/dy = 1
dy/dz = 2
dx/dz = dx/dy * dy/dz = 1 * 2 = 2
아 그럼 우리는 dloss/dw값을 알고 싶으니까, dloss/dout을 구하고 dout/dw값을 구하면 되겠구나!
dloss/dw = dloss/dout * dout/dw
dloss/db = dloss/dout * dout/db
이해를 돕기 위해 활성화함수는 사용하지 않았다 가정하고 역전파를 코드로 나타내면 다음과 같다.
def backpropagation_with_actfn(x, w, b, z, dz_dy,learning_rate=1e-3):
#dz_dy shape : batch_size, out_size
dy_dw = x #shape : batch_size, in_size
dy_db = 1
dz_dw = np.matmul(np.transpose(dy_dw), dz_dy) #dz_dy * dy_dw , shape : in_size, out_size
dz_db = dz_dy * dy_db #dz_dy * dy_db , shape n, out_size
w = w + learning_rate * dz_dw
b = b + learning_rate * dz_db
return w, b
우리가 다룬 이 방법을 Gradient Descent(GD)라고 한다.
optimizer (최적화함수)
여기까지 오면 우리는 인공신경망을 거의 이해했다. 1) 신경망 층은 가중치 w, 편향 b, 활성화함수로 이루어진 비선형함수고, 2) 이를 통과한 예측값 out과 실제값 y 사이의 loss를 구했으며, 2) 이를 통해 w와 b를 backpropagation을 통해 업데이트한다.
역전파과정에서 사용한 learning rate는 가중치 w를 얼마나 업데이트할 건지에 대한 하이퍼 파라미터였다. 만약에 dloss/dw가 0이면 어떻게 하지? 아니면 dloss/dw값이 너무 많이 크다면? w는 learning rate가 몇이든 간에 우리가 원하는(loss가 작아지는 방향으로는) 업데이트되지 않을 것이다. 0 * learning rate는 0일 테니까! 학습이 안되면 곤란하다. 그렇기 때문에 우리는 학습 시 최적화함수를 잘 골라야 한다.
최적화함수는 포스팅을 따로 할 정도로 여러 종류가 있지만, 지금은 위에서 다룬 Gradient Descent(GD)를 제외하고 Adam에 대해서만 아주아주 살짝만 다루고자 한다.
기존 gradient descent의 w 업데이트 방법
w = w + learning_rate * dloss/dw
Adam의 w 업데이트 방법
w = w + learning_rate * 1 / (√(v + e) ) * dloss/dw
이상한 수식을 하나 더 곱해주고 있다! 여기서 v는 momentum(속도)에 대한 수식으로, 미분 값이 어느 방향으로 기울기가 향하고 있는지에 대한 값에 이런저런 처리를 해준 값이다.
어렵겠지만 왜 내가 Adam을 소개했는지 결론을 말하자면, 학습할 때는 그냥 Adam을 쓰라는 거다. 이는 지금까지의 딥러닝 학습에서 통상적으로 좋은 결과를 냈기 때문으로, 최적화함수를 뭘 써야 할지 모르겠다 싶으면 Adam을 사용하면 된다.
cf) 수식을 어떻게 보느냐에 따라 learning rate앞에 붙는 부호가 +가 될 수도, -가 될수도 있다. 간단하게 본 포스팅에서는 + 로 통일한다.
python으로 딥러닝 직접 구현을 하는데에 있어서 꼭 필요한 개념을 설명해봤다. 이번 이론을 잘 생각하면 이전 포스팅의 직접 구현을 따라가는 데에 어려움이 없을 것이다.
'머신러닝 > 파이썬 구현 머신러닝' 카테고리의 다른 글
pytorch 공식 구현체로 보는 transformer MultiheadAttention과 numpy로 구현하기 (0) | 2023.07.01 |
---|---|
딥러닝 이론 optimizer 정리 - GD , SGD, Momentum, Adam (0) | 2022.05.14 |
파이썬으로 기초 CNN 구현하기 1 - conv, pooling layer (2) | 2022.04.17 |
파이썬으로 기초 RNN 구현하기 (1) | 2022.03.27 |
파이썬으로 기초 MLP 구현하기 (2) | 2022.03.09 |