수업내용 정리

[인공지능 응용] CIFAR10 예제 공부하기

Honey Badger 2023. 4. 6. 22:18

https://colab.research.google.com/github/pytorch/tutorials/blob/gh-pages/_downloads/4e865243430a47a00d551ca0579a6f6c/cifar10_tutorial.ipynb#scrollTo=f4krrQjVJkca

 

cifar10_tutorial.ipynb

Run, share, and edit Python notebooks

colab.research.google.com

 

Classifier(분류자) 학습시키기

 

 

  먼저, 데이터에 대해 생각해보자. 일반적으로 이미지, 텍스트, 오디오 또는 비디오 데이터를 처리해야 할 때, 데이터를 numpy 배열로 로드하는 표준 python 패키지를 사용할 수 있다. 그런 다음 이 배열을 Torch로 변환할 수 있다. 

 

<각 파일 형식 별로 유용한 패키지>

 

이미지: Pillow, OpenCV 

오디오 : Scipy, Librosa

텍스트 : Python, Cython기반 로드, NLTK, SpaCy

 

  특히 Vision을 위해, 우리는 ImageNet, CIFAR10, MNIST 등과 같은 일반적인 데이터 셋을 위한 데이터 로더와 이미지 즉, torchvision.datasets 및 torch.utils.data를 위한 데이터 변환기들이 있는 Torchvision이라는 패키지를 만들었다. 이렇게 하면 매우 편리하고 반복적으로 사용하는 코드를 작성할 필요가 없다. 

 

  이 튜토리얼에서는 CIFAR10 데이터 셋을 사용한다. 그것은 비행기, 자동차, 새, 고양이, 사슴, 개, 개구리, 말, 배, 트럭의 클래스를 가지고 있다. CIFAR-10의 이미지는 크기가 3x32x32 즉, 크기가 32x32 픽셀인 3채널 컬러 이미지이다. 

 

이미지 분류자를 학습시키려면 다음 단계를 순서대로 수행하여야 한다.

 

1. CIFAR10 학습을 로드 및 정규화시키고 TorchVision을 이용하여 데이터셋을 테스트한다.

2. Convolutional Neural Network(데이터로부터 직접 학습하는 딥러닝의 신경망 아키텍처)를 정의한다.

3. Loss Function(알고리즘이 예측한 값과 실제 정답의 차이를 비교하기 위한 함수)를 정의한다. 

4. 학습한 데이터에 대한 네트워크를 학습시킨다.

5. 테스트 데이터를 통해 네트워크 테스트를 수행한다. 

 

 

 

 

1. CIFAR10 로드 및 정규화하기.

 

    Torchvision을 사용하면 CIFAR10을 매우 쉽게 로드할 수 있다. 

# PyTorch 라이브러리 가져오기.
import torch 

# Torchvision 라이브러리 가져오기.
import torchvision 

# Torchvision에서 이미지 변환에 일반적으로 사용되는 변환 모듈 가져오기.
import torchvision.transforms as transforms

   

 

   Torchvision 데이터셋의 Output(출력값)은 0~1범위의 PILImage 이미지들이다. 그것을 -1~1범위로 정규화된 Tensor로 변환한다.  //만약 윈도우에서 실행 중 BrokenPipeError 오류가 발생한다면, torch.utils.data.DataLoader()함수의 num_worker을 0으로 설정해보자.

# 데이터 전처리를 위한 transform 객체 생성.
transform = transforms.Compose(
    [transforms.ToTensor(), # 데이터를 PyTorch Tensor로 변환. 
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) # 정규화 수행.

# Batch 사이즈 결정.
batch_size = 4 

# Torchvision의 CIFAR10 데이터 불러와 학습데이터(trainset)에 저장.
trainset = torchvision.datasets.CIFAR10(root='./data', # 데이터가 저장될 경로.
										train=True, # 학습용 데이터를 가져옴.
                                        download=True, # 데이터를 인터넷에서 다운로드.
                                        transform=transform) # 전처리를 위한 transform 객체 적용.

# 위의 학습데이터를 불러와 trainloder 생성.
trainloader = torch.utils.data.DataLoader(trainset, # 불러올 데이터셋.
										  batch_size=batch_size, # 배차 크기.
                                          shuffle=True, # 매번 데이터셋을 섞어서 순서를 바꿈.
                                          num_workers=2) # 데이터를 불러올 때 사용할 프로세스 개수.

# 테스트 데이터셋 및 데이터로더 생성. 
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size,
                                         shuffle=False, num_workers=2)

# 클래스 배열 정의.
classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

실행 결과는 다음과 같다. 

 

 

 

다음은 학습 이미지들을 테스트로 보여주는 코드이다. 

# 데이터 시각화를 위한 라이브러리 import.
import matplotlib.pyplot as plt

#수치 계산을 위한 라이브러리 import.
import numpy as np

# 이미지를 보여주기 위한 함수.
def imshow(img):
    img = img / 2 + 0.5     # 이미지 역정규화.
    npimg = img.numpy() # 이미지를 numpy로 변환.
    plt.imshow(np.transpose(npimg, (1, 2, 0))) # 재배열된 이미지배열을 화면에 표시.
    plt.show() # 재배열된 이미지를 실제 화면에 출력.

# 무작위로 선택된 학습 이미지와 라벨 가져오기.
dataiter = iter(trainloader)
images, labels = next(dataiter)

# 이미지를 그리드 형태로 만들고 이 그리드를 imshow함수를 사용하여 시각화.
imshow(torchvision.utils.make_grid(images))

# 해당 이미지의 클래스 라벨 출력.
print(' '.join(f'{classes[labels[j]]:5s}' for j in range(batch_size)))

 

실행결과는 다음과 같습니다.

 

 

 

 

 

 

 

2. Convolutional Neural Network 정의.

 

    이전의 Neural Networks 섹션에서 신경망을 복사하고, 정의된 대로 1-채널 이미지들을 가져가는 대신 3-채널 이미지들을 가져가도록 수정합니다.

# torch.nn 라이브러리를 사용하여 신경망 모델을 정의하는데 필요한 클래스와 함수 import.
import torch.nn as nn
import torch.nn.functional as F

# 신경망 모델을 정의하고 있는 클래스.
class Net(nn.Module):
	
    # 신경망 구조 정의 함수.
    def __init__(self): 
        super().__init__() # nn.Module의 생성자를 호출하여 신경망 모듈 초기화.
        
         # 3개의 입력 채널을 갖는 2차원 컨볼루션 레이어.6개의 출력 채널과 5x5 크기의 필터를 사용한다.
        self.conv1 = nn.Conv2d(3, 6, 5)
        
        # 2x2 크기의 윈도우를 사용하여 최대 풀링 레이어를 정의. 
        # 인접한 2x2 픽셀에서 가장 큰 값을 취하여 해상도를 절반으로 줄인다.
        self.pool = nn.MaxPool2d(2, 2)
        
        # 6개의 입력 채널과 16개의 출력 채널을 갖는 2차원 컨볼루션 레이어. 5x5 크기의 필터를 사용한다.
        self.conv2 = nn.Conv2d(6, 16, 5)
        
        # fully connected 레이어로, 이전 레이어에서 출력된 16x5x5 크기의 텐서를 120개의 뉴런으로 연결한다.
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        
        # 120개의 입력을 받아 84개의 출력값을 생성하는 fully connected 레이어.
        self.fc2 = nn.Linear(120, 84)
        
        # 84개의 입력을 받아 10개의 출력값을 생성하는 fully connnected 레이어.
        self.fc3 = nn.Linear(84, 10)
        

	# 위의 함수에서 정의된 레이어들을 차례로 연결하여 신경망 모델 형성.
    # Net 클래스의 객체에 입력값으로 넣을 때 호출되는 함수로, 네트워크의 순전파 과정을 구현한다.
    def forward(self, x):
    
    	# 입력 이미지를 컨볼루션 레이어에 통과시키고, 
        # relu 활성화 함수 적용 후 pool 레이어에 통과시킴.
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        
        x = torch.flatten(x, 1) # 배치를 제외한 모든 치수 평탄화. 이미지를 1차원으로 펼침.
        
        # fully connected 레이어를 순서대로 통과시키며, 각 출력값에 relu활성화 함수 적용.
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        
        # 마지막으로 fc3 레이어의 출력값을 반환.
        x = self.fc3(x)
        return x



net = Net()

 

 

 

 

 

 

 

3. 손실 함수 및 최적화 도구 정의.

 

   운동량이 있는 분류 크로스-엔트로피 손실과 SGD(확률적 경사 하강법)를 사용해 보자. 

# 최적화 알고리즘을 제공하는 optim 모듈 가져옴.
import torch.optim as optim

criterion = nn.CrossEntropyLoss() # 모델 출력값과 실제 레이블 간의 오차를 계산하여 학습을 이끌어내는 손실함수. 

# 경사하강법(SGD) 옵티마이저로, 모델 파라미터의 업데이트를 수행한다.
# net.parameters() : 모델의 학습 가능한 파라미터들을 반환.
# lr : learning rate(학습률)을 나타낸다.
# momentum : 관성을 나타내며, 학습의 안정성과 수렴 속도를 높이는데 도움을 준다. 
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

 

 

 

 

 

 

 

4. 네트워크 훈련시키기.

  

   데이터 반복기를 루프하여 입력을 네트워크에 공급하고 최적화해야 한다. 

 

for epoch in range(2):  # 데이터셋을 2번 반복해서 학습한다.

    running_loss = 0.0 # 루프가 실행될 때마다 손실을 0으로 초기화한다.
    
    # trainloader에서 데이터를 가져와서 루프를 실행한다.
    # enumerate함수를 사용하여 인덱스와 데이터를 함께 가져온다.
    for i, data in enumerate(trainloader, 0):
    
        # input값을 가져온다. 데이터는 [inputs, labels]의 리스트다.
        inputs, labels = data

        # 매개변수의 gradients를 0으로 설정한다.
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs) # 네트워크에 입력값을 넣고 출력을 계산.
        loss = criterion(outputs, labels) # 모델이 예측한 출력과 정답인 labels을 비교하여 손실을 계산.
        loss.backward() # 손실에 대한 gradients를 계산.
        optimizer.step() # 계산된 gradients로 optimizer를 업데이트하여 모델을 학습.

        # 통계를 출력.
        running_loss += loss.item() # 현재 배치에서 계산된 손실을 더해준다.
        if i % 2000 == 1999:    # 2000개의 미니 배치를 출력한다.
        	#2000개의 배치가 처리될 때마다 running_loss를 출력한다.
            print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
            running_loss = 0.0 # running_loss를 다시 0으로 초기화.

print('Finished Training')

실행결과는 다음과 같다.

# 학습된 모델 세이브
PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)

 

 

 

 

 

 

 

 

5. 테스트 데이터에서 네트워크 테스트.

  

   우리는 훈련 데이터셋에 대한 2개의 패스에 대해 네트워크를 훈련했다. 하지만 우리는 네트워크가 배운 것이 있는지 확인해볼 필요가 있다. 이를 확인하기 위해서 우리는 신경망이 출력하는 클래스 레이블을 예측하고 실제와 대조하여 이를 확인할 것이다. 예측이 올바르면 올바른 예측 목록에 표본을 추가한다. 

  먼저, 익숙해지기 위해 테스트셋의 이미지를 표시해보자.

 

# testloader를 이터레이터로 변환하여 dataiter에 할당.
dataiter = iter(testloader)

# dataiter에서 첫 번째 배치를 가져와 images와 labels로 분할하여 할당.
images, labels = next(dataiter)

# 이미지를 격자로 만든 후 imshow함수로 출력.
imshow(torchvision.utils.make_grid(images))

# labels 리스트에서 4개의 값만 출력하며, 해당하는 클래스 이름을 classes 리스트에서 찾아서 출력.
# 5개의 문자열 폭을 갖도록 포맷팅.
print('GroundTruth: ', ' '.join(f'{classes[labels[j]]:5s}' for j in range(4)))

 실행결과는 다음과 같다.

 

 

다음으로 저장된 모델로 다시 로드해보자.(여기서는 모델을 저장하고 다시 로드할 필요가 없다. 이 작업은 방법을 설명하기 위해 수행한 것임.)

net = Net() # Net 클래스의 인스턴스 생성.

# 사전 학습된 모델 파라미터를 로드하여 이전에 학습한 모델의 가중치를 복원.
net.load_state_dict(torch.load(PATH))

실행결과는 다음과 같다.

 

 

아웃풋은 10개의 클래스에 대한 에너지이다. 클래스에 대한 에너지가 높을수록 네트워크는 이미지가 특정 클래스의 것이라고 판단한다. 그럼, 가장 높은 에너지의 지수를 알아보자. 

# 텐서의 각 행마다 가장 큰 값과 그 인덱스를 반환한다. 
_, predicted = torch.max(outputs, 1)

# 4개의 이미지에 대한 예측 결과를 문자열로 표현하여 출력. 
print('Predicted: ', ' '.join(f'{classes[predicted[j]]:5s}'
                              for j in range(4)))

실행결과는 다음과 같다.

 

 

 

다음은 네트워크가 전체 데이터셋에서 어떻게 작동하는지 살펴보자.

correct = 0
total = 0
# 훈련중이 아니기 때문에 출력에 대한 gradients를 계산할 필요가 없다.
with torch.no_grad(): # gradients 계산 안함.
    for data in testloader:
        images, labels = data
        # 네트워크를 통해 이미지를 실행하여 아웃풋을 계산.
        outputs = net(images)
        # 가장 높은 에너지를 가진 클래스는 우리가 예측으로 선택한 것이다.
        _, predicted = torch.max(outputs.data, 1) # 각 이미지에서 가장 높은 값을 가진 클래스를 선택.
        total += labels.size(0) 
        correct += (predicted == labels).sum().item()

# 전체 테스트 데이터셋에 대한 정확도를 계산하여 출력.
print(f'Accuracy of the network on the 10000 test images: {100 * correct // total} %')

실행결과는 다음과 같다.

10%의 정확도인 확률보다 훨씬 좋아보인다. 네트워크가 뭔가를 배운 것처럼 보인다. 클래스들이 잘 수행한 작업과 잘 수행하지 못한 작업은 무엇일까?

 

# 각 클래스에 대한 예측을 카운트할 준비.
correct_pred = {classname: 0 for classname in classes}
total_pred = {classname: 0 for classname in classes}

# 위의 코드와 마찬가지로 gradients가 필요하지 않음.
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predictions = torch.max(outputs, 1)
        # 각 클래스에 대한 정확한 예측 수집.
        for label, prediction in zip(labels, predictions):
            if label == prediction:
                correct_pred[classes[label]] += 1
            total_pred[classes[label]] += 1


# 각 클래스의 정확도를 print한다.
for classname, correct_count in correct_pred.items():
    accuracy = 100 * float(correct_count) / total_pred[classname]
    print(f'Accuracy for class: {classname:5s} is {accuracy:.1f} %')

실행결과는 다음과 같다.