ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [기계학습]합성곱 신경망(CNN : Convolutional Nerual Network) (Part 2/2)
    코딩(Coding)/기계학습 2022. 2. 18. 12:03
    728x90

    합성곱 신경망(CNN :Convolutional Nerual Network) (Part 2/3)

    • 기존의 Fully-Connected 모델은 1차원의 데이터 말고 2차원 이상의 데이터를 사용하게 된다면, 해당 입력 데이터를 Flatten시켜 한 줄의 데이터로 만들어야 한다.
    • 이 과정에서 데이터의 손상이 발생하게 된다.
    • 이미지의 경우에는 상하좌우 이웃 픽셀의 정보가 손실된다.
    • 위 문제를 해결하기 위해 고안한 해결책이 바로 CNN이다.

    CNN 장점

    • 단순 Fully-connected 보다 학습시킬 weight가 적다.
    • 학습과 연산에 속도가 빠르며, 효율적이다.
    • 이미지나 영상데이터를 처리할 때 사용한다.

    CNN의 접근

    image


    이미지 표현 => Matrix

    image

    해당 실습에서 사용된 데이터와 코드(.ipynb)는 아래 링크에서 확인할 수 있습니다.

     

    패키지 Import

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    
    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    import torch.optim as optim
    from torch.utils.data import Dataset, DataLoader
    from torchsummary import summary as summary_
    
    
    from tqdm import tqdm, notebook

     

    데이터 살펴보기

    Fashion MNIST Dataset

    # label_tags = ["티셔츠/탑", "트루저", "풀오버", "드레스", "코트", "샌들", "셔츠", "스니커", "가방", "앵클부츠"]
    label_tags = ["T-shirt/top", "Trouser", "Pullover", "Dress", "Coat", "Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"]
    
    train_dataset = pd.read_csv("Data/fashion-mnist_train.csv")
    test_dataset = pd.read_csv("Data/fashion-mnist_test.csv")
    
    # Split to Image & Label
    train_images = (train_dataset.iloc[:, 1:].values).astype("float32")
    train_labels = train_dataset["label"].values
    test_images = (test_dataset.iloc[:, 1:].values).astype("float32")
    test_labels = test_dataset["label"].values

    Pandas 패키지를 통해서 Fashion_Mnist CSV 파일을 읽어온다.

    • Train 60000개, Test 10000개

     

    그 다음 Feature와 Label로 구분지어준다.

    # Split into Train, Valid Dataset
    from sklearn.model_selection import train_test_split
    train_images, valid_images, train_labels, valid_labels = train_test_split(train_images, 
                                                                              train_labels, 
                                                                              stratify = train_labels, 
                                                                              random_state = 42, 
                                                                              test_size = 0.2)

    SciKit-Learn 패키지의 Train_Test_Split 함수를 통해서 Train데이터 60000개를 48000개의 학습셋과 12000개의 검증셋으로 나눈다.

     

    # Reshape image's size to check for ours
    # (size, 784) => (size, 28, 28)
    train_images = train_images.reshape(train_images.shape[0], 28, 28)
    valid_images = valid_images.reshape(valid_images.shape[0], 28, 28)
    test_images = test_images.reshape(test_images.shape[0], 28, 28)

    이미지를 2차원 데이터로 차원 변환해준다.(784 -> 28x28)

     

    # Check Train, Valid, Test Image's Shape
    print("The Shape of Train Images: ", train_images.shape)
    print("The Shape of Valid Images: ", valid_images.shape)
    print("The Shape of Test Images: ", test_images.shape)
    
    # Check Train, Valid Label's Shape
    print("The Shape of Train Labels: ", train_labels.shape)
    print("The Shape of Valid Labels: ", valid_labels.shape)
    print("The Shape of Valid Labels: ", test_labels.shape)
    The Shape of Train Images:  (48000, 28, 28)
    The Shape of Valid Images:  (12000, 28, 28)
    The Shape of Test Images:  (10000, 28, 28)
    The Shape of Train Labels:  (48000,)
    The Shape of Valid Labels:  (12000,)
    The Shape of Valid Labels:  (10000,)

     

     

    데이터를 시각화 해보자

    # 데이터 시각화
    img = train_images[20]
    label = train_labels[20]
    
    print("Label :",label_tags[label])
    plt.imshow(img, cmap='gray'); plt.show()
    Label : Pullover

    output_11_1

     

     

    Dataset 정의

    class MyDataset(Dataset):
        def __init__(self, feature_data, label_data, num_classes = 10):
            self.x_data = feature_data
            self.y_data = label_data
            self.num_classes = num_classes
    
        def __len__(self):
            return len(self.x_data)
    
        def __getitem__(self, idx):
            # image
            img = self.x_data[idx] / 255.       # 명암값 정규화
            img = torch.FloatTensor(img)        # Tensor로 변환
            img = img.view(1, 28, 28)           # (channel, width, height)
    
            # label
            label = torch.tensor(self.y_data[idx])
            label = F.one_hot(label, num_classes = self.num_classes)        # one-hot 인코딩
            label = label.float()
    
            return img, label
    label_tags = ['T-Shirt', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt','Sneaker', 'Bag', 'Ankle Boot']
    
    train_dataset = MyDataset(train_images, train_labels)
    valid_dataset = MyDataset(valid_images, valid_labels)
    test_dataset = MyDataset(test_images, test_labels)
    
    train_loader = DataLoader(train_dataset, batch_size = 32, shuffle = True)
    valid_loader = DataLoader(valid_dataset, batch_size = 32)
    test_loader = DataLoader(test_dataset, batch_size = 32)
    
    print(train_loader)
    print(valid_loader)
    print(test_loader)

    TASK에 맞게 Dataset을 정의하고 DataLoader로 생성해준다.

    • Image는 기존에 28x28 Shape에서 1x28x28으로 변환해준다.(Channel, Width, Height)
    • Label은 0~9까지 10개의 Label을 One-Hot Encoding 해준다.
    • Batch_size = 32이다.필자 GPU가 좋지 못하다..

     

     

    훈련 & 검증 함수 정의

    loss_fn = nn.CrossEntropyLoss()
    
    def calc_acc(X, Y):
        x_val, x_idx = torch.max(X, dim=1)
        y_val, y_idx = torch.max(Y, dim=1)
        return (x_idx == y_idx).sum().item()
    
    def train(EPOCHS, model, train_loader, opt):
        train_loss_history = []
        valid_loss_history = []
        train_acc_history = []
        valid_acc_history = []
        for epoch in range(1, EPOCHS+1):
            model.train()
            train_acc = 0
            print("<<< EPOCH {} >>>".format(epoch))
            for batch_idx, (img,label) in enumerate(notebook.tqdm(train_loader)):
                img, label = img.to(DEVICE), label.to(DEVICE)
    
                output = model(img)                 # 순전파
                loss = loss_fn(output, label)       # 오차 계산
    
                opt.zero_grad()                     # opt내부 값 초기화
                loss.backward()                     # 오차 역전파
                opt.step()                          # 가중치 갱신
    
                train_acc += calc_acc(output, label)
                if batch_idx % 100 == 0 and batch_idx != 0:
                    print("Training : [{}/{} ({:.0f}%)]\tLoss: {:.6f}\t Acc : {:.3f}".format(
                        batch_idx * len(img), 
                        len(train_loader.dataset), 
                        100. * batch_idx / len(train_loader), 
                        loss.item(),
                        train_acc / len(train_loader.dataset)))
            t_loss, t_acc = evaluate(model, valid_loader)
            print("[{}] valid Loss : {:.4f}\t accuracy: {:.2f}%\n\n".format(epoch, t_loss, t_acc*100.))
    
            train_loss_history.append(loss.item())
            train_acc_history.append(train_acc / len(train_loader.dataset))
    
            valid_loss_history.append(t_loss.item())
            valid_acc_history.append(t_acc)
    
        return train_loss_history, train_acc_history, valid_loss_history, valid_acc_history
    
    def evaluate(model, valid_loader):
        model.eval()
        t_loss = 0
        correct = 0
    
        with torch.no_grad():
            for img, label in notebook.tqdm(valid_loader):
                img, label = img.to(DEVICE), label.to(DEVICE)
    
                output = model(img)
                t_loss += loss_fn(output, label)
    
                correct += calc_acc(output, label)
    
        t_loss /= len(valid_loader)
        t_acc = correct / len(valid_loader.dataset)
        return t_loss, t_acc
    
    def predict(model, lower=0, upper=10):
        model.eval()
        for idx in range(lower, upper):
            img, _ = test_dataset.__getitem__(idx)
    
            output = model(img.view(1, 1, 28, 28))
    
            o_val, o_idx = torch.max(output, dim=1)
    
            print("Label :", label_tags[o_idx.item()])
            plt.imshow(img.view(28, 28), cmap='gray')
            plt.show()
            print()

    학습을 위한 함수를 정의해 준다.

    • 훈련을 위한 Train() 함수 => Train_Loader
    • 검증을 위한 Evaluate()함수 => Valid_Loader or Test_Loader
    • 추론을 위한 Predict()함수 => Test_Loader(본 포스팅에선 사용하지 않는다.)

     

     

     

    모델 정의

    모델은 2가지를 정의하였다.

    • 단순히 선형변환을 사용하는 LinearNet
    • CNN을 이용한 분류기 CNN

     

    Linear Net

    3계층의 선형변환을 수행한다.

    class LinearNet(nn.Module):
        def __init__(self):
            super(LinearNet, self).__init__()
            self.fc1 = nn.Linear(784, 256)
            self.fc2 = nn.Linear(256, 64)
            self.fc3 = nn.Linear(64, 10)
    
            self.act_fn = nn.LeakyReLU()
    
        def forward(self, x):
            x = x.view(-1, 1*28*28)
    
            x = self.fc1(x)
            x = self.act_fn(x)
    
            x = self.fc2(x)
            x = self.act_fn(x)
    
            x = self.fc3(x)
            return x

     

    CNN

    Convolutional Dot + Pooling을 4계층으로 쌓고 2계층의 선형분류기를 쌓은 CNN 클래스이다.

    class CNN(nn.Module):
        def __init__(self):
            super(CNN, self).__init__()
            self.conv1 = nn.Conv2d(1, 8, kernel_size = 3, padding = 1)
            self.conv2 = nn.Conv2d(8 , 16, kernel_size = 3, padding = 1)
            self.conv3 = nn.Conv2d(16, 32, kernel_size = 3, padding = 1)
            self.conv4 = nn.Conv2d(32, 64, kernel_size = 3, padding = 1)
    
            self.pooling = nn.MaxPool2d(2, 2)
            self.flatten = nn.AdaptiveAvgPool2d(1)
    
            self.fc1 = nn.Linear(64, 24)
            self.fc2 = nn.Linear(24, 10)
    
            self.act_fn = nn.LeakyReLU()
    
        def forward(self, x):
            x = self.conv1(x)           # (batch, 1, 28, 28) -> (batch, 8, 28, 28)
            x = self.pooling(x)         # (batch, 8, 28, 28) -> (batch, 8, 14, 14)
            x = self.act_fn(x)
    
            x = self.conv2(x)           # (batch, 8, 14, 14) -> (batch, 16, 14, 14)
            x = self.pooling(x)         # (batch, 16, 14, 14) -> (batch, 16, 7, 7)
            x = self.act_fn(x)
    
            x = self.conv3(x)           # (batch, 16, 7, 7) -> (batch, 32, 7, 7)
            x = self.pooling(x)         # (batch, 32, 7, 7) -> (batch, 32, 3, 3)
            x = self.act_fn(x)
    
            x = self.conv4(x)           # (batch, 32, 3, 3) -> (batch, 64, 3, 3)
            x = self.pooling(x)         # (batch, 64, 3, 3) -> (batch, 64, 1, 1)
            x = self.act_fn(x)        
    
            x = self.flatten(x)         # # (batch, 64, 3, 3) -> (batch, 64, 1, 1)
            x = x.view(-1, 64*1*1)
    
            x = self.fc1(x)
            x = self.act_fn(x)
    
            x = self.fc2(x)
            return x

     

     

    훈련 및 검증

    이제 모든 준비는 끝났다.
    LinearNet부터 훈련/검증을 시작해보자

     

    Linear Net

    USE_CUDA = torch.cuda.is_available()
    DEVICE = "cuda" if USE_CUDA else "cpu"
    
    model = LinearNet().to(DEVICE)
    opt = optim.Adam(model.parameters())
    
    print("Device :", DEVICE)
    summary_(model,(1,28,28), device=DEVICE)
    Device : cuda
    ----------------------------------------------------------------
            Layer (type)               Output Shape         Param #
    ================================================================
                Linear-1                  [-1, 256]         200,960
             LeakyReLU-2                  [-1, 256]               0
                Linear-3                   [-1, 64]          16,448
             LeakyReLU-4                   [-1, 64]               0
                Linear-5                   [-1, 10]             650
    ================================================================
    Total params: 218,058
    Trainable params: 218,058
    Non-trainable params: 0
    ----------------------------------------------------------------
    Input size (MB): 0.00
    Forward/backward pass size (MB): 0.00
    Params size (MB): 0.83
    Estimated Total Size (MB): 0.84
    ----------------------------------------------------------------

    LinearNet은 단순한 3계층이다.

    사용하는 가중치 개수는 218,058이다.

     

    # 학습 시작 #
    t_loss_his, t_acc_his, v_loss_his, v_acc_his = train(EPOCHS = 10, model = model, train_loader = train_loader, opt = opt)
    <<< EPOCH 1 >>>
    Training : [3200/48000 (7%)]    Loss: 0.735302     Acc : 0.041
    ... 중간 생략 ...
    Training : [44800/48000 (93%)]    Loss: 0.350932     Acc : 0.752
    [1] valid Loss : 0.3948     accuracy: 84.87%
    
    <<< EPOCH 2 >>>
    Training : [3200/48000 (7%)]    Loss: 0.448148     Acc : 0.057
    ... 중간 생략 ...
    Training : [44800/48000 (93%)]    Loss: 0.507022     Acc : 0.803
    [2] valid Loss : 0.3696     accuracy: 86.38%
    
    ... 
    중간 생략
    ...
    
    <<< EPOCH 10 >>>
    Training : [3200/48000 (7%)]    Loss: 0.036901     Acc : 0.061
    ... 중간 생략 ...
    Training : [44800/48000 (93%)]    Loss: 0.253712     Acc : 0.851
    [10] valid Loss : 0.3202     accuracy: 88.84%

    ​ 10번의 학습 후 검증데이터에 대한 Loss와 정확도는 각각 0.3202와 88.84%이다.

     

     

    plt.plot(t_loss_his, label="train")
    plt.plot(v_loss_his, label="valid")
    plt.legend()
    plt.show()

    output_26_0

    plt.plot(t_acc_his, label="train")
    plt.plot(v_acc_his, label="valid")
    plt.legend()
    plt.show()

    output_27_0

    Test셋에 대한 검증 결과는 아래와 같다.

    v_loss, v_acc = evaluate(model, test_loader)
    print("Test Loss : {:.4f}\t accuracy: {:.2f}%\n".format(v_loss, v_acc*100.))
    Test Loss : 0.3141     accuracy: 88.88%

    CNN

    USE_CUDA = torch.cuda.is_available()
    DEVICE = "cuda" if USE_CUDA else "cpu"
    
    model = CNN().to(DEVICE)
    opt = optim.Adam(model.parameters())
    
    print("Device :", DEVICE)
    summary_(model,(1,28,28), device=DEVICE)
    Device : cuda
    ----------------------------------------------------------------
            Layer (type)               Output Shape         Param #
    ================================================================
                Conv2d-1            [-1, 8, 28, 28]              80
             MaxPool2d-2            [-1, 8, 14, 14]               0
             LeakyReLU-3            [-1, 8, 14, 14]               0
                Conv2d-4           [-1, 16, 14, 14]           1,168
             MaxPool2d-5             [-1, 16, 7, 7]               0
             LeakyReLU-6             [-1, 16, 7, 7]               0
                Conv2d-7             [-1, 32, 7, 7]           4,640
             MaxPool2d-8             [-1, 32, 3, 3]               0
             LeakyReLU-9             [-1, 32, 3, 3]               0
               Conv2d-10             [-1, 64, 3, 3]          18,496
            MaxPool2d-11             [-1, 64, 1, 1]               0
            LeakyReLU-12             [-1, 64, 1, 1]               0
    AdaptiveAvgPool2d-13             [-1, 64, 1, 1]               0
               Linear-14                   [-1, 24]           1,560
            LeakyReLU-15                   [-1, 24]               0
               Linear-16                   [-1, 10]             250
    ================================================================
    Total params: 26,194
    Trainable params: 26,194
    Non-trainable params: 0
    ----------------------------------------------------------------
    Input size (MB): 0.00
    Forward/backward pass size (MB): 0.13
    Params size (MB): 0.10
    Estimated Total Size (MB): 0.23
    ----------------------------------------------------------------

    CNN은 앞선 LinearNet보다는 복잡한 구조를 가졌지만,
    사용하는 가중치 개수는 26,194이다.
    LinearNet보다 대략 9~10배 정도 더 적은 가중치를 사용한다.
    과연, CNN은 적은 가중치로 어떤 성능을 보여줄까?

     

    # 학습 시작 #
    t_loss_his, t_acc_his, v_loss_his, v_acc_his = train(EPOCHS = 10, model = model, train_loader = train_loader, opt = opt)
    <<< EPOCH 1 >>>
    Training : [3200/48000 (7%)]    Loss: 1.139337     Acc : 0.024
    ... 중간 생략 ...
    Training : [44800/48000 (93%)]    Loss: 0.460232     Acc : 0.673
    [1] valid Loss : 0.5410     accuracy: 79.25%
    <<< EPOCH 2 >>>
    Training : [3200/48000 (7%)]    Loss: 0.342676     Acc : 0.055
    ... 중간 생략 ...
    Training : [44800/48000 (93%)]    Loss: 0.312424     Acc : 0.781
    
    ...
    중간 생략 
    ...
    
    <<< EPOCH 10 >>>
    Training : [3200/48000 (7%)]    Loss: 0.082751     Acc : 0.061
    ... 중간 생략 ...
    Training : [44800/48000 (93%)]    Loss: 0.157439     Acc : 0.847
    [10] valid Loss : 0.2796     accuracy: 89.85%

    ​ 10번의 학습 후 검증데이터에 대한 Loss와 정확도는 각각 0.2796와 89.85%이다.

     

    plt.plot(t_loss_his, label="train")
    plt.plot(v_loss_his, label="valid")
    plt.legend()
    plt.show()

    output_32_0

    plt.plot(t_acc_his, label="train")
    plt.plot(v_acc_his, label="valid")
    plt.legend()
    plt.show()

    output_33_0

     

    v_loss, v_acc = evaluate(model, test_loader)
    print("Test Loss : {:.4f}\t accuracy: {:.2f}%\n".format(v_loss, v_acc*100.))
    Test Loss : 0.2746     accuracy: 89.97%

     

    Test 데이터에 대해서는 Linear Net에 비해 대략 1%정도 더 높은 성능을 보인다.
    과연 이 1%의 성능이 더 좋은 것일까?

    필자는 9~10배 정도의 적은 가중치를 사용하고도 LinearNet보다 더 좋은 성능을 보인 CNN의 손을 들어주고 싶다.
    LinearNet과 CNN의 진정한 성능차이는 컬러이미지에서 들어난다.

     


    다음 포스팅은 Cifar-10 데이터셋을 통해서 코드 실습을 진행해보고자 한다.

    728x90

    댓글

Designed by black7375.