CNN Fashion-MNIST 테스트 (PyTorch)

Fashion-MNIST에 대한 설명은 아래 링크로 대신하겠습니다.

Fashion-MNIST is a dataset of Zalando‘s article images—consisting of a training set of 60,000 examples and a test set of 10,000 examples. Each example is a 28×28 grayscale image, associated with a label from 10 classes. We intend Fashion-MNIST to serve as a direct drop-in replacement for the original MNIST dataset for benchmarking machine learning algorithms. It shares the same image size and structure of training and testing splits.

본 예제 코드는 데이터셋을 학습해서 입력되는 이미지가 어떤 분류에 속하는지를 예측해보는 것입니다. Fashion-MNIST 데이터셋을 벡터 공간에 표시하면 위와 같은 이미지로 분류할 수 있습니다.

이제 학습을 위해 해당 데이터셋을 다운로드합니다.

# Define a transform to normalize the data
transform = transforms.Compose([transforms.ToTensor(),
                                transforms.Normalize((0.5,), (0.5,))])

# Download and load the training data
train_loader = torch.utils.data.DataLoader(datasets.FashionMNIST('../F_MNIST_data/', download=True, train=True, transform=transform), batch_size=128, shuffle=True)

# Download and load the test data
test_loader = torch.utils.data.DataLoader(datasets.FashionMNIST('../F_MNIST_data/', download=True, train=False, transform=transform), batch_size=128, shuffle=True)

다운로드한 데이터가 어떤 이미지가 있는지 살펴보기 위해서 랜덤하게 몇개의 샘플을 추출해서 표시해보겠습니다. 해당 이미지들은 10개 [‘t-shirt’, ‘trouser’, ‘pullover’, ‘press’, ‘coat’, ‘sandal’, ‘shirt’, ‘sneaker’, ‘bag’, ‘ankleboot’]로 분류할 수 있는 패션 아이템들입니다.

x_train, y_train = next(iter(train_loader))
x_valid, y_valid = next(iter(test_loader))

fig, ax = plt.subplots(5,5)
fig.set_size_inches((20,14))
for i in range(5):
    for j in range(5):
        idx = numpy.random.randint(128)
        ax[i][j].imshow(x_train[idx,0,:])
        ax[i][j].set_xlabel(label[y_train[idx].item()])
        ax[i][j].set_xticklabels([])
        ax[i][j].set_yticklabels([])

학습을 위한 모델을 선언합니다. 이전 MNIST 데이터셋을 테스트했을 때와 같은 모델을 재활용했습니다.

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        
        self.convs = nn.Sequential(
            nn.Conv2d(1, 10, kernel_size=3), # input_channel, output_channel, kernel_size
            nn.ReLU(),
            nn.BatchNorm2d(10),
            nn.Conv2d(10, 20, kernel_size=3, stride=2),
            nn.ReLU(),
            nn.BatchNorm2d(20),
            nn.Conv2d(20, 40, kernel_size=3, stride=2)
        )
        
        self.layers = nn.Sequential(
            nn.Linear(40*5*5, 500),
            nn.Dropout(p=0.2),
            nn.ReLU(),
            nn.BatchNorm1d(500),
            nn.Linear(500,250),
            nn.Linear(250,100),
            nn.Dropout(p=0.2),
            nn.ReLU(),
            nn.BatchNorm1d(100),
            nn.Linear(100,50),
            nn.Linear(50, 10),
            nn.Softmax(dim=-1)
        )

    def forward(self, x):
        x = self.convs(x)
        x = x.view(-1, 40*5*5)
        return self.layers(x)
    
cnn = Net().to(DEVICE)

이전 MNIST 코드는 하나의 mini batch 데이터만 학습했다면 이번에는 전체 데이터를 대상으로 학습을 진행합니다. 많은 학습을 거친다면 모델의 정확도가 높아지겠지만 성능을 높이는 테스트가 아니기 때문에 최소한의 학습 epcohs만 수행합니다.

optimizer = optim.Adam(cnn.parameters())
criterion = nn.CrossEntropyLoss()

hist_loss = []
hist_accr = []

epochs = 30

for epoch in range(epochs):

    for idx, (data, label) in enumerate(train_loader):
        data, label = data.to(DEVICE), label.to(DEVICE)
        output = cnn(data)
        loss = criterion(output, label)
        
        predict = torch.argmax(output, dim=-1) == label
        accuracy = predict.float().mean().item()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        hist_loss.append(loss.item())
        hist_accr.append(accuracy)

        if idx % 100 == 0:
            print('Epoch {}, idx {}, Loss : {:.5f}, Accuracy : {:.5f}'.format(epoch, idx, loss.item(), accuracy))

학습이 완료되고 학습의 진행이 어떻게 되었는지 알기 위해서 사전에 정의한 hist_loss와 hist_accr을 사용해서 시각화 해보겠습니다.

fig, ax = plt.subplots(2,1)
fig.set_size_inches((12,8))

ax[0].set_title('Loss')
ax[0].plot(hist_loss, color='red')
ax[0].set_ylabel('Loss')
ax[1].set_title('Accuracy')
ax[1].plot(hist_accr, color='blue')
ax[1].set_ylabel('Accuracy')
ax[1].set_xlabel('Epochs')

학습이 완료된 후에 테스트 데이터를 사용해서 모델의 정확도를 확인해보았고 결과 값으로 Accuracy : 0.93750를 얻었습니다.

cnn.eval()

with torch.no_grad():
    for idx, (data, label) in enumerate(test_loader):
        data, label = data.to(DEVICE), label.to(DEVICE)
        output = cnn(data)
        loss = criterion(output, label)
    
        predict = torch.argmax(output, dim=-1) == label
        accuracy =  predict.float().mean().item()
    
        print('Accuracy : {:.5f}'.format(accuracy))

CNN MNIST 테스트 (PyTorch)

CNN 알고리즘을 MNIST 데이터셋을 활용해서 테스트해봅니다.

CNN 알고리즘에 대한 다양한 많은 설명이 있으니 자세한 내용은 아래의 강의를 참고하시기 바랍니다. 비록 작은 부분의 차이들은 있을 수 있지만 본 예제 역시 인터넷에 많은 소스 코드와 다르지 않습니다.
단, 아래의 영상은 텐서플로우로 설명하는 영상이지만 본 예제는 파이토치로 구현되어 있으며 학습도 전체 데이터를 대상으로 하지 않고 첫번째 미니배치만 학습하는 것으로 작성했습니다.

필요한 라이브러리를 임포트하고 GPU 사용 설정하는 부분과 MNIST 데이터를 로드하는 부분에 대해서는 자세한 설명을 하지 않고 지나가겠습니다. GPU 설정이 필요 없는 CPU 상에서 예제를 구동하는 경우는 device 설정을 하지 않고 넘어가셔도 무방합니다.

GPU 서버가 없는 경우는 무료로 Colab을 이용하시는 것도 추천합니다.

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms, datasets

import matplotlib.pyplot as plt

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

BATCH_SIZE = 128

train_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data',
                   train=True,
                   download=True,
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=BATCH_SIZE, shuffle=True)
test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data',
                   train=False, 
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=BATCH_SIZE, shuffle=True)

MNIST 데이터는 DataLoader로 불러왔습니다. 데이터는 (128,1,28,28) 형태로 149개로 분리되어 있습니다. 테스트 데이터 역시 마찬가지입니다. 128은 배치사이즈, 1은 채널 사이즈, (28*28)은 이미지의 크기입니다.

본 테스트에서는 전체 149개 데이터를 모두 학습하지 않고 1세트만 301회 학습을 수행합니다. 더 높은 정확도 얻고자 하시는 분은 전체 데이터를 통해 더 많은 학습을 해보시기 바랍니다.

dataset = next(iter(train_loader)) # 학습 데이터
x_data = dataset[0] # x 데이터
y_data = dataset[1] # label 데이터

dataset = next(iter(test_loader)) # 검증 데이터
x_test = dataset[0].to(DEVICE) # x 데이터
y_test = dataset[1].to(DEVICE) # label 데이터

각 데이터 셋에 어떤 이미지가 있는지 확인해보기 위해서 아래와 같은 코드를 수행합니다. MNIST 데이터 셋은 동일한 크기의 손글씨 이미지가 들어있기 때문에 각각의 이미지를 표시해보면 0-9까지의 손글씨 이미지가 저장되어 있는 것을 확인 할 수 있습니다.
아래와 같이 0번째 배열에 숫자 5가 있는 것을 확인 할 수 있습니다.

plt.imshow(x_data[0,0,:])

참고로 CNN의 입력 데이터의 Shape은 아래의 그림과 같습니다. 입력 데이터는 4개의 차원으로 구성되어 있습니다. 가장 먼저는 Batch_Size로 해당 이미지의 갯수를 의미합니다. 다음에 나오는 것은 이미지의 Channel입니다. 3인경우는 RGB 값을 가지고 1인 경우는 대부분 단일 색상으로 표현하는 값입니다. 그리고 나오는 값은 Height, Width 값입니다.

이런 상태에서 [0,0,:]의 의미는 0번째 이미지에서 첫번째 채널의 이미지 데이터를 가지고 온다는 의미가 됩니다.

이제 학습을 위한 모델을 구성합니다. 본 모델을 크게 두부분으로 되어 있습니다. self.convs는 convolution을 수행하는 부분으로 원본 이미지에서 특징정보를 추출하는 부분입니다. 이때 중요한 것은 각 Conv2d를 수행하며 어떤 형태의 아웃풋이 나오는지 확인하는 것이 중요합니다.

예를 들어 28*28 이미지를 kernel_size 3으로 계산하면 출력되는 이미지 사이즈는 (26*26)입니다. 어떻게 이렇게 나오는지는 아래 식에서 확인하실 수 있습니다.

https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html

하지만 매번 위의 식을 통해서 계산하는 것은 좀 귀찮고 힘든 입니다. 위의 공식을 간단한 함수로 구현한 내용을 공유해드립니다.

def conv_output_shape(h_w, kernel_size=1, stride=1, pad=0, dilation=1):
    from math import floor
    if type(kernel_size) is not tuple:
        kernel_size = (kernel_size, kernel_size)
    h = floor( ((h_w[0] + (2 * pad) - ( dilation * (kernel_size[0] - 1) ) - 1 )/ stride) + 1)
    w = floor( ((h_w[1] + (2 * pad) - ( dilation * (kernel_size[1] - 1) ) - 1 )/ stride) + 1)
    return h, w

위와 같은 식을 거쳐서 self.convs 레이어의 최종 output_shape은 총 5*5 이미지 사이즈를 가진 40장의 이미지 데이터를 얻을 수 있습니다.

이렇게 얻은 데이터는 self.layers를 거치다 보면 최종 0~9까지의 숫자 정보를 얻을 수 있습니다.

PyTorch의 Conv2d 패키지는 프로그래머가 간단히 Convolution Layer를 구성할 수 있도록 해줍니다. 프로그래머는 간단히 입력 채널의 수와 출력 채널의 수 그리고 커널 사이즈와 스트라이드 정보만 맞춰주면 자동으로 이미지를 구성해줍니다.

아래의 경우는 최초 28×28 이미지를 입력하고 커널을 3으로 맞춰서 앞선 함수를 통해서 출력 shape을 보면 26×26의 이미지를 출력한다는 것을 확인 할 수 있습니다. 또 다음 레이어는 커널을 3, 스트라이드를 2로 정의하고 이전에 입력된 이미지의 크기를 입력하면 출력 이미지는 12×12로 표시되는 것을 확인 할 수 있습니다. 이런 방법으로 마지막 이미지가 출력되는 크기는 5×5의 이미지가 됩니다.

그렇게 되면 마지막의 fully-connected layer에 들어가는 값은 40×5×5의 입력 값이 됩니다. 그리고 맨 마지막까지 Linear Layer를 거치게 되면 10개의 값으로 출력되고 이에 Softmax를 취하면 0-9 중에 하나의 값을 예측하게 됩니다.

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        
        self.convs = nn.Sequential(
            nn.Conv2d(1, 10, kernel_size=3), # input_channel, output_channel, kernel_size
            nn.ReLU(),
            nn.BatchNorm2d(10),
            nn.Conv2d(10, 20, kernel_size=3, stride=2),
            nn.ReLU(),
            nn.BatchNorm2d(20),
            nn.Conv2d(20, 40, kernel_size=3, stride=2)
        )
        
        self.layers = nn.Sequential(
            nn.Linear(40*5*5, 500),
            nn.ReLU(),
            nn.BatchNorm1d(500),
            nn.Linear(500,250),
            nn.Linear(250,100),
            nn.ReLU(),
            nn.BatchNorm1d(100),
            nn.Linear(100,50),
            nn.Linear(50, 10),
            nn.Softmax(dim=-1)
        )

    def forward(self, x):
        x = self.convs(x)
        x = x.view(-1, 40*5*5)
        return self.layers(x)
    
cnn = Net().to(DEVICE)

이제 학습을 위한 모든 준비가 완료되었고 아래와 같이 학습을 수행합니다. 예제에서는 간단히 301회 학습을 수행했습니다. 학습이 진행되면서 loss와 accuracy 정보의 변화를 기록하기 위해서 list 변수를 각각 선언해줍니다.

또 100회 학습이 완료될 때마다 변화되는 loss와 accuracy 값을 화면에 출력해줍니다.

optimizer = optim.Adam(cnn.parameters())
criterion = nn.CrossEntropyLoss()

hist_loss = []
hist_accr = []

epochs = 301
for epoch in range(epochs):
    cnn.train()
    output = cnn(x_data)
    loss = criterion(output, y_data)
    
    predict = torch.argmax(output, dim=-1) == y_data
    accuracy = predict.float().mean().item()
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    hist_loss.append(loss)
    hist_accr.append(accuracy)
    
    if epoch % 100 == 0:
        print('epoch{}, {:.5f}, {:.5f}'.format(epoch, loss.item(), accuracy))

학습이 완료되고 loss와 accuracy 값을 그래프로 그려줍니다.
그래프를 보니 학습의 곡선이 완만하게 내려가고 정확도는 1에 가까운 값을 나타내어 학습이 잘이뤄지는 것을 확인할 수 있습니다.

그러나 training 데이터를 통한 학습정확도이기 때문에 검증용 데이터를 통해서 정확도 계산을 다시 할 필요가 있습니다.

fig, ax = plt.subplots(2,1)
fig.set_size_inches((12,8))

ax[0].set_title('Loss')
ax[0].plot(hist_loss, color='red')
ax[0].set_ylabel('Loss')
ax[1].set_title('Accuracy')
ax[1].plot(hist_accr, color='blue')
ax[1].set_ylabel('Accuracy')
ax[1].set_xlabel('Epochs')

검증용 데이터셋(test_dataloader)를 통해서 한 배치 정보를 얻어서 방금 수행한 모델의 정확도를 테스트해봅니다. 테스트 결과 0.7109375 값을 얻을 수 있었습니다. 높은 값은 아니지만 전체 469개 미니배치 중에서 1개 데이터셋만 테스트 했기 때문에 전체 데이터를 대상으로 테스트하면 보다 높은 정확도를 얻을 수 있을 것입니다.

cnn.eval()

with torch.no_grad():
    output = cnn(x_test)
    loss = criterion(output, y_test)
    
    predict = torch.argmax(output, dim=-1) == y_test
    accuracy =  predict.float().mean().item()
    
    print(accuracy)