Tensorflow 2.0 Tutorial ch8.2 - 전이 학습과 & Kaggle 대회

Page content

공지

  • 본 Tutorial은 교재 시작하세요 텐서플로 2.0 프로그래밍의 강사에게 국비교육 강의를 듣는 사람들에게 자료 제공을 목적으로 제작하였습니다.

  • 강사의 주관적인 판단으로 압축해서 자료를 정리하였기 때문에, 자세하게 공부를 하고 싶으신 분은 반드시 교재를 구매하실 것을 권해드립니다.

  • 본 교재 외에 강사가 추가한 내용에 대한 Reference를 확인하셔서, 추가적으로 학습하시는 것을 권유드립니다.

Tutorial

이전 강의가 궁금하신 분들은 아래에서 선택하여 추가 학습 하시기를 바랍니다.

I. 개요

전이 학습이란 미리 훈련된 모델을 다른 작업에 사용하기 위해 추가적인 학습을 시키는 것입니다. 이 때 훈련된 모델은 데이터에서 유의미한 특징(feature)을 뽑아내기 위한 특징 추출기(Feature Extractor)로 쓰이거나, 모델의 일부를 재학습시키기도 합니다.

이 부분에 대한 구체적인 이론 설명[8.2.1 모델의 일부를 재학습시키기]은 교재를 참고하시기를 바랍니다. 전이학습은 간단하게 말하면 훈련 시킬 레이어와 그렇지 않을 레이어를 구분해야 하고, 이때 freeze라는 용어를 사용합니다.

(1) 이슈

변경된 부분은 크게 2개입니다.

텐서플로의 버전을 2.1.0으로 다시 설치한 후 진행하는 코드를 첫 부분에 추가했습니다. (기존 코드를 최대한 살리기 위해 ImageDataGenerator를 사용해서 직접 학습을 시키려고 했습니다만 현재(2020.04.08) 텐서플로 2.2.0-rc2 버전에서는 ImageDataGenerator를 사용해서 model.fit()이나 model.fit_generator()를 학습시키려 할 경우 1 epoch 진행 후 무한루프에 걸리는 문제가 있기 때문에 텐서플로를 2.1.0으로 다시 설치했습니다.) 예제 8.16을 예제 8.24, 예제 8.25의 내용을 이용하여 수정했고 예제 8.19의 학습 코드도 일부 수정했습니다. 수정된 부분은 최대한 자세하게 주석을 달았습니다. 독자분들께 불편을 드려 죄송합니다.

[저자, 김환희]

II. 캐글 데이터와 연동

캐글은 2010년에 설립된 예측 모델 및 분석 대회를 위한 플랫폼으로 2017년에 구글에 인수되었습니다. 초기에는 전통적인 머신러닝 기법으로 풀 수 있는 테이블 데이터 위주의 문제들이 많았지만, 딥러닝의 발전으로 이미지, 음성, 자연어, 동영상 등 다양한 데이터에 대한 문제들이 올라옵니다.

한번 알면 어려운 부분은 아닙니다. 그러나 처음 접하는 분들에게는 늘 항상 어렵기 때문에, 잘 따라오시기를 바랍니다.

또한, 혹, 접근 이미지가 바뀌면, 댓글로 남겨주시기를 바랍니다. 수정하도록 하겠습니다.

(1) Kaggle Module 설치

다음 셀에서 짧은 명령어를 실행합니다.

!pip install kaggle
Requirement already satisfied: kaggle in /usr/local/lib/python3.6/dist-packages (1.5.6)
Requirement already satisfied: certifi in /usr/local/lib/python3.6/dist-packages (from kaggle) (2020.4.5.1)
Requirement already satisfied: six>=1.10 in /usr/local/lib/python3.6/dist-packages (from kaggle) (1.12.0)
Requirement already satisfied: urllib3<1.25,>=1.21.1 in /usr/local/lib/python3.6/dist-packages (from kaggle) (1.24.3)
Requirement already satisfied: tqdm in /usr/local/lib/python3.6/dist-packages (from kaggle) (4.38.0)
Requirement already satisfied: python-dateutil in /usr/local/lib/python3.6/dist-packages (from kaggle) (2.8.1)
Requirement already satisfied: requests in /usr/local/lib/python3.6/dist-packages (from kaggle) (2.21.0)
Requirement already satisfied: python-slugify in /usr/local/lib/python3.6/dist-packages (from kaggle) (4.0.0)
Requirement already satisfied: chardet<3.1.0,>=3.0.2 in /usr/local/lib/python3.6/dist-packages (from requests->kaggle) (3.0.4)
Requirement already satisfied: idna<2.9,>=2.5 in /usr/local/lib/python3.6/dist-packages (from requests->kaggle) (2.8)
Requirement already satisfied: text-unidecode>=1.3 in /usr/local/lib/python3.6/dist-packages (from python-slugify->kaggle) (1.3)

설치가 끝나면 API Token을 생성해야 합니다. 캐글이 없으신 분은 캐글 가입을 하시기를 바랍니다.

(2) API 연동

캐글 가입이 완료가 되면, My Account를 클릭하시기를 바랍니다. (그림 참조)

클릭 이후에 페이지 중간에 API 부분에서 Create New API Token을 생성합니다. 그러면 kaggle.json 파일이 다운로드 됩니다. 이 파일 에디터에 있는 usernamekey를 google colab의 한 셀에 붙여 넣기를 합니다. 그리고 아래 코드처럼 작성을 합니다.

import os
os.environ['KAGGLE_USERNAME']='user_id' # 독자의 ID
os.environ['KAGGLE_KEY']='user_api_token' # 독자의 캐글 API Token

위 코드를 작성합니다.

(3) Dog Breed Identification 대회

본 대회는 약 2년전에 개최된 대회입니다. 본 포스트에서는 캐글 대회에 제출해서 점수를 확인하는 것까지의 여정을 담았습니다.

III. 전이학습 모형 실습 예제

(1) tensorflow 설치 버전 확인

이슈에서 제기한 것처럼 반드시 아래 코드를 실행시키셔 2.3.0 버전으로 tensorflow를 설치하시기를 바랍니다.

!pip install tensorflow==2.3.0
Collecting tensorflow==2.1.0
[?25l  Downloading https://files.pythonhosted.org/packages/85/d4/c0cd1057b331bc38b65478302114194bd8e1b9c2bbc06e300935c0e93d90/tensorflow-2.1.0-cp36-cp36m-manylinux2010_x86_64.whl (421.8MB)
     |████████████████████████████████| 421.8MB 35kB/s 

. . . Successfully installed gast-0.2.2 tensorboard-2.1.1 tensorflow-2.1.0 tensorflow-estimator-2.1.0

(2) 데이터 다운로드

import os
os.environ['KAGGLE_USERNAME']='j2hoon85' # 독자의 ID
os.environ['KAGGLE_KEY']='10109b99cfbccf2eebbf5754a5b45cb7' # 독자의 캐글 API Token

# !kaggle competitions download -c dog-breed-identification

Notice 교재에서는 !kaggle competitions download -c dog-breed-identification 코드가 나와 있지만, 아래 코드로 수정하시기를 바랍니다.1

import tensorflow as tf
import numpy as np
import pandas as pd
import tensorflow_hub as hub
# 2020.02.01 현재 kaggle의 Stanford Dog Dataset 파일 구조가 변경되었습니다. 
# kaggle API를 사용하는 대신에 아래 링크에서 파일을 직접 받아오도록 수정되었습니다.
tf.keras.utils.get_file('/content/labels.csv', 'http://bit.ly/2GDxsYS')
tf.keras.utils.get_file('/content/sample_submission.csv', 'http://bit.ly/2GGnMNd')
tf.keras.utils.get_file('/content/train.zip', 'http://bit.ly/31nIyel')
tf.keras.utils.get_file('/content/test.zip', 'http://bit.ly/2GHEsnO')
Downloading data from http://bit.ly/2GDxsYS
483328/482063 [==============================] - 0s 1us/step
Downloading data from http://bit.ly/2GGnMNd
25206784/25200295 [==============================] - 1s 0us/step
Downloading data from http://bit.ly/31nIyel
361357312/361353329 [==============================] - 11s 0us/step
Downloading data from http://bit.ly/2GHEsnO
362848256/362841195 [==============================] - 11s 0us/step





'/content/test.zip'

다운로드가 완료되면 구글 코랩 좌측 상단에 있는 파일 메뉴를 클릭해서 정상적으로 다운로드를 받았는지 확인해봅니다.

  • labels.csv
  • sample_submission.csv
  • test.zip
  • train.zip

위 파일들이 있는지 확인한 뒤, train, test의 압축 데이터를 푼다.

!unzip train.zip
스트리밍 출력 내용이 길어서 마지막 5000줄이 삭제되었습니다.
  inflating: train/83bcff6b55ee179a7c123fa6103c377a.jpg  
  inflating: train/83be6d622ab74a5e7e08b53eb8fd566a.jpg  
  .
  .
  .
  inflating: train/ffd3f636f7f379c51ba3648a9ff8254f.jpg  
  inflating: train/fff43b07992508bc822f33d8ffd902ae.jpg  

(3) 라벨 데이터 확인

폴더가 있는 경우 먼저 폴더를 만들고 그 안에 압축 파일 안의 파일들을 복사하는 것을 확인할 수 있습니다. 이번에는 정답 라벨을 담고 있는 csv 파일을 확인합니다.

import pandas as pd
label_text = pd.read_csv('labels.csv')
print(label_text.head())
                                 id             breed
0  000bec180eb18c7604dcecc8fe0dba07       boston_bull
1  001513dfcb2ffafc82cccf4d8bbaba97             dingo
2  001cdf01b096e06d78e9e5112d419397          pekinese
3  00214f311d5d2247d5dfe4fe24b2303d          bluetick
4  0021f9ceb3235effd7fcde7f7538ed62  golden_retriever

이번에는 info() 함수를 활용해서 실행하도록 합니다.

label_text.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10222 entries, 0 to 10221
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   id      10222 non-null  object
 1   breed   10222 non-null  object
dtypes: object(2)
memory usage: 159.8+ KB

총 10,222장의 사진이 훈련 데이터에 포함되어 있음을 확인할 수 있습니다.

이번에는 nunique() 함수를 활용하여 해당 값의 겹치지 않는 숫자를 구한다.

label_text['breed'].nunique()
120

(4) 이미지 확인 시각화

실제로 어떤 사진들로 구성이 되어 있는지 이미지와 라벨을 함께 출력해서 확인합니다.

import PIL.Image as Image
import matplotlib.pyplot as plt

plt.figure(figsize=(12, 12))
for c in range(9):
  image_id = label_text.loc[c, 'id']
  plt.subplot(3, 3, c+1)
  plt.imshow(plt.imread('/content/train/' + image_id + '.jpg'))
  plt.title(str(c) + ', ' + label_text.loc[c, 'breed'])
  plt.axis('off')

plt.show()

png

각 견종은 다양한 각도에서 찍힌 것을 확인할 수 있습니다.

(5) 가중치 초기화 모델

  • 이 문제가 얼마나 어려운지 확인하기 위해 전이학습 전 먼저 MobileNet V2의 모든 레이어의 가중치를 초기화한 상태에서 학습시킵니다.

  • 어떤 뜻이냐면, 레이어의 구조는 같지만, ImageNet의 데이터로 미리 훈련된 이미지에 대한 지식은 전혀 없는 상태에서 학습시켜봅니다.

  • 텐서플로 허브를 이용하는 방법 외에 tf.keras에서도 MobileNet V2를 불러올 수 있습니다.

from tensorflow.keras.applications import MobileNetV2
mobilev2 = MobileNetV2()
Downloading data from https://github.com/JonathanCMitchell/mobilenet_v2_keras/releases/download/v1.1/mobilenet_v2_weights_tf_dim_ordering_tf_kernels_1.0_224.h5
14540800/14536120 [==============================] - 1s 0us/step
for layer in mobilev2.layers[:-1]:
    layer.trainable = True
    
for layer in mobilev2.layers[:-1]: 
    if 'kernel' in layer.__dict__:
        kernel_shape = np.array(layer.get_weights()).shape
        # weight를 평균이 0, 표준편차가 1인 random 변수로 초기화
        layer.set_weights(tf.random.normal(kernel_shape, 0, 1))
  • 위 코드는 MobileNetV2의 가중치를 모두 초기화한 것입니다.

  • 첫번째 for 문에서는 각 레이어의 훈련 가능 여부를 모두 True로 바꿉니다. 다만 마지막 레이어인 소프트맥스 Dense층은 사용하지 않을 것이기 때문에 Mobilev2.layers[:-1]명령으로 제외합니다.

  • 두번째 for 문에서는 각 레이어에 kernel이 있는지를 확인합니다.

  • 3장에서 뉴런은 가중치 w와 편향 b가 있다고 설명합니다. 이 때의 kernel은 바로 가중치 w에 해당하고, bias는 편향 b를 의미합니다. biasMobileNet V2에 존재하지 않기 때문에 kernel이 있는지만 검사해서 있을 경우 그 값을 모두 random변수로 초기화합니다.

(6) 훈련데이터 메모리 로드

초기화가 끝나면 이제 실제로 학습시킵니다. 마찬가지로 여기에 주의점이 있습니다. 용량문제로 인해 4.8일자로 소스코드가 변경되었습니다.

따라서, 아래 코드로 작성하실 것을 권유 드립니다.

# # 8.16 train 데이터를 메모리에 로드
# import cv2

# train_X = []
# for i in range(len(label_text)):
#     img = cv2.imread('/content/train/' + label_text['id'][i] + '.jpg')
#     img = cv2.resize(img, dsize=(224, 224))
#     img = img / 255.0
#     train_X.append(img)
# train_X = np.array(train_X)
# print(train_X.shape)
# print(train_X.size * train_X.itemsize, ' bytes')

# 2020.04.08 수정된 부분입니다.
# 예제 8.16은 고용량 RAM 모드를 지원하지 않는 무료 버전의 경우 OOM(Out Of Memory) 문제를 일으키기 때문에 주석처리합니다.
# 대신에 예제 8.24와 8.25를 이용해서 ImageDataGenerator로 학습을 시킵니다.

# 8.24 ImageDataGenerator가 처리할 수 있는 하위 디렉토리 구조로 데이터 복사
import os
import shutil

os.mkdir('/content/train_sub')

for i in range(len(label_text)):
    if os.path.exists('/content/train_sub/' + label_text.loc[i]['breed']) == False:
        os.mkdir('/content/train_sub/' + label_text.loc[i]['breed'])
    shutil.copy('/content/train/' + label_text.loc[i]['id'] + '.jpg', '/content/train_sub/' + label_text.loc[i]['breed'])

# 8.25 ImageDataGenerator를 이용한 train/validation 데이터 분리, Image Augmentation
from tensorflow.python.keras.preprocessing.image import ImageDataGenerator
from keras.applications.inception_resnet_v2 import preprocess_input

image_size = 224 # 이미지 사이즈가 299에서 224로 바뀌었습니다.
batch_size = 32

train_datagen = ImageDataGenerator(rescale=1./255., horizontal_flip=True, shear_range=0.2, zoom_range=0.2, width_shift_range=0.2, height_shift_range=0.2, validation_split=0.25)
valid_datagen = ImageDataGenerator(rescale=1./255., validation_split=0.25)

train_generator = train_datagen.flow_from_directory(directory="/content/train_sub/", subset="training", batch_size=batch_size, seed=42, shuffle=True, class_mode="categorical", target_size=(image_size, image_size))
valid_generator = valid_datagen.flow_from_directory(directory="/content/train_sub/", subset="validation", batch_size=1, seed=42, shuffle=True, class_mode="categorical", target_size=(image_size, image_size))
Using TensorFlow backend.


Found 7718 images belonging to 120 classes.
Found 2504 images belonging to 120 classes.

(6) 라벨 데이터 작성

이제 Y에 해당하는 라벨 데이터를 작성합니다.

unique_Y = label_text['breed'].unique().tolist()
train_Y = [unique_Y.index(breed) for breed in label_text['breed']]
train_Y = np.array(train_Y)

print(train_Y[:10])
print(train_Y[-10:])
[0 1 2 3 4 5 5 6 7 8]
[34 87 91 63 48  6 93 63 77 92]

여기서 라벨 데이터는 boston_bull, dingo와 같은 텍스트로 되어 있기 때문에 먼저 숫자로 바꾸는 과정이 필요합니다.

  • unique() 함수를 사용해 label_text['breed']를 구성하는 겹치지 않는 유일한 원소들을 구합니다. 그리고 tolist() 활용해 array에서 리스트로 변환합니다.

  • train_Y의 처음 값 10개와 마지막 값 10개를 확인합니다.

(7) 가중치 초기화 시킨 학습 모델 정의

이제 전이 학습 모델을 정의합니다. 이때 loss categorical_crossentropy로 바꿔주시기를 바랍니다. (교재와 다릅니다!)

x = mobilev2.layers[-2].output
predictions = tf.keras.layers.Dense(120, activation='softmax')(x)
model = tf.keras.Model(inputs=mobilev2.input, outputs=predictions)

# model.compile(optimizer='sgd', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy']) # 라벨이 원-핫 인코딩을 사용하기 때문에 sparse가 아닌 categorical_crossentropy를 사용합니다.
model.summary()
Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            [(None, 224, 224, 3) 0                                            
__________________________________________________________________________________________________
.
.
.
global_average_pooling2d (Globa (None, 1280)         0           out_relu[0][0]                   
__________________________________________________________________________________________________
dense (Dense)                   (None, 120)          153720      global_average_pooling2d[0][0]   
==================================================================================================
Total params: 2,411,704
Trainable params: 2,377,592
Non-trainable params: 34,112
__________________________________________________________________________________________________
  • 첫 번째 줄에서는 MobileNet V2에서 마지막 Dense레이어를 제외하기 위해 두 번째 레이어를 지정해서 그 레이어의 output을 x라는 변수에 저장합니다.

  • 그리고, 120개의 뉴런을 가진 Dense레이어를 새롭게 만듭니다.

predictions = tf.keras.layers.Dense(120, activation='softmax')(x)
  • 보통 모델을 정의할 때, 지금까지는 tf.keras.Sequential 모델만 사용했습니다. 그리고, 각 레이어는 모형의 안에 존재 했는데, 이번에는 조금 다릅니다.

  • 레이어를 함수처럼 사용하는 구문을 함수형(Functional) API라고 합니다.2 자세한 설명은 교재 또는 공식 문서를 참조하시기를 바랍니다.

(8) 정의된 모형 학습 (가중치 초기화)

model.fit을 활용해서 학습을 진행합니다.

# history = model.fit(train_X, train_Y, epochs=10, validation_split=0.25, batch_size=32)
steps_per_epoch = int(7718/32) # generator를 사용하기 때문에 1epoch 당 학습할 step수를 정합니다. batch_size인 32로 train_data의 크기를 나눠주면 됩니다.
history = model.fit(train_generator, validation_data=valid_generator, epochs=10, steps_per_epoch=steps_per_epoch) # model.fit()를 사용합니다.
WARNING:tensorflow:sample_weight modes were coerced from
  ...
    to  
  ['...']
WARNING:tensorflow:sample_weight modes were coerced from
  ...
    to  
  ['...']
Train for 241 steps, validate for 2504 steps
Epoch 1/10
241/241 [==============================] - 124s 516ms/step - loss: 4.8789 - accuracy: 0.0101 - val_loss: 9.5161 - val_accuracy: 0.0096
Epoch 2/10
241/241 [==============================] - 124s 514ms/step - loss: 4.8576 - accuracy: 0.0100 - val_loss: 8.6696 - val_accuracy: 0.0104
Epoch 3/10
241/241 [==============================] - 124s 516ms/step - loss: 4.8677 - accuracy: 0.0107 - val_loss: 9.1874 - val_accuracy: 0.0096
Epoch 4/10
241/241 [==============================] - 124s 513ms/step - loss: 4.8604 - accuracy: 0.0108 - val_loss: 8.2864 - val_accuracy: 0.0088
Epoch 5/10
241/241 [==============================] - 124s 513ms/step - loss: 4.8434 - accuracy: 0.0101 - val_loss: 8.3031 - val_accuracy: 0.0080
Epoch 6/10
241/241 [==============================] - 124s 514ms/step - loss: 4.8401 - accuracy: 0.0133 - val_loss: 7.9722 - val_accuracy: 0.0064
Epoch 7/10
241/241 [==============================] - 124s 514ms/step - loss: 4.8345 - accuracy: 0.0130 - val_loss: 7.2442 - val_accuracy: 0.0080
Epoch 8/10
241/241 [==============================] - 124s 516ms/step - loss: 4.8256 - accuracy: 0.0129 - val_loss: 6.9645 - val_accuracy: 0.0120
Epoch 9/10
241/241 [==============================] - 125s 518ms/step - loss: 4.8099 - accuracy: 0.0139 - val_loss: 7.0742 - val_accuracy: 0.0076
Epoch 10/10
241/241 [==============================] - 125s 517ms/step - loss: 4.8077 - accuracy: 0.0147 - val_loss: 6.5409 - val_accuracy: 0.0080

(9) 모형 결과 학습 시각화

모형 학습에 대한 결과를 시각화 합니다.

import matplotlib.pyplot as plt
plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], 'b-', label='loss')
plt.plot(history.history['val_loss'], 'r--', label='val_loss')
plt.xlabel('Epoch')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(history.history['accuracy'], 'g-', label='accuracy')
plt.plot(history.history['val_accuracy'], 'k--', label='val_accuracy')
plt.xlabel('Epoch')
plt.ylim(0, 0.1)
plt.legend()

plt.show()

png

모형의 학습 결과는 일단, 결론적으로 학습이 잘 되고 있다고 말하기는 어렵습니다.

다시 위 모형은 사전에 학습된 모형의 가중치를 제거한 모형을 학습 시킨 결과물입니다. 개념을 돕고자 이렇게 학습시키는 것이고, 실무에서는 이렇게 할 이유가 없습니다!

(10) 전이 학습 모형 정의

이번에는 전이 학습을 시도합니다. 일부 레이어의 가중치를 고정시킨 상태로 학습시킵니다.

from tensorflow.keras.applications import MobileNetV2
mobilev2 = MobileNetV2()

x = mobilev2.layers[-2].output
predictions = tf.keras.layers.Dense(120, activation='softmax')(x)
model = tf.keras.Model(inputs=mobilev2.input, outputs=predictions)

# 뒤에서 20개까지의 레이어는 훈련 가능, 나머지는 가중치 고정
for layer in model.layers[:-20]:
    layer.trainable = False
for layer in model.layers[-20:]:
    layer.trainable = True

# model.compile(optimizer='sgd', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy']) # 라벨이 원-핫 인코딩을 사용하기 때문에 sparse가 아닌 categorical_crossentropy를 사용합니다.
model.summary()
Model: "model_4"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_5 (InputLayer)            [(None, 224, 224, 3) 0                                            
__________________________________________________________________________________________________
.
.
__________________________________________________________________________________________________
global_average_pooling2d_4 (Glo (None, 1280)         0           out_relu[0][0]                   
__________________________________________________________________________________________________
dense_4 (Dense)                 (None, 120)          153720      global_average_pooling2d_4[0][0] 
==================================================================================================
Total params: 2,411,704
Trainable params: 1,204,280
Non-trainable params: 1,207,424
__________________________________________________________________________________________________
  • 함수형 API를 이용해 모델을 정의하는 과정은 그전과 비슷합니다.
  • model.summary()의 출력 결과에서 훈련 가능한 가중치의 수와 고정된 값의 가중치가 반반 정도로 비슷해진 것을 확인할 수 있다.
  • 뒤에서 20개까지의 레이어를 훈련 가능하게 하고, 나머지 레이어의 가중치는 고정시키는 작업을 합니다.

(11) 모형 학습 (전이 학습) 및 결과 시각화

이제 학습을 시키고, 시각화를 진행합니다.

  • 모형 학습입니다.
steps_per_epoch = int(7718/32) # generator를 사용하기 때문에 1epoch 당 학습할 step수를 정합니다. batch_size인 32로 train_data의 크기를 나눠주면 됩니다.
history = model.fit_generator(train_generator, validation_data=valid_generator, epochs=10, steps_per_epoch=steps_per_epoch) # model.fit_generator()를 사용합니다.
WARNING:tensorflow:sample_weight modes were coerced from
  ...
    to  
  ['...']
WARNING:tensorflow:sample_weight modes were coerced from
  ...
    to  
  ['...']
Train for 241 steps, validate for 2504 steps
Epoch 1/10
241/241 [==============================] - 170s 703ms/step - loss: 3.4284 - accuracy: 0.2918 - val_loss: 1.9956 - val_accuracy: 0.4772
Epoch 2/10
241/241 [==============================] - 167s 695ms/step - loss: 1.6856 - accuracy: 0.6227 - val_loss: 1.4928 - val_accuracy: 0.5843
Epoch 3/10
241/241 [==============================] - 166s 690ms/step - loss: 1.2359 - accuracy: 0.7022 - val_loss: 1.3797 - val_accuracy: 0.6082
Epoch 4/10
241/241 [==============================] - 166s 688ms/step - loss: 1.0110 - accuracy: 0.7482 - val_loss: 1.2314 - val_accuracy: 0.6522
Epoch 5/10
241/241 [==============================] - 166s 690ms/step - loss: 0.8846 - accuracy: 0.7791 - val_loss: 1.1955 - val_accuracy: 0.6526
Epoch 6/10
241/241 [==============================] - 166s 689ms/step - loss: 0.7828 - accuracy: 0.8078 - val_loss: 1.1607 - val_accuracy: 0.6697
Epoch 7/10
241/241 [==============================] - 166s 690ms/step - loss: 0.7045 - accuracy: 0.8246 - val_loss: 1.1379 - val_accuracy: 0.6733
Epoch 8/10
241/241 [==============================] - 166s 690ms/step - loss: 0.6532 - accuracy: 0.8380 - val_loss: 1.1241 - val_accuracy: 0.6865
Epoch 9/10
241/241 [==============================] - 166s 689ms/step - loss: 0.5957 - accuracy: 0.8557 - val_loss: 1.1469 - val_accuracy: 0.6737
Epoch 10/10
241/241 [==============================] - 166s 690ms/step - loss: 0.5488 - accuracy: 0.8631 - val_loss: 1.1132 - val_accuracy: 0.6809
  • 이번엔 시각화입니다.
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], 'b-', label='loss')
plt.plot(history.history['val_loss'], 'r--', label='val_loss')
plt.xlabel('Epoch')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(history.history['accuracy'], 'g-', label='accuracy')
plt.plot(history.history['val_accuracy'], 'k--', label='val_accuracy')
plt.xlabel('Epoch')
plt.ylim(0.3, 1)
plt.legend()

plt.show()

png

같은 네트워크 구조를 사용했지만, val_accuray1% 정도에 머물던 가중치 초기화 학습 모델에 비해 전혀 다른 결과(86.3%)가 나온 것을 확인할 수 있습니다. val_loss는 감소하는 추세이고, val_accuracy는 증가 추세여서 학습을 추가적으로 해도 네트워크의 성능이 보다 향상 될 것 같습니다. (다만, 시간은 많이 소요 됩니다!)

III. 특징 추출기

미리 훈련된 모델에서 데이터의 특징만 추출하고, 그 특징을 작은 네트워크에 통과시켜서 정답을 예측하는 방법도 있습니다. 자세한 설명은 교재 (p. 270)을 참조하시기를 바랍니다.

(1) 특징 추출기 불러오기

텐서플로 허브에서 Inception V3를 불러옵니다. Inception은 2014년에 구글이 ImageNet 대회를 위해 GoogleNet이라는 이름으로 발표한 컨볼루션 신경망입니다. V3는 세 번째로 개선된 버전입니다.

import tensorflow_hub as hub

inception_url = 'https://tfhub.dev/google/tf2-preview/inception_v3/feature_vector/4'
feature_model = tf.keras.Sequential([
    hub.KerasLayer(inception_url, output_shape=(2048,), trainable=False)
])
feature_model.build([None, 299, 299, 3])
feature_model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
keras_layer (KerasLayer)     multiple                  21802784  
=================================================================
Total params: 21,802,784
Trainable params: 0
Non-trainable params: 21,802,784
_________________________________________________________________
  • feature_model.build([None, 299, 299, 3]) 함수는 입력 데이터의 차원을 정의해서 넣습니다. 첫번째 차원은 배치 차원이기 때문에 입력이 몇개가 들어와도 상관없습니다.

(2) ImageDataGenerator 파일 복사

ImageDataGenerator는 라벨이 있는 데이터를 처리할 때 각 라벨의 이름을 하위 디렉터리로 가지고 있는 디렉토리를 받아서 그 데이터를 처리합니다. 반면에 캐글에서 내려받은 데이터들은 하위 디렉터리의 구분 없이 train폴더에 모든 이미지 파일이 저장되어 있습니다. 따라서 ImageDataGenerator가 처리할 수 있는 방식으로 데이터를 복사합니다.

import os
import shutil

os.mkdir('/content/train_sub')

for i in range(len(label_text)):
    if os.path.exists('/content/train_sub/' + label_text.loc[i]['breed']) == False:
        os.mkdir('/content/train_sub/' + label_text.loc[i]['breed'])
    shutil.copy('/content/train/' + label_text.loc[i]['id'] + '.jpg', '/content/train_sub/' + label_text.loc[i]['breed'])

(3) 훈련 및 검증 데이터 분리, 그리고 이미지 보강

먼저 훈련 및 검증 데이터로 분리하는 소스코드를 작성합니다. _datagen 함수 안에 있는 인수에 대한 설명은 교재 274페이지를 참고합나디.

from tensorflow.python.keras.preprocessing.image import ImageDataGenerator
from keras.applications.inception_resnet_v2 import preprocess_input

image_size = 299
batch_size = 32

train_datagen = ImageDataGenerator(rescale=1./255., horizontal_flip=True, shear_range=0.2, zoom_range=0.2, width_shift_range=0.2, height_shift_range=0.2, validation_split=0.25)
valid_datagen = ImageDataGenerator(rescale=1./255., validation_split=0.25)

train_generator = train_datagen.flow_from_directory(directory="/content/train_sub/", subset="training", batch_size=batch_size, seed=42, shuffle=True, class_mode="categorical", target_size=(image_size, image_size))
valid_generator = valid_datagen.flow_from_directory(directory="/content/train_sub/", subset="validation", batch_size=1, seed=42, shuffle=True, class_mode="categorical", target_size=(image_size, image_size))
Found 7718 images belonging to 120 classes.
Found 2504 images belonging to 120 classes.

이번에는 훈련 데이터를 특징 벡터로 변환합니다.

batch_step = (7718 * 3) // batch_size
train_features = []
train_Y = []
for idx in range(batch_step):
    if idx % 100 == 0:
        print(idx)
    x, y = train_generator.next()
    train_Y.extend(y)
    
    feature = feature_model.predict(x)
    train_features.extend(feature)

train_features = np.array(train_features)
train_Y = np.array(train_Y)
print(train_features.shape)
print(train_Y.shape)
0
100
200
300
400
500
600
700
(23084, 2048)
(23084, 120)
  • 첫번째 코드가 조금 중요한데, batch_step은 부족한 RAM에 비해 훈련시 필요한 메모리 부족을 해소하기 위해 단계별로 진행핟나는 뜻입니다.

  • batch_size로 나눠서 training 부분 집합을 3번 정도 반복해서 특징 벡터를 뽑아냅니다.

  • next() 함수를 사용하면 다음에 올 값을 반환받을 수 있습니다. 훈련 데이터는 이미지의 분류에 해당하는 y값이 있기 때문에 식의 좌변에서 x, y를 함께 받습ㄴ다. y값은 바로 train_Y에 저장해서 추후 활용하게 됩니다.

  • x값은 이미지 데이터에 해당하는 부분입니다. 이미 학습이 완료된 특징 추출기를 사용하기 때문에 predict()로 특징 벡터를 추출합니다. 특징 벡터는 feature라는 변수에 저장한 뒤 추후에 사용할 수 있도록 train_features에 저장합니다.

  • 최종 출력되는 Shape는 train_feature가 (23084, 2048)이고, train_Y가 (23084, 120)입니다.

  • 즉, 특징 벡터는 2,048차원의 벡터임을 확인할 수 있습니다.

마찬가지로 검증 데이터에도 적용하도록 합니다.

valid_features = []
valid_Y = []

for idx in range(valid_generator.n):
    if idx % 100 == 0:
        print(idx)
    x, y = valid_generator.next()
    valid_Y.extend(y)
    
    feature = feature_model.predict(x)
    valid_features.extend(feature)

valid_features = np.array(valid_features)
valid_Y = np.array(valid_Y)
print(valid_features.shape)
print(valid_Y.shape)
0
100
200
300
400
500
600
700
800
900
1000
1100
1200
1300
1400
1500
1600
1700
1800
1900
2000
2100
2200
2300
2400
2500
(2504, 2048)
(2504, 120)

(4) 작은 시퀀셜 모델 정의

이제 분류 모형을 위한 Sequential 모형을 정의합니다.

model = tf.keras.Sequential([
    tf.keras.layers.Dense(256, activation='relu', input_shape=(2048,)),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(120, activation='softmax')
])

model.compile(tf.optimizers.RMSprop(0.0001), loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_5 (Dense)              (None, 256)               524544    
_________________________________________________________________
dropout (Dropout)            (None, 256)               0         
_________________________________________________________________
dense_6 (Dense)              (None, 120)               30840     
=================================================================
Total params: 555,384
Trainable params: 555,384
Non-trainable params: 0
_________________________________________________________________
  • input_shape=(2048,)을 지정해서 특징 벡터를 받을 수 있도록 합니다.
  • Dense 레이어는 분류를 위해 softmax 활성화 함수를 지정하고 뉴런의 수는 견종의 수와 같은 120으로 지정합니다.
  • categorical_crossentropy가 사용된 이유는 train_Y의 마지막 차원이 1이 아닌 원-핫 벡터인 120이기 때문입니다.

정답의 인덱스만 기록된 희소 행렬(sparse matrix)을 Y로 사용할 때는 sparse_categorical_crossentropy를 사용하고, one-hot 벡터를 사용할 때는 sparse가 없는 버전을 사용합니다.

(5) 모형 학습 및 시각화

모형을 학습시키고 시각화로 확인합니다.

history = model.fit(train_features, train_Y, validation_data=(valid_features, valid_Y), epochs=10, batch_size=32)

import matplotlib.pyplot as plt
plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], 'b-', label='loss')
plt.plot(history.history['val_loss'], 'r--', label='val_loss')
plt.xlabel('Epoch')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(history.history['accuracy'], 'g-', label='accuracy')
plt.plot(history.history['val_accuracy'], 'k--', label='val_accuracy')
plt.xlabel('Epoch')
plt.ylim(0.8, 1)
plt.legend()

plt.show()
Train on 23084 samples, validate on 2504 samples
Epoch 1/10
23084/23084 [==============================] - 3s 123us/sample - loss: 2.8557 - accuracy: 0.4517 - val_loss: 0.9163 - val_accuracy: 0.8550
Epoch 2/10
23084/23084 [==============================] - 2s 105us/sample - loss: 0.9270 - accuracy: 0.7808 - val_loss: 0.4353 - val_accuracy: 0.8890
Epoch 3/10
23084/23084 [==============================] - 2s 105us/sample - loss: 0.6147 - accuracy: 0.8295 - val_loss: 0.3560 - val_accuracy: 0.8946
Epoch 4/10
23084/23084 [==============================] - 2s 105us/sample - loss: 0.5079 - accuracy: 0.8518 - val_loss: 0.3306 - val_accuracy: 0.8950
Epoch 5/10
23084/23084 [==============================] - 2s 104us/sample - loss: 0.4466 - accuracy: 0.8628 - val_loss: 0.3208 - val_accuracy: 0.8978
Epoch 6/10
23084/23084 [==============================] - 2s 105us/sample - loss: 0.4064 - accuracy: 0.8734 - val_loss: 0.3197 - val_accuracy: 0.8962
Epoch 7/10
23084/23084 [==============================] - 2s 105us/sample - loss: 0.3707 - accuracy: 0.8845 - val_loss: 0.3099 - val_accuracy: 0.9014
Epoch 8/10
23084/23084 [==============================] - 2s 105us/sample - loss: 0.3445 - accuracy: 0.8907 - val_loss: 0.3067 - val_accuracy: 0.9006
Epoch 9/10
23084/23084 [==============================] - 2s 106us/sample - loss: 0.3172 - accuracy: 0.9001 - val_loss: 0.3080 - val_accuracy: 0.8974
Epoch 10/10
23084/23084 [==============================] - 2s 106us/sample - loss: 0.2999 - accuracy: 0.9042 - val_loss: 0.3048 - val_accuracy: 0.8994

png

각 에포크당 3초가 걸릴 정도로 학습 속도가 매우 빨라진 것을 확인했습니다.

(6) 학습 모형 테스트

모델이 예측을 얼마나 잘하는지 알아보기 위해 검증 데이터의 이미지에 대한 분류를 시각화합니다.

그 전에 먼저 라벨의 이름을 따로 저장하는데, ImageDataGenerator에서 라벨을 인덱스로 저장할 때 알파벳 순으로 정렬된 순서로 저장하기 때문에 여기서도 마찬가지로 저장합니다.

그리고 알파벳순 1~5까지를 순서대로 출력해서 확인합니다.

unique_sorted_Y = sorted(unique_Y)
print(unique_sorted_Y[0:5])
['affenpinscher', 'afghan_hound', 'african_hunting_dog', 'airedale', 'american_staffordshire_terrier']

이제 검증 데이터를 시각화합니다.

import cv2
import random
plt.figure(figsize=(16,16))
  
for c in range(3):
    image_path = random.choice(valid_generator.filepaths)
    
    # 이미지 표시
    plt.subplot(3,2,c*2+1)
    plt.imshow(plt.imread(image_path))
    real_y = image_path.split('/')[3]
    plt.title(real_y)
    plt.axis('off')
    idx = unique_sorted_Y.index(real_y)
    
    # 예측값 표시
    plt.subplot(3,2,c*2+2)
    img = cv2.imread(image_path)
    img = cv2.resize(img, dsize=(299, 299))
    img = img / 255.0
    img = np.expand_dims(img, axis=0)
    
    # Inception V3를 이용한 특징 벡터 추출
    feature_vector = feature_model.predict(img)
    
    # Sequential 모델을 이용한 예측
    prediction = model.predict(feature_vector)[0]
    
    # 가장 높은 확률의 예측값 5개를 뽑음
    top_5_predict = prediction.argsort()[::-1][:5]
    labels = [unique_sorted_Y[index] for index in top_5_predict]
    color = ['gray'] * 5
    if idx in top_5_predict:
        color[top_5_predict.tolist().index(idx)] = 'green'
    color = color[::-1]
    plt.barh(range(5), prediction[top_5_predict][::-1] * 100, color=color)
    plt.yticks(range(5), labels[::-1])

png

Top-1으로 3개를 다 맞춘것으로 확인됩니다. 그러나 계속 실행하면 중간중간 잘 맞지 않는 부분도 있으나, 상위 5개에는 꼭 있음을 확인할 수 있습니다.

IV. Submission

예측 결과를 캐글에 올리도록 하는 소스코드를 구현합니다. 이미지와 관련된 캐글 대회에 나가더라도, 본 소스코드는 잘 숙지하셔서 응용하시기를 바랍니다.

(1) 테스트 데이터 압축 풀기

!unzip test.zip
스트리밍 출력 내용이 길어서 마지막 5000줄이 삭제되었습니다.
  inflating: test/82e41a906dbd9ec362a3d49cf6bbe645.jpg  
  .
  .
  .
  inflating: test/fffbff22c1f51e3dc80c4bf04089545b.jpg  

(2) submission 파일 확인

submission 파일을 확인합니다.

import pandas as pd
submission = pd.read_csv('sample_submission.csv')
print(submission.head())
print()
print(submission.info())
                                 id  ...  yorkshire_terrier
0  000621fb3cbb32d8935728e48679680e  ...           0.008333
1  00102ee9d8eb90812350685311fe5890  ...           0.008333
2  0012a730dfa437f5f3613fb75efcd4ce  ...           0.008333
3  001510bc8570bbeee98c8d80c8a95ec1  ...           0.008333
4  001a5f3114548acdefa3d4da05474c2e  ...           0.008333

[5 rows x 121 columns]

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10357 entries, 0 to 10356
Columns: 121 entries, id to yorkshire_terrier
dtypes: float64(120), object(1)
memory usage: 9.6+ MB
None

데이터 프레임의 첫열은 label.csv와 같은 id입니다. 나머지 열은 120개의 견종의 이름이 있고, 각 id에 대한 각 견종의 예측 값은 랜덤한 선택을 했을 때의 값 0.008333으로 채워져 있습니다. 이 대로 캐글에 제출해도 Multiclass Logloss로 산정되며 4.78749의 점수를 얻게 됩니다.

(3) 테스트 데이터 정제

테스트 데이터도 훈련 데이터와 마찬가지로 ImageDataGenerator를 사용하도록 합니다. 이 때 ImageDataGeneratorflow_from_directory() 함수로 이미지를 읽어 들이기 위해 하위 디렉토리가 꼭 필요합니다.

현재 테스트 데이터는 각 사진이 어떤 범주에 속하는지 알 수 없기 때문에 unknown이라는 폴더를 만들고 모든 데이터를 이곳에 복사하도록 합니다.

# 8.34 ImageDataGenerator가 처리할 수 있는 하위 디렉토리 구조로 데이터 복사
import os
import shutil

os.mkdir('/content/test_sub/')
os.mkdir('/content/test_sub/unknown/')

for i in range(len(submission)):
    shutil.copy('/content/test/' + submission.loc[i]['id'] + '.jpg', '/content/test_sub/unknown/')

test_sub 폴더 하위에 unknown이라는 폴더가 생기고 모든 파일이 이 안에 들어있음을 확인합니다.

이제 테스트 데이터를 불러오는 ImageDataGenerator를 정의합니다. 이미지 보강 등은 진행할 필요가 없기 때문에 보다 간소화됩니다.

from tensorflow.python.keras.preprocessing.image import ImageDataGenerator

test_datagen=ImageDataGenerator(rescale=1./255.)
test_generator=test_datagen.flow_from_directory(directory="/content/test_sub/",batch_size=1,seed=42,shuffle=False,target_size=(299, 299))
Found 10357 images belonging to 1 classes.

위와 같이 정상적으로 ImageDataGenerator가 만들어지면 이를 이용해서 벡터를 추출합니다.

(4) 테스트 데이터의 벡터 변환

test_features = []

for idx in range(test_generator.n):
    if idx % 100 == 0:
        print(idx)
        
    x, _ = test_generator.next()
    feature = feature_model.predict(x)
    test_features.extend(feature)

test_features = np.array(test_features)
print(test_features.shape)
0
100
200
.
.
.
10100
10200
10300
(10357, 2048)

각 이미지 데이터는 120의 길이를 가진 벡터로 변환이 되었습니다. 이렇게 생성된 벡터로 데스트 데이터의 정답을 예측합니다.

test_Y = model.predict(test_features, verbose=1)
10357/10357 [==============================] - 0s 41us/sample

model.predict() 함수는 verbose 인수의 값이 0으로 설정되어 있기 때문에 진행 과정을 보기 위해서는 verbose=1로 지정해야 합니다.

(5) 테스트 데이터 분류 라벨 확인

import random
import cv2
plt.figure(figsize=(16,16))
  
for c in range(3):
    image_path = random.choice(test_generator.filepaths)
    
    # 이미지 표시
    plt.subplot(3,2,c*2+1)
    plt.imshow(plt.imread(image_path))
    real_y = image_path.split('/')[3]
    plt.title(real_y)
    plt.axis('off')
    
    # 예측값 표시
    plt.subplot(3,2,c*2+2)
    img = cv2.imread(image_path)
    img = cv2.resize(img, dsize=(299, 299))
    img = img / 255.0
    img = np.expand_dims(img, axis=0)
    
    # Inception V3를 이용한 특징 벡터 추출
    feature_vector = feature_model.predict(img)
    
    # Sequential 모델을 이용한 예측
    prediction = model.predict(feature_vector)[0]
    
    # 가장 높은 확률의 예측값 5개를 뽑음
    top_5_predict = prediction.argsort()[::-1][:5]
    labels = [unique_sorted_Y[index] for index in top_5_predict]
    color = ['gray'] * 5
    plt.barh(range(5), prediction[top_5_predict][::-1] * 100, color=color)
    plt.yticks(range(5), labels[::-1])

png

저난적으로 모형은 꽤 학신을 가지고 테스트 데이터에 대해 답을 예측하고 있습니다.

(6) Submission 준비 및 파일 내보내기

submission의 준비작업은 아래와 같이 코드를 작성하고 실제로 예측값이 잘 저장됐는지 확인하기 위해 데이터의 일부를 출력합니다.

for i in range(len(test_Y)):
  print("letter I 're on time %d" % (i))
  for j in range(len(test_Y[i])):
    print("letter J're on time %d" % (j))
    breed_column = unique_sorted_Y[j]
    submission.loc[i, breed_column] = test_Y[i, j]

print(submission.iloc[:5, :5])
스트리밍 출력 내용이 길어서 마지막 5000줄이 삭제되었습니다.
We're on time 0
We're on time 1
.
.
.
We're on time 10317
We're on time 0
We're on time 1
.
.
                                 id  ...      airedale
0  000621fb3cbb32d8935728e48679680e  ...  6.192151e-07
1  00102ee9d8eb90812350685311fe5890  ...  2.064632e-07
2  0012a730dfa437f5f3613fb75efcd4ce  ...  3.297540e-06
3  001510bc8570bbeee98c8d80c8a95ec1  ...  1.829849e-07
4  001a5f3114548acdefa3d4da05474c2e  ...  6.606462e-07

[5 rows x 5 columns]

Submission 파일을 만드는데 생각보다 많은 시간이 소요됩니다. 인내심을 가지고 조금 기다리셔야 합니다. (약 28MB 용량의 파일)

이제 csv파일로 저장합니다. 버전 관리를 할 수 있는 이름으로 파일을 저장 합니다.

submission.to_csv('dogbreed_submission_inceptionV3_epoch10_299_20200429.csv', index=False)

위 코드를 실행하면 왼쪽 파일, 가상 환경에 저장되기 때문에 꼭 확인해서 다운로드 받기를 바랍니다. Colab에서는 연결이 끊기면 파일이 사라집니다.

(7) 파일 업로드 및 점수 확인

submission 파일 싸이트로 돌아가서 다운로드 받은 파일을 업로드 하고 결과를 확인합니다.

Score가 아래 그림에서 확인하는 것처럼 0.32208인 것을 확인할 수 있습니다.

참고로 위 대회는 2년전에 이미 마감되었기 때문에, 랭킹에 반영되지 않습니다. 그러나, 아래 그림을 통해서 현재 만든 모형의 결과가 어느정도 순위인지 확인은 할 수 있습니다.

대략적으로 510위 정도에 해당하는 모형이라고 할 수 있습니다. 이 순위는 상위 약 40%에 해당한다고 할 수 있습니다.

사실 이제 시작입니다. 모형을 반복해서 만들어서 성능을 끌어 올리는 것은 모든 머신러닝/딥러닝 개발자의 숙명이자 과제입니다.

III. 연습 파일

VI. Reference

김환희. (2020). 시작하세요! 텐서플로 2.0 프로그래밍: 기초 이론부터 실전 예제까지 한번에 끝내는 머신러닝, 딥러닝 핵심 가이드. 서울: 위키북스.

Karpathy, A. (2015). The Unreasonable Effectiveness of Recurrent Neural Networks. Retrieved April 26, 2020, from http://karpathy.github.io/2015/05/21/rnn-effectiveness/


  1. 2020년 4월 8일에 수정본이 있습니다. ↩︎

  2. 텐서플로 홈페이지 함수형 API에 관한 문서를 살펴보시기를 바랍니다. ↩︎