20. 전이학습과 미세조정 (2) - 인공지능 고급(시각) 강의 복습

안상훈·2024년 6월 27일
0

인공지능-시각

목록 보기
27/54
post-thumbnail

개요

본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 인공지능 고급-시각 강의의 CNN알고리즘 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.


1. Downstream Task란?

이전 포스트 인공지능 고급(시각) 강의 복습 - 21. 주요 CNN알고리즘 구현 : (1) Wide ResNet(WRN) 모델에서 WRN(Wide Residual Networks)모델을 공부했고 이전에 작성한 포스트처럼
맨땅에 학습 / 검증 / 실행 / 평가를 꾸준이 실행했고
각 모델에 대한 성능평가와 추론은 어느정도 한 듯 하다.

이제는 다른이들이 해당 모델을 사전학습 한 자료를 가져와서 이를 내가 하는 작업에 맞추는
Downstream task을 수행하고자 한다.

Downstream task 과정을 그림으로 표현하려 하니 이것저것 달라붙어야 하는 절차가 많아서 조금 복잡하긴 한데 정리를 하면 아래와 같다.

1) 사전에 학습시킬 모델(Target_model)을 선정한 뒤 이를 대용량의 데이터 셋(Huge Dataset)과 강력한 성능을 내는 워크스테이션(Super Computing Trainging Machine)을 통해 학습을 수행한다.

2) 위 과정을 통해 학습이 완료된 모델(Pre-trained model)을 관리 할 수 잇는 클라우드 플랫폼 torchvision, github, keras 등에 업로드 한다.

3) 사용자가 Pre-trained model을 다운로드 한 뒤 본인이 수행하고자 하는 작업(Task)적합한 데이터셋(Fine-turning dataset)으로 재 훈련(Re-training)을 수행하여 작업 목표를 달성할 수 있는 API를 생성한다.

위 작업을 수행하면서 전이학습, 미세조정이라는 개념이 등장하니 이것은 코드실습을 하는 과정에서 설명을 진행하도록 하겠다.

1.1 사전학습 모델 다운받기

학습이 완료된 모델(Pre-trained model)은 인터넷상에서 클라우드 서비스 시스템인 GitHub에서 학습이 완료된 모델을 찾아서 다운로드 할 수도 있지만

유명한 딥러닝 네트워크의 경우 Torchvision, Keras등의 라이브러리를 통해 손쉽게 다운로드가 가능하다.

이 중 Pytorch와 연계되어 있는 Torchvision라이브러리에서 관리하고 있는 학습이 완료된 모델(Pre-trained model)을 다운로드 받도록 하자

https://pytorch.org/vision/0.8/models.html

해당 웹 페이지에 접속하면 Torchvision라이브러리에서 다운로드 받을 수 있는 Pre-trained model은 총 12종이 있으며, 각 종별로 파생 네트워크가 존재하니 실제로는 더 많은 Pre-trained model을 제공하고 있다.
라고 보면 된다.

이 중 이전 포스트 인공지능 고급(시각) 강의 복습 - 21. 주요 CNN알고리즘 구현 : (1) Wide ResNet(WRN) 모델에서 다룬 WRN을 기반으로 실습을 진행하며, 추가자료로 VGG같이 곁들여서 설명하겠다.

우선 WRN에 속하는 Wide ResNet항목을 클릭하여
사용 방법을 확인하자


Torchvision라이브러리에서 제공되는 WRN계열 네트워크는 wide_resnet50_2, wide_resnet101_2 2가지이고
주요 인자는 pretrained만 보면 된다.
이 인자를 True = 학습이 완료된 모델로 다운로드
False = 학습이 안된 모델만 다운로드

이렇게 두가지로 나누어 볼 수 있다.

이전 포스트에서

#학습 완료된 모델 저장하기
MODEL_NAME='Inception_resenet_v2'

torch.save(model.state_dict(), f'{MODEL_NAME}.pth')

이런식으로 학습이 완료된 모델은 파라미터를 *.pth로 저장하는데
이 파일을 빼고 모델만 다운받는 것이 pretrained=False
옵션인 것이다.

그럼 pretrained=True 옵션을 살펴보면
ImageNet 데이터셋으로 학습을 시켰다는데

이거에 대해서 잠깐 알아보고 넘어가자

홈페이지 글씨가 작아서 잘 안보이긴 하는데
매년 대회를 열고, 대회용 데이터셋을 업로드 하는데
Training용 : 120만장
Validation용 : 5만장
Test용 : 10만장
이렇게 구성되고 구별해야 할 객체 종류(class)는 1000종이다.

이 정도라면 충분한 대용량의 데이터 셋(Huge Dataset)으로 볼 수 있겠다.

그리고 torchvisionPre-trained model을 사용하려면
입력 요구사항을 확인해야 한다.

이 입력요구사항을 확인하려면 해당 모델이 어떠한 방식으로 학습이 진행되었는지를 확인하자

입력 요구사항에 관한 문서를 확인하면
반복적으로 진행했던 이미지의 전처리 과정이 코드로 구현되어 있는데

아무튼 결론은

input_size=(3, 224, 224)

이렇게 입력해야 한다.

여기까지 사전학습의 개요에 대해 설명했으니 코드를 확인하자

from torchvision import models

# 사전 학습된 WRN_50_2 모델 불러오기
WRN50_2 = models.wide_resnet50_2(pretrained=True)

불러온 모델을

from torchsummary import summary #설계한 모델의 요약본 출력 모듈

summary(WRN50_2, input_size=(3, 224, 224), device='cpu')

을 통해서도 구조를 확인해 볼 수 있지만

이렇게 불러온 모델에 대해 레이어 별로 접근을 해야하니

print(WRN50_2)

으로 모델의 구조를 확인하는것을 권장한다

해당 모델을 print하면 딱 봐도 머리가 아파오는데

여기서 실습을 해보자

출력된 레이어 정보에서
layer1 0번째블록 내 conv3레이어의 파라미터를 출력하고자 한다면
아래의 코드로 작성하면 된다.

conv3_parameters = WRN50_2.layer1[0].conv3.parameters()

이 코드 실습을 하는 이유는
사전학습, 미세조정과정을 수행하는데 위 코드의 응용이 꼭 이뤄지기 때문이다.

아무튼 학습이 완료된 모델(Pre-trained model)을 다운받았고
구조도 살펴봤으니
다음챕터로 넘어가자


2. Fine-Turning 데이터셋

Pre-trained model다운받은 이유는

Downstream task과정을 수행하는데 이를 써먹으려고 다운을 받은 것이다.

그러면 내가 할 일이 뭔지 정의가 되어 있어야 할 것이다.

이번 코드 실습에서 내가 할 일은

인공지능 고급(시각) 강의 복습 - 17. 이미지 데이터 증강여기에서 사용했던
이 데이터셋의 강아지, 고양이를 분류하는 작업을 수행하고자 한다.

그러면 이번 챕터의 이름인
작업에 적합한 데이터셋(Fine-turning dataset)
당연히 Kaggle cats and Dogs Dataset이 됨을
알아차렸을 것이다.

아무튼 해당 포스트를 참조하면 다운로드 받은 데이터셋을

이런식으로 분류까지 완료를 했다.

그럼 분류가 완료된 데이터셋을 불러와서
커스텀 데이터셋을 만들어 보도록 하자.

cat_and_dog = ['Cat', 'Dog']
class CustomDataset(Dataset):
    def __init__(self, root, class_list, mode=None, transform=None):
        self.root = root
        self.mode = mode
        self.transform = transform
        self.class_list = class_list

        #이미지 경로 추출 후 저장변수
        self.img_list = self._img_mode()

    def _img_mode(self):
        if self.mode is not None:
            img_path = os.path.join(self.root, self.mode)
        
        result_list = []

        for subdir in self.class_list:
            subdir_path = os.path.join(img_path, subdir)
            if os.path.exists(subdir_path):
                img_files = [f for f in os.listdir(subdir_path) if os.path.isfile(os.path.join(subdir_path, f))]
                result_list.extend([os.path.join(subdir_path, f) for f in img_files])
            
            else:
                print(f"경로 검색 실패 : {subdir_path}")
        
        return result_list

    def __str__(self):
        # img_list가 동작 후에도 해당 리스트가 비어있으면 오류로 간주
        if not self.img_list:
            return "오류가 났으니 확인하시오"
        else:
            return f"찾은 이미지 개수 : {len(self.img_list)}"
        
    def __len__(self):
        return len(self.img_list)
    
    def __getitem__(self, idx):
        img_path = self.img_list[idx]
        image = Image.open(img_path).conver('RGB')

        #라벨 정보 추출
        for label, class_name in enumerate(self.class_list):
            if class_name in img_path:
                break
        
        if self.transform:
            image = self.transform(image)
            label = torch.tensor(label, dtype=torch.int64)

        return image, label
# Custom Dataset 생성
train_dataset = CustomDataset(root = '[/cats_and_dogs]폴더 경로',
                           mode = 'train', class_list = cat_and_dog)
test_dataset = CustomDataset(root = '[/cats_and_dogs]폴더 경로',
                           mode = 'val', class_list = cat_and_dog)

이렇게 매번 커스텀 데이터셋을 만드는건 데이터셋의 구조가
Pascal VOC같이 복잡할때나 쓰는 것이고
지금처럼 깔끔하게 서브폴더로 잘 구분된것 까지는
좀 쉽게 갈 필요성이 있다.

https://pytorch.org/vision/0.8/datasets.html#datasetfolder

여기서는 torchvision 라이브러리에서 제공하는
사전에 설정된 datasetFolder 모듈을 사용하도록 하자

설명을 보면 알겠지만
Fine-turning dataset의 메인 경로만 입력하면

서브 폴더 -> 클래스 서브 폴더 순으로만 정리되어 있으면
알아서 잘 데이터셋을 만들어준다

라고 보면 된다.

따라서 코드로는 아래와 같이 치면 된다.

from torchvision import datasets

# dataset.ImageFolder을 사용한 데이터셋 생성
root = '[/cats_and_dogs]폴더 경로'
img_dataset = {} #서브폴더('train', 'val', test')를 딕셔너리 형태로 관리하려고 이렇게 변수를 초기화함

img_dataset['train'] = datasets.ImageFolder(os.path.join(root, 'train'))
img_dataset['val'] = datasets.ImageFolder(os.path.join(root, 'val'))

그러면 torchvisiondatasets.ImageFolders 라이브러리가 잘 데이터셋을 분류했는지
확인도 해보자

import random
import matplotlib.pyplot as plt

# 임의의 데이터 하나 선택
train_idx = random.randint(0, len(img_dataset['train']) - 1)
val_idx = random.randint(0, len(img_dataset['val']) - 1)

train_image, train_label = img_dataset['train'][train_idx]
val_image, val_label = img_dataset['val'][val_idx]

# 클래스 이름 가져오기
class_names = img_dataset['train'].classes

# 서브플롯 생성
fig, axes = plt.subplots(1, 2, figsize=(12, 6))

# Train 이미지 출력
axes[0].imshow(train_image)
axes[0].set_title(f'Train Label: {class_names[train_label]}, {train_label}')
axes[0].axis('off')  # 축 숨기기

# Val 이미지 출력
axes[1].imshow(val_image)
axes[1].set_title(f'Val Label: {class_names[val_label]}, {val_label}')
axes[1].axis('off')  # 축 숨기기

# 서브플롯 사이 간격 조정
plt.tight_layout()
plt.show()

훌륭하게 라벨링까지 잘 해준것을 알 수 있다.

해당 데이터셋의 정규화를 위한 mean, std를 구하는 코드를 구동하고

#훈련 데이터셋의 mean, std를 구하기
from torch.utils.data import DataLoader
from torchvision.transforms import v2
from tqdm import tqdm

transforamtion = v2.Compose([
    v2.Resize((224,224)), #VGG19용 input_img로 리사이징
    v2.ToImage(),  # 이미지를 Tensor 자료형으로 변환
    v2.ToDtype(torch.float32, scale=True)
    #텐서 자료형변환 + [0~1]사이로 졍규화 해줘야함
])

img_dataset['train'].transform = transforamtion

dataloader = DataLoader(img_dataset['train'], batch_size=256, shuffle=False)

#GPU사용 가능여부 확인
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

#이미지의 총 개수 및 채널별 합계를 저장할 변수 초기화
mean = torch.zeros(3).to(device)
std = torch.zeros(3).to(device)
nb_sample = 0

#데이터셋을 순회하며 mean, std 계산
for images, _ in tqdm(dataloader):
    images = images.to(device)
    batch_samples = images.size(0) # 배치 내 이미지 수
    # 차원 형태 = (batch_size, channel(3), H, W)
    images = images.view(batch_samples, images.size(1), -1)
    mean += images.mean(2).sum(0)
    std += images.std(2).sum(0)
    nb_sample += batch_samples

mean /= nb_sample
std /= nb_sample

# mean과 std를 numpy 배열로 변환하여 소수점 4자리로 출력
mean_np = mean.cpu().numpy()
std_np = std.cpu().numpy()

print(f"Mean: {mean_np[0]:.4f}, {mean_np[1]:.4f}, {mean_np[2]:.4f}")
print(f"Std: {std_np[0]:.4f}, {std_np[1]:.4f}, {std_np[2]:.4f}")
100%|██████████| 69/69 [00:39<00:00,  1.76it/s]
Mean: 0.4884, 0.4554, 0.4172
Std: 0.2261, 0.2215, 0.2218
#정규화(mean, std)값을 구한 다음에는 GPU캐시 데이터 초기화를 해주자
torch.cuda.empty_cache()

데이터셋을 전처리를 해준 뒤 데이터로더를 만들어주자
from torchvision.transforms import v2
# 데이터 로더 생성하기
CAD_val = [[0.4884, 0.4554, 0.4172], [0.2261, 0.2215, 0.2218]]

transformation = v2.Compose([
    v2.Resize((224, 224)), #VGG19 -> [224, 224]
    v2.ToImage(),  # 이미지를 Tensor 자료형으로 변환
    v2.ToDtype(torch.float32, scale=True), #텐서 자료형을 [0~1]로 정규화
    v2.Normalize(mean=CAD_val[0], std=CAD_val[1]) #데이터셋 표준화
])

# 전처리 방법론을 데이터셋에 적용하기
img_dataset['train'].transform = transformation
img_dataset['val'].transform = transformation
from torch.utils.data import DataLoader

BATCH_SIZE = 64

train_loader = DataLoader(img_dataset['train'],
                            batch_size=BATCH_SIZE,
                            shuffle=True)
test_loader = DataLoader(img_dataset['val'],
                            batch_size=BATCH_SIZE,
                            shuffle=False)

여기까지 수행했다면

Downstream task을 수행하기 위해 사전에 작업해 두어야 할
Fine-turning dataset의 전처리가 완료된 것이다.

라고 볼 수있다.


3. 전이학습(Transfer Learning)

Fine-turning dataset의 전처리도 완료 되었겠다
불러온 학습이 완료된 모델(Pre-trained model)

재 훈련(Re-training)하는 과정을 수행하고 이를 통해 Downstream task를 완료해야한다.

그런데 여기서 문제가 있다.

불러온 Pre-trained model의 마지막 레이어를 살펴봐야 한다.

print(WRN50_2)

위 코드를 통해 전체 모델의 레이어 구조를 출력 할 수 있고
그 중 가장 말단 레이어를 본다면

print(WRN50_2.fc)
Linear(in_features=2048, out_features=1000, bias=True)

out_features가 1000으로 설정되어 있어

현재 수행하고자 하는 Downstream task에는 맞지 않다.

여기서는 이 CNN모델에 의 구조 개념을 좀 알아둘 필요성이 있는데

불러온 Pre-trained model의 구조를 나눠 본다면 위 사진처럼
3가지 항목

Feature Extractor
Classifier
Decision layer
로 간소화하여 설명할 수 있다.

이 각각의 항목에 대한 설명을 정리하면 아래와 같다.

1) Feature Extractor : 입력 이미지에서 의미있는 특징을 추출하는 역활을 수행하는 레이어 묶음

2) Classifier : 추출한 특징을 바탕으로 입력 이미지가 어떤 클래스에 속할 지 예측하는 레이어

3) Decision Layer : Classifier의 가장 마지막 레이어를 말하며, 해당 모델의 마지막 output을 출력하는 레이어

이 구조로 놓고 봤을 때 Pre-trained model의 마지막 Decision Layer은 출력 형태가 [1000]이니

이것을 Downstream task의 목적에 맞는 [Cat or Dog]의 이진 분류 문제에 맞는 출력 형태인 [1]로 바꿔줘야한다.


바로 이 작업이 전이학습(transfer learning)이다.

이때 이 Decision Layer만 갈아끼울지 아니면 Classifier를 통째로 들어내서 다른 Classifier를 붙여넣을지는 유저의 선택에 따라 달라진다.

이는 Classifier의 기능이 '클래스 종류를 분류하는 것'이고 Decision Layer은 그 출력의 형태만을 조정하는 레이어 이기에 수행하고자 하는 Downstream task에 따라서는 더 적합한 Classifier를 붙여넣을 필요성이 발생할 수도 있다.

아무튼 레이어를 갈아 끼우는 작업을 코드로 수행하면 아래와 같다.

# 모델의 마지막 분류 레이어 수정하기 위해
# 기존 마지막 레이어가 입력받는 차원 형태정보를 추출
num_features = WRN50_2.fc.in_features

# 이진 분류를 위해 출력 노드를 1로 설정
WRN50_2.fc = nn.Linear(num_features, 1)

지금의 사전학습 모델 : WRN50_2ClassifierDecision Layer 단 하나로 구성된 단촐한 구성이기에

여기에 임의로 설계한 Classifier를 붙여넣는 작업도 가능하다

이에 대한 코드는 아래와 같다

# 기존 모델의 fc 레이어를 확장된 classifier 블록으로 대체

# 모델의 마지막 분류 레이어 수정하기 위해
# 기존 마지막 레이어가 입력받는 차원 형태정보를 추출
num_features = WRN50_2.fc.in_features

# 새로운 classifier 블록 정의 및 기존 모델에 추가
WRN50_2.fc = nn.Sequential(
    nn.Linear(num_features, 1000),  # 기존 모델의 fc 출력 크기와 일치하도록 수정
    nn.Dropout(p=0.5),
    nn.Linear(1000, 500),  # 새로운 레이어
    nn.Dropout(p=0.5),
    nn.Linear(500, 1)      # 이진 분류를 위한 출력 크기 (1)
)

위 같은 방식으로 사전학습 모델 : WRN50_2Classifierfc레이어를 확장하여 더 성능을 향상시킬 수 있는 Classifier를 새로이 설계해 쓸 수도 있다.


4. 미세조정(Fine Turning)

앞서 설명한 전이학습(Transfer Learning)을 어떻게 설명할까 고민하다가
적합한 비유가 이것일 것 같아서 이미지를 첨부한다.

전이학습(Transfer Learning)전직, 직업변경이다.

대용량의 데이터 셋(Huge Dataset)으로 학습이 완료된 모델(Pre-trained model)은 용도 때려잡는 기사라고 치고

이 기사가 장착한 검은 약속된 승리의 검(+10강)이라 보면 된다.

이걸 Downstream task을 수행하려면 전사로 전직을 해야 하는 것이다.

근데 문제가 있다.
전사로 전직한 것 까지는 좋은데 장착 가능한 장비가 +1강짜리 형편없는 손도끼 인 것이다.

이게 전이학습 과정에서 발생하는 Classifier(Decision Layer)교체 작업이다.

이 새롭게 장착한 Classifier을 적어도 +9강의 울부짖는 양날도끼까지는 무기강화(Training)를 해줘야

진정으로 Downstream task 작업을 수행할 수 있을 것이다.


그런데 여기서 한가지 옵션이 발생한다.

위 그림처럼 장비만 강화하고 끝낼 지

아니면 이참에 장비 강화할 겸 겸사겸사 본체도 레벨업을
한번 더 하는 것이다.

레벨업을 조금 더 하는 과정이 Fine Turning이다.

그러면 이 레벨업을 어떻게 수행하느냐.. 가 문제인데
코드로 보면 이해하기가 쉽다.

1) 무기만 강화하기

# 모든 레이어의 파라미터를 Freeze
for param in WRN50_2.parameters():
    param.requires_grad = False

# 새로운 레이어만 학습 가능하도록 설정
for param in WRN50_2.fc.parameters():
    param.requires_grad = True

2) 본체도 레벨업 하기

# 모든 레이어의 파라미터를 Trainable로 조정
for param in WRN50_2.parameters():
    param.requires_grad = True

새로이 설계해서 기존 모델을 수정하는데 사용된 Classifier는 무조건 학습이 가능한 형태 로 만들어야 함은 이했을 것이다.

이것을 조정하는 메서드가 .requires_grad이고 이게
역전파를 수행 시 파라미터가 훈련될 항목임을 나타내는 메서드이다.

이게 False 이면 해당 레이어는 Freeze상태
반대로 True 이면 해당레이어는 디폴트 상태인 Trainable 상태가 되는 것이다.

(참고로 새로 설계한 모델은 무조건 .requires_grad=True 상태다. 이건 왜 그런지 이해를 해야한다...)

그럼 여기서 한발짝 더 나갈 수 있다.

위 사진처럼 어느 레이어까지
Freeze(.requires_grad=False) 할지
어느 레이어 부터는
Trainable(.requires_grad=True) 할지

를 결정해야 한다.
이것 또한 Downstream task작업을 수행하는데 있어 주요한 설계자의 역량 이라 볼 수 있을 것이다.

따라서Downstream task는 아래의 과정으로 작업절차를 요약할 수 있을 것이다.

1) 대용량 데이터셋으로 사전학습된 모델을 불러오기
2) Downstream task의 작업에 적합한 Fine-turning dataset의 준비 및 전처리
3) Downstream task의 요구결과물에 맞게 Classifier(Decision Layer) 수정 -> 전이학습
4) 수정된 모델의 Freeze/Trainable 레이어 비율 정의하기 -> 미세조정


5. 코드 실습

위 레이어별 Freeze/Trainable의 비율을 정하는 방식은

1) 무기만 강화하기(All Freeze)

# 모든 레이어의 파라미터를 Freeze
for param in WRN50_2.parameters():
    param.requires_grad = False

# 새로운 레이어만 학습 가능하도록 설정
for param in WRN50_2.fc.parameters():
    param.requires_grad = True

2) 본체도 레벨업 하기(All Trainable)

# 모든 레이어의 파라미터를 Trainable로 조정
for param in WRN50_2.parameters():
    param.requires_grad = True

이 두가지 경우로 놓고 코드 실습을 진행하고자 한다.

1) GPU에 모델 올리기

# GPU 사용 설정(이렇게 모델을 수정후 GPU로 이전이 유연하게 되네)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
WRN50_2 = WRN50_2.to(device)

2) 옵티마이저/손실함수 설계

import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR

All Freeze버전

# 손실 함수와 옵티마이저 설정
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.SGD(WRN50_2.fc.parameters(), lr=1e-3, momentum=0.9)

All Trainable 버전

# 손실 함수와 옵티마이저 설정
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.SGD(WRN50_2.parameters(), lr=1e-4, momentum=0.9)
scheduler = CosineAnnealingLR(optimizer, T_max=40, eta_min=1e-7)

모든 레이어를 얼릴 경우에는 Classifier(Decision Layer)만 학습시키기에 옵티마이저 설정의 step를 크게 해야 하지만

모든 레이어가 학습 가능한 상태라면 옵티마이저 설정의 step도 줄이고, 스케쥴러까지 도입하는게 좋다.
안그럼 과적합이 발생할 것이다.


3) 훈련/검증 코드

from tqdm import tqdm #훈련 진행상황 체크

#tqdm 시각화 도구 출력 사이즈 조절 변수
epoch_step = 5
def model_train(model, data_loader, 
                loss_fn, optimizer_fn, 
                processing_device, epoch, 
                scheduler_fn=None,):

    model.train()  # 모델을 훈련 모드로 설정

    global epoch_step

    # loss와 accuracy를 계산하기 위한 임시 변수를 생성
    run_size, run_loss, correct = 0, 0, 0

    # 특정 에폭일 때만 tqdm 진행상황 바 생성
    if (epoch + 1) % epoch_step == 0 or epoch == 0:
        progress_bar = tqdm(data_loader)
    else:
        progress_bar = data_loader

    for image, label in progress_bar:

        # 입력된 데이터를 먼저 GPU로 이전하기
        image = image.to(processing_device)
        # label = label.to(processing_device)
        
        # 이진분류 문제에서는 라벨의 차원축소 + float형 변환해야함
        label = label.to(processing_device).float().unsqueeze(1)

        # 전사 과정 수행
        output = model(image)
        loss = loss_fn(output, label)

        #backward과정 수행
        optimizer_fn.zero_grad()
        loss.backward()
        optimizer_fn.step()

        if scheduler_fn is not None:
            # 스케줄러 업데이트
            scheduler_fn.step()

        # #argmax = 주어진 차원에서 가장 큰 값을 가지는 요소의 인덱스를 반환
        # pred = output.argmax(dim=1) #예측값의 idx출력
        # correct += pred.eq(label).sum().item()

        # 예측값 계산(이진분류용)
        preds = torch.sigmoid(output) > 0.5
        correct += preds.eq(label).sum().item()

        #현재까지 수행한 loss값을 얻어냄
        run_loss += loss.item() * image.size(0)
        run_size += image.size(0)

        #tqdm bar에 추가 정보 기입
        if (epoch + 1) % epoch_step == 0 or epoch == 0:
            desc = (f"[훈련중]로스: {run_loss / run_size:.4f}, "
                    f"정확도: {correct / run_size:.4f}")
            progress_bar.set_description(desc)

    avg_accuracy = correct / len(data_loader.dataset)
    avg_loss = run_loss / len(data_loader.dataset)

    return avg_loss, avg_accuracy
def model_evaluate(model, data_loader, loss_fn, 
                   processing_device, epoch):
    model.eval()  # 모델을 평가 모드로 전환 -> dropout 기능이 꺼진다
    # batchnormalizetion 기능이 꺼진다.
    global epoch_step

    # gradient 업데이트를 방지해주자
    with torch.no_grad():

        # 여기서도 loss, accuracy 계산을 위한 임시 변수 선언
        run_loss, correct = 0, 0

        # 특정 에폭일 때만 tqdm 진행상황 바 생성
        if (epoch + 1) % epoch_step == 0 or epoch == 0:
            progress_bar = tqdm(data_loader)
        else:
            progress_bar = data_loader

        for image, label in progress_bar:  # 이때 사용되는 데이터는 평가용 데이터
            # 입력된 데이터를 먼저 GPU로 이전하기
            image = image.to(processing_device)
            # 이진분류 문제에서는 라벨의 차원축소 + float형 변환해야함
            label = label.to(processing_device).float().unsqueeze(1)

            # 모델 출력
            output = model(image)
            
            # # 모델의 평가 결과 도출 부분
            # pred = output.argmax(dim=1) #예측값의 idx출력
            # correct += torch.sum(pred.eq(label)).item()
            
            # 모델의 평가 결과(이진분류용)
            preds = torch.sigmoid(output) > 0.5
            correct += preds.eq(label).sum().item()

            run_loss += loss_fn(output, label).item() * image.size(0)

        accuracy = correct / len(data_loader.dataset)
        loss = run_loss / len(data_loader.dataset)

        return loss, accuracy

참고로 그동안 포스팅 했었던 model_train, model_evaluate 코드는 다중분류 문제였기에

지금의 코드는 이진분류용 함수로 변경되었다.

변경이 어디가 진행되었는지는 확인해보기 바란다
(주석도 달아놨으니 찾는데 어려움은 없을것이라 생각한다...)


4) 실행

All Freeze버전

# 학습과 검증 손실 및 정확도를 저장할 리스트
his_loss, his_accuracy  = [], []
num_epoch = 25

for epoch in range(num_epoch):
    # 훈련 손실과 훈련 성과지표를 반환 받습니다.
    train_loss, train_acc = model_train(WRN50_2, train_loader, 
                                        criterion, optimizer,
                                        device, epoch)

    # 검증 손실과 검증 성과지표를 반환 받습니다.
    test_loss, test_acc = model_evaluate(WRN50_2, test_loader, 
                                         criterion, device, epoch)

    # 손실과 성능지표를 리스트에 저장
    his_loss.append((train_loss, test_loss))
    his_accuracy.append((train_acc, test_acc))

    # epoch가 특정 배수일 때만 출력하기
    if (epoch + 1) % epoch_step == 0 or epoch == 0:
        print(f"epoch {epoch+1:03d} ", end=' ')
        print(f"훈련 로스: {train_loss:.4f}", end=' ')
        print(f"훈련 정확도: {train_acc:.4f}")
        print(f"검증 로스: {test_loss:.4f}", end=' ')
        print(f"검증 정확도: {train_acc:.4f}")

All Trainable 버전

fine_tuning_loss, fine_tuning_accuracy = [], []

num_epoch = 25

for epoch in range(num_epoch):
    # 훈련 손실과 훈련 성과지표를 반환 받습니다.
    train_loss, train_acc = model_train(WRN50_2, train_loader, 
                                        criterion, optimizer,
                                        device, epoch,
                                        scheduler_fn=scheduler)

    # 검증 손실과 검증 성과지표를 반환 받습니다.
    test_loss, test_acc = model_evaluate(WRN50_2, test_loader, 
                                         criterion, device, epoch)

    # 미세조정의 손실과 성능지표를 리스트에 저장
    fine_tuning_loss.append((train_loss, test_loss))
    fine_tuning_accuracy.append((train_acc, test_acc))

    # epoch가 특정 배수일 때만 출력하기
    if (epoch + 1) % epoch_step == 0 or epoch == 0:
        print(f"epoch {epoch+1:03d} ", end=' ')
        print(f"훈련 로스: {train_loss:.4f}", end=' ')
        print(f"훈련 정확도: {train_acc:.4f}")
        print(f"검증 로스: {test_loss:.4f}", end=' ')
        print(f"검증 정확도: {train_acc:.4f}")


Fine-turning dataset에 맞춰 모든 레이어를 All Trainable하게 조정한 뒤 훈련을 진행하는 미세조정 방식이 더 성능이 좋게 나왔으나, 이것은 과적합을 항상 경계하면서 진행해야 함을 꼭 숙지하자.

이것으로 WRN(Wide Residual Networks)학습이 완료된 버전(Pre-trained model)을 불러와서 Downstream task를 수행하는 실습을 완료했다.

profile
자율차 공부중

0개의 댓글