본문 바로가기
ML,DL

[머신러닝/딥러닝] 병든 잎사귀 식별 경진대회: 베이스라인 모델 및 성능개선

by 공부기 2023. 2. 5.

 

 

https://gongboogi.tistory.com/12

 

[머신러닝/딥러닝] 병든 잎사귀 식별 경진대회: 분석정리 및 시각화

이번 캐글 경진 대회는 병든 사과나무 잎사귀를 식별하는 다중분류 문제를 풀어보았다. Plant Pathology 2020 - FGVC7 https://www.kaggle.com/competitions/plant-pathology-2020-fgvc7 Plant Pathology 2020 - FGVC7 | Kaggle www.kaggl

gongboogi.tistory.com

 

 

이전 글에서 캐글의 병든 잎사귀 식별 경진대회의 데이터를 분석해보고 시각화해보았다. 이번에는 베이스라인 모델을 만들어 보자.

 

 

<참고 코드>

https://www.kaggle.com/code/akasharidas/plant-pathology-2020-in-pytorch/notebook

 

Plant Pathology 2020 in PyTorch

Explore and run machine learning code with Kaggle Notebooks | Using data from multiple data sources

www.kaggle.com

 

 

 

모델 생성 과정에서 특징은 다음과 같다.

 

- 데이터 준비 과정에서 이미지 변환기로 데이터를 증강

- 사전 훈련된 모델 (EfficientNet)을 사용해 전이 학습 진행

- 모델 훈련과 성능 검증을 동시에 진행하면서 훈련을 반복. (훈련을 반복하면서 중간중간 성능을 체크)

 

 

 

 

 

 

1. 시드값 고정 및 GPU 장비 설정

 

시드값 고정

 

먼저 파이토치를 임포트 하고 시드값을 고정한다. 시드값을 고정하는 이유는 다시 실행해도 같은 결과를 얻기 위함이다.

 

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)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.enabled = False

 

 

 

GPU 장비 설정

 

이어서 장비를 설정한다. 이번 대회와 같은 비정형 데이터를 모델링 하려면 연산량이 많아지므로 CPU를 사용하면 훈련 시간이 길어지는데, 다행히 캐글에서 GPU 환경을 제공한다.

 

캐글에서 GPU를 사용하려면 설정을 바꿔줘야 한다. 오른쪽 Setting 탭에서 Accelerator을 GPU로 바꿔준다. 기본값인 None은 CPU를 사용한다는 뜻이다.

 

 

* Accelerator를 변경하면 코드 환경 전체가 초기화 되므로 코드를 처음부터 다시 실행해야한다는 점을 유의하자.

 

 

이어서 다음 코드를 실행한다.

 

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

device

 

Accelerator를 GPU로 바꾸었기 때문데 device 변수에 CUDA가 할당 되어 있다.

 

 

 

 

 

2. 데이터 준비

 

데이터를 불러오고 훈련 데이터와 검증 데이터를 분리한다.

 

import pandas as pd

# 데이터 경로
data_path = '/kaggle/input/plant-pathology-2020-fgvc7/'

train = pd.read_csv(data_path + 'train.csv')
test = pd.read_csv(data_path + 'test.csv')
submission = pd.read_csv(data_path + 'sample_submission.csv')

 

 

 

훈련 데이터, 검증 데이터 분리

 

from sklearn.model_selection import train_test_split

# 훈련 데이터, 검증 데이터 분리
train, valid = train_test_split(train, 
                                test_size=0.1,
                                stratify=train[['healthy', 'multiple_diseases', 'rust', 'scab']],
                                random_state=50)

 

train_test_split를 사용해 전체 훈련 데이터인 train을 훈련 데이터와 검증 데이터로 분리했다. 

 

  •  test_size: 전체 데이터 세트에서 test(valid) 데이터 의 비율
  •  stratify: 데이터 클래스 분포 비율을 맞춰줌. 칼럼을 넣어주면 된다.

 

위 코드에서는 검증 데이터 비율을 0.1로 주었고,

타깃값이 골고루 분포되도록 분리하기 위해서 stratify 파라미터에 4개의 타깃값을 전달했다.

이는 원래 전체 훈련 데이터의 각 클래스 비율이 분리된 train 데이터와 valid데이터의 클래스 비율에도 적용되는 것이다. 

 

 

 

 

 

데이터셋 클래스 정의

 

 

import cv2
from torch.utils.data import Dataset # 데이터 생성을 위한 클래스
import numpy as np

class ImageDataset(Dataset):
    # 초기화 메서드(생성자)
    # 1.
    def __init__(self, df, img_dir='./', transform=None, is_test=False):
        super().__init__() # 상속받은 Dataset의 __init__() 메서드 호출
        # 전달받은 인수 저장
        self.df = df
        self.img_dir = img_dir
        self.transform = transform
        self.is_test = is_test
    
    # 데이터셋 크기 반환 메서드 
    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 + '.jpg' # 2. 이미지 파일 경로
        image = cv2.imread(img_path)              # 이미지 파일 읽기
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 이미지 색상 보정
        # 이미지 변환 
        if self.transform is not None:
            image = self.transform(image=image)['image']  # 3.
        # 테스트 데이터면 이미지 데이터만 반환, 그렇지 않으면 타깃값도 반환 
        if self.is_test:  # 4.
            return image # 5. 테스트용일 때
        else:
            # 타깃값 4개 중 가장 큰 값의 인덱스  # 6.
            label = np.argmax(self.df.iloc[idx, 1:5]) 
            return image, label # 훈련/검증용일 때  # 7.

 

파이토치로 신경망 모델을 구축하려면 데이터셋도 일정한 형식에 맞게 정의해줘야 한다.

파이토치에서 제공하는 Dataset 클래스를 활용해서 데이터셋 객체를 만들 수 있다. Dataset은 추상클래스로, Dataset을 상속받은 특수 메서드인 __len__()__getitem__()을 재정의(오버라이딩) 해야한다.

 

  • __init__(): imageDataset 클래스의 초기화 메서드

        - df: DataFrame 객체로 train 혹은 valid를 df 파라미터에 전달

        - img_dir: 이미지 데이터를 포함하는 경로

        - transform: 이미지 변환기. 이미지 셋을 만들 때 기본적인 전처리를 하기 위해 변환기를 넘겨줌

  • __len__(): 데이터셋 크기를 반환
  • __getitem__(): 인덱스를 전달받아 인덱스에 해당하는 데이터를 반환

 

 

 

1. 데이터셋을 테스트용으로 만들려면 초기화 메서드의 is_test 파라미터에 True를, 훈련이나 검증용 데이터를 만들려면 False를 전달한다.

 

__getitem__() 메서드로 데이터를 가져올 때 생성 시 지정한 4. is_test 값을 확인해서, 5. 테스트 데이터용이라면 타깃값이 없으므로 이미지 데이터만 반환하고, 훈련/검증용이라면 타깃값도 함께 반환한다.

 

2. ID에 확장자가 포함되어 있지 않으므로 이미지 파일 경로 끝에 파일 확장자(.jpg)를 붙였다. 

 

6. 훈련 혹은 검증용일 경우 타깃값은 4가지 중 가장 큰 값의 인덱스를 label에 할당한다. (healthy: 0, multiple_diseases: 1, rust: 2, scab: 3)  *argmax: 가장 높은 값의 인덱스를 반환

 

 

 

 

 

 

 

이미지 변환기 정의

 

데이터 증강용 이미지 변환기를 정의한다. 이번에는 albumentations가 제공하는 이미지 변환기를 사용할 것이다.

 

 

# 이미지 변환을 위한 모듈
import albumentations as A
from albumentations.pytorch import ToTensorV2

 

albumentations 모듈을 임포트하고 '훈련 데이터용' 변환기부터 정의한 후 이어서 '검증 및 테스트 데이터용'을 정의해보겠다.

 

 

훈련 데이터용 변환기

# 훈련 데이터용 변환기
transform_train = A.Compose([
    A.Resize(450, 650),       # 1. 이미지 크기 조절 
    A.RandomBrightnessContrast(brightness_limit=0.2, # 2. 밝기 대비 조절
                               contrast_limit=0.2, p=0.3),
    A.VerticalFlip(p = 0.2),    # 상하 대칭 변환
    A.HorizontalFlip(p = 0.5),  # 좌우 대칭 변환 
    A.ShiftScaleRotate(       # 3. 이동, 스케일링, 회전 변환
        shift_limit = 0.1,
        scale_limit = 0.2,
        rotate_limit = 30, p = 0.3),
    A.OneOf([A.Emboss(p = 1),   # 4. 양각화, 날카로움, 블러 효과
             A.Sharpen(p = 1),
             A.Blur(p = 1)], p = 0.3),
    A.PiecewiseAffine(p = 0.3), # 5. 어파인 변환 
    A.Normalize(),            # 6. 정규화 변환 
    ToTensorV2()              # 7. 텐서로 변환
])

 

변환기마다 파라미터가 다양하다. 자세한 사항은 albumentations 공식 문서를 참고하여 더 다양한 변환기를 만들 수 있다.

(https://github.com/albumentations-team/albumentations)

 

 

 

1. Resize: 이미지 크기를 조절하는 변환기. 임의로 450x650 크기로 조절했다. 첫 번째 파라미터가 높이고, 두 번째가 너비다. 

 

2. RandomBrightnessContrast: 이미지의 밝기와 대비를 조절하는 변환기. 각 파라미터의 의미는 다음과 같다.

  - brightness_limit: 이미지 밝기 조절값을 설정. 0.2로 지정하였으므로 -0.2 ~ 0.2 범위의 밝기 조절자를 갖는다. 전체 범위는 -1~1로 -1이면 어둡게(검은색), 1이면 밝게(흰색) 변한다. 

  - contrast_limit: 이미지 대비 조절값을 설정한다. 동작 방식은 brightness_limit와 같다.

  - p: 적용 확률 설정. 0.3을 전달하였으므로 30%의 확률로 변환기를 적용한다는 의미다.

 

3. ShiftScaleRotate: 이동, 스케일링, 회전 변환기. 

- shift_limit: 이동 조절값  (0.1  -> -0.1~0.1)

- scale_limit: 스케일링 조절값  (0.2  -> -0.2~0.2)

- rotate_limit: 회전 각도 조절값  (30  -> -30~30)

 

4. Emboss(양각화 효과) / Sharpen(날카롭게 만드는 효과) / Blur(블러 효과) 중에서 한 개를 선택 적용.

 

5. PiecewiseAffine: 어파인 변환기. 어파인 변환이란 이동, 확대/축소, 회전 등으로 이미지 모양을 전체적으로 바꾸는 변환.

 

6. Normalize: 값을 정규화 한다.

 

7. ToTensorV2(): 이미지 데이터를 텐서 형식으로 변환.

 

 

 

 

 

 

검증 및 테스트 데이터용 변환기

 

# 검증 및 테스트 데이터용 변환기
transform_test = A.Compose([
    A.Resize(450, 650), # 이미지 크기 조절 
    A.Normalize(),      # 정규화 변환
    ToTensorV2()        # 텐서로 변환
])

 

크기는 훈련 데이터와 똑같이 맞추는 게 좋으므로 450, 650으로 설정했다. 픽셀값 범위도 비슷해야 서로 비교하기 쉬우므로 정규화 해주고, 마지막으로 파이토치는 텐서 객체만 취급하기 때문에 ToTensorV2() 변환기가 꼭 필요하다.

 

 

 

 

 

 

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

 

데이터 준비의 마지막 단계로, 데이터셋을 정의한다.

 

img_dir = '/kaggle/input/plant-pathology-2020-fgvc7/images/'

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

 

훈련 데이터 셋을 만들 때는 훈련용 변환기를, 검증 데이터셋을 만들 때는 검증/테스트용 변환기를 전달한다.

 

 

 

def seed_worker(worker_id):   #데이터 로더 시드값 고정 함수
    worker_seed = torch.initial_seed() % 2**32
    np.random.seed(worker_seed)
    random.seed(worker_seed)
    
g = torch.Generator()  # 제너레이터 생성
g.manual_seed(0)  # 제너레이터 시드값 고정

 

모델 훈련 시간이 오래 걸리기 때문에 멀티프로세싱을 활용해보았다.

멀티프로세싱을 사용하려면 위와 같이 시드값을 고정해야 해서 seed_worker()을 정의하고 제너레이터를 생성해주었다.

데이터 로더로 데이터를 불러올 때 시드값을 고정하기 위해 필요한 코드로 보면 된다.

 

 

 

이어서 데이터 로더도 생성한다.

 

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

batch_size = 4

loader_train = DataLoader(dataset_train, batch_size=batch_size, 
                          shuffle=True, worker_init_fn=seed_worker,
                          generator=g, num_workers=2)
loader_valid = DataLoader(dataset_valid, batch_size=batch_size, 
                          shuffle=False, worker_init_fn=seed_worker,
                          generator=g, num_workers=2)

 

데이터 로더는 torch,utils.data의 DataLoader 클래스로 만들 수 있다.

 

  • dataset: 이미지 데이터셋 전달
  • batch_size: 배치 크기. (한번에 불러오는 데이터 크기)  훈련데이터가 1821개이므로 4 정도로 작게 설정했다.
  • shuffle: 데이터를 섞을지 여부.

 

 

 

 

 

 

 

3. 모델 생성

이번에는 모델을 직접 만들지 않고 사전에 훈련된 모델을 전이 학습시키는 방식을 써보려고 한다.

 

 

사전 훈련 모델(pretrained model)

말 그대로 이미 한 분야에서 훈련을 마친 모델이다.

 

전이학습(transfer learning)

사전 훈련 모델을 유사한 다른 영역에서 재훈련 시키는 기법. 쉽게 비유하면 전문가가 어느 특수한 상황에 대해 컨설팅 해주는 것과 비슷하다.

 

 

 

파이토치로 사전 훈련 모델을 이용하는 방법

 

1. torchvision.models 모듈 이용 

기본적인 사전 훈련 모델을 제공한다. 하지만 제공하는 모델이 많지 않다는 단점이 있다.

(https://pytorch.org/vision/stable/models)

 

2. pretrainedmodels 모듈 이용

pretrainedmodels도 사전 훈련 모델을 제공한다. 테슬라에서 자율주행차를 연구하는 레미 카덴이 만든 모듈이다. torchvision보다 많은 모델을 제공하며 깃허브에도 잘 정리되어 있다. 

(https://github.com/Cadene/pretrained-models.pytorch#pretrained-models-for-pytorch-work-in-progress)

 

3. 직접 구현한 모듈 이용

 

 

 

 

 

 

EfficientNet 모델 생성

사전 훈련 모델로 사용할 2019년 5월에 개발된 CNN 모델인 EfficientNet이다.

 

캐글환경에서 사용하려면 Luke Melas-Kyriazi라는 사람이 모듈로 구현해놓은 dfficientnet_pytorch를 설치해야한다. 코드는 다음과 같다.

 

!pip install efficientnet-pytorch==0.7.1

 

 

설치를 마치고 EfficientNet 모델을 임포트 한다.

from efficientnet_pytorch import EfficientNet # EfficientNet 모델

 

 

EfficientNet은 efficientnet-b0부터 b7까지 종류가 여러가지인데, 숫자가 높을수록 일반적으로 성능이 더 좋다고 한다. 성능이 가장 좋은 b7을 사용해볼 것이다.

 

 

# 사전 훈련된 efficientnet-b7 모델 불러오기
model = EfficientNet.from_pretrained('efficientnet-b7', num_classes=4) 

model = model.to(device) # 장비 할당

 

efficientnet-b7을 불러올 때 전달한 num_classes는 최종 출력값 개수를 뜻한다. 우리가 예측해야 하는 타깃값은 총 4개이므로 4를 전달했다.

 

 

 

 

 

 

4. 모델 훈련 및 성능 검증

 

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

 

import torch.nn as nn # 신경망 모듈

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

 

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

 

손실함수로 CrossEntropyLoss()를 사용하고, 옵티마이저로는 AdamW를 사용했다. AdamW는 Adam에 가중치 감쇠(가중치를 작게 조절)를 추가로 적용해서 일반화 성능이 더 우수하다. 

 

 

 

 

훈련 및 성능 검증

 

from sklearn.metrics import roc_auc_score # ROC AUC 점수 계산 함수
from tqdm.notebook import tqdm # 진행률 표시 막대 

epochs = 5

# 총 에폭만큼 반복
for epoch in range(epochs):
    # <훈련>
    model.train()        # 모델을 훈련 상태로 설정 
    epoch_train_loss = 0 # 에폭별 손실값 초기화 (훈련 데이터용)
    
    # '반복 횟수'만큼 반복 
    for images, labels in tqdm(loader_train):
        # 이미지, 레이블(타깃값) 데이터 미니배치를 장비에 할당 
        images = images.to(device)
        labels = labels.to(device)
        
        # 옵티마이저 내 기울기 초기화
        optimizer.zero_grad()
        # 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
        outputs = model(images)
        # 손실 함수를 활용해 outputs와 labels의 손실값 계산
        loss = criterion(outputs, labels)
        # 현재 배치에서의 손실 추가 (훈련 데이터용)
        epoch_train_loss += loss.item() 
        loss.backward() # 역전파 수행
        optimizer.step() # 가중치 갱신
    # 훈련 데이터 손실값 출력
    print(f'에폭 [{epoch+1}/{epochs}] - 훈련 데이터 손실값 : {epoch_train_loss/len(loader_train):.4f}')
    
    # <검증>
    model.eval()          # 모델을 평가 상태로 설정 
    epoch_valid_loss = 0  # 에폭별 손실값 초기화 (검증 데이터용)
    preds_list = []       # 예측 확률값 저장용 리스트 초기화 
    true_onehot_list = [] # 실제 타깃값 저장용 리스트 초기화 
    
    with torch.no_grad(): # 기울기 계산 비활성화
        # 미니배치 단위로 검증
        for images, labels in loader_valid:
            images = images.to(device)
            labels = labels.to(device)
            
            outputs = model(images)
            loss = criterion(outputs, labels)
            epoch_valid_loss += loss.item()
            
            preds = torch.softmax(outputs.cpu(), dim=1).numpy() # 예측 확률값
            # 실제값 (원-핫 인코딩 형식)
            true_onehot = torch.eye(4)[labels].cpu().numpy()  
            # 예측 확률값과 실제값 저장
            preds_list.extend(preds)
            true_onehot_list.extend(true_onehot)
    # 검증 데이터 손실값 및 ROC AUC 점수 출력 
    print(f'에폭 [{epoch+1}/{epochs}] - 검증 데이터 손실 값 : {epoch_valid_loss/len(loader_valid):.4f} / 검증 데이터 ROC AUC : {roc_auc_score(true_onehot_list, preds_list):.4f}')

 

 

tqdm은 진행률 상태를 표시하는 라이브러리다.주로 시간이 오래 걸리는 for문에서 사용하는데, 데이터로더를 tqdm으로 감싸 for문을 순회하면 다음과같이 진행 상태를 보여준다.

 

진행상태표시

바깥쪽 for문에서 에폭 단위로 훈련과 검증을 반복한다. 모델을 훈련할 때는 모델을 훈련상태로(model.train()), 검증할 때는 평가 상태(model.eval()로 설정한다.

 

preds_list와 true_onehot_list는 각각 예측 확률값, 실제 타깃값을 저장하는 빈 리스트다. 한 에폭을 거치며 예측한 확률값과 실제 타깃값을 저장해 ROC AUC를 구하고 실제 타깃값은 원-핫 인코딩으로 구현한다.

 

아래쪽의 .cpu().numpy() 코드는  ROC AUC룰 구하기 위해서 이 값을 CPU에 할당하고 넘파이로 변환하는 것이다. 구한 preds와 true_onehot을 각각 리스트에 추가하고 모든 검증 데이터에 대해 작업을 수행하면 검증 데이터 손실값과 ROC AUC 점수를 출력한다.

 

 

 

ROC AUC값이 0.9882로 나왔다. 

 

 

 

 

 

 

5. 예측 결과 및 결과 제출

훈련 후 예측을 하기 위해 테스트용 데이터셋과 데이터 로더를 생성하자.

 

dataset_test = ImageDataset(test, img_dir=img_dir, 
                            transform=transform_test, is_test=True)
loader_test = DataLoader(dataset_test, batch_size=batch_size, 
                         shuffle=False, worker_init_fn=seed_worker,
                         generator=g, num_workers=2)

 

 

예측

 

테스트 데이터를 활용해 타깃 확률을 예측한다.

 

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

preds = np.zeros((len(test), 4)) # 예측값 저장용 배열 초기화

with torch.no_grad():
    for i, images in enumerate(loader_test):
        images = images.to(device)
        outputs = model(images)
        # 타깃 예측 확률 
        preds_part = torch.softmax(outputs.cpu(), dim=1).squeeze().numpy()
        preds[i*batch_size:(i+1)*batch_size] += preds_part

 

예측 값을 저장하기 위해 0으로 채워진 배열을 준비하고 타깃 예측 확률을 구한다. outputs에는 신경망 출력값이 배치 크기만큼 존재한다. 이 출력값에 소프트맥스 함수로 확률값을 구해 preds_part에 할당했다.

마지막으로 preds_part를 이용해 preds 배열을 갱신하게 된다. 

 

이 코드가 끝나면 preds에는 모든 테스트 데이터의 예측 확률값들이 저장돼 있다.

 

 

 

submission[['healthy', 'multiple_diseases', 'rust', 'scab']] = preds
submission.to_csv('submission.csv', index=False)

 

마지막으로 제출 파일을 만들고 제출했더니 프라이빗 점수가 0.9699로 나왔다.

 

 

 

 

 

 


5. 성능개선

 

이어서 몇 가지 기법으로 성능 개선을 해보았다. 성능 개선을 하는 기법으로 다음 네 가지를 적용했다.

 

1. 에폭늘리기

2. 스케줄러 추가

3. TTA(테스트 단계 데이터 증강) 기법

4. 레이블 스무딩 적용

 

에폭 늘리기와 스케줄러 추가는 모델 훈련 및 성능 검증 단계 초반에 수행하고, TTA와 레이블 스무딩은 예측 단계에서 이루어지는 성능 개선 기법이다.

 

모델생성까지는 위와 동일하게 진행한 후 다음 작업을 실행한다.

 

 

 

모델 훈련 및 성능 검증: 스케줄러 설정

 

스케줄러는 훈련 과정에서 학습률을 조정하는 기능을 제공한다. 훈련 초반에는 빠르게 가중치를 갱신하기 위해 학습률이 큰 게 좋다. 훈련을 진행하며 학습률을 점차 줄이면 최적의 가중치를 찾기가 더 수월하다고 한다.

이번에는 get_cosine_schedule_with_warmup() 스케줄러를 사용해보았다. 이 스케줄러는 지정한 값만큼 학습률을 증가시켰다가 코사인 그래프 모양으로 점차 감소시킨다.  *지정한 학습률: 옵티마이저에서 지정한 학습률(0.00006)

 

 

from transformers import get_cosine_schedule_with_warmup

epochs = 39 # 총 에폭

# 스케줄러 생성
scheduler = get_cosine_schedule_with_warmup(optimizer,  # 옵티마이저전달
                                            num_warmup_steps=len(loader_train)*3, # 학습률도달
                                            num_training_steps=len(loader_train)*epochs) # 훈련을 마치는데 필요한 반복 횟수

 

해당 스케줄러는 transformer 모듈에서 제공한다. 총 에폭을 기존 5에서 39로 크게 늘렸다. 에폭이 너무 작으면 과소적합, 많으면 과대적합이 일어나기 쉽다. 

이어서 앞서 정의한 옵티마이저를 전달하면 해당 옵티마이저로 가중치를 갱신할 때 스케줄러로 학습률을 조정한다.

num_warmup_steps 파라미터는 몇 번만에 지정한 학습률에 도달하지 뜻한다. len(loader_train)은 1에폭의 반복 수인데, 3에폭만에 지정한 학습률에 도달하도록 *3을 해주었다. 

num_training_steps는 모든 훈련을 마치는데 필요한 반복 횟수다. 총 39번을 훈련할 것이기 때문len(loader_train)*epochs를 전달했다.

 

이 스케줄러를 적용하면 에폭 3에서 지정한 학습률 0.00006에 도달하고 그 다음부터는 코사인 그래프 모양으로 학습률이 점차 감소한다.

 

 

 

훈련 및 성능 검증

 

이제 모델을 훈련시키며 성능을 검증해본다. 스케줄러 코드를 제외하면 모델 훈련 코드도 베이스라인과 일치하다.

 

from sklearn.metrics import roc_auc_score # ROC AUC 점수 계산 함수
from tqdm.notebook import tqdm # 진행률 표시 막대

# 총 에폭만큼 반복
for epoch in range(epochs):
    # <훈련>
    model.train()        # 모델을 훈련 상태로 설정
    epoch_train_loss = 0 # 에폭별 손실값 초기화 (훈련 데이터용)
    
    # '반복 횟수'만큼 반복 
    for images, labels in tqdm(loader_train):
        # 이미지, 레이블(타깃값) 데이터 미니배치를 장비에 할당 
        images = images.to(device)
        labels = labels.to(device)
        
        # 옵티마이저 내 기울기 초기화
        optimizer.zero_grad()
        # 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
        outputs = model(images)
        # 손실 함수를 활용해 outputs와 labels의 손실값 계산
        loss = criterion(outputs, labels)
        # 현재 배치에서의 손실 추가 (훈련 데이터용)
        epoch_train_loss += loss.item() 
        loss.backward()  # 역전파 수행
        optimizer.step() # 가중치 갱신
        scheduler.step() # 스케줄러 학습률 갱신
        
    # 훈련 데이터 손실값 출력
    print(f'에폭 [{epoch+1}/{epochs}] - 훈련 데이터 손실값 : {epoch_train_loss/len(loader_train):.4f}')
    
    # <검증>
    model.eval()          # 모델을 평가 상태로 설정 
    epoch_valid_loss = 0  # 에폭별 손실값 초기화 (검증 데이터용)
    preds_list = []       # 예측 확률값 저장용 리스트 초기화
    true_onehot_list = [] # 실제 타깃값 저장용 리스트 초기화
    
    with torch.no_grad(): # 기울기 계산 비활성화
        # 미니배치 단위로 검증
        for images, labels in loader_valid:
            images = images.to(device)
            labels = labels.to(device)
            
            outputs = model(images)
            loss = criterion(outputs, labels)
            epoch_valid_loss += loss.item()
            
            preds = torch.softmax(outputs.cpu(), dim=1).numpy() # 예측 확률값
            # 실제값 (원-핫 인코딩 형식)
            true_onehot = torch.eye(4)[labels].cpu().numpy() 
            # 예측 확률값과 실제값 저장
            preds_list.extend(preds)
            true_onehot_list.extend(true_onehot)
    # 검증 데이터 손실값 및 ROC AUC 점수 출력 
    print(f'에폭 [{epoch+1}/{epochs}] - 검증 데이터 손실값 : {epoch_valid_loss/len(loader_valid):.4f} / 검증 데이터 ROC AUC : {roc_auc_score(true_onehot_list, preds_list):.4f}')

 

에폭수를 크게 늘려서 모델을 훈련하는 데 긴 시간이 걸린다. 실습을 진행해보니 한 에폭마다 8분 정도가 걸렸다. 다 돌리려면 5시간 넘게 걸리기 때문에 베이스라인의 ROC AUC값보다 높아질 때까지 실행해보았다. 

 

 

베이스라인의 ROC AUC값인 0.9882에서 스케줄러 설정으로 성능 개선이 되고 있는 것을 확인했다. 

 

 

 

 

 

 

예측: TTA(테스트 단계 데이터 증강)

 

긴 훈련을 마쳤다면 테스트 데이터를 활용해 예측한 뒤 제출하면 된다. 예측에서 성능을 개선할 수 있는 TTA와 레이블 스무딩 기법에 대해 알아보자.

 

 

TTA(테스트 단계 데이터 증강)

 

위에서 이미지 변환 패키지 albumentations로 훈련 데이터를 증강시켰다. 훈련 데이터가 많으면 모델 성능도 좋아지는데 이런 데이터 증강 기법을 테스트 단계에서 이용하여 예측 성능을 더 높일 수 있다. 이렇게 테스트 단계에서 활용하는 데이터 증강 기법을 TTA(Test-Time-Augmentation)라고 한다.

 

베이스라인에서 했던 것처럼 일반적으로 훈련된 모델이 테스트 데이터 원본을 활용해 타깃값을 예측하는데, TTA를 적용하면 테스트 데이터를 여러 차례 변형해서 예측한다. 테스트 데이터가 늘어난 효과를 얻을 수 있는 것이다. 

 

TTA를 활용하는 순서는 다음과 같다.

  1. 테스트 데이터에 여러 변환 적용
  2. 변환된 텍스트 데이터별로 타깃 확률값 예측
  3. 타깃 예측 확률의 평균 구하기

3에서 구한 평균 확률을 최종 제출하면 앙상블 효과로 원본 데이터로 예측할 때보다 성능이 좋아질 가능성이 높다.

 

 

 

원본 테스트 데이터로 먼저 예측하고, TTA를 적용해서 예측해보았다.

 

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

preds_test = np.zeros((len(test), 4)) # 예측값 저장용 배열 초기화

with torch.no_grad():
    for i, images in enumerate(loader_test):
        images = images.to(device)
        outputs = model(images)
        # 타깃 예측 확률
        preds_part = torch.softmax(outputs.cpu(), dim=1).squeeze().numpy()
        preds_test[i*batch_size:(i+1)*batch_size] += preds_part

 

먼저 원본 데이터로 예측하는 코드를 작성했다. preds 변수명을 preds_test로 작성한 것을 제외하면 같다. 

 

submission_test = submission.copy() # 제출 샘플 파일 복사

submission_test[['healthy', 'multiple_diseases', 'rust', 'scab']] = preds_test

 

테스트 데이터 원본으로 예측한 타깃값인 preds_test를 제출 샘플 파일 복사를한 submission_test에 저장해두었다. 다음은 TTA를 적용해서 예측해 볼 것이다.

 

 

 

num_TTA = 7 # TTA 횟수

preds_tta = np.zeros((len(test), 4)) # 예측값 저장용 배열 초기화 (TTA용)

# TTA를 적용해 예측
for i in range(num_TTA):
    with torch.no_grad():
        for i, images in enumerate(loader_TTA):
            images = images.to(device)
            outputs = model(images)
            # 타깃 예측 확률
            preds_part = torch.softmax(outputs.cpu(), dim=1).squeeze().numpy()
            preds_tta[i*batch_size:(i+1)*batch_size] += preds_part

 

TTA를 7번 수행했다. 더 수행하면 앙상블 효과가 커지지만 그만큼 시간이 오래 걸리고 효과도 미미해진다.

 

 

 

preds_tta /= num_TTA

 

TTA를 적용한 예측 확률 preds_tta를 구하고 이 값의 평균을 내기 위해서 TTA 횟수(num_TTA)를 나누어준다. preds_tta를 구할 때 횟수만큼 누적했기 때문이다.

 

 

submission_tta = submission.copy() 

submission_tta[['healthy', 'multiple_diseases', 'rust', 'scab']] = preds_tta

 

submission_test.to_csv('submission_test.csv', index=False)
submission_tta.to_csv('submission_tta.csv', index=False)

 

TTA를 적용해서 예측한 타깃 확률을 저장하고 두 예측값을 각각 제출파일로 만들었다.

 

 

 

 

 

레이블 스무딩

마지막으로 성능을 조금 더 높일 레이블 스무딩 기법을 활용해본다. 딥러닝 모델이 특정 타깃값일 확률을 1에 매우 가깝게 예측하는 경우가 있을 수 있다. 이렇게 되면 일반화 성능이 떨어져 최종 제출 시 평가 점수가 안 좋게 나올 수 있다. 이런 경우 사용하는 보정 기법이 레이블 스무딩으로 과잉 확신한 예측값을 보정해준다.

 

 

def apply_label_smoothing(df, target, alpha, threshold):
    # 타깃값 복사
    df_target = df[target].copy()
    k = len(target) # 타깃값 개수
    
    for idx, row in df_target.iterrows():
        if (row > threshold).any():         # 임계값을 넘는 타깃값인지 여부 판단
            row = (1 - alpha)*row + alpha/k # 레이블 스무딩 적용  
            df_target.iloc[idx] = row       # 레이블 스무딩을 적용한 값으로 변환
    return df_target # 레이블 스무딩을 적용한 타깃값 반환

 

  • df: DataFrame
  • target: 타깃값 이름 리스트
  • alpha: 레이블 스무딩 강도
  • threshold: 레이블 스무딩을 적용할 최솟값

각 타깃값에 대해 임곗값을 넘는지 판단하고, 임곗값을 넘으면 레이블 스무딩을 적용한다. 해당 코드는 레이블 스무딩 수식을 그대로 적용했다. 그 다음 레이블 스무딩을 적용한 값으로 변환하는 함수다.

 

이 함수를 실제로 적용하면 다음과 같다. 

 

alpha = 0.001 # 레이블 스무딩 강도
threshold = 0.999 # 레이블 스무딩을 적용할 임계값

# 레이블 스무딩을 적용하기 위해 DataFrame 복사
submission_test_ls = submission_test.copy()
submission_tta_ls = submission_tta.copy()

target = ['healthy', 'multiple_diseases', 'rust', 'scab'] # 타깃값 열 이름

# 레이블 스무딩 적용
submission_test_ls[target] = apply_label_smoothing(submission_test_ls, target, 
                                                   alpha, threshold)
submission_tta_ls[target] = apply_label_smoothing(submission_tta_ls, target, 
                                                  alpha, threshold)

submission_test_ls.to_csv('submission_test_ls.csv', index=False)
submission_tta_ls.to_csv('submission_tta_ls.csv', index=False)

 

레이블 스무딩 강도는 0.001, 임곗값은 0.999로 설정했다. 레이블 스무딩 적용 전후 결과도 제출하기 위해 이전에 만든 예측값들을 복사해서 사용했다. 그럼 제출 파일은 총 4개가 된다. 

 

 

 

 

교재에 나온 결과로는 성능개선 후 실제로 전체적인 점수가 많이 오른 것을 볼 수 있었다. TTA와 레이블 스무딩까지 적용한 결과의 점수가 가장 좋은 결과를 보였다. 이번 실습을 통해 스케줄러를 추가하고, TTA와 레이블 스무딩 적용까지 모델 성능을 개선하는 다양한 방법에 대해 알게되었다.

 

 

 

 

 

 


참고 교재

 

머신러닝·딥러닝 문제해결 전략 |  신백균

 

[머신러닝·딥러닝 문제해결 전략 - chapter12]