ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 10. 이미지 분류 사전학습모형
    general ML, DL, NLP/딥러닝 2022. 4. 21. 20:43

    *본 게시물은 22-1학기 연세대학교 일반대학원 딥러닝을이용한비정형데이터분석(이상엽 교수님) 수업 내용을 정리한 것입니다.

     

    이미지 분류는 크게 2가지로 나뉠 수 있습니다.

    CNN을 사용한 이미지 분류 사전학습모형을 사용한 이미지 분류
    직접 convolutional network를 구축하는 방법(from scratch)
    단점 존재
        - 정교한 모형을 구축하는 것이 어려움
        - 성능을 높이기 위해서는 많은 학습 데이터가 요구됨
        - 학습에 시간이 오래 걸림 
        - 매우 많은 computing power가 필요
    옆의 단점들을 보완할 수 있는 방법

    표에서도 알 수 있다시피 개인이 CNN을 구축하여 성능을 높이는 데에는 한계점이 존재합니다. 이를 보완하기 위해 보편적으로 이미지 분류 task 수행 시에는 사전학습모형을 사용합니다. 

     

    1. 사전학습모형(pre-trained model)

    사전학습모형이란 다른 task를 위해 이미 방대한 양의 학습 데이터로 학습이 되어있는 모형입니다. 이로인해 이미 모형의 구조가 정해져 있는 상태이며, 해당 구조에 맞는 최적 파라미터 값이 이미 정해져 있습니다. 사전학습모형의 장점은 다음과 같습니다.

    + 성능이 좋은 정교한 모형
    +오랜 시간에 걸쳐 대용량의 데이터를 학습한 결과를 사용할 수 있다
       * 최적 파라미터 재사용: 사전학습모형의 훈련 데이터와 유사한 데이터에 대해 task 수행 시 효과적
    + scratch 부터 학습을 할 필요가 없으며, 조금만 학습해도 됨: 학습 시간이 적게 걸림

    유명한 사전학습 모형들은 이미지넷 경진대회(ILSVRC; ImageNet large Scale visual Recognition)에서 좋은 성능을 보인 모형들입니다. VGGNet(14'), InceptionNet(GoogleNet), ResNet(15') 등이 여기 속합니다(다음 포스팅에서 자세하게 다루겠습니다)

     

    2. 사전학습모형 사용 방법

    사전학습 모형을 사용하는 방법은 크게 2가지가 있습니다. 하나는 사전학습 모형의 결과값을 그대로 사용하는 것과, 다른 하나는 전이학습입니다. 전자는 모형 구조 및 파라미터를 변형하지 않는 방법이고, 후자는 그 반대입니다.

     

    (1) 사전학습모형 결과값&파라미터를 그대로 사용하기

    이 방법은 사전학습모형의 원래 구조와, 원래 학습 데이터를 통해 얻어진 최적 파라미터의 값을 그대로 사용하는 방법입니다. 때문에 수행하고자 하는 task가 사전학습모형의 task와 같거나 비슷한 경우에만 그대로 사용합니다. 예로 'Imagenet 1000'이란 사전학습모형은 동물 사진으로 사전학습된 모형인데, 내가 수행하고자 하는 task가 동물 종류 분류일 경우 그대로 활용할 수 있습니다. 


    i) 사전학습모형: VGG16

    Keras에서 사전학습모형을 클래스 형태로 바로 가져올 수 있습니다. VGG16의 경우에도 Keras의 클래스로 제공됩니다.

    클래스는 바로 가져다 쓸 수 없으므로, 클래스 이름과 동일한 생성자 함수를 사용하여 객체를 생성한 후에 사용해야 합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # Keras에서 VGG16 모형: VGG16라는 클래스로 제공
     
    from tensorflow.keras.applications.vgg16 import VGG16 
     
    # VGG16라는 클래스를 VGG16() 생성자 함수를 통해 model 이란 객체를 만듦
    model = VGG16(weights='imagenet', include_top=True# 총 파라미터수 138,357,544
     
    # 사전학습모형 클래스는 weights, include_top이란 2가지 파라미터를 가짐
    # weights: imagenet/none(=새롭게 학습 시 선택)
    #nclude_top: 사전학습모형의 dense 레이어를 가져올지 설정- True/False
     
    model.summary()
     
    '''
    # VGG16의 탑층                                                              
     fc1 (Dense)                 (None, 4096)              102764544                                                    
     fc2 (Dense)                 (None, 4096)              16781312  
     predictions (Dense)         (None, 1000)              4097000   
     '''
    cs

    위 코드에서는 weights와 include_top을 모두 원래의 VGG16 것으로 가져오는 코드입니다. 즉 파라미터도 원래 VGG16의 것, 탑층도 원래의 VGG16 것을 가져오는 코드입니다. 여기서 top층이란 보편적으로 CNN 기반 모형에 붙어있는 dense 층을 의미합니다.

    노란색 박스 부분이 top layer입니다

    이렇듯 원래 VGG16모델의 생김새는 아래와 같은데, 이전에 CNN으로 구축했을 때에 비하면 정말 많은 convolutional layer가 많은 것을 확인할 수 있습니다. 또한 VGG16를 그대로 가져오는 것이므로 VGG16의 탑층 역시 확인이 가능합니다.

    이걸 VGG16의 논문에서는 아래와 같은 그림으로 설명하고 있습니다.

     

    만일 VGG16에서 탑층을 뺀다면? 다음과 같이 파라미터의 수가 1/10으로 변하면서, dense layer들이 모두 사라진 것을 확인할 수 있습니다.

    1
    2
    3
    # include_top = True와 비교
    model1 = VGG16(weights='imagenet', include_top=False# 총 파라미터가 14,714,688로 1/10으로 줄어듦
    model1.summary()
    cs

     

    ii) VGG16에 맞는 이미지 사이즈 맞추기

    기존 사전학습모형을 그대로 사용하기 위해서는 사전학습모형이 입력받는 모양으로 변형시켜야 합니다. 보편적으로는 특정 크기의 정사각형 형태로 이미지를 받으며 ,VGG16의 경우 224*224*n 사이즈로 받습니다.

    이에 따라 특정 크기의 정사각형으로 이미지를 만들기 위해서는 ①이미지를 정말 정사각형으로 만든 후 → ② 이를 축소&확대하는 resize를 시행합니다. 일단 이미지를 불러오겠습니다. 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from tensorflow.keras.applications.vgg16 import preprocess_input
    from tensorflow.keras.preprocessing import image
     
    import matplotlib.pyplot as plt
    from PIL import Image
    from tensorflow.keras.applications.imagenet_utils import decode_predictions
    from tensorflow.keras.applications.imagenet_utils import preprocess_input
    import numpy as np
     
    # 분류하고자 하는 이미지
    img = Image.open('cat.jpg')
    img.size # 원 이미지 사이즈는 300*280
     
    plt.imshow(np.asarray(img))
    cs

    이미지 사이즈가300*280인데요, 정사각형이 아닙니다ㅠ_ㅠ 이미지는 다음과 같이 보여집니다. 

    쪼금 정사각형이 아닙니다 ^^;

     

    ① 정사각형으로 만들기 

    ⑴ cropping: 가운데를 중심으로 하여 정사각형으로 만듭니다.

       ※ 그렇다면 이미지가 한 쪽으로 쏠렸을 때 cropping은 어떻게 할까요.....?

            - 이럴 땐 다른 방법을 사용하는 것이 속 편합니다. 주로 padding이 대신 쓰입니다.

            - 또 다른 방법은 object detection 알고리즘(SSD, YOLO...)을 사용하는 것입니다.

              이를 위해 원래 이미지에서 bounding box를 사용하여 대상을 찾고, open CV를 사용해서 대상만 잘라버립니다. 

    ⑵ warpping: 가로, 세로를 서로 다른 비율로 확대/축소해서 정사각형으로 만듭니다.

           -  그림판에서 그냥 그림을 조작해서 정사각형으로 만든다고 생각하면 되겠습니다. 

           -  억지로 잡아 늘리거나 줄이기 때문에 (...) 왜곡이 발생하게됩니다 ㅠ

    ⑶ padding: 세로로 긴 이미지의 경우 가로를 패딩하게 되며, 주로 zero-padding을 시행하게 됩니다.

    이상엽 교수님 수업 자료입니다

    이를 코드로 풀어내면 다음과 같습니다. crop()을 사용합니다. 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    w, h = img.size
    = min(w, h)
    = (h - s) // 2
    = (w - s) // 2
    print(w, h, x, y, s) # 짧은 축으로 center cropping: 가로를 세로 280 기준으로 자름
    # 300, 280, 10, 0, 280
    img = img.crop((x, y, x+s, y+s))
    # 4-tuple defining the left, upper, right, and lower pixel coordinate
    plt.imshow(np.asarray(img))
    img.size
    cs

    280 * 280의 정사각형이 되었습니다.

     

    ②  resize 하기

    ①에서 크롭핑을 한 후, VGG16가 받는 사이즈인 224*224로 변경하는 방법입니다. 

    어떤 사전학습모형을 사용하느냐에 따라 입력 크기가 다르기 때문에, input_shape를 반드시 확인해야 합니다. 이는 아래 코드로 가능합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    '''
     VGG16이 입력받는 크기로 이미지를 resize (확대 또는 축소) 하는 방법
    이를 위해 먼저 입력받는 이미지의 크기를 아래와 같이 확인해야 합니다
    '''
    model.layers[0].input_shape #[(None, 224, 224, 3)]
    # 224x224의 칼라이미지를 입력 받는 것을 확인 가능
    # layer[0]은 입력층
    # 컬러 이미지를 받으므로 3
     
    '''
    원하는 사이즈로 변환
    '''
    target_size = 224
    img = img.resize((target_size, target_size)) # resize from 280x280 to 224x224
    plt.imshow(np.asarray(img))
    cs

    224*224로 변환이 되었습니다.

    iii) VGG16에 넣어서 학습시키기 위한 변형(array, 차원 확장) 

    위에까지는 이미지 데이터를 변경하는 방법이었습니다. 그러나 컴퓨터는 수학밖에 모르는 바보이므로 이를 array로 변환해주어야 합니다. 정확히는 CNN based모형이 array형태로 입력을 받기 때문에 이미지를 array로 변환하는 과정이 필요합니다.

    또한 사전학습모형은 대부분 mini batch로 이미지를 받게 됩니다. 즉,  이미지를 한꺼번에 여러개를 받게 되는데, 이때 3D(224*224*3)인 이미지 낱장을 여러개로 묶게 되므로 입력은 4D(한번에 n장, 224, 224, 3)가 되어야 합니다. 가령 10장이 한꺼번에 들어가는 경우는 (10, 224, 224, 3)이 됩니다. 

    이에 4차원 array로 차원을 올려주는 과정도 필요한데, 이런 차원 확장은 np.expand_dims(np_img, axis=0)을 통해 축을 하나 더 추가함으로써 이루어집니다. 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 아직은 3차원
    np_img = image.img_to_array(img)
    np_img.shape # (224, 224, 3)
     
     
    # 4차원으로 올려주기
    img_batch = np.expand_dims(np_img, axis=0)
    img_batch.shape # (1, 224, 224, 3)
    img_batch
    cs

    여기까지 완료되었으면 이제 여느 이미지 분류처럼 정규화를 하고 predict()를 수행하게 됩니다. fit()없이 바로 predict()를 수행하는 이유는 아래에서 설명하겠습니다.

     

    iv)  이미지 normalization & predict()

      앞선 포스팅에서도 설명하였지만, 이미지가 가진 원래의 색상 정보는 0부터 255까지 총 256개입니다. 그러나 이 정보를 그대로 사용하지 않고 normalization을 항상 진행하게 됩니다. 이는 학습 속도 및 모형 성능을 높이기 위해 시행하는 것입니다.

      대부분은 0부터 1이하의 값을 가지게 하는 normalization을 진행하나, 사전학습모형마다 다릅니다. VGG16는 0을 기준으로 centering하는 방식으로 정규화를 진행합니다. 이는 VGG16 모형에 대한 preprocess.input() 함수로 진행할 수 있습니다. (사전학습모형마다 preprocess.input()을 쓸 수 있는데, 정규화 방식도 모형에 따라 다르다고 합니다 :D)

    1
    2
    pre_processed = preprocess_input(img_batch)
    pre_processed # 0기준으로 centering하는 것이 VGG16의 정규화 과정 
    cs

    정규화된 값은 위와 같습니다 그동안 본 normalization과는 조금 다르네요 D:

    그 다음은 바로 예측입니다(?)

    이는 지금까지 VGG16이 훈련된 데이터와 비슷한 데이터(동물)를 사용하였고, 또 모형과 파라미터를 전혀 변형시키지 않았기 때문입니다. 즉 새로운 데이터로 새롭게 학습할 필요가 없으므로 준비된 test 이미지에 대해 바로 예측으로 진행할 수 있습니다.

    모형을 변경시키지 않았으므로 라벨 수는 VGG16의 것인 1000개가 됩니다. 출력은 softmax를 통해 확률값으로 리턴되며, 나올 수 있는 값은 총 1000개입니다. 보통은 top=k를 사용하여 k개 만큼의 결과를 봅니다. 여기서는 top 5를 볼 예정입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    y_preds = model.predict(pre_processed)
    y_preds.shape
    # 종속변수가 취할 수 있는 값의 수 = 1000
     
    import numpy as np
    np.set_printoptions(suppress=True, precision=10)
    y_preds # 1000개의 값: 1000개 종속변수 별로 특정 종속변수 확률을 가질 확률
    # 각 원소의 값은 종속변수가 각 값을 갖을 확률을 의미
    '''
    array([[0.0000032612, 0.0000060079, 0.0000025853, 0.0000069873,
            0.0000008464, 0.0000032147, 0.0000008438, 0.0000131594,
            0.0000044511, 0.0000009792, 0.0000006534, 0.0000008105,
            0.0000073706, 0.0000035371, 0.0000006063, 0.0000171213,
            0.0000014485, 0.0000036095, 0.0000035043, 0.0000004541,
            0.0000025327, 0.0000025048, 0.0000009287, 0.0000013124,
            0.0000670184, 0.0000008082, 0.0000007956, 0.0000008279,
            0.0000010994, 0.00000127  , 0.0000017228, 0.0000017564,
            0.0000003946, 0.0000003532, 0.0000008471, 0.0000040001,
            0.0000070445, 0.00000962  , 0.0000015835, 0.0000202382,
            0.000003013 , 0.0000035757, 0.0000009824, 0.0000045424,
            0.000008345 , 0.0000017351, 0.0000234198, 0.0000037385,
            0.0000024718, 0.000002973 , 0.0000048447, 0.0000349352,
            0.0000018158, 0.0000009033, 0.0000042851, 0.0000025488,
            0.0000017399, 0.0000007366, 0.0000015019, 0.0000028745,
            0.0000030252, 0.0000010806, 0.0000006394, 0.0000003941,........
    '''
     
    #가장 높은 확률 리턴
    np.max(y_preds) # 0.464918
     
    # 상위 5위 확률 리턴
    decode_predictions(y_preds, top=5# 상위 5개의 라벨만 보여줌 
    # tabby는 줄무늬 고양이: 잘 맞춘 것 같습니다>..^^....
    '''
    [[('n02123045', 'tabby', 0.464918),
      ('n02123159', 'tiger_cat', 0.352474),
      ('n02124075', 'Egyptian_cat', 0.0988085),
      ('n04040759', 'radiator', 0.005434628),
      ('n03223299', 'doormat', 0.004960792)]]
    '''
     
    cs

    이렇게 cats라는 이미지에 대해 VGG16를 이용하여 라벨 분류를 한 결과, 'tabby'라는 라벨로 이미지가 분류되었습니다. 만일 다른 이미지에 대해 VGG16을 사용하여 분류하고 싶은 경우 img = Image.open('dog.jpg') 로 지정하여 같은 과정으로 분류를 하면 됩니다

     

     

    (2) 전이학습(transfer learning)

      모형의 일부를 변형(층 제거, 추가 등...)하거나 파라미터 일부 혹은 전체를 새로운 학습 데이터로  재학습(fine tuning)시키는 방법입니다. 모형의 구조나 파라미터가 변경되는 특징이 있습니다. 때문에 전이학습을 할 때에는 수행하고자 하는 task에 맞는 새로운 학습 데이터가 필요하며, 성능 좋은 기존 모델을 재활용하는 형태이기에 처음부터 CNN 등을 쌓아서 모델 구조를 생성하는 것보다 훨씬 효율적인 학습이 가능합니다.  

    구체적인 전이학습 종류에는 3가지가 존재합니다.

     

    1) 전통적 기계학습 알고리즘과의 emsemble

    -사전학습모형이 출력하는 값을 feature로 하여 전통적인 기계학습 알고리즘(logistic regression, SVM, XGBoost....)등과 앙상블하는 방법입니다.

    -최근에는 많이 사용되지 않습니다.

     

    2) 기존 모형의 일부 layer 제거(보통 top layer) +여기에 다른 레이어를 추가하여 분류 수행

    -모델의 일부를 변형하는 방법입니다. 그림으로 표현하면 아래와 같습니다.

    사전학습모형의 기존 탑층을 자르고 새로운 레이어를 추가합니다. 여기서는 FC 레이어가 새로 추가되었습니다.

    여기에서는  기존 MobileNet에서 탑층을 제거한 후, 새로운 층을 추가하여 해당 층에 존재하는 파라미터에 대해 새로운 학습을 진행하는 것에 대해서 소개할 예정입니다. 앞서 사전학습모형을 그대로 사용할 때와 마찬가지로 데이터 별로 폴더에 따로 저장해주어야 하는데요, 여기에서는 쓰임별로 훈련 데이터/평가 데이터, 그리고 라벨별로 고양이/강아지를 분리해서 저장합니다. 

    위의 내용을 코드로 구현하면 아래와 같습니다. 데이터 불러오는 과정에서 폴더가 모두 개별적으로 나누어져 있음을 확인할 수 있습니다. 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 모듈, fucntion 불러오기 
    import tensorflow as tf
    from tensorflow.keras.preprocessing.image import ImageDataGenerator
    from tensorflow.keras.preprocessing import image
    from tensorflow.keras.models import Model
    from tensorflow.keras.layers import Input, Flatten, Dense, GlobalAveragePooling2D
    from tensorflow.keras.applications.mobilenet import MobileNet, preprocess_input
    import math
    import numpy as np
     
     
    # 데이터 불러오기: 데이터별로 저장된 폴더가 모두 다름을 확인할 수 있습니다.
    TRAIN_DATA_DIR = './cats_and_dogs_small/train' # 데이터 저장 경로
    VALIDATION_DATA_DIR = './cats_and_dogs_small/validation'
    TEST_DATA_DIR = './cats_and_dogs_small/test'
     
    TRAIN_SAMPLES = 800*2 # 몇 장의 사진이 있을지도 지정(800장 * 2개의 클래스: 캣, 독) 
    VALIDATION_SAMPLES = 400*2  # 400장 * 2개의 레벨
    NUM_CLASSES = 2 # 맞추고자 하는 클래스 수 
    IMG_WIDTH, IMG_HEIGHT = 224224 # image size for MobileNet
    BATCH_SIZE = 64 # 미니배치 크기도 미리 지정 
    cs

     

    i) Data Augmentation

    data augmentation이란 원본 이미지에 약간의 변형을 가하여 새로운 이미지를 생성하여 학습 데이터의 양을 증가시키는 기법입니다. (컴퓨터는 변형된 이미지를 다른 이미지로 인식합니다 ... 생각보다 바보네요....)

    특히 모델에 새로운 층을 추가하는 경우, 그리고 학습 데이터 양이 많지 않은 경우 데이터 증강 기법을 사용할 수 있습니다. 

    새로운 층을 추가하는 경우 새롭게 학습해야 하는 많은 수의 파라미터
    fine-tuning의 경우에도 파라미터 수가 증가하여 증강이 꼭 필요
    학습 데이터 양이 많지 않은 경우 학습 데이터가 적다면 과적합이 발생함
    이에 따라 정답이 있는 이미지를 추가로 준비하거나(어려움 ㅠㅠ)
    혹은 지금 가지고 있는 데이터에 augmentation을 시행함 

     주요한 방법으로는 다음의 방법들이 있습니다. 오른쪽은 보기만해도 너무 어려워보이므로 Keras의 ImageDataGenerator 클래스로 구현할 수 있는 왼쪽 방법을 사용합니다. 이 때 data augmentation은 오직 훈련 데이터에만 적용합니다.

    Affine transformation Deep learning based methods
    Rotation
    Random Shift: 이미지를 좌, 우로 움직임
    Zoom: 이미지에 대해 줌인 / 줌아웃
    Flipping: 이미지 뒤집기
    auto-encoder를 사용한 feature space augmentation
    GAN을 사용한 방법...

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # data augmentation 지정 
    # 이미지데이터제너레이터 생성자 함수를 통해 affine transformation을 지정 
    # 데이터 증강은 "학습 데이터"에 대해서만 수행 
    # 검증 데이터, 평가 데이터는 원래 그대로의 이미지에 대해서만 평가하고자 하는 목적을 가짐
      # 때문에 원래 이미지는 훼손하면 안됨 
    train_datagen = ImageDataGenerator(preprocessing_function=preprocess_input,
                                       # 셀의 값을 normalization
                                       rotation_range=20,
                                       width_shift_range=0.2,
                                       height_shift_range=0.2,
                                       zoom_range=0.2
                                       horizontal_flip=True
                                       vertical_flip=True)
    val_datagen = ImageDataGenerator(preprocessing_function=preprocess_input) # 검증 데이터는 aug하지 않음, 전처리만 함 
    # We don't augment data for validation dataset!
    cs

    위 코드는 train_datagen이라는 객체에 ImgeDataGenerator라는 생성자 함수로 구체적으로 증강을 어떻게 할 것인지 affine 파라미터로 지정을 해놓은 상태입니다. 또한 val_datagen이란 객체에는 ImageDataGenerator를 통해 validation data에 대해 전처리를 어떻게 가해라라는 내용이 담겨있습니다. 이들을 실제 데이터로 불러올 때는 아래 코드와 같이 불러옵니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    # train_datagen을 사용해서 학습 데이터를 불러옴, 실제 data augmentation이 이뤄짐
    # flow_from_directory fucntion으로 불러옴
    # 경로에 저장된 학습 데이터를 불러옴: 불러올 때 aug.가 된 상태로 불러옴 
    # 이미지 크기도 자동적으로 변환: 224*224
    # 한번에 배치사이즈 64만큼 불러옴 (원본데이터 그대로를 사용하는 것이 아닌, 원본 데이터를 aug.한 이미지를 사용)
    # 랜덤하게 불러오는 것도 포함 
    train_generator = train_datagen.flow_from_directory(TRAIN_DATA_DIR,# 경로에 저장된 학습 데이터를 불러옴: 불러올 때 aug.가 된 상태로 불러옴 
                                                        target_size=(IMG_WIDTH,# 이미지 크기도 자동적으로 변환: 224*224
                                                                     IMG_HEIGHT),
                                                        batch_size=BATCH_SIZE,# 랜덤하게 불러오는 것도 포함 
                                                        shuffle=True,
                                                        seed=12345,
                                                        class_mode='categorical')
    # val_datagen을 이용해서 검증 데이터를 불러옴
    # 검증 데이터는 그대로 불러옴 # affine 파라미터를 세팅하지 않았기 때문 
    validation_generator = val_datagen.flow_from_directory(
        VALIDATION_DATA_DIR,
        target_size=(IMG_WIDTH, IMG_HEIGHT),
        batch_size=BATCH_SIZE,
        shuffle=False,
        class_mode='categorical')
     
    # 학습데이터 1600장
    # 검증은 800장
    # 위 상태까지는 종속변수 지정할 필요가 없음, 폴더 이름에 따라 자동으로 지정됨
    # Found 1600 images belonging to 2 classes.
    # Found 800 images belonging to 2 classes.
     
    cs

    경로에 저장된 데이터들 중 훈련 데이터는 train_datagen에 저장된 내용처럼 증강을 거친 후에 불러져오고, 평가 데이터는 val_datagen에 저장된 내용처럼 불러와집니다.

     

    ii) top layer를 뺀 MobileNet 불러오기

    이번에는 탑층을 제외한 사전학습모형을 불러오겠습니다. 앞서 탑층을 빼는 방법은 include_top에 False를 설정하는 것이었습니다. 아래 코드와 같이 구현됩니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    # 사전학습모형 불러오기: MobileNet() 생성자 함수로 객체 생성
    # 모바일넷을 그대로 사용하는 것이 아닌, 일부를 변경해서 사용:top 부분 레이어를 제거하고 사용할 예정
    # 새로운 레이어를 추가했으므로 학습 데이터를 또 추가: 학습데이터가 충분치 않으므로 aug.까지 동원한 것 
    # 새로운 모형은 사전학습모형을 불러와서 top 부분 레이어를 없애버림   
    model = MobileNet()
    # model에는 top부분을 포함할지 말지 결정하는 include_top을 가짐 
    # 이를 아래 model1과 같이 False로 설정하면 됨
     
     
    '''
    여기가 top층
    global_average_pooling2d (G  (None, 1, 1, 1024)       0         
     lobalAveragePooling2D)                                          
                                                         
     dropout (Dropout)           (None, 1, 1, 1024)        0         
                                                                     
     conv_preds (Conv2D)         (None, 1, 1, 1000)        1025000   
                                                                     
     reshape_2 (Reshape)         (None, 1000)              0         
                                                                     
     predictions (Activation)    (None, 1000)              0
    =========================================================
    Total params: 4,253,864
    Trainable params: 4,231,976
    Non-trainable params: 21,888
    =========================================================         
                                                                     
    '''
     
    model1 = MobileNet(include_top=False, input_shape=(IMG_WIDTH, IMG_HEIGHT, 3))
    # model1 객체에는 top부분을 포함할지 말지 결정하는 include_top을 가짐 
    # 이를 False로 설정하면 top층이 날아감
    # 탑층이 날아갔기에 출력층도 날아감: 몇 개의 레이어를 더 넣어줘야 함 
    model1.summary()
    '''
    위 top층이 제외되어 파라미터 수가 줄어들었습니다.
    =========================================================
    Total params: 3,228,864
    Trainable params: 3,206,976
    Non-trainable params: 21,888
    =========================================================
    '''
    cs

     

    위 코드에서 불러온 상태는 탑층이 없는 MobileNet 상태입니다. 이에 따라 새로운 레이어를 추가해야 하는데 이번에는 사용자 함수를 만들어서 아예 탑층이 빠진 모델을 불러온 후, 그 상태에서 functional한 방법으로 추가해보겠습니다. functional 방법은 sequential과 다르게 시루떡처럼 층층이 층을 쌓지 않습니다. (문과가 보기에는 상당히 어려운) 함수를 사용하는 것과 비슷하게 층을 쌓게 됩니다. 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    # 새로운 레이어 끼워넣기 
     
    def model_maker():
        # 일단 먼저 top 층을 제외한 사전학습 모형을 불러옴: include_top으로 결정
        # 합성곱 층의 가장 마지막 형태는 7*7*1024 (None, 7, 7, 1024)의 3차원 형태 
        base_model = MobileNet(include_top=False,
                               input_shape=(IMG_WIDTH, IMG_HEIGHT, 3)) # 탑층이 날아간 Mobilenet
        # 새로운 층을 추가할 때에는 sequential, functional이 있는데, transfer learning을 할 때는 주로 functional을 사용 
        # functional은 함수처럼 추가함 
        for layer in base_model.layers[:]:
            layer.trainable = False 
            # 각 layer의 학습 여부 결정 => 기본값은 True로 되어 있음
            # 이번 예제에서는 모형의 구조만 변경, 파라미터는 새롭게 학습하지 않음
        # Input 클래스를 이용해서 입력 데이터의 형태를 지정
        input1 = Input(shape=(IMG_WIDTH, IMG_HEIGHT, 3))
        custom_model = base_model(input1) # 탑층을 제외한 기존층은 custom_model에 다 들어가있음(소괄호 안에는 기존 구조를 담은 객체를 인자로 입력) 
        # 현 시점까지 custom_model은 base_model을 그대로 받고 있음: base_model은 탑층이 제거된 모바일넷 모형 
     
        # 합성곱만 있는 base_model에 글로벌에버리지풀링 레이어를 쌓음: by. functional 방법
        custom_model = GlobalAveragePooling2D()(custom_model)# Global층을 쌓음: 생성자함수를 먼저 입력하고, 소괄호 안에는 기존 구조를 담은 객체를 함수에 전달하는 인자로 입력 
        # 글로벌에버리지풀링: 각 채널 (즉, 하나의 7x7 채널)에서 평균값 하나만 추출
        
        custom_model = Dense(64, activation='relu')(custom_model) # 다음은 Dense층을 쌓음(소괄호 안에는 이전 객체)
        predictions = Dense(NUM_CLASSES, activation='softmax')(custom_model) # 출력층: 분류에서 사용되는 문제기에 활성화 함수는 softmax를 사용 
        # 모든 최종 결과물은 predictions에 저장이 된 상태 
        return Model(inputs=input1, outputs=predictions) # 인풋은 입력층, 아웃풋은 출력층을 입력 
     
    '''
    만든 모델을 살펴보면...
    1. 일단은 모바일넷의 합성곱 층만을 사용 
    2. 1의 합성곱 층에 글로벌에버리지풀링을 추가 
    3. 2에 dense(노드 수가 64인)추가
    4. 마지막에 출력층 추가 
    * Global pooling은 액티베이션 맵의 뎁스 하나에서 하나의 값만 출력 
    * 합성곱 마지막 층은 7*7*1024 : 즉 마지막 뎁스는 총 1024 dim.
    * 글로벌 풀링 적용 시 총 1024개의 값이 나옴
    * dense는 64
    '''
    cs

    위 코드를 좀 더 깔끔하게 표현하면 다음과 같습니다. 이 방법이 functional 방법입니다.

    윗 단계 객체가 아래 단계에 있는 함수의 인자가 됩니다.

    위에서 만든 model_maker()함수로 만들어낸 최종 모델입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    model_final = model_maker()
    model_final.summary()
    '''
    만든 모델을 살펴보면...
    1. 일단은 모바일넷의 합성곱 층만을 사용 
    2. 1의 합성곱 층에 글로벌에버리지풀링을 추가 
    3. 2에 dense(노드 수가 64인)추가
    4. 마지막에 출력층 추가 
    * Global pooling은 액티베이션 맵의 뎁스 하나에서 하나의 값만 출력 
    * 합성곱 마지막 층은 7*7*1024 : 즉 마지막 뎁스는 총 1024 dim.
    * 글로벌 풀링 적용 시 총 1024개의 값이 나옴: 이에 따라 결과는 (None,1024)
    * dense는 64이므로 꼴이 (None, 64)가 됨: 파라미터 수는 1025*64
    *출력층은 65*2 = 130
    '''
    cs

    그 뒤는 여느 이미지 분류 모형과 똑같습니다. 컴파일 후 훈련시킵니다. 그리고 훈련된 모델로 평가를 진행하게 됩니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 데이터 준비, 모형 구축, 그 다음은 학습
    # 학습이 필요한 이유는 새로 추가한 층의 파라미터를 업데이트 해야하기 때문 
    # 학습은 마지막으로 만든 모델에 대해서 함 : model에 대해 하면 대략난감
    # 학습을 하기 위해서는 비용함수(CE), 비용함수 최저값을 찾기 위한 옵티마이저, 성능 지표를 지정 
    # 학습 시 이미지데이터제너레이터를 통해 불러오고 있으믕ㄹ 유념해야 함 : 때문에 얘가 fit 펑션의 첫번쨰 인자 
    # 제너레이터를 통해 한번에 64개 이미지를 aug.를 해서 보내줌 
     
    model_final.compile(loss='categorical_crossentropy',
                  optimizer=tf.keras.optimizers.Adam(0.001),
                  metrics=['acc'])
    history = model_final.fit(
        train_generator, # 학습데이터를 불어오는 역할을 하는 train_generator를 첫번째 인자로 지정
        steps_per_epoch=TRAIN_SAMPLES // BATCH_SIZE, # number of updates, 1 epoch 당 업데이트 횟수를 지정
        epochs=20,
        validation_data=validation_generator, # 검증 데이터를 불러옴: 역시 제너레이터를 통해
        validation_steps=VALIDATION_SAMPLES // BATCH_SIZE) # 업데이트 횟수를 지정해주어야 함 귀찮..!
    # 이렇게 하면 ImageDataGenerator가 각 batch마다 random하게 BATCH_SIZE (=64)에 해당하는 이미지를 생성함
    cs

    훈련 추이는 아래와 같이 그래프를 플롯하여 확인할 수 있습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 과적합인지 아닌지를 체크 
    # 이 상태로는 모르므로 에폭 수를 높여야 함: 과적합 전까지 
    import matplotlib.pyplot as plt
    plt.plot(history.history['loss'])
    plt.plot(history.history['val_loss'])
    plt.xlabel('epoch')
    plt.ylabel('loss')
    plt.legend(['train','val'])
    plt.show()
     
     
    # 과적합인지 아닌지를 체크 
    # 이 상태로는 모르므로 에폭 수를 높여야 함: 과적합 전까지 
    import matplotlib.pyplot as plt
    plt.plot(history.history['acc'])
    plt.plot(history.history['val_acc'])
    plt.xlabel('epoch')
    plt.ylabel('accuracy')
    plt.legend(['train','val'])
    plt.show()
    cs

     

    훈련된 모델을 가지고 test를 수행합니다. 해당 코드는 test dataset 통째로에 대한 test 결과입니다. 이미지를 불러오는 것 역시 위에서 train_datagen, val_datagen과 비슷합니다. 훈련된 모델은 약 0.98의 accuracy를 보입니다 엄청 높네요 wow

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 모형의 성능을 평가: test(평가) 데이터셋에 대해 평가 
    test_datagen = ImageDataGenerator(preprocessing_function=preprocess_input)
     
    test_generator = test_datagen.flow_from_directory(
        TEST_DATA_DIR,
        target_size=(IMG_WIDTH, IMG_HEIGHT),
        batch_size=BATCH_SIZE,
        shuffle=False,
        class_mode='categorical'# 애도 제너레이터를 통해서 불러옴 
    #Found 800 images belonging to 2 classes.
     
    model_final.evaluate(test_generator, steps=800 // BATCH_SIZE,verbose=1# 테스트 제너레이터가 평가데이터 저장 폴더에서 알아서 불러와줌 
    # 에폭당 업데이트 횟수를 지정: 800장의 테스트 이미지// 배치사이즈로 나눔 
    # 같은 문제를 단순 CNN으로 풀었을 때보다 성능이 훨씬 높아짐
    # 사전학습 모형을 이용해야 하는 이유
    '''
    12/12 [==============================] - 9s 755ms/step - loss: 0.0558 - acc: 0.9831
    [0.055832359939813614, 0.9830729365348816]
    '''
    cs

    낱장 이미지에 대해서도 테스트를 수행할 수 있습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 하나의 이미지에 대해서 라벨 예측
     
    np.set_printoptions(suppress=True)
    import matplotlib.pyplot as plt
    img_path = 'cat.jpg'
    img = image.load_img(img_path, target_size=(224224))
    img_array = image.img_to_array(img)
    plt.imshow(img_array/255)
    expanded_img_array = np.expand_dims(img_array, axis=0)
    preprocessed_img = expanded_img_array / 255
    # 낱개 이미지 라벨 예측
    prediction = model_final.predict(preprocessed_img)
    print(np.array(prediction[0])) #[0.99997497 0.00002498]
    cs

     

    3) Fine Tuning

      사전학습모형이 가진 파라미터 일부 혹은 전체를 새로운 학습 데이터로 새롭게 학습하는 방법입니다. 좀 더 정확히는 2)의 방법과 같이 탑층을 날리는 것만이 아니라, 모형 내 특정 층의 파라미터를 다시 새롭게 학습하는 방법이라 할 수 있습니다.

      사전학습모형 사용 시 초기값은 기존 사전학습모형이 가지고 있는 최적 파라미터 값이므로 새로운 파라미터 값을 찾더라도 효율적인 학습(시간, 데이터 양)이 가능하다는 장점이 있습니다. 

    사전학습모형의 '특정층' 파라미터를 새롭게 학습하는 것이므로 어떤 층을 새롭게 학습할 지를 골라야 합니다. 보통은 '출력층'에 가까운 파라미터를 새롭게 학습합니다. 이는 입력층 가까이 있는 층과 출력층 가까이 있는 층의 특징이 다르기 때문입니다. 아래는 사람 얼굴을 인식하는 task를 위한 각 합성곱층을 시각화한 그림인데, 입력층과 출력층 부분에서 차이가 나는 것을 알 수 있습니다. 출력층 쪽 합성곱층을 시각화한 것이 사람 얼굴과 근접합니다. 즉 사람 얼굴을 인식할 수 있는 feature들이 매우 잘 추출된 상태인 것입니다. 

    이상엽 교수님 수업자료입니다.

    입력층과 가까운 층 출력층과 가까운 층 
    ▷이미지가 가진 일반적 정보(ex. 색상...)가 추출됨
    ▷ task가 바뀐다고 해도 출력값이 거의 비슷하므로 그대로 재사용해야하는 가능성이 높음
    ▶task에 특화된 정보들이 추출됨
    ▶따라서 task에 적합한 파라미터를 가지도록 훈련시켜야 함 

     

    앞서 탑층을 빼고 가져올 때 layer.trainable = False를 했던 적이 있습니다. 이는 탑층을 제외한 원래 사전학습모형의 층을 건드리지 않기 위함이었습니다. (층을 freeze한다고도 표현합니다)

      그러나 fine-tuning은 사전학습모형의 원래 레이어를 건드리게 됩니다. 이에 따라 layer.trainable=True를 지정하게 되는데, 사실 trainable의 본래 설정값은 True이므로 실질적으로는 얼리고 싶은 레이어를 슬라이싱으로 지정하여 False를 지정해줍니다.

      코드는 다음과 같습니다. 탑층을 빼고 불러온 MobileNet에 대해 마지막에서 세번째([:-2])까지는 fine tuning을 하지 않고, 나머지 끄트머리 2개 층과 dense층( 2)와 동일)을 새롭게 학습하는 코드입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def model_maker():
        base_model = MobileNet(include_top=False,
                               input_shape=(IMG_WIDTH, IMG_HEIGHT, 3))
     
        for layer in base_model.layers[:-2]: # 불러온 사전학습모형 레이어 중 마지막에서 세번째까지는 freeze
            layer.trainable = False # Top 층을 제외한 나머지 2개의 층은 새롭게 학습됨(원래 trainable의 설정값은 True이기 때문) 
     
        input1 = Input(shape=(IMG_WIDTH, IMG_HEIGHT, 3))
        custom_model = base_model(input1)
        custom_model = GlobalAveragePooling2D()(custom_model)
        custom_model = Dense(64, activation='relu')(custom_model)
        predictions = Dense(NUM_CLASSES, activation='softmax')(custom_model)
        return Model(inputs=input1, outputs=predictions)
    cs

    fine-tuning을 해야하는 레이어 개수는 ① task 유사도가 떨어질수록, ② 새로운 학습 데이터의 양이 많아질수록 늘어나게 됩니다. 즉 새로 학습할 파라미터 수가 많아지면 레이어 개수가 늘어납니다. 

     

     

      정리하자면 CNN을 사용하여 이미지를 분류하는 방법은 크게 1) 직접 CNN 층을 쌓거나, 2) 사전학습모형을 사용하는 방법이 있습니다. 보편적으로는 사전학습모형을 많이 쓰며, 이 또한 2가지 방법으로 나뉩니다. 위에서는 이러한 사항을 정리하였습니다 :D

    (1) 사전학습모델 그대로 쓰기 (2) 사전학습모형 변형: 전이학습(transfer learning)
    사전학습모형과 동일/유사한 task를 수행할 때   1) 기계학습 알고리즘과 결합: 앙상블은 거의 쓰이지 않음
      2) 모형 구조 변경: 기존 탑층을 빼고 다른 층을 추가
      3)  fine-tuning: 파라미터 변경, 문제 유사도/학습 데이터 양에 따라 파인튜닝할 레이어 개수 결정 

    'general ML, DL, NLP > 딥러닝' 카테고리의 다른 글

    12. LSTM(Long Short-Term Memory)  (0) 2022.06.28
    11. RNN(Recurrent Neural Network)  (0) 2022.06.28
    9. CNN 코드 - cats & dogs  (0) 2022.04.21
    8. CNN 코드 - MNIST, Cifar-10  (0) 2022.04.21
    7. CNN(Convolutional Neural Network)  (0) 2022.04.20

    댓글

Designed by Tistory.