[kaggle] - 항공 사진 내 선인장 식별

Jeonghwan Kim·2022년 12월 3일
0

Intro

캐글의 항공 사진 내 선인장 식별 경진대회 'Aerial Cactus Identification' compeition에 참가해 딥러닝 모델을 다루는 방법을 연습해 보았다.
드론이 보호 구역을 돌아다니며 찍은 항공사진에서 딥러닝 기술로 선인장을 식별하는 작업을 해내야 한다.
csv파일 뿐 아니라 이미지 파일 또한 활용해 이미지 데이터에 선인장이 있을 확률을 예측해야 한다.


EDA

데이터 둘러보기

  • 데이터를 불러와 살펴본다.

    import pandas as pd
    
    # 데이터 경로
    data_path = '/kaggle/input/aerial-cactus-identification/'
    
    labels = pd.read_csv(data_path + 'train.csv')
    submission = pd.read_csv(data_path + 'sample_submission.csv')
    labels.head()

    • 이미지의 파일명과 선인장을 포함하는지 여부가 담겨있다.

데이터 시각화

  • 타깃값 분포

    • 파이그래프로 타깃값 분포를 살펴보면 1:3정도의 비율을 갖는 것을 알 수 있다.

      import matplotlib as mpl
      import matplotlib.pyplot as plt
      %matplotlib inline
      
      mpl.rc('font', size=15)
      plt.figure(figsize=(7, 7))
      
      label = ['Has cactus', 'Hasn\'t cactus'] # 타깃값 레이블
      # 타깃값 분포 파이 그래프
      plt.pie(labels['has_cactus'].value_counts(), labels=label, autopct='%.1f%%');

  • 이미지 출력

    • ZipFile 클래스를 활용해 압축파일을 푼다.

      from zipfile import ZipFile
      
      # 훈련 이미지 데이터 압축 풀기
      with ZipFile(data_path + 'train.zip') as zipper:
          zipper.extractall()
      
      # 테스트 이미지 데이터 압축 풀기
      with ZipFile(data_path + 'test.zip') as zipper:
          zipper.extractall()
    • os.listdr()로 디렉터리에 파일이 몇개나 들어있는지 알아본다.

      import os
      
      num_train = len(os.listdir('train/'))
      num_test = len(os.listdir('test/'))
      
      print(f'훈련 데이터 개수: {num_train}')
      print(f'테스트 데이터 개수: {num_test}')

    • OpenCV 라이브러리로 선인장을 포함하는 이미지 파일을 읽어온다. 길쭉한 물체인 선인장을 볼 수 있다.

      import matplotlib.gridspec as gridspec
      import cv2 # OpenCV 라이브러리 임포트
      
      mpl.rc('font', size=7)
      plt.figure(figsize=(15, 6))    # 전체 Figure 크기 설정
      grid = gridspec.GridSpec(2, 6) # 서브플롯 배치(2행 6열로 출력)
      
      # 선인장을 포함하는 이미지 파일명(마지막 12개) 
      last_has_cactus_img_name = labels[labels['has_cactus']==1]['id'][-12:]
      
      # 이미지 출력 
      for idx, img_name in enumerate(last_has_cactus_img_name):
          img_path = 'train/' + img_name                 # 이미지 파일 경로 
          image = cv2.imread(img_path)                   # 이미지 파일 읽기 
          image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 이미지 색상 보정 
          ax = plt.subplot(grid[idx])
          ax.imshow(image)                               # 이미지 출력 

    • 선인장을 포함하지 않는 이미지또한 불러와 형태를 살펴본다.

      plt.figure(figsize=(15, 6))    # 전체 Figure 크기 설정
      grid = gridspec.GridSpec(2, 6) # 서브플롯 배치
      
      # 선인장을 포함하지 않는 이미지 파일명(마지막 12개) 
      last_hasnt_cactus_img_name = labels[labels['has_cactus']==0]['id'][-12:]
      
      # 이미지 출력 
      for idx, img_name in enumerate(last_hasnt_cactus_img_name):
          img_path = 'train/' + img_name                 # 이미지 파일 경로
          image = cv2.imread(img_path)                   # 이미지 파일 읽기
          image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 이미지 색상 보정
          ax = plt.subplot(grid[idx])
          ax.imshow(image)                               # 이미지 출력 

    • image.shpae()으로 이미지 형상을 출력해보면 (32,32,3)의 결과를 얻을 수 있는데, 가로, 세로 크기가 32 x 32, 채널 수가 3개인 것을 알 수 있다. R,G,B로 이루어진 컬러 이미지이기에 채널이 3개이다.


Baseline Model

시드값 고정 및 GPU 장비 설정 -> 데이터 준비 (훈련/검증 데이터 분리, 데이터셋 클래스 정의, 데이터셋 생성, 데이터 로더 생성) -> 모델 생성 -> 모델 훈련 (손실함수, 옵티마이저 설정, 모델 훈련) -> 성능 검증 -> 예측 및 제출
의 순서로 진행한다.

시드값 고정 및 GPU 장비 설정

  • pytorch를 import하고 시드값을 고정한다. 머신러닝에서의 random_state와 같은 역할로, pytorch 딥러닝 모델링에서는 맨 처음에 고정한다.

    • 결과를 재현할 필요가 없다면 시드값 고장은 생략하는게 좋을 수 있는데, 속도가 느려지고 예측 성능도 떨어질 수 있기 때문이다.
    import torch # 파이토치 
    import random
    import numpy as np
    import os
    
    # 시드값 고정
    seed = 50
    os.environ['PYTHONHASHSEED'] = str(seed)
    random.seed(seed)                # 파이썬 난수 생성기 시드 고정
    np.random.seed(seed)             # 넘파이 난수 생성기 시드 고정
    torch.manual_seed(seed)          # 파이토치 난수 생성기 시드 고정 (CPU 사용 시)
    torch.cuda.manual_seed(seed)     # 파이토치 난수 생성기 시드 고정 (GPU 사용 시)
    torch.cuda.manual_seed_all(seed) # 파이토치 난수 생성기 시드 고정 (멀티GPU 사용 시)
    torch.backends.cudnn.deterministic = True # 확정적 연산 사용
    torch.backends.cudnn.benchmark = False    # 벤치마크 기능 해제
    torch.backends.cudnn.enabled = False      # cudnn 사용 해제
  • GPU 장비 설정

데이터 준비

  • 훈련, 검증 데이터 분리

    from sklearn.model_selection import train_test_split
    
    # 훈련 데이터, 검증 데이터 분리
    train, valid = train_test_split(labels, 
                                    test_size=0.1,
                                    stratify=labels['has_cactus'],
                                    random_state=50)
    • train_test_split()을 이용해 훈련, 검증 데이터를 분리한다.
  • 데이터셋 클래스 정의

    import cv2 # OpenCV 라이브러리
    from torch.utils.data import Dataset # 데이터 생성을 위한 클래스
    
    class ImageDataset(Dataset):
        # 초기화 메서드(생성자)
        def __init__(self, df, img_dir='./', transform=None):
            super().__init__() # 상속받은 Dataset의 생성자 호출
            # 전달받은 인수들 저장
            self.df = df
            self.img_dir = img_dir
            self.transform = transform
    
        # 데이터셋 크기 반환 메서드 
        def __len__(self):
            return len(self.df)
    
        # 인덱스(idx)에 해당하는 데이터 반환 메서드 
        def __getitem__(self, idx):
            img_id = self.df.iloc[idx, 0]    # 이미지 ID
            img_path = self.img_dir + img_id # 이미지 파일 경로 
            image = cv2.imread(img_path)     # 이미지 파일 읽기 
            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 이미지 색상 보정
            label = self.df.iloc[idx, 1]     # 이미지 레이블(타깃값)
    
            if self.transform is not None:
                image = self.transform(image) # 변환기가 있다면 이미지 변환
            return image, label
    • Dataset 클래스를 활용해 데이터셋 객체를 만든다. 데이터셋의 크기를 반환하는 메서드와 인덱스에 해당하는 데이터를 반환해주는 메서드를 만들어준다.
  • 데이터셋 생성

    • ImageDataset 클래스를 이용해 데이터셋을 만드는데, pytorch 모델로 이미지를 다루려면 이미지 데이터를 Tensor 타입으로 바꾸어야 한다.

      from torchvision import transforms # 이미지 변환을 위한 모듈
      
      transform = transforms.ToTensor()
      • torchvision은 pytorch용 CV 라이브러리이고, transforms는 다양한 이미지 변환기를 제공하는 모듈이다.
      • 이미지를 Teonsor로 바꾸면 (가로 픽셀 수, 세로 픽셀 수, 채널 수)의 형태가 (채널 수, 가로 픽셀 수, 세로 픽셀 수)로 변한다. 배치가 추가되면 맨 앞에 배치 크기가 들어간다.
    • 앞에서 정의한 ImageDataset() 클래스를 사용해 훈련, 검증 데이터셋을 만든다.

      dataset_train = ImageDataset(df=train, img_dir='train/', transform=transform)
      dataset_valid = ImageDataset(df=valid, img_dir='train/', transform=transform)
      • df 파라미터에 데이터를 전달하고, img_dir에 이미지 데이터의 경로, transform에는 방금 만든 transform 변환기를 전달한다.
  • 데이터 로더 생성

    • 데이터 로더는 지정한 배치 크기만큼 데이터를 불러오는 개체로, 딥러닝 모델을 훈련할 때는 주로 배치 단위로 데이터를 가져와 훈련한다.

      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)
      • 배치 크기는 2의 제곱수로 설정하는게 효율적이다. 배치 크기가 작으면 규제 효과가 있어 일반화 성능이 좋아지지만 훈련 시간이 길어진다.

모델 생성

  • CNN(합성곱 신경망) 모델을 만드는데, nn.Module을 상속해 정의하고 순전파 후 결과를 반환하는 forward()를 재정의한다.

    import torch.nn as nn # 신경망 모듈
    import torch.nn.functional as F # 신경망 모듈에서 자주 사용되는 함수
    
    class Model(nn.Module):
        # 신경망 계층 정의 
        def __init__(self):
            super().__init__() # 상속받은 nn.Module의 __init__() 메서드 호출
    
            # 첫 번째 합성곱 계층 
            self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, 
                                   kernel_size=3, padding=2) 
            # 두 번째 합성곱 계층 
            self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, 
                                   kernel_size=3, padding=2) 
            # 최대 풀링 계층 
            self.max_pool = nn.MaxPool2d(kernel_size=2) 
            # 평균 풀링 계층 
            self.avg_pool = nn.AvgPool2d(kernel_size=2) 
            # 전결합 계층 
            self.fc = nn.Linear(in_features=64 * 4 * 4, out_features=2)
    
        # 순전파 출력 정의 
        def forward(self, x):
            x = self.max_pool(F.relu(self.conv1(x)))
            x = self.max_pool(F.relu(self.conv2(x)))
            x = self.avg_pool(x)
            x = x.view(-1, 64 * 4 * 4) # 평탄화
            x = self.fc(x)
            return x
    • __init__()에서 모델에서 쓸 신경망 계층을 정의하고 forward()에서 조합해 모델을 완성한다.
    • nn.Conv2D()로 합성곱 계층을 만들 때 stride를 명시하지 않으면 기본값인 1이 사용되고, 입력 채널 수는 앞 계층의 출력 채널 수와 같아야 한다.
    • 풀링 계층에서는 stride를 명시하지 않으면 stride 크기와 풀링 크기가 동일하게 설정된다.
  • 정의한 Model 클래스로 CNN 모델을 생성하여 device 장비에 할당해주고, model을 출력하면 모델의 전체 구조를 볼 수 있다.

    model = Model().to(device)
    
    model

모델 훈련

  • 손실 함수로 CrossEntropy를 설정해준다.

    # 손실함수
    criterion = nn.CrossEntropyLoss()
  • 최적 가중치를 찾아주는 옵티마이저로는 SGD를 사용해준다.

    # 옵티마이저
    optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
  • 모델 훈련 절차

    1. 데이터 로더에서 배치 크기만큼 데이터를 불러온다.
    2. 불러온 이미지 데이터와 레이블(타깃값) 데이터를 장비에 할당한다.
    3. 옵티마이저 내 기울기를 초기화한다.
    4. 신경망 모델에 입력 데이터(이미지)를 전달해 순전파하여 출력값(예측값)을 구한다.
    5. 예측값과 실제 레이블(타깃값)을 비교해 손실을 계산한다.
    6. 손실을 기반으로 역전파를 수행한다.
    7. 역전파로 구한 기울기를 활용해 가중치를 갱신한다.
    8. 1~7 절차를 반복횟수만큼 되풀이한다.
    9. 1~8 절차를 에폭만큼 반복한다.
    epochs = 10 # 총 에폭
    # 총 에폭만큼 반복
    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}')

성능 검증

  • 훈련이 끝난 후 valid data로 평가지표인 ROC AUC 값을 구한다.

    from sklearn.metrics import roc_auc_score # ROC AUC 점수 계산 함수 임포트
    
    # 실제값과 예측 확률값을 담을 리스트 초기화
    true_list = []
    preds_list = []
    • 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}')

    • 예측 확률을 구하는 outputs.cpu()와 labels.cpu()는 이전에 GPU에 할당했던 outputs, labels 데이터를 다시 CPU에 할당하는 것이다. roc_auc_score()는 pytorch가 아니라 scikitlearn 함수라 gpu에 있는 데이터를 직접 사용하지 못해 바꾸어주는 것이다.

예측

  • test data를 담은 데이터셋과 데이터 로더를 만들어준다. 배치크기는 32로 한다.

    dataset_test = ImageDataset(df=submission, img_dir='test/', transform=transform)
    loader_test = DataLoader(dataset=dataset_test, batch_size=32, shuffle=False)
  • test data에서 타깃값이 1일 확률을 예측해 본다. 모델 성능을 검증하는 코드와 비슷하지만 test data에는 타깃값이 없어 for 문에 labels 변수를 할당하지 않았다. tolist()를 호출해 tensor를 list 타입으로 변경해 최종 제출 할 수 있게끔 해준다.

    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)
  • 커밋 후 제출하면 0.9837점 정도로 낮은 등수를 기록하기에 성능을 개선하도록한다.


성능 개선

  • 다양한 이미지 변환 수행, 깊은 CNN 모델, 뛰어난 옵티마이저 사용, 에폭 수 증가 등을 통해 성능을 개선시킬 수 있다.

  • 데이터셋 클래스를 정의하는 부분까지 Baseline의 코드와 같고, 이미지 변환기를 정의하는 부분부터 다르다.

  • 이미지의 변환을 통해 데이터 수를 늘리는 '데이터 증강(data augmentation)'을 해준다.

  • Pad(), RandomHorizontalFlip(), RandomRotation() 등 다양한 변환기를 Compose()로 묶어 하나의 변환기처럼 사용한다.

  • 다양한 이미지 변환기를 활용할 때 훈련 데이터용과 검증 및 테스트 데이터용은 따로 만드는데, 훈련 시에는 다양한 변환을 적용하는 것이 좋지만 검증 및 테스트 시에는 원본 이미지와 너무 달라지면 예측이 어려워지기 때문이다.

    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()로 이미지를 Tensor 객체로 만든다. 다른 transforms 변환기들이 Tensor 객체를 입력받으므로 가장 앞에 둔다.
    • transforms.Pad()로 이미지 주변에 패딩을 추가한다. padding_mode = 'symmetric'은 패딩을 추가할 때 상하좌우 대칭이 되는 모양으로 만들어주는 것이다.
    • transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip()은 각각 이미지를 무작위로 좌우, 상하 대칭 변환한다. 변환할 이미지의 비율을 설정할 수 있는데, 기본값은 0.5로 전체 이미지 중 50%를 무작위로 뽑아 대칭 변환하는 것이다.
    • transforms.RandomRotation()으로 이미지를 회전 시킨다. 파라미터에 10을 전달하면 -10~10도 사이의 값만큼 무작위로 회전한다.
    • transforms.Normalize()는 데이터를 지정한 평균과 분산에 맞게 정규화해준다. 0~1사이 값으로 설정해준다. 이 문제에서 이미지 데이터는 R,G,B로 구성이 되어 있으므로 각각 정규화해야하기에 평균과 분산에 값을 세 개씩 전달한다.
  • 데이터셋 및 데이터 로더를 생성해준다.

    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)
    • 앞의 데이터셋에서 이미지 변환기를 전달했기에 데이터 로더로 데이터를 불러올 때마다 이미지 변환을 수행한다. 따라서 원본 이미지는 같지만 에폭마다 서로 다른 이미지로 훈련하는 효과를 얻을 수 있다.

모델 생성

  • 모델의 예측력을 높이기 위해 신경망 계층을 Baseline 보다 더 깊게 만들어주고, 배치 정규화를 적용하고 활성화 함수를 LeakyRelu로 바꾸어준다.

    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
    • nn.BatchNorm2D()로 배치 정규화(Batch Normalization)를 해주었다. 파라미터로 채널 수를 전달한다.
    • LeakyReLu()로 활성화 함수를 설정한다.
    • 전결합 계층은 2개 정의해주었다.
  • 정의한 Model 클래스를 활용해 CNN 모델을 만든 뒤 device에 할당한다.

    model = Model().to(device)

모델 훈련

  • 손실 함수와 옵티마이저를 설정해준다.

    # 손실 함수
    criterion = nn.CrossEntropyLoss()
    # 옵티마이저
    optimizer = torch.optim.Adamax(model.parameters(), lr=0.00006)
    • 손실 함수는 Baseline과 같이 CrossEntropy로 설정해주고, 옵티마이저는 Adamax를 사용한다. 배치 크기가 줄어들수록 학습률은 작게 설정해야 한다.
  • 데이터를 증강해 훈련할 데이터가 많아졌으니 에폭을 늘려준다.

    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}')    

성능 검증

  • 검증 데이터로 모델 성능을 평가한다. Baseline 코드와 같다.

    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로 상승했다. 1에 가까우니 거의 완벽히 분류해낸 것이다.

예측 및 결과 제출

  • transform_test 변환기를 이용해 데이터셋을 만들고 test data로 예측한다.

    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로 1221명 중 455등으로 상위 37% 정도를 기록했다.


최종

  • 성능을 조금 더 높이기 위해서는 훈련 데이터를 9:1로 나눠 9로만 모델을 훈련하고 1은 검증용으로 쓰고 훈련에 사용하지 않는 방법을 사용하면 된다. 이렇게 하면 성능을 매우 개선하여 최종 점수 0.9999를 기록할 수 있다.

  • pytorch를 활용해 딥러닝 모델을 구축하는 연습을 할 수 있었고, 이미지 변환, 옵티마이저 등을 다뤄볼 수 있었다.

  • github에 해당 코드를 올려두었다.

참고: 머신러닝·딥러닝 문제해결 전략 (캐글 수상작 리팩터링으로 배우는 문제해결 프로세스와 전략)

참고: https://www.kaggle.com/code/bonhart/simple-cnn-on-pytorch-for-beginers/notebook

0개의 댓글