반응형

2편 -

[AI/Self-Study] - Keras : ImageDataGenerator 대신에 tf.data로 빠르게 학습하기 2

 

Keras : ImageDataGenerator 대신에 tf.data로 빠르게 학습하기 2

Keras : ImageDataGenerator 대신에 tf.data로 빠르게 학습하기 1편 : lynnshin.tistory.com/26 Keras : ImageDataGenerator 대신에 tf.data로 빠르게 학습하기 1 이미지 분류 프로젝트 진행하면서 Keras의 Image..

lynnshin.tistory.com

 

이미지 분류 프로젝트 진행하면서 Keras의 ImageDataGenerator를 쓰는데 너무 느리다는 문제점이 발생했다.


구글링을 통해 Tensorflow의 tf.data API를 사용하면 이미지를 로딩하는 시간을 약 5배정도 감소시킬 수 있다고 해 직접 ImageDataGenerator를 사용해 EfficientNet 모델을 훈련시키는 코드를 tf.data로 바꿔보았다.

 

결과는 epoch=2 기준으로 ImageDataGenerator는 한 epoch당 2시간 (ETA) 예상 소요시간이 나왔고, tf.data API는 epoch 2번을 다 도는데 30분 정도 밖에 걸리지 않았다.

 

즉 ==> 실제로 훈련시간이 매우 단축되었다!

 

유용한 참고 링크들:

 

tf.data API사용하기

0. 하이퍼파라미터 세팅

BATCH_SIZE = 32
IMG_HEIGHT = 224
IMG_WIDTH = 224
AUTOTUNE = tf.data.experimental.AUTOTUNE

tf.data.experimental.AUTOTUNE = 작동하는 Network가 스스로 설정해 dataset을 잘 불러올 수 있게 결정 

  • 나중에 성능 최적화을 위해 프리페치(prefetch)와 멀티스레드 적재와 전처리를 사용할 때 텐서플로우가 환경을 기반으로 동적으로 적절한 스레드 개수를 선택 및 buffer size를 선택해준다.
  • 멀티스레드로 데이터를 적재하고 전처리 하면 CPU의 멀티 코어를 활용해 GPU에서 훈련 스텝을 수행하는 것보다 짧은 시간안에 한 배치 데이터를 준비할 수 있다.

 

1. 데이터 불러오기

list_files() 함수는 파일 경로를 섞은 데이터셋을 반환한다. Shuffle를 원하지 않는다면 shuffle=False로 지정하면 된다.

path로 파일이 위치해 있는 경로를 넣으면 된다. 

나중에 이 path에서 label를 가져오는 함수가 있음으로 label (target)이 될 부분과 파일명 부분을 /*/* 로 표시

# Create a source dataset from input data
train_list_ds = tf.data.Dataset.list_files(str(train_path+'/*/*'),shuffle=True) # 파일 경로를 섞은 데이터셋 반환
valid_list_ds = tf.data.Dataset.list_files(str(valid_path+'/*/*'),shuffle=False)

아래 코드로 데이터 셋을 확인할 수 있다. (이미지 파일의 경로가 String으로 출력)

take() 메서드는 데이터 셋에 있는 몇 개의 아이템만 볼 수 있도록 해준다.

for f in train_list_ds.take(5): # check
  print(f.numpy())

 

 

2. 데이터 준비

tf.data를 사용해 이미지를 지정된 배치 단위로 로딩한다.

 

아래는

1) tf.data.Dataset.list_files로 가지고온 dataset list 에서  (string으로 저장됨) slicing을 이용해서 label (target) 이름을 가지고 온다. 폴더 구조 상 data file path에서 (train_path+'/*/*') 뒤에서 두번째가 label 이 될 디렉토리

2) 경로에서 이미지를 읽어서 이미지를 tensor로 변환하고 resize

3) image 와 label 를 tensor로 반환

# 파일의 트리 구조를 사용하여 class_names 목록을 컴파일
CLASS_NAMES = np.array(sorted(os.listdir(train_path)))

def get_label(file_path):
  # convert the path to a list of path components
  parts = tf.strings.split(file_path, os.path.sep)
  
  # The second to last is the class-directory
  # path 의 뒤에서 2번째가 output class 디렉토리
  return parts[-2] == CLASS_NAMES
def decode_img(img):
  # convert the compressed string to a 3D uint8 tensor
  img = tf.image.decode_jpeg(img, channels=3)
  # resize the image to the desired size
  return tf.image.resize(img, [IMG_WIDTH, IMG_HEIGHT])
def process_path(file_path):
  label = get_label(file_path)
  # load the raw data from the file as a string
  img = tf.io.read_file(file_path)
  img = decode_img(img)
  return img, label

 

 

3. 성능 최적화

데이터 셋을 효율적으로 셔플링, 반복, 배치를 적용한 데이터 셋을 만들어서 반환. 

마지막에 prefetch()를 호출하면 데이터 셋은 항상 한 배치가 미리 준비되어 있도록 한다. 즉, 훈련 알고리즘이 한 배치로 작업을 하는 동안 데이터 셋은 동시에 다음 배치를 준비 (예를 들면, 디스크에서 데이터를 읽고 전처리) 한다. => 성능을 크게 향상 시킴

 

뒤에 map() 메서드를 호출할 때 num_parallel_calls 매개변수를 지정해 멀티스레드로 데이터를 적재하고 전처리 하면 CPU의 멀티 코어를 활용해서 GPU에서 훈련 스텝을 수행하는 것보다 짧은 시간에 한 배치 데이터를 준비할 수 있다 => 훈련 속도가 더 빨라짐

 

* 데이터 셋이 메모리에 모두 들어갈 수 있을 정도로 작을 때 RAM에 모두 캐싱 할 수 있는 cache() 메서드를 사용하면 훈력 속도를 크게 높일 수 있다. 일반적으로 데이터를 적재하고 전처리 한 후에, 셔플링, 반복, 배치, 프리페치 하기 전에 캐시를 수행한다 (각 샘플을 매 에포크가 아니라 한번만 읽고 전처리 하지만 에포크 마다 다르게 셔플링 되게 하고 다음 배치도 미리 준비할 수 있다).

def prepare_for_training(ds, cache=True, shuffle_buffer_size=1000):
  if cache:
    if isinstance(cache, str):
      ds = ds.cache(cache)	
    else:
    	ds = ds.cache()

  ds = ds.shuffle(buffer_size=shuffle_buffer_size)
  ds = ds.repeat()
  ds = ds.batch(BATCH_SIZE)
  ds = ds.prefetch(buffer_size=AUTOTUNE)
  return ds

+ repeat() batch() 순서에 따라 다른점

https://www.tensorflow.org/guide/data

 

 

4. 데이터 변환

map() 메서드를 호출해 아이템을 변환 (데이터 전처리 작업에 활용) 한다. 이때 num_parallel_calls 매개변수를 지정하면 복잡한 계산을 포함할 때 여러 스레드로 나눠서 속도를 높일 수 있다.

* map() 과 apply()의 차이점 : map() 메서드는 각 아이템에 변환을 적용하고, apply() 메서드는 데이터셋 전체에 변환을 적용한다.

train_labeled_ds = train_list_ds.map(process_path, num_parallel_calls=AUTOTUNE) # 전처리
test_labeled_ds = test_list_ds.map(process_path, num_parallel_calls=AUTOTUNE)
train_ds = prepare_for_training(train_labeled_ds) # 셔플링, 반복, 배치, 프리페치 적용
test_ds = prepare_for_training(test_labeled_ds)

 

 

5. 학습

# 1. model
model = Sequential()
model.add(EfficientNetB0(include_top=False, pooling = 'avg', weights = "imagenet"))
model.add(Dense(n, activation='softmax'))
# model.summary()

# checkpoint 저장 path 설정
checkpoint_path = '저장할 경로 + 이름.ckpt'

# callbacks
modelcheckpoint = ModelCheckpoint(checkpoint_path, monitor='val_loss', mode='min', save_best_only=True, save_weights_only=True, verbose=0)
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=10, verbose=1)
time_callback = TimeHistory() # 훈련 시간

# tensorboard 설정
to_hist = TensorBoard(log_dir='로그 저장할 폴더', histogram_freq=0, write_graph=True, write_images=True)


# 2. compile, fit
model.compile(optimizer = 'adam', loss = tf.keras.losses.categorical_crossentropy, metrics = ['acc'])

hist = model.fit(train_ds,
                steps_per_epoch=int(len(train_labeled_ds) / BATCH_SIZE),
                epochs=100,
                validation_data=test_ds,
                validation_steps=int(len(test_labeled_ds) / BATCH_SIZE),
                callbacks=[modelcheckpoint, reduce_lr, time_callback, to_hist])

print(time_callback.times) # 리스트 반환 (fit한 시간)
model.load_weights(checkpoint_path)

#모델 저장
model_path = '모델 저장 경로 + 이름.h5'
model.save(model_path)
반응형