포스트

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

conv 블록 3개짜리 CNN을 처음부터 학습해 baseline을 세웁니다. augmentation 유무에 따른 과적합 차이를 수치로 확인하고, 3천 장 규모에서 scratch 학습의 한계를 기록했습니다.

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

왜 scratch부터 하는가

바로 사전학습 모델을 쓰면 더 높은 점수가 나온다는 것은 이미 알고 있습니다. 그래도 scratch 학습을 먼저 하는 이유는, transfer learning의 효과를 숫자로 말하려면 비교 대상이 필요하기 때문입니다. 무작위 추측이 2.7%(1/37)인 문제에서 작은 CNN이 어디까지 가는지를 먼저 기록합니다.

모델: SmallCNN

딥러닝 기초 9편에서 다룬 conv, BatchNorm, ReLU, MaxPool 구성을 그대로 쌓은 3블록 CNN입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import torch.nn as nn

def conv_block(in_ch: int, out_ch: int) -> nn.Sequential:
    return nn.Sequential(
        nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1),
        nn.BatchNorm2d(out_ch),
        nn.ReLU(inplace=True),
        nn.MaxPool2d(2),
    )

class SmallCNN(nn.Module):
    def __init__(self, num_classes: int = 37):
        super().__init__()
        self.features = nn.Sequential(
            conv_block(3, 32),
            conv_block(32, 64),
            conv_block(64, 128),
        )
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(128, num_classes),
        )

    def forward(self, x):
        return self.head(self.features(x))

파라미터는 약 10만 개입니다. AdaptiveAvgPool2d(1)로 공간 차원을 평균 내기 때문에 입력 크기가 달라져도 동작하고, Flatten 뒤에 큰 Linear 층을 두는 것보다 파라미터가 훨씬 적습니다.

학습 루프

optimizer는 AdamW(learning rate 1e-3), 손실은 CrossEntropyLoss, 30 epoch 학습입니다. 루프 구조는 PyTorch 기초 3편에서 정리한 형태 그대로이고, epoch마다 validation accuracy를 재서 최고 기록일 때만 체크포인트를 저장합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
best_acc = 0.0
for epoch in range(30):
    model.train()
    for x, y in train_loader:
        x, y = x.to(device), y.to(device)
        optimizer.zero_grad()
        loss = criterion(model(x), y)
        loss.backward()
        optimizer.step()

    acc = evaluate(model, val_loader, device)
    if acc > best_acc:
        best_acc = acc
        torch.save(model.state_dict(), "outputs/small_cnn/best.pt")

마지막 epoch의 모델이 아니라 validation 기준 최고 모델을 남기는 것이 핵심입니다. 과적합이 시작된 뒤의 epoch는 저장되지 않습니다.

결과: augmentation이 가른 것

같은 모델을 augmentation 없이(Resize와 CenterCrop만) 한 번, 3편의 train_tf로 한 번 학습했습니다.

실험train accuracyvalidation accuracy
augmentation 없음99.2%31.8%
RandomResizedCrop + Flip78.5%41.3%

augmentation이 없으면 train은 사실상 전부 맞히는데 validation은 31.8%에 멈춥니다. 2,944장을 30번 반복해서 보는 동안 모델이 데이터를 외워버린 결과입니다. augmentation을 켜면 train accuracy는 오히려 내려가지만 validation은 9.5%p 올라갑니다. 딥러닝 기초 8편에서 정리한 과적합과 규제의 관계가 그대로 재현됩니다.

Baseline이 말해주는 것

최종 baseline은 validation accuracy 41.3%입니다. 무작위 추측 2.7%보다는 훨씬 높으니 모델이 뭔가 배우긴 했지만, 세 번에 두 번 가까이 틀리는 수준이라 분류기로 쓸 수는 없습니다.

원인은 모델 크기가 아니라 데이터 양입니다. 블록을 4개, 5개로 늘리고 dropout을 넣어봐도 validation은 45% 근처를 넘지 못했습니다. 품종 구분에 필요한 미세한 시각 특징(귀 모양, 털 결, 주둥이 길이)을 3천 장으로 처음부터 배우기에는 데이터가 부족합니다.

이 한계가 다음 편의 출발점입니다. ImageNet 120만 장으로 이미 시각 특징을 배워둔 모델을 가져오면 이 문제가 어떻게 달라지는지 확인합니다.

다음 글: 반려동물 품종 분류기 만들기 (5) Transfer learning: ResNet fine-tuning

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