pytorch

[pytorch] 이미지 분류를 위한 LeNet-5 구현

독립성이 강한 ISFP 2023. 5. 15. 12:31
728x90
반응형

이미지 분류는 주어진 이미지를 다양한 클래스 또는 범주로 분류하는 것을 의미합니다.

이때, 분류의 목적은 이미지에 특정 대상이 존재하는지 여부를 판단하는 것입니다.

 

예를 들어, 고양이와 개를 분류하는 문제를 생각해 보면, 이미지 분류 모델은 입력된 이미지가 고양이인지 개인지를 판단합니다.

모델은 이미지에 대한 특징을 추출하고, 추출된 특징을 기반으로 해당 이미지가 고양이인지 개인지를 예측합니다.


LeNet은 1990년대 후반 Yann LeCun 등이 개발한 초기 합성곱 신경망(CNN) 아키텍처로, 주로 필기체 숫자 인식에 사용되었으며 컴퓨터 비전 작업에서 심층 학습의 발전에 중요한 역할을 했습니다.

현대적인 아키텍처에 비해 레이어 수가 적지만, CNN의 발전을 이해하는 데 기초로 작용합니다.

LeNet 아키텍처를 8개의 레이어로 분할하여 설명해보겠습니다

 

32 x 32 크기의 이미지에 합성곱층과 최대 풀링층이 쌍으로 두 번 적용된 후 완전연결층을 거쳐 이미지가 분류되는 신경망입니다.

  1. 입력 레이어: LeNet은 32x32 픽셀 크기의 흑백 이미지를 입력으로 받습니다.
  2. 합성곱 레이어: 첫 번째 합성곱 레이어는 5x5 크기의 6개 필터를 입력 이미지에 적용하고, 이후 렐루(Relu) 활성화 함수를 적용합니다.
  3. 최대 풀링 레이어: 첫 번째 합성곱 레이어 뒤에는 최대 풀링 레이어가 이어지는데, 겹치지 않는 2x2 영역에서 최대 풀링을 수행하여 공간적인 차원을 줄입니다.
  4. 합성곱 레이어: 두 번째 합성곱 레이어는 이전 레이어의 출력에 5x5 크기의 16개 필터를 적용하고, 이후 렐루(Relu) 활성화 함수를 적용합니다.
  5. 최대 풀링 레이어: 이전의 최대 풀링 레이어와 유사하게, 겹치지 않는 2x2 영역에서 최대 풀링을 수행합니다.
  6. 완전 연결 레이어: 합성곱과 풀링 레이어 이후에 출력은 펼쳐지고, 렐루(Relu) 활성화 함수가 적용된 120개 뉴런을 가진 완전 연결 레이어에 연결됩니다.
  7. 완전 연결 레이어: 이전 레이어의 다음에는 84개 뉴런을 가진 또 다른 완전 연결 레이어가 이어지는데, 여기에도 렐루(Relu) 활성화 함수가 적용됩니다.
  8. 출력 레이어: 마지막 완전 연결 레이어는 출력 레이어로, 숫자 인식 작업의 10가지 가능한 클래스에 해당하는 10개의 뉴런을 포함합니다. 일반적으로 소프트맥스(softmax) 활성화 함수를 사용하여 클래스에 대한 확률 분포를 생성합니다.
계층 유형 특성 맵 크기 커널 크기  스트라이드 활성화 함수
이미지 32X32   -- -
합성곱층 6 28X28 5X5 렐루(Relu)
최대 풀링층 6 14X14 2X2 2 -
합성곱층 16 10X10 5X5 1 렐루(Relu)
최대 풀링층 16 5X5 2X2 2 -
완전연결층 - 120 - - 렐루(Relu)
완전연결층 - 84 - - 렐루(Relu)
완전연결층 - 2 - - 소프트맥스(softmax)

 

 


1. 라이브러리 호출

import torch
import torchvision
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms # 이미지 변환(전처리) 기능을 제공
from torch.autograd import Variable
from torch import optim # 경사하강법을 이용하여 가중치를 구하기 위한 옵티마이저
import os # 파일 경로에 대한 함수들을 제공
import cv2
from PIL import Image
from tqdm import tqdm_notebook as tqdm # 진행 상황 표현
import random
import torch.nn as nn
import torch.nn.functional as F
from matplotlib import pyplot as plt

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

tqdm진행 상태를 시각적으로 보여주기 위해 바(bar) 형태로 표시하는 라이브러리입니다.

주로 모델 훈련 과정에서 진행 상태를 확인하고자 할 때 사용됩니다.

tqdm은 반복문이나 작업의 진행 상황을 모니터링하며, 진행 막대와 텍스트로 표시하여 사용자에게 진행 상태를 시각적으로 전달합니다.

이를 통해 모델의 학습 진행 상황을 실시간으로 확인하고, 훈련 시간 등의 정보를 제공하여 개발자가 효율적으로 모델을 관리할 수 있게 도와줍니다.

2. 이미지 데이터셋 전처리

먼저 모델 학습에 필요한 데이터셋의 전처리(텐서 변환)가 필요합니다.

class ImageTransform():    
    def __init__(self, resize, mean, std):
        self.data_transform = {
            'train': transforms.Compose([
                transforms.RandomResizedCrop(resize, scale=(0.5, 1.0)),
                transforms.RandomHorizontalFlip(),
                transforms.ToTensor(),
                transforms.Normalize(mean, std)
            ]), ## 1
            'val': transforms.Compose([
                transforms.Resize(256),
                transforms.CenterCrop(resize),
                transforms.ToTensor(),
                transforms.Normalize(mean, std)
            ])
        }
        
    def __call__(self, img, phase): ## 2
        return self.data_transform[phase](img)

1. torchvision.transform은 이미지 데이터를 변환하여 모델(네트워크)의 입력으로 사용할 수 있게 변환해 줍니다. 

  • transforms.Compose : 이미지를 변형할 수 있는 방식들의 묶음입니다.
  • transforms.RandomResizedCrop(resize, scale=(0.5,1.0)): 이미지 중에서 임의의 영역을 자르고(resize), 크기를 조절하는 함수입니다. 먼저, 입력된 이미지의 높이와 너비 중 작은 쪽을 resize(224x224)로 지정된 크기로 조정합니다. 그리고 scale 인자로 지정된 범위 내에서 임의의 비율을 선택하여 이미지를 늘리거나(resize) 줄입니다. 이때, scale은 자를 이미지의 비율을 의미하며, (0.5, 1.0)과 같이 지정되면 원래 이미지 크기의 50%에서 100% 크기까지 임의로 자르게 됩니다. 즉, 이 함수는 입력된 이미지에서 임의의 부분을 자르고, 크기를 조정하여 데이터 증강(augmentation)을 수행하는 데 사용됩니다.
  • transforms.RandomHorizontalFlip(): 입력된 이미지를 50%의 확률로 좌우 반전시키는(preprocessing) 함수입니다.
    즉, 훈련 이미지 중 반은 위아래 뒤집힌 상태로 두고, 반은 그대로 사용합니다.
  • transforms.ToTensor() : torchvision 라이브러리의 메서드 중 ImageFolder를 비롯하여, 이미지를 읽을 때 PIL(Python Imaging Library)을 사용합니다. PIL을 사용하여 이미지를 읽으면 생성되는 이미지는 배열의 차원이 (높이 H x 너비 W x 채널 수 C)로 표현되고, 각 픽셀의 값은 0~255 범위를 가집니다.
    PyTorch의 모델은 입력 데이터의 형태가 (배치 크기 x 채널 수 C x 높이 H x 너비 W)정규화float32 형태여야 합니다. 따라서 PIL 이미지나 NumPy 배열(ndarray)을 PyTorch 텐서로 변환할 때는 transforms.ToTensor() 메서드를 사용합니다. 이 메서드는 입력된 이미지나 배열의 데이터 타입을 float32로 변경하고, 이미지의 픽셀 값을 0~1 범위로 정규화하여 PyTorch 텐서로 반환합니다. 변환된 텐서는 모델의 입력 데이터로 사용됩니다. 예를 들어, 입력 이미지가 크기가 (H, W, C)인 ndarray일 경우에는 ToTensor 함수를 사용하여 크기가 (C, H, W)인 PyTorch 텐서로 변환됩니다.
  • transforms.Normailze(mean, std) : 전이 학습에서 사용되는 사전 훈련된 모델들은 대부분 ImageNet 데이터셋에서 훈련되었습니다. 따라서 사전 훈련된 모델을 사용하기 위해서는 ImageNet 데이터의 각 채널별 평균과 표준편차에 맞는 정규화를 해주어야 합니다.
    즉, Normalize 메서드 안에 사용된 (mean:0. 485, 0. 456, 0. 406), (std:0.229.0.224.0. 225)는 ImageNet에서 이미지들의 RGB 채널마다 평균과 표준편차를 의미합니다. 참고로 OpenCV를 사용해서 이미지를 읽어 온다면 RGB가 아닌 BGR 이미지이므로 채널 순서에 주의해야 합니다.

2. __call__ 메서드는 해당 클래스의 인스턴스를 호출할 때 실행되는 메서드입니다. 위 코드에서 ImageTransform 클래스의 인스턴스를 호출하면 __call__ 메서드가 실행되고, 이 메서드는 전달된 이미지와 처리할 단계(phase)에 따라서 data_transform 딕셔너리에서 해당하는 전처리 과정을 선택하여 적용합니다.

즉, __call__ 메서드는 입력 이미지와 전처리 단계를 받아서 해당 단계에 맞는 전처리 과정을 수행하고, 결과 이미지를 반환하는 역할을 합니다.

  • img는 처리할 이미지입니다. 전처리 과정은 이 이미지에 대해서 수행됩니다.
  • phase는 현재 전처리 단계를 나타내는 문자열입니다. 위 코드에서는 'train' 또는 'val' 중 하나가 사용됩니다. 이 값에 따라서 data_transform 딕셔너리에서 적용할 전처리 과정이 결정됩니다. 'train'인 경우에는 RandomResizedCrop과 RandomHorizontalFlip이 적용되고, 'val'인 경우에는 Resize와 CenterCrop이 적용됩니다.

즉, __call__ 메서드는 입력 이미지 img와 전처리 단계 phase를 받아서 해당 단계에 맞는 전처리 과정을 수행하고, 결과 이미지를 반환하는 역할을 합니다.

3. 이미지 데이터셋을 불러온 후 훈련, 검증, 테스트로 분리

cat_directory = './dogs-vs-cats/Cat/'
dog_directory = './dogs-vs-cats/Dog/'

## 1
cat_images_filepaths = sorted([os.path.join(cat_directory, f) 
                               for f in os.listdir(cat_directory)])   
dog_images_filepaths = sorted([os.path.join(dog_directory, f) 
                               for f in os.listdir(dog_directory)])
                               
## 2                               
images_filepaths = [*cat_images_filepaths, *dog_images_filepaths] 

## 3
correct_images_filepaths = [i for i in images_filepaths if cv2.imread(i) is not None]    

random.seed(42) ## 4
random.shuffle(correct_images_filepaths)

# 일부 데이터만 사용
train_images_filepaths = correct_images_filepaths[:400]    
val_images_filepaths = correct_images_filepaths[400:-10]  
test_images_filepaths = correct_images_filepaths[-10:]    

print(len(train_images_filepaths), len(val_images_filepaths), len(test_images_filepaths))

1. cat_images_filepaths = sorted([os.path.join(cat_directory, f) for f in os.listdir(cat_directory)])  

  • os.listdir(cat_directory): 주어진 경로(cat_directory)에 있는 모든 파일과 디렉토리의 이름을 리스트 형태로 반환하는 함수입니다. 위 코드에서 cat_directory는 고양이 이미지가 저장된 디렉토리의 경로를 의미합니다.
  • os.path.join: 주어진 경로들을 하나의 경로로 연결해 주는 함수입니다.
    예를 들어, os.path.join('./dogs-vs-cats/Cat/', 'cat.0.jpg')와 같이 함수에 경로들을 인자로 전달하면, './dogs-vs-cats/Cat/cat.0.jpg'와 같이 전체 경로를 반환합니다.
  • sorted: 데이터를 정렬된 리스트로 만들어서 반환합니다.

2. images_filepaths = [*cat_images_filepaths, *dog_images_filepaths] 

두 변수를 하나의 리스트로 합쳐줍니다.

-> ['./dogs-vs-cats/Cat/cat.0.jpg', './dogs-vs-cats/Dog/dog.0.jpg',

,'./dogs-vs-cats/Cat/cat.1.jpg', './dogs-vs-cats/Dog/dog.1.jpg']

'./dogs-vs-cats/Cat/cat.2.jpg', './dogs-vs-cats/Dog/dog.2.jpg' ....]

 

3. correct_images_filepaths = [i for i in images_filepaths if cv2.imread(i) is not None] 

images_filepaths에 있는 파일 경로 중에서 cv2.imread() 함수가 None을 반환하지 않는 파일 경로만 모아서 리스트로 만듭니다.
이를 통해 이미지 파일이 아닌 파일 경로는 제외됩니다.

 

4.  random.seed(42):  난수 발생기(random number generator)를 초기화합니다.  이를 초기화하는 것은 난수를 생성하는 시드(seed) 값을 고정시키는 것입니다. 이렇게 하면 같은 시드 값으로 초기화한 난수 발생기를 사용하면 항상 같은 난수를 생성할 수 있습니다. 따라서, 코드의 실행 결과를 재현하는 데 유용합니다.

4. test 데이터셋 이미지 확인

import matplotlib.pyplot as plt
import cv2
import os

def display_image_grid(images_filepaths, predicted_labels=(), cols=5):
    rows = len(images_filepaths) // cols  
    fig, ax = plt.subplots(rows, cols, figsize=(12, 6))
    ax = ax.ravel()

    for i, image_filepath in enumerate(images_filepaths): # 경로를 정규화 함
        image = cv2.imread(image_filepath)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) ## 1
        
        true_label = os.path.normpath(image_filepath).split(os.sep)[-2] ## 2
        predicted_label = predicted_labels[i] if predicted_labels else true_label ## 3
        color = "green" if true_label == predicted_label else "red"

        ax[i].imshow(image)
        ax[i].set_title(predicted_label, color=color)
        ax[i].axis("off")

    plt.tight_layout()
    plt.show()

 


기본적인 데이터셋 준비가 완료되었습니다.

이제부터 모델 학습을 위한 구체적인 단계들이 시작될 텐데 순서는 다음과 같습니다.

먼저 데이터셋에는 학습할 데이터의 경로를 정의하고 그 경로에서 데이터를 읽어 옵니다. 

데이터셋 크기가 클 수 있으므로 __init__ 에서 전체 데이터를 읽어오는 것이 아니라 경로만 저장해 놓고, __getitem__ 메서드에서 이미지를 읽어 옵니다.

즉, 데이터를 어디에서 가져올지 결정합니다. 이후 데이터로더에서 데이터셋의 데이터를 메모리로 불러오는데, 한꺼번에 전체 데이터를 불러오는 것이 아니라 배치 크기만큼 분할하여 가져옵니다.

 

이번에 살펴볼 DogvsCatDataset() 클래스는 데이터를 불러오는 방법을 정의합니다. 이번 예제의 목적은 다수의 개와 고양이 이미지가 포함된 데이터에서 이들을 예측하는 것입니다. 따라서 레이블(정답) 이미지에서 고양이와 개가 포함될 확률을 코드로 구현합니다.

예를 들어 고양이가 있는 이미지의 레이블은 0이 되고, 개가 있는 이미지의 레이블은 1이 되도록 코드를 구현합니다.

 

5. 이미지 데이터셋 클래스 정의

class DogvsCatDataset(Dataset):
    def __init__(self, file_list, transform=None, phase='train'):
        self.file_list = file_list
        self.transform = transform # DogvsCatDataset 클래스를 호출할 때 transform에 대한 매개변수를 받아 옵니다.
        self.phase = phase # train 적용

    def __len__(self): # images_filepaths 데이터셋의 전체 길이 반환
        return len(self.file_list)
    
    def __getitem__(self,idx): # 데이터셋에서 데이터를 가져오는 부분으로 결과는 텐서 형태 
        img_path = self.file_list[idx]
        img = Image.open(img_path) # img_path 위치에서 이미지 데이터들을 가져옴
        img_transformed = self.transform(img, self.phase) # 이미지에 'train' 전처리 적용
        label = img_path.split('/')[-1].split('.')[0] ## 1

        if label == 'dog':
            label = 1
        elif label == 'cat':
            label = 0
        return img_transformed, label

1. 이미지 경로 "./dogs-vs-cats/Cat/cat.0.jpg" 에서 빨간색 부분인 cat을 추출하는 것

즉, 이미지 데이터에 대한 레이블 값(dog, cat)을 가져옵니다.

6. 변수 값 정의

전처리에서 사용할 변수에 대한 값을 정의합니다.

size = 224
mean = (0.485, 0.456, 0.406)
std = (0.229, 0.224, 0.225)
batch_size = 32

7. 이미지 데이터셋 정의

훈련과 검증 용도의 데이터셋을 정의합니다.

앞에서 정의한 DogvsCatDataset() 클래스를 이용하여 훈련과 검증 데이터셋을 준비하고, 전처리를 적용합니다.

train_dataset = DogvsCatDataset(train_images_filepaths, 
                                transform=ImageTransform(size,mean, std), 
                                phase='train') # train 이미지에 train_transforms를 적용
val_dataset = DogvsCatDataset(val_images_filepaths, 
                              transform=ImageTransform(size,mean, std), 
                              phase='val') # val 이미지에 val_transforms를 적용

index = 0
print(train_dataset.__getitem__(index)[0].size()) # 훈련 데이터 train_dataset.__getitem__[0][0]의 크기(size) 출력
print(train_dataset.__getitem__(index)[1]) # 훈련 데이터의 레이블 출력

출력된 결과를 확인해 보면 이미지는 컬러 상태에서 224x224 크기를 가지며 레이블이 1로 출력되었습니다.

즉, 훈련 데이터셋의 레이블이 1 값을 갖기 때문에 개라는 이미지가 포함되어 있다는 것을 유추해 볼 수 있습니다.

8. 데이터로더 정의

전처리와 함께 데이터셋을 정의했기 때문에 이제 메모리로 불러와서 훈련을 위한 준비를 합니다.

train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) ## 1
val_dataloader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)

# train_dataloader와 val_dataloader를 합쳐서 표현
dataloader_dict = {'train': train_dataloader, 'val': val_dataloader}

batch_iterator = iter(train_dataloader)
inputs, label = next(batch_iterator)
print(inputs.size())
print(label)

1. 데이터로더는 배치 관리를 담당합니다. 한 번에 모든 데이터를 불러오면 메모리에 부담을 줄 수 있기 때문에 데이터를 그룹으로 쪼개서 조금씩 불러옵니다.

< train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) > 

  • 첫 번째 파라미터:  데이터를 불러오기 위한 데이터셋
  • batch_size: 한 번에 불러올 데이터 크기, 여기서는 32개씩 데이터를 불러옴
  • shuffle: 데이터를 가져올 때 임의로 섞어서 가져옴

다음은 데이터로더를 이용하여 train 데이터셋을 메모리로 불러온 후 데이터셋의 크기와 레이블을 출력한 결과입니다.

9. 모델의 네트워크 클래스

이제 데이터셋을 학습시킬 모델의 네트워크를 설계하기 위한 클래스를 생성합니다.

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        
        # 입력 : (3,244,244) 출력 : (16,220,220)
        self.cnn1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5, stride=1, padding=0) 
        self.relu1 = nn.ReLU() 
        self.maxpool1 = nn.MaxPool2d(kernel_size=2) # 220/2 -> (16,110,110)
        
        # 입력 : (16,110,110) 출력 : (32,106,106)
        self.cnn2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=0) 
        self.relu2 = nn.ReLU() # activation
        self.maxpool2 = nn.MaxPool2d(kernel_size=2) # 110/2 -> (32,53,53)   
        
        self.fc1 = nn.Linear(32*53*53, 512) 
        self.relu5 = nn.ReLU()         
        self.fc2 = nn.Linear(512, 2) 
        self.output = nn.Softmax(dim=1)        
    
    def forward(self, x):
        out = self.cnn1(x) 
        out = self.relu1(out)
        out = self.maxpool1(out)
        out = self.cnn2(out) 
        out = self.relu2(out) 
        out = self.maxpool2(out) 
        out = out.view(out.size(0), -1) # 완결연결층에 데이터를 전달하기 위해 데이터 형태를 1차원으로 바꿉니다.
        out = self.fc1(out) 
        out = self.fc2(out)                    
        out = self.output(out)
        return out

네트워크의 각 부분들은 통과할 때마다 입력과 출력의 형태가 바뀝니다.

 

Conv2d 계층에서의 출력 크기 구하는 공식

출력 크기 = (W - F + 2P) / S + 1     

  • W: 입력 데이터의 크기
  • F: 커널 크기
  • P: 패딩 크기
  • S: 스트라이드

MaxPool2d 계층에서의 출력 크기 구하는 공식

출력 크기 = IF / F

  • IF: 입력 필터의 크기
  • F: 커널 크기

10. 모델 객체 생성

LeNet()을 model이라는 이름으로 객체를 생성하여 모델 학습을 위한 준비를 합니다.

model = LeNet()
print(model)

11. torchsummary 라이브러리를 이용한 모델의 네트워크 구조 확인

출력 결과가 한눈에 들어오지 않는다면 torchsummary 라이브러리를 사용해 볼 수 있습니다.

torchsummary는 케라스와 같은 형태로 모델을 출력해 볼 수 있는 라이브러리입니다.

#!pip install torchsummary
from torchsummary import summary
summary(model, input_size=(3, 224, 224)) # 모델의 네트워크, (채널, 너비, 높이)

앞에서 모델 출력 결과와 비교해 보면 네트워크 내 파라미터 수와 구조가 이해하기 쉽게 표현되어 있는 것을 확인할 수 있습니다. 출력되는 정보로는 총 파라미터 수, 입력 크기, 네트워크 총 크기 등이 있습니다.

12. 옵티마이저와 손실 함수 정의

모델의 네트워크 구성이 완료되었기 때문에 이제 옵티마이저와 손실 함수를 정의합니다.

optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
criterion = nn.CrossEntropyLoss()

Momentum SGD는 SGD에 관성(momentum)을 추가한 방법입니다. 이를 통해 기울기가 가리키는 방향으로 더 일관된 업데이트가 이루어지고, 지역 최소점에서 빠져나오는 데 도움을 줍니다.

 

< optim.SGD(model.parameters(), lr=0.001, momentum=0.9) > 

  • model.parameters(): 경사 하강법을 통해 궁극적으로 업데이트하고자 하는 파라미터는 가중치와 bias입니다. 
  • lr: 가중치를 변경할 때 얼마나 크게 변경할지 결정합니다.
  • momentum: 업데이트의 크기와 방향을 조절하며, momentum 값이 높을수록 이전 업데이트가 현재 업데이트에 더 큰 영향을 주어 선택된 방향으로 지속적으로 진행되는 최적화 과정을 만듭니다.

13. 모델의 파라미터와 손실 함수를 CPU에 할당

model = model.to(device)
criterion = criterion.to(device)

14. 모델 학습 함수 정의

모델을 학습시킬 함수를 정의합니다.

학습 용도이기 때문에 model.train()을 사용합니다.

def train_model(model, dataloader_dict, criterion, optimizer, num_epoch):    
    since = time.time()
    best_acc = 0.0
    
    for epoch in range(num_epoch):
        print('Epoch {}/{}'.format(epoch + 1, num_epoch))
        print('-'*20)
        
        for phase in ['train', 'val']:           
            if phase == 'train':
                model.train() # 모델을 학습시키겠다는 의미
            else:
                model.eval()
                
            epoch_loss = 0.0
            epoch_corrects = 0
            					# dataloader_dict는 train_loader를 의미함
            for inputs, labels in tqdm(dataloader_dict[phase]):
                inputs = inputs.to(device) # 훈련 데이터셋을 CPU에 할당
                labels = labels.to(device)
                optimizer.zero_grad() # 역전파를 실행하기 전 기울기를 0으로 초기화
                
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels) # 손실 함수를 이용한 오차 계산
                    
                    if phase == 'train':
                        loss.backward() # 학습 가능한 모든 파라미터에 대해 기울기 계산
                        optimizer.step() # 파라미터 업데이트
                        
                    epoch_loss += loss.item() * inputs.size(0) 
                    epoch_corrects += torch.sum(preds == labels.data) # 정답 == 예측 -> epoch_corrects에 저장
                    
            epoch_loss = epoch_loss / len(dataloader_dict[phase].dataset) # 최종 오차 계산
            epoch_acc = epoch_corrects.double() / len(dataloader_dict[phase].dataset) # 최고 정확도 계산 
            
            print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc)) 
            
            if phase == 'val' and epoch_acc > best_acc: # 검증 데이터셋에 대한 가장 최적의 정확도 저장
                best_acc = epoch_acc
                best_model_wts = model.state_dict()
                
    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))
    return model

 

15. 모델 학습

import time

num_epoch = 10
model = train_model(model, dataloader_dict, criterion, optimizer, num_epoch)

검증 데이터셋을 이용한 모델 학습 결과 최고 57%의 정확도를 보이고 있습니다.

16. 모델 테스트를 위한 함수 정의

훈련 데이터셋과 더불어 테스트 데이터셋을 모델에 적용하여 정확도를 측정해 보겠습니다.

측정 결과는 데이터 프레임에 담아 둔 후 csv 파일로 저장합니다.

test 용도의 데이터셋을 이용하므로 model.eval()을 사용합니다

import pandas as pd
id_list = []
pred_list = []
_id=0

# 역전파 중 텐서들에 대한 변화도를 계산할 필요가 없음을 나타내는 것으로, 훈련 데이터셋의 모델 학습과 가장 큰 차이점입니다.
with torch.no_grad(): 
    for test_path in tqdm(test_images_filepaths): # test 데이터셋 이용
        img = Image.open(test_path)
        _id =test_path.split('/')[-1].split('.')[1]
        transform = ImageTransform(size, mean, std)
        img = transform(img, phase='val') # test 데이터셋 전처리
        img = img.unsqueeze(0) ## 1
        img = img.to(device)

        model.eval()
        outputs = model(img)
        preds = F.softmax(outputs, dim=1)[:, 1].tolist() ## 2       
        id_list.append(_id)
        pred_list.append(preds[0])
       
res = pd.DataFrame({
    'id': id_list,
    'label': pred_list
}) # test 예측 결과인 Id와 레이블을 데이터 프레임에 저장

res.sort_values(by='id', inplace=True)
res.reset_index(drop=True, inplace=True)

res.to_csv('LesNet.csv', index=False) # 데이터프레임을 csv 파일로 저장

1. torch.unsqueeze는 텐서에 차원을 추가할 때 사용합니다. 또한 0은 차원이 추가될 위치를 의미합니다.

예를 들어 형태가 (3)인 텐서가 있다고 가정을 해보겠습니다. 0위치에 차원을 추가하면 형태가 (1,3)이 됩니다.

 

(2,2)인 텐서에서

  • 0 위치에 차원 추가 → (1,2,2)
  • 1 위치에 차원 추가 → (2,1,2)
  • 2 위치에 차원 추가 → (2,2,1)

2. softmax는 지정된 차원을 따라 텐서의 요소가 범위에 있고 합계가 1이 되도록 크기를 다시 조정합니다.

< F.softmax(outputs, dim=1)[:, 1].tolist() >

  • outputs, dim=1 : outputs에 softmax를 적용하여 각 행의 합이 1이 되도록 합니다.
  • [:, 1] : 값 중 모든 행에서 두 번째 칼럼(1번째 인덱스)을 가져옵니다.
  • tolist() : 배열을 리스트 형태로 변환합니다.

17. 테스트 데이터셋의 예측 결과 호출

모델 예측 함수를 실행한 결과입니다.

예측 결과를 csv 파일로 저장하는 함수였기 때문에 실행 결과는 큰 의미가 없으며 단순히 처리가 되었다고 이해하면 됩니다.

res.head(10)

예측 결과 레이블이 0.5보다 크면 개를, 0.5 보다 작으면 고양이를 의미합니다.


 

 

[pytorch] 이미지 분류를 위한 AlexNet 구현

AlexNet은 컴퓨터 비전 분야의 발전에 중요한 역할을 한 합성곱 신경망(CNN) 아키텍처입니다. 이는 Alex Krizhevsky, Ilya Sutskever, Geoffrey Hinton에 의해 개발되었으며, 2012년 ImageNet 대규모 시각 인식 챌린지

resultofeffort.tistory.com

 

728x90
반응형