본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 인공지능 고급-시각 강의의 CNN알고리즘 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.
이전 포스트 인공지능 고급(시각) 강의 복습 - 21. 주요 CNN알고리즘 구현 : (1) Wide ResNet(WRN) 모델
에서 WRN(Wide Residual Networks)모델을 공부했고 이전에 작성한 포스트처럼
맨땅에 학습 / 검증 / 실행 / 평가를 꾸준이 실행했고
각 모델에 대한 성능평가와 추론은 어느정도 한 듯 하다.
이제는 다른이들이 해당 모델을 사전학습
한 자료를 가져와서 이를 내가 하는 작업에 맞추는
Downstream task을 수행하고자 한다.
이 Downstream task 과정을 그림으로 표현하려 하니 이것저것 달라붙어야 하는 절차가 많아서 조금 복잡하긴 한데 정리를 하면 아래와 같다.
1) 사전에 학습시킬 모델(Target_model)을 선정한 뒤 이를 대용량의 데이터 셋(Huge Dataset)과 강력한 성능을 내는 워크스테이션(Super Computing Trainging Machine)을 통해 학습을 수행한다.
2) 위 과정을 통해 학습이 완료된 모델(Pre-trained model)을 관리 할 수 잇는 클라우드 플랫폼 torchvision
, github
, keras
등에 업로드 한다.
3) 사용자가 Pre-trained model을 다운로드 한 뒤 본인이 수행하고자 하는 작업(Task)에 적합한 데이터셋(Fine-turning dataset)으로 재 훈련(Re-training)을 수행하여 작업 목표를 달성할 수 있는 API를 생성한다.
위 작업을 수행하면서 전이학습
, 미세조정
이라는 개념이 등장하니 이것은 코드실습을 하는 과정에서 설명을 진행하도록 하겠다.
위 학습이 완료된 모델(Pre-trained model)은 인터넷상에서 클라우드 서비스 시스템인 GitHub
에서 학습이 완료된 모델을 찾아서 다운로드 할 수도 있지만
유명한 딥러닝 네트워크의 경우 Torchvision
, Keras
등의 라이브러리를 통해 손쉽게 다운로드가 가능하다.
이 중 Pytorch와 연계되어 있는 Torchvision
라이브러리에서 관리하고 있는 학습이 완료된 모델(Pre-trained model)을 다운로드 받도록 하자
https://pytorch.org/vision/0.8/models.html
해당 웹 페이지에 접속하면 Torchvision
라이브러리에서 다운로드 받을 수 있는 Pre-trained model은 총 12종이 있으며, 각 종별로 파생 네트워크가 존재하니 실제로는 더 많은 Pre-trained model을 제공하고 있다.
라고 보면 된다.
이 중 이전 포스트 인공지능 고급(시각) 강의 복습 - 21. 주요 CNN알고리즘 구현 : (1) Wide ResNet(WRN) 모델
에서 다룬 WRN을 기반으로 실습을 진행하며, 추가자료로 VGG같이 곁들여서 설명하겠다.
우선 WRN에 속하는 Wide ResNet
항목을 클릭하여
사용 방법을 확인하자
Torchvision
라이브러리에서 제공되는 WRN계열 네트워크는 wide_resnet50_2
, wide_resnet101_2
2가지이고
주요 인자는 pretrained만 보면 된다.
이 인자를 True
= 학습이 완료된 모델로 다운로드
False
= 학습이 안된 모델만 다운로드
이렇게 두가지로 나누어 볼 수 있다.
이전 포스트에서
#학습 완료된 모델 저장하기
MODEL_NAME='Inception_resenet_v2'
torch.save(model.state_dict(), f'{MODEL_NAME}.pth')
이런식으로 학습이 완료된 모델은 파라미터를 *.pth
로 저장하는데
이 파일을 빼고 모델만 다운받는 것이 pretrained=False
옵션인 것이다.
그럼 pretrained=True
옵션을 살펴보면
ImageNet 데이터셋으로 학습을 시켰다는데
이거에 대해서 잠깐 알아보고 넘어가자
홈페이지 글씨가 작아서 잘 안보이긴 하는데
매년 대회를 열고, 대회용 데이터셋을 업로드 하는데
Training용 : 120만장
Validation용 : 5만장
Test용 : 10만장
이렇게 구성되고 구별해야 할 객체 종류(class)는 1000종이다.
이 정도라면 충분한 대용량의 데이터 셋(Huge Dataset)으로 볼 수 있겠다.
그리고 torchvision
의 Pre-trained model을 사용하려면
입력 요구사항을 확인해야 한다.
이 입력요구사항을 확인하려면 해당 모델이 어떠한 방식으로 학습이 진행되었는지를 확인하자
입력 요구사항에 관한 문서를 확인하면
반복적으로 진행했던 이미지의 전처리 과정이 코드로 구현되어 있는데
아무튼 결론은
input_size=(3, 224, 224)
이렇게 입력해야 한다.
여기까지 사전학습의 개요에 대해 설명했으니 코드를 확인하자
from torchvision import models
# 사전 학습된 WRN_50_2 모델 불러오기
WRN50_2 = models.wide_resnet50_2(pretrained=True)
불러온 모델을
from torchsummary import summary #설계한 모델의 요약본 출력 모듈
summary(WRN50_2, input_size=(3, 224, 224), device='cpu')
을 통해서도 구조를 확인해 볼 수 있지만
이렇게 불러온 모델에 대해 레이어 별로 접근을 해야하니
print(WRN50_2)
으로 모델의 구조를 확인하는것을 권장한다
해당 모델을 print하면 딱 봐도 머리가 아파오는데
여기서 실습을 해보자
출력된 레이어 정보에서
layer1의 0번째블록 내 conv3레이어의 파라미터를 출력하고자 한다면
아래의 코드로 작성하면 된다.
conv3_parameters = WRN50_2.layer1[0].conv3.parameters()
이 코드 실습을 하는 이유는
사전학습
, 미세조정
과정을 수행하는데 위 코드의 응용이 꼭 이뤄지기 때문이다.
아무튼 학습이 완료된 모델(Pre-trained model)을 다운받았고
구조도 살펴봤으니
다음챕터로 넘어가자
Pre-trained model다운받은 이유는
Downstream task과정을 수행하는데 이를 써먹으려고 다운을 받은 것이다.
그러면 내가 할 일이 뭔지 정의가 되어 있어야 할 것이다.
이번 코드 실습에서 내가 할 일은
인공지능 고급(시각) 강의 복습 - 17. 이미지 데이터 증강
여기에서 사용했던
이 데이터셋의 강아지
, 고양이
를 분류하는 작업을 수행하고자 한다.
그러면 이번 챕터의 이름인
작업에 적합한 데이터셋(Fine-turning dataset)은
당연히 Kaggle cats and Dogs Dataset
이 됨을
알아차렸을 것이다.
아무튼 해당 포스트를 참조하면 다운로드 받은 데이터셋을
이런식으로 분류까지 완료를 했다.
그럼 분류가 완료된 데이터셋을 불러와서
커스텀 데이터셋을 만들어 보도록 하자.
cat_and_dog = ['Cat', 'Dog']
class CustomDataset(Dataset):
def __init__(self, root, class_list, mode=None, transform=None):
self.root = root
self.mode = mode
self.transform = transform
self.class_list = class_list
#이미지 경로 추출 후 저장변수
self.img_list = self._img_mode()
def _img_mode(self):
if self.mode is not None:
img_path = os.path.join(self.root, self.mode)
result_list = []
for subdir in self.class_list:
subdir_path = os.path.join(img_path, subdir)
if os.path.exists(subdir_path):
img_files = [f for f in os.listdir(subdir_path) if os.path.isfile(os.path.join(subdir_path, f))]
result_list.extend([os.path.join(subdir_path, f) for f in img_files])
else:
print(f"경로 검색 실패 : {subdir_path}")
return result_list
def __str__(self):
# img_list가 동작 후에도 해당 리스트가 비어있으면 오류로 간주
if not self.img_list:
return "오류가 났으니 확인하시오"
else:
return f"찾은 이미지 개수 : {len(self.img_list)}"
def __len__(self):
return len(self.img_list)
def __getitem__(self, idx):
img_path = self.img_list[idx]
image = Image.open(img_path).conver('RGB')
#라벨 정보 추출
for label, class_name in enumerate(self.class_list):
if class_name in img_path:
break
if self.transform:
image = self.transform(image)
label = torch.tensor(label, dtype=torch.int64)
return image, label
# Custom Dataset 생성
train_dataset = CustomDataset(root = '[/cats_and_dogs]폴더 경로',
mode = 'train', class_list = cat_and_dog)
test_dataset = CustomDataset(root = '[/cats_and_dogs]폴더 경로',
mode = 'val', class_list = cat_and_dog)
이렇게 매번 커스텀 데이터셋을 만드는건 데이터셋의 구조가
Pascal VOC
같이 복잡할때나 쓰는 것이고
지금처럼 깔끔하게 서브폴더로 잘 구분된것 까지는
좀 쉽게 갈 필요성이 있다.
https://pytorch.org/vision/0.8/datasets.html#datasetfolder
여기서는 torchvision
라이브러리에서 제공하는
사전에 설정된 datasetFolder
모듈을 사용하도록 하자
설명을 보면 알겠지만
Fine-turning dataset의 메인 경로만 입력하면
서브 폴더 -> 클래스 서브 폴더 순으로만 정리되어 있으면
알아서 잘 데이터셋을 만들어준다
라고 보면 된다.
따라서 코드로는 아래와 같이 치면 된다.
from torchvision import datasets
# dataset.ImageFolder을 사용한 데이터셋 생성
root = '[/cats_and_dogs]폴더 경로'
img_dataset = {} #서브폴더('train', 'val', test')를 딕셔너리 형태로 관리하려고 이렇게 변수를 초기화함
img_dataset['train'] = datasets.ImageFolder(os.path.join(root, 'train'))
img_dataset['val'] = datasets.ImageFolder(os.path.join(root, 'val'))
그러면 torchvision
의 datasets.ImageFolders
라이브러리가 잘 데이터셋을 분류했는지
확인도 해보자
import random
import matplotlib.pyplot as plt
# 임의의 데이터 하나 선택
train_idx = random.randint(0, len(img_dataset['train']) - 1)
val_idx = random.randint(0, len(img_dataset['val']) - 1)
train_image, train_label = img_dataset['train'][train_idx]
val_image, val_label = img_dataset['val'][val_idx]
# 클래스 이름 가져오기
class_names = img_dataset['train'].classes
# 서브플롯 생성
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
# Train 이미지 출력
axes[0].imshow(train_image)
axes[0].set_title(f'Train Label: {class_names[train_label]}, {train_label}')
axes[0].axis('off') # 축 숨기기
# Val 이미지 출력
axes[1].imshow(val_image)
axes[1].set_title(f'Val Label: {class_names[val_label]}, {val_label}')
axes[1].axis('off') # 축 숨기기
# 서브플롯 사이 간격 조정
plt.tight_layout()
plt.show()
훌륭하게 라벨링까지 잘 해준것을 알 수 있다.
해당 데이터셋의 정규화를 위한 mean
, std
를 구하는 코드를 구동하고
#훈련 데이터셋의 mean, std를 구하기
from torch.utils.data import DataLoader
from torchvision.transforms import v2
from tqdm import tqdm
transforamtion = v2.Compose([
v2.Resize((224,224)), #VGG19용 input_img로 리사이징
v2.ToImage(), # 이미지를 Tensor 자료형으로 변환
v2.ToDtype(torch.float32, scale=True)
#텐서 자료형변환 + [0~1]사이로 졍규화 해줘야함
])
img_dataset['train'].transform = transforamtion
dataloader = DataLoader(img_dataset['train'], batch_size=256, shuffle=False)
#GPU사용 가능여부 확인
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#이미지의 총 개수 및 채널별 합계를 저장할 변수 초기화
mean = torch.zeros(3).to(device)
std = torch.zeros(3).to(device)
nb_sample = 0
#데이터셋을 순회하며 mean, std 계산
for images, _ in tqdm(dataloader):
images = images.to(device)
batch_samples = images.size(0) # 배치 내 이미지 수
# 차원 형태 = (batch_size, channel(3), H, W)
images = images.view(batch_samples, images.size(1), -1)
mean += images.mean(2).sum(0)
std += images.std(2).sum(0)
nb_sample += batch_samples
mean /= nb_sample
std /= nb_sample
# mean과 std를 numpy 배열로 변환하여 소수점 4자리로 출력
mean_np = mean.cpu().numpy()
std_np = std.cpu().numpy()
print(f"Mean: {mean_np[0]:.4f}, {mean_np[1]:.4f}, {mean_np[2]:.4f}")
print(f"Std: {std_np[0]:.4f}, {std_np[1]:.4f}, {std_np[2]:.4f}")
100%|██████████| 69/69 [00:39<00:00, 1.76it/s]
Mean: 0.4884, 0.4554, 0.4172
Std: 0.2261, 0.2215, 0.2218
#정규화(mean, std)값을 구한 다음에는 GPU캐시 데이터 초기화를 해주자
torch.cuda.empty_cache()
from torchvision.transforms import v2
# 데이터 로더 생성하기
CAD_val = [[0.4884, 0.4554, 0.4172], [0.2261, 0.2215, 0.2218]]
transformation = v2.Compose([
v2.Resize((224, 224)), #VGG19 -> [224, 224]
v2.ToImage(), # 이미지를 Tensor 자료형으로 변환
v2.ToDtype(torch.float32, scale=True), #텐서 자료형을 [0~1]로 정규화
v2.Normalize(mean=CAD_val[0], std=CAD_val[1]) #데이터셋 표준화
])
# 전처리 방법론을 데이터셋에 적용하기
img_dataset['train'].transform = transformation
img_dataset['val'].transform = transformation
from torch.utils.data import DataLoader
BATCH_SIZE = 64
train_loader = DataLoader(img_dataset['train'],
batch_size=BATCH_SIZE,
shuffle=True)
test_loader = DataLoader(img_dataset['val'],
batch_size=BATCH_SIZE,
shuffle=False)
여기까지 수행했다면
Downstream task을 수행하기 위해 사전에 작업해 두어야 할
Fine-turning dataset의 전처리가 완료된 것이다.
라고 볼 수있다.
Fine-turning dataset의 전처리도 완료 되었겠다
불러온 학습이 완료된 모델(Pre-trained model)을
재 훈련(Re-training)하는 과정을 수행하고 이를 통해 Downstream task를 완료해야한다.
그런데 여기서 문제가 있다.
불러온 Pre-trained model의 마지막 레이어를 살펴봐야 한다.
print(WRN50_2)
위 코드를 통해 전체 모델의 레이어 구조를 출력 할 수 있고
그 중 가장 말단 레이어를 본다면
print(WRN50_2.fc)
Linear(in_features=2048, out_features=1000, bias=True)
out_features가 1000으로 설정되어 있어
현재 수행하고자 하는 Downstream task에는 맞지 않다.
여기서는 이 CNN모델에 의 구조 개념을 좀 알아둘 필요성이 있는데
불러온 Pre-trained model의 구조를 나눠 본다면 위 사진처럼
3가지 항목
Feature Extractor
Classifier
Decision layer
로 간소화하여 설명할 수 있다.
이 각각의 항목에 대한 설명을 정리하면 아래와 같다.
1) Feature Extractor : 입력 이미지에서 의미있는 특징을 추출하는 역활을 수행하는 레이어 묶음
2) Classifier : 추출한 특징을 바탕으로 입력 이미지가 어떤 클래스에 속할 지 예측하는 레이어
3) Decision Layer : Classifier의 가장 마지막 레이어를 말하며, 해당 모델의 마지막 output
을 출력하는 레이어
이 구조로 놓고 봤을 때 Pre-trained model의 마지막 Decision Layer은 출력 형태가 [1000]
이니
이것을 Downstream task의 목적에 맞는 [Cat or Dog]
의 이진 분류 문제에 맞는 출력 형태인 [1]로 바꿔줘야한다.
이때 이 Decision Layer만 갈아끼울지 아니면 Classifier를 통째로 들어내서 다른 Classifier를 붙여넣을지는 유저의 선택에 따라 달라진다.
이는 Classifier의 기능이 '클래스 종류를 분류하는 것'이고 Decision Layer은 그 출력의 형태만을 조정하는 레이어 이기에 수행하고자 하는 Downstream task에 따라서는 더 적합한 Classifier를 붙여넣을 필요성이 발생할 수도 있다.
아무튼 레이어를 갈아 끼우는 작업을 코드로 수행하면 아래와 같다.
# 모델의 마지막 분류 레이어 수정하기 위해
# 기존 마지막 레이어가 입력받는 차원 형태정보를 추출
num_features = WRN50_2.fc.in_features
# 이진 분류를 위해 출력 노드를 1로 설정
WRN50_2.fc = nn.Linear(num_features, 1)
지금의 사전학습 모델 : WRN50_2은 Classifier이 Decision Layer 단 하나로 구성된 단촐한 구성이기에
여기에 임의로 설계한 Classifier를 붙여넣는 작업도 가능하다
이에 대한 코드는 아래와 같다
# 기존 모델의 fc 레이어를 확장된 classifier 블록으로 대체
# 모델의 마지막 분류 레이어 수정하기 위해
# 기존 마지막 레이어가 입력받는 차원 형태정보를 추출
num_features = WRN50_2.fc.in_features
# 새로운 classifier 블록 정의 및 기존 모델에 추가
WRN50_2.fc = nn.Sequential(
nn.Linear(num_features, 1000), # 기존 모델의 fc 출력 크기와 일치하도록 수정
nn.Dropout(p=0.5),
nn.Linear(1000, 500), # 새로운 레이어
nn.Dropout(p=0.5),
nn.Linear(500, 1) # 이진 분류를 위한 출력 크기 (1)
)
위 같은 방식으로 사전학습 모델 : WRN50_2은 Classifier인 fc
레이어를 확장하여 더 성능을 향상시킬 수 있는 Classifier를 새로이 설계해 쓸 수도 있다.
앞서 설명한 전이학습(Transfer Learning)을 어떻게 설명할까 고민하다가
적합한 비유가 이것일 것 같아서 이미지를 첨부한다.
전이학습(Transfer Learning)은 전직
, 직업변경
이다.
대용량의 데이터 셋(Huge Dataset)으로 학습이 완료된 모델(Pre-trained model)은 용도 때려잡는 기사라고 치고
이 기사가 장착한 검은 약속된 승리의 검(+10강)이라 보면 된다.
이걸 Downstream task을 수행하려면 전사로 전직을 해야 하는 것이다.
근데 문제가 있다.
전사로 전직한 것 까지는 좋은데 장착 가능한 장비가 +1강짜리 형편없는 손도끼 인 것이다.
이게 전이학습 과정에서 발생하는 Classifier(Decision Layer)교체 작업이다.
이 새롭게 장착한 Classifier을 적어도 +9강의 울부짖는 양날도끼까지는 무기강화(Training)를 해줘야
진정으로 Downstream task 작업을 수행할 수 있을 것이다.
위 그림처럼 장비만 강화하고 끝낼 지
아니면 이참에 장비 강화할 겸 겸사겸사 본체도 레벨업을
한번 더 하는 것이다.
이 레벨업을 조금 더 하는 과정이 Fine Turning
이다.
그러면 이 레벨업을 어떻게 수행하느냐.. 가 문제인데
코드로 보면 이해하기가 쉽다.
1) 무기만 강화하기
# 모든 레이어의 파라미터를 Freeze
for param in WRN50_2.parameters():
param.requires_grad = False
# 새로운 레이어만 학습 가능하도록 설정
for param in WRN50_2.fc.parameters():
param.requires_grad = True
2) 본체도 레벨업 하기
# 모든 레이어의 파라미터를 Trainable로 조정
for param in WRN50_2.parameters():
param.requires_grad = True
새로이 설계해서 기존 모델을 수정하는데 사용된 Classifier는 무조건 학습이 가능한 형태 로 만들어야 함은 이했을 것이다.
이것을 조정하는 메서드가 .requires_grad
이고 이게
역전파를 수행 시 파라미터가 훈련될 항목임을 나타내는 메서드이다.
이게 False 이면 해당 레이어는 Freeze상태
반대로 True 이면 해당레이어는 디폴트 상태인 Trainable 상태가 되는 것이다.
(참고로 새로 설계한 모델은 무조건 .requires_grad=True
상태다. 이건 왜 그런지 이해를 해야한다...)
그럼 여기서 한발짝 더 나갈 수 있다.
위 사진처럼 어느 레이어까지
Freeze(.requires_grad=False
) 할지
어느 레이어 부터는
Trainable(.requires_grad=True
) 할지
를 결정해야 한다.
이것 또한 Downstream task작업을 수행하는데 있어 주요한 설계자의 역량 이라 볼 수 있을 것이다.
따라서Downstream task는 아래의 과정으로 작업절차를 요약할 수 있을 것이다.
1) 대용량 데이터셋으로 사전학습된 모델을 불러오기
2) Downstream task의 작업에 적합한 Fine-turning dataset의 준비 및 전처리
3) Downstream task의 요구결과물에 맞게 Classifier(Decision Layer) 수정 -> 전이학습
4) 수정된 모델의 Freeze/Trainable 레이어 비율 정의하기 -> 미세조정
위 레이어별 Freeze/Trainable의 비율을 정하는 방식은
1) 무기만 강화하기(All Freeze)
# 모든 레이어의 파라미터를 Freeze
for param in WRN50_2.parameters():
param.requires_grad = False
# 새로운 레이어만 학습 가능하도록 설정
for param in WRN50_2.fc.parameters():
param.requires_grad = True
2) 본체도 레벨업 하기(All Trainable)
# 모든 레이어의 파라미터를 Trainable로 조정
for param in WRN50_2.parameters():
param.requires_grad = True
이 두가지 경우로 놓고 코드 실습을 진행하고자 한다.
1) GPU에 모델 올리기
# GPU 사용 설정(이렇게 모델을 수정후 GPU로 이전이 유연하게 되네)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
WRN50_2 = WRN50_2.to(device)
2) 옵티마이저/손실함수 설계
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR
All Freeze버전
# 손실 함수와 옵티마이저 설정
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.SGD(WRN50_2.fc.parameters(), lr=1e-3, momentum=0.9)
All Trainable 버전
# 손실 함수와 옵티마이저 설정
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.SGD(WRN50_2.parameters(), lr=1e-4, momentum=0.9)
scheduler = CosineAnnealingLR(optimizer, T_max=40, eta_min=1e-7)
모든 레이어를 얼릴 경우에는 Classifier(Decision Layer)만 학습시키기에 옵티마이저 설정의 step를 크게 해야 하지만
모든 레이어가 학습 가능한 상태라면 옵티마이저 설정의 step도 줄이고, 스케쥴러까지 도입하는게 좋다.
안그럼 과적합이 발생할 것이다.
3) 훈련/검증 코드
from tqdm import tqdm #훈련 진행상황 체크
#tqdm 시각화 도구 출력 사이즈 조절 변수
epoch_step = 5
def model_train(model, data_loader,
loss_fn, optimizer_fn,
processing_device, epoch,
scheduler_fn=None,):
model.train() # 모델을 훈련 모드로 설정
global epoch_step
# loss와 accuracy를 계산하기 위한 임시 변수를 생성
run_size, run_loss, correct = 0, 0, 0
# 특정 에폭일 때만 tqdm 진행상황 바 생성
if (epoch + 1) % epoch_step == 0 or epoch == 0:
progress_bar = tqdm(data_loader)
else:
progress_bar = data_loader
for image, label in progress_bar:
# 입력된 데이터를 먼저 GPU로 이전하기
image = image.to(processing_device)
# label = label.to(processing_device)
# 이진분류 문제에서는 라벨의 차원축소 + float형 변환해야함
label = label.to(processing_device).float().unsqueeze(1)
# 전사 과정 수행
output = model(image)
loss = loss_fn(output, label)
#backward과정 수행
optimizer_fn.zero_grad()
loss.backward()
optimizer_fn.step()
if scheduler_fn is not None:
# 스케줄러 업데이트
scheduler_fn.step()
# #argmax = 주어진 차원에서 가장 큰 값을 가지는 요소의 인덱스를 반환
# pred = output.argmax(dim=1) #예측값의 idx출력
# correct += pred.eq(label).sum().item()
# 예측값 계산(이진분류용)
preds = torch.sigmoid(output) > 0.5
correct += preds.eq(label).sum().item()
#현재까지 수행한 loss값을 얻어냄
run_loss += loss.item() * image.size(0)
run_size += image.size(0)
#tqdm bar에 추가 정보 기입
if (epoch + 1) % epoch_step == 0 or epoch == 0:
desc = (f"[훈련중]로스: {run_loss / run_size:.4f}, "
f"정확도: {correct / run_size:.4f}")
progress_bar.set_description(desc)
avg_accuracy = correct / len(data_loader.dataset)
avg_loss = run_loss / len(data_loader.dataset)
return avg_loss, avg_accuracy
def model_evaluate(model, data_loader, loss_fn,
processing_device, epoch):
model.eval() # 모델을 평가 모드로 전환 -> dropout 기능이 꺼진다
# batchnormalizetion 기능이 꺼진다.
global epoch_step
# gradient 업데이트를 방지해주자
with torch.no_grad():
# 여기서도 loss, accuracy 계산을 위한 임시 변수 선언
run_loss, correct = 0, 0
# 특정 에폭일 때만 tqdm 진행상황 바 생성
if (epoch + 1) % epoch_step == 0 or epoch == 0:
progress_bar = tqdm(data_loader)
else:
progress_bar = data_loader
for image, label in progress_bar: # 이때 사용되는 데이터는 평가용 데이터
# 입력된 데이터를 먼저 GPU로 이전하기
image = image.to(processing_device)
# 이진분류 문제에서는 라벨의 차원축소 + float형 변환해야함
label = label.to(processing_device).float().unsqueeze(1)
# 모델 출력
output = model(image)
# # 모델의 평가 결과 도출 부분
# pred = output.argmax(dim=1) #예측값의 idx출력
# correct += torch.sum(pred.eq(label)).item()
# 모델의 평가 결과(이진분류용)
preds = torch.sigmoid(output) > 0.5
correct += preds.eq(label).sum().item()
run_loss += loss_fn(output, label).item() * image.size(0)
accuracy = correct / len(data_loader.dataset)
loss = run_loss / len(data_loader.dataset)
return loss, accuracy
참고로 그동안 포스팅 했었던 model_train
, model_evaluate
코드는 다중분류 문제였기에
지금의 코드는 이진분류용 함수로 변경되었다.
변경이 어디가 진행되었는지는 확인해보기 바란다
(주석도 달아놨으니 찾는데 어려움은 없을것이라 생각한다...)
4) 실행
All Freeze버전
# 학습과 검증 손실 및 정확도를 저장할 리스트
his_loss, his_accuracy = [], []
num_epoch = 25
for epoch in range(num_epoch):
# 훈련 손실과 훈련 성과지표를 반환 받습니다.
train_loss, train_acc = model_train(WRN50_2, train_loader,
criterion, optimizer,
device, epoch)
# 검증 손실과 검증 성과지표를 반환 받습니다.
test_loss, test_acc = model_evaluate(WRN50_2, test_loader,
criterion, device, epoch)
# 손실과 성능지표를 리스트에 저장
his_loss.append((train_loss, test_loss))
his_accuracy.append((train_acc, test_acc))
# epoch가 특정 배수일 때만 출력하기
if (epoch + 1) % epoch_step == 0 or epoch == 0:
print(f"epoch {epoch+1:03d} ", end=' ')
print(f"훈련 로스: {train_loss:.4f}", end=' ')
print(f"훈련 정확도: {train_acc:.4f}")
print(f"검증 로스: {test_loss:.4f}", end=' ')
print(f"검증 정확도: {train_acc:.4f}")
All Trainable 버전
fine_tuning_loss, fine_tuning_accuracy = [], []
num_epoch = 25
for epoch in range(num_epoch):
# 훈련 손실과 훈련 성과지표를 반환 받습니다.
train_loss, train_acc = model_train(WRN50_2, train_loader,
criterion, optimizer,
device, epoch,
scheduler_fn=scheduler)
# 검증 손실과 검증 성과지표를 반환 받습니다.
test_loss, test_acc = model_evaluate(WRN50_2, test_loader,
criterion, device, epoch)
# 미세조정의 손실과 성능지표를 리스트에 저장
fine_tuning_loss.append((train_loss, test_loss))
fine_tuning_accuracy.append((train_acc, test_acc))
# epoch가 특정 배수일 때만 출력하기
if (epoch + 1) % epoch_step == 0 or epoch == 0:
print(f"epoch {epoch+1:03d} ", end=' ')
print(f"훈련 로스: {train_loss:.4f}", end=' ')
print(f"훈련 정확도: {train_acc:.4f}")
print(f"검증 로스: {test_loss:.4f}", end=' ')
print(f"검증 정확도: {train_acc:.4f}")
Fine-turning dataset에 맞춰 모든 레이어를 All Trainable하게 조정한 뒤 훈련을 진행하는 미세조정 방식이 더 성능이 좋게 나왔으나, 이것은 과적합을 항상 경계하면서 진행해야 함을 꼭 숙지하자.
이것으로 WRN(Wide Residual Networks)의 학습이 완료된 버전(Pre-trained model)을 불러와서 Downstream task를 수행하는 실습을 완료했다.