본문 바로가기

AI/영상인식(Vision)

(이미지 분류 고급) 2_EfficientNet을 이용한 대선후보 분류 Hands-On

AI에서 이미지 공부할 때 MNIST만 돌려보는 건 지겹지 않은가??
이미지 분류 고급과정에선 아래와 같이 이미지 분류의 A to Z를 Hands-on 과정으로 다룬다.

1. 이미지 자동 크롤링
2. EfficientNet을 이용한 이미지 분류
3. GradCAM을 이용한 XAI(Explainable AI)
4. 적대적 공격(Adversarial attack)으로 내 모델 공격하기

 

2022 대선후보 데이터셋 구축: 이전글 참고 https://keep-steady.tistory.com/29

 

그중 두 번째, EfficientNet을 이요한 이미지 분류
- 목적: 최신 이미지 분류 모델 학습
- 순서
   1) 데이터 준비
       - dataset
       - dataloader
    2) EfficientNet 모델 준비

    3) 학습

 

전 글 1. 이미지 자동 크롤링(https://keep-steady.tistory.com/29)에선 내가 정한 클래스를 구글&네이버에서 자동으로 크롤링하여 데이터셋을 구축하는 법을 배웠다. 본 글에서는 현재 이미지 분류 최고모델 EfficientNet을 이용하여 이미지 분류를 실습해본다.

pytorch 1.8.0v 사용~!

 

1) EfficientNet-pytorch

EfficientNet은 최근 ImageNet 대회에서 가벼운 모델로 최고 성능을 냈다.  AutoML and Compound Scaling를사용하여이전 모델들 보다 훨씬 작으면서도 빠르다. 아래 그래프를 보면 유명한 ResNet-152 모델보다 성능이 좋은 EfficientNet-B1은 ResNet에 비해 파라미터가 1/5 수준이다. 성능을 높이는데 집착하여 모델을 깊고 무겁게 쌓았던 옛날과 다르게 요즘은 IOT 장비에 포팅하기 위해 가볍게 만드는 추세이다. 실제 물체인식(Object Detection) 연구들도 워크스테이션에선 1초당 10FPS를 웃돌며 빠르고 잘 돌아가지만, 라즈베리파이에서는 간신히 1FPS를 기대하기 힘들고 Jetson Nano에서도 5fps를 간신히 이뤄낸다. 즉 연구들은 연구로 끝날 뿐 실제 적용하기 위해서는 이와같은 연구가 필수이다.

아래 그림을 보면 아! 이모델을 꼭 써야겠구나! 싶다. EfficientNet은 모델 사이즈 별로 B0~B7 버전이 있다. B0이 제일 가벼운 모델이다. 나는 IOT 디바이스에서 수행되는걸 중시 여기기 때문에 B0기준으로 보겠다. 동급 성능을 내는 ResNet-50, DenseNet-169보다 1/5, 1/2.6배 모델이 작다. B4부터는 파라미터가 커져도 크게 성능 향상이 없어서 고성능을 요할땐 B4 모델을 선택해야 겠다. 아무튼 겁나 빠른데 또 좋다는 뜻!

 

모델의 구조는 뭐 채널을 건들고, 깊이를 적절히 하고 wider하게 해봤다가 섞었다가 등등을 하여 CNN을 효율적으로 만들었다고 한다. 작년부턴가, 사람이 모델을 이래저래 만져보고 실험해서 이 모델이 좋아요! 하던 연구는 이제 끝났다. 그저 AutoML이 ~~모델이 짱이어요! 하면 네~ 하고 쓰게되는 시대가 됐다. 자! 모르겠다 일단 써보자.

 

1-1) EfficientNet-pytorch 설치

https://github.com/lukemelas/EfficientNet-PyTorch 링크에 Installation 방법이 나와있다. 아주 쉽다.

아래 두가지 방법이다. 그냥 pip로 설치하면 쉽지만, 난 코드중 마음에 안드는 부분이 있어서 수정할꺼라 밑에껄 사용했다. '그냥 가져다 쓰면 되지!!!' 라는 생각은....결국 한계에 부딫힌다. 코드도 막상 보면 쉽다 ㅎ

- 간단한 설치 with pip

pip install efficientnet_pytorch

- 코드를 받아서 설치 - 추천!

git clone https://github.com/lukemelas/EfficientNet-PyTorch
cd EfficientNet-Pytorch
pip install -e .

 

1-2) EfficientNet-pytorch 사용법

아래와 같이 아주아주 간단하게 모델을 불러올 수 있다. 위에는 모델(구조)만 가져오는거고 아래는 이미 학습된 weight까지 가져오는 코드이다.

    -  Load an EfficientNet:

from efficientnet_pytorch import EfficientNet
model = EfficientNet.from_name('efficientnet-b0')

    - Load a pretrained EfficientNet:

from efficientnet_pytorch import EfficientNet
model = EfficientNet.from_pretrained('efficientnet-b0')

 

1-3) 이제 이 model을 transfer-learning으로 학습해보자

EfficientNet은 ImageNet에 맞춰 학습되있으므로 마지막 출력 차원이 1000차원이다. 1000개 중 한개를 고르는 식.

내가 customize해서 모델을 구성할 땐 출력차원을 조정해줘야 한다. 모델은 2단계로 나뉜다.

1. CNN을 이용한 특징(feature) 추출(이미지 -> 특징, [1, 3, 224, 224] -> [1, 1280, 7, 7])

2. 특징(feature)을 이용하여 Linear-layer 후 Softmax로 분류(1, 1280, 7, 7) -> (1, 1000)

이 중 2단계를 내 입맛에 맞게 바꾼다. Softmax 출력을 7로 바꾸자(내 custom dataset은 사진이 주워졌을 때 7명중(이재명, 윤석열, 심상정 등등) 누구냐로 분류하는 문제니까)

from efficientnet_pytorch import EfficientNet
model = EfficientNet.from_pretrained('efficientnet-b0')

# ... image preprocessing as in the classification example ...
print(img.shape) # torch.Size([1, 3, 224, 224])

features = model.extract_features(img)
print(features.shape) # torch.Size([1, 1280, 7, 7])

 

2) Custom 학습

본 글에서 사용할 데이터는 차기 대선후보 데이터셋이다. 네이버/구글에서 대선후보로 거론되는 7명(이재명, 윤석열, 심상정, 안철수, 이낙연, 홍준표, 이준석) 사진을 1000장 씩 수집하였고, 마스크를 썼거나 두 후보가 동시에 있는 사진은 제외하여 평균 1000장 씩 사용하였다. 데이터셋 수집은 전 글을 참고하면 된다^^(https://keep-steady.tistory.com/29)

 

학습 코드는 http://blog.naver.com/PostView.nhn?blogId=sw4r&logNo=221734454450 를 참고했다.

 

2-1) 선언

from_pretrained로 기 학습된 모델을 가져오고, num_classes에 내 데이타의 분류 사이즈를 입력한다

## 학습 코드
import numpy as np
import json
from PIL import Image
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
from torchvision import transforms
import matplotlib.pyplot as plt
import time
import os
import copy
import random

from efficientnet_pytorch import EfficientNet
model_name = 'efficientnet-b0'  # b5

image_size = EfficientNet.get_image_size(model_name)
print(image_size)
model = EfficientNet.from_pretrained(model_name, num_classes=7)

2-1-1) 혹시 미리 학습된 weight들은 고정하고 뒷부분 2단계만 학습하고 싶다면 아래 코드를 추가한다

# fc 제외하고 freeze
# for n, p in model.named_parameters():
#     if '_fc' not in n:
#         p.requires_grad = False
# model = torch.nn.parallel.DistributedDataParallel(model)

 

2-2) 데이타 불러오기

아래 코드를 이용하여 데이타를 불러온다.

내 데이타는 아래 그림과 같이 president_data 폴더 밑에 7개의 폴더를 만들고, 그 폴더엔 각각 대선주자(추미애, 홍준표, 황교안, 이재명, 이낙연, 박원순, 심상정) 사진이 들어있다.

 

폴더별로 정리된 데이터는 datasets.ImageFolder 함수로 편하게 불러올 수 있다. transforms.Compose 안의 Resize, ToTensor, Normalize 기능으로 다양한 크기의 이미지들을 한번에 정돈하여 불러올수 있다ㅠㅠㅠ 감동ㅠ

ImageNet은 입력이 224x224 형식이므로 이에 맞춰 Resize 해준다. 그리고 torch의 입력형태인 Tensor로 바꿔 준 후 Normalize 해준다. Normalize는 많은 이미지의 평균, 표준편차가 아래 나와있는 값이라고 한다. 그래서 많은 연구들에서 이 값을 그대로 사용한다. 그냥 사용하자.

그리고 sklearn의 train_test_split 함수로 전체 데이터를 train/valid/test 셋으로 나눠준다(8:1:1)

그리고 각 dataloader를 만든 후 나중에 batch로 불러올거다

## 데이타 로드!!
batch_size  = 128
random_seed = 555
random.seed(random_seed)
torch.manual_seed(random_seed)

## make dataset
from torchvision import transforms, datasets
data_path = 'president/president_data'  # class 별 폴더로 나누어진걸 확 가져와서 라벨도 달아준다
president_dataset = datasets.ImageFolder(
                                data_path,
                                transforms.Compose([
                                    transforms.Resize((224, 224)),
                                    transforms.ToTensor(),
                                    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
                                ]))
## data split
from sklearn.model_selection import train_test_split
from torch.utils.data import Subset
train_idx, tmp_idx = train_test_split(list(range(len(president_dataset))), test_size=0.2, random_state=random_seed)
datasets = {}
datasets['train'] = Subset(president_dataset, train_idx)
tmp_dataset       = Subset(president_dataset, tmp_idx)

val_idx, test_idx = train_test_split(list(range(len(tmp_dataset))), test_size=0.5, random_state=random_seed)
datasets['valid'] = Subset(tmp_dataset, val_idx)
datasets['test']  = Subset(tmp_dataset, test_idx)

## data loader 선언
dataloaders, batch_num = {}, {}
dataloaders['train'] = torch.utils.data.DataLoader(datasets['train'],
                                              batch_size=batch_size, shuffle=True,
                                              num_workers=4)
dataloaders['valid'] = torch.utils.data.DataLoader(datasets['valid'],
                                              batch_size=batch_size, shuffle=False,
                                              num_workers=4)
dataloaders['test']  = torch.utils.data.DataLoader(datasets['test'],
                                              batch_size=batch_size, shuffle=False,
                                              num_workers=4)
batch_num['train'], batch_num['valid'], batch_num['test'] = len(dataloaders['train']), len(dataloaders['valid']), len(dataloaders['test'])
print('batch_size : %d,  tvt : %d / %d / %d' % (batch_size, batch_num['train'], batch_num['valid'], batch_num['test']))

 

2-3) 데이터 확인

datasets.ImageFolder를 처음써봐서 진짜 잘 가져왔는지 확인이 필요했다. 그래서 transfoms로 tensor로 변환된 이미지를 불러와 class 이름과 같이 보여주며 확인하는 함수이다.

## 데이타 체크
import torchvision
def imshow(inp, title=None):
    """Imshow for Tensor."""
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = std * inp + mean
    inp = np.clip(inp, 0, 1)
    plt.imshow(inp)
    if title is not None:
        plt.title(title)
    plt.pause(0.001)  # pause a bit so that plots are updated

num_show_img = 5

class_names = {
    "0": "chumiae",      # "0": "추미애"
    "1": "hongjunpyo",   # "1": "홍준표"
    "2": "hwanggyoan",   # "2": "황교안"
    "3": "leejaemyung",  # "3": "이재명"
    "4": "leenakyeon",   # "4": "이낙연"
    "5": "parkwonsun",   # "5": "박원순"
    "6": "simsangjung"   # "6": "심상정"
}

# train check
inputs, classes = next(iter(dataloaders['train']))
out = torchvision.utils.make_grid(inputs[:num_show_img])  # batch의 이미지를 오려부친다
imshow(out, title=[class_names[str(int(x))] for x in classes[:num_show_img]])
# valid check
inputs, classes = next(iter(dataloaders['valid']))
out = torchvision.utils.make_grid(inputs[:num_show_img])  # batch의 이미지를 오려부친다
imshow(out, title=[class_names[str(int(x))] for x in classes[:num_show_img]])
# test check
inputs, classes = next(iter(dataloaders['test']))
out = torchvision.utils.make_grid(inputs[:num_show_img])  # batch의 이미지를 오려부친다
imshow(out, title=[class_names[str(int(x))] for x in classes[:num_show_img]])

음. train/valid/test 데이타셋 모두 class 이름과 이미지가 잘 연결됐군!! 아래 이미지를 좀더 멋있게 보이게 하려다가 그냥 말았다.

2-4) 학습 코드 작성

학습코드는 수없이도 짜봤다. fastai 등 이를 편하게 해주는 것들은 아주 많지만 결국 내가 원하는대로 customize 해서 쓰게되더라. 근데 이 코드는 정말 간결하고 편했다 ㅎㅎ 앞으론 이 코드를 써야지. 한 epoch을 돌때 마다 훈련하고, 그걸 평가해서 평가 정확도가 높으면 저장하는 식이다. 

def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    since = time.time()

    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0
    train_loss, train_acc, valid_loss, valid_acc = [], [], [], []
    
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # Each epoch has a training and validation phase
        for phase in ['train', 'valid']:
            if phase == 'train':
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode

            running_loss, running_corrects, num_cnt = 0.0, 0, 0
            
            # Iterate over data.
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                # zero the parameter gradients
                optimizer.zero_grad()

                # forward
                # track history if only in train
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # statistics
                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)
                num_cnt += len(labels)
            if phase == 'train':
                scheduler.step()
            
            epoch_loss = float(running_loss / num_cnt)
            epoch_acc  = float((running_corrects.double() / num_cnt).cpu()*100)
            
            if phase == 'train':
                train_loss.append(epoch_loss)
                train_acc.append(epoch_acc)
            else:
                valid_loss.append(epoch_loss)
                valid_acc.append(epoch_acc)
            print('{} Loss: {:.2f} Acc: {:.1f}'.format(phase, epoch_loss, epoch_acc))
           
            # deep copy the model
            if phase == 'valid' and epoch_acc > best_acc:
                best_idx = epoch
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())
#                 best_model_wts = copy.deepcopy(model.module.state_dict())
                print('==> best model saved - %d / %.1f'%(best_idx, best_acc))

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
    print('Best valid Acc: %d - %.1f' %(best_idx, best_acc))

    # load best model weights
    model.load_state_dict(best_model_wts)
    torch.save(model.state_dict(), 'president_model.pt')
    print('model saved')
    return model, best_idx, best_acc, train_loss, train_acc, valid_loss, valid_acc

2-5) 중요한 각종 설정

GPU를 쓸것인가 말것인가 결정한 후 모델, 이미지를 device로 보내줘야 한다.

나는 분류문제이므로 CrossEntropy를 사용한다. 종종 여기 Negative log를 씌운걸 쓰기도 한다. 이 데이터셋에서는 SGD, RMS 를 써봤는데 SGD가 잘 동작했다. lr(learning_rate)를 미세하게 조정하여 최적의 값을 찾았다. 이게 안맞으면 발산하고 진동한다. 그리고 exp_lr_scheduler로 조금씩 lr이 줄어들도록 했다. 그냥 서서히 줄어들면 되는데 그냥 썼다.

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")  # set gpu

model = model.to(device)

criterion = nn.CrossEntropyLoss()

optimizer_ft = optim.SGD(model.parameters(), 
                         lr = 0.05,
                         momentum=0.9,
                         weight_decay=1e-4)

lmbda = lambda epoch: 0.98739
exp_lr_scheduler = optim.lr_scheduler.MultiplicativeLR(optimizer_ft, lr_lambda=lmbda)

 

2-6) 학습!!!!!!!!!!

와, 길었다. 드디어 학습 시작! Teslar P100 한대로 2시간 가량 걸렸다. P100 4대를 사용해도 되지만 모델이 엄청 크지 않으면 그냥 1대로 돌리고 다른짓하다 확인하는게 정신건강에 좋다. 아무리 multi-gpu가 잘되있다 해도 4대를 최고효율로 돌리려면 이것저것 귀찮은 설정을 해야만 한다. 총 300 epoch을 수행했다. 그 중 175 epoch일 때 valid 데이터셋의 정확도가 97.5로 최고였다. 학습은 오래시킨다고 무조건 좋아지지 않는다. 오버피팅(Overfitting)이 일어나므로 꼭 꼭 학습 추이를 살펴봐야 한다.

model, best_idx, best_acc, train_loss, train_acc, valid_loss, valid_acc = train_model(model, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=300)

아래 그림은 EfficientNet의 2가지 버전을 학습한 결과이다. b0 모델과 b5 모델을 학습했다. b0 모델은 가장 가벼운 모델로 파라미터 수가 5.3M 이다. b5는 꽤 무거운 모델로 파라미터 수가 30M, b0에 비해 6배 크다. 크다는건 그만큼 느리다. b0는 P100 GPU 사용 시 batch_size를 256으로 했지만 b5는 32로 줄여야만 했다. 모델이 크니 메모리를 많이 잡아서 이다. 학습시간은 똑같이 300 epoch시 b0는 140분, b5는 516분으로 훨씬 느려졌다. 하지만 정확도는 validation accuracy 97.5 VS 97.8로 비슷비슷했다. 그렇다면 이 데이터셋이 대해서는 b0 승!!

 

EfficientNet-b0(좌) VS EfficientNet-b5(우)

 

 

2-7) 결과 그래프 출력

위에서 학습된 모델의 학습 그래프를 확인하자. 꼭 해야한다. 오버피팅은 정말 극복하기 힘들다. 마지막 epoch의 모델을 선택하는 어리석은 행동은 절대 하지말자!!

## 결과 그래프 그리기
print('best model : %d - %1.f / %.1f'%(best_idx, valid_acc[best_idx], valid_loss[best_idx]))
fig, ax1 = plt.subplots()

ax1.plot(train_acc, 'b-')
ax1.plot(valid_acc, 'r-')
plt.plot(best_idx, valid_acc[best_idx], 'ro')
ax1.set_xlabel('epoch')
# Make the y-axis label, ticks and tick labels match the line color.
ax1.set_ylabel('acc', color='k')
ax1.tick_params('y', colors='k')

ax2 = ax1.twinx()
ax2.plot(train_loss, 'g-')
ax2.plot(valid_loss, 'k-')
plt.plot(best_idx, valid_loss[best_idx], 'ro')
ax2.set_ylabel('loss', color='k')
ax2.tick_params('y', colors='k')

fig.tight_layout()
plt.show()

아래 그래프는 학습 그래프이다. 300번 돌렸는데 175번째의 결과가 가장 좋았다. 나는 정확도와 loss를 둘다 동시에 그린다. 학습데이터의 정확도는 거의 100에 가깝지만 역시 valid 데이터셋은 그에 못미친다. 97.5가 나왔다. 그래도 훌륭하지 Efficient-B0모델로!! 더 높이고 싶으면 B-7 모델을 쓰면된다. 하지만 난 그게 목적이 아니니까!

Efficient-B0 학습 그래프
Efficient-B5 학습 그래프

 

 

2-8) 자 그럼 학습이 잘 됐는데 체크해볼까??

아래 코드는 위에서 학습된 모델을 test 데이터셋에 돌려서 성능을 확인해 보는 코드다.

train/valid에 대해서만 평가했으므로 전혀 다른 test 데이터셋의 결과는 어떻게 될까??

아~~~ B0 모델의 정확도는 94.7% 이다. 역시 valid 정확도 97.5에 비하면 떨어진다. 이 간격을 줄이려면 데이터셋을 훨씬 키워야 한다. 그래도 가벼운 모델로 95%는 충분히 높다.

B5 모델은 94.4%였다. train, validation의 성능은 더 좋았지만 test 데이터에 대해서는 오히려 가벼운 b0 모델보다 떨어졌다. 즉, 모델이 크기만 하다고 무조건 좋은건 아니다.

def test_and_visualize_model(model, phase = 'test', num_images=4):
    # phase = 'train', 'valid', 'test'
    
    was_training = model.training
    model.eval()
    fig = plt.figure()
    
    running_loss, running_corrects, num_cnt = 0.0, 0, 0

    with torch.no_grad():
        for i, (inputs, labels) in enumerate(dataloaders[phase]):
            inputs = inputs.to(device)
            labels = labels.to(device)

            outputs = model(inputs)
            _, preds = torch.max(outputs, 1)
            loss = criterion(outputs, labels)  # batch의 평균 loss 출력

            running_loss    += loss.item() * inputs.size(0)
            running_corrects+= torch.sum(preds == labels.data)
            num_cnt += inputs.size(0)  # batch size

    #         if i == 2: break

        test_loss = running_loss / num_cnt
        test_acc  = running_corrects.double() / num_cnt       
        print('test done : loss/acc : %.2f / %.1f' % (test_loss, test_acc*100))

    # 예시 그림 plot
    with torch.no_grad():
        for i, (inputs, labels) in enumerate(dataloaders[phase]):
            inputs = inputs.to(device)
            labels = labels.to(device)

            outputs = model(inputs)
            _, preds = torch.max(outputs, 1)        

            # 예시 그림 plot
            for j in range(1, num_images+1):
                ax = plt.subplot(num_images//2, 2, j)
                ax.axis('off')
                ax.set_title('%s : %s -> %s'%(
                    'True' if class_names[str(labels[j].cpu().numpy())]==class_names[str(preds[j].cpu().numpy())] else 'False',
                    class_names[str(labels[j].cpu().numpy())], class_names[str(preds[j].cpu().numpy())]))
                imshow(inputs.cpu().data[j])          
            if i == 0 : break


    model.train(mode=was_training);  # 다시 train모드로
    
    ## TEST!
    test_and_visualize_model(model, phase = 'test')

이재명을 이재명으로, 심싱정을 심상정으로 잘 분류해낸다.

 

 

Conclusion

다음 장에서는 분류한 모델을 분석해볼 거다. XAI라고도 불리는데 분류 모델이 어떻게 분류했는지를 분석하는 연구들이 있다. CAM, GradCAM 등 모델로 심상정 의원을 심상정으로 분류했을 때, 입력의 어디를 보고 분류했는지 체크해 볼거다!! 이를 보면 인공지능의 한계, 오버피팅 문제를 알 수 있고 모델의 신뢰성을 파악 할 수 있다.

EfficientNet은 가볍고 좋은 모델이다. 하지만 pytorch의 구현상의 문제로 opencv의 dnn함수로 포팅할 수 없고, onnx 변환과 tensorrt 사용의 한계가 있어서 좀더 시간이 필요할 거 같다.

확실히 프로덕트에 포팅하기 위해서는 아직 tensorflow를 따라갈 수 없는거같다. 그래도 연구용으로 아주 편하고 좋아서 pytorch를 포기할 수 없다!! 라고 하면서 결국 TF를 다시 followup 해야겠다는 생각이 든다.

B0 vs B5 비교

위 그래프는 대선후보 데이터셋을 EfficientNet-B0, B5 두 모델로 분류해본 결과 그래프다. 본 데이터셋에서는 가벼운 B0 모델이 오히려 테스트셋에 대해 성능도 약간 좋았다. 학습시간도 빠르고 모델도 가벼워 꼭 무거운 B5 모델은 쓰지 않아도 되겠다. 역시 또 느끼는건 모델이 크다고 무조건 좋은건 아니다. IOT에 탑재할때나 서버에서 API로 서비스할 때는 무거운 모델은 확실히 부담이 된다. 명심하자! 가볍고 좋은모델을 선택하자

 

 

Reference

1) pytorch로 구현된 EfficientNet : https://github.com/lukemelas/EfficientNet-PyTorch

    - 원 논문은 TF로 구현되있지만 torch에서 잘 동작하도록 만들어줬다. 하지만 onnx 변환을 해도 OpenCV에서 읽히지 않는 문제가 있다. 또 jit 변환이 해결이 안되 torch 외 타 플랫폼에서는 사용하기 힘든 한계가 있다!!, 그래도 난 TF는 쓰고싶지않다

2) 이미지 분류 코드 참고 : http://blog.naver.com/PostView.nhn?blogId=sw4r&logNo=221734454450

    - Transfer learning을 이용한 이미지 분류 코드, 잘 짜여있다. 다만 기 학습된 weight는 freeze하진 않는다. 이건 자유니까

3) pytorch로 된 gradcam 적용 코드 : https://github.com/sidml/EfficientNet-GradCam-Visualization/blob/master/efficientnet-gradcam-comparison-to-other-models.ipynb

    - gradcam이 쉽게 구현되있다. 이 외 다른코드들은 hook을 등록하고 복잡한데 이건 feature extraction, classification 2단계로 나누기만 하면 쉽게 적용 가능하다