DNN을 이용한 Fashion-MNIST 데이터에 대한 Classifier

처음 딥러닝을 테스트 하기 위해 흔히 사용하는 데이터는 MNIST 입니다. 0~9까지의 손글씨에 대한 28×28 크기의 이미지입니다. 이미지 데이터와 함께 라벨 데이터도 제공되므로 바로 활용할 수 있는 데이터입니다. MNSIT에 영감을 받은 Fashion-MNIST라는 데이터가 존재합니다. 총 10가지의 패션 아이템에 대한 이미지와 라벨입니다. 제공되는 이미지의 예는 아래와 같습니다.

위 이미지들에서 상단부터 3개의 Rows씩을 한 그룹으로 묶으면 각각의 그룹이 의미하는 것은 아래의 표와 그 항목의 순서가 같습니다.

Label Description
0 T-shirt/top
1 Trouser
2 Pullover
3 Dress
4 Coat
5 Sandal
6 Shirt
7 Sneaker
8 Bag
9 Ankle boot

훈련을 위한 이미지와 라벨의 수는 각각 60,000개, 시험을 위한 이미지와 라벨의 수는 각각 10,000개입니다. 이 글은 Fashion-MNIST를 PyTorch를 이용해 훈련을 시켜보는 코드와 그 결과에 대한 설명입니다.

먼저 필요한 라이브러리를 import 합니다.

import torch
import torch.nn as nn
import torchvision.datasets as dset
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
from torch.utils.data import DataLoader

하이퍼 파라메터는 다음과 같습니다.

batch_size = 100
num_epochs = 250
learning_rate = 0.0001

훈련에 필요한 데이터와 시험에 필요한 데이터를 다운로드 받아야 하는데, PyTorch에서는 이를 위한 도구를 지원하므로, 이를 활용하여 다음 코드처럼 MNIST_Fashion 폴더에 데이터를 다운받고 데이터를 활용할 준비를 합니다.

root = './MNIST_Fashion'
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize(mean=(0.5,), std=(0.5,))])
train_data = dset.FashionMNIST(root=root, train=True, transform=transform, download=True)
test_data = dset.FashionMNIST(root=root, train=False, transform=transform, download=True)

train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, shuffle=True, drop_last=True)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=batch_size, shuffle=False, drop_last=True)

이제 모델을 정의(아래 코드의 4번 코드인 DNN 클래스의 __init__ 함수)하고 사용할 손실함수(아래의 37번 코드)와 최소의 손실값을 가지는 가중치와 편향값을 찾기 위해 방법(아래의 38번 코드) 및 맨 처음 가중치(아래의 30번 weights_init 코드와 35번 코드)를 초기화합니다.

device = torch.device(device + ":0")

class DNN(nn.Module):
    def __init__(self):
        super(DNN, self).__init__()

        self.layer1 = nn.Sequential(
            torch.nn.Linear(784, 256, bias=True),
            torch.nn.BatchNorm1d(256),
            torch.nn.ReLU()
        )

        self.layer2 = nn.Sequential(
            torch.nn.Linear(256, 64, bias=True),
            torch.nn.BatchNorm1d(64),
            torch.nn.ReLU()
        )

        self.layer3 = nn.Sequential(
            torch.nn.Linear(64, 10, bias=True)
        )
    
    def forward(self, x):
        x = x.view(x.size(0), -1) # flatten
        x_out = self.layer1(x)
        x_out = self.layer2(x_out)
        x_out = self.layer3(x_out)
        return x_out

def weights_init(m):
    if isinstance(m, nn.Linear):
        nn.init.xavier_normal_(m.weight) 

model = DNN().to(device)
model.apply(weights_init)

criterion = torch.nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

위에서 사용한 모델은 DNN(Deep Neural Network)으로써 다음과 같습니다.

다음은 훈련(Train)에 대한 코드입니다.

costs = []
total_batch = len(train_loader)
for epoch in range(num_epochs):
    total_cost = 0

    for i, (imgs, labels) in enumerate(train_loader):
        imgs, labels = imgs.to(device), labels.to(device)

        outputs = model(imgs)
        loss = criterion(outputs, labels)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        total_cost += loss

    avg_cost = total_cost / total_batch
    print("Epoch:", "%03d" % (epoch+1), "Cost =", "{:.9f}".format(avg_cost))  
    costs.append(avg_cost)              

옳바르게 학습이 된다면 에폭이 증가될때마다 손실값은 점점 줄어 들게 되는데, X축이 에폭, Y축이 손실(비용)인 아래의 그래프의 결과를 도출했습니다.

이제 학습된 가중치와 편향값을 통해 테스트를 하는 코드는 아래와 같습니다.

model.eval()
with torch.no_grad():
    correct = 0
    total = 0
    for i, (imgs, labels) in enumerate(test_loader):
        imgs, labels = imgs.to(device), labels.to(device)
        outputs = model(imgs)
        _, argmax = torch.max(outputs, 1)
        total += imgs.size(0)
        correct += (labels == argmax).sum().item()
    
    print('Accuracy for {} images: {:.2f}%'.format(total, correct / total * 100))                

정확도는 89.39%가 도출되었는데요. 이 정확도를 점더 시각적으로 인지하기 위해 테스트 데이터에서 36개의 이미지를 뽑아 각각의 이미지가 어떤 것으로 분류되었는지 확인하는 코드를 작성해 보면 다음과 같습니다.

label_tags = {
    0: 'T-Shirt', 
    1: 'Trouser', 
    2: 'Pullover', 
    3: 'Dress', 
    4: 'Coat', 
    5: 'Sandal', 
    6: 'Shirt',
    7: 'Sneaker', 
    8: 'Bag', 
    9: 'Ankle Boot'
}

columns = 6
rows = 6
fig = plt.figure(figsize=(10,10))
 
model.eval()
for i in range(1, columns*rows+1):
    data_idx = np.random.randint(len(test_data))
    input_img = test_data[data_idx][0].unsqueeze(dim=0).to(device) 
 
    output = model(input_img)
    _, argmax = torch.max(output, 1)
    pred = label_tags[argmax.item()]
    label = label_tags[test_data[data_idx][1]]
    
    fig.add_subplot(rows, columns, i)
    if pred == label:
        plt.title(pred + ', right !!')
        cmap = 'Blues'
    else:
        plt.title('Not ' + pred + ' but ' +  label)
        cmap = 'Reds'
    plot_img = test_data[data_idx][0][0,:,:]
    plt.imshow(plot_img, cmap=cmap)
    plt.axis('off')
    
plt.show() 

위의 10번 코드에서 unsqueeze() 함수를 사용한 것은 원본 데이터의 Shape가 (1, 28, 28)인데, 이를 모델에 입력되는 데이터의 Shape인 (1, 1, 28, 28)로 변환해야 하기 때문입니다. 결과는 다음과 같습니다.

위의 이미지를 보면 분류가 3개 정도 틀린 것으로 표시됩니다. 주로 T셔츠를 그냥 셔츠로 분류하거나 스웨터를 셔츠로 분류한 경우입니다. 이 경우 CNN으로 학습하면 정확도를 더욱 향상시킬 수 있습니다.

넥스젠(NexGen)의 지오코딩(Geocoding) 기능

NexGen에서 제공하는 지오코딩 기능을 소개합니다. 지오코딩을 통해 주소를 좌표로 변환하여 원하는 심벌로 지도에 표시할 수 있는데요. 넥스젠에서는 지오코딩을 통해 변환된 공간 데이터는 기본적인 수치지도 레이어와 동일하게 라벨 등과 같은 모든 기능을 활용할 수 있습니다. 아래는 NexGen의 지오코딩 기능에 대한 소개 동영상입니다.

넥스젠의 지오코딩 기능은 사용이 심플하지만 그 어떤 GIS 프로그램에서 제공하는 지오코딩 보다 직관적이고 강력합니다. 넥스젠의 지오코딩은 이미 2014년부터 현재까지 매우 많은 분들이 사용하고 있는, Geocoder-Xr의 기반 서비스를 그대로 활용하고 있습니다.

주소 좌표 변환 툴, Geocoder-Xr

NexGen의 지오코딩은 항상 최신 주소 DB 사용할 수 있습니다. 최신 주소 DB를 통해 변환된 위치를 최신의 배경지도 위에 중첩할 수 있습니다. 이제 NexGen을 통해 웹에서도 바로 지오코딩을 하고, 지도 상에서 빠르게 시각화해 보세요.

GeoJSON Format(형식)

웹에서 위치 데이터를 로컬이나 서버단에서 읽고 저장하기 위한 포맷으로 무엇을 사용할 것인지 고민하다가, GeoJSON으로 결정을 했다. 사실 고민은 단 1도 안한듯.. 당연히 GeoJSON 이지 !

GeoJSON은 JSON으로 위치 데이터와 속성 데이터를 저장하는 형식이다. 다른 GIS 포맷과는 다르게 IETF(Internet Engineering Task Force)에서 제안되어 유지되고 있단다. RFC 번호는 7946이다. 좌표는 경위도 좌표체계로 저장되는데, 흔히 구글맵이나 OSM에서는 (위도, 경도)의 순서로 저장하는데 반해, GeoJSON은 (경도, 위도)의 순서로 저장된다고 한다.

GeoJSON은 다음과 같은 장점을 갖는다.

  • XML과 비교하여 스카마나 태그 규칙에 대해 훨씬 자유롭다. 사실 XML은 보기만해도 두통이 먼저 밀려온다.
  • 데이터 용량이 다른 포맷에 비해 상대적으로 작다.
  • JSON 형식이므로 프로그래밍 언어에서 쉽게 객체화 시킬 수 있다. 특히 Javascript에서는 단 1줄로 객체화 시킬 수 있다.
  • 다양한 응용 프로그램에 적재되기에 용이하며, 실제로 다양한 응용 프로그램에서 활용 된다.

실제 빈 GeoJSON 포맷은 아래와 같다.

{
    "type": "FeatureCollection",
    "features": []
}

features 안에 실제 데이터가 담기는데, 빈 Feature 요소는 다음과 같다.

{
    "type": "Feature",
    "geometry": {},
    "properties": {}
}

geometry에 종류(type)과 좌표(coordinates) 정보가 저장되고, properties에 속성 정보가 Key-Values로 자유롭게 저장된다. 아래는 기본 Geometry 종류에 대한 예이다. (출처: https://ko.wikipedia.org/wiki/GeoJSON)

{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [102.0, 0.5]
            },
            "properties": {
                "prop0": "value0"
            }
        },
        {
            "type": "Feature",
            "geometry": {
                "type": "LineString",
                "coordinates": [[102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]]
            },
            "properties": {
                "prop0": "value0",
                "prop1": 0.0
            }
        },
        {
            "type": "Feature",
            "geometry": {
                "type": "Polygon",
                "coordinates": [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0],[100.0, 1.0], [100.0, 0.0]]]
            },
            "properties": {
                "prop0": "value0",
                "prop1": { "this": "that" }
            }
        }
    ]
}

좀 더 자세한 기본 형태의 지오메트리와 복잡한 형태의 지오메트리에 대한 정보는 앞 예제의 출처를 참고하자.

Model 확장과 가중치값 변경을 통한 예측 정확도 향상

이전에 작성한 “PyTorch를 이용한 간단한 머신러닝”이라는 아래의 글에서는 은닉층이 없는 입력과 출력층으로만 구성된 모델을 사용했습니다. 그리고 가중치 및 편향값의 최적화를 위한 방법은 SGD, 즉 확률적 경사하강을 사용했습니다. 정확도는 대략 90%정도 나왔었습니다.

PyTorch를 이용한 간단한 머신러닝

이에 대해 은닉층을 2개 추가하고 매개변수의 최적화를 위한 방식을 SGD가 아닌 Adam을 사용하여 정확도를 향상시켜 보겠습니다. 은닉층이 추가 되었으므로 활성화 함수가 필요한데, 역전파에서 미분값 소실(Vanishing Gradient)이 발생할 가능성이 큰 시그모이드 함수가 아닌 ReLU 함수를 사용합니다. 즉, 모델은 다음과 같습니다.

위 모델을 구성하기 위한 PyTorch의 코드는 다음과 같습니다.

linear1 = torch.nn.Linear(784, 256, bias=True).to(device)
linear2 = torch.nn.Linear(256, 256, bias=True).to(device)
linear3 = torch.nn.Linear(256, 10, bias=True).to(device)
relu = torch.nn.ReLU()
model = torch.nn.Sequential(linear1, relu, linear2, relu, linear3).to(device)

그리고 매개변수에 대한 최적화 방법을 Adam을 사용하므로 이에 대한 코드는 아래와 같구요. 각 최적화 방식이 어떤식으로 작동하는지 시각적으로 확인할 수 있는 유용한 사이트인 http://www.denizyuret.com/2015/03/alec-radfords-animations-for.html을 참고하시기 바랍니다.

optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

기존의 소스코드에서 위의 변경된 부분이 반영된 전체 코드는 아래와 같습니다.

import torch
import torchvision

batch_size = 1000

mnist_train = torchvision.datasets.MNIST(root="MNIST_data/", train=True, transform=torchvision.transforms.ToTensor(), download=True)
mnist_test = torchvision.datasets.MNIST(root="MNIST_data/", train=False, transform=torchvision.transforms.ToTensor(), download=True)
data_loader = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, drop_last=True)

device = torch.device("cuda:0")

linear1 = torch.nn.Linear(784, 256, bias=True).to(device)
linear2 = torch.nn.Linear(256, 256, bias=True).to(device)
linear3 = torch.nn.Linear(256, 10, bias=True).to(device)
relu = torch.nn.ReLU()
model = torch.nn.Sequential(linear1, relu, linear2, relu, linear3).to(device)

loss = torch.nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

total_batch = len(data_loader)
training_epochs = 15

for epoch in range(training_epochs):
    total_cost = 0

    for X, Y in data_loader:
        X = X.view(-1, 28 * 28).to(device)
        Y = Y.to(device)
        
        hypothesis = model(X)
        cost = loss(hypothesis, Y)

        optimizer.zero_grad()
        cost.backward()
        optimizer.step()

        total_cost += cost 

    avg_cost = total_cost / total_batch
    print("Epoch:", "%03d" % (epoch+1), "cost =", "{:.9f}".format(avg_cost))

with torch.no_grad():
    X_test = mnist_test.data.view(-1, 28 * 28).float().to(device)
    Y_test = mnist_test.targets.to(device)
    prediction = model(X_test)
    correct_prediction = torch.argmax(prediction, 1) == Y_test
    accuracy = correct_prediction.float().mean()
    print("Accuracy: ", accuracy.item())

최적화 기법마다 학습률의 값은 다릅니다. 물론 에폭에 대한 반복수도 달라질 수 있습니다. 이러한 모델의 구성과 하이퍼 파라메터인 학습률과 반복 에폭수 등은 AI 전문가가 상황에 따라 결정해야 합니다. 결과적으로 위와 같은 모델의 확장과 최적화 방법의 변경 등을 통한 정확도는 약 97%로 출력되는데, 기존의 90%에서 대폭 향상된 것을 알 수 있습니다.

넥스젠(NexGen)의 통계지도 기능

NexGen은 통계 데이터를 지도 상에 시각화할 수 있는 강력한 기능을 제공합니다. 작성할 수 있는 통계지도에는 주제도와 차트맵이 있는데요. 아래는 주제도를 작성하는 기능에 대한 동영상입니다. 주제도는 통계값에 대한 하나의 특징을 각 지역별로 비교하는데 유용합니다. 아래의 동영상에서 소개되는 주제도에서는 여자수와 남자수를 합한 값을 특징으로 사용하였습니다.

또한 아래는 차트맵을 작성하는 동영상입니다. 차트맵은 통계값에 대해서 여러 개의 특징을 하나의 영역에 대해서 비교함과 동시에 여러개의 지역별로도 비교할 수 있는 방법입니다. 아래의 동영상에서 소개되는 차트맵은 여자수와 남자수를 각각의 특징으로 사용하고 있습니다.

위의 동영상에서 사용하는 통계 데이터 및 조금 더 자세한 내용은 아래의 글을 통해 살펴보실 수 있습니다.

지리정보시스템(GIS)를 활용한 통계지도