사용한 데이터 셋: https://www.kaggle.com/datasets/tongpython/cat-and-dog?select=test_set
3. 데이터 증강
■ 학습 데이터가 너무 적으면 새로운 데이터에 일반화할 수 있는 모델을 훈련할 수 없기 때문에 과대적합이 발생한다.
# https://www.kaggle.com/datasets/tongpython/cat-and-dog?select=test_set에서 데이터 셋 압축 해제
import os
print("Training directory contents:", os.listdir(train_dir))
print("Validation directory contents:", os.listdir(valid_dir))
```#결과#```
Training directory contents: ['cats', 'dogs']
Validation directory contents: ['cats', 'dogs']
````````````
- train set과 valid set은 cats와 dogs 폴더로 이루어짐.
■ 케라스 ImageDataGenerator 클래스로 rescale 옵션을 지정해 이미지의 각 픽셀 값을 0~1 범위로 정규화할 수 있다.
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# 이미지 데이터 제네리어터 정의
image_gen = ImageDataGenerator(rescale = (1/255.))
■ 이미지 제너레이터 객체에 flow_from_directory 함수를 적용하면, 지정한 폴더에서 이미지를 가져와 반복 이터레이션으로 데이터 셋을 처리한다.
train_gen = image_gen.flow_from_directory(train_dir,
batch_size=32,
target_size=(224, 224),
classes=['cats','dogs'],
class_mode = 'binary',
seed=2024)
valid_gen = image_gen.flow_from_directory(valid_dir,
batch_size=32,
target_size=(224, 224),
classes=['cats','dogs'],
class_mode = 'binary',
seed=2024)
```#결과#```
Found 8005 images belonging to 2 classes.
Found 2023 images belonging to 2 classes.
````````````
# 제너레이터에서 첫 번째 배치의 이미지와 라벨 추출
images, labels = next(train_gen)
print('Images shape:', images.shape) # (N, H, W, C)
print('Labels shape:', labels.shape)
```#결과#```
Images shape: (32, 224, 224, 3)
Labels shape: (32,)
`````````````
- 먼저 폴더의 디렉터리를 지정한 다음,
- batch_size 속성에 하나의 Mini-Batch를 구성하는 데이터의 개수, 배치 크기를 지정하고,
- target_size 속성에는 이미지의 크기(H, W) = (224, 224)를 지정한다.
- 사용하는 이미지는 컬러 이미지이므로 이렇게 배치 크기와 이미지 크기를 지정하면 한 번의 순전파 & 역전파 과정에서 (N, H, W, C) = (32, 224, 224, 3) 형태가 흘러 들어간다.
- classes 속성에는 클래스 레이블을 지정한다. (실제 이미지가 들어 있는 폴더 이름을 지정하였다.)
- class_mode에는 이진 분류 문제이므로 'binary'를 지정한다.
- 랜덤 시드 값도 지정할 수 있다.
from tensorflow import keras
from tensorflow.keras import layers
inputs = keras.Input(shape=(224, 224, 3))
## CNN
x = layers.BatchNormalization()(inputs)
x = layers.Conv2D(filters=32, kernel_size=3, activation='relu')(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.BatchNormalization()(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation='relu')(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.BatchNormalization()(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation='relu')(x)
x = layers.MaxPooling2D(pool_size=2)(x)
## FCN
x = layers.Flatten()(x)
x = layers.Dropout(0.5)(x) # 드롭아웃
outputs = layers.Dense(1, activation="sigmoid")(x) # cat vs dog 이진 분류 문제니까 출력층 함수는 시그모이드
model = keras.Model(inputs=inputs, outputs=outputs)
model.compile(optimizer=tf.optimizers.Adam(learning_rate = 0.001),
loss = 'binary_crossentropy',
metrics = ['accuracy'])
history = model.fit(train_gen, validation_data = valid_gen, epochs = 30)
- 배치 정규화와 드롭아웃을 적용하였음에도 과대적합이 빠르게 발생하는 것을 확인할 수 있다.
이런 경우 컴퓨터 비전에 특화된 과대적합 억제 방법으로 데이터 증강(data augmentation)이 있다.
■ 과대적합은 학습할 데이터가 너무 적어 새로운 데이터에 일반화할 수 있는 모델이 훈련되지 않으면 발생한다.
■ 따라서 무수히 많은 데이터가 주어진다면, 데이터 분포의 가능한 모든 측면을 모델이 학습할 수 있으므로 과대적합을 억제할 수 있을 것이다.
■ 데이터 증강은 기존 훈련 데이터로부터 더 많은 훈련 데이터를 생성하는 방법으로, 여러 랜덤한 변환을 적용해 그럴듯한 이미지를 생성해서 데이터를 늘린다. 단, 훈련할 대 모델이 정확히 같은 데이터를 두 번 만나지 않도록 주의해야 한다.
■ 케라스에서는 모델 시작 부분에 여러 개의 데이터 증강 레이어를 추가할 수 있다.
https://keras.io/api/layers/preprocessing_layers/image_augmentation/
Keras documentation: Image augmentation layers
keras.io
■ 예를 들어 ImageDataGenerator를 사용하지 않는다면 다음과 같이 데이터 증강을 입력 이미지에 적용해서 모델을 정의할 수 있다.
data_augmentation = keras.Sequential(
[
layers.RandomFlip("horizontal"), # 랜덤하게 50% 이미지를 수평으로 뒤집는다.
layers.RandomRotation(0.1), # [-10%, +10%] 범위 안에서 랜덤한 값만큼 입력 이미지를 회전한다.(전체 원에 대한 비율이다. 각도로 나타내면 [-36도, + 36도]에 해당된다.)
layers.RandomZoom(0.2), # [-20%, 20%] 범위 안에서 랜덤한 비율만큼 이미지를 확대 또는 축소한다.
]
)
inputs = keras.Input(shape=(224, 224, 3))
x = data_augmentation(inputs) # 데이터 증강
x = layers.Rescaling(1./255)(x) # 픽셀 값 0~1 범위로 정규화
## CNN
x = layers.BatchNormalization()(inputs)
x = layers.Conv2D(filters=32, kernel_size=3, activation='relu')(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.BatchNormalization()(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation='relu')(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.BatchNormalization()(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation='relu')(x)
x = layers.MaxPooling2D(pool_size=2)(x)
## FCN
x = layers.Flatten()(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs=inputs, outputs=outputs)
■ ImageDataGenerator를 사용하면 클래스 함수의 매개변수 속성으로 데이터 증강 기법을 지정할 수 있다.
tf.keras.preprocessing.image.ImageDataGenerator | TensorFlow v2.16.1
tf.keras.preprocessing.image.ImageDataGenerator | TensorFlow v2.16.1
DEPRECATED.
www.tensorflow.org
# Augmentation 적용한 이미지 데이터 제너레이터 정의
image_gen_aug = ImageDataGenerator(rescale=1/255.,
horizontal_flip=True, # 좌우 반전 True
rotation_range=35, # 회전 35도
zoom_range=0.2) # 이미지 줌 확대
train_gen_aug = image_gen_aug.flow_from_directory(train_dir,
batch_size=32,
target_size=(224,224),
classes=['cats','dogs'],
class_mode = 'binary',
seed=2024)
valid_gen_aug = image_gen_aug.flow_from_directory(valid_dir,
batch_size=32,
target_size=(224,224),
classes=['cats','dogs'],
class_mode = 'binary',
seed=2024)
model_aug = keras.Model(inputs=inputs, outputs=outputs)
model_aug.compile(optimizer=tf.optimizers.Adam(learning_rate = 0.001),
loss = 'binary_crossentropy',
metrics = ['accuracy'])
history_aug = model_aug.fit(train_gen_aug, validation_data=valid_gen_aug, epochs=30)
- 데이터 증강 후, 과대적합이나 과소적합이 발생하지 않고 검증 정확도도 이전 모델이 비해 증가한 것을 확인할 수 있다.
■ 모델의 파라미터(필터 개수나 모델에 있는 층(layer)의 수)를 튜닝하고 데이터를 증강하면 더 높은 정확도를 얻을 수도 있다. ■ 하지만 데이터가 적기 때문에 처음부터 다시 훈련해서 더 높은 정확도를 달성하기 어렵다. 이런 상황에서 정확도를 높이기 위한 방법으로 전이 학습을 이용해 사전 훈련된 모델을 사용하는 방법이 있다.
4. 전이 학습(Transfer Learning)
■ 전이 학습은 기존에 학습된 모델에서 모델 일부를 가져와 그대로 사용하고, 출력층만 새롭게 추가하여 새로운 모델을 만드는 방법이다.
■ 사전에 훈련을 받은 딥러닝 모델의 구조와 가중치를 그대로 가져오고, 출력층만 변경하여 모델을 만들 수 있다. 예를 들어 다음과 같이 VGG-16 모델을 활용해서 분류 모델을 만들 수 있다.
from tensorflow.keras.applications.vgg16 import VGG16
pre_trained_vgg16 = VGG16(weights='imagenet', include_top=False, input_shape=[224, 224, 3])
pre_trained_vgg16.summary()
```#결과#```
Model: "vgg16"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_6 (InputLayer) [(None, 224, 224, 3)] 0
block1_conv1 (Conv2D) (None, 224, 224, 64) 1792
block1_conv2 (Conv2D) (None, 224, 224, 64) 36928
block1_pool (MaxPooling2D) (None, 112, 112, 64) 0
block2_conv1 (Conv2D) (None, 112, 112, 128) 73856
block2_conv2 (Conv2D) (None, 112, 112, 128) 147584
block2_pool (MaxPooling2D) (None, 56, 56, 128) 0
block3_conv1 (Conv2D) (None, 56, 56, 256) 295168
block3_conv2 (Conv2D) (None, 56, 56, 256) 590080
block3_conv3 (Conv2D) (None, 56, 56, 256) 590080
block3_pool (MaxPooling2D) (None, 28, 28, 256) 0
block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160
block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808
block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808
block4_pool (MaxPooling2D) (None, 14, 14, 512) 0
block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808
block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808
block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808
block5_pool (MaxPooling2D) (None, 7, 7, 512) 0
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
_________________________________________________________________
````````````
- include_top은 네트워크 Top 층에 놓인 완전연결 계층의 분류기를 포함할지 안 할지 지정하는 옵션이다.
- include_top = False로 설정하여 FCN 구조인 출력층은 가져오지 않는다. Top 층에 해결하려는 문제에 맞는 출력층을 추가해야 하기 때문이다.
- 만약 i nclude_top = True로 설정한다면, 합성곱 계층 위에 완전연결 계층이 추가되기 때문에 원본 모델과 동일한 input_shape을 사용해야 한다.
4.1 데이터 증강을 사용한 특성 추출
■ 데이터 증강을 사용한 '특성 추출'은 학습 속도도 느리고 비용이 많이 들지만, 사전 훈련된 모델에 데이터 증식 기법을 사용해서 작은 이미지 데이터 셋을 사용할 경우 괜찮은 모델 성능을 기대할 수 있다.
■ 이렇게 하려면, 먼저 합성곱 계층을 동결해서 훈련 중에 가중치가 업데이트되지 않도록 설정해야 한다.
■ 사전 훈련된 모델을 사용하는 가장 큰 이유는 해당 모델에 잘 학습된 가중치를 그대로 사용하기 위해서이다.
(이 예에서는 imagenet 데이터 셋으로 학습했을 때의 가중치를 사용한다.)
■ 만약 동결하지 않는다면, 훈련 과정에서 사전 학습된 가중치가 업데이트된다. 즉, 동결되지 않은 합성곱 계층으로 인해 사전에 학습된 표현이 훈련하는 동안 변경되므로 사전 훈련된 모델을 불러온 의미가 없어진다.
■ 그러므로 학습된 가중치가 업데이트되지 않도록 불러온 모델의 trainable 속성을 False로 설정한다. Ture로 설정하면 사전 학습된 가중치까지 훈련 과정에서 업데이트되기 때문이다.
■ 새롭게 정의한 완전연결 계층만 가중치가 업데이트되도록 설정한다.
pre_trained_vgg16.trainable = False
■ 실제로 VGG-16 모델의 trainable 속성을 True로 설정하면 훈련 가능한 가중치의 개수가 26개임을 확인할 수 있다.
pre_trained_vgg16.trainable = True
print('훈련 가능한 가중치 개수:',len(pre_trained_vgg16.trainable_weights))
```#결과#```
훈련 가능한 가중치 개수: 26
`````````````
pre_trained_vgg16.trainable = False
print('동결 후 훈련 가능한 가중치 개수:',len(pre_trained_vgg16.trainable_weights))
```#결과#```
동결 후 훈련 가능한 가중치 개수: 0
`````````````
■ 그다음, Sequential API나 함수형 API를 통해 불러온 사전 훈련된 모델의 합성곱 계층과 완전연결 계층인 새로운 출력층을 정의한 다음, 두 계층을 합쳐 새로운 모델을 만들면 된다.
## 새로운 모델의 합성곱 계층 정의
base_model = keras.Sequential(name='base_model')
for layer in pre_trained_vgg16.layers:
base_model.add(layer)
## 합성곱 계층 동결
for layer in base_model.layers:
layer.trainable = False
print(layer.name, layer.trainable)
```#결과#```
block1_conv1 False
block1_conv2 False
block1_pool False
block2_conv1 False
block2_conv2 False
block2_pool False
block3_conv1 False
block3_conv2 False
block3_conv3 False
block3_pool False
block4_conv1 False
block4_conv2 False
block4_conv3 False
block4_pool False
block5_conv1 False
block5_conv2 False
block5_conv3 False
block5_pool False
````````````
## 새로운 출력층 정의
new_top = base_model.output
new_top = layers.Flatten()(new_top)
new_top = layers.Dropout(0.5)(new_top)
# new_top = layers.Dense(1024, activation = 'relu')(new_top) # 활성화 함수를 추가해도 된다.
new_top = layers.Dense(1, activation ='sigmoid')(new_top)
## 새로운 모델 정의(사전 훈련된 모델의 합성곱 계층 + 새롭게 정의한 출력층(완전연결 계층))
new_model.summary()
```#결과#```
Model: "model"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) [(None, 224, 224, 3)] 0
block1_conv1 (Conv2D) (None, 224, 224, 64) 1792
block1_conv2 (Conv2D) (None, 224, 224, 64) 36928
block1_pool (MaxPooling2D) (None, 112, 112, 64) 0
block2_conv1 (Conv2D) (None, 112, 112, 128) 73856
block2_conv2 (Conv2D) (None, 112, 112, 128) 147584
block2_pool (MaxPooling2D) (None, 56, 56, 128) 0
block3_conv1 (Conv2D) (None, 56, 56, 256) 295168
block3_conv2 (Conv2D) (None, 56, 56, 256) 590080
block3_conv3 (Conv2D) (None, 56, 56, 256) 590080
block3_pool (MaxPooling2D) (None, 28, 28, 256) 0
block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160
block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808
block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808
block4_pool (MaxPooling2D) (None, 14, 14, 512) 0
block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808
block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808
block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808
block5_pool (MaxPooling2D) (None, 7, 7, 512) 0
flatten_1 (Flatten) (None, 25088) 0
dropout_1 (Dropout) (None, 25088) 0
dense_1 (Dense) (None, 1) 25089
=================================================================
Total params: 14,739,777
Trainable params: 25,089
Non-trainable params: 14,714,688
_________________________________________________________________
````````````
- 가중치가 업데이트 되는 부분은 위에서 새롭게 정의한 완전연결 계층 부분이다.
■ ImageDataGenerator를 사용하지 않는다면 다음과 같이 데이터 증강을 입력 이미지에 적용한 다음, 사전 훈련된 모델의 특성을 preprocess_input 함수로 추출해서(vgg16 모델에 입력될 이미지를 전처리, 이 예에서는 증강된 이미지) 새로운 모델을 정의할 수 있다.
https://www.tensorflow.org/api_docs/python/tf/keras/applications/vgg16/preprocess_input
tf.keras.applications.vgg16.preprocess_input | TensorFlow v2.16.1
Preprocesses a tensor or Numpy array encoding a batch of images.
www.tensorflow.org
inputs = keras.Input(shape=(224, 224, 3))
x = data_augmentation(inputs) # 데이터 증강
## 사전 훈련된 모델
x = keras.applications.vgg16.preprocess_input(x) # vgg16 특성 추출
x = pre_trained_vgg16(x) # 동결된 상태의 vgg16(include_top=False)의 합성곱 계층 사용
## 새로운 출력층 정의
x = layers.Flatten()(x)
...
...
outputs = layers.Dense(1, activation="sigmoid")(x)
new_model = keras.Model(inputs=inputs, outputs=outputs)
new_model.compile(optimizer=tf.optimizers.Adam(learning_rate = 0.001),
loss = 'binary_crossentropy',
metrics = ['accuracy'])
## 증강된 데이터 사용
new_model_history = new_model.fit(train_gen_aug, validation_data=valid_gen_aug, epochs=30)
- 다소 변동성이 보이지만, 훈련 결과가 과대적합으로 이어지지 않으며, 검증 정확도가 0.9 이상으로 약 10% 증가한 것을 확인할 수 있다.
5. 미세 조정(Fine Tuning)
■ 모델 재사용 시, 특성 추출을 보완하는 기법으로 fine tuning이 있다.
■ 주어진 문제에 조금 더 밀접하게 사용하기 위해 재사용 모델의 표현을 일부 조정하는 방법으로 특성 추출에 사용했던 동결 모델(trainable 속성을 False로 설정한 모델)의 상위 층 몇 개를 동결에서 해제하고 새로 추가한 층과 함께 훈련한다.
- 이때, 배치 정규화 층은 동결을 해제하지 않는다.
■ 상위 몇 개의 층만 미세 조정하는 이유는 다음과 같다.
- 합성곱의 하위 층들은 일반적이고 재사용 가능한 특성들(에지, 색깔, 질감 등)을 인코딩한다. 반면, 상위 층은 좀더 특화된 특성('강아지 눈'이나 '고양이 귀' 같은) 특성을 인코딩한다.
- 새로운 문제에 재사용한 모델을 사용할 경우, 필요한 것은 구체적인 특성이므로 상위 층만 미세 조정하는 것이 유리하다. 하위 층으로 갈수록 미세 조정에 대한 효과가 감소한다.
■ 파인 튜닝의 단계는
- (1) 합성곱 계층이 동결된 사전 훈련된 모델에 새로운 FCN 분류기를 추가해서 새로운 모델 정의
- (2) 새로운 모델 훈련 후, 파인 튜닝할 일부 층의 동결을 해제한다. 이때 배치 정규화 계층의 동결은 해제하면 안 된다.
- (3) 동결을 해체한 층과 새로운 FCN 분류기 층으로 구성된 모델을 훈련한다.
■ 예를 들어, 새롭게 정의한 new_model의 FCN 분류기는 VGG-16의 FCN 분류기가 아닌 현재 문제에 맞춰 새롭게 정의한 계층이다. 여기서 Top 층과 맞물려 있는 합성곱 기반층(Conv block)에서 몇 개의 최상위 합성곱 계층만 같이 학습하여 현재 문제에 맞게 모델을 미세 조정한다.
■ 단, 훈련해야 할 파라미터(업데이트해야 할 파라미터)가 많을수록 모델이 복잡해져 과대적합될 가능성이 커진다. 따라서 이 예에서는 Conv block 5에 있는 합성곱 계층 3개와 FCN 분류기를 같이 파인 튜닝한다.
# # Top 층과 맞물려 있는 합성곱 계층은 동결 해제
base_model.trainable = True
for layer in base_model.layers[:-4]:
layer.trainable = False
print(layer.name, layer.trainable)
```#결과#```
````````````
new_model.compile(optimizer=tf.optimizers.Adam(learning_rate = 0.0001),
loss = 'binary_crossentropy',
metrics = ['accuracy'])
new_model.summary()
```#결과#```
````````````
- 학습률을 낮춘 Adam 옵티마이저를 사용한다. 학습률을 낮추는 이유는 미세 조정하는 합성곱 계층에서 학습된 특징을 조금씩 수정하기 위해서이다.
new_model_history2 = new_model.fit(train_gen_aug, validation_data=valid_gen_aug, epochs=30)
참고) https://www.tensorflow.org/tutorials/images/transfer_learning?hl=ko
사전 학습된 ConvNet을 이용한 전이 학습 | TensorFlow Core
사전 학습된 ConvNet을 이용한 전이 학습 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 튜토리얼에서는 사전 훈련된 네트워크에서 전이 학습을 사용하여
www.tensorflow.org
'텐서플로' 카테고리의 다른 글
임베딩(Embedding) 순환신경망(Recurrent Neural Network, RNN) (0) | 2025.01.21 |
---|---|
텐서플로 합성곱 신경망(CNN) (1) (0) | 2024.11.15 |
케라스(Keras) (2) (0) | 2024.08.20 |
케라스(Keras) (1) (0) | 2024.08.18 |
텐서(Tensor) (1) (0) | 2024.08.10 |