Simple Network 구현하기

Simple Neural Network from scratch with Tensorflow

Tensorflow를 이용해, 이전까지 알아본 신경망의 기본적인 내용을 바탕으로 간단한 신경망을 구현해보자. 우선, 필자는 M1 Macbook Air에 python 3.9버전을 올려 apple tensorflow 2.8 버전 환경을 사용하고 있음을 알린다. M1 환경에서 tensorflow를 설치하는 방법은 Apple Developer 문서를 참고하면 된다.(자세한 설치방법 등은 구글링하면 많이 나오는데 댓글에 남기면 답변드리도록 하겠습니다😃)

tensorflow에는 이를 기반으로 한 keras라는 매우 간편하면서도, 간단한 딥러닝 라이브러리가 존재한다. 그러나 keras로 구현하는 코드는 신경망이 작동되는 기본적인 원리를 파악하기 힘들기 때문에, 우선은 직접 구현하는 것을 살펴보도록 하자. 텐서플로의 사용법 등 기본적인 내용은 다루지 않고, 신경망을 구현하는 것만 우선 다루어보도록 하자. 이와 관련하여 역전파 등 간단한 딥러닝 메커니즘을 직접 구현한 글(Reference 참고)이 있어서, 이번 글은 이를 번역하고 추가적인 설명을 덧붙이는 방식으로 썼다.

MNIST 데이터셋 불러오기

거의 대부분의 딥러닝/머신러닝 관련 책을 구입하면 첫 장에 예제로 사용되는 데이터가 MNIST 데이터셋이다. MNIST는 한자리 숫자 이미지를 분류하는 문제로 텐서플로와 케라스를 설치하면 기본적으로 불러올 수 있게끔 되어있다.

import tensorflow as tf
from tensorflow import keras
from keras.datasets import mnist

필요한 모듈을 로드하고, 데이터셋을 다음과 같이 불러올 수 있다.

(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train.shape

위 코드를 실행하면 (60000, 28, 28) 이라는 결과값이 출력되는데, 이는 각 MNIST 데이터셋의 이미지가 28×2828\times 28

import matplotlib.pyplot as plt
img = X_train[0]
img_reshaped = img.reshape(28,28)
img_reshaped.shape

plt.figure(figsize=(4,4))
plt.title('sample of ' + str(y_train[0]))
plt.imshow(img_reshaped, cmap='gray')
plt.show()

아래와 같은 훈련 데이터셋의 첫 번째 이미지(그래프)를 얻을 수 있다.

Simple Feedfoward Neural Network

Initializing Network

앞서 살펴본 MNIST 데이터를 처리할 수 있는 신경망을 만들어보도록 하자. 여기서는 가장 단순한 Feedfoward Neural Network만을 다루며, 이는 신경망 내의 노드끼리 순환이 일어나지 않는 것을 의미한다. 이전 글에서 살펴본 신경망들과 같은 형태이다. 우선 신경망을 구축하기 위해서는 Layer 개수와, Layer별 노드 수를 설정해서(hyperparameter) 이에 맞는 Weight matrix, Bias matrix 변수를 설정할 수 있게끔 해야할 것이다. 여기서는 python의 클래스(class)로 특별히 2개의 hidden layer를 갖는 Fully-connected MLP를 구현해보도록 하자. (Fully-connected인 이유는 레이어간 연산을 행렬연산으로 쉽게 구현할 수 있기 때문이다❗️)

class Network(object):
    def __init__(self, n_layers):
        self.params = [ ]

        self.W1 = tf.Variable(
        tf.random.normal([n_layers[0],n_layers[1]],stddev=0.1), name='W1'
        )
        self.b1 = tf.Variable(tf.zeros([1,n_layers[1]]))

        self.W2 = tf.Variable(
				tf.random.normal([n_layers[1], n_layers[2]], stddev=0.1), name='W2'
				)
        self.b2 = tf.Variable(tf.zeros([1,n_layers[2]]))

        self.W3 = tf.Variable(
				tf.random.normal([n_layers[2], n_layers[3]],stddev=0.1), name='W3'
        )
        self.b3 = tf.Variable(tf.zeros([1, n_layers[3]]))

        self.params = [self.W1, self.b1, self.W2, self.b2, self.W3, self.b3]

위 코드는 2-hidden layer mlp에 필요한 변수(W1,W2,W3,b1,b2,b3) 들을 텐서플로의 변수객체 tf.Variable으로 설정하며 동시에 가중치행렬(W1,W2,W3\mathbf{W_1,W_2,W_3}

신경망에서 처리되는 데이터 행렬은 모두 행row이 샘플개수, 열column은 특성 개수를 의미한다. 예를 들어, MNIST의 경우 Input layer의 Data Matrix를 살펴보면, 각 데이터(이미지)가 784개의 특성을 가지므로 (이는 노드 개수를 의미한다❗️) Input layer는 784개 노드로 구성되며, Input Matrix는 60000×78460000\times784

X2=h(X1W1+b1)(1)\mathbf X_2 = h(\mathbf X_1 \mathbf W_1+\mathbf b_1)\tag{1}

를 만족하는 784×30784\times 30

Forward & Backward Pass

def forward(self, x):
    X_tf = tf.cast(x, dtype=tf.float32)
    Z1 = tf.matmul(X_tf, self.W1) + self.b1
    Z1 = tf.nn.relu(Z1)
    Z2 = tf.matmul(Z1, self.W2) + self.b2
    Z2 = tf.nn.relu(Z2)
    Z3 = tf.matmul(Z2, self.W3) + self.b3
    Y = tf.nn.sigmoid(Z3)
    return Z3

이제 위와 같이 Forward pass를 처리하는 함수 forward를 정의할 수 있다. 이때 코드를 보면 알 수 있듯이, 각각의 Z들은 각 레이어에서 처리된 데이터를 의미하며, 행렬곱 tf.matmul이 사용되어 위의 식 (1)처럼 계산된다. 또한, 위 코드의 경우 두 hidden layer에서는 활성함수로 ReLU를, 마지막 출력층에서는 시그모이드 함수를 사용했다. 여기서 함수의 출력값으로 세번째 출력값을 사용하는 이유는, 손실함수에서 sigmoid를 구현하도록 할 것이기 때문이다!

다음으로, backward pass와 역전파를 통한 학습을 정의하기 위해 우선 손실함수를 계산하는 코드를 다음과 같이 작성해야 한다.

def loss(self, y_true, logits):
    y_true_tf = tf.cast(tf.reshape(y_true, (-1,1)), dtype = tf.float32)
    logits_tf = tf.cast(tf.reshape(logits, (-1,1)), dtype = tf.float32)
    return tf.nn.sigmoid_cross_entropy_with_logits(y_true_tf,logits_tf)

여기서 tf.reshape는 추정하는 라벨 데이터(y)의 형태를 변환하는데, 앞서 정의한 신경망에서 모든 텐서는 N×특성 수N\times\text{특성 수}

def backward(self, x, y):
    optimizer = tf.keras.optimizers.SGD(learning_rate=0.01)
    with tf.GradientTape() as tape:
        predicted = self.forward(x)
        current_loss = self.loss(y, predicted)
    grads = tape.gradient(current_loss, self.params)
    optimizer.apply_gradients(zip(grads, self.params))

여기서 tf.GradientTape는 Forward pass 과정(self.forward)의 모든 연산의 순서와 결과를 저장하고, 이를 바탕으로 Backward pass 과정(self.backward)에서 역순으로 연산을 수행하며 각 노드의 그래디언트를 계산하게끔 해주는 모듈이다(Tensorflow 문서 참조). 위 모델에서 Optimization은 가장 기본적인 SGD(Tensorflow 공식 문서)를 활용하도록 했으며, 학습률(eta)는 0.01로 설정한 것을 확인할 수 있다. 이를 바탕으로 tape.gradient에서 그래디언트를 계산할 수 있으며, 이를 바탕으로 optimizer.apply_gradients를 통해 계산한 그래디언트로 경사하강이 이루어진다. 사실 이렇게 정의된 backward 함수는 모델 학습 함수인 model.fit()과 동일하게 작동한다.

이렇게 구현한 모든 함수들을 Network 클래스로 함께 묶어주면 된다(최종코드 참고).

Model Fitting

먼저 모델을 적합시키기 위한 기본 작업을 진행해보도록 하자. 앞서 Network 클래스를 실행시키기 위해서는 레이어별 노드 개수인 n_layers가 필요했고, 이에 맞추어 손실함수 등을 계산하기 위해 데이터 역시 변환해야 한다.

n_layers = [784, 100, 30, 10]
epochs = 5
X_train = tf.reshape(X_train,[60000,784])
y_one_hot = tf.one_hot(y_train, depth = 10)

net = Network(n_layers)

첫번째 hidden layer는 100개의 노드를, 두번째는 30개의 노드를 갖도록 설정했다. 또한, 기존의 Input data는 28×2828\times 28

모델의 성능을 평가하기 위해 어떤 성능지표를 사용해야할 지 정해야 하는데, 여기서는 분류에서 가장 단순하게 사용되는 precision정밀도를 사용해보도록 하자. 참고로 한 클래스에 대한 정밀도는 TPTP+FPTP\over{TP+FP}

from sklearn.metrics import precision_score, f1_score
for epoch in range(epochs):
    net.backward(X_reshape,y_one_hot)
    acc =f1_score(y_true = tf.math.argmax(y_one_hot,1), y_pred = tf.math.argmax(net.forward(X_reshape),1),average='micro')
    if epoch % 10 == 0:
        print('accuracy score : {score}'.format(score = acc * 100))

이를 바탕으로 위 코드처럼 정해진 epoch만큼 forward-backward pass를 진행시켜 모델을 다음과 같이 학습하게 할 수 있다(성능은 매우 구리다😅 단순한 딥러닝 모델을 직접 구현해보는데 의의가 있다!).

전체 코드 : https://github.com/ddangchani/braverep/blob/main/Supplyments/simplenn.ipynb

References

좋은 웹페이지 즐겨찾기