본문 바로가기

Data Science/Machine Learning 구현

[ML] Tensorflow.keras의 flow_from_directory, flow_from_dataframe 사용법

반응형

🔊 해당 포스팅은 권철민님의 CNN Fundamental 완벽 가이드 강의를 듣고 난 후 배운 내용을 정리하고자 하는 목적 하에 작성되는 포스팅입니다. 하단의 포스팅에서 사용되는 실습 코드 및 자료는 필자가 직접 재구성한 자료이며 권철민님의 자료를 그대로 인용하지 않았음을 필히 알려드립니다. 

 

저번 포스팅에서는Tensorflow.keras의 ImageDataGenerator에 대해 배워보았다. ImageDataGenerator가 어떻게 이미지 데이터 소스로부터 데이터를 얻어와 이미지를 전처리 및 증강 기법을 적용하고 모델에 어떤 형태로 입력으로 넣어주는지에 대해 알아보았었다. 이번 포스팅에서는 그 단계 중에 ImageDataGenerator 객체를 생성한 후 Numpy Array Iterator로 생성 및 변환해줄 때 사용하는 flow 관련 함수들인 flow_from_directory, flow_from_dataframe 사용방법에 대해 알아보자.

 

ImageDataGenerator는 하나의 데이터 파이프라인 역할을 수행한다.


먼저 ImageDataGenerator는 이미지 데이터를 가져오는 Data Loading과 이미지 데이터를 전처리 및 증강 기법을 적용하는 Preprocessing 단계를 한 번에 수행시켜주는 일종의 파이프라인 역할을 수행한다고 했다. 저번 포스팅에서도 살펴본 아래의 자료를 다시 한 번 살펴보면서 단계를 회고해보자.

 

ImageDataGenerator의 파이프라인 과정

 

위 단계를 텍스트로 단계화시키면 다음과 같다.

 

  1. ImageDataGenerator가 원본 데이터 소스 즉, jpg 나 jpeg와 같은 이미지 파일들을 Numpy Array 형태로 가져온 후 사용자가 설정한 여러가지 증강 기법을 적용할 준비를 함. 이 단계를 수행함으로써 ImageDataGenerator 객체가 생성됨
  2. flow() 또는 flow_from_directory() 또는 flow_from_dataframe() 함수로 Numpy Array Iterator 객체를 만들어줌. 이 때, X(image) array, Y(label) array, 배치 사이즈, 데이터 셔플 유무를 설정. 참고로 Iterator는 Python의 Iterator와 효과가 동일하다. Iterator는 내부적으로 yield를 사용하여 배치 사이즈만큼 그때 그때 마다 데이터들을 전달하여 메모리를 절약할 수 있다.
  3. 우리가 사용할 이미지 분류 모델 설계
  4. 설계한 모델에 fit() 또는 fit_generator() 함수를 호출하면 1번~2번 단계가 실질적으로 수행되고 설정된 배치사이즈만큼의 Numy Array 데이터를 Tensor로 바꾸고 이 Tensor들을 설계한 모델의 인풋으로 입력되어 본격적인 학습을 시작한다.

참고로 fit() 관련 함수를 호출할 때에 실질적으로 1번 단계부터 실행되므로 Spark의 Transformation, Action 연산처럼 마치 Lazy Execution 같이 수행된다고 할 수 있을 것 같다.

1. 디렉토리만 잘 설정해줘. 나머지는 내가 다 할게! , flow_from_directory()

flow_from_directory() 는 이름에서도 알 수 있다시피 어떤 디렉토리와 관련된 함수라는 것을 알 수 있다. 여기서 어떤 디렉토리란 무엇일까? 바로 메타 데이터가 담긴 디렉토리를 의미한다. 메타 데이터는 또 무엇인가? 여기서는 이미지 데이터 파일(jpg, jpeg)들과 해당 이미지들이 무슨 이미지를 나타내는지 텍스트로 표현한 문자열(cat, dog와 같은 문자열들)이 되겠다. 이제 좀 더 피부에 와닿게 느껴보기 위해서 하단의 실습코드를 살펴보자. 참고로 실습는 일부 GPU가 지원되는 Kaggle Notebook을 활용하였다. Kaggle Notebook에서 하단 그림의 Add data 부분을 클릭해 여기에 있는 데이터셋을 추가하면 Kaggle Notebook 서버 상에서 /kaggle/input 디렉토리에 해당 데이터들이 저장된다.

 

Kaggle Notebook

 

참고로 /kaggle/input/cat-and-dog 디렉토리 내부의 구조는 아래와 같다.

 

cat-and-dog 디렉토리 구조

 

위 cats, dogs 디렉토리들 내부에는 디렉토리들 이름에 맞는 강아지 또는 고양이 jpg 파일들이 들어있다. flow_from_directory() 는 인자로 설정해주는 directory의 바로 하위 디렉토리 이름을 레이블이라고 간주하고 그 레이블이라고 간주한 디렉토리 아래의 파일들을 해당 레이블의 이미지들이라고 알아서 추측하여 Numpy Array Iterator를 생성하게 된다. 위 cat-and-dog에 맞는 flow_from_directory() 소스코드는 아래와 같다.

 

from tensorflow.keras.preprocessing.image import ImageDataGenerator

# Train ImageDataGenerator
train_gen = ImageDataGenerator(horizontal_flip=True, rescale=1/255.)
# Test ImageDataGenerator
test_gen = ImageDataGenerator(rescale=1/255.)

train_flow_gen = train_gen.flow_from_directory(directory='/kaggle/input/cat-and-dog/training_set/training_set',
                                              target_size=(244, 244),  # 사용할 CNN 모델 입력 사이즈에 맞게 resize
                                              class_mode='categorical',
                                              batch_size=64,
                                              shuffle=True)
test_flow_gen = test_gen.flow_from_directory(directory='/kaggle/input/cat-and-dog/test_set/test_set',
                                            target_size=(244, 244),  # 사용할 CNN 모델 입력 사이즈에 맞게 resize
                                            class_mode='categorical',
                                            batch_size=64,
                                            shuffle=False)

 

참고로 flow_from_directory() 인자의 의미들은 다음과 같다.

 

  • target_size : 추후에 설계할 모델에 들어갈 인풋 이미지 사이즈 중 Width, Height를 입력
  • batch_size : 이미지 데이터 원본 소스에서 한 번에 얼마만큼의 이미지 데이터를 가져올 것인지
  • class_mode
    • 'categorical' : 'categorical_crossentropy' 처럼 멀티-레이블 클래스인데, 원-핫 인코딩된 형태
    • 'sparse' : 'sparse_categorical_crossentropy' 처럼 멀티-레이블 클래스인데, 레이블 인코딩된 형태
    • 'binary' : 'binary_crossentropy' 처럼 이진 분류 클래스로, 0 또는 1인 형태

학습, 테스트 데이터에 맞는 Numpy Array Iterator를 만들어주었으니 이제 모델을 설계하고 학습시켜보자. 참고로 학습, 평가 시 사용할 수 있는 fit_generator() 와 evaluate_generator() 는 추후에 Tensorflow에서 deprecated 예정이라고 하니 앞으로 그냥 fit(), evaluate()를 사용하는 것을 권장한다고 한다.

 

from tensorflow.keras.applications import Xception
from tensorflow.keras.layers import Input, GlobalAveragePooling2D, Dense
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam

def create_model(verbose=False):
    input_tensor = Input(shape=(224, 224, 3))
    pretrained_model = Xception(input_tensor=input_tensor, include_top=False, weights='imagenet')
    pretrained_output = pretrained_model.output
    
    # customize Classifier layer
    x = GlobalAveragePooling2D()(pretrained_output)
    x = Dense(units=128, activation='relu')(x)
    output = Dense(units=2, activation='softmax')(x)
    
    model = Model(inputs=input_tensor, outputs=output)
    if verbose:
        model.summary()
    return model

# 모델 정의
model = create_model(verbose=False)
# 모델 compile
model.compile(optimizer=Adam(lr=0.0001), loss='categorical_crossentropy', metrics=['accuracy'])
# 모델 학습(fit)
train_hist = model.fit(train_flow_gen, epochs=2)
# 모델 성능 테스트 데이터로 평가
test_hist = model.evaluate(test_flow_gen)

2. 메타 데이터를 Pandas DataFarme으로 저장해 전달해! , flow_from_dataframe()

flow_from_dataframe() 에서 dataframe은 판다스의 데이터프레임을 의미하는 것이 맞다. 결국 위에서 말한 메타 데이터를 판다스의 데이터프레임에다가 저장 후 ImageDataGenerator에게 전달해준다는 의미인데, 데이터 프레임 안에 이미지 Array를 저장할 수는 없을 테고 어떤 정보를 저장하는 것일까? 그것은 바로 이미지 데이터가 있는 디렉토리 경로와 레이블값을 저장해주는 것이다. 추가로 저장할 수 있는 것은 해당 이미지를 Train 데이터인지 Test 데이터인지 또는 Validation 데이터인지 칼럼을 추가할 수도 있을 것이다.

 

우선 현재 /kaggle/input/cat-and-dog 라는 디렉토리에 데이터들이 저장되어 있으니 os 라이브러리를 활용해 디렉토리 경로를 모두 탐색하고 Train, Test 또 강아지, 고양이에 맞게 레이블과 이미지 파일 경로들을 판다스 데이터프레임에 저장해보자.

 

import os
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split

paths = []
data_type = []
labels = []
for dirname, _, filenames in os.walk('/kaggle/input/cat-and-dog'):
    for filename in filenames:
        if '.jpg' in filename:
            path = dirname + '/' + filename
            paths.append(path)
            
            if '/training_set/' in path:
                data_type.append('train')
            elif '/test_set/' in path:
                data_type.append('test')
            else:
                data_type.append('N/A')
            
            if 'dogs' in path:
                labels.append('dog')
            elif 'cats' in path:
                labels.append('cat')
            else:
                labels.append('N/A')

print(len(paths), len(data_type), len(labels))
data_df = pd.DataFrame({'path': paths, 'data_type': data_type, 'label': labels})

train_df = data_df[data_df['data_type'] == 'train']
test_df = data_df[data_df['data_type'] == 'test']

# 클래스 불균형 막기 위해 stratify 인자에 레이블에 해당하는 pd.Series 형태로 추가
tr_df, val_df = train_test_split(train_df, stratify=train_df['label'], test_size=0.15, random_state=42)
print('Train:', tr_df.shape, 'Valid:', val_df.shape, 'Test:', test_df.shape)

 

이제 데이터 종류에 맞게 ImageDataGenerator와 Numpy Array Iterator를 다음과 같이 생성해보자.

 

from tensorflow.keras.preprocessing.image import ImageDataGenerator

# 데이터 종류에 맞게 ImageDataGenerator 객체 생성
tr_gen = ImageDataGenerator(horizontal_flip=True, rescale=1/255.)
val_gen = ImageDataGenerator(rescale=1/255.)
test_gen = ImageDataGenerator(rescale=1/255.)

# 데이터 종류에 맞는 Pandas.DataFrame으로부터 Numpy Array Iterator 생성
tr_flow_gen = tr_gen.flow_from_dataframe(dataframe=tr_df, x_col='path', y_col='label',
                                        target_size=(244, 244), class_mode='binary',
                                        batch_size=64, shuffle=True)
val_flow_gen = val_gen.flow_from_dataframe(dataframe=val_df, x_col='path', y_col='label',
                                          target_size=(244, 244), class_mode='binary',
                                          batch_size=64, shuffle=False)
test_flow_gen = test_gen.flow_from_dataframe(datafrmae=test_df, x_col='path', y_col='label',
                                            target_size=(244, 244), class_mode='binary',
                                            batch_size=64, suffle=False)

 

flow_from_dataframe() 인자에 대한 설명은 flow_from_directory() 와 거의 비슷하지만 차이점이라면 dataframe을 사용하는 만큼 데이터 종류에 맞게 생성한 데이터 프레임을 넣어주는 것 밖에 없다. 참고로 위 코드에서는 class_mode를 binary로 설정했는데, 어차피 강아지/고양이 분류는 이진 분류로도 볼 수 있으므로 이번엔 색다르게 이진 분류로 넣었다. 여기서 binary로 설정해주었으므로 모델 마지막 layer에 sigmoid로 활성함수를 변경해주고 컴파일할 때 loss 값을 binary_crossentropy 로 넣어주어야 한다는 점은 잊지말자!

 

from tensorflow.keras.applications import Xception
from tensorflow.keras.layers import Input, GlobalAveragePooling2D, Dense
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam

def create_model(verbose=False):
    input_tensor = Input(shape=(224, 224, 3))
    pretrained_model = Xception(input_tensor=input_tensor, include_top=False, weights='imagenet')
    pretrained_output = pretrained_model.output
    
    # customize Classifier layer
    x = GlobalAveragePooling2D()(pretrained_output)
    x = Dense(units=128, activation='relu')(x)
    # 이진 분류이므로 활성함수를 sigmoid로 변경
    output = Dense(units=1, activation='sigmoid')(x)
    
    model = Model(inputs=input_tensor, outputs=output)
    if verbose:
        model.summary()
    return model

model = create_model(verbose=False)
# 모델 compile
model.compile(optimizer=Adam(lr=0.0001), loss='binary_crossentropy', metrics=['accuracy'])
# 모델 학습
train_hist = model.fit(tr_flow_gen, epochs=2, validation_data=val_flow_gen)
# 테스트 데이터로 모델 평가
test_hist = model.evaluate(test_flow_gen)
반응형