본문 바로가기
ML.DL

[DL 경진대회] 항공 사진 내 선인장 식별(2)

by 권미정 2023. 1. 30.

<Musthave 머신러닝딥러닝 문제해결 전략> 11장을 실습한 내용입니다.


4. 성능 개선

지난 글에서는 간단한 CNN 모델을 사용해 베이스라인을 만들었습니다. 이번엔 다음 네 가지를 개선해서 성능을 높여 보겠습니다.

  1. 다양한 이미지 변환을 수행하기
  2. 더 깊은 CNN 모델을 만들기
  3. 더 뛰어난 옵티마이저를 사용하기
  4. 훈련 시 에폭 수를 늘리기

① 데이터 준비

먼저, 시드값 고정부터 '데이터 준비'의 '2. 데이터셋 클래스 정의'까지는 베이스라인과 똑같이 진행합니다.

 

이미지 변환기 정의

이미지를 변환하여 데이터 수를 늘리는 방식을 데이터 증강이라고 하는데, 이를 해 줄 이미지 변환기를 직접 정의해 보겠습니다. 성능을 개선하기 위해 다양한 이미지 변환기를 활용할 텐데, 훈련 데이터용과 검증 및 테스트 데이터용을 따로 만듭니다.

from torchvision import transforms # 이미지 변환을 위한 모듈

# 훈련 데이터용 변환기
transform_train = transforms.Compose([transforms.ToTensor(),
                                      transforms.Pad(32, padding_mode='symmetric'),
                                      transforms.RandomHorizontalFlip(),
                                      transforms.RandomVerticalFlip(),
                                      transforms.RandomRotation(10),
                                      transforms.Normalize((0.485, 0.456, 0.406),
                                                           (0.229, 0.224, 0.225))])

# 검증 및 테스트 데이터용 변환기
transform_test= transforms.Compose([transforms.ToTensor(),
                                    transforms.Pad(32, padding_mode='symmetric'),
                                    transforms.Normalize((0.485, 0.456, 0.406),
                                                         (0.229, 0.224, 0.225))])

transforms.Compose()로 여러 변환기들을 하나로 묶었습니다. 사용된 변환기들을 간단히 살펴보겠습니다.

  • transforms.ToTensor() : 이미지를 텐서 객체로 만듭니다.
  • transforms.Pad() : 이미지 주변에 패딩을 추가합니다.
  • transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip() : 각각 무작위로 이미지를 좌우, 상하 대칭 변환합니다.
  • transforms.RandomRotation() : 이미지를 회전시킵니다.
  • transforms.Normalize() : 데이터를 지정한 평균과 분산에 맞게 정규화해 줍니다.

 

데이터셋 및 데이터 로더 생성

ImageDataset 클래스로 훈련 및 검증 데이터셋을 만듭니다. 전달하는 변환기 빼고는 베이스라인 코드와 똑같습니다. 훈련 데이터셋을 만들 때는 훈련용 변환기를, 검증 데이터셋을 만들 때는 검증/테스트용 변환기를 전달합니다.

dataset_train = ImageDataset(df=train, img_dir='train/', transform=transform_train)
dataset_valid = ImageDataset(df=valid, img_dir='train/', transform=transform_test)

데이터 로더도 만들겠습니다.

from torch.utils.data import DataLoader # 데이터 로더 클래스

loader_train = DataLoader(dataset=dataset_train, batch_size=32, shuffle=True)
loader_valid = DataLoader(dataset=dataset_valid, batch_size=32, shuffle=False)

 

② 모델 생성

이번에는 더 깊은 CNN을 만들겠습니다. 신경망 계층이 깊어지면 대체로 예측력이 좋아집니다. 배치 정규화를 적용하고 활성화 함수를 Leacky ReLU로 바꿔서 성능을 높이겠습니다.

{합성곱, 배치 정규화, 최대 풀링} 계층이 총 5개에, 전결합 계층도 2개로 늘렸습니다.

import torch.nn as nn # 신경망 모듈
import torch.nn.functional as F # 신경망 모듈에서 자주 사용되는 함수

class Model(nn.Module):
    # 신경망 계층 정의
    def __init__(self):
        super().__init__() # 상속받은 nn.Module의 __init__() 메서드 호출
        # 1 ~ 5번째 {합성곱, 배치 정규화, 최대 풀링} 계층 
        self.layer1 = nn.Sequential(nn.Conv2d(in_channels=3, out_channels=32,
                                              kernel_size=3, padding=2),
                                    nn.BatchNorm2d(32), # 배치 정규화
                                    nn.LeakyReLU(), # LeakyReLU 활성화 함수
                                    nn.MaxPool2d(kernel_size=2))

        self.layer2 = nn.Sequential(nn.Conv2d(in_channels=32, out_channels=64,
                                              kernel_size=3, padding=2),
                                    nn.BatchNorm2d(64),
                                    nn.LeakyReLU(),
                                    nn.MaxPool2d(kernel_size=2))
        
        self.layer3 = nn.Sequential(nn.Conv2d(in_channels=64, out_channels=128,
                                              kernel_size=3, padding=2),
                                    nn.BatchNorm2d(128),
                                    nn.LeakyReLU(),
                                    nn.MaxPool2d(kernel_size=2))
        
        self.layer4 = nn.Sequential(nn.Conv2d(in_channels=128, out_channels=256,
                                              kernel_size=3, padding=2),
                                    nn.BatchNorm2d(256),
                                    nn.LeakyReLU(),
                                    nn.MaxPool2d(kernel_size=2))
        
        self.layer5 = nn.Sequential(nn.Conv2d(in_channels=256, out_channels=512,
                                              kernel_size=3, padding=2),
                                    nn.BatchNorm2d(512),
                                    nn.LeakyReLU(),
                                    nn.MaxPool2d(kernel_size=2))
        # 평균 풀링 계층 
        self.avg_pool = nn.AvgPool2d(kernel_size=4) 
        # 전결합 계층
        self.fc1 = nn.Linear(in_features=512 * 1 * 1, out_features=64)
        self.fc2 = nn.Linear(in_features=64, out_features=2)

    # 순전파 출력 정의 
    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.layer5(x)
        x = self.avg_pool(x)
        x = x.view(-1, 512 * 1 * 1) # 평탄화
        x = self.fc1(x)
        x = self.fc2(x)
        return x

마지막으로 방금 정의한 Model 클래스를 활용해 CNN 모델을 만든 뒤, 장비에 할당하겠습니다.

model = Model().to(device)

 

③ 모델 훈련

손실 함수와 옵티마이저 설정

손실 함수는 베이스라인과 같은 걸로 하겠습니다.

# 손실 함수
criterion = nn.CrossEntropyLoss()

옵티마이저는 Adamax로 바꿔 보겠습니다.

# 옵티마이저
optimizer = torch.optim.Adamax(model.parameters(), lr=0.00006)

 

모델 훈련

훈련을 더 많이 하기 위해 에폭 수를 10에서 70으로 늘리겠습니다. 나머지 코드는 베이스라인과 같습니다.

epochs = 70 # 총 에폭

# 총 에폭만큼 반복
for epoch in range(epochs):
    epoch_loss = 0 # 에폭별 손실값 초기화
    
    # '반복 횟수'만큼 반복 
    for images, labels in loader_train:
        # 이미지, 레이블 데이터 미니배치를 장비에 할당 
        images = images.to(device)
        labels = labels.to(device)
        
        # 옵티마이저 내 기울기 초기화
        optimizer.zero_grad()
        # 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
        outputs = model(images)
        # 손실 함수를 활용해 outputs와 labels의 손실값 계산
        loss = criterion(outputs, labels)
        # 현재 배치에서의 손실 추가
        epoch_loss += loss.item() 
        # 역전파 수행
        loss.backward()
        # 가중치 갱신
        optimizer.step()
        
    print(f'에폭 [{epoch+1}/{epochs}] - 손실값: {epoch_loss/len(loader_train):.4f}')

총 70 에폭만큼 훈련하며 손실값을 출력했습니다. (실행이 엄청 오래 걸리네요...)

 

④ 성능 검증

검증 데이터로 모델 성능을 평가하겠습니다. 이번에도 코드는 베이스라인과 같습니다.

from sklearn.metrics import roc_auc_score # ROC AUC 점수 계산 함수 임포트

# 실제값과 예측 확률값을 담을 리스트 초기화
true_list = []
preds_list = []

model.eval() # 모델을 평가 상태로 설정 

with torch.no_grad(): # 기울기 계산 비활성화
    for images, labels in loader_valid:
        # 이미지, 레이블 데이터 미니배치를 장비에 할당 
        images = images.to(device)
        labels = labels.to(device)
        
        # 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
        outputs = model(images)
        preds = torch.softmax(outputs.cpu(), dim=1)[:, 1] # 예측 확률값
        true = labels.cpu() # 실제값 
        # 예측 확률값과 실제값을 리스트에 추가
        preds_list.extend(preds)
        true_list.extend(true)
        
# 검증 데이터 ROC AUC 점수 계산 
print(f'검증 데이터 ROC AUC : {roc_auc_score(true_list, preds_list):.4f}')

베이스라인의 ROC AUC는 0.9902였는데, 성능 개선을 하니 0.9998이 되었네요! ROC AUC의 최댓값이 1이니 거의 완벽한 점수입니다.

 

⑤ 예측 및 결과 제출

이제 테스트 데이터로 예측해 볼 시간입니다! 이번에도 transform_test 변환기를 이용해 데이터셋을 만들었습니다.

dataset_test = ImageDataset(df=submission, img_dir='test/', 
                            transform=transform_test)
loader_test = DataLoader(dataset=dataset_test, batch_size=32, shuffle=False)

# 예측 수행
model.eval() # 모델을 평가 상태로 설정

preds = [] # 타깃 예측값 저장용 리스트 초기화

with torch.no_grad(): # 기울기 계산 비활성화
    for images, _ in loader_test:
        # 이미지 데이터 미니배치를 장비에 할당
        images = images.to(device)
        
        # 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
        outputs = model(images)
        # 타깃값이 1일 확률(예측값)
        preds_part = torch.softmax(outputs.cpu(), dim=1)[:, 1].tolist()
        # preds에 preds_part 이어붙이기
        preds.extend(preds_part)

 

제출 파일을 만듭니다.

submission['has_cactus'] = preds
submission.to_csv('submission.csv', index=False)

이미지 파일은 삭제합니다.

import shutil

shutil.rmtree('./train')
shutil.rmtree('./test')

 

커밋 후 제출해 봅시다.

최종 점수가 0.9998로 점수가 많이 올랐습니다!

 

점수를 더 높이는 방법

이번 장에서는 제공된 훈련 데이터를 9:1로 나눠 9 만큼만 모델을 훈련했습니다. 아래 캐글러의 코드를 참고해서, '훈련 데이터 전체'로 모델을 훈련시키면 최종 점수가 0.9999가 된다고 합니다.

 

 

첫 딥러닝 경진대회에 참가해 보았습니다. 난이도가 낮아 따라가기는 쉬웠지만, 머신러닝 경진대회와는 다르게 새로운 절차와 개념이 등장해서 재미있었습니다. 다음에는 조금 더 어려운 경진대회에 참여해 보겠습니다!

 

댓글