IT/AI\ML

[python/Tensorflow2.0] Mnist 학습 모델 (2)-CNN;Sequential, Functional, Model Subclassing

개발자 두더지 2020. 4. 20. 22:57
728x90

0. NN Implementation Flow in TensorFlow

1. Set hyper parameters

; learning rate, training epochs, batch size, ect.

2. Make a data pipelining

; use tf.data

# data pipeline이란 데이터 셋을 load하고, 앞에서 설정했던 batch_size만큼 테이터를 가져 와서 네트워크에 공급하는 역할

3. Build a neural network model

; tf.keras의 sequential API / tf.keras의 Functional API / tf.keras의 subclassing

4. Define a loss function

; cross entropy

# 뉴럴 네트워크의 output과 우리가 가진 정답(label)간의 error비교

# classification 문제를 풀어볼 것이므로 cross entropy를 사용

5. Calculate a gradient

; use tf.GradientTape

# loss값을 weight에 대해 미분해 Gradient를 계산

6. Select an optimizer

; Adam optimizer

# Gradient를 이용해 weight를 update

# ---------------------------------------- 여기까지가 사실상 학습부분

7. Define a metric for model's performance

; accuracy

# 테스트 데이터 셋을 이용하여 모델이 얼마나 잘 학습했는지 확인을 위한 성능 측정 metric

# Classification 문제에서는 metric으로 accuracy를 많이 사용

8. (optional) Make a checkpoint for saving

9. Train and Validate a neural network model

뉴럴 네트워크를 만들 때 위와 같은 순서로 구현하는 것을 권장한다. 여기서 사용될 임의의 CNN은 아래와 같이 구성되어 있다.

마지막에는 fully-connected layer을 두 단을 쌓았다. convolution은 모두 3x3, stride는 1, padding 옵션은 same으로 하고, max pooling은 모두 2x2, stride는 2, padding 옵션은 마찬가지로 same으로 하여 구현한다.

Convolution을 할 때 사용하는 filter의 개수는 첫 번째 Convolution에서는 32개, 두 번째 Convolution에서는 64개, 세 번째 Convolution에서는 128개 이렇게 두 배씩 늘렸으며, pooling을 해서 가로, 세로 사이즈는 계속 절반(14 > 7 > 4 )으로 줄어들게 하였다.


1. Importing libraries

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.utils import to_categorical
import numpy as np
import matplotlib.pyplot as plt
import os

print(tf.__version__)
print(keras.__version__)

2.1.0

2.2.4-tf

 

2. Hyper Parameters

learning_rate = 0.001
training_epochs = 15
batch_size = 100

tf.random.set_seed(777)

 

3. Creating a Checkpoint Directory

cur_dir = os.getcwd()
ckpt_dir_name = 'checkpoints'
model_dir_name = 'minst_cnn_seq'

checkpoint_dir = os.path.join(cur_dir, ckpt_dir_name, model_dir_name)
os.makedirs(checkpoint_dir, exist_ok=True)

checkpoint_prefix = os.path.join(checkpoint_dir, model_dir_name)

 

4. MNIST Data

## MNIST Dataset #########################################################
mnist = keras.datasets.mnist
class_names = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
##########################################################################

## Fashion MNIST Dataset #################################################
#mnist = keras.datasets.fashion_mnist
#class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
##########################################################################

 

5. Datasets

 

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()    
    
train_images = train_images.astype(np.float32) / 255.
test_images = test_images.astype(np.float32) / 255.
# train_images과 test_images는 gray scale되어 마지막 채널 1이 생략된 상태이다.
# 즉 train_images는 (60000, 28, 28)형태, test_images는 (10000, 28, 28)형태의 3차원인데
# CNN에 입력값은 4차원 tensor여야 하므로 np.expand_dims을 사용하여 끝자리(axis=-1)에 차원을 하나 추가 
train_images = np.expand_dims(train_images, axis=-1)
test_images = np.expand_dims(test_images, axis=-1)

# to_categorical API를 사용하여 One hot Encoding  
train_labels = to_categorical(train_labels, 10)
test_labels = to_categorical(test_labels, 10)    
    
# tf.data.Dataset.from_tensor_slices를 이용해 데이터를 batch_size만큼 잘라서 공급하는 부분
train_dataset = tf.data.Dataset.from_tensor_slices((train_images, train_labels)).shuffle(
                buffer_size=100000).batch(batch_size)
# 훈련의 경우 데이터의 shuffle이 필요하지만, 테스트의 경우 shuffle이 필요하지 않으므로 shuffle생략
test_dataset = tf.data.Dataset.from_tensor_slices((test_images, test_labels)).batch(batch_size)

 

6. Model function

1) tf.keras의 sequential API 를 사용하는 경우(function 형태)

 

def create_model():
    model = keras.Sequential() #keras.Sequential()은 벽돌을 한 장, 한 장 쌓아 올리듯이 model.add를 사용해 레이어를 하나씩 추가
    # 첫 번째 layer에는 (필수는 아니지만) 디버깅의 편의성을 위해 input_shape 옵션을 사용하는 것이 좋다. 
    model.add(keras.layers.Conv2D(filters=32, kernel_size=3, activation=tf.nn.relu, padding='SAME', 
                                  input_shape=(28, 28, 1)))
    # keras.layers.MaxPool2D의 default는 2x2에 stride 2, padding='VALID'인 pooling이다.
    # 앞서 말했듯, 우리가 설계한 pooling 레이어는 2x2에 stride 2, padding='SAME'이므로 padding='VALID'의 옵션만 변경해준다.
    model.add(keras.layers.MaxPool2D(padding='SAME'))
    model.add(keras.layers.Conv2D(filters=64, kernel_size=3, activation=tf.nn.relu, padding='SAME'))
    model.add(keras.layers.MaxPool2D(padding='SAME'))
    model.add(keras.layers.Conv2D(filters=128, kernel_size=3, activation=tf.nn.relu, padding='SAME'))
    model.add(keras.layers.MaxPool2D(padding='SAME'))
    # Flatten()을 이용해 fully connected layer로 들어가기 전, 출력된 feature map을 벡터를 펴주는 작업을 한다.
    model.add(keras.layers.Flatten())
    model.add(keras.layers.Dense(256, activation=tf.nn.relu))
    # Dropout을 넣어준 이유는 Convolution에 비해 Dense layer안의 파라미터 수가 더 많기 때문이다. 
    model.add(keras.layers.Dropout(0.4))
    model.add(keras.layers.Dense(10))
    return model

 

model = create_model() # 모델 생성
model.summary() # summary()메소드를 이용하면 model에 대한 정보를 확인할 수 있다.

Model: "sequential"

_________________________________________________________________

Layer (type) Output Shape Param #

=================================================================

conv2d (Conv2D) (None, 28, 28, 32) 320

_________________________________________________________________

max_pooling2d (MaxPooling2D) (None, 14, 14, 32) 0

_________________________________________________________________

conv2d_1 (Conv2D) (None, 14, 14, 64) 18496

_________________________________________________________________

max_pooling2d_1 (MaxPooling2 (None, 7, 7, 64) 0

_________________________________________________________________

conv2d_2 (Conv2D) (None, 7, 7, 128) 73856

_________________________________________________________________

max_pooling2d_2 (MaxPooling2 (None, 4, 4, 128) 0

_________________________________________________________________

flatten (Flatten) (None, 2048) 0

_________________________________________________________________

dense (Dense) (None, 256) 524544

_________________________________________________________________

dropout (Dropout) (None, 256) 0

_________________________________________________________________

dense_1 (Dense) (None, 10) 2570

=================================================================

Total params: 619,786 Trainable params: 619,786 Non-trainable params: 0 _________________________________________________________________

< Sequential API의 한계 >

We can't make

- Multi-input models

- Multi-oput models

- Models with shared layers(the same layer called several times)

; 하나의 레이어를 share하는 경우 혹은 같은 레이어를 여러 번 반복해서 호출하는 경우

- Model with non-sequential data flow (e.g.. Residual connections)

즉, inception이나 residual connections의 돌아와서 덧셈하는 부분 등을 구현할 수 없다.

functional API는 이러한 한계를 극복할 수 있다.

 

2) tf.keras의 Functional API를 사용하는 경우(function 형태)

def create_model():
    inputs = keras.Input(shape=(28, 28, 1))
    # 여기에서 달라지는 점이 바로 마지막에 (inputs)부분이다. 마지막 괄호()안에 이 레이어에서 사용할 입력 레이어를 명시해주는 부분이다.
    # sequential API에서는 당연히 이전 레이어의 출력이 다음 레이어의 입력으로 들어간다는 암묵적인 가정하에 만들어진 구조인데,
    # functional API에서는 이를 명시하도록 되어 있다.
    # keras.layers.Conv2D의 첫 번째 괄호는 init메서드에 의해 동작하는 부분이고 두 번째는 call메서드에 의해 동작하는 부분이다.
    conv1 = keras.layers.Conv2D(filters=32, kernel_size=[3, 3], padding='SAME', activation=tf.nn.relu)(inputs)
    pool1 = keras.layers.MaxPool2D(padding='SAME')(conv1)
    conv2 = keras.layers.Conv2D(filters=64, kernel_size=[3, 3], padding='SAME', activation=tf.nn.relu)(pool1)
    pool2 = keras.layers.MaxPool2D(padding='SAME')(conv2)
    conv3 = keras.layers.Conv2D(filters=128, kernel_size=[3, 3], padding='SAME', activation=tf.nn.relu)(pool2)
    pool3 = keras.layers.MaxPool2D(padding='SAME')(conv3)
    pool3_flat = keras.layers.Flatten()(pool3)
    dense4 = keras.layers.Dense(units=256, activation=tf.nn.relu)(pool3_flat)
    drop4 = keras.layers.Dropout(rate=0.4)(dense4)
    logits = keras.layers.Dense(units=10)(drop4)
    return keras.Model(inputs=inputs, outputs=logits)
model = create_model()
model.summary()

Model: "model"

_________________________________________________________________

Layer (type) Output Shape Param #

=================================================================

input_1 (InputLayer) [(None, 28, 28, 1)] 0 # input_1 (InputLayer) 앞서 sequencial API의 차이점은 여기서 확인 가능 > input layer가 생겼다.

_________________________________________________________________

conv2d (Conv2D) (None, 28, 28, 32) 320

_________________________________________________________________

max_pooling2d (MaxPooling2D) (None, 14, 14, 32) 0

_________________________________________________________________

conv2d_1 (Conv2D) (None, 14, 14, 64) 18496

_________________________________________________________________

max_pooling2d_1 (MaxPooling2 (None, 7, 7, 64) 0

_________________________________________________________________

conv2d_2 (Conv2D) (None, 7, 7, 128) 73856

_________________________________________________________________

max_pooling2d_2 (MaxPooling2 (None, 4, 4, 128)

_________________________________________________________________

flatten (Flatten) (None, 2048) 0

_________________________________________________________________

dense (Dense) (None, 256) 524544

_________________________________________________________________

dropout (Dropout) (None, 256) 0

_________________________________________________________________

dense_1 (Dense) (None, 10) 2570

=================================================================

Total params: 619,786 Trainable params: 619,786 Non-trainable params: 0 _________________________________________________________________

3) Model Subclassing을 사용하는 경우(class 형태)

<Subclassing의 장점>

- Bild a fully-customizable model by subclassing tf.keras.Model

; 웬만한 복잡한 모델은 fuctional API를 사용하면 다 구현이 가능하지만,

subclassing 방법을 사용하면 그 안에 operation등을 자유롭게 추가할 수 있다.

- Create layers in th __init__method and set them as attributes of the class intance

; 사용 방법은 클래스 형태로 만든 다음에 레이어 부분을 init 메서드에 다 선언해주고,

- Define the forward pass in the call method

; 그 다음에 call메서드에서 그 부분에 입력을 연결해주는 방식으로 구현

>> 클래스의 형태로 모델을 만들게 되면 여러 개의 모델을 동시에 학습시킨 다음 나중에 inference할 때 그 모델들을 이용해 성능을 향상시키는 Ensemble이라는 방법을 사용하기 쉬워진다.

class MNISTModel(tf.keras.Model): # tf.keras.Model의 하위 클래스임을 선언
    def __init__(self):
        super(MNISTModel, self).__init__() # 상위 메서드인 init 메서드 호출
        # 여기서 부터는 이 안에서 functional API를 쓰느냐, sequential API를 쓰느냐에 따라 방법이 두 가지로 나뉠 수 있는데,
        # 여기서는 바로 이전에 했던 functional API를 사용하였다.
        self.conv1 = keras.layers.Conv2D(filters=32, kernel_size=[3, 3], padding='SAME', activation=tf.nn.relu)
        self.pool1 = keras.layers.MaxPool2D(padding='SAME')
        self.conv2 = keras.layers.Conv2D(filters=64, kernel_size=[3, 3], padding='SAME', activation=tf.nn.relu)
        self.pool2 = keras.layers.MaxPool2D(padding='SAME')
        self.conv3 = keras.layers.Conv2D(filters=128, kernel_size=[3, 3], padding='SAME', activation=tf.nn.relu)
        self.pool3 = keras.layers.MaxPool2D(padding='SAME')
        self.pool3_flat = keras.layers.Flatten()
        self.dense4 = keras.layers.Dense(units=256, activation=tf.nn.relu)
        self.drop4 = keras.layers.Dropout(rate=0.4)
        self.dense5 = keras.layers.Dense(units=10)

    # call 함수에서 앞에서 작성한 입력을 쭉 기술하여서 연결
    def call(self, inputs, training=False):
        net = self.conv1(inputs)
        net = self.pool1(net)
        net = self.conv2(net)
        net = self.pool2(net)
        net = self.conv3(net)
        net = self.pool3(net)
        net = self.pool3_flat(net)
        net = self.dense4(net)
        net = self.drop4(net)
        net = self.dense5(net)
        return net
model = MNISTModel()
temp_inputs = keras.Input(shape=(28, 28, 1))
model(temp_inputs)
model.summary()

Model: "mnist_model"

_________________________________________________________________

Layer (type) Output Shape Param #

=================================================================

conv2d (Conv2D) (None, 28, 28, 32) 320

_________________________________________________________________

max_pooling2d (MaxPooling2D) (None, 14, 14, 32) 0

_________________________________________________________________

conv2d_1 (Conv2D) (None, 14, 14, 64) 18496

_________________________________________________________________

max_pooling2d_1 (MaxPooling2 (None, 7, 7, 64) 0

_________________________________________________________________

conv2d_2 (Conv2D) (None, 7, 7, 128) 73856

_________________________________________________________________

max_pooling2d_2 (MaxPooling2 (None, 4, 4, 128) 0

_________________________________________________________________

flatten (Flatten) (None, 2048) 0

_________________________________________________________________

dense (Dense) (None, 256) 524544

_________________________________________________________________

dropout (Dropout) (None, 256) 0

_________________________________________________________________

dense_1 (Dense) (None, 10) 2570

=================================================================

Total params: 619,786 Trainable params: 619,786 Non-trainable params: 0 _________________________________________________________________

7. Loss Function

@tf.function
def loss_fn(model, images, labels): # 모델, 이미지, 정답을 입력 받는다.
    logits = model(images, training=True) # training=True로 설정하면, Dropout이 적용되게 된다.
    loss = tf.reduce_mean(tf.keras.losses.categorical_crossentropy(
        y_pred=logits, y_true=labels, from_logits=True))    
    return loss

 

8. Calculating Gradient

@tf.function
def grad(model, images, labels): 
# tf.GradientTape()를 사용하여 loss를 쭉 기록해놓고 back propagation을 사용하여 Gradient 계산
    with tf.GradientTape() as tape:
        loss = loss_fn(model, images, labels)
    return tape.gradient(loss, model.variables) 
    #tape.gradient의 첫 번째 파라미터는 미분이 적용되는 값, 두 번째는 미분할 값이다.
    #이 코드를 해석하자면 'model에 있는 모든 파라미터에 대해서 Gradient를 계산해주세요'이다.

 

9. Caculating Model's Accuracy

@tf.function
def evaluate(model, images, labels):
    logits = model(images, training=False) # Accuracy는 학습(training) 단계가 아니므로 False로 설정
    correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(labels, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    return accuracy

 

10. Optimizer

optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)

 

11. Creating a Checkpoint

checkpoint = tf.train.Checkpoint(cnn=model)

 

12. Training

@tf.function
def train(model, images, labels):
    grads = grad(model, images, labels)
    optimizer.apply_gradients(zip(grads, model.trainable_variables))
# train my model
# training_epochs동안에 바깥쪽 for 루프가 돌고 한 epoch동안 두 번째 for문을 돈다.
print('Learning started. It takes sometime.')
for epoch in range(training_epochs): 
    avg_loss = 0.
    avg_train_acc = 0.
    avg_test_acc = 0.
    train_step = 0
    test_step = 0    
    
    for images, labels in train_dataset:
        train(model, images, labels)
        #grads = grad(model, images, labels)                
        #optimizer.apply_gradients(zip(grads, model.variables)) # optimizer.apply_gradients로 weight들이 업데이트됨
        # 이 아래에서 부터는 사실상 학습에 꼭 필요한 코드는 아니며, loss값과 accuracy를 학습 중간중간에 계산해 평균을 내고 출력해보기 위한 코드이다.
        loss = loss_fn(model, images, labels)
        acc = evaluate(model, images, labels)
        avg_loss = avg_loss + loss
        avg_train_acc = avg_train_acc + acc
        train_step += 1
    avg_loss = avg_loss / train_step
    avg_train_acc = avg_train_acc / train_step
    
    # 한 epoch만큼 training이 끝나면 test_dataset을 넣고 모델 평가를 하고, checkpoint 저장
    for images, labels in test_dataset:        
        acc = evaluate(model, images, labels)        
        avg_test_acc = avg_test_acc + acc
        test_step += 1    
    avg_test_acc = avg_test_acc / test_step    

    print('Epoch:', '{}'.format(epoch + 1), 'loss =', '{:.8f}'.format(avg_loss), 
          'train accuracy = ', '{:.4f}'.format(avg_train_acc), 
          'test accuracy = ', '{:.4f}'.format(avg_test_acc))
    
    checkpoint.save(file_prefix=checkpoint_prefix)

print('Learning Finished!')

참고자료

https://www.edwith.org/boostcourse-dl-tensorflow/lecture/43744/

https://github.com/deeplearningzerotoall/TensorFlow/blob/master/tf_2.x/lab-11-1-mnist-cnn-keras-sequential-eager.ipynb

https://www.edwith.org/boostcourse-dl-tensorflow/lecture/43745/

https://github.com/deeplearningzerotoall/TensorFlow/blob/master/tf_2.x/lab-11-2-mnist-cnn-keras-functional-eager.ipynb

https://www.edwith.org/boostcourse-dl-tensorflow/lecture/43746/

https://github.com/deeplearningzerotoall/TensorFlow/blob/master/tf_2.x/lab-11-3-mnist-cnn-keras-subclassing-eager.ipynb

 

728x90