본문 바로가기

[Project] Fashion MNIST Classifier (패션 아이템 분류기)

Derrick 발행일 : 2022-06-04
728x90
반응형

 

※ 학습 프로젝트 목표
: Fashion MNIST Dataset을 활용하여 분류기(classifier) 학습하고, 성능 검사!
→ 28 x 28 픽셀 70,000개의 흑백 이미지로, 신발, 드레스 등 10개의 카테고리(class)가 존재한다.
→ 패션 이미지 데이터를 입력으로 주면 어떤 이미지인지 예측하여 반환되는 모델 설계
# 개요
 → Multi layer perceptron, Batch normalization, ReLU를 사용하여 Neural Network 설계
 → 성능도(Accuracy) 개선 - Augmentation, Dropout 등 적용

 


 

1. Package Load (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

# cuda 사용 설정 및 가능 여부 확인
device = "cuda" if torch.cuda.is_available() else "cpu"
print('GPU 사용 가능 여부: {}'.format(torch.cuda.is_available()))
'''GPU 사용 가능 여부 : True'''

# Random seed 설정
torch.manual_seed(1)
if device == 'cuda':
  torch.cuda.manual_seed_all(1)

# Google Drive 연동
from google.colab import drive
drive.mount('/content/gdrive')

 

- torchvision.datasets
 : 패션 아이템 Dataset이 들어있다. (torch.utils.data.Dataset을 상속한 이미지 데이터셋
- torchvision.transforms
 : 이미지에 쓸 수 있는 다양한 변환 필터를 포함하고 있는 모듈 (텐서 변환, Resize, Crop 등)
- matplotlib.pyplot : 데이터 시각화를 위해 사용

 

2. 하이퍼파라미터(Hyperparameter) 설정

batch_size = 100                    # mini-batch의 크기
num_epochs = 5                      # 학습할 epoch의 수
learning_rate = 0.001               # 학습율

 

- 하이퍼파라미터는 Neural Network를 통해 학습되는 것이 아니라 설계자가 직접 결정하는 값
- batch_size는 한번에 몇개씩 데이터를 읽을 것인가를 의미한다.
 → DataLoader에 데이터셋을 넣고 batch_size를 지정해주면 그만큼 데이터를 읽는다.
 ex) size = 100 -> 1 epoch 마다 100개의 이미지 데이터를 읽을 것이다.

 

3. Dataset과 DataLoader 할당

 torchvision에서 제공하는 Fashion MNIST datasete을 정의하고, 전체 dataset을 mini-batch 단위로 쪼개어 neural network에서 학습 반복문 안에 데이터를 공급하도록 DataLoader를 정의한다.

 

from torch.utils.data import DataLoader

transform = transforms.Compose([transforms.ToTensor()])            # Tensor로 변환

train_data = dset.FashionMNIST(root='./data', train=True, transform=transform, download=True)
test_data = dset.FashionMNIST(root='./data', train=False, transform=transform, download=True)
# root = FashionMNIST 데이터셋이 위치하는 경로

# 모델에게 전달할 데이터 공급 코드 - DataLoader
train_loader = DataLoader(
    dataset=train_data,
    batch_size=batch_size,      # batch_size만큼 잘라서 Load
    shuffle=True,
    drop_last=True              # 마지막 Data가 batch_size만큼 안되면 버려(True)
)

test_loader = DataLoader(
    dataset=test_data,
    batch_size=batch_size,
    shuffle=False,
    drop_last=True
)

 

- FashionMNIST 데이터셋을 가져오기 위해 torchvision의 datasets 패키지를 통해 다운!
- torch.utils.data : Dataset의 표준을 정의하고 Dataset을 불러오고 자르는데 사용한다.
- transforms.Compose() : 여러 전처리 함수들을 하나로 묶어서 사용할 수 있다.

 

4. 샘플 데이터 시각화 - 테스트용

- 로드한 이미지 데이터를 테스트 용도로 출력해보자!
- 각 class의 이름과 인덱스를 Dictionary 형태로 저장하고, for문을 이용하여 train_data 중 무작위로 25개 추출 후 (5 x 5) 형태로 출력!

 

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

# 출력 이미지 사이즈 설정
columns = 5
rows = 5
fig = plt.figure(figsize=(8,8))

# 데이터를 불러서 numpy 배열에 넣어주고 imshow하면 시각화 가능
for i in range(1, columns*rows+1):
    data_idx = np.random.randint(len(train_data))
    img = train_data[data_idx][0][0,:,:].numpy()      # numpy()를 통해 torch Tensor를 numpy array로 변환
    label = labels_map[train_data[data_idx][1]]       # item()을 통해 torch Tensor를 숫자로 변환

    fig.add_subplot(rows, columns, i)
    plt.title(label)
    plt.imshow(img, cmap='gray')
    plt.axis('off')
plt.show()


>>

null

 

- img(이미지)는 파이토치 Tensor형태이기 때문에, numpy() 함수를 사용하여 matplotlib과 호환되는 numpy 배열로 변환해야 한다. + 이미지 데이터 = (가로, 세로, 색상), 흑백 = 1로 표현
- plt.figure()를 통해 이미지를 출력할 도화지 생성 - matplotlib
- add_subplot() : 생성한 figure 객체에 대해 도화지 속에 일부 이미지를 삽입 가능하도록 한다.
 → 즉, 큰 도화지 위에 작은 이미지를 구역마다 출력할 수 있도록 한다.

 

5. DNN 모델 설계 ***

class DNN(nn.Module):
    # 초기화
    def __init__(self, num_classes=10): 
        # super(부모님) 꼭 호출! (DNN(자기자신) 넣어서)
        super(DNN, self).__init__()

        # 시퀀스를 하나의 레이어로 묶을 수 있다
        self.layer1 = nn.Sequential(            
            # layer_1
            nn.Linear(28*28, 512),
            nn.BatchNorm1d(512),        # BatchNorm 적용
            nn.ReLU()                   # ReLU
        )

        self.layer2 = nn.Sequential(
            # layer_2
            nn.Linear(512, 256),
            nn.BatchNorm1d(256),        # BatchNorm 적용
            nn.ReLU()                   # ReLU
        )
        self.layer3 = nn.Sequential(
            # layer_3
            nn.Linear(256,10)            # 마지막 class는 10
        )

    # x값 받아서 forward
    def forward(self, x):
        x = x.view(x.size(0), -1)     # flatten (데이터를 펼쳐서 집어넣는 애)
        out = self.layer1(x)
        out = self.layer2(out)
        out = self.layer3(out)
        return out

 

- 3 layer를 거치면서 입력 feature의 수(= 입력 이미지의 픽셀의 수, 28x28)에서 최종 출력은 10개.
- Classification 네트워크의 마지막 activation function은 주로 softmax가 적용되기 때문에, 마지막을 제외한 모든 layer에 ReLU, batchnorm을 적용시킨다.
- CrossEntropy Loss function에 softmax function 포함되기 때문에, 별도 선언은 불필요하다

 

6. Weight initialization 정의 및 모델 선언

- Network 내 weight를 Xavier_normal를 통해 초기화
- model.apply( )를 통해서 가중치 초기화를 적용할 수 있다.

 

# Weigh 초기화 함수 선언
def weights_init(m):
    if isinstance(m, nn.Linear): # 모델의 모든 MLP 레이어에 대해서

        # Weight를 xavier_normal로 초기화
        nn.init.xavier_normal_(m.weight)  # 언더바_ 가 맨 뒤에 붙으면 "실제 그 값을 바꾼다"라는 뜻

# 앞서 생성한 DNN 함수 호출
model = DNN().to(device)

# 모델에 weight_init 함수를 적용하여 weight를 초기화
model.apply(weights_init) 

 

>>
DNN(
  (layer1): Sequential(
    (0): Linear(in_features=784, out_features=512, bias=True)
    (1): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU()
  )
  (layer2): Sequential(
    (0): Linear(in_features=512, out_features=256, bias=True)
    (1): BatchNorm1d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU()
  )
  (layer3): Sequential(
    (0): Linear(in_features=256, out_features=10, bias=True)
  )
)

 

7. Loss function과 Optimizer 정의

- 생성한 모델 학습을 위해 손실함수(loss funcion)을 정의
- Neural Network는 경사하강(Gradient descent) 방법을 이용하여 손실함수의 값을 줄이는 방향으로 parameter를 update 하게 된다. -> 이 때, Optimizer를 사용한다.

 

# 손실함수 정의
criterion = nn.CrossEntropyLoss()

# 손실함수의 최소값을 찾아주는 알고리즘 Adam
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

 

8. Training (학습)

데이터를 공급해주는 train_loader가 for문을 통해 mini-batch만큼 데이터를 가져오면 위에 정의한 model에게 전달하고, 출력값을 loss function을 통해 손실값(loss값)을 얻어낸다.
 → 모델은 이 loss값이 적어지는 방향으로 parameter를 업데이트하며, 이 수행은 optimizer가 한다.

 

for epoch in range(num_epochs):

    # 모델 train mode
    model.train()

    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()             # 이전에 계산된 gradient 모두 초기화
        loss.backward()                    # 가중치 계산
        optimizer.step()                 # optimizer를 통해 parameter 업그레이드

        _, argmax = torch.max(outputs, 1)
        accuracy = (labels == argmax).float().mean()

        if (i+1) % 100 == 0:
            print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}, Accuracy: {:.2f}%'.format(
                epoch+1, num_epochs, i+1, len(train_loader), loss.item(), accuracy.item() * 100))

 

- outputs = model(imgs) : model에 imgs 데이터를 주고, 그 출력을 outputs 변수에 저장
- loss = criterion(outputs, labels)
 : 모델의 outputs과 train_loader에서 제공된 실제값 labels를 통해, 그 차이로 손실값(loss)를 구하고 그 결과를 loss 변수에 저장한다.

 

>>
Epoch [1/5], Step [100/600], Loss: 0.4120, Accuracy: 85.00%
Epoch [1/5], Step [200/600], Loss: 0.4327, Accuracy: 85.00%
Epoch [1/5], Step [300/600], Loss: 0.3966, Accuracy: 86.00%
...
Epoch [5/5], Step [400/600], Loss: 0.3932, Accuracy: 83.00%
Epoch [5/5], Step [500/600], Loss: 0.3408, Accuracy: 88.00%
Epoch [5/5], Step [600/600], Loss: 0.3412, Accuracy: 88.00%

 

9. Test (테스트 및 성능도 체크)

- model.eval( ) - 모델을 평가(evaluation) 모드로 설정
: Batch norm과 dropout이 training을 수행할 때와 test의 경우 작동되는 방식이 서로 다르기 때문에 eval mode를 꼭 설정해야 한다. (test시, 일관된 결과를 얻기 위해)
 
- torch.no_grad( ) - torch.Tensor의 requires_grad를 False!
 : test할 때, backpropagation으로 gradient를 계산할 필요가 없기 때문에 'requires_grad=False'으로 바꿔줌으로써 메모리 낭비하는 것을 방지할 수 있다.

 

# 모델 eval mode
model.eval()
test_loss = 0

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에 삽입
        outputs = model(imgs).to(device)

        # loss율 계산
        loss = criterion(outputs, labels)

        # test_loss 업데이트
        test_loss += loss.item()*imgs.size(0)

        # max()를 통해 최종 출력이 가장 높은 class 선택
        # 예측과 정답을 비교하여 일치할 경우, correct에 1을 더한다
        _, argmax = torch.max(outputs, 1)
        total += imgs.size(0)
        correct += (labels == argmax).sum().item()

    # test loss율 평균 계산
    test_loss = test_loss/len(test_loader.dataset)
    print('Test Loss : {:.4f}'.format(test_loss))

    print(
    'Test accuracy of the network on the {} test images: {:.2f}%'.format(total, correct / total * 100))

 

>>
Test Loss : 0.3500
Test accuracy of the network on the 10000 test images: 87.64%

 

→ 최종 성능이 대략 88% 전후로 나온다면 학습이 잘 된 것으로 해석할 수 있다.

 

10. 성능도(Accuracy) 향상!

→ Dropout, Data Augmentation을 적용 후, 모델 성능도 향상되는 것을 확인해보자

 

10-1. Data Augmentation 적용

transform = transforms.Compose(
    [transforms.ToTensor(),                         # Tensor로 변환
     transforms.RandomRotation(30),                 # 랜덤 각도로 회전
     transforms.RandomHorizontalFlip(),             # 랜덤으로 수평 뒤집기
     transforms.Normalize(mean=[0.5,], std=[0.5,])    # RGB의 모든 픽셀 정규화
     ])

 

10-2. Dropout 적용 (Overfitting 방지)

self.layer1 = nn.Sequential(            
    # layer_1
    nn.Linear(28*28, 512),
    nn.BatchNorm1d(512),        # BatchNorm 적용
    nn.ReLU(),                  # ReLU
    nn.Dropout(0.25)            # Dropout 적용(0.25 비율)
)

self.layer2 = nn.Sequential(
    # layer_2
    nn.Linear(512, 256),
    nn.BatchNorm1d(256),        # BatchNorm 적용
    nn.ReLU(),                  # ReLU
    nn.Dropout(0.25)            # Dropout 적용
        )

 

10-3. Training과 Test 후 Loss값과 Accuracy 확인

>>
# Training part
Epoch [1/50], Step [100/600], Loss: 0.6822, Accuracy: 70.00%
Epoch [1/50], Step [200/600], Loss: 0.6680, Accuracy: 75.00%
Epoch [1/50], Step [300/600], Loss: 0.6136, Accuracy: 80.00%
...
Epoch [50/50], Step [400/600], Loss: 0.4216, Accuracy: 85.00%
Epoch [50/50], Step [500/600], Loss: 0.3225, Accuracy: 87.00%
Epoch [50/50], Step [600/600], Loss: 0.1827, Accuracy: 94.00%

# Test part
Test Loss : 0.1682
Test accuracy of the network on the 10000 test images: 95.28%

 

- epoch이 증가할수록 점차 Loss값은 감소하고 Accuracy는 증가하는 것을 확인할 수 있다.
- 모델에 Augmentation과 Dropout을 적용 후 높은 성능에 도달하는데, 어느 정도 시간이 소요됨으로, epoch을 더 늘리는 것이 좋다. (위의 실습은 epoch = 50)



[ 참고 : 파이토치로 시작하는 딥러닝 기초(boostcourse), 모두의딥러닝 ]

 

 

댓글