Transfer Learning을 해 봅시다. 전이학습이라고도 하는데, pretrained 모델을 가져다가 추가로 학습하는 걸 전이학습이라고 합니다. 이 실습은 구글에서 제공하는 개/고양이 분류 실습을 기초로 하였는데 말이죠, 고마운 구글입니다.
LINK LOCATION : Transfer learning and fine-tuning | TensorFlow Core
https://www.tensorflow.org/tutorials/images/transfer_learning?hl=en
DATA LOCATION : https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip
Binary 분류를 처음 하게 되면 개와 고양이를 구분하는 모형을 만드는 것이 마치 국룰인 것 처럼 처음에 배우게 되는데, 학습하는데 시간도 많이 걸리고, 생각만큼 성능이 좋지도 않습니다. 대게의 경우 실망할 정도의 성능이 나옵니다. 그런 결과는 여러가지 이유가 있는데, 그것은 차치하고 보통의 경우에는 데이터가 풍부한 경우가 많지 않습니다. 이럴 때 이미 학습된 모형을 가져다가 추가로 학습하는 것을 Fine tuning 또는 Transfer learning이라고 합하는데, 적당한 크기의 고양이와 개를 구분하는 전이학습을 해보려고 합니다.
개와 고양이를 작은 데이터로 분류! 분류!
기본적으로 Transfer Learning의 구조는 이렇습니다.
대략 입력을 넣고 Pretrained 모형을 붙이고 그 뒤에 Classifier를 붙입니다. 이야기를 진행하면서 이야기 하겠지만 Classifier 쪽을 Head(Top)이라고 부릅니다. 판단은 머리가 하니까 그런 모양인데 아무래도 우리는 위에서 부터 모형을 그려내려오니까 적응이 잘 안되는 Term입니다.
일단 중간에 Convolutional Layers를 Mobile Net V2 (Pretrained Model)로 사용할 것이고요, Output쪽의 FC는 그냥 1개만 사용할 것입니다.
import os
import tensorflow as tf
2024-02-06 14:47:10.470434: I tensorflow/stream_executor/platform/default/dso_loader.cc:48] Successfully opened dynamic library libcudart.so.10.1
[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')] 1 Physical GPUs, 1 Logical GPUs ['/device:CPU:0', '/device:XLA_CPU:0', '/device:XLA_GPU:0', '/device:GPU:0'] 2.3.1
일단 구글에서 제공하는 개/고양이 데이터를 다운 받아서 이걸 해 보려고 합니다. 그러니까 당연히 데이터를 다운 받습니다. 고마워요!를 외치고 받아요.
!wget https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip
!unzip cats_and_dogs_filtered.zip
이걸 풀고나면 validation과 train folder로 나뉩니다. 얏호. 자, 이것들에 대한 path와 train_data와 validation data를 정리해 봅시다. 그리고 test 데이터가 따로 제공되지 않으니까, validation 데이터에서 test 데이터도 조금 빼 내 주고요.
import matplotlib.pyplot as plt
import numpy as np
import os
from tensorflow.keras.preprocessing import image_dataset_from_directory
# 다운 받은 데이터 path를 만들어 두고요,
PATH = os.path.join(os.getcwd(), 'cats_and_dogs_filtered')
# train과 validation 폴더 이름도 정해주고요.
train_dir = os.path.join(PATH, 'train')
validation_dir = os.path.join(PATH, 'validation')
# image_dataset_from_directory를 이용하면 batch size와 image size에 맞춰서 자동으로 나눠줍니다. 너무 멋쟁이! 단, tf2.3이상이어야 되는 것 같아요.
# Batch size는 32, Image Size는 160x160입니다.
BATCH_SIZE = 32
IMG_SIZE = (160, 160)
train_dataset = image_dataset_from_directory(train_dir,
shuffle=True,
batch_size=BATCH_SIZE,
image_size=IMG_SIZE)
validation_dataset = image_dataset_from_directory(validation_dir,
shuffle=True,
batch_size=BATCH_SIZE,
image_size=IMG_SIZE)
# Test dataset을 validation dataset에서 가져와봐요~ 20%정도? (그래서 //5로 연산합니다)
size_batches = tf.data.experimental.cardinality(validation_dataset)
test_dataset = validation_dataset.take(size_batches // 5)
validation_dataset = validation_dataset.skip(size_batches // 5)
Found 2000 files belonging to 2 classes. Found 1000 files belonging to 2 classes.
어떤 데이터들이 있는지 한번 볼까요? 눈으로 보면 조금 더 확실하게 알 수 있으니까요. 짜잔.
plt.figure(figsize=(10, 10))
class_names = train_dataset.class_names
for images, labels in train_dataset.take(1): # 이렇게 하면 single batch를 가져올 수 있습니다. 헤헿
for i in range(30): # 32개이지만, 그냥 30개만 봐요.
ax = plt.subplot(6, 5, i + 1)
plt.imshow(images[i].numpy().astype("uint8"))
plt.title(class_names[labels[i]])
plt.axis("off")
대략~ 고양이와 개 밖에 없군요. 개 고양이 같은 엉뚱한 그림도 좀 있었으면 더 재미 있었을텐데 싶긴한데, 레이블도 없는 쓰레기를 입력으로 넣을 순 없으니까요. 그런 걸 실제 예측해 보는 건 재미있겠군요.
어쨌든, 나중에 Diffusion 모형 같은거 할 때 또 보겠지만, IO를 원활하게 하기 위해 데이터들을 buffer에 넣어두도록 하겠습니다. 이건 꽤나 쓸만한 건데, AUTOTUNE이 tensorflow 버전에 따라 tf.data.AUTOTUNE이거나, tf.data.experimental.AUTOTUNE이거나 하니까 이건 참고로 알아두면 좋겠습니다.
AUTOTUNE = tf.data.experimental.AUTOTUNE
train_dataset = train_dataset.prefetch(buffer_size=AUTOTUNE)
validation_dataset = validation_dataset.prefetch(buffer_size=AUTOTUNE)
test_dataset = test_dataset.prefetch(buffer_size=AUTOTUNE)
그리고, 학습할 때만 쓰긴 할 건데, augmentation도 Keras에게 시켜 봐요. 편리한 세상이라고 중얼거리면서 layer추가!
layer_augmentation = tf.keras.Sequential([
tf.keras.layers.experimental.preprocessing.RandomFlip('horizontal'),
tf.keras.layers.experimental.preprocessing.RandomRotation(0.2),
])
자, 이제 준비가 슬슬 되어가는군요. 전이학습을 위해서 이미 학습이 된 mobilenet_v2를 이용할거에요! 이건 이미 keras에 준비되어 있으니까, 가져와욥. 정말 구글은 대다나다.
자 이제부터 전체 모형을 이어 붙이기 위한 준비물들을 준비하는 겁니다. 착착착착 준비해요.
layer_monet_input = tf.keras.applications.mobilenet_v2.preprocess_input
아, 그리고 moblenet_v2는 입력을 -1~+1사이에 있는 값들을 받는데, 지금 keras에서 loading한 개/고양이 데이터는 0~255거든요. 그러니까 이걸 0~1로 바꿔주는 layer도 하나 만들어 두고요! 전처리로 해도 되겠지만, layer로 처리하면 모형에서 다 처리하니까 나중에 편리하겠습니다.
layer_rescale = tf.keras.layers.experimental.preprocessing.Rescaling(1./127.5, offset= -1)
전이학습에서 가장 중요한 것은 include_top=False로 하여 마지막 FC를 제외하고 사용하는 것이 뽀인트입니다. 뽀인트 살려서 model_base를 만들어봐요. 참고로 입력은 160x160x3의 이미지가 됩니다. 160x160 pixel, 3 channels라는 의미입니다.
자, model_base로 pretrained model을 불러서 만들어 봅시다.
IMG_SHAPE = (160 , 160 , 3)
model_base = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE,
include_top=False,
weights='imagenet')
기존에 학습된 것들은 망가트리지 않을 거라서 model_base는 학습하지 않는 것으로 세팅! 하겠습니다. 이 부분도 전이학습에서 매우 중요한 부분입니다. 잊지 마세요.
model_base.trainable = False
어째 어째 여기까지 왔습니다. mobilenet의 출력을 확인해 보자면,
image_batch, label_batch = next(iter(train_dataset))
output_batch = model_base(image_batch)
print(output_batch.shape)
(32, 5, 5, 1280)
자, 보면 5x5x1280의 출력을 내고, 32개의 batch 크기가 됩니다. GlobalAverage Pooling이 꼭 필요한가 싶은데 이걸 하면 어느 정도 공간적인 정보를 풀링한 feature vector을 만드는 레이어가 됩니다.
layer_global_average = tf.keras.layers.GlobalAveragePooling2D()
output_batch_average = layer_global_average(output_batch)
print(output_batch_average.shape)
(32, 1280)
마지막쯤에 필요한 Classifier를 위한 Fully connected layer를 준비합시다. Binary Classifier니까 마지막에 FC로 Dense(1)로 준비해 주세요. 이것만 하면 모형을 위한 준비물은 전부 준비한 것 같습니다.
layer_out_fully_connected = tf.keras.layers.Dense(1)
prediction_on_batch_out = layer_out_fully_connected(output_batch_average)
print(prediction_on_batch_out.shape)
(32, 1)
output_batch_average가 32 batch 사이즈니까, 당연히 prediction_on_batch_out도 32 batch겠군요.
자, 이제 전체 모형을 묶어 보시죠.
이제까지 준비한 것들을 다음과 같이 묶어서 전체 모형을 완성해 봅시다. 입력 - augmentation - mobilenet - pooling - dropout(과적합방지) - fc dense(1) 순으로 구성합니다! 짜잔.
모형은 처음에 제시했지만, 잊어버렸을 수가 있으니까
요런 모양새입니다.
inputs = tf.keras.Input(shape=(160, 160, 3))
x = layer_augmentation(inputs)
x = layer_monet_input(x)
x = model_base(x)
x = layer_global_average(x)
x = tf.keras.layers.Dropout(0.2)(x)
outputs = layer_out_fully_connected(x)
model = tf.keras.Model(inputs, outputs)
모형을 만들었으니, 이걸 compile해서 학습이 가능한 상태로 만들고요! optimizer는 Adam, Loss는 Binary Cross Entropy를 사용해서 만듭니다! 이제 다 왔어요. 흠. 여기에서 from_logits라는 인자를 cross entropy에 넣게 되는데, 처음 보죠? 이것은 모형에 Activation을 넣지 않았기 때문에, 마지막 Dense(1)의 출력이 logit이기 때문입니다. logit은 이전에도 이미 알아 보았듯이 activation에 넣기 전의, score느낌의 값입니다. 지금의 모형은 출력이 logit으로 나오기 떄문에 (activation이 없어서) from_logit을 True로 해서 학습을 하면 됩니다. 사실 이게 계산을 더 stable하게 할 수 있다고 해서 이런 식으로 모형을 구성하는 경우가 있는데, 사실 확실하게 장점이 있는지는 잘 모르겠군요.
Logit은
"Logistic Regression의 환장파티 - Sigmoid 출력값이 왜 확률인가요?" 편에서 이미 다뤘던 내용이니까, 참고하면 도움이 될거라 생각합니다.
자, 이제 훈련을 개시!
base_learning_rate = 0.0001
model.compile(optimizer=tf.keras.optimizers.Adam(lr=base_learning_rate),
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
metrics=['accuracy'])
처음에 10번만 훈련을 해 보겠습니다. 어떻게 되나 꽤나 궁금하군요. 자, ㄱㄱㄱ
first_step_epochs = 10
history = model.fit(train_dataset,
epochs=first_step_epochs,
validation_data=validation_dataset)
Epoch 1/10 63/63 [==============================] - 3s 53ms/step - loss: 0.7815 - accuracy: 0.5160 - val_loss: 0.5406 - val_accuracy: 0.6448 Epoch 2/10 63/63 [==============================] - 3s 43ms/step - loss: 0.5726 - accuracy: 0.6735 - val_loss: 0.3997 - val_accuracy: 0.7884 Epoch 3/10 63/63 [==============================] - 3s 42ms/step - loss: 0.4649 - accuracy: 0.7580 - val_loss: 0.3074 - val_accuracy: 0.8787 Epoch 4/10 63/63 [==============================] - 3s 43ms/step - loss: 0.3838 - accuracy: 0.8180 - val_loss: 0.2560 - val_accuracy: 0.9035 Epoch 5/10 63/63 [==============================] - 3s 43ms/step - loss: 0.3135 - accuracy: 0.8620 - val_loss: 0.2100 - val_accuracy: 0.9369 Epoch 6/10 63/63 [==============================] - 3s 43ms/step - loss: 0.2995 - accuracy: 0.8625 - val_loss: 0.1856 - val_accuracy: 0.9468 Epoch 7/10 63/63 [==============================] - 3s 42ms/step - loss: 0.2712 - accuracy: 0.8870 - val_loss: 0.1661 - val_accuracy: 0.9567 Epoch 8/10 63/63 [==============================] - 3s 42ms/step - loss: 0.2549 - accuracy: 0.8895 - val_loss: 0.1517 - val_accuracy: 0.9567 Epoch 9/10 63/63 [==============================] - 3s 43ms/step - loss: 0.2461 - accuracy: 0.8925 - val_loss: 0.1352 - val_accuracy: 0.9616 Epoch 10/10 63/63 [==============================] - 3s 42ms/step - loss: 0.2310 - accuracy: 0.8955 - val_loss: 0.1302 - val_accuracy: 0.9666
학습 결과를 보니, 단 10번의 epoch만에 약 96%의 정확도를 보입니다. 머야 머야. 왤케 잘하는거죠? 어쨌든 그러면 조금 더 실행해 볼 만한 것은 mobile net 일부를 학습에 참여 시켜보는 방법이 있겠습니다. 이거 한번 해보죠.
이것이 바로 Fine Tuning이라는 것 입니닷!!!
# base model에 얼마나 많은 layer가 있는지 확인해 보고요,
print("Number of layers in the base model: ", len(model_base.layers))
# 바로 전에는 mobile net을 아예 학습에 참여시키지 않았지만, 이번엔 뒤쪽 100개 layer는 학습에 참여시킬 셈입니다. 일단 모형 전체를 trainable로 세팅 해 주고요.
model_base.trainable = True
# 100개 Fine tuning 시작점을 정해주고요,
fine_tune_at = 100
# 0~100까지는 freeze시켜서는 학습에 참여하지 않도록 해 줍니다.
for layer in model_base.layers[:fine_tune_at]:
layer.trainable = False
# 한번 봅시다. 실제로 trainable이 잘 세팅 되었는지 말이죠.
import pandas as pd
display_layers = [(layer, layer.name, layer.trainable) for layer in model_base.layers]
pd.DataFrame(display_layers, columns=['Layer Type', 'Layer Name', 'Layer Trainable'])
Number of layers in the base model: 155
Layer Type | Layer Name | Layer Trainable | |
---|---|---|---|
0 | input_1 |
False |
|
1 | Conv1_pad |
False |
|
2 | Conv1 |
False |
|
3 | bn_Conv1 |
False |
|
4 | Conv1_relu |
False |
|
... | ... | ... | ... |
150 | block_16_project |
True |
|
151 | block_16_project_BN |
True |
|
152 | Conv_1 |
True |
|
153 | Conv_1_bn |
True |
|
154 | out_relu |
True |
|
155 rows × 3 columns
잘은 모르겠는데, 여튼 이렇게 추가로 학습에 참여하면 더 좋은 결과가 나올 것 도 같긴 한데 시도해 봅니다! 아까는 Adam이번엔 RMSProp으로 시도!
model.compile(optimizer = tf.keras.optimizers.RMSprop(lr=base_learning_rate/10),
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
metrics=['accuracy'])
# 10 epoch동안 추가로 fine-tuning 진행해 보시죠.
fine_tune_epochs = 10
total_epochs = first_step_epochs + fine_tune_epochs
history_fine = model.fit(train_dataset,
epochs=total_epochs,
initial_epoch=history.epoch[-1],
validation_data=validation_dataset)
Epoch 10/20 63/63 [==============================] - 3s 51ms/step - loss: 0.3704 - accuracy: 0.8645 - val_loss: 0.0942 - val_accuracy: 0.9715 Epoch 11/20 63/63 [==============================] - 3s 43ms/step - loss: 0.2476 - accuracy: 0.9060 - val_loss: 0.0758 - val_accuracy: 0.9777 Epoch 12/20 63/63 [==============================] - 3s 43ms/step - loss: 0.2122 - accuracy: 0.9145 - val_loss: 0.0711 - val_accuracy: 0.9790 Epoch 13/20 63/63 [==============================] - 3s 44ms/step - loss: 0.1694 - accuracy: 0.9360 - val_loss: 0.0684 - val_accuracy: 0.9740 Epoch 14/20 63/63 [==============================] - 3s 44ms/step - loss: 0.1628 - accuracy: 0.9300 - val_loss: 0.0518 - val_accuracy: 0.9790 Epoch 15/20 63/63 [==============================] - 3s 43ms/step - loss: 0.1680 - accuracy: 0.9280 - val_loss: 0.0515 - val_accuracy: 0.9790 Epoch 16/20 63/63 [==============================] - 3s 44ms/step - loss: 0.1458 - accuracy: 0.9445 - val_loss: 0.0479 - val_accuracy: 0.9827 Epoch 17/20 63/63 [==============================] - 3s 44ms/step - loss: 0.1189 - accuracy: 0.9485 - val_loss: 0.0451 - val_accuracy: 0.9790 Epoch 18/20 63/63 [==============================] - 3s 43ms/step - loss: 0.1254 - accuracy: 0.9535 - val_loss: 0.0446 - val_accuracy: 0.9839 Epoch 19/20 63/63 [==============================] - 3s 44ms/step - loss: 0.1197 - accuracy: 0.9475 - val_loss: 0.0476 - val_accuracy: 0.9765 Epoch 20/20 63/63 [==============================] - 3s 43ms/step - loss: 0.1254 - accuracy: 0.9440 - val_loss: 0.0404 - val_accuracy: 0.9839
loss, accuracy = model.evaluate(test_dataset)
print('Test accuracy :', accuracy)
print(test_dataset)
6/6 [==============================] - 0s 22ms/step - loss: 0.0559 - accuracy: 0.9740 Test accuracy : 0.9739583134651184
여기에서 특이점은 compile할 때 모형에 이미 loss를 binarycrossentropy로 설정 했으니까, 마지막 layer에 sigmoid가 없더라도 그것에 맞춰서 evaluate해 줍니다.
대신 실제로 predict를 할 때에는 sigmoid를 달아서 0.5기준으로 개/고양이를 판단해야 합니다.
# Test 데이터에서 image를 batch만큼 batch로 예측을 해 보면, (눈으로 확인해 보려구요)
image_batch, label_batch = test_dataset.as_numpy_iterator().next()
predictions = model.predict_on_batch(image_batch).flatten()
# ★★★★
# 모형이 logits로 결과를 주니까, sigmoid를 씌워서! 아까 from_logits=True 였잖아요? 그러니까 수동으로 sigmoid를 통과시켜야 합니다.
# 예츠으으으윽!!!
predictions = tf.nn.sigmoid(predictions)
predictions = tf.where(predictions < 0.5, 0, 1)
# 예측과 Label결과물을 좀 살펴보고,
print('Predictions:\n', predictions.numpy())
print('Labels:\n', label_batch)
# 눈으로 확인해 보자구요. (예측/레이블 순서로 표시)
plt.figure(figsize=(10, 10))
for i in range(30):
ax = plt.subplot(5, 6, i + 1)
plt.imshow(image_batch[i].astype("uint8"))
plt.title(class_names[predictions[i]] + "/" + class_names[label_batch[i]])
plt.axis("off")
Predictions: [0 0 0 1 1 0 1 0 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 1 0 1 0 1 1 0 0 1] Labels: [0 0 0 1 1 0 1 0 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 1 0 1 0 1 1 0 0 1]
결과가 어떤지 xor로 확인해 보면! (정답과 prediction이 다른 개수를 구하는 가장 쉬운 방법!)
np.sum(np.bitwise_xor(predictions.numpy(), label_batch))
0
결과가 0이라는건 모든 결과가 같다는 것이거든요. 후아. 모두 맞췄군요. 이거 좀 무서운데요.
이런 식으로 기존의 pretrained model을 활용해서 fine tuning을 하면 transfer learning이 완성되게 되는 뭐 그런 이야기입니다. 사실 이런 식으로 할 수 있다는 걸 알면 다른 인공지능 모형들도 같은 방식으로 추가 학습이 가능할거라는 사실을 알게되는 것으로 이 글의 임무는 완료했다고 봅니다. ㅎㅎ
단 10번의 epoch로 fine tuning을 해서 이정도의 성능을 보인다면 다른 사람들이 만들어 놓은 모형에 fine tuning을 하는 것은 어찌보면 당연하게도 해야만 하는 것 아닐까요
Transfer Learning을 처음 할 때 일반적인 느낌과 알던 것에 비하여 전혀 다른 Term이 하나 있는데, top(또는 head)라는 단어입니다. 전이학습을 하기 위해 top을 include하지 않겠다고 했는데, 모형을 위에서 부터 그려 내려오니까 top이 입력 부분 아닌가?하고 생각을 하게 되는데, 실제로는 출력부분을 top이라고 부릅니다. 사실 이 top은 head와 같은 의미인데, 가장 상위의 판단을 하는 것을 top이라고 부르기 때문에 출력부분을 top(head)이라고 부른다고 합니다. 그게 head라고 하니까 더 그럴 듯하기도 합니다. 그래도 어쨌든 계속 헷갈리는 건 어쩔 수 없습니다.
댓글