2025. 2. 21. 16:33ㆍ머신러닝 수난기 (프로젝트)
Dogs vs. Cats (1) : https://one-plus-one-is-two.tistory.com/3
Dogs vs. Cats (2) : https://one-plus-one-is-two.tistory.com/4
Dogs vs. Cats (3) : https://one-plus-one-is-two.tistory.com/5
이전 포스팅에서 "작동하기는 하는 모델" 만들기에 성공했다. 이번 포스팅의 목표는 다음과 같다고 했다.
모델 성능을 개선시키기 전에 ImageDataGenerator()를 사용한 코드를 image_dataset_from_directory()를 사용한 코드로 바꿔보기로 하자.
데이터세트 생성 코드 수정
image_dataset_from_directory() 함수에 대한 문서는 다음 링크에서 확인할 수 있다.
Image Data loading : https://keras.io/api/data_loading/image/
Keras documentation: Image data loading
Image data loading [source] image_dataset_from_directory function keras.utils.image_dataset_from_directory( directory, labels="inferred", label_mode="int", class_names=None, color_mode="rgb", batch_size=32, image_size=(256, 256), shuffle=True, seed=None, v
keras.io
keras.utils.image_dataset_from_directory(
directory,
labels="inferred",
label_mode="int",
class_names=None,
color_mode="rgb",
batch_size=32,
image_size=(256, 256),
shuffle=True,
seed=None,
validation_split=None,
subset=None,
interpolation="bilinear",
follow_links=False,
crop_to_aspect_ratio=False,
pad_to_aspect_ratio=False,
data_format=None,
verbose=True,
)
- directory : 이미지 파일이 있는 경로를 적어주면 된다. train 폴더에 있으니 "train/"이라고 입력하면 된다.
- labels : "inferred" 또는 None 또는 리스트/튜플을 입력하면 된다.
"inferred"를 전달하면 하위 디렉터리 기준으로 라벨링된다고 한다. 예를 들어, train 폴더 안에 cat 폴더와 dog 폴더가 나뉘어 있어 각각의 폴더에 이미지가 들어있다면, cat 폴더에 있는 이미지는 "cat"으로, dog 폴더에 있는 이미지는 "dog"로 분류된다. 하지만 이 문제에서는 디렉터리가 그렇게 구성되어 있지 않다. train 폴더 안에 고양이 사진과 개 사진이 혼합되어 있고, 고양이인지 개인지는 파일명을 통해 알 수 있을 뿐이다. 따라서 이 문제에서 labels를 "inferred"로 설정하는 것은 적합하지 않다. 디폴트값이 "inferred"이기 때문에 다른 값으로 바꿔야 한다.
None을 입력하면 자동 라벨링을 하지 않는다는 뜻이다. 이 경우, 사용자가 수동으로 라벨링해야 한다.
리스트/튜플을 전달할 경우, 디렉터리 내 이미지 파일의 개수와 같은 길이를 가져야 한다. 리스트/튜플 내 요소와 이미지 파일명을 하나하나 매칭하여 라벨링을 진행한다. 이때, 레이블은 파일명의 알파벳 순서대로 정렬되어 있어야 한다.
나는 리스트/튜플을 전달하는 방법으로 라벨링을 수행하려고 한다. - label_mode : 레이블을 어떻게 인코딩시킬지 문자열로 전달하면 된다. "int"를 전달하면 레이블이 정수로 인코딩되고, "categorical"을 전달하면 레이블이 categorical vector로 인코딩된다(이는 손실 함수를 categorical cross entropy로 설정했을 때 적합하다고 한다). 그리고 "binary"를 전달하면 레이블이 실수형으로 0. 또는 1.으로 인코딩된다고 한다(이는 손실 함수를 binary cross entropy로 설정했을 때 적합하다고 한다). 그리고 라벨이 없을 때는 None을 전달한다고 한다. 나는 손실 함수를 binary cross entropy로 설정했기 때문에 "binary"를 전달하는게 좋을 것 같다.
- class_names : labels="inferred"일 때, 클래스 이름을 직접 지정할 수 있다고 한다. 나는 labels에 리스트/튜플을 직접 전달하기로 했으므로 이건 패스
- color_mode : 흑백 이미지(채널 개수 1)일 때 "grayscale", RGB 컬러 이미지(채널 개수 3)일 때 "rgb", RGBA 컬러 이미지(채널 개수 4)일 때 "rgba"를 전달하면 된다고 한다. 나는 채널 개수를 3개 사용하므로 디폴트값인 "rgb"를 사용하면 된다.
- batch_size : 배치 크기
- image_size : 이미지 크기. 이미지 크기는 2편에서 모델 설계할 때 IMAGE_SIZE로 정의한 바 있으므로 그걸 끌어다 사용하면 된다.
- shuffle : 데이터를 섞을지 결정한다. 시계열 데이터라면 섞으면 안되겠지만 이건 시계열 데이터가 아니므로 True로 둬도 괜찮다.
- seed : 시드값. 훈련 데이터를 고정시키고 싶으면 시드값을 지정하면 된다. 보통 같은 데이터 내에서 성능 개선 여부를 평가하거나 어떤 것이 더 좋은 모델인지 평가하고자 할 때 훈련 데이터를 고정하게 된다.
- validation_split : 0 이상 1 이하의 실수값으로 검증 데이터의 비율을 정한다. 이전 코드에서 0.3으로 지정했으므로 이번에도 0.3으로 지정할 것이다.
- subset : validation_split을 지정한다면, 데이터가 훈련 데이터와 검증 데이터로 나뉠 것이다. subset은 이 중 훈련 데이터와 검증 데이터 중 어떤 것을 반환할 지 선택하도록 하는 파라미터이다. "training"을 전달하면 훈련 데이터가 반환되며, "validation"을 전달하면 검증 데이터가 반환되고, "both"를 전달하면 둘 다 반환된다.
다른 파라미터들은 필요한 경우에 문서를 참조하도록 하자.
위에서 정리한 내용에 따라 데이터세트를 만들어 봤다.
# 데이터 라벨링
directory = "train/"
sorted_filenames = sorted(filenames)
labels = [0 if filename.split(".")[0] == "cat" else 1 for filename in sorted_filenames]
먼저, 자동 라벨링을 사용할 수 없으니 내가 직접 라벨링 해주기 위해 labels 리스트를 만들었다. 이는 image_dataset_from_directory() 함수의 labels 인자로 전달해줄 것이다.
from tensorflow.keras.utils import image_dataset_from_directory
training_data = image_dataset_from_directory(
directory,
labels=labels,
label_mode="binary",
image_size=IMAGE_SIZE,
seed=42,
validation_split=0.3,
subset="training"
)
validation_data = image_dataset_from_directory(
directory,
labels=labels,
label_mode="binary",
image_size=IMAGE_SIZE,
seed=42,
validation_split=0.3,
subset="validation"
)
그리고 이렇게 데이터세트를 만들어놓고 모델을 학습시켰는데...!
아니 또 왜 검증 정확도가 0.5 주변에서 맴돈다는 말이냐 ㅠㅠ
그런데 훈련 데이터에 대한 정확도는 0.93까지 올라간 걸 보면 검증 데이터 쪽에 문제가 있는 것 같은데... 라벨링이 제대로 됐는지 한 번 직접 확인해봤다.
# 배치에서 이미지와 라벨 가져오기
image_batch, label_batch = next(iter(training_data))
# 9개 이미지 출력
plt.figure(figsize=(10, 10))
for i in range(9):
plt.subplot(3, 3, i + 1)
plt.imshow(image_batch[i].numpy().astype("uint8")) # 이미지 변환
plt.title(f"Label: {label_batch[i].numpy()}") # 라벨 표시
plt.axis("off")
plt.show()
먼저 training_data 내의 데이터를 직접 확인해보기로 했다. tensorflow.data.Dataset 객체는 반복자를 사용해서 이미지(x)의 레이블(y)을 직접 확인할 수 있다. 출력 결과는 다음과 같았다.

# 배치에서 이미지와 라벨 가져오기
image_batch, label_batch = next(iter(validation_data)) # training_data -> validation_data로 바꾸기!
# 9개 이미지 출력
plt.figure(figsize=(10, 10))
for i in range(9):
plt.subplot(3, 3, i + 1)
plt.imshow(image_batch[i].numpy().astype("uint8")) # 이미지 변환
plt.title(f"Label: {label_batch[i].numpy()}") # 라벨 표시
plt.axis("off")
plt.show()

화질이 안 좋아서 애매한건 차치하더라도 딱 봐도 강아지인게 0으로 라벨링 되어있거나, 누가 봐도 고양이인게 1로 라벨링 되어있는 경우가 다수 발견되었다. 이렇게 검증 데이터쪽 라벨링이 이상하게 되어 있었기 때문에 검증 데이터의 정확도가 0.5로 찍히는 것이었다.
ChatGPT에게 물어보니 labels를 디폴트값으로 놔뒀으면 문제가 없지만, 나처럼 리스트/튜플을 직접 전달한 경우에 validation_split을 지정하면 라벨링이 어긋날 수 있는 문제가 생긴다고 하더라... 지피티가 준 해결책은 다음과 같다.
from tensorflow.keras.utils import image_dataset_from_directory
training_data, validation_data = image_dataset_from_directory(
directory,
labels=labels,
label_mode="binary",
image_size=IMAGE_SIZE,
seed=42,
validation_split=0.3,
subset="both"
)
이전에 작성했던 코드는 training_data는 subset을 "training"으로, validation_data는 subset을 "validation"으로 설정해서 훈련 데이터와 검증 데이터를 나눴었다.
하지만 이 코드는 subset을 "both"로 설정했다. 이렇게 설정하면 훈련 데이터와 검증 데이터가 모두 반환된다. 이것을 training_data와 validation_data에 한꺼번에 대입시키면 training_data에는 훈련 데이터가, validation_data에는 검증 데이터가 알아서 들어가고, 라벨링도 꼬이지 않는다고 한다.
이렇게 데이터세트를 다시 만들어놓고 검증 데이터의 라벨링을 확인해봤다.
오..? 이번엔 뭔가 제대로 라벨링된 것 같다. 지체 없이 바로 훈련에 들어갔다.
와 미친 할렐루야 (지피티는 신이다. 지피티 없었으면 공부 어떻게 했냐)
근데 아직 썩 만족스럽진 않다.
데이터셋을 ImageDataGenerator로 생성했을 때는 정확도가 76.62%로 나왔는데, 지금은 65.37%로 약 11%p나 떨어진 수치가 나왔다. 분명 모델을 같은걸로 썼는데 어떻게 정확도가 11%p나 떨어졌을까?
이거는 이전 코드에서 답을 찾을 수 있었다.
코드를 제대로 안 보고 넘어가긴 했지만, 이전 코드를 보니 ImageDataGenerator 객체를 생성할 때 이렇게 리스케일링(rescaling)하는 부분이 있었다. 이미지 픽셀 값은 [0, 255] 범위로 반환되는데, 모델은 0부터 1까지의 값으로 학습하는 것이 일반적이라고 한다. image_dataset_from_directory() 함수도 마찬가지로 픽셀 값을 [0, 255] 범위로 반환하므로, 이를 [0, 1] 범위가 되도록 리스케일링하는 작업이 필요하다.
training_data = training_data.map(lambda x, y : (x/255.0, y))
validation_data = validation_data.map(lambda x, y : (x/255.0, y))
리스케일링하는 방법은 이전에 생성한 데이터셋의 x값에 255.0을 나누는 방식으로 한다. 리스케일링한 후 모델을 학습시킨 결과는 다음과 같다.
검증 정확도(val_accuracy)가 74.45%까지 올랐다! 그래도 76.62%에 비하면 낮긴 하지만 이 정도면 데이터셋의 차이라고 봐도 무방할 것 같다. 그러면 이제 성능 개선 방안을 한 번 생각해보자.
성능 개선
1트
가장 먼저 눈에 띄는 것은 훈련 데이터셋에 대한 정확도이다.
검증 데이터셋의 정확도(val_accuracy)는 70%대 중반에서 머무르는 반면에, 훈련 데이터셋의 정확도(accuracy)는 거의 100%에 치닫고 있다. 훈련 데이터셋에 대한 정확도가 이렇게 높은건 결코 좋은 현상이 아니다. 모델이 훈련 데이터셋에만 과적합 된다면 새로운 데이터에 대해서는 정확도가 떨어질 위험이 있기 때문이다. 과적합을 해결한다고 할 때, 가장 먼저 떠오르는 방법은 뭐니뭐니해도 드롭아웃(Dropout)이다.
드롭아웃 기법이란, 모델을 학습할 때 은닉층에 배치된 노드 중 일부를 학습되지 않도록 임의로 끄는 것이다. 노드 또는 층이 많아지면 과적합 현상이 발생할 수 있는데, 랜덤하게 노드를 끄면 학습 데이터에 지나치게 치우쳐서 학습되는 과적합을 방지할 수 있다.
keras.layers.Dropout(rate, noise_shape=None, seed=None, **kwargs)
Dropout 계층은 위와 같이 만들 수 있다. 노드를 어느 정도의 비율로 끌 지를 정하는 rate 파라미터가 가장 중요한 파라미터라고 할 수 있다. 나머지 파라미터에 대한 설명은 다음 문서에서 읽도록 하자.
Dropout layer : https://keras.io/api/layers/regularization_layers/dropout/
사실 교재 코드에 원래 이렇게 드롭아웃 계층도 있었는데, 내가 드롭아웃의 중요성을 직접 체감해보기 위해 빼버렸다.
드롭아웃 계층을 뺐을 때 과적합이 되는 것을 직접 확인했으니 이제 드롭아웃 계층을 다시 넣어서 모델을 학습시켜봤다.
드롭아웃 계층을 추가하니 훈련 정확도는 약 99%에서 88%로 떨어졌지만, 검증 정확도가 이전에 비해 약 3%p 정도나 증가하였다 (0.7445 → 0.7744). 모델 학습은 이미 알고 있는 데이터에 대한 정확도보다 처음 보는 데이터에 대한 정확도가 더 중요하기 때문에 훈련 정확도가 떨어진 것과는 상관 없이 검증 정확도가 올랐다면 개선에 성공한 것으로 볼 수 있다.
2트
그러면 이번에는 이미지 크기를 한 번 키워보기로 하자.
이미지 크기를 28×28로 했을 때는 이렇게 모자이크를 씌운 것처럼 사진이 선명하지 않았다. 만약에 너비와 높이를 각각 4배씩 늘려 112×112로 키운다면 사진이 어떻게 보일까?
사진이 뭔가 확실히 선명해졌다! 사진이 선명하면 모델 학습할 때도 뭔가 도움이 되지 않을까? 기대를 품고 학습을 진행시켜봤다
그런데 웬걸. 학습 시간은 더 오래 걸리는데 정확도는 오히려 더 떨어졌다. 컴퓨터 입장에서는 28×28 데이터가 학습하기 더 쉽나보다. 컴퓨터의 세계는 이해하기가 어렵다. 어쨌든 이미지 크기를 키워서 성능을 높이려는 시도는 실패한걸로...
3트
세 번째로 생각한 방법은 검증 데이터의 비율을 줄이는 것이다. 나는 검증 데이터의 비율을 0.3으로 정했는데, 사실 검증 데이터의 비율이 클수록 그만큼 학습에 사용할 데이터가 없어지게 된다. 따라서 검증 데이터의 비율을 기존의 절반인 0.15로 줄여보려고 한다.
검증 정확도가 가장 높을 때까지는 78.8%까지 올랐으나 마지막 에포크에서 아쉽게도 77.47%로 떨어졌다. 1트에서는 검증 정확도가 78%를 못 넘었던 것과는 달리 이번에는 중간중간에 78%를 넘은 부분도 있어서 성능 개선이 조금은 됐다고 볼 수 있겠지만, 그리 큰 효과는 보지 못한 것 같다.
4트
성능을 올리는 가장 확실한 방법 중 하나는 계층을 더 추가하는 것이다.
기존 모델에서는 합성곱 계층 2개, 풀링 계층 1개로 계층의 개수가 적은 편에 속한다.
여기에서는 합성곱 계층을 3개로 늘리고, 중간중간에 풀링 계층과 드롭아웃 계층을 더 추가해서 모델의 복잡도를 더 높이려고 한다.
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Dropout, Flatten, Dense, Rescaling
IMAGE_WIDTH = 28
IMAGE_HEIGHT = 28
IMAGE_SIZE = (IMAGE_WIDTH, IMAGE_HEIGHT)
IMAGE_CHANNELS = 3
model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS)))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
model.add(Conv2D(128, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.50))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss="binary_crossentropy", optimizer="adam", metrics=['accuracy'])
model.summary()
위와 같이 모델을 더 복잡하게 구성하였다. 그러면 이번엔 성능 개선이 됐을까?
이런 망할 성능이 더 떨어졌다 😢 무지성으로 복잡하게 한다고 성능이 좋아지는건 아닌가보다. 그럼 뭐 어떻게 해야됨... 내가 머신러닝을 괜히 어렵다 한게 아님
내가 너무 풀링이랑 드롭아웃을 너무 덕지덕지 껴붙였나? 싶어서 맥스풀링이랑 드롭아웃 계층 몇 개를 빼고 다시 학습시켜봤다.
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Dropout, Flatten, Dense, Rescaling
IMAGE_WIDTH = 28
IMAGE_HEIGHT = 28
IMAGE_SIZE = (IMAGE_WIDTH, IMAGE_HEIGHT)
IMAGE_CHANNELS = 3
model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS)))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(128, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.50))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss="binary_crossentropy", optimizer="adam", metrics=['accuracy'])
model.summary()
이 모델로 학습을 시킨 결과는 다음과 같다.
80% 넘길래 "오??" 했는데 마지막 에포크에서 76.91%로 떨어졌다. 이건 또 무슨 억까냐
근데 이제 뭔가 희망이 보이기 시작한다. 만약에 여기서 이미지 크기를 더 키우면 어떨까?
2트에서 이미지 크기를 키운다고 무조건 성능이 좋아지는건 아니라는걸 확인했지만, 맥스 풀링 계층이 하나 더 추가되었기 때문에 계층을 지날수록 데이터의 크기가 작아진다.
처음에 만든 모델에서 flatten 계층을 지나기 전 Output Shape는 (12, 12, 64)였다. 이를 평탄화하면 12×12×64 = 9,216 크기의 1D 데이터가 나온다.
그렇다면 맥스풀링을 하나 더 추가한 이 모델은 어떤가? 풀링 계층을 거칠때마다 데이터의 크기가 1/2로 줄어드는데, 풀링 계층을 2개로 만드니 데이터의 너비와 높이가 (5, 5)까지로 줄었다. 이를 평탄화 했을 때 1차원 데이터의 크기는 5×5×128 = 3,200으로 기존의 9,216에 비해 크게 줄었다. 이렇게 되면 학습이 효과적으로 이루어지지 않을 수 있다. 따라서 처음에 주어지는 이미지의 너비와 높이를 28×28에서 56×56으로 늘려보기로 했다.
Flatten 계층을 거치기 전 Output Shape가 (12, 12, 128)이 되어 평탄화 시켰을 때 1차원 데이터의 크기는 12×12×128 = 18,432가 되었다. 그 결과는 어떻게 되었을까?
정확도가 무려 84.67%까지 올랐다! 지금까지 시도해봤던 성능 개선 방법 중 가장 효과적이었다.
그러면 여기서 궁금증이 하나 생겼다. 4트 처음에 만들었던 모델도 이미지 크기를 키우면 학습이 잘 되지 않을까?
4트 처음에 만들었던 모델의 Output Shape를 다시 살펴보니 (1, 1, 128)까지 줄어들었다. 이게 어떻게 정확도가 75%나 나온건지 신기할 정도 😂
이 경우는 이미지 크기를 확실히 크게 주는게 좋을 것 같다. 이미지 사이즈를 기존의 4배씩인 112×112로 줘봤다.
86.72%라는 놀라운 성능 개선 결과를 보였다! 모델이 간단하면 이미지 사이즈를 작게 주는 것이 좋지만, 모델이 복잡할수록 이미지 사이를 크게 주는게 좋은 것 같다.
혹시나 해서 이미지 크기를 224×224로 늘려봤다 과연 성능이 더 향상될까?
시간은 더 오래걸려놓고 성능은 떨어졌다 😅 112×112로 학습했을 땐 한 에포크당 10초 정도 걸렸는데 224×224로 늘리니까 35초까지 늘어났다. 그리고서는 성능은 더 떨어졌다.
오늘의 교훈 : 욕심을 부리지 말자.
결론적으로, 최고의 성능을 뽑아낸 모델 코드는 다음과 같다.
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Dropout, Flatten, Dense, Rescaling
IMAGE_WIDTH = 112
IMAGE_HEIGHT = 112
IMAGE_SIZE = (IMAGE_WIDTH, IMAGE_HEIGHT)
IMAGE_CHANNELS = 3
model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS)))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
model.add(Conv2D(128, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.50))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss="binary_crossentropy", optimizer="adam", metrics=['accuracy'])
model.summary()
다음 포스팅의 목표는
1. 모델 저장하기
2. 모델의 작동 결과 실제로 확인하기
3. 모델 제출해보기
로 하자.
'머신러닝 수난기 (프로젝트)' 카테고리의 다른 글
Dogs vs. Cats (6) [PyTorch] (0) | 2025.02.25 |
---|---|
Dogs vs. Cats (5) (0) | 2025.02.23 |
Dogs vs. Cats (3) (0) | 2025.02.20 |
Dogs vs. Cats (2) (0) | 2025.02.19 |
Dogs vs. Cats (1) (0) | 2025.02.16 |