✍🏻 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을 가진 모델을 저장까지 해준다.
이미 학습이 잘 된 모델을 나에게 맞게 조금만 변형하여 사용하는 것을 전이학습이라 한다.
이때 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를 얻을 수 있다.
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.load
의 split
옵션을 이용하면 불러올 때 각 인덱스를 기준으로 데이터를 분리해서 가져올 수 있다.
📌 데이터 확인
# 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
모델은 위와 같은 구조를 가진다. 그리고 아래와 같은 코드로 학습된 모델을 불러올 수 있다.
# 사전 훈련된 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'
# (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)
정답들은 초록 글씨로, 오답들은 빨간 글씨로 출력하게 했는데 대부분 정답을 맞춘 것 처럼 보여졌다.