포스트

반려동물 품종 분류기 만들기 (3) 데이터 준비와 EDA

torchvision으로 Oxford-IIIT Pet을 내려받아 클래스 분포와 이미지 크기를 확인하고, train과 validation에 서로 다른 transform을 적용하는 분할 방법과 DataLoader 구성을 정리했습니다.

반려동물 품종 분류기 만들기 (3) 데이터 준비와 EDA

데이터 다운로드

Oxford-IIIT Pet은 torchvision에 내장되어 있어서 다운로드가 코드 한 줄입니다.

1
2
3
4
5
6
7
8
from torchvision.datasets import OxfordIIITPet

raw = OxfordIIITPet(root="data", split="trainval",
                    target_types="category", download=True)

print(len(raw))          # 3680
print(len(raw.classes))  # 37
print(raw.classes[:3])   # ['Abyssinian', 'American Bulldog', 'American Pit Bull Terrier']

split은 trainval(3,680장)과 test(3,669장) 두 가지입니다. 1편의 원칙대로 test는 6편의 최종 평가 전까지 건드리지 않고, trainval을 다시 train과 validation으로 나눠 사용합니다.

EDA: 학습 전에 확인한 것

모델링 전에 확인한 것은 두 가지, 클래스 분포와 이미지 크기입니다.

1
2
3
4
from collections import Counter

counts = Counter(raw._labels)
print(min(counts.values()), max(counts.values()))  # 93 100

클래스당 93장에서 100장 사이로 거의 균등합니다. 클래스 불균형 처리(가중치, 오버샘플링)가 필요 없고, accuracy를 기본 지표로 써도 됩니다.

이미지 크기는 제각각입니다. 몇 장을 열어보면 300에서 500px 사이가 대부분이고, 가로세로 비율도 제멋대로입니다. CNN은 고정 크기 입력을 받으므로 전처리에서 224x224로 통일합니다. 224는 ImageNet 사전학습 모델의 표준 입력 크기라서, 5편의 fine-tuning과 같은 조건을 쓰기 위해 baseline부터 이 크기로 고정합니다.

Transform: train과 evaluation을 다르게

train에는 augmentation을 넣고, validation과 test는 결정적 전처리만 씁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from torchvision import transforms

IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]

train_tf = transforms.Compose([
    transforms.RandomResizedCrop(224, scale=(0.6, 1.0)),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])

eval_tf = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])
  • RandomResizedCrop은 매 epoch 다른 영역을 잘라 데이터가 사실상 늘어나는 효과를 냅니다. 3,000장 규모에서는 필수에 가깝고, 효과는 4편에서 수치로 확인합니다
  • RandomHorizontalFlip은 좌우 반전입니다. 동물 사진은 좌우가 뒤집혀도 같은 품종이므로 안전한 augmentation입니다
  • normalize 값은 ImageNet 통계를 사용합니다. 사전학습 모델이 이 통계로 학습되었기 때문이고, baseline도 같은 값으로 통일해 조건을 맞춥니다

분할의 함정: transform이 다른 두 Dataset

trainval을 8:2로 나눌 때 주의할 점이 있습니다. random_split으로 하나의 Dataset을 나누면 train과 validation이 같은 transform을 공유하게 되어, validation에도 augmentation이 적용됩니다. 그러면 validation 점수가 epoch마다 흔들려서 모델 선택 기준으로 쓰기 어렵습니다.

해결은 같은 데이터를 transform만 다르게 두 번 열고, 동일한 인덱스로 나누는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
from torch.utils.data import Subset

train_full = OxfordIIITPet(root="data", split="trainval",
                           target_types="category", transform=train_tf)
val_full = OxfordIIITPet(root="data", split="trainval",
                         target_types="category", transform=eval_tf)

g = torch.Generator().manual_seed(42)
perm = torch.randperm(len(train_full), generator=g)
n_val = int(len(train_full) * 0.2)

train_ds = Subset(train_full, perm[n_val:].tolist())
val_ds = Subset(val_full, perm[:n_val].tolist())

print(len(train_ds), len(val_ds))  # 2944 736

generator의 seed가 같으면 인덱스 순열이 같으므로 두 Subset이 겹치지 않습니다. 클래스가 균등한 데이터라 stratified 분할 없이 무작위 분할로 충분합니다.

DataLoader

1
2
3
4
5
6
from torch.utils.data import DataLoader

train_loader = DataLoader(train_ds, batch_size=64, shuffle=True,
                          num_workers=2, pin_memory=True)
val_loader = DataLoader(val_ds, batch_size=64, shuffle=False,
                        num_workers=2, pin_memory=True)

batch size 64는 224x224 입력 기준 Colab T4(16GB)에서 여유 있게 돌아가는 크기입니다. validation은 순서가 결과에 영향을 주지 않으므로 shuffle을 끕니다.

마지막으로 DataLoader에서 배치 하나를 꺼내 shape과 이미지를 눈으로 확인합니다. normalize된 텐서는 그대로 그리면 색이 깨져 보이므로, 시각화할 때만 mean과 std를 역으로 적용해 되돌립니다. 여기서 라벨과 이미지가 맞게 붙어 있는지, augmentation이 과하지 않은지를 확인하고 넘어갑니다.

다음 글에서는 이 DataLoader로 작은 CNN을 처음부터 학습해 baseline을 세웁니다.

다음 글: 반려동물 품종 분류기 만들기 (4) Baseline: CNN을 처음부터 학습

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.