David의 개발 이야기!

DNN을 활용한 MNIST 분류 모델 구현 본문

인공지능공부

DNN을 활용한 MNIST 분류 모델 구현

david.kim2028 2023. 8. 7. 22:31
반응형

DNN == Deep Neural Network

 

심층 신경망(Deep Neural Network, DNN)은 입력층(input layer)과 출력층(output layer) 사이에 여러 개의 은닉층(hidden layer)들로 이뤄진 인공신경망(Artificial Neural Network, ANN)이다.

 

이를 이용하여, 손글씨 숫자이미지를 분류하는 모델을 구현해보고자 한다.

 

1. 필요한 모듈 불러오기

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

from torchvision import datasets, transforms
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

-> CUDA 를 사용할수 있는 GPU가 있는지 확인하고, 사용가능하면 GPU 를 그렇지 않으면, CPU 를 사요하게 설정하는 코드

 

2. 데이터 불러오기

#MNIST 데이터겟 불러오기
train_dataset = datasets.MNIST(root="./mnist_data", train=True, transform=transforms.ToTensor(), download=True)
test_dataset = datasets.MNIST(root="./mnist_data", train=False, transform=transforms.ToTensor())

batch_size = 64

train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)

train dataset에 6만장, test dataset 에 1만장 있음을 확인할 수 있다. 

 

* train_loader, test_loader 생성하는 이유

-> 데이터셋을 작은 배치로 나누어 모델을 훈련시키기 위해 

 

3. 모델 구축

class Net(nn.Module):
  def __init__(self):
    super(Net, self).__init__()
    self.l1 = nn.Linear(784, 480)
    self.l2 = nn.Linear(480, 200)
    self.l3 = nn.Linear(200, 80)
    self.l4 = nn.Linear(80, 10)
  
  def forward(self, x):
    x = x.view(-1, 784)
    x = F.relu(self.l1(x))
    x = F.relu(self.l2(x))
    x = F.relu(self.l3(x))
    return self.l4(x)
model = Net().to(device)
print(model)

마지막 layer에서는 10개의 숫자중 하나로 분류하는 것임으로, 10이어야한다. 

 

4. 학습방법설정 및 성능평가

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.2)

def train(epoch):
  model.train()
  for batch_idx, (data, target) in enumerate(train_loader):
    data, target = data.to(device), target.to(device)
    optimizer.zero_grad()
    output = model(data)
    loss = criterion(output, target)
    loss.backward()
    optimizer.step() # 모델 가중치 업데이트
    if batch_idx % 100 == 0:
        print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.8f}'.format(
            epoch, batch_idx * len(data), len(train_loader.dataset),
            100 * batch_idx / len(train_loader), loss.data))

criterion 은, 분류모델임으로 CrossEntropy 를 통해 Loss를 평가하고, 

optimizer는, SGD를 사용한다.

 

momentum에 대한 설명

 

def test():
    model.eval()
    loss = 0
    correct = 0
    for data, target in test_loader:
        data, target = data.to(device), target.to(device)
        output = model(data)
        loss += criterion(output, target).data.item()
        pred = output.data.max(1, keepdim=True)[1]
        correct += pred.eq(target.data.view_as(pred)).cpu().sum()
    loss /= len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        loss, correct, len(test_loader.dataset),
        100 * correct / len(test_loader.dataset)))

model.eval() : 모델을 평가

loss 에 총 손실을 저장하고, correct 에 올바르게 분류된 샘플 수 를 저장 

model(data) : 현재 배치의 예측값을 얻음 

criterion(output, target) 을 사용하여 예측과 실제 레이블 사이의 손실을 계산하고, 총 손실에 더함

output.data.max(1, keepdim=True)[1] : 각 샘플의 예측된 클래스 인덱스를 찾음(최대 예측값 찾기)

pred.eq(target.data.view_as(pred)).cpu().sum() 을 사용하여 예측이 실제 레이블과 일치하는 횟수 셈(정확도 계산)

loss /= len(test_loader.dataset) : 평균 손실 계산

 

output.data.max(1, keepdim=True)[1] 설명 :

output: 모델로부터 얻은 예측 결과를 담은 텐서입니다. 만약 배치 크기가 64이고 10개의 클래스가 있다면, 이 텐서의 크기는 (64, 10)이 될 것입니다. 각 행은 10개의 클래스에 대한 확률 또는 점수를 나타냅니다.

output.data: 텐서에서 데이터 부분을 가져옵니다. 이는 예측 결과를 담고 있는 부분이며, 자동 미분 계산 그래프와 연결되지 않았습니다.

max(1, keepdim=True): 이 부분은 텐서의 두 번째 차원(인덱스 1)을 따라 최대 값을 찾습니다. 여기서는 각 샘플(행)에서 최대 점수를 가진 클래스를 찾는 것과 같습니다. keepdim=True는 반환된 최대값이 원래 텐서와 동일한 차원을 유지하도록 합니다. 이 연산은 값과 인덱스를 반환하는데, 값은 최대 점수이고 인덱스는 해당 점수의 위치, 즉 클래스 인덱스입니다.

[1]: 이 연산은 최대 연산에서 반환된 두 값 중에서 인덱스(두 번째 값)만을 선택합니다. 결국 이 코드는 모델이 각 샘플에 대해 예측한 클래스의 인덱스를 담은 텐서를 반환합니다.  이 코드 조각은 각 샘플에 대해 가장 확률(또는 점수)이 높은 클래스를 찾는 데 사용됩니다. 이것은 다중 클래스 분류 문제에서 일반적으로 사용되는 패턴입니다.

 

pred.eq(target.data.view_as(pred)).cpu().sum() 설명 :

 

target.data: 실제 타깃 레이블을 담은 텐서입니다. .data는 자동 미분 계산 그래프와 연결되지 않은 순수한 데이터 부분을 가져옵니다.

view_as(pred): view_as 메서드는 pred와 동일한 형상을 가진 텐서로 target.data를 재구성합니다. 여기서 pred는 예측된 클래스 인덱스를 담은 텐서이므로, target.data도 동일한 형상을 가져야 비교할 수 있습니다.

pred.eq(...): .eq 메서드는 입력 텐서와 pred를 요소별로 비교하여 같으면 True, 다르면 False인 불리언 텐서를 반환합니다. 여기서는 pred (예측된 클래스 인덱스)와 target.data.view_as(pred) (실제 클래스 인덱스)를 비교합니다.

cpu(): 결과 불리언 텐서를 CPU로 이동시킵니다. 이 코드는 GPU에서 실행되고 있을 수 있으므로, .cpu() 호출은 연산을 CPU로 전환하여 이후의 연산을 수행합니다.

sum(): 불리언 텐서에서 True값의 개수를 세기 위해 합계를 계산합니다. 이 값은 현재 배치에서 올바르게 분류된 샘플 수를 나타냅니다. 종합하면, 이 코드는 현재 배치의 예측된 클래스와 실제 클래스를 비교하여 올바르게 분류된 샘플 수를 반환합니다. 이 정보는 전체 테스트 데이터셋을 반복하면서 정확도를 계산하는 데 사용됩니다.

 

for epoch in range(1, 11):
  train(epoch)
  test()

 

5. 제대로 되는지 확인해보기

import numpy as np
import matplotlib.pyplot as plt

image_data = test_dataset[0][0].to(device)
image_label = test_dataset[0][1]
print('숫자 이미지 X의 크기:', image_data.size())
print('숫자 이미지 X의 레이블:', image_label)
print(model(image_data))
plt.imshow(image_data.cpu().numpy()[0], cmap='gray')

반응형
Comments