PyTorch 심층신경망(DNN) 모델 생성 후 Fashion MNIST 이미지 분류기 생성, 학습, 예측, 검증 성능 측정하기
Aug 9, 2022

PyTorch 심층신경망(Deep Neural Network) 모델을 생성하고 PyTorch의 내장 데이터셋인 Fashion MNIST 데이터셋을 로드하여 이미지 분류기를 생성, 학습, 예측, 검증 성능 측정하는 방법을 알아보겠습니다.

최대한 주석을 꼼꼼히 달아 놓았습니다. 처음 PyTorch를 활용하여 신경망 모델을 생성해 보시는 분들은 주석을 꼼꼼히 참고해 주시기 바랍니다.

import numpy as np
import torch
from torch.utils.data import Dataset
from torchvision import datasets, transforms
  • torchvision.datasets 에서 데이터 로드

  • 아래 링크에서 built-in datasets의 목록을 확인해 볼 수 있습니다.

torchvisionImage Transform 에 대하여 생소하다면 다음의 링크를 참고해 주시기 바랍니다.

# Image Transform 정의
transform = transforms.Compose([
    transforms.ToTensor(),
])

Fashion MNIST 내장 데이터셋을 로드하여 실습을 진행합니다.

# train(학습용) 데이터셋 로드
train_data = datasets.FashionMNIST(root='data', 
                                   train=True,        # 학습용 데이터셋 설정(True)
                                   download=True, 
                                   transform=transform                
                                  )
# test(학습용) 데이터셋 로드
test_data = datasets.FashionMNIST(root='data', 
                                  train=False,        # 검증용 데이터셋 설정(False)
                                  download=True, 
                                  transform=transform
                                 )

FashionMNIST 데이터셋 시각화

  • 총 10개의 카테고리로 구성되어 있으며, Label은 아래 코드에서 labels_map에 정의되어 있습니다.

  • 출처: zalandoresearch/fashion-mnist

import matplotlib.pyplot as plt

labels_map = {
    0: "t-shirt/top",
    1: "trouser",
    2: "pullover",
    3: "dress",
    4: "coat",
    5: "sandal",
    6: "shirt",
    7: "sneaker",
    8: "bag",
    9: "ankle boot",
}

figure = plt.figure(figsize=(10, 10))
cols, rows = 6, 5

for i in range(1, cols * rows + 1):
    sample_idx = torch.randint(len(train_data), size=(1,)).item()
    img, label = train_data[sample_idx]
    figure.add_subplot(rows, cols, i)
    plt.title(labels_map[label])
    plt.axis("off")
    plt.imshow(torch.permute(img, (1, 2, 0)), cmap='gray')
plt.show()

torch.utils.data.DataLoader

DataLoader는 배치 구성과 shuffle등을 편하게 구성해 주는 util 입니다.

batch_size = 32 # batch_size 지정
num_workers = 8 # Thread 숫자 지정 (병렬 처리에 활용할 쓰레드 숫자 지정)
train_loader = torch.utils.data.DataLoader(train_data, 
                                           batch_size=batch_size,
                                           shuffle=True, 
                                           num_workers=num_workers)
test_loader = torch.utils.data.DataLoader(test_data, 
                                          batch_size=batch_size,
                                          shuffle=False, 
                                          num_workers=num_workers)

train_loader의 1개 배치의 shape 출력

# 1개의 배치 추출 후 Image, label의 shape 출력
img, lbl = next(iter(train_loader))
img.shape, lbl.shape
(torch.Size([32, 1, 28, 28]), torch.Size([32]))

배치사이즈인 32가 가장 첫번째 dimension에 출력되고, 그 뒤로 채널, 세로, 가로 순서로 출력이 됩니다.

즉, greyscale 의 28 X 28 이미지 32장이 1개의 배치로 구성이 되어 있습니다.

모델 정의

CUDA 설정이 되어 있다면 cuda를! 그렇지 않다면 cpu로 학습합니다.

(제 PC에는 GPU가 2대 있어서 cuda:0로 GPU 장비의 index를 지정해 주었습니다. 만약 다른 장비를 사용하고 싶다면 cuda:1 이런식으로 지정해 주면 됩니다)

# device 설정 (cuda:0 혹은 cpu)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
cuda:0

아래의 모델은 DNN으로 구성하였습니다. 추후, 모델 부분을 CNN이나 pre-trained model로 교체할 수 있습니다.

import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim


class DNNModel(nn.Module):
    def __init__(self):
        super(DNNModel, self).__init__()
        self.fc1 = nn.Linear(28*28, 512)
        self.fc2 = nn.Linear(512, 128)
        self.fc3 = nn.Linear(128, 32)
        self.output = nn.Linear(32, 10)
    
    def forward(self, x):
        x = x.view(-1, 28*28)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = F.relu(self.fc3(x))
        x = self.output(x)
        return x     
model = DNNModel() # Model 생성
model.to(device)   # device 에 로드 (cpu or cuda)
DNNModel(
  (fc1): Linear(in_features=784, out_features=512, bias=True)
  (fc2): Linear(in_features=512, out_features=128, bias=True)
  (fc3): Linear(in_features=128, out_features=32, bias=True)
  (output): Linear(in_features=32, out_features=10, bias=True)
)
# 옵티마이저를 정의합니다. 옵티마이저에는 model.parameters()를 지정해야 합니다.
optimizer = optim.Adam(model.parameters(), lr=0.0005)

# 손실함수(loss function)을 지정합니다. Multi-Class Classification 이기 때문에 CrossEntropy 손실을 지정하였습니다.
loss_fn = nn.CrossEntropyLoss()

훈련(Train)

from tqdm import tqdm  # Progress Bar 출력
def model_train(model, data_loader, loss_fn, optimizer, device):
    # 모델을 훈련모드로 설정합니다. training mode 일 때 Gradient 가 업데이트 됩니다. 반드시 train()으로 모드 변경을 해야 합니다.
    model.train()
    
    # loss와 accuracy 계산을 위한 임시 변수 입니다. 0으로 초기화합니다.
    running_loss = 0
    corr = 0
    
    # 예쁘게 Progress Bar를 출력하면서 훈련 상태를 모니터링 하기 위하여 tqdm으로 래핑합니다.
    prograss_bar = tqdm(data_loader)
    
    # mini-batch 학습을 시작합니다.
    for img, lbl in prograss_bar:
        # image, label 데이터를 device에 올립니다.
        img, lbl = img.to(device), lbl.to(device)
        
        # 누적 Gradient를 초기화 합니다.
        optimizer.zero_grad()
        
        # Forward Propagation을 진행하여 결과를 얻습니다.
        output = model(img)
        
        # 손실함수에 output, label 값을 대입하여 손실을 계산합니다.
        loss = loss_fn(output, lbl)
        
        # 오차역전파(Back Propagation)을 진행하여 미분 값을 계산합니다.
        loss.backward()
        
        # 계산된 Gradient를 업데이트 합니다.
        optimizer.step()
        
        # output의 max(dim=1)은 max probability와 max index를 반환합니다.
        # max probability는 무시하고, max index는 pred에 저장하여 label 값과 대조하여 정확도를 도출합니다.
        _, pred = output.max(dim=1)
        
        # pred.eq(lbl).sum() 은 정확히 맞춘 label의 합계를 계산합니다. item()은 tensor에서 값을 추출합니다.
        # 합계는 corr 변수에 누적합니다.
        corr += pred.eq(lbl).sum().item()
        
        # loss 값은 1개 배치의 평균 손실(loss) 입니다. img.size(0)은 배치사이즈(batch size) 입니다.
        # loss 와 img.size(0)를 곱하면 1개 배치의 전체 loss가 계산됩니다.
        # 이를 누적한 뒤 Epoch 종료시 전체 데이터셋의 개수로 나누어 평균 loss를 산출합니다.
        running_loss += loss.item() * img.size(0)
        
    # 누적된 정답수를 전체 개수로 나누어 주면 정확도가 산출됩니다.
    acc = corr / len(data_loader.dataset)
    
    # 평균 손실(loss)과 정확도를 반환합니다.
    # train_loss, train_acc
    return running_loss / len(data_loader.dataset), acc

평가(Evaluate)

def model_evaluate(model, data_loader, loss_fn, device):
    # model.eval()은 모델을 평가모드로 설정을 바꾸어 줍니다. 
    # dropout과 같은 layer의 역할 변경을 위하여 evaluation 진행시 꼭 필요한 절차 입니다.
    model.eval()
    
    # Gradient가 업데이트 되는 것을 방지 하기 위하여 반드시 필요합니다.
    with torch.no_grad():
        # loss와 accuracy 계산을 위한 임시 변수 입니다. 0으로 초기화합니다.
        corr = 0
        running_loss = 0
        
        # 배치별 evaluation을 진행합니다.
        for img, lbl in data_loader:
            # device에 데이터를 올립니다.
            img, lbl = img.to(device), lbl.to(device)
            
            # 모델에 Forward Propagation을 하여 결과를 도출합니다.
            output = model(img)
            
            # output의 max(dim=1)은 max probability와 max index를 반환합니다.
            # max probability는 무시하고, max index는 pred에 저장하여 label 값과 대조하여 정확도를 도출합니다.
            _, pred = output.max(dim=1)
            
            # pred.eq(lbl).sum() 은 정확히 맞춘 label의 합계를 계산합니다. item()은 tensor에서 값을 추출합니다.
            # 합계는 corr 변수에 누적합니다.
            corr += torch.sum(pred.eq(lbl)).item()
            
            # loss 값은 1개 배치의 평균 손실(loss) 입니다. img.size(0)은 배치사이즈(batch size) 입니다.
            # loss 와 img.size(0)를 곱하면 1개 배치의 전체 loss가 계산됩니다.
            # 이를 누적한 뒤 Epoch 종료시 전체 데이터셋의 개수로 나누어 평균 loss를 산출합니다.
            running_loss += loss_fn(output, lbl).item() * img.size(0)
        
        # validation 정확도를 계산합니다.
        # 누적한 정답숫자를 전체 데이터셋의 숫자로 나누어 최종 accuracy를 산출합니다.
        acc = corr / len(data_loader.dataset)
        
        # 결과를 반환합니다.
        # val_loss, val_acc
        return running_loss / len(data_loader.dataset), acc

모델 훈련(training) & 검증

# 최대 Epoch을 지정합니다.
num_epochs = 20

min_loss = np.inf

# Epoch 별 훈련 및 검증을 수행합니다.
for epoch in range(num_epochs):
    # Model Training
    # 훈련 손실과 정확도를 반환 받습니다.
    train_loss, train_acc = model_train(model, train_loader, loss_fn, optimizer, device)

    # 검증 손실과 검증 정확도를 반환 받습니다.
    val_loss, val_acc = model_evaluate(model, test_loader, loss_fn, device)   
    
    # val_loss 가 개선되었다면 min_loss를 갱신하고 model의 가중치(weights)를 저장합니다.
    if val_loss < min_loss:
        print(f'[INFO] val_loss has been improved from {min_loss:.5f} to {val_loss:.5f}. Saving Model!')
        min_loss = val_loss
        torch.save(model.state_dict(), 'DNNModel.pth')
    
    # Epoch 별 결과를 출력합니다.
    print(f'epoch {epoch+1:02d}, loss: {train_loss:.5f}, acc: {train_acc:.5f}, val_loss: {val_loss:.5f}, val_accuracy: {val_acc:.5f}')
100% 1875/1875 [00:04<00:00, 445.34it/s]
val_loss has been improved from inf to 0.42429. Saving Model!
epoch 01, loss: 0.55653, acc: 0.80107, val_loss: 0.42429, val_accuracy: 0.84950
100% 1875/1875 [00:04<00:00, 460.48it/s]
val_loss has been improved from 0.42429 to 0.38282. Saving Model!
epoch 02, loss: 0.37441, acc: 0.86393, val_loss: 0.38282, val_accuracy: 0.86280
100% 1875/1875 [00:04<00:00, 453.84it/s]
epoch 03, loss: 0.33538, acc: 0.87767, val_loss: 0.38931, val_accuracy: 0.86290
100% 1875/1875 [00:04<00:00, 455.05it/s]
val_loss has been improved from 0.38282 to 0.35129. Saving Model!
epoch 04, loss: 0.30863, acc: 0.88853, val_loss: 0.35129, val_accuracy: 0.86890
100% 1875/1875 [00:04<00:00, 447.22it/s]
val_loss has been improved from 0.35129 to 0.34075. Saving Model!
epoch 05, loss: 0.28994, acc: 0.89215, val_loss: 0.34075, val_accuracy: 0.87460
100% 1875/1875 [00:04<00:00, 450.35it/s]
epoch 06, loss: 0.27176, acc: 0.89980, val_loss: 0.36094, val_accuracy: 0.87150
100% 1875/1875 [00:04<00:00, 443.07it/s]
epoch 07, loss: 0.25868, acc: 0.90415, val_loss: 0.34475, val_accuracy: 0.87800
100% 1875/1875 [00:04<00:00, 447.50it/s]
val_loss has been improved from 0.34075 to 0.32414. Saving Model!
epoch 08, loss: 0.24608, acc: 0.90673, val_loss: 0.32414, val_accuracy: 0.88970
100% 1875/1875 [00:04<00:00, 448.74it/s]
epoch 09, loss: 0.23623, acc: 0.91130, val_loss: 0.32660, val_accuracy: 0.88340
100% 1875/1875 [00:04<00:00, 450.33it/s]
epoch 10, loss: 0.22619, acc: 0.91510, val_loss: 0.32550, val_accuracy: 0.88620
100% 1875/1875 [00:04<00:00, 450.43it/s]
epoch 11, loss: 0.21585, acc: 0.91827, val_loss: 0.33025, val_accuracy: 0.88710
100% 1875/1875 [00:04<00:00, 444.86it/s]
epoch 12, loss: 0.20716, acc: 0.92187, val_loss: 0.35040, val_accuracy: 0.88430
100% 1875/1875 [00:04<00:00, 441.60it/s]
epoch 13, loss: 0.19787, acc: 0.92447, val_loss: 0.35395, val_accuracy: 0.88630
100% 1875/1875 [00:04<00:00, 445.40it/s]
epoch 14, loss: 0.18987, acc: 0.92833, val_loss: 0.34877, val_accuracy: 0.88420
100% 1875/1875 [00:04<00:00, 450.62it/s]
epoch 15, loss: 0.18462, acc: 0.92942, val_loss: 0.32642, val_accuracy: 0.89690
100% 1875/1875 [00:04<00:00, 445.37it/s]
epoch 16, loss: 0.17759, acc: 0.93293, val_loss: 0.36149, val_accuracy: 0.88150
100% 1875/1875 [00:04<00:00, 446.42it/s]
epoch 17, loss: 0.17055, acc: 0.93517, val_loss: 0.33631, val_accuracy: 0.89640
100% 1875/1875 [00:04<00:00, 450.76it/s]
epoch 18, loss: 0.16626, acc: 0.93688, val_loss: 0.36313, val_accuracy: 0.89320
100% 1875/1875 [00:04<00:00, 452.99it/s]
epoch 19, loss: 0.15961, acc: 0.93843, val_loss: 0.38574, val_accuracy: 0.88100
100% 1875/1875 [00:04<00:00, 450.54it/s]
epoch 20, loss: 0.15411, acc: 0.94178, val_loss: 0.36963, val_accuracy: 0.89350

저장한 가중치 로드 후 검증 성능 측정

# 모델에 저장한 가중치를 로드합니다.
model.load_state_dict(torch.load('DNNModel.pth'))
# 최종 검증 손실(validation loss)와 검증 정확도(validation accuracy)를 산출합니다.
final_loss, final_acc = model_evaluate(model, test_loader, loss_fn, device)
print(f'evaluation loss: {final_loss:.5f}, evaluation accuracy: {final_acc:.5f}')
evaluation loss: 0.32414, evaluation accuracy: 0.88970


관련 글 더보기

- PyTorch Dataset을 상속받은 사용자 정의 이미지 데이터셋(CustomImageDataset) 구성하기

- PyTorch 전이학습(transfer learning)과 미세조정(fine tuning)으로 이미지 분류기 생성, 예측, 검증성능 평가하기

- PyTorch 로컬이미지 로드(ImageFolder) 후, DataLoader 생성 및 CNN이미지 분류 모델 생성, 학습, 검증 성능 측정하기

- PyTorch 정형데이터를 CustomDataset으로 서브클래싱(SubClassing)한 후 예측 모델 생성 및 학습하기

- PyTorch의 자동미분(AutoGrad)을 활용하여 정형 데이터 예측기(regression model)구현

데이터 분석, 머신러닝, 딥러닝의 대중화를 꿈 꿉니다.