pytorch 재활훈련3 -transfer learning 해보기-

1. 실제 비즈니스에서 딥러닝 구현하는 방식

 

학습이 끝난 모델을 사용해 ILSVRC의 1000종류 클래스에서 이미지 라벨을 예측했지만,

 

실제 비즈니스에서는 예측하고자 하는 이미지의 라벨이 ILSVRC에서 사용한 1000종류 클래스와는 다르므로, 자신의 데이터로 딥러닝 모델을 다시 학습시켜야 한다.

 

1-1) 파이토치를 활용한 딥러닝 구현 흐름

 

 

먼저 앞으로 구현할 딥러닝 응용 기술의 전체 그림을 파악

 

1) 구체적으로는 전처리, 후처리, 네트워크 모델의 입출력을 파악한다.

 

2) 다음으로는 Dataset 클래스를 작성

 

입력 데이터와 라벨 등을 쌍으로 갖는 클래스

 

Dataset에는 데이터에 대한 전처리 클래스의 인스턴스를 할당해서 파일을 읽을 때 자동으로 전처리를 적용

 

훈련데이터, 검증데이터, 테스트데이터에 대한 Dataset을 작성

 

3) DataLoader 클래스 작성

 

Dataset에서 데이터를 어떻게 가져올지 설정하는 클래스

 

일반적으로 딥러닝에서는 미니 배치 학습을 실시하여 여러 데이터를 동시에 Dataset에서 가지고 와 네트워크를 학습

 

DataLoader는 Dataset에서 미니 배치를 쉽게 가지고 올 수 있도록 한다

 

훈련데이터와 검증 데이터, 테스트 데이터의 DataLoader를 만든다.

 

DataLoader가 완성되면, 입력 데이터에 대한 사전 준비가 완료된다.

 

4) 네트워크 모델을 작성

 

네트워크 모델 생성은

 

4-1) 처음부터 전부 스스로 만드는 경우,

 

4-2) 학습된 모델을 로드하여 사용하는 경우

 

4-3) 학습된 모델을 기반으로 변경하는 경우가 있다

 

딥러닝을 응용할때는 학습된 모델을 기반으로 수정하는 4-3)이 많다.

 

5) 네트워크 모델을 만든 후 네트워크 모델의 순전파 함수 forward를 정의

 

네트워크 모델이 단순하다면 데이터 모델을 구축한 층은 앞에서 뒤로 흐른다.

 

딥러닝에서 응용할때는 순전파가 복잡한 경우가 많다.

 

네트워크가 도중에 나뉘기도 한다.

 

복잡한 순서를 제대로 전달하려면 순전파 함수를 정확하게 정의해야 한다.

 

처음에는 순전파 함수를 이해하기 어렵다

 

6) 역전파를 위한 손실함수 정의

 

간단한 딥러닝 기법이라면 제곱 오차와 같이 단순하지만, 응용 기법에 따라 매우 복잡할 수 있다.

 

7) 네트워크 모델의 결합 파라미터를 학습시킬 때의 최적화 기법 설정

 

역전파로 결합 파라미터의 gradient가 구해진다.

 

최적화 방법으로 이 gradient를 사용해 결합 파라미터의 수정량을 어떻게 계산할지 설정

 

최적화 방법에는 momentum SGD 등이 있다.

 

8) 학습/검증 실시

 

기본적으로 epoch마다 훈련 데이터와 검증 데이터의 성능을 확인

 

검증 데이터의 성능을 향상시킬 수 없으면, 훈련 데이터가 과학습해 학습을 종료시키는 경우가 많다.

 

검증 데이터의 성능이 향상되지 않을 때 학습을 종료하는 방법을 early stopping이라고 한다.

 

9) 학습이 완료되면 마지막으로 테스트 데이터를 추론

 

 

2. transfer learning 구현해보기

 

transfer learning을 사용하여 적은 양의 데이터로 원래 이미지 분류용 딥러닝 모델 구축하기

 

파이토치 튜토리얼에서 제공하는 '개미'와 '벌'의 이미지를 분류하는 모델 학습

 

2-1) 전이학습

 

학습된 모델을 기반으로 최종 출력층을 바꿔 학습하는 기법

 

학습된 모델의 최종 출력층을 보유 중인 데이터에 대응하는 출력층으로 바꾸고, 교체한 출력층의 가중치를 소량의 데이터로 다시 학습

 

입력층에 가까운 부분의 가중치는 학습된 값으로, 변화시키지 않는다

 

학습된 모델에 기반하는 전이학습은 보유 중인 데이터가 적더라도 뛰어난 성능의 딥러닝을 실현하기 좋다.

 

입력층에 가까운 층의 가중치도 학습된 값으로 갱신하는 경우는 fine tuning이라고 부른다.

 

 

3. 패키지 부르기

 

#import package

import glob

import os.path as osp

import random

import numpy as np

import json

from PIL import Image
from tqdm import tqdm

import matplotlib.pyplot as plt
%matplotlib inline


import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import torchvision
from torchvision import models, transforms

 

실험 재현을 위해 난수 설정을 반드시 해준다

 

#난수 시드 설정
torch.manual_seed(1234)
np.random.seed(1234)
random.seed(1234)

 

 

4. 데이터셋 작성

 

이미지 전처리 클래스인 ImageTransform을 만든다

 

이는 훈련시와 추론시에 각각 다른 전처리를 해준다.

 

훈련 시에는 데이터 확장을 수행한다.

 

데이터 확장(data augmentation)은 데이터에 대해 epoch마다 이미지 변환을 다르게 적용하여 데이터를 부풀리는 기법이다.

 

훈련시의 전처리에 RandomResizedCrop과 RandomHorizontalFlip을 수행한다

 

RandomResizedCrop(resize, scale=(0.5,1.0))은 scale에 지정된 0.5~1.0 크기로 이미지를 확대하거나 축소한다.

 

화면 비율을 3/4에서 4/3중 하나로 변경해 이미지를 가로 혹은 세로로 늘이고, 마지막으로 resize에서 지정한 크기로 이미지를 자른다.

 

RandomHorizontalFlip()은 이미지의 좌우를 50%의 확률로 반전시킨다.

 

따라서, 동일한 train data도 epoch마다 조금씩 다른 이미지가 생성된다.

 

그래서 다양한 데이터를 학습할 수 있고, 테스트 데이터에 대한 일반화 성능 개선에 도움이 된다.

 

#입력 이미지의 전처리 클래스
#훈련시와 추론시 처리가 다르다

class ImageTransform():
    
    """
    이미지 전처리 클래스, 훈련시, 검증시의 동작이 다르다.
    이미지 크기를 리사이즈하고, 색상을 표준화

    훈련시에는 RandomResizedCrop과 RandomHorizontalFlip으로 데이터 확장

    Attributes
    ---------------
    resize: int
        리사이즈 대상 이미지의 크기

    mean: (R,G,B)
        각 색상 채널의 평균값
    
    std : (R,G,B)
        각 색상 채널의 표준편차
    """

    def __init__(self, resize, mean, std):
        
        self.data_transform = {
            'train': transforms.Compose([
                transforms.RandomResizedCrop(
                    resize,scale = (0.5,1.0) #데이터 확장
                ),
                transforms.RandomHorizontalFlip(), #데이터 확장
                transforms.ToTensor(), #텐서 변환
                transforms.Normalize(mean,std) #표준화
            ]),
            'val': transforms.Compose([
                transforms.Resize(resize), #리사이즈
                transforms.CenterCrop(resize), #이미지 중앙을 resize*resize로 자르기
                transforms.ToTensor(), #텐서로 변환
                transforms.Normalize(mean,std) #표준화
            ])
        }

        def __call__(self,img,phase='train'):
            
            """
            Parameters
            -----------
            phase: 'train' or 'val'
                전처리 모드를 지정
            """

            return self.data_transform[phase](img)

 

훈련모드로 실시한 동작을 확인

 

얼굴 근처가 잘리고 옆으로 늘어나며 좌우가 바뀐다.

 

결과는 실행할때마다 다르다(random resized, horizontalflip 등등때문)

 

#훈련할때 이미지 전처리 확인

#1. 이미지 읽기

image_file_path = '/content/data/goldenretriever.jpg'
img = Image.open(image_file_path) #[높이][폭][색]

#2. 원본 이미지 표시
plt.imshow(img)
plt.show()

#3. 이미지 전처리, 처리된 이미지 표시

size = 224

mean = (0.485, 0.456, 0.406)

std = (0.229, 0.224, 0.225)

transform = ImageTransform(size,mean,std)
img_transformed = transform(img,phase='train') #torch.Size([3,224,224])

#(색,높이,너비)를 (높이, 너비, 색상)으로 바꾸고 0-1로 값을 제한해 표시

img_transformed = img_transformed.numpy().transpose((1,2,0))
img_transformed = np.clip(img_transformed, 0,1)

#실행할때마다 결과가 다르다(random)
plt.imshow(img_transformed)
plt.show()

 

원본

 

이미지 전처리

 

다음으로 이미지 파일 경로를 리스트형 변수에 저장하는 make_datapath_list 함수를 만든다.

 

훈련데이터는 개미, 벌 이미지가 243장

 

검증데이터는 153장

 

훈련데이터와 검증 데이터의 파일 경로 리스트를 작성

 

파일 경로 문자열을 osp.join으로 작성하고, glob으로 파일 경로를 가져온다.

 

def make_datapath_list(phase="train"):
    
    """
    데이터의 경로를 저장한 리스트를 작성한다.

    parameters
    -----------
    phase: 'train' or 'val'
        훈련 데이터 또는 검증 데이터를 지정
    
    Returns
    -----------
    path_list : list
        데이터 경로를 저장한 리스트
    """

    rootpath = "./data/hymenoptera_data/"
    target_path = osp.join(rootpath+phase+'/**/*.jpg')
    print(target_path)

    path_list = [] #여기에 저장

    #glob를 이용하여 하위 디렉토리의 파일 경로를 가져온다
    for path in glob.glob(target_path):
        
        path_list.append(path)
    
    return path_list

#실행
train_list = make_datapath_list(phase="train")
val_list = make_datapath_list(phase="val")

train_list

 

마지막으로 전처리 클래스와 함수를 사용하여 Dataset 클래스를 작성

 

torchvision.datasets.ImageFolder 클래스로 dataset 클래스를 작성할 수 있지만, 다양한 딥러닝에 응용하기 위해, 직접 작성할 줄 알아야겠다.

 

이미지를 읽어올때, 전처리 클래스인 ImageTransform을 적용시킨다.

 

이미지가 개미라면 label을 0으로, 벌이면 1로 한다.

 

dataset 클래스를 상속한 원래의 dataset을 만들때는 dataset에서 하나의 데이터를 꺼내는 메소드인 __getitem__()과 dataset의 파일 수를 반환하는 __len__() 메소드를 구현해야 한다.

 

#개미와 벌의 이미지에 대한 dataset 작성

class HymenopteraDataset(data.Dataset):
    
    """
    개미와 벌 이미지의 dataset class
    PyTorch의 dataset 클래스를 상속

    attributes
    -----------
    file_list : 리스트
        화상 경로를 저장한 리스트
    transform : object
        전처리 클래스의 인스턴스
    phase : 'train' or 'test'
        학습인지 훈련인지를 설정
    """

    def __init__(self, file_list, transform=None, phase='train'):
        
        self.file_list = file_list #파일 경로 리스트
        self.transform = transform #전처리 클래스의 인스턴스
        self.phase = phase #train or val 지정

    def __len__(self):
        
        """이미지 개수를 반환"""
        return len(self.file_list)
    
    def __getitem__(self,index):
        
        """
        전처리한 이미지의 tensor 형식의 데이터와 라벨 취득
        """

        #index번째의 이미지를 로드
        img_path = self.file_list[index]
        img = Image.open(img_path) #[높이][너비][색]

        #이미지 전처리
        img_transformed = self.transform(
            img, self.phase) #torch.Size([3,224,224])
            
        #이미지 라벨을 파일 이름에서 추출
        if self.phase == 'train':
            
            label = img_path[30:34]
        
        elif self.phase == 'val':
            
            label = img_path[28:32]
        
        #라벨을 숫자로 변경
        if label == 'ants':
            
            label = 0
        
        elif label == 'bees':
            
            label = 1
        
        return img_transformed, label

 

 

만든 것을 실행하고 동작을 확인해본다.

 

만들때는 훈련데이터셋과 테스트데이터셋 서로 다른 phase로 만들 수 있다.

 

#실행
train_dataset = HymenopteraDataset(
    file_list = train_list, transform = ImageTransform(size,mean,std), phase='train'
)

val_dataset = HymenopteraDataset(
    file_list = val_list, transform = ImageTransform(size,mean,std), phase='val'
)

#동작 확인

index = 0
print(train_dataset.__getitem__(index)[0].size()) #이미지 텐서 크기
print(train_dataset.__getitem__(index)[1]) #라벨

 

 

5. dataloader 작성

 

이제 데이터셋을 사용해서 dataloader를 작성한다.

 

dataloader는 pytorch에서 제공하는 torch.utils.data.DataLoader 클래스를 그대로 사용한다.

 

훈련용 dataloader는 shuffle = True로 설정하고 이미지를 꺼내는 순서가 랜덤이 되도록 한다.

 

훈련용 및 검증용 dataloader를 작성하고 양자를 사전형 변수 dataloaders_dict에 저장한다

 

이는 학습 및 검증 시 쉽게 다루기 위해서이다.

 

보유 중인 pc의 메모리 사이즈가 작거나, 학습을 수행할 때 'Torch: not enough memory...' 오류가 표시된다면 batch_size를 작게해야한다.

 

#미니 배치 크기
batch_size = 32

#dataloader 작성
train_dataloader = torch.utils.data.DataLoader(
    train_dataset, batch_size = batch_size, shuffle=True
)

val_dataloader = torch.utils.data.DataLoader(
    val_dataset, batch_size = batch_size, shuffle=False
)

#쉽게 다루기 위해 사전형 변수에 정리
dataloaders_dict = {"train":train_dataloader, "val": val_dataloader}

#동작 확인
#반복자(iterator)로 변환
batch_iterator = iter(dataloaders_dict["train"])
inputs, labels = next(
    batch_iterator) #첫번째 요소를 추출
print(inputs.size())
print(labels)

 

6. 모델 작성

 

데이터 사용 준비가 완료되었으면, 모델을 만든다

 

앞에서 학습된 vgg-16 모델을 로드한다.

 

출력 유닛의 수는 개미와 벌로 2가지이다.

 

vgg-16 모델의 classifier 모듈 끝에 있는 fully connected layer 층을 교체한다.

 

"net.classifier[6] = nn.Linear(in_features=4096, out_features=2)"를 실행하면 출력 유닛이 두개인 fully connected layer 층으로 교체된다.

 

원래는 forward 함수를 정의해야 모델을 작성하는 것이지만, 이미 학습된 모델을 사용하므로 forward 함수는 구현할 필요 없다.

 

#학습된 vgg-16 모델을 로드
#vgg-16 모델의 인스턴스를 생성

use_pretrained = True #학습된 파라미터를 사용한다
net = models.vgg16(pretrained = use_pretrained)

#vgg-16의 마지막 출력층의 출력 유닛을 개미, 벌 2개로 바꾼다

net.classifier[6] = nn.Linear(in_features=4096, out_features=2)

#훈련 모드
net.train()

print("네트워크 설정 완료: 학습된 가중치를 읽어들여 훈련 모드로 설정했다.")

 

7. 손실함수 정의

 

모델을 작성했으면, 손실함수를 정의한다.

 

이번 이미지 분류 작업은 일반적인 클래스 분류이다.

 

크로스 엔트로피 오차 함수를 사용

 

크로스 엔트로피는 fully connected layer에서 나온 출력에 대해 소프트맥스 함수를 적용하고, 클래스 분류의 손실함수인 음의 로그 가능도 NLL(negative log likelihood loss)을 계산

 

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

 

8. 최적화 기법 설정하기

 

최적화 방법을 정한다.

 

먼저 transfer learning으로 학습하고, 변화시킬 파라미터를 설정

 

네트워크 모델의 파라미터에 대해 requires_grad = True로 설정한 파라미터는 역전파로 gradient를 계산해서, 학습 시에 값이 변한다.

 

파라미터를 고정시켜 갱신하지 않도록 설정하려면, requires_grad= False로 설정

 

#전이학습에서 학습시킬 파라미터를 params_to_update 변수에 저장
params_to_update = []

#학습시킬 파라미터 이름
update_param_names = ["classifier.6.weight", "classifier.6.bias"]

#학습시킬 파라미터 이외에는 gradient를 계산하지 않고, 변하지 않게 설정
for name, param in net.named_parameters():
    if name in update_param_names:
        
        param.requires_grad = True
        params_to_update.append(param)
        print(name)
    
    else:
        
        param.requires_grad = False

#params_to_update의 내용을 확인
print("---------------")
print(params_to_update)

 

이렇게 parameter를 업데이트하고자 하는 layer를 설정했고, 그러한 파라미터는 params_to_update에 있다.

 

이것을 optim.SGD의 params에 넣어주면, 그러한 파라미터만 업데이트 된다.

 

# 최적화 기법 설정
optimizer = optim.SGD(params=params_to_update, lr=0.001, momentum=0.9)

 

 

9. 학습 및 검증

 

모든 준비가 완료되면 학습, 검증을 실시

 

모델을 훈련시키는 train_model 함수를 정의

 

train_model은 학습과 검증을 에폭마다 교대로 실시한다

 

학습 시 net을 훈련모드로, 검증시에는 검증 모드로

 

파이토치에서 학습과 검증으로 네트워크 모드를 전환하는 것은, 드롭아웃 층과 같이 학습과 검증에 동작이 서로 다른 층이 있기 때문이다.

 

코드의 with torch.set_grad_enabled(phase == 'train'):은 학습시에만 gradient를 계산하는 설정

 

검증시에는 gradient를 게산할 필요가 없어서 생략

 

#모델을 학습시키는 함수 작성

def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs):
    
    #epoch 루프
    for epoch in range(num_epochs):
        
        print('Epoch {}/{}'.format(epoch+1, num_epochs))
        print('------------')

        #epoch별 학습 및 검증 루프
        for phase in ['train','val']:
            
            if phase == 'train':
                
                net.train() #모델을 훈련모드로
            
            else:
                
                net.eval() #모델을 검증 모드로

        
            epoch_loss = 0.0 #epoch 손실의 합
            epoch_corrects = 0 #epoch 정답의 수

            #미 학습시 검증 성능을 확인하기 위해 epoch=0의 훈련 생략
            if (epoch == 0) and (phase == 'train'):

                continue

            #데이터 로더로 미니 배치를 꺼내는 루프
            for inputs, labels in tqdm(dataloaders_dict[phase]):

                #optimizer를 초기화
                optimizer.zero_grad()

                #forward path
                with torch.set_grad_enabled(phase == 'train'):

                    outputs = net(inputs)
                    loss = criterion(outputs, labels) #손실
                    _, preds = torch.max(outputs, 1) #라벨

                    #훈련시에는 역전파
                    if phase == 'train':

                        loss.backward()
                        optimizer.step()

                    #반복 결과 계산
                    #loss 합계 갱신

                    epoch_loss += loss.item() * inputs.size(0)

                    #정답 수의 합계 갱신
                    epoch_corrects += torch.sum(preds == labels.data)

            #epoch당 loss와 정답률 표시

            epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset)

            epoch_acc = epoch_corrects.double()/len(dataloaders_dict[phase].dataset)

            print('{}: Loss:{:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))

 

반복문에서 loss.item()*inputs.size(0)을 누적합해주는 부분은, loss에는 미니배치로 평균 손실이 저장되어 있다.

 

이 값을 loss.item()으로 꺼내고

 

손실은 미니 배치 크기의 평균값으로 되어, 미니 배치의 크기인 input.size(0) = 32를 곱해서 미니 배치의 총 손실을 구한다.

 

실제로 학습과 검증을 수행해본다.

 

#학습, 검증 실시
num_epochs = 2
train_model(net, dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)

 

학습 결과는 다음과 같다.

 

 

처음에는 train을 건너 뛰고 검증해보니 42%

 

1epoch학습으로 train이 65%, 검증이 94%까지 올라갔다.

 

겨우 1epoch으로 200장의 학습 데이터에서 개미와 벌의 이미지를 정확하게 학습하였다.

 

훈련데이터의 정답률이 2epoch에서 검증 데이터보다 낮은 것은 2가지 이유가 있다.

 

첫번째는 훈련 데이터 학습이 여덟번 반복하는 동안 네트워크는 학습하면서 성능이 높아진다.

 

검증데이터는 여덟번 반복하여 학습한 네트워크에서 추론한 결과이므로 성능이 좋아진다.

 

둘째, 훈련 데이터는 data augmentation이 적용되어 데이터 확장에 따라 이미지가 변형된다.

 

여기서 크게 변형되면 분류가 어려워진다.

 

소량의 데이터에서도 학습된 모델을 이용한 전이학습으로 높은 성능의 딥러닝을 실현할 수 있다.

 

 

TAGS.

Comments