[딥러닝 홀로서기] Lab15. How to Write Well-Organized DL Code from Scratch

YJ·2024년 11월 2일
0

딥러닝 홀로서기

목록 보기
15/24
post-thumbnail

이 블로그글은 2019년 조재영(Kevin Jo), 김승수(SeungSu Kim)님의 딥러닝 홀로서기 세미나를 수강하고 작성한 글임을 밝힙니다.

💡 도움이 되셨다면 ♡와 팔로우 부탁드려요! 미리 감사합니다.

Github 링크 : 실습 코드 링크

CIFAR 10 이미지 분류하기

💡 목표 CIFAR10 이미지를 분류하는 MLP 개발

CIFAR 10이란?

  • 대표적인 이미지 분류 데이터셋
  • 총 6만장의 컬러이미지(RGB)로 구성된다.
  • 10개의 카테고리가 존재한다.
    • 비행기 (airplane)
    • 자동차 (automobile)
    • 새 (bird)
    • 고양이 (cat)
    • 사슴 (deer)
    • 개 (dog)
    • 개구리 (frog)
    • 말 (horse)
    • 배 (ship)
    • 트럭 (truck)
  • 각 클래스마다 6,000장의 이미지가 포함되어 있다.
  • 데이터셋은 학습용과 테스트용으로 나뉘어져 있다.

실습

라이브러리 불러오기

import torch
import torchvision
import torchvision.transforms as transforms
  • torchvision 라이브러리를 사용하여 CIFAR-10 데이터를 다운로드한다.
  • torchvision.transforms를 이용해 불러온 이미지 데이터를 전처리하여 학습에 적합한 형태로 변환한다.

데이터 다운로드 및 전처리

전처리 정의

transform = transforms.Compose(
    {transforms.ToTensor(), 
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))}
)
  • ToTensor() : 이미지를 Tensor로 변환 (0~1 범위로 스케일 조정)
  • Normalize() : 각 RGB 채널을 평균 0.5로 shift하고, 표준편차 0.5로 나누어 (-1, 1) 범위로 변환

데이터셋 다운로드 및 분리

trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainset, valset = torch.utils.data.random_split(trainset, [40000, 10000])
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
  • torchvision.datasets.CIFAR10 : CIFAR-10 데이터셋을 다운로드하고 로드
  • torch.utils.data.random_split() : 데이터셋을 랜덤하게 두개로 나눔
    • 40,000개는 Train
    • 10,000개는 Validation

데이터 로더 생성


trainloader = torch.utils.data.DataLoader(trainset, batch_size = batch_size,
                                          shuffle = True, num_workers=2)

valloader = torch.utils.data.DataLoader(valset, batch_size = batch_size,
                                         shuffle = False, num_workers=2)

testloader = torch.utils.data.DataLoader(testset, batch_size = batch_size,
                                         shuffle=False, num_workers=2)
  • DataLoader : 모델 학습에 필요한 데이터를 미니배치 단위로 효율적으로 불러와주는 역할

불러온 데이터 확인하기

이미지 시각화

import matplotlib.pyplot as plt
import numpy as np

def imshow(img):
  img = img / 2 + 0.5
  npimg =img.numpy()
  plt.imshow(np.transpose(npimg, (1, 2, 0)))
  plt.show()

classes = ('plane', 'car', 'bird', 'cat', 'dear', 'dog', 'forg', 'horse', 'ship', 'truck')

dataiter = iter(trainloader)
images, labels = next(dataiter)

imshow(torchvision.utils.make_grid(images))

print(' '.join('%5s' % classes[labels[j]] for j in range(4))) # ground truth 출력

  • torchvision.utils.make_grid(images) : 배치 형식의 여러 이미지를 하나의 그리드로 배열

💡 Ground Truth란?
모델 학습이나 평가 시 정답 레이블을 의미한다.

데이터 타입과 shape 확인

print(type(images))
print(images.shape) 
  • 입력은 torch Tensor 객체임을 확인
  • shape는 [4, 3, 32, 32]로 순서대로 미니 배치 크기, RGB, 가로, 세로를 의미한다.
print(type(labels))
print(labels.shape)
print(labels)
  • 레이블 역시 torch Tensor 객체임을 확인
  • shape는 [4]로 미니배치 크기를 의미한다.
  • labels를 각 이미지가 속한 클래스의 인덱스 값을 나타낸다.

모델 생성

모델 정의

import torch.nn as nn

class MLP(nn.Module) :
  def __init__(self, in_dim, out_dim, hid_dim, n_layer, act):
    super(MLP, self).__init__()
    self.in_dim = in_dim
    self.out_dim = out_dim
    self.hid_dim = hid_dim
    self.n_layer = n_layer
    self.act = act

    self.fc_in = nn.Linear(self.in_dim, self.hid_dim)

    self.linears = nn.ModuleList(nn.Linear(self.hid_dim, self.hid_dim) for _  in range(self.n_layer-1) )

    self.fc_out = nn.Linear(self.hid_dim, self.out_dim)

    if self.act == 'relu':
      self.act = nn.ReLU()
    
  def forward(self, x):
    x = self.act(self.fc_in(x))
    for fc in self.linears:
      x = self.act(fc(x))
    x = self.fc_out(x)
    return x
  • 다층 퍼셉트론(MLP) 신경망을 정의한다.
  • __init__: 입력 차원, 출력 차원, 은닉층 차원, 레이어 수, 활성화 함수를 인자로 받아, 입력층, 은닉층, 출력층 레이어를 정의한다.
    • fc_in: 입력층을 은닉층으로 연결하는 첫 번째 레이어
    • linears: 은닉층들을 연결하는 레이어 목록 (n_layer에 따라 레이어 수 결정)
    • fc_out: 마지막 은닉층을 출력층으로 연결하는 레이어
    • act: 활성화 함수로 ReLU를 사용하도록 설정
  • forward: 입력 데이터 x가 각 레이어를 지나며 활성화 함수로 변환되고, 마지막 출력층에서 결과가 반환된다.

모델 객체 생성

model = MLP(3072, 10, 100, 4, 'relu')

Loss/Optimization 설정

import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
  • criterion: 손실 함수로 교차 엔트로피 손실(CrossEntropyLoss)을 사용한다.
  • optimizer: 확률적 경사 하강법(SGD) 최적화 알고리즘을 사용하여 모델의 가중치를 업데이트한다.

모델 학습

for epoch in range(2):
  running_loss = 0.0
  for i, data in enumerate(trainloader):
    inputs, labels = data
  
    inputs = inputs.view(-1, 3072) 

    optimizer.zero_grad()

    outputs = model(inputs)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()
  
    running_loss += loss.item()
    if i % 2000 == 1999:
      print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 2000))
      running_loss = 0.0

print('Finished Training')
  • inputs = inputs.view(-1, 3072)에서 입력 이미지를 1차원으로 변환해, MLP 모델의 입력 차원(3072)에 맞춰준다.

Validation 및 Test

Validation

correct = 0
total = 0
val_loss = 0
with torch.no_grad():
  for data in valloader:
    images, labels = data
    images = images.view(-1, 3072)
    outputs = model(images)

    loss = criterion(outputs, labels)
    val_loss += loss.item()

    _, predicted = torch.max(outputs.data, 1)
    total += labels.size(0)
    correct += (predicted == labels).sum().item()

  val_loss = val_loss / len(valloader)
  acc = 100 * correct / total
  
print(f'Accuracy of the network on the 10000 val images: {acc:.2f}% {val_loss:.2f}')
  • 검증 데이터셋에 대한 모델 평가를 수행
correct = 0
total = 0
with torch.no_grad():
  for data in testloader:
    images, labels = data
    images = images.view(-1, 3072)
    outputs = model(images)
    _, predicted = torch.max(outputs.data, 1)
    total += labels.size(0)
    correct += (predicted == labels).sum().item()

  acc = 100 * correct / total

print(f'Accuracy of the network on the 10000 test images: {acc}%')
  • 테스트 데이터셋에 대한 모델 평가를 수행

💡 with 구문이란?

  • 파이썬에서 특정 작업의 시작과 종료를 자동으로 관리해주는 구문
    • 예) 파일 처리 : with open(...) as file , 파일을 열고 블록이 끝나면 파일을 자동으로 닫아줌
    • 예) PyTorch 예측 과정: with torch.no_grad() , 기울기 계산을 임시로 꺼서 메모리를 절약하고 속도를 높여줌

Experiment로 모듈화하기

장점

  1. 실험 관리와 반복 실험의 편의성
    • 하이퍼파라미터 변경만으로 반복 실험이 가능해집니다. (argparse를 이용하여)
    • 실험 결과 저장, 그래프 작성 등을 자동화할 수 있습니다.
  2. 재사용성과 코드 가독성 증가
    • 실험 과정을 하나의 모듈로 만들어 코드 구조를 깔끔하게 유지한다.
    • 코드가 단순해져 전체적인 실험 흐름이 명확해지고 가독성이 높아지기 때문에, 팀 프로젝트나 협업 환경에서 특히 유용하다.

파라미터 설정 부분 (argparse이용)

import argparse

# seed 설정
seed = 123
np.random.seed(seed)
torch.manual_seed(seed)

parser = argparse.ArgumentParser()
args = parser.parse_args("")

args.n_layer = 5
args.in_dim = 3072
args.out_dim = 10
args.hid_dim = 100
args.act = 'relu'
args.lr = 0.001
args.mm = 0.9
args.epoch = 2
  • 코드 일일이 수정할 필요없이 실험 파라미터를 쉽게 조정

Seed란?

  • 난수 생성의 시작값으로, 같은 Seed 값을 사용하면 매번 동일한 난수 생성
    • 예) np.random.seed(seed) : Numpy 라이브러리에서 난수 생성 seed 고정
    • 예) torch.manual_seed(123) : PyTorch 라이브러리에서 난수 생성 seed 고정
  • 딥러닝 실험에서 결과 재현성을 보장하기 위해 사용

Experiment 함수 생성

def experiment(args):
  model = MLP(args.in_dim, args.out_dim, args.hid_dim, args.n_layer, args.act)
  model.cuda()

  criterion = nn.CrossEntropyLoss()
  optimizer = optim.SGD(model.parameters(), lr = args.lr, momentum = args.mm)
  
  ## ==== Train ==== ##
  for epoch in range(args.epoch):

    model.train()  # 학습 모드 설정

    running_loss = 0.0
    train_loss = 0.0
    for i, data in enumerate(trainloader):
      inputs, labels = data
      # reshape, 여기서 -1은 남아있는 몫을 채워줌
      inputs = inputs.view(-1, 3072) 

      inputs = inputs.cuda()
      labels = labels.cuda()

      optimizer.zero_grad()

      outputs = model(inputs)
      loss = criterion(outputs, labels)
      loss.backward()
      optimizer.step()
    
      running_loss += loss.item()
      train_loss += loss.item()
      if i % 2000 == 1999: # print every 2000 mmini-batches
        print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 2000))
        running_loss = 0.0
    
    # 에포크 종료 후 미니배치 개수로 나누기
    train_loss = train_loss / len(trainloader)

    ## ==== validation ==== ##
    model.eval()  # 평가 모드 설정

    correct = 0
    total = 0
    val_loss = 0
    with torch.no_grad():
      for data in valloader:
        images, labels = data
        images = images.view(-1, 3072)

        images = images.cuda()
        labels = labels.cuda()

        outputs = model(images)
        loss = criterion(outputs, labels)
        val_loss += loss.item()

        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

      val_loss = val_loss / len(valloader)
      acc = 100 * correct / total
    
    print(f'Epoch {epoch}, Train Loss : {train_loss:.2f}, Val Loss : {val_loss:.2f}, Val Acc : {acc}')

  ## ==== Evaluation ==== ##
  model.eval()  # 테스트 평가 모드 설정

  correct = 0
  total = 0
  with torch.no_grad():
    for data in testloader:
      images, labels = data
      images = images.view(-1, 3072)

      images = images.cuda()
      labels = labels.cuda()
      
      outputs = model(images)
      _, predicted = torch.max(outputs.data, 1)
      total += labels.size(0)
      correct += (predicted == labels).sum().item()

    test_acc = 100 * correct / total

  print(f'Accuracy of the network on the 10000 test images: {test_acc}%')

  return train_loss, val_loss, train_loss, test_acc
  • 자주 수정하는 하이퍼파라미터를 args에 등록하여 함수 내부 코드 수정없이 여러 실험이 가능하다.

간단한 사용 예시

list_var1 = [4, 5, 6]
list_var2 = [50, 100, 150]

for var1 in list_var1:
    for var2 in list_var2:
        args.n_layer = var1
        args.hid_dim = var2
        result = experiment(args)
        print(result)
  • layer 수를 4, 5, 6으로 설정
  • hideen layer의 차원 수를 50, 100, 150으로 설정
  • 각 조합에 대해 experiment(args)를 실행하고, 결과를 출력한다.
profile
제 글이 유익하셨다면 ♡와 팔로우로 응원 부탁드립니다.

0개의 댓글

관련 채용 정보