[딥러닝] 식물 잎의 사진으로 질병 분류하기(전이학습을 배워보자 / 텐서플로 허브)

황성미·2023년 11월 2일
0
post-thumbnail
post-custom-banner

✍🏻 1일 공부 이야기.


오늘 실습한 코드 내용은 위 깃허브에 업로드해두었습니다. 사진을 클릭하면 이동해요 !


식물 잎의 사진으로 질병 분류하기(전이학습을 배워보자)

파일 정리

일단 캐글에서 받은 데이터라고 해서 캐글 페이지를 찾고 다운로드 없이 코랩에서 바로 불러오는 작업을 하려고 했는데에.. 캐글 페이지를 찾지 못했다 😭

언젠가 쓸 수도 있을테니 링크는 첨부해둬야징

📌 캐글 데이터 코랩에서 불러오기

여튼, 그러면 일단 강의자료에 있는 구글 드라이브의 데이터를 사용할 수 밖에 없는데 그렇게 되면 다운을 받아야되나? 싶었다.

다운로드 받는데 3, 4시간... 이 걸린다길래 이건 아니다하고 바로 종료 후 방법을 찾았다.

이번에는 GPU를 사용하기 위해 colab에서 실습할꺼라 위 사진의 드라이브 바로가기 를 눌러 내 드라이브에 바로 추가해주었다.

그리고 코랩에서 1번 아이콘을 눌러 마운트를 시켜주고(코랩에서 드라이브 환경에 있는 파일을 쓸 수 있게 해줌) 방금 추가해준 dataset.zip의 경로를 복사해준다. (마우스 우클릭 - 경로 복사)


📌 압축 파일 풀기

# 데이터 압축 파일 풀기
!unzip -qq '/content/drive/MyDrive/제로베이스스쿨 공부/Deep Learning/data/dataset.zip' -d './dataset'

📌 데이터 정리

현재 폴더에는 각 클래스로 구분된 폴더에 사진이 여러 장 있다.
따라서 Train / Test / Val 폴더로 구분된 데이터용 폴더를 하나 새로 만들어주고 각 폴더에 랜덤으로 사진을 넣어주는 작업을 실시해보려고 한다.

  • 폴더 생성
import os

original_dataset_dir = './dataset'
classes_list = os.listdir(original_dataset_dir) # 데이터셋이 있는 폴더

base_dir = './splitted' # 나중에 Train, Test, Val 데이터를 분리시킬 폴더
os.mkdir(base_dir)

# 데이터 정리를 위한 목록 및 폴더 생성
import shutil

## base_dir 하위에 각각의 폴더 생성
train_dir = os.path.join(base_dir, 'train')
os.mkdir(train_dir)

validation_dir = os.path.join(base_dir, 'val')
os.mkdir(validation_dir)

test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)

for cls in classes_list: # 생성된 폴더에도 기존 데이터에 있던 하위 폴더들 생성 
  os.mkdir(os.path.join(train_dir, cls))
  os.mkdir(os.path.join(validation_dir, cls))
  os.mkdir(os.path.join(test_dir, cls))

  • 생성된 폴더에 데이터 넣기
# 데이터 확인
import math

for cls in classes_list:
    path = os.path.join(original_dataset_dir, cls) # 기존 데이터의 폴더명
    fnames = os.listdir(path) # path 폴더에 있는 파일명 저장 

    # 불러온 파일들을 6:2:2로 train / val / test로 분리
    train_size = math.floor(len(fnames) * 0.6)
    validation_size = math.floor(len(fnames) * 0.2)
    test_size = math.floor(len(fnames) * 0.2)

    # 각 인덱스에 해당하는 파일들을 각 폴더에 저장 
    train_fnames = fnames[:train_size]
    print("Train size((",cls,") :", len(train_fnames))
    for fname in train_fnames:  
        src = os.path.join(path, fname) # 기존 파일 경로
        dst = os.path.join(os.path.join(train_dir, cls), fname) # 카피할 파일 경로
        shutil.copyfile(src, dst) # 새로 생성된 train dir에 파일 저장

    validation_fnames = fnames[train_size:(validation_size + train_size)]
    print("Validation size((",cls,") :", len(validation_fnames))
    for fname in validation_fnames:  
        src = os.path.join(path, fname)
        dst = os.path.join(os.path.join(validation_dir, cls), fname)
        shutil.copyfile(src, dst) # 새로 생성된 validation dir에 파일 저장

    test_fnames = fnames[(train_size + validation_size):(validation_size + train_size + test_size)]
    print("Test size((",cls,") :", len(test_fnames))
    for fname in test_fnames:  
        src = os.path.join(path, fname)
        dst = os.path.join(os.path.join(test_dir, cls), fname)
        shutil.copyfile(src, dst) # 새로 생성된 test dir에 파일 저장

위 코드를 실행시키면 사진과 같이 splitted 폴더 아래 test / train / val 폴더가 생기고 각 폴더 안에 랜덤으로 섞인 사진들이 들어간 것을 볼 수 있다.



학습


📌 GPU에서 작업하기

코랩에서 이번 실습을 하게 된 이유는 바로 cuda 환경에서 작업하기 위함이다.

먼저 GPU를 사용할 수 있도록 [런타임] - [런타임 유형 변경] 을 클릭하고 GPU를 선택해준 후

아래 코드를 실행시켜 GPU를 사용할 수 있는지 확인해보자.

import torch
import os

USE_CUDA = torch.cuda.is_available()
DEVICE = torch.device('cuda' if USE_CUDA else 'cpu')
BATCH_SIZE = 256
EPOCH = 30

DEVICE

💻 출력

device(type='cuda')

잘 설정되었다면 위와 같이 cuda가 출력될 것이다.

전처리

다음으로는 torch에 맞게 데이터를 변형시켜주어야한다.

import torchvision.transforms as transforms
from torchvision.datasets import ImageFolder

# 사진의 파일이 제각각이므로 resize를 통해 64 * 64로 맞춰주고 tensor로 변환
transform_base = transforms.Compose([transforms.Resize((64, 64 )), transforms.ToTensor()])

# ImageFolder: 폴더 구조로 이루어진 데이터셋을 처리하기 위한 클래스
train_dataset = ImageFolder(root='./splitted/train', transform=transform_base)
val_dataset = ImageFolder(root='./splitted/val', transform=transform_base)

# 배치로 쪼개기
from torch.utils.data import DataLoader

train_loader = torch.utils.data.DataLoader(train_dataset,
                                           batch_size=BATCH_SIZE,
                                           shuffle=True,
                                           num_workers=4)

val_loader = torch.utils.data.DataLoader(val_dataset,
                                        batch_size=BATCH_SIZE,
                                        shuffle=True,
                                        num_workers=4)

앞서 실습에서는 사진 사이즈가 모두 동일하여 resize 할 필요가 없었지만 이번 실습에서는 사진의 크기가 제각각이여서 64 * 64로 동일하게 맞춰주고 tensor로 변환해주는 작업을 해주었다.

ImageFolder 클래스에 대해 잘 모르겠어서 GPT에게 물어보았더니 아래와 같이 답변해주었다.

그래도 잘...ㅎㅎㅎ 🤯🤯

여튼 배치도 여러 개 나눠주고

# 배치로 쪼개기
from torch.utils.data import DataLoader

train_loader = torch.utils.data.DataLoader(train_dataset,
                                           batch_size=BATCH_SIZE,
                                           shuffle=True,
                                           num_workers=4)

val_loader = torch.utils.data.DataLoader(val_dataset,
                                        batch_size=BATCH_SIZE,
                                        shuffle=True,
                                        num_workers=4)

첫번째 배치만 따로 떼어놓고 본 결과는 위와 같다.

torch.Size([256, 3, 64, 64])에 주의하여 이제 모델을 구성해주어야 한다.


모델링


일단 만들고자 하는 모델은 위와 같은 구성이다.
이를 코드로 구현하면 아래와 같다.


📌 모델 구성 및 선언

# 모델링
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        ## nn.Conv2D(입력받는 채널 수, 출력할 채널 수, 커널 사이즈, padding 옵션)
        self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
        self.conv3 = nn.Conv2d(64, 64, 3, padding=1)
        self.fc1 = nn.Linear(4096, 512) # 8 * 8 * 64 = 4096
        self.fc2 = nn.Linear(512, 33)

    def forward(self, x):

        x = self.conv1(x) # (64, 64) 
        x = F.relu(x)
        x = self.pool(x) # (32, 32) 
        x = F.dropout(x, p=0.25, training=self.training) # train 모드에서만 dropout 사용

        x = self.conv2(x) # (32 , 32)
        x = F.relu(x)
        x = self.pool(x) # (16, 16)
        x = F.dropout(x, p=0.25, training=self.training)

        x = self.conv3(x) # (16, 16)
        x = F.relu(x)
        x = self.pool(x) # (8, 8)
        x = F.dropout(x, p=0.25, training=self.training)

        x = x.view(-1, 4096) # flatten()
        x = self.fc1(x)
        x = F.relu(x)
        x = F.dropout(x, p=0.5, training = self.training)
        x = self.fc2(x)

        return F.log_softmax(x, dim=1)

# 모델 선언    
model_base = Net().to(DEVICE)
optimizer = optim.Adam(model_base.parameters(), lr=0.001)

📌 학습 및 평가

그리고 이제 각 배치 데이터에 대하여 학습을 시키고 평가를 해야한다. 하지만 이렇게 데이터가 방대한 경우 학습과 평가에 대한 코드를 함수로 만들어준 후 각 epoch 중 가장 좋은 성능을 지닌 모델을 저장하는 방향으로 코드를 짜는 것이 좋다.

- 함수 생성

# 학습
def train(model, train_loader, optimizer):
    model.train() # 모드 선언
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(DEVICE), target.to(DEVICE)

        optimizer.zero_grad()
        output = model(data)
        loss = F.cross_entropy(output, target)

        loss.backward()
        optimizer.step()    

# 평가
def evaluate(model, test_loader):
    model.eval() # 모드 선언
    test_loss = 0
    correct = 0

    # with 자원을 열면 닫지 않아도 with 구문이 끝나면 알아서 닫아줌(error대응 잘함)
    with torch.no_grad(): # gradient가 없는동안 아래 동작을 실시하라
        for data, target in test_loader:
            data, target = data.to(DEVICE), target.to(DEVICE)
            output = model(data)

            test_loss += F.cross_entropy(output, target, reduction='sum').item()

            pred = output.max(1, keepdim=True)[1] # 최대값 위치의 클래스가 최종 클래스
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    test_accuracy = 100. * correct / len(test_loader.dataset)   # 맞는 개수
    return test_loss, test_accuracy

- 실제 실행 코드

import time
import copy 

def train_baseline(model, train_loader, val_loader, optimizer, num_epochs = 30):
    best_acc = 0.0 # 가장 좋은 acc를 저장할 변수
    best_model_wts = copy.deepcopy(model.state_dict()) # 가장 좋은 acc 모델의 weight 저장

    for epoch in range(1, num_epochs + 1):
        since = time.time()
        train(model, train_loader, optimizer) # 학습 
        train_loss, train_acc = evaluate(model, train_loader) # train에 대한 평가
        val_loss, val_acc = evaluate(model, val_loader) # val에 대한 평가

        if val_acc > best_acc:  # 30번의 epoch 중 가장 val_accuracy가 좋은 weight를 저장
            best_model_wts = copy.deepcopy(model.state_dict())
        
        time_elapsed = time.time() - since
        print('----------------- epoch {} -----------------'.format(epoch))
        print('train Loss: {:.4f}, Accuracy: {:.2f}%'.format(train_loss, train_acc))
        print('val Loss: {:.4f}, Accuracy: {:.2f}%'.format(val_loss, val_acc))
        print('Complieted in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
    model.load_state_dict(best_model_wts)
    return model

base = train_baseline(model_base, train_loader, val_loader, optimizer, EPOCH)
torch.save(base, 'baseline.pt') # 파이토치 파일로 가장 좋은 acc 모델 저장

그러면 위와 같이 각 epoch에 대해 출력해주고 가장 좋은 acc을 가진 모델을 저장까지 해준다.


전이학습

이미지 증강(augmentation)

이미 학습이 잘 된 모델을 나에게 맞게 조금만 변형하여 사용하는 것을 전이학습이라 한다.

이때 weight까지 그대로 가져오는 경우도 있고 일부분은 구조만 가져와서 학습을 시켜 가중치를 매기는 경우도 있다.

모델 전체를 가져올지, 일부분만 가져올지 등 여러 고민에 대한 생각은 대부분 위와 같은 사고를 통해 결정된다.

이 실습을 해보자.

이미지 데이터는 구하기 쉬운 데이터가 아니여서 가지고 있는 데이터를 최대한 활용하는 것이 중요하다. 또한 이미지 데이터 수가 충분하다고 하더라도 과적합을 방지하기 위해 일부러 이미지를 변형시키기도 한다.

데이터를 일부러 변형시켜서 그 수를 늘리는데, 무작위로 자르고 뒤집고 회전시키며 색조와 명도를 조정하는 등의 이미지 증강 작업을 수행한다.

data_transforms = {     # 과적합 방지용 -> 돌리고, 상하좌우 반전, 이미지 자르기, 색상
    'train' : transforms.Compose([transforms.Resize([64, 64]),  
        transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(), 
        transforms.RandomCrop(52), transforms.ToTensor(),    
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]),  
    
    'val' : transforms.Compose([transforms.Resize([64, 64]),
        transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(),
        transforms.RandomCrop(52), transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]),
}

data_dir = './splitted'

# 데이터 변형
image_datasets = {x: ImageFolder(root=os.path.join(data_dir, x), 
                                 transform=data_transforms[x]) for x in ['train', 'val']}
# 배치 나누기 
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], 
                                              batch_size = BATCH_SIZE,
                                              shuffle=True,
                                              num_workers=4) for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}

class_names = image_datasets['train'].classes # 클래스명 담긴 변수

학습된 모델 불러오기

from torchvision import models 을 이용하면 여러 학습된 모델을 불러올 수 있다.

from torchvision import models

# resnet50
resnet = models.resnet50(pretrained=True)   
                        # True - 학습이 완료된 weight 가져옴 / False - 구조만 가져옴
'''
대부분 사전 학습된 모델과 우리의 데이터의 출력될 클래스의 숫자가 다를 것이다.
이를 생각하고 항상 마지막 레이어의 숫자를 변형해주어야한다!!! 
'''
num_ftrs = resnet.fc.in_features # in_features - 마지막 레이어 채널 숫자에 해당하는 것 
resnet.fc = nn.Linear(num_ftrs, len(class_names)) # len(class_names) : 33개로 수정 
resnet = resnet.to(DEVICE)

criterion = nn.CrossEntropyLoss()
'''
filter(lambda p: p.requires_grad, resnet.parameters())
사전 학습된 모델의 weight까지 가져오는 것이지만, 마지막 레이어가 바뀌었기 때문에
해당 weight는 다시 학습시킬 필요가 있다.
'''
optimizer_ft = optim.Adam(filter(lambda p: p.requires_grad, resnet.parameters()), lr=0.001)

from torch.optim import lr_scheduler    # epoch에 따라 running rate를 바꾸는 작업을 해줌
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)    
                  # 7 epoch마다 running rate를 0.1씩 감소 시킴

중간 중간 주석처리된 부분을 이해해주어야한다.


모델 수정 후 학습시키기

📌 학습시키지 않을 레이어는 고정

ct = 0
for child in resnet.children(): # resnet 모델의 하위 레이어를 for문으로 추출 
    ct += 1
    if ct < 6:  # 입력에 가까운 0-5번 레이어
        # 학습하지 않도록 고정
        for param in child.parameters():
            param.requires_grad = False
    # 따로 설정하지 않은 6번 ~ 레이어는 학습 시킴 

📌 나머지 레이어들에 대해 학습

# 학습을 하면서 가장 acc가 좋은 모델 저장
def train_resnet(model, criterion, optimizer, scheduler, num_epochs=25):
  best_model_wts = copy.deepcopy(model.state_dict())
  best_acc = 0.0 # 가장 좋은 acc가 될 변수

  for epoch in range(num_epochs):
      print('----------------- epoch {} -----------------'.format(epoch+1))
      since = time.time()
      for phase in ['train', 'val']:
          # 모드 설정 
          if phase == 'train':
              model.train()
          else:
              model.eval()

          # 각 epoch를 시작할 때 마다 loss, corrects 초기
          running_loss = 0.0
          runnung_corrects = 0

          # 학습
          for inputs, labels in dataloaders[phase]:
              inputs = inputs.to(DEVICE)
              labels = labels.to(DEVICE)

              optimizer.zero_grad()

              # Train 데이터라면 gradients 업데이트를 허가 
              with torch.set_grad_enabled(phase == 'train'):
                  outputs = model(inputs)
                  _, preds = torch.max(outputs, 1)
                  loss = criterion(outputs, labels)

                  if phase == 'train':
                      loss.backward()
                      optimizer.step()
              
              # loss 와 맞는 개수 계산 
              # inputs.size(0) : 배치 사이즈
              running_loss += loss.item() * inputs.size(0)
              runnung_corrects += torch.sum(preds == labels.data)
          if phase == 'train': # learning rate 업데이트
              scheduler.step()
          
          epoch_loss = running_loss/dataset_sizes[phase]
          epoch_acc = runnung_corrects.double()/dataset_sizes[phase]

          print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))

          # acc가 이전보다 좋아졌다면 업데이트
          if phase == 'val' and epoch_acc > best_acc:
              best_acc = epoch_acc
              best_model_wts = copy.deepcopy(model.state_dict())
      
      time_elapsed = time.time() - since
      print('Complieted in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
  print('Best val Acc: {:.4f}'.format(best_acc))

  model.load_state_dict(best_model_wts)

  return model

💻 출력


평가하기

# 전처리
# 과적합 방지용 transform 그대로 적용
transform_resnet = transforms.Compose([
        transforms.Resize([64, 64]),  
        transforms.RandomCrop(52), 
        transforms.ToTensor(),    
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])

test_resNet = ImageFolder(root='./splitted/test', transform=transform_resnet)
test_loader_resNet = torch.utils.data.DataLoader(test_resNet, 
                                              batch_size = BATCH_SIZE,
                                              shuffle=True,
                                              num_workers=4)

# 저장된 모델 불러오기
resnet50 = torch.load('resnet50.pt')
resnet50.eval() # 평가 모드 선언 
test_loss, test_accuracy = evaluate(resnet50, test_loader_resNet)

print('ResNet test acc:  ', test_accuracy)

💻 출력

ResNet test acc: 98.97358868444111

test 데이터에 대해 전처리를 해주고 저장된 모델을 불러와 평가를 해주면 위와 같은 accuracy를 얻을 수 있다.



tensorflow_datasets을 통해 알아보는 전이학습과 미세조정

from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf
import os
import numpy as np
import matplotlib.pyplot as plt

keras = tf.keras

import tensorflow_datasets as tfds
tfds.disable_progress_bar()

전이학습

📌 데이터 준비

(raw_train, raw_validation, raw_test), metadata = tfds.load(
    'cats_vs_dogs',
    # 데이터를 인덱스로 분리하여 각각 train, val, test 데이터로 이용
    split = ['train[:80%]' , 'train[80%:90%]', 'train[90%:]'],
    with_info = True,
    as_supervised = True # 데이터가 라벨과 함께 튜플 형태로 저장
)

tensorflow_datasets에 있는 cats_vs_dogs 데이터를 불러와주었다.

tfds.loadsplit옵션을 이용하면 불러올 때 각 인덱스를 기준으로 데이터를 분리해서 가져올 수 있다.

📌 데이터 확인

# 2개
get_label_name = metadata.features['label'].int2str

for image, label in raw_train.take(2):
  plt.figure()
  plt.imshow(image)
  plt.title(get_label_name(label))

💻 출력

metadata의 label을 get_label_name 변수에 저장해두고 .take()를 통해 2개만 가져오게 시켜 그림과 라벨을 같이 뽑아낼 수도 있다.


전처리

이미 학습된 모델을 가지고 오는 것이므로 해당 모델에 맞게 다양한 전처리를 해줄 필요가 있다.

아래 코드는 이미지 사이즈를 160 * 160 으로 resize해주고 -1과 1 사이의 값을 가지도록 scale을 해준 작업이다.

IMG_SIZE = 160

def format_example(image, label):
  image = tf.cast(image, tf.float32)
  image = (image / 127.5) - 1 # scale
        # -1과 1 사이의 값을 가지도록 0과 255의 중간값으로 나눠주고 1 빼줌
  image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE))
  return image, label
  
  
# map 함수를 이용해 빠르게 적용
train = raw_train.map(format_example)
validation = raw_validation.map(format_example)
test = raw_test.map(format_example)

또한 배치를 만들고 섞어주었다.

# 배치 만들고 shuffle
BATCH_SIZE = 32
SHUFFLE_BUFFER_SIZE = 1000

train_batches = train.shuffle(SHUFFLE_BUFFER_SIZE).batch(BATCH_SIZE)
validation_batches = validation.batch(BATCH_SIZE)
test_batches = test.batch(BATCH_SIZE)

# 확인
for image_batch, label_batch in train_batches.take(1):
  pass
image_batch.shape # 적용한 이미지 사이즈와 배치 사이즈가 적용됨

💻 출력

TensorShape([32, 160, 160, 3])

첫 번째 배치에 대해서만 확인하면 되므로 바로 break를 걸어주었다.


MobileNet V2 모델

MobileNet V2 모델은 위와 같은 구조를 가진다. 그리고 아래와 같은 코드로 학습된 모델을 불러올 수 있다.

# 사전 훈련된 MobileNetV2
IMG_SHAPE = (IMG_SIZE, IMG_SIZE, 3)

base_model = tf.keras.applications.MobileNetV2(input_shape = IMG_SHAPE,
                                               include_top = False, 
                                               weights = 'imagenet')

이때 몇 가지 설정을 해주어야한다.

  • include_top = False
    : 모델의 맨 위층(맨 마지막 레이어)에는 사전 훈련된 데이터의 원 핫 인코딩이 되어있는 상태일 것이다. 이를 우리 데이터로 바꿔주어야하므로 맨 위층을 빼고 가져오도록 설정
  • weights = 'imagenet'
    : 'imagenet'으로 학습된 모델을 불러오도록 설정
# (160 , 160, 3 )-> (5, 5, 1280)
feature_batch = base_model(image_batch)
feature_batch.shape # 1280 : 채널 수

💻 출력

TensorShape([32, 5, 5, 1280])

이렇게 불러온 모델을 확인해보면 (160 , 160, 3 )-> (5, 5, 1280) 변환된 것을 확인할 수 있다.

이때 1280이 채널 수이다.


📌 가중치를 그대로 사용

먼저 학습된 모델의 가중치를 그대로 사용해보자.

# 가중치를 그대로 사용하기 위함
base_model.trainable = False

위와 같이 설정해두고 base_model.summary()를 실행해주면 아래와 같이 Trainable params의 수가 0이 뜰 것이다.


학습된 모델의 가중치는 그대로 사용한다고 했지만,
우리는 한 가지 작업을 더 해주어야한다.

🌟 바로 마지막 레이어를 우리 데이터에 맞게 추가해주어야하는 것!

지금은 마지막 레이어를 GlobalAveragePooling2D층 Dense 층을 이용해 쌓아주었다.

# GlobalAveragePooling2D층
# 채널 마다의 평균값을 이용
global_average_layer = tf.keras.layers.GlobalAveragePooling2D()
feature_batch_average = global_average_layer(feature_batch)
# feature_batch_average.shape # TensorShape([32, 1280])

# Dense 층
prediction_layer = keras.layers.Dense(1)
prediction_batch = prediction_layer(feature_batch_average)
# prediction_batch.shape # TensorShape([32, 1])

📌 최종 전체 모델 구성

# 전체 모델 구성
model = tf.keras.Sequential([
    base_model, # 기존 mobilenet
    # 우리가 추가한 레이어
    global_average_layer,
    prediction_layer
])

# 컴파일
base_learning_rate = 0.0001
model.compile(optimizer = tf.keras.optimizers.RMSprop(lr = base_learning_rate), 
              loss = tf.keras.losses.BinaryCrossentropy(from_logits = True),
              metrics = ['accuracy'])

추가할 마지막 레이어를 기존 base_model과 함께 쌓아주고 컴파일해주면 모델링은 끝났다.

- 개와 고양이를 분류하는 문제이므로 BinaryCrossentropy를 이용함.

학습을 시키지 않은 상태의 성능도 확인해볼 수 있다.

학습을 시키지 않았으니 성능이 좋지 않은 것은 당연하다.

그렇다면 얼른 학습시킨 성능을 살펴보자.

📌 학습 및 평가

# 학습
history = model.fit(train_batches,
                    epochs = initial_epochs,
                    validation_data = validation_batches)
# 전체 연산을 다 해야하므로 시간이 오래 걸리지만 
# mobilenet을 다시 학습하는 것보단 훨씬 빠름

# 평가
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']

loss = history.history['loss']
val_loss = history.history['val_loss']

plt.figure(figsize = (8, 8))
plt.subplot(2, 1, 1)
plt.plot(acc, label = 'Training Acc')
plt.plot(val_acc, label = 'Val Acc')
plt.legend(loc = 'lower right')
plt.ylabel('Acc')
plt.ylim([min(plt.ylim()), 1])
plt.title('Training and Val Acc')

plt.subplot(2, 1, 2)
plt.plot(loss, label = 'Training Loss')
plt.plot(val_loss, label = 'Val Loss')
plt.legend(loc = 'upper right')
plt.ylabel('Cross Entropy')
plt.ylim([0,1.0])
plt.title('Training and Val Loss')
plt.xlabel('epoch')
plt.show()

loss는 점점 떨어지고 있고 acc는 점점 상승하고 있다.

이것만으로도 좋지만,
이번에는 가중치 중 일부만 학습된 모델의 가중치를 사용하고 일부는 튜닝을 해보도록 설정해보자.

📌 미세 조정

일단 먼저 모든 pram에 대해 trainable하게 변경해주어야한다.

# 모두 trainable하게 변경
base_model.trainable = True
print('Number of layers in the base model : ', len(base_model.layers))

💻 출력

Number of layers in the base model : 154

우리가 튜닝할 수 있는 총 레이어의 개수가 154개라는 것을 확인할 수 있다.

모든 층을 튜닝하기 보단, 100번째의 레이어부터 튜닝 가능하도록 설정해줄 것이다.

# 100번째 층부터 튜닝 가능하게 설정
fine_tune_at = 100

# fine_tune_at 층 이전의 모든 층 고정
for layer in base_model.layers[:fine_tune_at]:
  layer.trainable = False
  
# 학습 비율 낮춤
model.compile(optimizer = tf.keras.optimizers.RMSprop(lr = base_learning_rate), 
              loss = tf.keras.losses.BinaryCrossentropy(from_logits = True),
              metrics = ['accuracy'])
model.summary()

💻 출력

위와 같이 설정해주면 trainable param이 0이었던 이전과 달리 1862721개를 튜닝할 수 있게 되었다.


그리고 이제 epoch을 돌며 학습을 시켜주어야하는데

앞서 학습시켰던 모델에 이어서 학습할 수 있도록 설정해줄 수도 있다.

# 20번의 epoch
fine_tune_epochs = 10
total_epochs = initial_epochs + fine_tune_epochs # 20

history_fine = model.fit(train_batches,
                         epochs = total_epochs,
                         # 이전 모델의 epoch부터 학습을 시작하게 해서
                         # 10 epoch부터 이어서 학습하게 함
                         initial_epoch = history.epoch[-1],
                         validation_data = validation_batches)

이전 history의 history.epoch[-1]을 추출하여 처음 시작하는 epoch 지점을 잡아주고 10 epoch을 더 돌게 해주면 아래와 같이 10 epoch부터 시작하여 20 epoch까지 도는 것을 확인할 수 있다.


📌 평가

# 최초 history에 방금 학습 결과 추가
acc += history_fine.history['accuracy']
val_acc += history_fine.history['val_accuracy']

loss += history_fine.history['loss']
val_loss += history_fine.history['val_loss']

그리고 이전의 학습 결과에 성능을 추가하고
튜닝을 시작하게 된 시점부터 그래프를 더 추가하여 그리면 아래와 같다.

음... 원래는 loss는 계속 떨어지고 acc는 계속 높아지는 그래프를 원했는데 많이 변동성이 심해진 것 같다 😅😅

여튼 이렇게 일부 가중치는 튜닝할 수도 있다는 사실을 기억하자.



텐서플로 허브

텐서플로 허브에서도 사전 훈련된 모델을 가져오고 데이터도 사용할 수 있다.

이번에는 텐서플로 허브 공식 사이트의 코드를 따라하며 다른 이미지로도 실습해보자.

공식 사이트 속 어디에 있는지 계속 찾았는데 이제야 찾았다... 🫠

📌 Mobilenet V2 가져오기

#pip install -U tf-hub-nightly
import tensorflow_hub as hub

# mobilenet 가져오기
classifier_url = 'https://tfhub.dev/google/tf2-preview/mobilenet_v2/classification/2'

# 사전 훈련된 MobileNetV2
IMG_SHAPE = (224, 224)

classifier = tf.keras.Sequential(
    hub.KerasLayer(classifier_url, 
    # Mobilenet은 224, 224, 3(RGB) input 형태
    input_shape = IMG_SHAPE + (3,))
)
classifier.summary()

💻 출력

앞서 실습했던 Mobilenet V2를 텐서플로 허브에서 가져오려면 위와 같은 코드를 실행시키면 된다.

그리고 예측해볼 이미지를 하나 확인해보자.
원래는 데이터의 구성이 어떠한지 확인해보고 넘어가고 싶었는데 각 데이터가 의미하는 바가 무엇인지 찾지는 못했다.

예측해본 바로는 사진을 받아서 해당 이미지가 어떤 카테고리(ImageNetLabels.txt)에 속해있는지를 예측해보는 과정인 것 같다.

# 이미지 하나 확인
import PIL.Image as Image

url = "https://storage.googleapis.com/download.tensorflow.org/example_images/grace_hopper.jpg"
grace_hopper = tf.keras.utils.get_file('image.jpg', url)
grace_hopper = Image.open(grace_hopper).resize(IMG_SHAPE)
grace_hopper

💻 출력


📌 학습 및 예측

# 정규화 및 예측
grace_hopper = np.array(grace_hopper) / 255.0
print(grace_hopper.shape)

result = classifier.predict(grace_hopper[np.newaxis, ])

# argmax로 인덱스 찾기
predicted_class = np.argmax(result[0], axis = -1) # 653

# label을 받아서 해당 클래스의 라벨 값 추출
url = "https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt"
labels_path = tf.keras.utils.get_file('ImageNetLabels.txt', url)
imagenet_labels = np.array(open(labels_path).read().splitlines())

# 확인
plt.imshow(grace_hopper)
plt.axis('off')
predicted_class_name = imagenet_labels[predicted_class]
_ = plt.title("Prediction : " + predicted_class_name.title())

💻 출력

군복이라는 것을 잘 예측했다!



아래는 여러 꽃 사진에 대한 클래스를 예측하는 실습이다.

📌 데이터 준비

url = "https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz"
data_root = tf.keras.utils.get_file('flower_photos', 
                                    url, 
                                    untar = True) #압축 풀기
                                    
                                    
# rescale 및 라벨 인식
image_generator = tf.keras.preprocessing.image.ImageDataGenerator(rescale = 1/255)
image_data = image_generator.flow_from_directory(str(data_root), target_size = IMG_SHAPE)

📌 배치 하나에 대한 예측 결과

# 배치 생성
for image_batch, label_batch in image_data:
  print("Image batch shape : ", image_batch.shape) # (32, 224, 224, 3)
  print("Label batch shape : ", label_batch.shape)  # (32, 5)
  break
  
# 배치 하나에 대한 예측 결과
result_batch = classifier.predict(image_batch)

predicted_class_names = imagenet_labels[np.argmax(result_batch, axis = -1)]

# 확인
plt.figure(figsize = (10, 9))
plt.subplots_adjust(hspace = 0.5)
for n in range(30):
  plt.subplot(6, 5, n+1)
  plt.imshow(image_batch[n])
  plt.title(predicted_class_names[n])
  plt.axis('off')
_ = plt.suptitle('ImageNet prediction')

💻 출력

은근 틀린 것들도 보인다.


📌 모델 생성

이번에는 특성추출기를 가져오고 dense 레이어 하나를 붙인 모델을 만들어보자.

# 특징 추출기 가져오기
feature_extractor_url = 'https://tfhub.dev/google/tf2-preview/mobilenet_v2/feature_vector/2'

feature_extractor_layer = hub.KerasLayer(feature_extractor_url,
                                         input_shape = (224, 224, 3))

feature_batch = feature_extractor_layer(image_batch)

# dense 레이어 추가
from tensorflow.keras import layers

feature_extractor_layer.trainable = False

model = tf.keras.Sequential([
    feature_extractor_layer,
              # 마지막 라벨은 나의 데이터 클래스에 맞춰서
    layers.Dense(image_data.num_classes, activation = 'softmax')
])

# 마지막 레이어
predictions = model(image_batch)

# 컴파일
model.compile(
    optimizer = tf.keras.optimizers.Adam(),
    loss = 'categorical_crossentropy',
    metrics = ['acc']
)
model.summary()

💻 출력


📌 배치별 loss와 acc을 반환해주는 함수

# callback 정의
class CollectBatchStats(tf.keras.callbacks.Callback):
  # loss와 acc를 배치별로 출력해줌
  def __init__(self):
    self.batch_losses = []
    self.batch_acc = []

  def on_train_batch_end(self, batch, logs = None):
    self.batch_losses.append(logs['loss'])
    self.batch_acc.append(logs['acc'])
    self.model.reset_metrics()

📌 학습 및 성능 평가

# 학습
steps_per_epoch = np.ceil(image_data.samples/image_data.batch_size)

batch_stats_callback = CollectBatchStats()

history = model.fit_generator(image_data, epochs = 2, 
                              steps_per_epoch = steps_per_epoch,
                              callbacks = [batch_stats_callback])
                              
# class name 할당
class_names  = sorted(image_data.class_indices.items(), key = lambda pair:pair[1])
class_names = np.array([key.title() for key, value in class_names])

# 다시 예측
predicted_batch = model.predict(image_batch)
predicted_id = np.argmax(predicted_batch, axis = -1)
predicted_label_batch = class_names[predicted_id]

label_id = np.argmax(label_batch, axis = -1)

정답들은 초록 글씨로, 오답들은 빨간 글씨로 출력하게 했는데 대부분 정답을 맞춘 것 처럼 보여졌다.

profile
데이터 분석가(가 되고픈) 황성미입니다!
post-custom-banner

0개의 댓글