본문 바로가기

텐서플로

텐서플로 합성곱 신경망(CNN) (2)

사용한 데이터 셋: 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)

데이터 증강 전 vs. 후

- 데이터 증강 후, 과대적합이나 과소적합이 발생하지 않고 검증 정확도도 이전 모델이 비해 증가한 것을 확인할 수 있다.

■ 모델의 파라미터(필터 개수나 모델에 있는 층(layer)의 수)를 튜닝하고 데이터를 증강하면 더 높은 정확도를 얻을 수도 있다. 하지만 데이터가 적기 때문에 처음부터 다시 훈련해서 더 높은 정확도를 달성하기 어렵다. 이런 상황에서 정확도를 높이기 위한 방법으로 전이 학습을 이용해 사전 훈련된 모델을 사용하는 방법이 있다.

 

4. 전이 학습(Transfer Learning) 

 전이 학습은 기존에 학습된 모델에서 모델 일부를 가져와 그대로 사용하고, 출력층만 새롭게 추가하여 새로운 모델을 만드는 방법이다.

[출처] https://www.tredence.com/blog/transfer-learning-and-data-augmentation-in-deep-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