대학원 일기

딥러닝 들여다보기 본문

AI/인공지능 기초

딥러닝 들여다보기

대학원생(노예) 2023. 11. 13. 11:17

신경망

머신러닝/딥러닝 과학자들도 자연에서 답을 찾으려 노력했고, 우리 뇌 속의 신경망 구조에 착안해서 퍼셉트론(Perceptron)이라는 형태를 제안하며 이를 연결한 형태를 인공신경망(Artificial Neural Network)이라고 부르기 시작했습니다.

입력층(input layer), 최종 출력값이 있는 출력층(output layer), 그리고 그 사이에 있는 층인 은닉층(hidden layer)이 있습니다. 보통 입력층과 출력층 사이에 몇 개의 층이 존재하든 모두 은닉층이라고 부릅니다.

레이어 개수를 셀 때는 노드와 노드 사이의 연결하는 부분이 몇 개 존재하는지 세면 보다 쉽게 알 수 있습니다.

인공신경망 중에서도 위의 이미지처럼 2개 이상의 레이어를 쌓아서 만든 것을 보통 다층 퍼셉트론(Multi-Layer Perceptron; MLP) 이라고 부릅니다. 그리고 입력층, 출력층을 제외한 은닉층이 많아지면 많아질수록 인공신경망이

DEEP 해졌다고 이야기합니다. 우리가 지금 알아보려고 하는 딥러닝이 바로 이 인공신경망이 DEEP 해졌다는 뜻에서 나온 단어입니다. 그래서 우리가 하려는 딥러닝은 충분히 깊은 인공신경망을 활용하며 이를 보통 다른 단어로 DNN(Deep Neural Network) 라고 부릅니다.

Fully-Connnected Nerual Network는 서로 다른 층에 위치한 노드 간에는 연결 관계가 존재하지 않으며, 인접한 층에 위치한 모든 노드-노드 쌍에 대해 연결이 존재한다는 의미를 내포합니다.

앞에서 설명한 입력층-은닉층, 은닉층-출력층 사이에는 사실 각각 행렬(Matrix)이 존재합니다.

예를 들어 입력값이 100개, 은닉 노드가 20개라면 입력층-은닉층의 모든 노드-노드 쌍에 대해 연결이 존재하므로 사실

입력층-은닉층 사이에는 100x20 의 형태를 가진 행렬이 존재합니다. 똑같이, MNIST 데이터처럼 10개의 클래스를 맞추는 문제를 풀기 위해 출력층이 10개의 노드를 가진다면 은닉층-출력층 사이에는 20x10 의 형태를 가진 행렬이 존재하게 됩니다.

이 행렬들을 Parameter 혹은 Weight라고 부릅니다. 두 단어는 보통 같은 뜻으로 사용되지만, 실제로 Parameter에는 위의 참고 자료에서 다룬 bias 노드도 포함된다는 점만 유의해 주세요.

이때 인접한 레이어 사이에는 아래와 같은 관계가 성립합니다.

$ y=W * X+b$ 

weight_init_std = 0.1
input_size = 784
hidden_size=50

# 인접 레이어간 관계를 나타내는 파라미터 W를 생성하고 random 초기화
W1 = weight_init_std * np.random.randn(input_size, hidden_size)  

# 바이어스 파라미터 b를 생성하고 Zero로 초기화
b1 = np.zeros(hidden_size)

# 은닉층 출력
a1 = np.dot(X, W1) + b1

활성 함수

활성화 함수는 보통 비선형 함수를 사용하는데 이 비선형 함수를 MLP 안에 포함시키면서 모델의 표현력이 좋아지게 됩니다. (정확히는 레이어 사이에 이 비선형 함수가 포함되지 않은 MLP는 한 개의 레이어로 이루어진 모델과 수학적으로 다른 점이 없습니다.)

sigmoid

$ σ(x)=\frac{1}{1+e^{−x}}$ 

def sigmoid(x):
  return 1 / (1 + np.exp(-x))
  • vanishing gradient 현상이 발생한다.
  • exp 함수 사용 시 비용이 크다.

tanh

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

  • tanh 함수는 함수의 중심값을 0으로 옮겨 sigmoid의 최적화 과정이 느려지는 문제를 해결.
  • vanishing gradient 문제 존재.

ReLU

$f(x) = max(0, x)$

  • sigmoid, tanh 함수에 비해 학습이 빠름.
  • 연산 비용이 크지 않고, 구현이 매우 간단하다.

어떤 함수를 사용할까

  • 우선 가장 많이 사용되는 함수는 ReLU이다. 간단하고 사용이 쉽기 때문에 우선적으로 ReLU를 사용한다.
  • ReLU를 사용한 이후 Leakly ReLU등 ReLU계열의 다른 함수도 사용 해본다.
  • sigmoid의 경우에는 사용하지 않도록 한다.
  • tanh의 경우도 큰 성능은 나오지 않는다.

SoftMax

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 
    x = x - np.max(x) # 오버플로 대책
    return np.exp(x) / np.sum(np.exp(x))
y_hat = softmax(a2)

 

손실 함수

비선형 활성화 함수를 가진 여러 개의 은닉층을 거친 다음 신호 정보들은 출력층으로 전달됩니다. 이때 우리가 원하는 정답과 전달된 신호 정보들 사이의 차이를 계산하고, 이 차이를 줄이기 위해 각 파라미터들을 조정하는 것이 딥러닝의 전체적인 학습 흐름입니다. 이 차이를 구하는 데 사용되는 함수는 손실함수(Loss function) 또는 비용함수(Cost function)라고 부릅니다.

평균 제곱 오차

$MSE = \dfrac {1} {n} \sum_{i=1}^{n} (Y_i - \hat {Y_i})^2$

Cross Entropy

두 확률분포 사이의 유사도가 클수록 작아지는 값입니다. 아직 별로 학습되지 않은 현재의 모델이 출력하는 softmax 값

$\hat y$은 10개의 숫자 각각의 확률이 대부분 0.1 근처를 오가는 정도입니다.

모델을 학습하게 되면, \hat y이 점점 정답에 가까워지게 됩니다.

$E = - \sum_{i=1}^{n} t_i \log{y_i}$ (ti​, yi​는 각각 정답 벡터와 모델의 예측값 벡터 \hat y의 원소들)

# 정답 라벨을 One-hot 인코딩하는 함수
def _change_one_hot_label(X, num_category):
    T = np.zeros((X.size, num_category))
    for idx, row in enumerate(T):
        row[X[idx]] = 1
    return T

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 훈련 데이터가 원-핫 벡터라면 정답 레이블의 인덱스로 반환
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

Loss = cross_entropy_error(y_hat, t)
Loss

경사하강법

오차를 줄이기 위한 방법, 각 단계에서의 기울기를 구해서 해당 기울기가 가리키는 방향으로 이동하는 방법.

학습률(learning rate) 이라는 개념을 도입해 기울기 값과 이 학습률을 곱한 만큼만 이동.

# softmax값의 출력으로 Loss를 미분한 값
batch_num = y_hat.shape[0]
dy = (y_hat - t) / batch_num
print(dy.shape)
dy

여기서 $dy = \frac { \partial Loss}{\partial y}$, 일단 dy가 구해지면 다른 기울기들은 chain-rule로 쉽게 구해집니다.

중간에 sigmoid가 한번 사용되었으므로, 활성화함수에 대한 gradient도 고려되어야 합니다.

def affine_layer_backward(dy, cache):
  X, W, b = cache
  dX = np.dot(dy, W.T)
  dW = np.dot(X.T, dy)
  db = np.sum(dy, axis=0)
  return dX, dW, db # dX는 사용하진 않음.

 

learning_rate = 0.1
def update_params(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate):
    W1 = W1 - learning_rate*dW1
    b1 = b1 - learning_rate*db1
    W2 = W2 - learning_rate*dW2
    b2 = b2 - learning_rate*db2
    return W1, b1, W2, b2
# 파라미터 초기화
W1 = weight_init_std * np.random.randn(input_size, hidden_size)
b1 = np.zeros(hidden_size)
W2 = weight_init_std * np.random.randn(hidden_size, output_size)
b2 = np.zeros(output_size)

# Forward Propagation
a1, cache1 = affine_layer_forward(X, W1, b1)
z1 = sigmoid(a1)
a2, cache2 = affine_layer_forward(z1, W2, b2)

# 추론과 오차(Loss) 계산
y_hat = softmax(a2)
t = _change_one_hot_label(Y_digit, 10)   # 정답 One-hot 인코딩
Loss = cross_entropy_error(y_hat, t)

print(y_hat)
print(t)
print('Loss: ', Loss)

# Backward Propagation        
dy = (y_hat - t) / X.shape[0]
dz1, dW2, db2 = affine_layer_backward(dy, cache2)
da1 = sigmoid_grad(a1) * dz1
dX, dW1, db1 = affine_layer_backward(da1, cache1)

# 경사하강법을 통한 파라미터 업데이트    
learning_rate = 0.1
W1, b1, W2, b2 = update_params(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate)
def predict(W1, b1, W2, b2, X):
  a1 = np.dot(X, W1) + b1
  z1 = sigmoid(a1)
  a2 = np.dot(z1, W2) + b2
  y = softmax(a2)
  
  return y
def accuracy(W1, b1, W2, b2, x, y):
  y_hat = predict(W1, b1, W2, b2, x)
  y_hat = np.argmax(y_hat, axis=1)
  
  accuracy = np.sum(y_hat == y) / float(x.shape[0])
  return accuracy

 

Comments