pytorch 재활훈련2 -사전학습된 vgg16 사용하기-

1. ImageNet, VGG-16

 

Pytorch는 ImageNet 데이터셋중 ILSVRC2012(클래스 1000개, 학습데이터:120만장, 검증 데이터:5만장, 테스트데이터:10만장)으로 신경망의 결합 파라미터를 학습한 다양한 모델을 사용가능

 

VGG-16은 2014년 ILSVRC에서 2위를 차지한 합성곱 신경망

 

옥스퍼드대의 VGG팀이 16층으로 구성해서 VGG-16

 

11,13,19층 다양한 버전의 모델도 존재함

 

구성이 간단하여 다양한 딥러닝 응용 기술의 기반 네트워크로 사용

 

 

2. 사용할 패키지 불러오기

 

import numpy as np

import json

from PIL import Image

import matplotlib.pyplot as plt
%matplotlib inline

import torch
import torchvision
from torchvision import models, transforms

#파이토치 버전
print("PyTorch Version: ", torch.__version__)
print("Torchvision Version: ", torchvision.__version__)

 

3. 학습된 VGG-16 모델 읽기

 

ImageNet으로 파라미터를 사전학습한 VGG-16모델을 읽어온다

 

#학습된 VGG-16 모델 읽어오기
#처음 실행시에는, 학습된 파라미터를 다운로드해야해서, 시간이 좀 걸린다

##vgg-16 모델 인스턴스 생성

use_pretrained = True #학습된 파라미터 사용하겠다

net = models.vgg16(pretrained = use_pretrained)
net.eval() #추론모드(평가모드)

#모델의 네트워크 구성을 출력
print(net)

 

출력 결과를 살펴보자

 

VGG(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU(inplace=True)
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU(inplace=True)
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU(inplace=True)
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU(inplace=True)
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU(inplace=True)
    (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU(inplace=True)
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU(inplace=True)
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU(inplace=True)
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): ReLU(inplace=True)
    (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (27): ReLU(inplace=True)
    (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (29): ReLU(inplace=True)
    (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
  (classifier): Sequential(
    (0): Linear(in_features=25088, out_features=4096, bias=True)
    (1): ReLU(inplace=True)
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU(inplace=True)
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=4096, out_features=1000, bias=True)
  )
)

 

features와 classifier라는 두 모듈로 나누어져있다

 

각 모듈속에는 합성곱 층(convolution)과 전결합 층(fully connected layer = linear layer) 이 있다

 

 

vgg-16은 이름처럼 16층은 아니고 38층으로 구성됨

 

여기서 16층은 convolution과 fully connected layer 층의 수를 나타낸다.

 

입력의 크기는 RGB 채널 3, 높이, 너비 224pixel로 [batch_num, 3, 224, 224]가 된다.

 

batch_num은 미니 배치의 사이즈

 

입력은 처음에 3*3 크기의 64채널 convolution filter + ReLU를 2번 넘어간다.

 

이후 2*2크기의 max pooling을 통과한다.

 

그 결과 입력의 크기는 절반인 112*112가 된다.

 

convolution, ReLU, max pooling을 5번 통과하고 최종 features 모듈 끝에 있는 max pooling을 빠져나오면 (512,7,7) 텐서가 된다.

 

파이토치에서 처리하는 데이터는 tensor라고 부른다.

 

입력은 features 모듈을 통과하고, classifier 모듈에 들어간다.

 

첫번째 전결합층은 입력 요소 수가 25088, 출력 수가 4096이다.

 

25088은 classifier로 들어오는 입력 크기 (512,7,7)에서 512*7*7 = 25088

 

fully connected layer 이후에는 ReLU, dropout 층을 통과

 

여기에 fully connected layer, ReLU, dropout 조합을 통과해서 마지막에는 출력 유닛의 수가 1000인 fully connected layer를 통과한다.

 

출력 수 1000은 ImageNet 데이터셋 클래스 수인 1000에 대응한다

 

 

4. 입력의 전처리 클래스

 

학습된 vgg-16 모델을 읽은 후에는 vgg-16 이미지를 입력하는데 필요한 전처리 부분을 작성

 

전처리로 이미지 크기를 224*224로 변경하고, 색상 정보를 normalization한다.

 

색상 정보의 normalization은 RGB를 평균 (0.485, 0.456,0.406), 표준편차 (0.229, 0.224, 0.225)의 조건으로 표준화

 

해당 normalization조건은 ILSVRC2012 데이터셋의 지도 데이터로 구해지는 값이다.

 

사전학습된 VGG-16 모델은 이 조건으로 전처리한 이미지를 학습했기때문에, 같은 전처리를 해줘야한다.

 

이미지의 전처리 클래스로 BaseTransform 클래스 만들기

 

#입력 이미지의 전처리 클래스

class BaseTransform():
    
    """
    이미지의 크기를 resize하고, 색상을 normalization한다.

    attributes
    -----------

    resize : int
        리사이즈 대상의 이미지 크기
    
    mean : (R,G,B)
        각 색상 채널의 평균
    
    std : (R,G,B)
        각 색상 채널의 표준편차
    
    """

    def __init__(self, resize, mean, std):
        
        self.base_transform = transforms.Compose([
            transforms.Resize(resize), #짧은 변의 길이가 resize 크기
            transforms.CenterCrop(resize), #이미지 중앙을 resize * resize로 자르기
            transforms.ToTensor(), #torch tensor로 변환
            transforms.Normalize(mean,std) #색상 정보의 표준화
        ])
    
    #인스턴스를 구체적인 함수를 지정하지 않고, 호출하면 실행시키는 매직 메소드
    def __call__(self, img):
        
        return self.base_transform(img)

 

만든 클래스가 어떻게 동작하는지 확인해본다

 

#이미지 전처리 동작 확인하기

#1. 이미지 읽기

image_file_path = '/content/data/goldenretriever.jpg'
img = Image.open(image_file_path) #[높이][너비][RGB]

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

#3. 전처리된 이미지 표시
resize = 224
mean = (0.485, 0.456, 0.406)
std = (0.229, 0.224, 0.225)

transform = BaseTransform(resize,mean,std)

#인스턴스 이름으로 호출하면 __call__함수 실행
img_transformed = transform(img)

#PIL 라이브러리는 [높이, 너비, 색상]으로 이미지를 다룸
#[색상, 높이, 너비]를 [높이, 너비, 색상]으로 변환하고 0-1로 값을 제한하여 표시
#그냥 넣으면 에러남

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

 

참고로 파이토치는 이미지를 [channel, height, width]로 다루지만, PIL 라이브러리에서는 [height, width, channel]로 다룬다.

 

imshow 할때, [높이, 너비, 채널]로 안들어가면 에러남

 

원본

 

 

전처리 이미지

 

 

5. 출력 결과로 라벨 예측하는 후처리 클래스

 

vgg-16 모델의 1000차원 출력을 라벨명으로 변환하는 ILSVRCPredictor 클래스 생성

 

ILSVRC의 라벨명은 사전에 준비한 imagenet_class_index.json 사용

 

 # ILSVRC 라벨 정보를 읽어 사전형 변수를 생성합니다
ILSVRC_class_index = json.load(open('/content/data/imagenet_class_index.json','r'))

ILSVRC_class_index

"""
 (전략)
 
 '697': ['n03877472', 'pajama'],
 '698': ['n03877845', 'palace'],
 '699': ['n03884397', 'panpipe'],
 '700': ['n03887697', 'paper_towel'],
 '701': ['n03888257', 'parachute'],
 '702': ['n03888605', 'parallel_bars'],
 '703': ['n03891251', 'park_bench'],
 '704': ['n03891332', 'parking_meter'],
 '705': ['n03895866', 'passenger_car'],
 '706': ['n03899768', 'patio'],
 '707': ['n03902125', 'pay-phone'],
 '708': ['n03903868', 'pedestal'],
 '709': ['n03908618', 'pencil_box'],
 '710': ['n03908714', 'pencil_sharpener'],
 '711': ['n03916031', 'perfume'],
 '712': ['n03920288', 'Petri_dish'],
 '713': ['n03924679', 'photocopier'],
 '714': ['n03929660', 'pick'],
 '715': ['n03929855', 'pickelhaube'],
 '716': ['n03930313', 'picket_fence'],
 '717': ['n03930630', 'pickup'],
 '718': ['n03933933', 'pier'],
 
 (후략)
 """

 

vgg-16 모델로 출력된 값은 torch.Size([1,1000])크기의 파이토치 텐서 형식

 

이를 numpy array로 변환

 

먼저 출력 값을 네트워크에서 분리하는 .detach()를 적용

 

분리한 텐서에 .numpy()를 적용하여 numpy형으로 변환

 

np.argmax()로 최댓값 인덱스를 얻는다.

 

이를 maxid = np.argmax(out.detach().numpy())처럼 한 줄로 표현

 

이후에는 maxid에 해당하는 라벨명을 사전형 변수 ILSVRC_class_index에서 얻는다.

 

#출력 결과에서 라벨을 예측하는 후처리 클래스

class ILSVRCPredictor():
    
    """
    ILSVRC 데이터에 대한 모델의 출력에서 라벨을 구함

    Attributes
    ---------------
    class_index : dictionary
           클래스 index와 라벨명을 대응시킨 사전형 변수
    
    """

    def __init__(self, class_index):
        
        self.class_index = class_index
    
    def predict_max(self, out):
        
        """
        최대 확률의 ILSVRC 라벨명 가져오기

        Parameters
        --------------

        out : torch.Size([1,1000])
            
            Net에서 출력
        
        Returns
        ---------------

        predicted_label_name : str
                가장 예측 확률이 높은 라벨명
        
        """

        maxid = np.argmax(out.detach().numpy())
        predicted_label_name = self.class_index[str(maxid)][1]

        return predicted_label_name

 

6. 사전학습 vgg모델로 이미지 결과 예측

 

이미지 전처리용 BaseTransform 클래스와 네트워크 출력의 후처리용 ILSVRCPredictor 클래스 생성

 

 

입력 이미지가 BaseTransform 클래스로 전처리 후에 vgg-16 모델에 들어감

 

모델에서 1000차원의 출력 torch.Size([1,1000])이 나오고, ILSVRCPredictor에 의해 가장 예측 확률이 높은 라벨명으로 변환되어 최종 예측 결과 출력

 

이러한 흐름을 구현하여 학습된 VGG 모델로 이미지를 예측

 

파이토치 네트워크에 이미지를 입력할 때 데이터를 미니 배치 형태로 준다

 

unsqueeze_(0)를 사용하여 입력 데이터에 미니 배치의 차원 추가

 

 

#ILSVRC 라벨 정보를 읽어 사전형 변수 생성

ILSVRC_class_index = json.load(open('/content/data/imagenet_class_index.json','r'))

#ILSVRCPredictor 인스턴스
predictor = ILSVRCPredictor(ILSVRC_class_index)

#입력 이미지 읽기
image_file_path = '/content/data/goldenretriever.jpg'

img = Image.open(image_file_path) #[높이][너비][색]

#전처리 후에 배치 크기 차원 추가
#전처리 클래스
transform = BaseTransform(resize, mean, std)

img_transformed = transform(img) #torch.Size([3,224,224])

inputs = img_transformed.unsqueeze_(0) #torch.Size([1,3,224,224])

#모델에 입력하고, 모델 출력을 라벨로 변환함
out = net(inputs) #torch.Size([1,1000])
result = predictor.predict_max(out)

#예측 결과 출력
print("입력 이미지의 예측 결과: ", result)

"""
입력 이미지의 예측 결과:  golden_retriever
"""

 

프로그램을 실행하면, golden_retriever가 출력되고 골든 리트리버 이미지를 제대로 분류한 것을 확인할 수 있습니다.

TAGS.

Comments