사람에 대한 Detection, Segmentation @A.I-TestBed

딥러닝 모델을 활용하여 어떤 이미지에서 사람의 위치를 찾아 내는 방식은 Detection과 Segmentation으로 분류할 수 있으며, Segmentation은 다시 Instance Segmentation과 Semantic Segmentation으로 나뉩니다.

Detection은 이미지에서 사람의 위치를 사각형 영역으로 잡아주는 방식이고 Segmentation은 이미지에서 사람에 해당하는 위치를 화소(Pixel)단위로 잡아줍니다. Segmentation 방식 중 Instance는 하나의 이미지에 여러명의 사람이 있다면 각 사람(각 Instance 별로)에 대해 분리해 픽셀 위치를 잡아주고, Semantic은 이미지에서 사람이라는 의미(Semantic)를 가지는 픽셀들을 잡아줍니다.

Segmentation 방식은 Detection 보다 학습과 메모리 소비가 훨씬 더 많이 소요되며, 훈련 데이터 중 레이블을 만들기가 훨씬 어렵습니다. Detection의 레이블은 사람에 해당하는 사각형 영역만을 지정하면 되지만, Segmentation의 레이블은 사람에 해당하는 픽셀을 모두 지정해줘야 하기 때문입니다.

Detection 방식의 모델은 매우 다양한데, 대표적으로 R-CNN 등이 있으며, Segmentation 방식의 모델에는 R-CNN을 통해 먼저 위치를 사각형 영역으로 잡고 다시 이 사각형 영역에 대해서 사람에 해당하는 픽셀을 잡는 Mask R-CNN이 있으며, 그냥 처음부터 이미지의 모든 픽셀에 대해 사람인지를 잡는 FCN 모델이 있습니다. Mask R-CNN 모델은 Instance Segmentation 방식이고 FCN 모델은 Semantic Segmentation 방식입니다. FCN 모델은 Semantic Segmentation 방식의 가장 기본이 되는 모델로 이 모델을 기본으로 Semantic Segmentation 방식을 더 개선한 다양한 모델이 파생되었습니다.

아래의 동영상은 머신러닝을 테스트하기 위한 TestBed 웹페이지로써 Detection과 Segmentation에 대한 기능을 보여줍니다.

참고로 위의 동영상에서 Detection과 Instance Segmentation의 결과에 대해 추론(hypothesis)값이 90% 이상으로 잡았습니다. 이 값을 좀더 내린다면 사람으로 잡지 못한 이미지의 부분에 대해서도 검출될 것입니다.

Accuracy, Precision, Recall

어떤 문제가 True, False인지를 예측할 때, 얼마나 정확하게 예측하는지의 척도로써 사용되는 3가지입니다. 먼저 Accuracy는 다음과 같습니다.

다음은 Precision입니다.

다음은 Recall입니다.

위의 공식에서 언급된 TP, FP, FN, FN은 다음과 같습니다. 정답이 참인지 거짓인지에 따라 T(True), F(False)로 표기하고 추정이 참인지 거짓인지에 따라 P(Positive), N(Negative)로 표기한 것입니다.

정리하면 Accuracy는 전체 예측에 대해 옳바르게 예측한 비율이고, Precision은 True라고 예측한 것 중에서 실제로 True인 것에 대한 비율입니다. Recall은 실제 True인 것 중에서 True라고 예측한 것에 대한 비율입니다.

감기 예측 모델을 예로 들면 Accuracy는 전체 예측에 대해 얼마나 올바르게 예측했는지에 대한 지표이고, Precision은 얼마나 정확하게 감기라고 예측하는지에 대한 지표이며, Recall은 실제 감기인 사람에 대해서 얼마나 정확하게 감기라고 예측하는지에 대한 지표입니다.

적은 수의 데이터를 이용한 머신러닝

이미지와 라벨 데이터 각각 50개를 이용한 머신러닝에 대한 실험입니다. Segmentation 방식이며 BOX 검출을 위한 모델은 Fast R-CNN을 사용했습니다. 하이퍼파라메터 중 에폭수 만을 변경하여 테스트했고 러닝이 완료되면 50개의 훈련 데이터 중에 하나로 시험했습니다. 시험 데이터와 훈련 데이터는 나누는게 일반적이나, 워낙 훈련 데이터가 소량인지라 시각적으로 시험 결과를 부각시키기 위함입니다. 글 중에는 주관적인 직관으로 작성된 내용도 포함하고 있습니다.

가장 먼저 학습을 전혀 하지 않은, 즉 에폭이 0인 경우에도 시험을 했습니다. 아래는 에폭이 0인 경우와 1인 경우에 대한 결과입니다.

Segmentation을 위한 모델은 Mask R-CNN을 이용했는데, 이 모델은 가장 먼저 특징점을 찾습니다. 찾을 특징점의 최대 개수는 100로 지정하였으므로, 학습이 전혀 되지 않아도 일단 100개를 찾게되었지만 검출 결과는 사람이 아닙니다. 하지만 특징점 추출은 이미지의 Gradient가 가장 심한 곳이 대부분일 것으므로 사람과 배경 경계, 건물의 창문 모서리 등과 같은 코너(Corner) 지점에서 검출이 되는 것으로 예상할 수 있습니다. 주목할 점은 검출 대상이 사람인지에 대한 최대 확률은 약 50%로 나왔다는 점입니다. 이는 전혀 학습되지 않을 경우 해당 픽셀이 사람인지 아닌지에 대한 결정은 단순 확률로써 50% 이기 때문입니다. 이제 학습을 1 에폭 수행했을 경우 픽셀 검출이 사람으로 특정되는 것을 볼 수 있습니다. 검출 대상 개수는 총 77개로 줄어들었지만 해당 검출 대상이 사람이라는 확률은 약 28%가 도출되었습니다. 또한 검출 픽셀이 대상의 중심으로 몰려있다는 점이 주목할만합니다. 에폭수를 늘리면 검출대상과 검출 대상이 사람인지에 대한 확률이 올라갈 것으로 기대하고 에폭 수를 3과 10을로 지정하여 학습을 시켰고, 그 결과는 다음과 같습니다.

검출 대상 결과는 좀더 사람의 형태에 확장되면서 접근하고 있습니다. 검출 대상도 에폭수에 비례하여 줄어들었구요. 그러나 검출 픽셀이 사람인지에 대한 확률은 3 에폭에서는 약 33%이고 10 에폭에서는 약 27%입니다. 오히려 더 줄어 들었습니다. 너무 적은 데이터만을 가지고 학습 한 결과라고 예상합니다. 확인을 위해 에폭을 20과 100으로 잡아 학습 시킨 결과는 아래와 같습니다.

20 에폭에서는 검출 개수가 더 줄었지만, 100 에폭에서는 오히려 검출 개수가 늘어났습니다. 또한 검출 대상이 사람인지에 대한 확률값이 각각 약 32%과 약 29%로 증가는 전혀 없다고, 아니 오히려 떨어졌습니다. 이 역시 훈련 데이터가 너무 적기 때문인 것으로 판단됩니다.

일단 소량의 데이터를 통해 학습 시킨 이유는 모델의 검증을 위해서입니다. 학습 각 단계마다 손실값이 줄어들고 있는지 등을 빠르게 확인하기 위해서인데.. 실제로 이 실험의 경우 에폭이 증가 할수록 손실값은 줄어드는 경향을 보였고 특정 에폭지점 이상부터는 손실값이 더 이상 감소하지 않는 것을 볼 수 있었습니다. 이런 손실값의 경향에 따라 모델은 어느 정도 접합하다라고 판단할 수 있고, 데이터를 더 추가하여 학습 시키면 더 나은 결과를 얻을 수 있을 것입니다. 데이터를 더 이상 추가할 수 없는 경우에는 하이퍼 파라메터를 변경하면서 정확도를 향상 시킬 수 있습니다.

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으로 학습하면 정확도를 더욱 향상시킬 수 있습니다.