-
ResNet-101 Classification 실습 연습deep learning/computer vision 2024. 2. 11. 23:53
Computer Vision에서의 classification model들에 대한 outline을 검토하면서, 대표적인 모델인 ResNet-101을 활용하여 CIFAR10 데이터셋에 대한 기본적인 classification 모델을 실습해보고자 한다.
PyTorch를 활용하여 Classfication 학습 및 테스트 모델을 만들어보면서,
- 모델 구축에 사용되는 PyTorch 문법을 연습해보고,
- Classification Model의 전체적인 Architecture를 학습하고,
- 주요 Hyper-Parameter들에 대한 조정을 통한 성능 변화 여부 검토,
- Loss Function 및 Optimizer에 대한 분석을 진행해보려고 한다.
Classification Model Architecture는 크게 6단계로 구분하여 진행하였는데,
① 주요 Module importing, ② Data Transform, ③ Dataset 및 DataLoader 생성, ④ Model 생성 및 Hyper-parameters 설정 (중간중간 설정하였다), ⑤ Model Train과 Validation 진행, 마지막으로 ⑥ Custom Dataset을 활용한 Test를 진행해보았다.
코드 구현의 각 부문에서 헷갈리거나 잘 몰랐던 내용들은 주석을 통해 각각 표시해두었다
Module Import
import os, sys, glob, csv, cv2, tqdm from typing import Tuple, List, Dict import numpy as np from PIL import Image # 이미지 처리를 위한 라이브러리 import matplotlib.pyplot as plt # torch library # : Tensor와 같은 다차원 배열을 조작하는 기본 기능 # : 신경망 모델의 정의와 학습, 최적화 알고리즘을 포함한 PyTorch의 핵심 모듈 import torch from torch import Tensor import torch.nn as nn import torch.nn.functional as F from torch.utils.data import Dataset, DataLoader from torch.utils.data.sampler import SubsetRandomSampler # PyTorch의 데이터 로딩 및 처리를 위한 클래스 및 함수 # torchvision library # : PyTorch에서 이미지 및 비디오 데이터셋 및 전처리를 위한 라이브러리 # : CV 작업에 특화 (이미지 분류 모델들의 사전 학습된 가중치도 있음) import torchvision from torchvision import transforms, models import torch.optim as optim
Data Transform
# 데이터 전처리 형식 규정 train_transform = transforms.Compose( [ transforms.RandomHorizontalFlip(), # 훈련에 있어 Segmentation 기법 사용 transforms.ToTensor(), transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) ] ) valid_transform = transforms.Compose( [ transforms.ToTensor(), transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) ] )
- ToTensor() 메서드를 통해 0과 1사이의 정규분포를 갖는 텐서 값으로의 변환과 스케일링을 동시에 수행하지만, Normalize() 메서드를 통해 -1과 1사이로 스케일링을 추가로 진행
- Normalize()를 통해 [0, 1]의 범위값이 아닌 [-1, 1]로 스케일링 했을 때의 주요 장점은 다음과 같다.
- Zero-centered input
- 데이터가 평균이 0인 중앙에 위치하게 됨에 따라, 학습 과정에서 가중치 업데이트가 안정적으로 이루어짐 (이 장점은 [0, 1]로의 스케일링을 통해서도 얻을 수 있는 장점
- 기울기 편향 극복
- 0을 기준으로 좌우 대칭인 구조(Symmetry)를 갖고 있으며, 역전파 결과 output이 음수값이 될 수 있어 이동 편향에 따른 기울기 편향을 상대적으로 극복할 수 있음 ([0, 1]의 경우 항상 양수값을 가짐으로, 이동 편향이 발생)
- 표현 가능한 attribute 갯수가 더 다양
- [0, 1]의 범위를 [-1, 1]로 확장하여 표현할 수 있다는 점에서 더 다양한 attribute를 표현할 수 있고, 이는 연쇄적인 gradient의 계산에 있어 vanishing 문제를 극복할 수 있음
- mode collapse 문제의 상대적 해결이 가능
- 이를 통한 강건한 학습(Robustness)이 가능
- Zero-centered input
Dataset 및 DataLoader 생성
Dataset Downloading
# Hyper-Parameter batch_size = 32 val_size = 0.2 # Download CIFAR10 and define Train and Validation Dataset train_dataset = torchvision.datasets.CIFAR10(root=r'.\data', train=True, download=True, transform=train_transform) # 50000의 (3, 32, 32) 데이터와 labels valid_dataset = torchvision.datasets.CIFAR10(root=r'.\data', train=True, download=True, transform=valid_transform) # 클래스 정의 : 10개의 클래스 classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
Train DataLoader와 Validation DataLoader의 생성
# validation data를 만들기 위해, split 기준선 만드는 작업 num_train = len(train_dataset) indices = list(range(num_train)) # train_dataset의 인덱스를 만들기 : 50000개의 인덱스 리스트 split = int(np.floor(val_size * num_train)) # np.floor : 배열의 각 요소를 내림하여 새로운 배열을 생성 train_idx, val_idx = indices[split:], indices[:split] train_sampler = SubsetRandomSampler(train_idx) valid_sampler = SubsetRandomSampler(val_idx) # SubsetRandomSampler : Samples elements randomly from a given list of indices # DataLoader의 역할 : Download한 데이터를 불러오는 역할로, batch size, workers, shuffle 여부 등을 지정하여 load하는 역할 train_loader = DataLoader(train_dataset, batch_size=batch_size, sampler=train_sampler, num_workers=2) valid_loader = DataLoader(valid_dataset, batch_size=batch_size, sampler=valid_sampler, num_workers=2) # num_workers : 데이터를 로드할 때 사용할 병렬로드(worker)의 개수를 지정 (데이터 로드 속도 향상 가능)
- SubsetRandomSampler의 역할은 DataLoader에서 data를 batch 단위로 loading할 때 각 sampler에 할당된 index를 무작위적으로 sampling하기 위한 것으로, shuffle의 역할을 하는 것과 동일
- 위 코드의 문제점은,
- train_idx와 val_idx가 이미 편향적으로 추출되고 있다는 점에서, overfitting이 발생할 여지가 있다는 것이다.
- 이를 해결하기 위해서는 다음과 같은 코드를 추가하는 것이 좋아 보인다.
from sklearn.model_selection import train_test_split
train_idx, val_idx = train_test_split(indices, test_size=val_size, random_state=42)
train_sampler = SubsetRandomSampler(train_idx)
val_sampler = SubsetRandomSampler(val_idx)Test DataLoader 생성
# Test Data set test_dataset = torchvision.datasets.CIFAR10(root=r'.\data', train=False, download=True, transform=valid_transform) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=2)
데이터를 시각화하여 보고 싶을 때는 다음과 같이 시각화해볼 수 있을 것 같다.
# 이미지 데이터 시각화 def imshow(img): img = img / 2 + 0.5 # 모델 학습을 위해 normalize한 데이터를 복원하기 위해 unnormalize 작업 수행 npimg = img.numpy() plt.imshow(np.transpose(npimg, (1, 2, 0))) # (1, 2, 0)의 순서로 차원을 변경 # PyTorch의 경우 (channel, width, height)로 되어있기 때문에 이를 변경경 plt.show() # 학습 이미지 얻기 dataiter = iter(train_loader) # train_loader에서 iterator 생성 (iterator로의 변환) images, labels = next(dataiter) # 다음 미니배치를 가져오는 함수 # 이미지 출력 : 하나의 grid로 이미지 출력 imshow(torchvision.utils.make_grid(images)) # 라벨 프린트 print(' '.join(f'{classes[labels[j]]:5s}' for j in range(batch_size)))
Custom Dataset 및 DataLoader 생성
# Custom Dataset Class 만들기 - test할 때 사용 class CUSTOMDataset(Dataset): def __init__(self, mode: str = 'test', transforms: transforms = None): # mode : str = 'test' 데이터셋의 모드를 지정하는 것으로, CUSTOMDataset은 test 목적으로 사용되는 데이터임을 명시 # 이때 transform은 별도로 명시하지 않는 한 데이터 변환을 수행하지 않음을 의미 self.mode = mode self.transforms = transforms self.images = [] # 데이터셋의 이미지를 저장할 리스트 self.labels = [] # 데이터셋의 라벨을 저장할 리스트 for folder in os.listdir(r".\data\custom_dataset\custom_dataset"): files = os.path.join(r".\datacustom_dataset\custom_dataset", folder) if os.path.isdir(files): files_path = os.listdir(files) for file in files_path: if file == '._.DS_Store': continue self.images.append(os.path.join(files, file)) self.labels.append(classes.index(folder)) def __len__(self) : return len(self.labels) def __getitem__(self, index: int) -> Tuple[Tensor]: image = image.open(self.images[index]).convert('RBG') if self.transfroms is not None: image = self.transfroms(image) image = np.array(image) label = self.labels[index] return image, label # Custom Dataset 형성과 Dataloader 생성 custom_dataset = CUSTOMDataset('test', transforms=valid_transform) custom_loader = DataLoader(dataset=custom_dataset, batch_size=batch_size, shuffle=False, num_workers=2)
- Custom dataset의 경우 다운로드를 통해 확보된 dataset과는 별개로, 추가적인 test를 진행하기 위해 생성
Model Creation
# PyTorch를 활용한 모델 로드 : ResNet-101 모델 사용 model = torch.hub.load('pytorch/vision:v0.10.0', 'resnet101', pretrained=True) model
- torch.hub.load() 함수를 사용하면 특정 도메인의 다양한 모델을 사용할 수 있다.
- 이번 실습에서는 ResNet-101을 사용하며 ImageNet에서 사용된 학습 가중치를 활용한다.
- ResNet-101 모델의 output shape은 (batch_size, 1000)으로 나오는데, 이는 1개의 mini_batch 내 data_points들에 대해서 1000개의 features를 갖고 있으며, 각 feature들은 해당 feature가 나올 확률분포의 값을 나타내고 있다.
- model 입력시 전체 layer의 갯수와 kernel의 크기, 배치 순서, input size 등 layers의 전반적인 정보들이 나오기 때문에 자세한 architecture를 파악할 수 있다.
# model의 hyper-parameter 정의 learning_rate = 0.001 momentum = 0.9 epochs = 10 best_acc = 0.0 # 목적함수와 optimizer 정의 criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr = learning_rate, momentum=momentum)
- ADAM을 활용하면 성능이 더 좋게 나올 것으로 예상되나, 가장 기본적인 SGD를 적용해보았다.
- 목적 손실함수를 CrossEntropyLoss로 사용한 이유는..? 조금더 공부해보고 채우려고 한다.
Train, Validation, Test 과정 Creation
# train function : forward and backward def train(epoch): train_loss = 0.0 # 초기 loss 값 model.train() for i, data in enumerate(tqdm.tqdm(train_loader), 0): inputs, labels = data[0], data[1] # parameter gradients를 초기화 : 새로운 batch에 대한 gradient를 계산하기 전에 계산된 gradient를 초기화 optimizer.zero_grad() ## forward propagation step : 연산 수행 # 입력에 대한 model의 출력 생성 outputs = model(inputs) # 손실함수 계산 및 gradient update loss = criterion(outputs, labels) loss.backward() # 역전파 실시 optimizer.step() # 각 step 별로 최적화 알고리즘 실시 - parameter update train_loss += loss.item() # 각 에폭에서 계산된 loss를 .item() method를 통해 python scalar 값으로 반환 # epoch 단위로 loss를 계산하여 누적하는 방식 return train_loss
# validation function : forward def valid(): # validation은 train과 달리 반복학습을 시키는 것이 아니므로 epoch이 불필요 val_loss = 0.0 val_accuracy = 0.0 # 해당 context에 포함된 블록에서는 gradient 계산 혹은 tensor update가 진행되지 않음 # gradient와 tensor update가 불필요한 경우에 with 구문과 함께 사용 with torch.no_grad(): model.eval() for i, data in enumerate(tqdm.tqdm(valid_loader), 0): inputs, labels = data[0], data[1] ## forward propagation step : 연산 수행 # 입력에 대한 model의 출력 생성 outputs = model(inputs) # 손실함수 계산 loss = criterion(outputs, labels) val_loss += loss.item() # accuracy 계산을 위한 예측 라벨 # output = values, indices (최대값과 해당 index를 반환 / dim = 최대값을 찾을 차원) # model을 통과한 output은 (batch_size, 1000)의 shape를 갖고 있고, # torch.max()는 1번째 index에 해당하는 datapoints 들에 대해서 가장 큰 값의 index를 반환 _, predicted = torch.max(input=outputs, dim=1) # 예측 값과 정답값이 같은 횟수를 모두 더해 val_accuracy 변수에 할당 val_accuracy += (predicted == labels).sum().item() return val_loss, val_accuracy
# test function : forward def test(test_loader): correct, total = 0, 0 correct_class = {classname : 0 for classname in classes} total_class = {classname : 0 for classname in classes} model.eval() with torch.no_grad(): for i, data in enumerate(tqdm.tqdm(test_loader), 0): inputs, labels = data[0], data[1] outputs = model(inputs) _, predicted = torch.max(input=outputs, dim=1) total += labels.size(0) # .size() : 텐서의 크기를 나타내는 method # ()안의 수는 텐서의 크기 index를 입력하는 것 correct += (predicted == labels).sum().item() for label, prediction in zip(labels, predicted): if label == prediction: correct_class[classes[label]] += 1 total_class[classes[label]] += 1 print(f"Accuracy of the network on the 10000 test images: {100 * correct // total} %") for classname, correct_count in correct_class.items(): if total_class[classname] == 0: continue accuracy = 100 * float(correct_count) / total_class[classname] print(f"Accuracy for class : {classname:5s} is {accuracy:.1f} %")
Model Execution : Train and Validation
# 모델 저장 경로 정의 model_path = r'.\cifar_resnet101.pth' for epoch in range(epochs): # 학습 메소드 실행 train_loss = train(epoch) print(f'[{epoch + 1}] loss: {train_loss / len(train_loader):.3f}') # 검증 메소드 실행 val_loss, val_acc = valid() vaild_acc = val_acc / (len(valid_loader)*batch_size) print(f'[{epoch + 1}] loss: {val_loss / len(valid_loader):.3f} acc: {vaild_acc:.3f}') # 정확도가 기존 베스트를 갱신할 경우 모델 저장 if vaild_acc >= best_acc: best_acc = vaild_acc torch.save(model.state_dict(), model_path) print('Finished Training')
- 결과는 아래와 같은 형식으로 나온다
# evaluate custom dataset model.load_state_dict(torch.load(model_path)) # torch.load(model_path) : 지정된 경로에 저장된 모델의 상태 사전을 불러오기 # model.load_state_dict : 모델에 불러온 상태 사전을 적용 test(custom_loader) # evaluate test dataset model.load_state_dict(torch.load(model_path)) test(test_loader)
'deep learning > computer vision' 카테고리의 다른 글
Classficiation Model Outline (0) 2024.02.10 Classification Evaluation Metrics (1) 2024.02.09