※ 본 게시물에 사용된 내용의 출처는 대다수 <펭귄브로의 3분 딥러닝-파이토치맛>에서 사용된 자료이며, 개인적인 의견과 해석이 추가된 부분도 존재합니다. 그림의 경우 교재를 따라 그리거나, 제 임의대로 추가 수정한 부분도 존재합니다. 저 역시도 공부하고자 포스팅한 게시물이니, 잘못된 부분은 댓글로 알려주시면 감사하겠습니다.
- Fashion MNIST
- Deep Neural Network
Fashion MNIST 데이터셋 분석해보기
- 28×28 픽셀의 흑백 이미지
- 70,000장 (train set : 60,000 / test set : 10,000) 으로 구성
- 총 10개의 클래스(의류,가방)등의 이미지를 포함한 데이터셋
- 이미 가공되어 있는 데이터셋이기 때문에 단순히 Torch로 다운받아 사용할 수 있다.
이번 챕터에서는 FashionMNIST를 사용하여, 아래와 같은 DNN 분류 네트워크를 설계하고자 한다.
다만 네트워크 설계에 앞서 MNIST 데이터셋 로딩 및 실제 데이터셋 샘플을 열어보고 분석해보자
- 사용코드
코드 실행에 필요한 라이브러리들을 임포트해준다.
from torchvision import datasets, transforms, utils
from torch.utils import data
# 학습 데이터를 컨트롤 하기 위한 기본 모듈 임포트
import matplotlib.pyplot as plt
import numpy as np
# matplotlib : 시각화 / numpy : 행렬, 다차원 배열 컨트롤 라이브러리
- torch.utils.data : 데이터셋 표준을 정의하고, 데이터셋 로딩, 자르기, 섞기 등 다양한 툴이 정의된 모듈.
파이토치 모델을 학습하기 위해 데이터셋 표준을 torch.utils.data.Dataset(데이터셋을 나타내는 추상 클래스)에 정의해놓고, Dataset모듈을 따로 상속하는 파생 클래스를 정의하여, torch.utils.data.DataLoader(데이터를 묶고, 섞고, 병렬처리하며 동시에 데이터를 일정 크기만큼 불러올수 있게끔하는 반복자)의 인스턴스 입력으로 사용 가능.
- torchvision.datasets : torch.utils.data.Dataset을 상속하는 이미지 데이터셋 모음으로, 다양한 데이터셋(CIFAR, COCO, Flickr, MNIST, ImageNet, UCF101...)을 기본적으로 포함
- torchvision.transforms : 이미지 데이터셋에 쓸 수 있는 여러 변환 필터를 담은 모듈
ex) 텐서로 변환, 크기 조절(resize), 크롭(crop), 밝기 조절(brightness), 대비(contrast) 등을 조절하는데 사용.
- torchvision.utils : 이미지 데이터 저장, 시각화를 위한 툴을 포함한 모듈
# 이미지를 텐서로 변경
transform = transforms.Compose([
transforms.ToTensor()
])
torchvision의 transform안에는 앞서 말한 Resize(), Normalize()와 같은 데이터 변환 함수를 포함하고 있다.
trainset = datasets.FashionMNIST(
root = './.data/',
train = True,
download = True,
transform = transform
)
testset = datasets.FashionMNIST(
root = './.data/',
train = False,
download = True,
transform = transform
)
따로 내려받을 필요도 없고 단순히 download 플래그를 True로 변경해주면 현재 root위치를 기준으로 해당 데이터셋 유무를 확인 후, 자동으로 저장한다.
batch_size = 16
# 데이터 로딩(공급) 객체 선언
train_loader = data.DataLoader(
dataset = trainset,
batch_size = batch_size
)
test_loader = data.DataLoader(
dataset = testset,
batch_size = batch_size
)
그후 다음과 같이 DataLoader 객체를 선언하여, 데이터 로딩(공급) 객체를 정의해준다.
# 반복문에서 사용할 수 있도록 iter()함수 적용
dataiter = iter(train_loader)
# next()함수로 다음 배치 1개 갖고오기
images, labels = next(dataiter)
DataLoader가 준비됐으니, 배치 1개만 뽑아 실제 데이터 샘플을 뽑아보도록 하자. iter()함수를 적용하여 반복문 안에서 이용할 수 있는 dataiter 객체를 만들고, next()함수로 배치 1개를 가져올 수 있도록 한다. 배치는 앞에서 batch_size = 16으로 정의했으므로, 1개의 배치 안엔 16개의 이미지, 레이블이 포함되게 된다.
# 데이터 체크
# utils.make_grid : 여러 이미지를 하나로 묶어보기 위함
img = utils.make_grid(images, padding=0)
npimg = img.numpy()
plt.figure(figsize=(10, 7))
plt.imshow(np.transpose(npimg, (1,2,0)))
plt.show()
다음 코드를 사용하여, 실제 16개의 이미지를 하나로 묶어 샘플 이미지를 뽑아보면 다음과 같다.
print(labels)
CLASSES = {
0: 'T-shirt/top',
1: 'Trouser',
2: 'Pullover',
3: 'Dress',
4: 'Coat',
5: 'Sandal',
6: 'Shirt',
7: 'Sneaker',
8: 'Bag',
9: 'Ankle boot'
}
for label in labels:
index = label.item()
print(CLASSES[index])
실제 라벨도 찍어보면 다음과 같이 상단 이미지의 클래스 라벨을 찍어볼 수 있다.
idx = 1
item_img = images[idx]
item_npimg = item_img.squeeze().numpy()
plt.title(CLASSES[labels[idx].item()])
print(item_npimg.shape)
plt.imshow(item_npimg, cmap='gray')
plt.show()
이미지를 하나 찍어보면 다음과 같으며, 이 이미지는 가로(28) x 세로(28) x 흑백(1)사이즈의 특징값 벡터를 갖고 있다.
인공신경망으로 분류 네트워크 구현해보기
- 사용코드
앞에서 언급했던 것처럼 DNN을 설계하여 주어진 이미지의 클래스 라벨을 예측하는 모델을 구현한다.
우선, 모델 설계에 앞서 필요한 라이브러리 및 환경 셋팅을 진행한다.
import torch
# DNN 모델의 재료라 할 수 있는 nn 모듈
import torch.nn as nn
# optimizer를 위한 optim 모듈
import torch.optim as optim
# nn 모듈의 함수 버전 functional 모듈
import torch.nn.functional as F
# torchvision의 데이터셋인 MNIST를 사용하려 아래 라이브러리 임포트
from torchvision import transforms, datasets
# 현재 컴퓨터에서 CUDA(GPU Library)를 이용할 수 있는지 알아보기 위한 Flag
USE_CUDA = torch.cuda.is_available()
# USE_CUDA가 True일 경우, GPU를 할당 그 외엔 CPU 메모리 할당
DEVICE = torch.device("cuda" if USE_CUDA else "cpu")
# Epoch과 Batch_size 선언
EPOCHS = 30
BATCH_SIZE = 64
이후, 아까와 같은 방식으로 데이터셋을 다운받고, DataLoader까지 선언해준다.
transform = transforms.Compose([
transforms.ToTensor()
])
trainset = datasets.FashionMNIST(
root = './.data/',
train = True,
download = True,
transform = transform
)
testset = datasets.FashionMNIST(
root = './.data/',
train = False,
download = True,
transform = transform
)
train_loader = torch.utils.data.DataLoader(
dataset = trainset,
batch_size = BATCH_SIZE,
shuffle = True,
)
test_loader = torch.utils.data.DataLoader(
dataset = testset,
batch_size = BATCH_SIZE,
shuffle = True,
)
본격적인 인공 신경망 설계를 진행한다.
인공 신경망은 아래 그림과 같이 크게 3개의 네트워크로 구성된 신경망으로 각 layer를 nn.Linear를 사용해 생성자(self.fcN)마다 선언해주도록 한다. 자세한 코드는 아래 Net클래스의 Init함수를 참고해보자.
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(784, 256)
self.fc2 = nn.Linear(256, 128)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
# input data(image) = x
# x.shape = torch.Size([64, 1, 28, 28])
x = x.view(-1, 784)
# x.shape = torch.Size([64, 784])
x = F.relu(self.fc1(x))
# x.shape = torch.Size([64, 256])
x = F.relu(self.fc2(x))
# x.shape = torch.Size([64, 128])
x = self.fc3(x)
# x.shape = torch.Size([64, 10])
return x
네트워크는 총 3개의 Fully Connected Layer로 구성되어 있으며, fc1, fc2는 ReLU activation function까지 포함하는 형태로 구성되어 있다. 맨 마지막 fc3 layer가 최종적으로 10개의 각 클래스에 대한 값을 출력하고, 이 중 가장 큰 값이 모델의 최종 예측값이 된다고 볼 수 있다.
fc1과 fc2의 역할은 입력 데이터를 받아 각 레이어마다 weight를 행렬곱하고 bias를 더하는 일반적인 Dense layer의 선형 결합 연산을 수행하기 때문에 역전파 과정에서 발생할 수 있는 vanishing gradient 문제를 해결하기 위해 다음과 같이 ReLU 활성화 함수를 적용하게 된다.
실제 데이터 흐름은 forward 함수에 정의되어 있는데, 맨 처음 입력되는 1장의 이미지에 대한 데이터로는 [색, 높이, 너비]=([1, 28, 28]) 사이즈의 특징 벡터를 입력받게 되며, 두번째 라인의 view() 함수를 통해 1차원으로 변경해주게 된다. 이는 Linear Layer인 fc1을 거치기 위함이며, 이때 앞에서 batch size를 64개를 설정해놓았으므로, 앞에서 정의한 DataLoader에 의해서 64개의 이미지 데이터를 한꺼번에 묶어서 입력받게 된다. (실제로 pdb(python debugger)를 사용하여 데이터의 크기를 체크해보면 ([64, 1, 28, 28])로 정의되어 있는 것을 확인할 수 있다)
이를 fc1과 fc2를 거치면서 가중치 연산을 진행하고 마지막 fc3(x)이라는 레이어를 통과함으로써 최종적으로 ([64, 10]) 각 64개의 batch 마다 10개씩 클래스에 대한 예측값을 추출하는 것을 확인할 수 있다.
모델 설계가 끝난 후에는 실제 코드를 수행하기 위해 모델을 선언해보도록 한다. 이때, 아래 코드와 같이 to함수로 GPU로 학습시키기 위해 GPU 메모리로 보내주도록 한다. 이 과정에서 앞서 정의했던 DEVICE변수를 끌어다 사용하도록 한다.
model = Net().to(DEVICE)
optimizer = optim.SGD(model.parameters(), lr=0.01)
모델의 최적화 알고리즘은 pytorch에 내장되어 있는 SGD(Stochastic Gradient Descent)를 사용한다.
이제, 본격적인 학습 함수를 정의하면 아래와 같다.
def train(model, train_loader, optimizer):
# 모델을 학습 모드로 전환
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
# 학습 데이터를 DEVICE의 메모리로 보냄
data, target = data.to(DEVICE), target.to(DEVICE)
# 매 반복(iteration)마다 기울기를 계산하기 위해 zero_grad() 호출
optimizer.zero_grad()
#실제 모델의 예측값(output) 받아오기
output = model(data)
# 정답 데이터와의 Cross Entropy Loss 계산
# loss는 mini batch인 64개의 클래스의 오차 평균값
loss = F.cross_entropy(output, target)
# 기울기 계산
loss.backward()
# 가중치 수정
optimizer.step()
이 때, Loss함수의 경우 일전엔 2개의 클래스라서 binary cross entropy 를 사용하였나, 이번엔 클래스가 10개이니깐 cross entropy 사용하도록 한다.
Epoch만큼 돌고 나면 적당히 학습된 모델을 얻게 되며, 이때 학습이 완료된 모델을 다음 함수를 사용하여 테스트해보도록 하자
def evaluate(model, test_loader):
# 모델을 평가 모드로 전환
model.eval()
# 필요한 변수 초기화
# Test과정에서의 Loss = test_loss
# 실제 모델의 예측이 정답과 맞은 횟수 = correct
test_loss = 0
correct = 0
with torch.no_grad(): # 평가 과정에서는 기울기를 계산하지 않으므로, no_grad명시
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)과 정답 클래스를 비교하여 일치할 경우 correct에 1을 더합니다.
pred = output.max(1, keepdim=True)[1]
# eq() 함수는 값이 일치하면 1을, 아니면 0을 출력.
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
최종적으로 아래 코드로 실제 훈련 및 테스트를 진행하면 다음과 같은 결과를 얻을 수 있다.
for epoch in range(1, EPOCHS + 1):
train(model, train_loader, optimizer)
test_loss, test_accuracy = evaluate(model, test_loader)
print('[{}] Test Loss: {:.4f}, Accuracy: {:.2f}%'.format(
epoch, test_loss, test_accuracy))
최종적으로 단순 FC 3개의 Layer만으로도 86.23%의 성능을 보이는 모델을 만들 수 있었다.