jetson tx2 developer kit 를 이용한 image binaryclassification(pytorch)

심우석·2022년 7월 13일
1

computer vision

목록 보기
1/2
post-thumbnail

오늘은 젯슨 보드를 이용한 개 고양이 이진분류를 해보겠습니다.

사진 데이터는 kaggle의 dog vs cat dataset을 가져와서 사용했고 그중 trainset 개 고양이 600장 testset 200장씩 사용했습니다

https://www.kaggle.com/c/dogs-vs-cats

jetson 보드의 원활한 사용을 위해서는 jetpack 설치를 권장합니다.(아래 링크 참조)

https://mickael-k.tistory.com/87

젯슨에서 pytorch 를 사용하려면 다운받은 젯팩버전에 따라 torch 와 torchvision을 다운받아야 합니다.

다운 방법은 아래 링크참고

https://forums.developer.nvidia.com/t/pytorch-for-jetson-version-1-11-now-available/72048

저는 코드를 cell별로 실행하기 위해 spyder3를 이용하여 python3을 실행했습니다.

참고로 저는

python 3.6.9

jetpack 4.6.0

torch 1.8.0

torchvision 0.9.1버전을 사용했습니다.

torchvision은 0.9.0을 권장하나 저는 torchvision의 인자들이 실행되지 않아서 위의 방법으로 다운후

가장 가까운 버전인 0.9.1로 업그레이드 했습니다.

이어서 코드입니다.

# -*- coding: utf-8 -*-
import os 
import torch
import torchvision
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') #GPU 할당
print(device)
from PIL import Image
import glob
import torch.nn as nn
import torch.optim as optim
import numpy as np
import random
from tqdm import tqdm
from torch.optim import optimizer
from torchvision import transforms
from torchvision import models

코드 작업에 앞서 필요한 application들을 import 해준후 device를 확인합니다.

혹시 device가 cuda가 아닌 cpu로 나타나시는 분들은 torch 삭제후 알맞은 버전으로 재설치 하시면 됩니다.

print(torchvision.__version__)
print(torch.__version__)

시작에 앞서 버전을 확인하여 줍니다

#하이퍼 파라미터 튜닝
CFG = {
    'IMG_SIZE':64, #이미지 사이즈128
    'EPOCHS':50, #에포크
    'BATCH_SIZE':64, #배치사이즈
    'SEED':41, #시드
}

먼저 하이퍼 파라미터들을 수정이 쉽게 선언해 줍니다.

def seed_everything(seed):
    random.seed(seed)
    os.environ['PYTHONHASHSEED'] = str(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = True

seed_everything(CFG['SEED'])

값들이 계속 바뀌지 않게 random seed를 고정시켜줍니다.

train_path="/home/nvidia/Pictures/train/"
test_path="/home/nvidia/Pictures/test/"
Dataset_path = "/home/nvidia/Downloads/"

각각의 경로를 설정하여 줍니다.

train_transform = torchvision.transforms.Compose([
                    #transforms.ToPILImage(), #Numpy배열에서 PIL이미지로
                    transforms.Resize([CFG['IMG_SIZE'], CFG['IMG_SIZE']]), #이미지 사이즈 변형
                    transforms.RandomHorizontalFlip(), # Horizontal Flip
                    # transforms.RandomCrop(32, padding=2), 
                    transforms.RandomRotation(degrees=10,interpolation=transforms.InterpolationMode.NEAREST),
                    transforms.RandomPerspective(distortion_scale=.15,p=.15,interpolation=transforms.InterpolationMode.NEAREST),
                    transforms.ToTensor(), #이미지 데이터를 tensor
                    transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) #이미지 정규화
                    
                    ])

test_transform = torchvision.transforms.Compose([
                    #transforms.ToPILImage(),
                    transforms.Resize([CFG['IMG_SIZE'], CFG['IMG_SIZE']]),
                    transforms.ToTensor(),
                    transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))
                    ])

augmentation을 포함하여 이미지를 변형할 형태를 만들어줍니다.

계산량이 늘어날시 커널이 죽는 현상이 발생하므로 저는 계산량 축소를 위해 이미지 사이즈를 64*64로 비교적 작게 설정하였습니다.

각 이미지의 픽셀들을 평균 0.5 표준편차 0.5로 정규화 시켜줍니다.

class catdogDataset(Dataset): 
    def __init__(self, path, train=True, transform=None): 
        self.path = path 
        if train: 
            self.cat_path = train_path + 'cat/' 
            self.dog_path = train_path + 'dog/' 
        else: 
            self.cat_path = test_path + 'cat/' 
            self.dog_path = test_path + 'dog/' 
        self.cat_img_list = glob.glob(self.cat_path + '/*.jpg') 
        self.dog_img_list = glob.glob(self.dog_path + '/*.jpg') 
        self.transform = transform 
        self.img_list = self.cat_img_list + self.dog_img_list 
        self.label_list = [0] * len(self.cat_img_list) + [1] * len(self.dog_img_list) 
    def __len__(self): 
        return len(self.img_list) 
    def __getitem__(self, idx): 
        img_path = self.img_list[idx] 
        label = self.label_list[idx] 
        img = Image.open(img_path) 
        if self.transform is not None: 
            img = self.transform(img) 
        return img, label 

파일별로 이미지들이 들어가 있기 때문에 이미지가 들어간 폴더의 이름에 따라 labeling을 설정한 dataset을 만들 틀을 만들어 줍니다.

train_dataset = catdogDataset(path=train_path, train=True, transform=train_transform) 
vali_dataset=catdogDataset(path=test_path, train=False, transform=test_transform)
#for i in range(4):
#    train_dataset+=catdogDataset(path=train_path, train=True, transform=train_transform) 
train_loader = DataLoader(train_dataset, batch_size = CFG['BATCH_SIZE'], shuffle=True, num_workers=0)
vali_loader = DataLoader(vali_dataset, batch_size = CFG['BATCH_SIZE'], shuffle=False, num_workers=0) 

위의 dataset 틀을 사용해서 train data와 test data를 dataset으로 만들고 dataloader에 로드시켜줍니다.

역시 계산량의 축소를 위해서 대이터의 증가는 주석처리해줌 모습입니다.

model = models.vgg11(progress=True)
#%%
model.classifier
#%%
print(model)

모델은 torchvision의 모델중 비교적 계산량이 적은 vgg11모델을 가져왔습니다.

model.fc = nn.Sequential(
    nn.Linear(1000,2)
)
model = model.to(device)

저희는 이진분류이기 때문에 마지막에 결과 클래스값이 두개로 바뀌는 denselayer를 추가시켜 줍니다.

그후 model을 device에 할당해줍니다.

criterion = torch.nn.CrossEntropyLoss()
#optimizer = optim.SGD(model.parameters(), 
#                         lr = 0.05,
#                         momentum=0.9,
#                         weight_decay=1e-4)
optimizer=optim.Adam(model.parameters(),lr=1e-4,weight_decay=1e-6)
scheduler = None

loss function은 crossentropy .optimizer는 adam을 사용했습니다.

loss function을 binarycrossentropy로 사용해도 무관합니다.

최적화 방법역시 adam이 아닌 다른 최적화 방법을 사용해도 무관합니다.

def train(model, optimizer, train_loader, scheduler, device):
model.to(device)
n = len(train_loader)

#Loss Function 정의
#criterion = nn.CrossEntropyLoss().to(device)
best_acc = 0
best_apoch=0   
for epoch in range(1,CFG["EPOCHS"]+1): #에포크 설정
    model.train() #모델 학습
    running_loss = 0.0
        
    for img, label in tqdm(iter(train_loader)):
        img, label = img.to(device), label.to(device) #배치 데이터
        optimizer.zero_grad() #배치마다 optimizer 초기화
    
        # Data -> Model -> Output
        logit = model(img) #예측값 산출
        loss = criterion(logit, label) #손실함수 계산
        
        # 역전파
        loss.backward() #손실함수 기준 역전파 
        optimizer.step() #가중치 최적화
        running_loss += loss.item()
        train_loss=running_loss / len(train_loader)  
    print('[%d] Train loss: %.10f' %(epoch, running_loss / len(train_loader)))
    
    if scheduler is not None:
        scheduler.step()
        
    #Validation set 평가
    model.eval() #evaluation 과정에서 사용하지 않아야 하는 layer들을 알아서 off 시키도록 하는 함수
    vali_loss = 0.0
    correct = 0
    
    with torch.no_grad(): #파라미터 업데이트 안하기 때문에 no_grad 사용
        for img, label in tqdm(iter(vali_loader)):
            img, label = img.to(device), label.to(device)

            logit = model(img)
            vali_loss += criterion(logit, label)
            pred = logit.argmax(dim=1, keepdim=True)  #11개의 class중 가장 값이 높은 것을 예측 label로 추출
            correct += pred.eq(label.view_as(pred)).sum().item() #예측값과 실제값이 맞으면 1 아니면 0으로 합산
    vali_acc = 100 * correct / len(vali_loader.dataset)
    print('Vail set: Loss: {:.4f}, Accuracy: {}/{} ( {:.0f}%)\n'
          .format(vali_loss / len(vali_loader), correct, len(vali_loader.dataset), 100 * correct / len(vali_loader.dataset)))
    
    #베스트 모델 저장
    if best_acc < vali_acc:
        best_acc = vali_acc
        best_epoch=epoch
        torch.save(model.state_dict(), './best_model.pth') #이 디렉토리에 best_model.pth을 저장
        print('Model Saved.')
return best_acc,best_apoch,vali_acc,vali_loss,train_loss

다음은 train part입니다.

각 epoch별로 loss function을 optimizer를 이용해 최적화 하는 방향으로 각 parameter를 업데이터시켜 줍니다.

epoch에 의한 overfitting을 예방하기 위해 train 진행중 validation의 정확도가 가장 높을때 모델의 parameter들을 저장해줍니다.

(early stopping)

bestacc,bestepoch,valacc,valloss,train_loss=train(model, optimizer, train_loader, scheduler, device)
위의 train 함수를 이용해 적절한 인자들을 대입해 train시켜줍니다.

train 진행중에는 jetsonboard로 다른 작업을 하는것을 권장하지 않습니다.

과부하로 인해서 kernel이 죽을떄가 많더라구요.

def evaluate(model, device):
model.to(device)

#Loss Function 정의
#criterion = nn.CrossEntropyLoss().to(device)
best_acc = 0
best_apoch=0   
vali_loss=0
correct=0
with torch.no_grad(): #파라미터 업데이트 안하기 때문에 no_grad 사용
    for img, label in tqdm(iter(vali_loader)):
        img, label = img.to(device), label.to(device)
        logit = model(img)
        vali_loss += criterion(logit, label)
        pred = logit.argmax(dim=1, keepdim=True)  #11개의 class중 가장 값이 높은 것을 예측 label로 추출
        correct += pred.eq(label.view_as(pred)).sum().item() #예측값과 실제값이 맞으면 1 아니면 0으로 합산
        vali_acc = 100 * correct / len(vali_loader.dataset)
    print('Vail set: Loss: {:.4f}, Accuracy: {}/{} ( {:.0f}%)\n'.format(vali_loss / len(vali_loader), correct, len(vali_loader.dataset), 100 * correct / len(vali_loader.dataset)))

모델 학습후 그 모델이 얼마나 잘 학습되었는지 확인하는 평가모델입니다.

def predict(model, vali_loader, device):
model.eval()
model_pred = []
with torch.no_grad():
for img,label in tqdm(iter(vali_loader)):
img,label = img.to(device),label.to(device)

        pred_logit = model(img)
        pred_logit = pred_logit.argmax(dim=1, keepdim=True).squeeze(1)

        model_pred.extend(pred_logit.tolist())
return model_pred

라벨링이 되어있지 않은 데이터의 라벨을 예측하는 라벨모델입니다.

efficient net같은 경우에는 line5에서 label이 없어도 작동해서 편했는데 vgg11는 img와 label이 같이 있어야 자동하더라구요.

예측할 dataloader를 임의의 라벨을 대입후 예측값을 보시면 될것 같습니다.

# Validation Accuracy가 가장 뛰어난 모델을 불러옵니다.
checkpoint = torch.load('/home/nvidia/Downloads/best_model.pth')
model = models.vgg11(progress=True)
model.fc = nn.Sequential(
    nn.Linear(1000,2)
)
model = model.to(device)
model.load_state_dict(checkpoint)
early stopping으로 저장해놨던 best model을 불러와서 새 모델의 파라미터들에 넣어줍니다.

evaluate(model,device)
preds = predict(model, vali_loader, device)
preds[0:5]

모델의 정확도를 평가하고 처음 이미지 5개의 예측값을 봅니다.

저같은 경우 이진 분류임에도 불구하고 정확도가 80%밖에 나오지 않더라구요.

하지만 이미지 사이즈를 많이 축소시켰고 augmentation의 부재와

traindata가 1200개로 적었던 것을 생각하면 나쁘지 않은 결과라고 생각합니다.

여러분들은 더 좋은 모델을 만들길 바랄게요.

봐주셔서 감사합니다~

0개의 댓글