ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [DL 경진대회] 항공 사진 내 선인장 식별(2)
    ML.DL 2023. 1. 30. 17:27

    <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가 된다고 합니다.

     

     

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

     

Designed by Tistory.