안경잡이개발자

728x90
반응형

  파이토치(PyTorch)의 공식 문서에서 전이 학습(transfer learning)에 관해 설명하고 있는 문서는 다음과 같다.

 

  ▶ PyTorch Transfer Learningpytorch.org/tutorials/beginner/transfer_learning_tutorial.html

 

Transfer Learning for Computer Vision Tutorial — PyTorch Tutorials 1.7.1 documentation

Note Click here to download the full example code Transfer Learning for Computer Vision Tutorial Author: Sasank Chilamkurthy In this tutorial, you will learn how to train a convolutional neural network for image classification using transfer learning. You

pytorch.org

  공식 문서에서는 전이 학습(transfer learning)의 대표적인 두 가지 시나리오를 언급한다.

 

  1. 전체 네트워크를 fine-tuning 하는 방식

  2. 사전학습된(pre-trained) 네트워크를 고정된 특징 추출기(fixed feature extractor)로 사용하는 방식

 

  여기에서 1번과 2번의 실제 구현상의 차이점은 사전학습된 네트워크에 대하여 다음의 코드 부분을 넣느냐 마느냐이다. 아래 코드는 사전학습된(pre-trained) 네트워크의 가중치를 고정할 때 사용하는 코드이다.

 

for param in model_conv.parameters():
    param.requires_grad = False

 

  만약 2번 방식(fixed feature extractor)대로 사전학습된 네트워크의 가중치를 특징 추출기로 고정한다면, 뒤쪽에 있는 FC 레이어만 업데이트가 될 것이다. 또한 이 경우에는 앞쪽 레이어에 대한 기울기(gradient)를 계산하지 않아도 되기 때문에 학습 속도가 빨라진다. 참고로 2번의 방식을 사용하는 경우 optimizer에서는 FC 레이어의 파라미터에 대해서만 업데이트한다고 명시해야 한다. (optimizer는 계산된 기울기(gradient)를 이용해 업데이트(update)를 수행한다.)

 

  [참고 1] requires_grad를 False로 설정한 레이어에 대하여 optimizer에서 업데이트를 하겠다고 명시하더라도, 어차피 구해진 gradient 값 자체가 없기 때문에 업데이트가 수행되지 않기는 한다.

 

  [참고 2] PyTorch에서 모델을 초기화하면 기본 설정으로 requires_grad 값이 True가 된다. (이는 사전 학습된 네트워크를 불러올 때에도 마찬가지다!) 따라서 별도로 명시하지 않는다면 자동으로 기울기(gradient)를 추적하기 때문에, 학습하지 않고자 하는 레이어에 대해서는 requires_grad 값을 명시적으로 False로 설정할 필요가 있다.

 

  다만 공식 문서에서는 간단한 이진 분류(binary classification) 예시를 들고 있기 때문에 1번과 2번의 성능 차이가 크게 나지 않으며, 디테일한 하이퍼 파라미터 세팅을 하지 않아도 높은 성능이 나온다. 하지만 실제로 CIFAR-10과 같이 클래스의 개수가 많은 데이터셋을 이용하는 경우에는 성능 차이가 크게 날 수 있다.

 

  필자의 경우 클래스가 3개인 경우, 클래스가 10개인 경우에 대하여 학습을 진행해 보았다. 이때 2번 방법대로 마지막 FC 레이어만 학습하도록 한 경우에는 학습이 정상적으로 수행되지 않았다. 하이퍼 파라미터 세팅에 많은 신경을 써야 하는 것으로 보인다. 따라서 클래스의 개수가 많은 상황에서 빠르게 높은 정확도(high accuracy)를 얻고 싶다면 1번의 방법대로 앞쪽 네트워크를 고정하지 않고 전체 네트워크를 fine-tuning 하는 것이 유리할 수 있다.

 

  예를 들어 필자의 경우 CIFAR-10에 대하여 전이 학습(transfer learning)을 수행한 경험이 있는데, 다른 코드 부분은 완전히 동일하게 유지한 상태로 한 번은 다음과 같은 코드를 사용했다.

 

net = torchvision.models.resnet18(pretrained=True)

# 마지막 레이어의 차원을 10차원으로 조절
num_features = net.fc.in_features
net.fc = nn.Linear(num_features, 10)
net = net.to(device)

 

  그리고 한 번은 다음과 같은 코드를 사용했다.

 

net = torchvision.models.resnet18(pretrained=True)
for param in net.parameters():
    param.requires_grad = False

# 마지막 레이어의 차원을 10차원으로 조절
num_features = net.fc.in_features
net.fc = nn.Linear(num_features, 10)
net = net.to(device)

 

  첫째 경우(fine-tuning)에는 한 번의 epoch만으로 순식간에 94% 정도의 test accuracy를 얻을 수 있었지만, 둘째 경우(fixed feature extractor)에는 여러 번의 epoch을 반복해도 90% 이상의 성능은 얻을 수 없었다. 다시 말해 2번 방법이 학습 속도 측면에서 유리할 수 있으나, 클래스가 많은 상황에서는 성능이 낮게 나오는 문제가 발생할 수 있다.

 

  [참고] 이 문제는 해외 기술 블로그에서도 자주 다루어지고 있는 내용이다. 정리하자면 다음과 같다.

 

  ① Frozen: 앞쪽의 특징 추출기(feature extractor)에 대하여 역전파를 수행하지 않는 방법이다. 일반적으로 목표 작업(target task)의 레이블(label) 수가 적고 오버피팅(overfitting)을 예방하기 위한 목적으로 사용된다.

  ② Fine-tuning: 앞쪽의 특징 추출기(feature extractor)에 대하여 역전파를 수행하는 방법이다. 일반적으로 목표 작업(target task)의 레이블(label) 수가 많을 때 사용한다.

728x90
반응형

728x90
반응형

  최근에 데이터셋을 직접 구축하여, 내가 만든 데이터셋으로 학습(Training)을 해야 하는 일이 생겼다.

 

  PyTorch에서는 ImageFolder라는 라이브러리를 제공한다. 이는 다음과 같은 계층적인 폴더 구조를 가지고 있는 데이터셋을 불러올 때 사용할 수 있다. 다시 말해 다음과 같이 각 이미지들이 자신의 레이블(Label) 이름으로 된 폴더 안에 들어가 있는 구조라면, ImageFolder 라이브러리를 이용하여 이를 바로 불러와 객체로 만들면 된다.

 

dataset/
	0/
		0.jpg
		1.jpg
        	...
	1/
		0.jpg
		1.jpg
		...
	...
	9/
		0.jpg
		1.jpg
		...

 

  한 번 연습을 위해서 기존에 존재하는 CIFAR-10 데이터셋을 불러와서, 이를 계층적인 폴더 구조가 되도록 이미지를 저장하는 소스코드를 만들어 보자. 그 다음에 다시 ImageFolder 라이브러리로 동일한 CIFAR-10 데이터셋을 불러오면 성공이다.

 

  먼저 다음과 같이 기본적으로 PyTorch에서 제공하고 있는 CIFAR-10 데이터셋을 불러와보자.

 

import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import matplotlib.image as image
import numpy as np

transform_train = transforms.Compose([
    transforms.ToTensor(),
])

train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4)

 

  CIFAR-10의 경우 10개의 레이블로 구성된 데이터셋이므로, 각 레이블의 이미지가 몇 번 등장했는지를 기록해주는 변수를 선언하자.

 

import os

num_classes = 10
number_per_class = {}

for i in range(num_classes):
    number_per_class[i] = 0

 

  이후에 이미지 Torch 객체레이블 정수 값이 들어왔을 때, 이를 실제 폴더에 저장해주는 함수를 작성하자.

 

def custom_imsave(img, label):
    path = 'dataset/' + str(label) + '/'
    if not os.path.exists(path):
        os.makedirs(path)
    
    img = img.numpy()
    img = np.transpose(img, (1, 2, 0))
    image.imsave(path + str(number_per_class[label]) + '.jpg', img)
    number_per_class[label] += 1

 

  이제 만들어진 도구들을 이용하여 CIFAR-10 데이터셋에서 데이터를 배치 단위로 읽으며, 배치에 포함된 각 이미지를 하나씩 정확한 폴더에 저장될 수 있도록 하자.

 

def process():
    for batch_idx, (inputs, targets) in enumerate(train_loader):
        print("[ Current Batch Index: " + str(batch_idx) + " ]")
        for i in range(inputs.size(0)):
            custom_imsave(inputs[i], targets[i].item())

process()

 

  이후에 한 번 0번 레이블(비행기)의 첫 번째 이미지를 출력하도록 해보자. 정상적으로 잘 출력된다.

 

from PIL import Image
from matplotlib.pyplot import imshow

img = Image.open('dataset/0/0.jpg')
imshow(np.asarray(img))

 

 

  이제 ImageFolder 라이브러리를 이용해서, 우리가 저장한 이미지들을 이용해 다시 PyTorch 데이터셋 객체로 불러올 수 있는지 확인해보도록 하자.

 

from torchvision.datasets import ImageFolder

train_dataset = ImageFolder(root='./dataset', transform=transform_train)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4)

 

  이미지 출력용 함수는 다음과 같다. PyTorch의 경우 [Batch Size, Channel, Width, Height]의 구조를 가지고 있어서, 이를 matplotlib로 출력하기 위해서는 [Width, Height, Channel]의 순서로 변경해주어야 한다.

 

def custom_imshow(img):
    img = img.numpy()
    plt.imshow(np.transpose(img, (1, 2, 0)))
    plt.show()

 

  이제 이미지를 하나씩 출력하도록 해보자.

 

def process():
    for batch_idx, (inputs, targets) in enumerate(train_loader):
        custom_imshow(inputs[0])

process()

 

  실행 결과, 다음과 같이 정상적으로 데이터셋이 구성되었다는 사실을 알 수 있다.

 

728x90
반응형

728x90
반응형

  실험을 하면서 자주 쓰는 코드인데, 따로 정리를 해놓지 않아서 매 번 입력을 하고 있다. 그래서 정리하려고 한다. 일단 Dataset 객체를 불러올 때는 데이터를 전처리하는 부분이 들어간다. PyTorch의 경우 ToTenser() 함수를 불러오면, 이미지가 자동으로 [0, 1]의 값으로 변경된다. 예를 들어 CIFAR-10 학습용 데이터셋을 불러오는 코드는 다음과 같다.

 

import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

transform_train = transforms.Compose([
    transforms.ToTensor(),
])

train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4)

 

  이렇게 불러온 이미지는 실제로 Tensor 객체로 존재하며, 각 원소의 값이 0부터 1 사이의 값이다. 그렇기 때문에 이를 화면에 출력하고자 한다면, 이 값을 다시 0부터 255 사이의 값으로 늘려야 하는 건지 궁금할 수 있다. 다행히도 파이썬의 matplotlib는 기본적으로 0부터 1사이의 값이라고 해도 알아서 인식하여 정상적인 이미지로 출력해준다. 하지만 별도로 OpenCV 등에서 활용하고자 한다면, 추가적인 전처리가 필요할 수 있다.

 

  또한 기본적으로 PyTorch는 이미지 데이터셋을 [Batch Size, Channel, Width, Height] 순서대로 저장하기 때문에, 이를 matplotlib로 출력하기 위해서는 각 이미지를 [Width, Height, Channel] 형태로 변경해 줄 필요가 있다. 그것은 numpy 라이브러리의 transpose() 함수를 이용하여 해결할 수 있다.

 

def custom_imshow(img):
    img = img.numpy()
    plt.imshow(np.transpose(img, (1, 2, 0)))
    plt.show()

 

  그럼 이제 이렇게 정의된 imshow() 함수를 이용하여 데이터셋의 이미지를 배치당 하나씩 출력해보자.

 

def process():
    for batch_idx, (inputs, targets) in enumerate(train_loader):
        custom_imshow(inputs[0])

process()

 

  실행 결과는 다음과 같다.

 

728x90
반응형