딥러닝/pytorch

HardNet 개발

scjung 2020. 2. 5. 16:30

하도 정리가 안되서 정리용 블로그 시작

 

이미지에서 segmentation 을 추출하는 모델

 

input 이미지의 width,height와 output 이미지의 width, height가 동일

 

input은 [channel, width, height] 라면

output 은 [classes, width, height] 로 각 픽셀별로 이미지화하여 나오게 됨

 

논문은 아래 링크

https://arxiv.org/abs/1909.00948

 

HarDNet: A Low Memory Traffic Network

State-of-the-art neural network architectures such as ResNet, MobileNet, and DenseNet have achieved outstanding accuracy over low MACs and small model size counterparts. However, these metrics might not be accurate for predicting the inference time. We sug

arxiv.org

기존에 mmdetection을 사용하여 segmentation 모델을 개발하였으나 너무 오래 걸려서 더 빠르고 가벼운 hardnet으로 변경

성능 테스트는 일단 학습시켜서 IOU 지표로 비교해보도록 하고

 

 

 

일단 목적은 image classification인데 정확도를 높이기 위해 segmentation을 먼저 앞에서 돌리고 classification을 진행

확실히 그냥 classification 모델 돌리는 것보다 정확도가 어마어마하게 차이 남 (90% 대에서 6~7% 차이)

 

이미지 예시나 정확한 프로젝트 용도는 회사 기밀이라 패스

 

 

 

 

 

pytorch을 사용하여 개발

모델 코드는 아래의 링크를 가져다 사용함

https://github.com/PingoLH/FCHarDNet/blob/master/ptsemseg/models/hardnet.py

 

PingoLH/FCHarDNet

Fully Convolutional HarDNet for Segmentation in Pytorch - PingoLH/FCHarDNet

github.com

평가 지표인 metrix로는 IOU를 사용함

코드는 아래

https://www.kaggle.com/iezepov/fast-iou-scoring-metric-in-pytorch-and-numpy

 

Fast IOU scoring metric in PyTorch and numpy

Explore and run machine learning code with Kaggle Notebooks | Using data from TGS Salt Identification Challenge

www.kaggle.com

참고로 0,1로만 이루어져있는 이미지에 대한 평가 지표라 class에 따른 평가를 수행하기 위해서 아래 코드와 같이 변경하였다

 

import torch
import numpy as np 

SMOOTH = 1e-6

def iou_numpy(outputs: np.array, labels: np.array):
    outputs = outputs.squeeze(1)
    
    intersection = (outputs & labels).sum((1, 2))
    union = (outputs | labels).sum((1, 2))
    
    iou = (intersection + SMOOTH) / (union + SMOOTH)
    
    thresholded = np.ceil(np.clip(20 * (iou - 0.5), 0, 10)) / 10
    
    return thresholded  # Or thresholded.mean()

def iou_pytorch(outputs: torch.Tensor, labels: torch.Tensor, device='cpu'):
    # You can comment out this line if you are passing tensors of equal shape
    # But if you are passing output from UNet or something it will most probably
    # be with the BATCH x 1 x H x W shape
    #outputs = outputs.squeeze(1)  # BATCH x 1 x H x W => BATCH x H x W
    outputs = torch.max(outputs, 1)[1]
    
    intersection = torch.where(((outputs.int() != 0) | (labels.int() != 0)) & (outputs.int() == labels.int()), torch.Tensor([1]).to(device), torch.Tensor([0]).to(device))
    intersection = intersection.float().sum((1, 2))
    
    union = torch.where(((outputs.int() != 0) | (labels.int() != 0)), torch.Tensor([1]).to(device), torch.Tensor([0]).to(device))
    union = union.float().sum((1, 2))
    
    iou = (intersection + SMOOTH) / (union + SMOOTH)  # We smooth our devision to avoid 0/0
    
    thresholded = torch.clamp(20 * (iou - 0.5), 0, 10).ceil() / 10  # This is equal to comparing with thresolds
    
    return thresholded.mean()  # Or thresholded.mean() if you are interested in average across the batch
    

 

 

 

이미지 로딩

 

pytorch의 dataloader를 사용하여 custom

from torch.utils.data import Dataset
import torch
import cv2
import numpy as np

def HardNetLoader(img_list, label_img_list, label_list, batch_size):
    hardnet_dataset = HardNetDataset(img_list, label_img_list, label_list)
    hardnet_dataloader = torch.utils.data.DataLoader(hardnet_dataset, batch_size=batch_size)
    
    return hardnet_dataloader


    
class HardNetDataset(Dataset):
    def __init__(self, img_list, label_img_list, label_list):
        self.img_list = img_list
        self.label_img_list = label_img_list
        self.label_list = label_list
        
        self.samples = len(self.img_list)
        
    def __len__(self):
        return self.samples
        
    def __getitem__(self, idx):
        image = cv2.imread(self.img_list[idx], 1)/255
        image = np.transpose(image, (2,0,1))
        
        label_img = cv2.imread(self.label_img_list[idx], 0)
        label_img[label_img != 0] = self.label_list[idx]

        return image, label_img

메모리가 부족하여 필요할 때마다 opencv 를 통하여 데이터 로딩

 

pytorch에 들어가는 데이터는 [channel, width, height]

하지만 opencv 는 [width, height, channel] 순으로 로딩되어서 transpose 로 순서를 바꿈

 

2020.02.05 : 바보같이 이미지를 255로 안 나누고 진행했었다..... 그래놓고 결과 테스트할 때는 255로 나눠서 넣었으니 안보였지

 

Loss function

 

코드는 아래

https://github.com/meetshah1995/pytorch-semseg/blob/master/ptsemseg/loss/loss.py

 

meetshah1995/pytorch-semseg

Semantic Segmentation Architectures Implemented in PyTorch - meetshah1995/pytorch-semseg

github.com

2 dimentional corss entropy 인데 해당 코드는 treshhold를 사용해서 일정 이하 값의 loss는 버리는 것으로 보인다 (굳이?)

나중에 추가적인 공부가 필요

 

optimizer

 

그냥 pytorch 의 SGD를 사용

optimizer = torch.optim.SGD(hard_net.parameters(), lr=0.02, weight_decay=0.0005, momentum=0.9)

상무님이 SGD 대신 Adam 쓰라고 하셔서 코드 변경

optimizer = torch.optim.Adam(hard_net.parameters(), lr=0.0005, weight_decay=1e-4)

SGD든 Adam 이든 튜닝의 영역이라 일단 기본인 Adam으로 쓰자고 하셨다

 

물론 learning rate는 0.02가 나도 높다고 생각했지만 일단 복붙한거라 안건드렸는데 0.0005로 수정

weight decay는 hardnet adam으로 검색해서 나온 코드에서의 파라미터를 넣었다

 

 

torch용 코드를 또 따로 짬

 

from tqdm import tqdm
from torch import nn
import numpy as np
import torch
import iou
import os

def load(model, path:str, slice_idx:int=0):
    load_state_dict = torch.load(path)
    model_state_dict = model.state_dict()
    
    set_state_dict = {}
    for k, v in load_state_dict.items():
        key = k[slice_idx:]
        if key in model_state_dict:
            set_state_dict[key] = v
            
    model_state_dict.update(set_state_dict)
    model.load_state_dict(model_state_dict)
    print("[LOAD] success at {}".format(path))


def predict(model, data, device='cpu') -> np.ndarray:
    
    if not isinstance(model, nn.Module):
        print('[E0RROR] predict : model isn\'t torch nn.Module')
        raise TypeError
        
    if not isinstance(data, torch.Tensor):
        print('[ERROR] data isn\'t torch tensor')

    model.eval()
    
    with torch.no_grad():
        model = model.to(device)
        data_tensor = data.to(device, dtype=torch.float)
        
        _pred_result = model(data_tensor)
        
    return _pred_result.cpu().numpy()




def validate(model, data_loader, loss_fn, acc_fn=None, device='cpu') -> float:
    if not isinstance(model, nn.Module):
        print('[ERROR] validate : model isn\'t torch nn.Module')
        raise TypeError
    
    model.eval()
    
    _valid_loss = 0.0
    _acc_n = 0
    
    if acc_fn is not None:
        _acc_sum = 0.0
    else:
        _acc_sum = None
    
    with torch.no_grad():
        model = model.to(device)
        
        print("[VALID] start")
        for _data, _label in tqdm(data_loader):
            _data_tensor = _data.to(device, dtype=torch.float)
            _label_tensor = _label.to(device, dtype=torch.long)
            
            _pred_result = model(_data_tensor)
            
            _valid_loss += loss_fn(_pred_result, _label_tensor).item()
            
            if acc_fn is not None:
                _acc_sum += acc_fn(_pred_result, _label_tensor).item()
                _acc_n += 1
    
    return _valid_loss/_acc_n, _acc_sum/_acc_n



def train(model, train_data_loader, valid_data_loader=None, loss_fn=None, optimizer=None, epochs:int=1, acc_fn=None, weight_path:str='weight/', device='cpu'):
    if not isinstance(model, nn.Module):
        print('[ERROR] validate : model isn\'t torch nn.Module')
        raise TypeError
    
    model = model.to(device)
    model.train()
    
    for epoch in range(epochs):
        _train_loss_sum = 0.0
        _train_loss_n = 0
        print("[TRAIN] start")
        for _train_data, _train_label in tqdm(train_data_loader):
            _train_data_tensor = _train_data.to(device, dtype=torch.float)
            _train_label_tensor = _train_label.to(device, dtype=torch.long)
            #_train_label_tensor = _train_label.long()
            
            optimizer.zero_grad()
            
            _train_result = model(_train_data_tensor)
            
            _loss = loss_fn(_train_result, _train_label_tensor)
            
            _loss.backward()
            optimizer.step()
            
            _train_loss_sum += _loss.item()
            _train_loss_n += 1
            #print('[batch] epoch {}: {}'.format(epoch, _loss.item()))
            
        print('[train] epoch {}: {}'.format(epoch, _train_loss_sum/_train_loss_n))
            
        _val_acc = None
        
        if valid_data_loader:
            _valid_loss, _val_acc = validate(model, valid_data_loader, loss_fn, acc_fn, device)
            print('[valid] epoch {}: {}'.format(epoch, _valid_loss))
            model.train()
        
        if not os.path.isdir(weight_path):
            os.makedirs(weight_path)
        
        
        if _val_acc is not None:
            weight_nm = os.path.join(weight_path, 'model_{}_acc_{:.4f}.pth'.format(epoch, _val_acc))
            
        else:
            weight_nm = os.path.join(weight_path, 'model_{}_loss_{:.4f}.pth'.format(epoch, _train_loss_sum/_train_loss_n))
            
        torch.save(model.state_dict(), weight_nm)
        print('[SAVE] weight save at {}'.format(weight_nm))
        
        

코드 중에서 맘에 안드는 부분도 있지만 일단 패스

나중에 수정해도 블로그 코드는 수정 안할꺼임

일단은 돌아는 가니 냅둔다

 

weight에서 'module.' 이 계속 키 값 앞에 붙어서 지우기 위해 새로 dict 만들고 update하는 번거로운 작업을 수행

 

 

메인코드

 

from torch.utils.data import TensorDataset, DataLoader
from torch import nn
import torch
import hardnet
import data_util
import torch_util
import hardnet_dataloader
from hardnet_loss import *
import random
import iou
import hardnet_loss
import cv2
import numpy as np
import os
import multiprocessing as mp
import time

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

hard_net = hardnet.HardNet(4)
hard_net = nn.DataParallel(hard_net)
hard_net = hard_net.to(device)

img_list, label_img_list, label_list = data_util.getDataPath('../data/images_ori/')

random.seed(100)
random.shuffle(img_list)
random.seed(100)
random.shuffle(label_img_list)
random.seed(100)
random.shuffle(label_list)

slice_idx = int(len(img_list)*0.8)
batch_size = 32
num_workers = 6

#optimizer = torch.optim.SGD(hard_net.parameters(), lr=0.0005, weight_decay=0.0005, momentum=0.9)
optimizer = torch.optim.Adam(hard_net.parameters(), lr=0.0005, weight_decay=1e-4)

train_loader = hardnet_dataloader.HardNetLoader(img_list[:slice_idx], label_img_list[:slice_idx], label_list[:slice_idx], batch_size, num_workers=num_workers, pin_memory=True)
valid_loader = hardnet_dataloader.HardNetLoader(img_list[slice_idx:], label_img_list[slice_idx:], label_list[slice_idx:], batch_size, num_workers=num_workers, pin_memory=True)

loss_fn = bootstrapped_cross_entropy2d().to(device)

torch_util.train(hard_net, train_loader, valid_loader, loss_fn, optimizer, epochs=20, acc_fn=iou.iou_pytorch, weight_path='weight/hardnet/',device=device)

 

원래 jupyter로 메인 코드 돌려서 하나씩 찍어보는데 일단은 나누는 건 위에서 알아서

 

위에 쓴 코드들은 다른 파일로 빼놓음 (optimizer 제외)

 

dataparallel 을 써도 느린건 여전하니 apex나 distribute parallel 을 찾아서 주피터 상에서 사용할 수 있도록 커스텀 예정

(망할 놈들이 무조건 main 파일을 terminal 상에서 돌리도록 예제를 만들어놔서 눈물을 흘리면서 코드 분석....)