본문 바로가기
ML & DL/tensorflow

Hand SIGNS 분류 예제 (2) - CNN 구조 사용

by 별준 2020. 11. 17.

(tensorflow v2.3.0)

 

2020/11/17 - [ML & DL/tensorflow] - Hand SIGNS 분류 예제 (1)

 

Hand SIGNS 분류 예제 (1)

(tensorflow v2.3.0) Coursera Deep Learning 특화과정에 실습으로 진행했던 SIGNS dataset을 가지고, 위 이미지와 같이 분류를 해보도록 할 것입니다. Coursera 실습은 tensorflow 1로 진행되는데, 2버전에 맞추..

junstar92.tistory.com

이번에는 이전 게시글에서 사용했던 dataset으로 Convolutional Neural Network(ConvNet) 구조로 모델을 구현하고 학습을 해보려고 합니다. 

그리고, 이전 게시글에서 언급했듯이 label data를 one_hot 처리를 해서 loss object로 'categorical entropy'를 사용할 것입니다.

 

1. Dataset 준비

준비과정은 이전과 유사하며, Y_train과 Y_test에 대해서 tf.one_hot 함수를 통해 각 라벨에 해당하는 확률로 변경시켜 줍니다. 즉, 각 라벨에 해당하는 열은 1이고 나머지 열은 0이 됩니다.

import tensorflow as tf
import matplotlib.pyplot as plt
import h5py
import numpy as np
import math

def load_dataset():
    train_dataset = h5py.File('train_signs.h5', "r")
    train_set_x_orig = np.array(train_dataset["train_set_x"][:]) # your train set features
    train_set_y_orig = np.array(train_dataset["train_set_y"][:]) # your train set labels

    test_dataset = h5py.File('test_signs.h5', "r")
    test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # your test set features
    test_set_y_orig = np.array(test_dataset["test_set_y"][:]) # your test set labels

    classes = np.array(test_dataset["list_classes"][:]) # the list of classes
    
    return train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig, classes
    
# Loading the data (signs)
X_train_orig, Y_train_orig, X_test_orig, Y_test_orig, classes = load_dataset()

# Example of a picture
index = 6
plt.imshow(X_train_orig[index])
print ("y = " + str(np.squeeze(Y_train_orig[index])))

X_train = X_train_orig.astype(np.float32)/255.
X_test = X_test_orig.astype(np.float32)/255.
Y_train = np.array(tf.one_hot(Y_train_orig, 6))
Y_test = np.array(tf.one_hot(Y_test_orig, 6))
print ("number of training examples = " + str(X_train.shape[0]))
print ("number of test examples = " + str(X_test.shape[0]))
print ("X_train shape: " + str(X_train.shape))
print ("Y_train shape: " + str(Y_train.shape))
print ("X_test shape: " + str(X_test.shape))
print ("Y_test shape: " + str(Y_test.shape))

Y_train과 Y_test가 각 라벨의 확률을 나타내기 때문에, (m, 6)의 형태로 나타납니다. 6은 라벨 class의 개수입니다.

 

그리고 mini-batch를 사용해서 최적화를 할 예정인데, 직접 mini_batch를 무작위로 섞는 함수를 사용합니다.

def random_mini_batches(X, Y, mini_batch_size = 64, seed = 0):
    m = X.shape[0]
    mini_batches = []
    np.random.seed(seed)
    
    # Step 1: Shuffle (X, Y)
    permutation = list(np.random.permutation(m))
    shuffled_X = X[permutation,:,:,:]
    shuffled_Y = Y[permutation,:]
    
    # Step 2: Partition (shuffled_X, shuffled_Y)
    num_complete_minibatches = math.floor(m/mini_batch_size)
    
    for k in range(0, num_complete_minibatches):
        mini_batch_X = shuffled_X[k * mini_batch_size:k * mini_batch_size + mini_batch_size,:,:,:]
        mini_batch_Y = shuffled_Y[k * mini_batch_size:k * mini_batch_size + mini_batch_size,:]
        mini_batch = (mini_batch_X, mini_batch_Y)
        mini_batches.append(mini_batch)
        
    # Handling the end case
    if m % mini_batch_size != 0:
        mini_batch_X = shuffled_X[num_complete_minibatches * mini_batch_size:m,:,:,:]
        mini_batch_Y = shuffled_Y[num_complete_minibatches * mini_batch_size:m,:]
        mini_batch = (mini_batch_X, mini_batch_Y)
        mini_batches.append(mini_batch)
    
    return mini_batches

오늘 두 가지의 방법으로 동일한 모델을 구현할 예정인데, 첫 번째 방법에서는 이 함수를 사용하고, 두 번째 방법에서는 이 함수를 사용하지 않고, mini-batch를 사용해서 학습해보도록 하겠습니다.

 

시작하기에 앞서서 구현해야될 ConvNet의 구조는 다음과 같습니다.

Conv2D(F=8, stride=1, padding='same') -> ReLU -> Max pool(filter=(8,8), strides=(8,8)

-> Conv2D(F=16, stride=1, padding='same') -> ReLU -> Max pool(filter=(4,4), strides=(4,4)

-> Flatten -> Softmax(6 units)

 

2-1. tf.keras.Model을 상속받는 Model Class 구현 및 학습

www.tensorflow.org/tutorials/quickstart/advanced?hl=ko

 

텐서플로 2.0 시작하기: 전문가용  |  TensorFlow Core

Note: 이 문서는 텐서플로 커뮤니티에서 번역했습니다. 커뮤니티 번역 활동의 특성상 정확한 번역과 최신 내용을 반영하기 위해 노력함에도 불구하고 공식 영문 문서의 내용과 일치하지 않을 수

www.tensorflow.org

위 튜토리얼에서 알려주는 방법을 토대로 구현을 해보았습니다.

class ConvNet(tf.keras.Model):
    def __init__(self):
        super(ConvNet, self).__init__()
        self.conv1 = tf.keras.layers.Conv2D(8, (4, 4), strides=(1,1), padding='same',
                                            activation='relu', kernel_initializer=tf.keras.initializers.GlorotUniform(seed=0))
        self.maxpool1 = tf.keras.layers.MaxPool2D(pool_size=(8,8), strides=(8,8), padding='same')
        self.conv2 = tf.keras.layers.Conv2D(16, (2, 2), strides=(1,1), padding='same',
                                            activation='relu', kernel_initializer=tf.keras.initializers.GlorotUniform(seed=0))
        self.maxpool2 = tf.keras.layers.MaxPool2D(pool_size=(4,4), strides=(4,4), padding='same')
        self.flatten = tf.keras.layers.Flatten()
        self.d1 = tf.keras.layers.Dense(6, activation='softmax',
                                        kernel_initializer=tf.keras.initializers.GlorotUniform(seed=0))
    
    def call(self, x):
        x = self.conv1(x)
        x = self.maxpool1(x)
        x = self.conv2(x)
        x = self.maxpool2(x)
        x = self.flatten(x)
        x = self.d1(x)
        return x

class 생성자를 통해서 layer들을 만들어주고, call을 통해 \(\hat{y}\)를 계산합니다. 이 예측값은 각 라벨의 확률을 의미합니다.

 

다음으로는 학습을 진행할 함수를 구현합니다.

# Model
def Model_1(X_train, Y_train, X_test, Y_test, learning_rate=0.009,
         num_epochs=100, minibatch_size=64, print_cost=True):
    tf.random.set_seed(4)
    (m, n_H0, n_W0, n_C0) = X_train.shape
    seed = 3
    costs = []
    model = ConvNet()

    loss_object = tf.keras.losses.CategoricalCrossentropy()
    optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)

    train_loss = tf.keras.metrics.Mean(name='train_loss')
    train_accuracy = tf.keras.metrics.CategoricalCrossentropy(name='train_accuracy')

    for epoch in range(num_epochs):
        minibatch_cost=0
        num_minibatches = int(m/minibatch_size)
        seed = seed + 1
        minibatches = random_mini_batches(X_train, Y_train, minibatch_size, seed)

        for minibatch in minibatches:
            (minibatch_X, minibatch_Y) = minibatch
            
            with tf.GradientTape() as tape:
                pred = model(minibatch_X)
                cost = loss_object(minibatch_Y, pred)
            
            grads = tape.gradient(cost, model.trainable_variables)
            optimizer.apply_gradients(zip(grads, model.trainable_variables))

            train_loss(cost)
            train_accuracy(minibatch_Y, pred)

            minibatch_cost += cost / num_minibatches
        
        if print_cost and epoch % 5 == 0:
            print(f'Cost after epoch {epoch}: {minibatch_cost}')
        costs.append(minibatch_cost)
    
    # plot the cost
    plt.plot(np.squeeze(costs))
    plt.ylabel('cost')
    plt.xlabel('iterations (per tens)')
    plt.title("Learning rate =" + str(learning_rate))
    plt.show()
    
    # training set
    y_pred = model(X_train)
    y_pred = tf.math.argmax(y_pred, 1)
    y_true = tf.math.argmax(Y_train, 1)
    train_acc = tf.math.reduce_mean(tf.cast(tf.equal(y_pred, y_true), tf.float32))
    y_pred = model(X_test)
    y_pred = tf.math.argmax(y_pred, 1)
    y_true = tf.math.argmax(Y_test, 1)
    test_acc = tf.math.reduce_mean(tf.cast(tf.equal(y_pred, y_true), tf.float32))
    
    print(f'Train acc : {train_acc}')
    print(f'Test acc  : {test_acc}')

    return model

이전글에서 구현하던 방식과는 다르게 model class를 직접 구현하고, 직접 예측 확률값과 loss를 구한 다음에 tf.GradientTape()를 사용해 연산을 기록하고 자동으로 미분항를 구하고 있습니다. 

그리고 학습을 진행합니다. learning_rate(0.009), epochs(100), minibatch_size(64)는 기본값으로 사용했습니다.

m = Model_1(X_train, Y_train, X_test, Y_test)

이전 게시글에서 Regularization을 적용했을 때와 유사한 결과가 나오고 있습니다. 아직 overfitting이 되었을 가능성이 있어 보이기도 하네요.

 

모델의 구성을 살펴보면 아래처럼 구성된 것을 볼 수 있습니다.

2-2. keras sequential model을 통한 구현 및 학습

두 번째 방법으로 구현을 해보도록 하겠습니다. 이 방법은 이전 게시글들에서 사용하던 방법과 동일합니다. 

def Model_2(X_train, Y_train, X_test, Y_test, learning_rate=0.009,
               num_epochs=100, minibatch_size=64):
    tf.random.set_seed(0)
    seed = 3
    (m, n_H0, n_W0, n_C0) = X_train.shape
    n_y = Y_train.shape[1]
    costs = []
    
    # initialize parameters
    #parameters = initialize_parameters()
    # setting optimizer
    optimizer = tf.optimizers.Adam(learning_rate=learning_rate)
    
    # CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED
    model = tf.keras.models.Sequential([
        tf.keras.layers.Conv2D(8, (4, 4), strides=(1,1), padding="SAME",
                              activation='relu', input_shape=(64, 64, 3),
                              kernel_initializer=tf.keras.initializers.GlorotUniform(seed=0)),
        tf.keras.layers.MaxPool2D(pool_size=(8,8), strides=(8,8), padding="SAME"),
        tf.keras.layers.Conv2D(16, (2, 2), strides=(1,1), padding="SAME", activation='relu',
                              kernel_initializer=tf.keras.initializers.GlorotUniform(seed=0)),
        tf.keras.layers.MaxPool2D(pool_size=(4,4), strides=(4,4), padding="SAME"),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(6, activation='softmax',
                              kernel_initializer=tf.keras.initializers.GlorotUniform(seed=0))
    ])
    
    model.summary()
    
    model.compile(optimizer=optimizer,
                 loss='categorical_crossentropy',
                 metrics=['accuracy'])
    
    hist = model.fit(X_train, Y_train, epochs=num_epochs, batch_size=minibatch_size, verbose=1)
    
    costs = hist.history['loss']

    for epoch in range(0, num_epochs, 5):
        print(f'Cost after epoch {epoch}: {costs[epoch]}')
    
    # plot the cost
    plt.plot(np.squeeze(costs))
    plt.ylabel('cost')
    plt.xlabel('iterations (per tens)')
    plt.title("Learning rate =" + str(learning_rate))
    plt.show()
    
    train_acc = hist.history['accuracy'][-1]
    y_pred = model.predict(X_test)
    y_pred = tf.math.argmax(y_pred, 1)
    y_true = tf.math.argmax(Y_test, 1)
    test_acc = tf.math.reduce_mean(tf.cast(tf.equal(y_pred, y_true), tf.float32))
    
    print(f'Train acc : {train_acc}')
    print(f'Test acc  : {test_acc}')
    
    return model

그리고 학습을 진행합니다.

m2 = Model_2(X_train, Y_train, X_test, Y_test)

이전 게시글과 유사한 현상이 ConvNet에서도 발생했습니다. 아마, 초기화가 적절하지 않았기 때문에 발생했다고 추측하고 global seed를 0에서 2로 변경해서 다시 학습을 진행해보았습니다.

 

global seed = 2(tf.random.set_seed(2))

m3 = Model_2(X_train, Y_train, X_test, Y_test)

결과가 정상적인 성능으로 나왔습니다. class로 구현한 것보다 조금 덜 overfitting해보이며, Test 성능은 거의 동일하게 나오고 있습니다.

 

- Regularization 적용

두 번째 모델을 가지고 L2 Regularization을 적용해보도록 하겠습니다.

def Model_2_Reg(X_train, Y_train, X_test, Y_test, learning_rate=0.009,
               num_epochs=100, minibatch_size=64, lambd=0.01):
    tf.random.set_seed(2)
    seed = 3
    (m, n_H0, n_W0, n_C0) = X_train.shape
    n_y = Y_train.shape[1]
    costs = []
    
    # initialize parameters
    #parameters = initialize_parameters()
    # setting optimizer
    optimizer = tf.optimizers.Adam(learning_rate=learning_rate)
    # setting regularizer
    regularizer = tf.keras.regularizers.L2(lambd)
    
    # CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED
    model = tf.keras.models.Sequential([
        tf.keras.layers.Conv2D(8, (4, 4), strides=(1,1), padding="SAME",
                              activation='relu', input_shape=(64, 64, 3),
                              kernel_initializer=tf.keras.initializers.GlorotUniform(seed=0),
                              kernel_regularizer=regularizer),
        tf.keras.layers.MaxPool2D(pool_size=(8,8), strides=(8,8), padding="SAME"),
        tf.keras.layers.Conv2D(16, (2, 2), strides=(1,1), padding="SAME", activation='relu',
                              kernel_initializer=tf.keras.initializers.GlorotUniform(seed=0),
                              kernel_regularizer=regularizer),
        tf.keras.layers.MaxPool2D(pool_size=(4,4), strides=(4,4), padding="SAME"),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(6, activation='softmax',
                              kernel_initializer=tf.keras.initializers.GlorotUniform(seed=0),
                              kernel_regularizer=regularizer)
    ])
    
    model.summary()
    
    model.compile(optimizer=optimizer,
                 loss='categorical_crossentropy',
                 metrics=['accuracy'])
    
    hist = model.fit(X_train, Y_train, epochs=num_epochs, batch_size=minibatch_size, verbose=1)
    
    costs = hist.history['loss']

    for epoch in range(0, num_epochs, 5):
        print(f'Cost after epoch {epoch}: {costs[epoch]}')
    
    # plot the cost
    plt.plot(np.squeeze(costs))
    plt.ylabel('cost')
    plt.xlabel('iterations (per tens)')
    plt.title("Learning rate =" + str(learning_rate))
    plt.show()
    
    train_acc = hist.history['accuracy'][-1]
    y_pred = model.predict(X_test)
    y_pred = tf.math.argmax(y_pred, 1)
    y_true = tf.math.argmax(Y_test, 1)
    test_acc = tf.math.reduce_mean(tf.cast(tf.equal(y_pred, y_true), tf.float32))
    
    print(f'Train acc : {train_acc}')
    print(f'Test acc  : {test_acc}')
    
    return model
m4 = Model_2(X_train, Y_train, X_test, Y_test)

lambda의 값이 너무 커서 파라미터의 영향을 너무 감소시켜버린 것 같습니다.

 

- lambda = 0.001

m5 = Model_2_Reg(X_train, Y_train, X_test, Y_test, lambd=0.001)

lambda 값을 감소시켰더니, 테스트 성능이 조금 증가했습니다만, 학습 성능도 증가해버렸네요. Regularization의 효과가 쪼금은 나타난 것 같습니다. 그렇지만 dataset의 크기가 작은 편이기 때문에 overfitting을 줄이기에는 조금 힘들어 보이긴 하네요.. 이 부분은 나중에 성능을 확인해볼 수 있는 큰 dataset이 있을 때, 다시 비교해보도록 하겠습니다.

 

마지막으로 epochs를 200으로 증가시켜서 학습을 해보았습니다.

m6 = Model_2_Reg(X_train, Y_train, X_test, Y_test, num_epochs=200, lambd=0.001)

거의 90%의 테스트 성능을 달성했습니다 ! 

댓글