SJ_Koding

02. [Dacon 교육] Fashion MNIST : 의류 클래스 예측 (csv파일)아주 쉽게 따라하기. (Pytorch 이용) 본문

AI Competition

02. [Dacon 교육] Fashion MNIST : 의류 클래스 예측 (csv파일)아주 쉽게 따라하기. (Pytorch 이용)

성지코딩 2022. 1. 14. 15:12

csv파일로 이루어진 fasion mnist 셋을 Pytorch를 이용하여 분류하는 방법을 알아보겠습니다. 

 

https://dacon.io/competitions/open/235594/overview/description

 

[이미지] Fashion MNIST : 의류 클래스 예측 - DACON

좋아요는 1분 내에 한 번만 클릭 할 수 있습니다.

dacon.io

데이터 셋은 이 dacon 대회 사이트를 통해 다운로드 가능합니다.

 

먼저 필요한 모듈을 import합니다.

import pandas as pd
import numpy as np
from torch.utils.data import Dataset, DataLoader
from torch import nn
from torchvision import transforms
import matplotlib.pyplot as plt
import torch

 

그 후 다운로드 받은 데이터 셋을 적절한 위치에 저장한 후 pd.read_csv() 을 통해 그 파일들을 가져옵니다.

저는 Google Colab환경에서 구글드라이브를 마운트해 파일을 불러왔습니다.

이 때, lndex_col = 'index' 는 'index'의 컬럼을 인덱스 번호로 지정한다는 의미입니다.

train데이터는 train, test데이터는 test라는 변수명을 사용합니다.

train = pd.read_csv('/content/drive/MyDrive/dacon/fasion_mnist_classifier/train.csv', index_col='index')
test = pd.read_csv('/content/drive/MyDrive/dacon/fasion_mnist_classifier/test.csv', index_col='index')

 

train셋의 상위 5개를 출력해봅니다.

train.shape

(60000, 785)
60000개의 데이터를 가지고 있는 것을 확인할 수 있습니다.

 

 

train셋의 상위 5개를 출력해봅니다.

train.head()

출력 결과를 보면, label데이터와 pixel1 부터 pixel 784 까지 존재하는 것을 확인할 수 있고. 5 row x 785 columns의 형태를 띕니다. 여기서 추측이 하나 가능한데, 784는 28의 제곱수입니다. 즉 이 이미지는 28 * 28의 사이즈를 가질 수 있다라고 추측이 가능합니다. 각 픽셀의 숫자들은 0~255의 값을 가지고있으며, 이는 한 픽셀당 밝기값을 의미합니다.

흑백 이미지이므로 RGB값은 존재하지 않습니다.

 

 

이미지 시각화 해보기
train셋의 0번째 컬럼은 인덱스 값이므로, 1번째 부터 픽셀값을 읽어와 출력해줄겁니다. 그러기 위해서 plt의 imshow함수를 사용할 것이며 이미지를 28 * 28로 변환해주어야합니다. dataFrame의 인덱스로 값을 접근할 경우 iloc을 사용하면 됩니다. shape을 변환해주기 위해서 darray형태로 잠시 변환해줍니다. 
plt.imshow(np.array(train.iloc[2, 1:]).reshape(28, 28), cmap = 'gray')

2번째 데이터에 있는 이미지 출력

 

Pytorch의 Dataset을 상속받아 나만의 데이터셋 인스턴스를 생성해주는 클래스를 구현합니다. 

__len__() 에서는 학습 데이터의 개수를 리턴해주고, __getitem__()은 앞서 만든 리스트의 인덱스 값을 참조해 데이터에 관한 여러 일처리를 진행한 후에 그 결과를 반환합니다. 아래의 코드에서는 __init__()에서 인자로 가져온 transform을 적용시킨 데이터를 반환하게됩니다.

class train_Dataset(Dataset):
    def __init__(self, data, transform = None):
        self.fashion_mnist = list(data.values)
        self.transform = transform
        label, img = [], []

        for one_line in self.fashion_mnist:
            label.append(one_line[0])
            img.append(one_line[1:])

        self.label = np.array(label)
        self.img = np.array(img).reshape(-1, IMAGE_SIZE, IMAGE_SIZE, CHANNEL).astype('float32')

    def __len__(self):
        return len(self.label)

    def __getitem__(self, idx):
        label, img = self.label[idx], self.img[idx]
        if self.transform:
            img = self.transform(img)
        
        return label, img

 

 

사용자 정의의 test_dataset클래스를 만들어줍니다. 여기서는 train_dataset과 차이가 있는데, 먼저 test셋에는 label이 존재하지 않기 때문에 append시 슬라이싱 범위를 바꿔주어야 하며 반환값이 기존 label, img에서 img하나만 반환하게 됩니다.

class test_Dataset(Dataset):
    def __init__(self, data, transform = None):
        self.fashion_mnist = list(data.values)
        self.transform = transform
        img = []

        for one_line in self.fashion_mnist:
            img.append(one_line[:])

        self.img = np.array(img).reshape(-1, IMAGE_SIZE, IMAGE_SIZE, CHANNEL).astype('float32')

    def __len__(self):
        return len(self.fashion_mnist)

    def __getitem__(self, idx):
        img = self.img[idx]
        
        if self.transform:
            img = self.transform(img)
        
        return img

 

하이퍼 파라메타를 설정합니다.

BATCH_SIZE = 25
LR = 5e-3
NUM_CLASS = 10
IMAGE_SIZE = 28
CHANNEL = 1
Train_epoch = 30

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu'

 

사용자 transform을 구현합니다. 여기서는 ToTensor() 하나만 적용시켜줄겁니다. ToTensor()는 값을 Tensor형으로 변환해주며 값을 0.0에서 1.0으로 스케일링 해줍니다.
그 후 위에서 구현한 train_Dataset, test_Dataset을 통해 값을 가져오고, 이를 Pytorch의 DataLoader함수에 넣어줍니다.

DataLoader함수는 데이터를 미니배치로 처리해주고, GPU를 통한 병렬처리, 학습효율의 향상효과를 가질 수 있습니다.

My_transform = transforms.Compose([
    transforms.ToTensor(), # default : range [0, 255] -> [0.0, 1.0]
])

Train_data = train_Dataset(train, transform=My_transform)
Test_data = test_Dataset(test, transform=My_transform)

Train_dataloader = DataLoader(dataset=Train_data,
                              batch_size = BATCH_SIZE,
                              shuffle=False)

Test_dataloader = DataLoader(dataset=Test_data,
                             batch_size = 1,
                             shuffle=False)

 

 
 
사용자 모델을 정의해줍니다. 이를 위해 nn.Module을 상속받아주어야 합니다.  
아래 코드를 보면 총 3개의 레이어와 마지막 fc레이어로 총 4개의 레이어로 구성되어있습니다.  
layer들은 Conv2d, BatchNorm, ReLU, POOL의 집합체로 이루어져 있습니다.
Conv2d는 이미지의 feature(특징)을 뽑아내는 역할을 수행하며
BatchNorm은 정규화의 효과를 가지면서 특정 분포에 쏠리지 않게 방지하는 역할을 수행하고
ReLU는 비선형성을 위한 활성화함수이며
POOL은 이미지의 사이즈를 줄이기 위한 과정입니다.
마지막 FC의 경우 레이어들을 모두 통과한 데이터를 일자로 펴주고, 이를 분류할 클래스의 개수에 맞게 바꿔주는 연산을 수행합니다. 이 기능은 nn.Linear가 수행해줍니다.
그 후 forward에서 레이어를 통과해주는 작업을 수행하고 이 결과를 반환합니다.

 

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

        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1), # 28 * 28 * 16
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)) # 14 * 14 * 16
        
        self.layer2 = nn.Sequential(
            nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1), # 14 * 14 * 32
            nn.BatchNorm2d(32),
            nn.ReLU()
            # nn.MaxPool2d(kernel_size=2, stride=2)) 7 * 7 * 32
        )

        self.layer3 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )  # 7 * 7 * 64
        
        self.fc = nn.Linear(7 * 7 * 64, num_of_class)

    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = self.layer3(out)
        out = out.reshape(out.size(0), -1)
        out = self.fc(out)
        return out

 

 

 

이제 짜여진 모델을 통해 모델 학습을 진행하면 됩니다.

Adam optimizer를 사용하고 CELoss를 통해 학습을 진행합니다.

모델의 학습 순서

1. 모델에 데이터 넣기 output = model(image)

2. 로스 구하기 loss= criterion(output, label).

3. 옵티마이저 기울기 초기화하기

 이상적으로 학습이 이루어지기 위해선 한번의 학습이 완료되어지면(즉, Iteration이 한번 끝나면) gradients를 항상 0으로 만들어 주어야 합니다. 만약 gradients를 0으로 초기화해주지 않으면 gradient가 의도한 방향이랑 다른 방향을 가르켜 학습이 원하는 방향으로 이루어 지지 않습니다.
출처: 
https://algopoolja.tistory.com/55

4. 역전파 진행해주기

5. 최적화 과정 진행하기

 

여기서 최적의 모델을 얻기위해서 로스가 가장 낮은값이 나오는 경우 torch.save()를 통해 모델을 저장하고

추후 저장된 모델을 사용하여 예측을 진행하였습니다.

def train():
    model = My_model(NUM_CLASS).to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr = LR)
    criterion = nn.CrossEntropyLoss()
    min_Loss = 100000
    for epoch in range(1, Train_epoch + 1):
        for batch_id, (label, image) in enumerate(Train_dataloader):
            label, image = label.to(device), image.to(device)
            output = model(image)
            loss = criterion(output, label)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            if batch_id % 1000 == 0:
                print('Loss :{:.4f} Epoch[{}/{}]'.format(loss.item(), epoch, Train_epoch))
                if loss.item() < min_Loss:
                    torch.save(model, '/content/drive/MyDrive/dacon/fasion_mnist_classifier/best_model.pt')
                    min_Loss = loss.item()
                    print('Model save!! found minimum loss')
    return model

 

아래는 학습된 모델을 통해 예측값을 반환하는 함수입니다. flatten을 해준 이유는 submission파일에 값을 대입하기 위함입니다. flatten은 다차원 데이터를 1차원으로 펴주는 역할을 수행합니다.

def test(model):
    model = torch.load('/content/drive/MyDrive/dacon/fasion_mnist_classifier/best_model.pt') # 모델 불러오기
    print('success load best_model')
    pred = []
    with torch.no_grad():
        correct = 0
        total = 0
        for image in Test_dataloader:
            image = image.to(device)

            outputs = model(image)

            predicted = np.array(torch.argmax(outputs, dim=1).cpu())
            pred.append(predicted)
    
    print(np.array(pred).flatten().shape)
    return np.array(pred).flatten()

그리고 실행합니다.

if __name__ == '__main__':
    model = train()
    pred = test(model)

최소로스 0.0006일때의 모델을 저장하였습니다.

학습이 정상적으로 완료되었습니다. 이제 submission파일에 값을 넣고 제출하면 됩니다.

submission = pd.read_csv('/content/drive/MyDrive/dacon/fasion_mnist_classifier/sample_submission.csv')
submission['label'] = pred
submission

submission.to_csv('/content/drive/MyDrive/dacon/fasion_mnist_classifier/submission.csv', index=False)

정확도는 0.9119가 나왔고 97위를 기록했습니다. 성능향상보다는 기본적으로 제출하기 위한 연습이었습니다.

 

감사합니다.

-sjkoding-