본문 바로가기
딥러닝/프로젝트

[개인]R-CNN 을 이용한 BCCD type 구분 -(2) fine tuning

by 혜 림 2021. 6. 29.

R-CNN 논문 리뷰는 지난 포스팅을 참고하길 바란다: https://hyelimkungkung.tistory.com/25

 

Rich feature hierarchies for accurate object detection and semantic segmentation

Rich feature hierarchies for accurate object detection and semantic segmentation 늘 느끼는 거지만... 말로 이해한 걸 표현하는 건 너무 어렵다 말로 느끼는 걸 표현하는 건 쉬운데 이해한 걸 표현하기는 어..

hyelimkungkung.tistory.com

프로젝트 개요

(0) 서론, 데이터 셋 소개: https://hyelimkungkung.tistory.com/27
(1) selective serarch: https://hyelimkungkung.tistory.com/28?category=935193
(2) fine tuning: https://hyelimkungkung.tistory.com/29
(3) linear svm, (4) 결론:  https://hyelimkungkung.tistory.com/30

 

(2) Fine Tuning 

 

 

이번의 개요는 다음과 같다. 

 

(1) positive sample, negative sample 을 32:96의 비율로 배치 만들기

(2) pre train 한 알렉스 넷을 통해 훈련시키기

(3) 훈련한 모델을 저장하기

 

 

(1) positive sample, negative sample 을 32:96의 비율로 배치 만들기

custom.py

 

pytorch에서 커스텀 데이터 셋을 만들고, 이용하기 위해서는 

torch.utils.data.Dataset과 torch.utils.data.DataLoader를 주로 사용한다. 

dataset은 말 그대로 데이터 셋 클래스를 만드는 것이고, dataloader는 데이터셋을 배치별로 로드하는 역할이다

본 프로젝트에서 사용한 내용은 아래와 같다. 

 

 

필요한 라이브러리
from random import random

import torch
from torch.utils.data import Sampler
from torch.utils.data import Dataset
class CustomDataset
class CustomDataset(Dataset):
  def __init__(self):
    
    self.x_data = train_images
    self.y_data =[ [i] for i in train_labels]

  def __len__(self):

    return len(self.x_data)

  def __getitem__(self, idx):

    x = torch.FloatTensor(self.x_data[idx])
    y = torch.LongTensor(self.y_data[idx])

    return x,y

만약 본인이 custom dataset을 만들고 싶다면 3 함수만 정의해주면 된다

__init__ , __len__, __getitem__

 

하나씩 보도록 하자

 

우선 torch.utils.data.Dataset 클래스를 상속받는다

 파이썬에서 상속은 class 상속받을클래스명 (상속할 클래스명) 으로 이루어진다

class CustomDataset(Dataset):

 파이썬에서 상속은 class 상속받을클래스명 (상속할 클래스명) 으로 이루어진다

 

먼저 __init__은 

 

  def __init__(self):
    
    self.x_data = train_images
    self.y_data =[ [i] for i in train_labels]

이렇게 생긴 놈이다. class 에서 __init__은 객체가 생성되면 바로 실행하는 함수를 의미한다.

train_images나 train_labels는 글로벌 변수다. 

 

이 init을 통해서 우리가 이용할 데이터를 이케 등록한다? 정도로 생각하면 될 것 같다.

 

다음으로 __len__은 전체 데이터의 수다.

self.x_data나 self.y_data 수는 당근 똑같아야 하니, x_data의 길이가 아니라 y_data를 사용해도 무방할 것이다

    def __len__(self):
        return len(self.x_data)

 

마지막으로 __iter__은 매 iter마다 데이터를 가져오는 애다.

아까 __init__에서 전체 데이터를 등록했으니 거기서 하나씩 가져오게 하는 녀석인거다

이 함수 덕분에 우리가 for 문을 통해서 데이터 하나씩 읽어올 수 있는 것이다

    def __getitem__(self, idx):
        x = torch.FloatTensor(self.x_data[idx])
        y = torch.LongTensor(self.y_data[idx])

        return x, y

필요에 따라 더 많은 값을 return 할 수 있을 것이다. 만약 그랬다면 변수 생성에 주의하자. 

 

 

class customsampler
## custom batch 만들기 32개의 positive와 96개의 negative
import random 
import numpy as np
from torch.utils.data import Sampler

class customsampler(Sampler):

  def __init__(self, data, positive_num, negative_num):

    self.data = data
    self.total = len(self.data)
    self.positive_num = positive_num
    self.negative_num = negative_num

    self.batch_size = self.positive_num + self.negative_num
    self.iter = self.total // self.batch_size

    self.positive_list = len([i for i, predict in enumerate(self.data) if predict != 0 ])

    self.idx = list(range(self.total)) 

  def __iter__(self):
    batch = list()

    for i in range(self.iter):

      temp = np.concatenate((random.sample(self.idx[:self.positive_list], self.positive_num),
                            random.sample(self.idx[self.positive_list:], self.negative_num)))

      random.shuffle(temp)
      batch.extend(temp)
    return iter(batch)

  def __len__(self) -> int:
    return self.iter * self.batch_size

  def get_num_batch(self) -> int:
    return self.iter




 

 fine tuning에서 우리는 32 개의 positive sample과 96개의 negative sample로 하나의 배치를 구성해야한다. 

 그러기 위해서 sampler 클래스를 상속하고 있는 것을 알 수 있다. 

 sampler는 이후 dataload의 파라미터로 들어가게 된다. 

 

 애초부터 positive_data 리스트와 negative_data 리스트를 만들어뒀던 게 아니기 때문에 나는 이 클래스를 통해서 구분을 만들었다. 

 

-> 은 python 에서 function annotation 이란 것이다. 실은 이번에 처음 알게 됐다.

다른 언어를 한 번 다뤄본 사람이라면 파이썬이 가진 특징 중 하나가 바로 변수를 선언할 때 변수 타입을 지정하지 않는 것이다. C 할때는 항상 int i 이런 식으로 선언했었고... javascript에서도 var, const 이렇게 선언을 했었는데 파이썬은 그런게 아무 필요가 없다. 그래서 솔직히 C하다가 파이썬으로 돌아왔을 때는 이 부분이 제일 불편했다. 뭔지 명시적으로 해놓고 시작하지 않으니까 내가 하나씩 다 나중에 확인해야 하는 불편쓰... 

 하여튼 그런 점에서 -> 은 def를 통해서 만든 사용자 지정 함수의 return 값의 타입을 명시해놓는 기능인 것이다. 

 

 위에서 __len__에 -> int 라고 쓰인 것은 결과값은 int라는 것을 말해준 것이다. 

 실제로 코드에는 영향을 주지 않고, 주석같으 느낌이라고 한다. 

 

참고로 여기서 헷갈릴 만한 건 iter, epoch, 그리고 batch인데.

 

 전체 dataset을 batch 사이즈로 나눈만큼 iter 해서 학습고, iter 한 번에는 batch 사이즈만큼만 훈련한다. 

 이 과정을 epoch만큼 반복하는 거다. 그러면 배치의 수가 iter이 되는 것을 이해할 수 있을 것이다.  

 

 

 

(2) pre train 한 알렉스 넷을 통해 훈련시키기

fineTuning.py

 

import torch
import torchvision.models as models
import torch.nn as nn
from torch.utils.data import DataLoader

from selectiveSearch import region_proposal
from customdata import CustomSampler,CustomDataset

positive_num = 32
negative_num = 96
batch_size = positive_num + negative_num

train_images, train_labels = region_proposal('finetune')
print('region_proposal ended')

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

model = models.alexnet(pretrained=True)
num_features = model.classifier[6].in_features

model.classifier[6] = nn.Linear(num_features, 4)
model = model.to(device)
criterion = nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.0001)

dataset = CustomDataset(train_images, train_labels)
sampler = CustomSampler(train_labels, positive_num, negative_num)
dataloader = DataLoader(dataset, batch_size=batch_size, sampler=sampler)

total_batch = len(dataloader)

epochs = 15
for epoch in range(epochs):
    avg_cost = 0.0
    for num, data in enumerate(dataloader):
        imgs, labels = data

        imgs = imgs.to(device)
        labels = labels.to(device)

        model.train()
        optimizer.zero_grad()

        print(imgs.shape)
        out = model(imgs.permute(0, 3, 1,2))  ## pytorch의 경우 인자의 순서가 (batchsize, channel, height, width) 우리는 보통 (batchsize,height,width,channel)이라고 생각하는데
        labels = labels.squeeze()  ## crossentropy input size 가 (N,C) 배치사이즈, 클래스 수 target size는 (N) 이 돼야해서 squeeze로 차원 변경
        loss = criterion(out, labels)
        loss.backward()
        optimizer.step()

        avg_cost += loss / total_batch

    print(f'[Epoch:{epoch + 1}] cost = {avg_cost}')
print('Learning Finished!')

 

보면 pre trained 한 알렉스 넷 classifer의 마지막 layer output node수만 바꾼 걸 알 수 있다.

우리가 classify하고 싶은 3가지의 class 에 background 하나까지 더해서 총 4개다. 

 

fine tuning에선 softmax로 학습하고, linear SVM에서는 class 별로 linear svm을 시행한다. 

그런데 참고한 구현에서 2개의 output node가 필요한 경우만 있어서 씁 어렵다 class 별로 하는게..

 

근데 사실 그때는 gpu의 한계 때문에 class specific linear SVM을 한 거니까 그냥 softmax를 써도 되지 않을까? 하는 마음...

 

아 그리고 보면 model.train()이 있는데, model.eval()과 함께 알아두자. 

model.eval()은 evaluate 모드? 정도로 생각하면 될 것 같다. 그래서 저때는 dropout 등을 시행하지 않는다. 

model.eval()과 with torch.no_grad 를 함께 쓴다고 한다. 

 

(3) 훈련한 모델을 저장하기

torch.save(model.state_dict(), 'data/content/BCCD/fine_tuned.pth')  ## 가중치들 저장

 

댓글