https://gongboogi.tistory.com/14
[머신러닝/딥러닝] 흉부 엑스선 기반 폐렴 진단: 분석정리 및 시각화
드디어! 마지막 장으로 이번에는 경진대회에 참여하지 않고 캐글러가 공유한 데이터셋으로 모델링 연습을 실습해보았다 이번 데이터셋에서는 흉부 엑스선 이미지가 나오는데, 이 이미지들을
gongboogi.tistory.com
저번 글에서 흉부 엑스선 이미지 데이터셋을 살펴보고 시각화해보았다. 이번에는 베이스라인 모델을 만들고 성능개선을 진행해보았다. 베이스라인 모델링 때 훈련과 예측 단계를 함수로 묶어서 성능 개선 때 재활용할 것이고, efficientnet의 여러 버전을 시도한다.
1. 베이스라인 모델: 시드값 고정 및 GPU 장비 설정
시드값 고정
import torch # 파이토치
import random
import numpy as np
import os
# 시드값 고정
seed = 50
os.environ['PYTHONHASHSEED'] = str(seed)
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.enabled = False
GPU 장비 설정
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
캐글이라면 먼저 오른쪽에 Setting 탭에서 Accelerator를 GPU로 바꾸고 할당해준다.
2. 데이터 준비
# 데이터 경로
data_path = '/kaggle/input/chest-xray-pneumonia/chest_xray/'
# 훈련, 검증, 테스트 데이터 경로 설정
train_path = data_path + 'train/'
valid_path = data_path + 'val/'
test_path = data_path + 'test/'
저번과 마찬가지로 각 디렉터리별 경로로 불러온다. 이번에는 데이터셋 클래스를 직접 정의하지 않고 ImageFolder라는 데이터셋 생성기를 사용할 거라 간단하다.
데이터 증강을 위한 이미지 변환기 정의
torchvision의 transforms를 활용해 이미지 변환기를 만든다.
from torchvision import transforms
# 훈련 데이터용 변환기
transform_train = transforms.Compose([
transforms.Resize((250, 250)), # 이미지 크기 조정
transforms.CenterCrop(180), # 중앙 이미지 확대
transforms.RandomHorizontalFlip(0.5), # 좌우 대칭
transforms.RandomVerticalFlip(0.2), # 상하 대칭
transforms.RandomRotation(20), # 이미지 회전
transforms.ToTensor(), # 텐서 객체로 변환
transforms.Normalize((0.485, 0.456, 0.406),
(0.229, 0.224, 0.225))]) # 정규화
# 테스트 데이터용 변환기
transform_test = transforms.Compose([
transforms.Resize((250, 250)),
transforms.CenterCrop(180),
transforms.ToTensor(),
transforms.Normalize((0.485, 0.456, 0.406),
(0.229, 0.224, 0.225))])
저번 장에서는 albumentations 변환기를 사용했는데 이번에는 transforms을 사용했다. 밑에서 다룰 ImageFolder 때문인데, ImageFolder는 데이터셋을 만들어주는 라이브러리로 torchvision.transforms에서 만든 변환기를 받도록 설계되어있기 때문이다.
훈련 데이터용 변환기와 테스트 데이터용 변환기를 한 번에 만들었다. 이미지를 출력했을 때 이미지 가로 세로 크기가 일정하지 않았으므로 똑같이 맞추어 주기 위해 두 변환기 모두 첫 번째로 Resize 변환기로 이미지 크기를 일정하게 조정했다.
데이터셋 및 데이터 로더 생성
데이터셋을 만들 차례다. 데이터셋 클래스를 정의하지 않는데 이는 타깃값이 같은 이미지끼리 디렉터리로 구분되어 있으면 ImageFolder 클래스를 이용해 바로 데이터셋을 만들 수 있기 때문이다.
각 디렉터리에 담긴 이미지의 타깃값은 다음과 같이 생성한다.
이제 ImageFolder을 활용해 훈련, 검증 데이터셋을 만들어보자.
from torchvision.datasets import ImageFolder
# 훈련 데이터셋
datasets_train = ImageFolder(root=train_path, transform=transform_train)
# 검증 데이터셋
datasets_valid = ImageFolder(root=valid_path, transform=transform_test)
ImageFolder()은 root에 전달한 경로에 있는 이미지들로 바로 데이터셋을 만들어준다. transform 파라미터에는 위에서 torchvision의 transform으로 만든 변환기를 전달해주었다. albumentations로 만든 변환기를 전달하면 오류가 발생하니 주의하도록 한다.
ImageFolder은 데이터셋 클래스를 별도로 정의하지 않아도 돼서 편리하지만 타깃값이 같은 데이터들이 같은 디렉터리에 모여있어야 한다
훈련시간이 오래걸리므로 이번에도 멀티프로세싱을 사용하기 위해 데이터 로더의 시드값 고정을 위한 seed_worker() 함수와 제너레이터를 정의했다.
def seed_worker(worker_id):
worker_seed = torch.initial_seed() % 2**32
np.random.seed(worker_seed)
random.seed(worker_seed)
# 제너레이터 시드값 고정
g = torch.Generator()
g.manual_seed(0)
데이터 로더도 만들어준다.
from torch.utils.data import DataLoader
batch_size = 8
loader_train = DataLoader(dataset=datasets_train, batch_size=batch_size,
shuffle=True, worker_init_fn=seed_worker,
generator=g, num_workers=2)
loader_valid = DataLoader(dataset=datasets_valid, batch_size=batch_size,
shuffle=False, worker_init_fn=seed_worker,
generator=g, num_workers=2)
배치 크기는 8로 설정해주었는데, 여러 값을 넣어보며 성능이 달라지는지 실험해보는 것도 좋은 방법이다.
3. 모델 생성
훈련시킬 모델을 준비하는데 이번에도 사전 훈련 모델로 성능이 좋은 EfficientNet을 사용하기 위해 설치하고 임포트해준다.
!pip install efficientnet-pytorch==0.7.1
from efficientnet_pytorch import EfficientNet
# 모델 생성
model = EfficientNet.from_pretrained('efficientnet-b0', num_classes=2)
# 장비 할당
model = model.to(device)
EfficientNet은 b0부터 b7까지 있는데 이번 베이스라인에서는 가장 간단한 b0을 사용하려고 한다. num_classes 파라미터에는 최종 출력값 개수를 입력하면 되는데, 이진분류 문제이므로 2를 전달한다.
print('모델 파라미터 개수 :', sum(param.numel() for param in model.parameters()))
numel()은 텐서 객체가 갖는 구성요소의 총 개수를 구해준다.
모델 파라미터 개수는 약 4백만 개다. 파라미터 개수가 많을수록 복잡한 모델인데 제법 많다. 이 많은 파라미터 값을 업데이트 하며 훈련하기 때문에 시간이 걸린는 것이다.
4. 모델 훈련 및 성능 검증
손실 함수와 옵티마이저 설정
분류 문제이므로 손실 함수는 CrossEntropyLoss()로 설정한다.
import torch.nn as nn
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
옵티마이저는 Adam을 사용하고 학습율은 0.01로 설정했다.
훈련 함수 작성
train()이라는 훈련 함수를 만든다. 훈련 함수를 만들면 다른 상황에서도 응용할 수 있어 활용 범위가 넓고 여러 모델을 앙상블할 때 코드가 간결해지는 효과가 있다. 먼저 다음은 전체코드인데 코드가 길기 때문에 각 부분을 따로 한번 볼 것이다.
from sklearn.metrics import accuracy_score # 정확도 계산 함수
from sklearn.metrics import recall_score # 재현율 계산 함수
from sklearn.metrics import f1_score # F1 점수 계산 함수
from tqdm.notebook import tqdm # 진행률 표시 막대
def train(model, loader_train, loader_valid, criterion, optimizer,
scheduler=None, epochs=10, save_file='model_state_dict.pth'):
valid_loss_min = np.inf # 최소 손실값 초기화 (검증 데이터용)
# 총 에폭만큼 반복
for epoch in range(epochs):
print(f'에폭 [{epoch+1}/{epochs}] \n-----------------------------')
# == [ 훈련 ] ==============================================
model.train() # 모델을 훈련 상태로 설정
epoch_train_loss = 0 # 에폭별 손실값 초기화 (훈련 데이터용)
# '반복 횟수'만큼 반복
for images, labels in tqdm(loader_train):
# 이미지, 레이블(타깃값) 데이터 미니배치를 장비에 할당
images = images.to(device)
labels = labels.to(device)
# 옵티마이저 내 기울기 초기화
optimizer.zero_grad()
# 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
outputs = model(images)
# 손실 함수를 활용해 outputs와 labels의 손실값 계산
loss = criterion(outputs, labels)
# 현재 배치에서의 손실 추가 (훈련 데이터용)
epoch_train_loss += loss.item()
loss.backward() # 역전파 수행
optimizer.step() # 가중치 갱신
if scheduler != None: # 스케줄러 학습률 갱신
scheduler.step()
# 훈련 데이터 손실값 출력
print(f'\t훈련 데이터 손실값 : {epoch_train_loss/len(loader_train):.4f}')
# == [ 검증 ] ==============================================
model.eval() # 모델을 평가 상태로 설정
epoch_valid_loss = 0 # 에폭별 손실값 초기화 (검증 데이터용)
preds_list = [] # 예측값 저장용 리스트 초기화
true_list = [] # 실젯값 저장용 리스트 초기화
with torch.no_grad(): # 기울기 계산 비활성화
for images, labels in loader_valid:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
epoch_valid_loss += loss.item()
# 예측값 및 실제값
preds = torch.max(outputs.cpu(), dim=1)[1].numpy()
true = labels.cpu().numpy()
preds_list.extend(preds)
true_list.extend(true)
# 정확도, 재현율, F1 점수 계산
val_accuracy = accuracy_score(true_list, preds_list)
val_recall = recall_score(true_list, preds_list)
val_f1_score = f1_score(true_list, preds_list)
# 검증 데이터 손실값 및 정확도, 재현율, F1점수 출력
print(f'\t검증 데이터 손실값 : {epoch_valid_loss/len(loader_valid):.4f}')
print(f'\t정확도 : {val_accuracy:.4f} / 재현율 : {val_recall:.4f} / F1 점수 : {val_f1_score:.4f}')
# == [ 최적 모델 가중치 찾기 ] ==============================
# 현 에폭에서의 손실값이 최소 손실값 이하면 모델 가중치 저장
if epoch_valid_loss <= valid_loss_min:
print(f'\t### 검증 데이터 손실값 감소 ({valid_loss_min:.4f} --> {epoch_valid_loss:.4f}). 모델 저장')
# 모델 가중치를 파일로 저장
torch.save(model.state_dict(), save_file)
valid_loss_min = epoch_valid_loss # 최소 손실값 갱신
return torch.load(save_file) # 저장한 모델 가중치를 불러와 반환
훈련 함수 작성1: 뼈대
이 함수는 기본적으로 어떻게 돌아가는지 살펴보자.
def train(model, loader_train, loader_valid, criterion, optimizer,
scheduler=None, epochs=10, save_file='model_state_dict.pth'):
# 총 에폭만큼 반복
for epoch in range(epochs):
# == [ 훈련 ] ==============================================
# 미니배치 단위로 훈련
for images, labels in tqdm(loader_train):
# 기울기 초기화
# 순전파
# 손실값 계산(훈련데이터용)
# 역전파
# 가중치 갱신
# 학습률 갱신
# == [ 검증 ] ==============================================
# 미니배치 단위로 검증
for images, labels in loader_valid:
# 순전파
# 손실값 계산(검증 데이터용)
# == [ 최적 모델 가중치 찾기 ] ==============================
# 현 에폭에서의 손실값이 최소 손실값 이하면 (현재까지 최적 모델)모델 가중치 저장
return torch.load(save_file) # 저장한 모델 가중치를 불러와 반환
- 에폭 수만큼 훈련과 검증을 반복하면서 최적 모델의 가중치를 찾아서 마지막에 반환하는 구조다.
- 데이터 로더를 이용하므로 훈련과 검증을 미니배치 단위로 수행한다. (전체 데이터를 작은 단위로 나누어 학습)
훈련 함수 작성2: 훈련
이어서 훈련 코드를 실제로 보면
# 총 에폭만큼 반복
for epoch in range(epochs):
print(f'에폭 [{epoch+1}/{epochs}] \n-----------------------------')
# == [ 훈련 ] ==============================================
model.train() # 모델을 훈련 상태로 설정
epoch_train_loss = 0 # 에폭별 손실값 초기화 (훈련 데이터용)
# '반복 횟수'만큼 반복
for images, labels in tqdm(loader_train):
# 이미지, 레이블(타깃값) 데이터 미니배치를 장비에 할당
images = images.to(device)
labels = labels.to(device)
# 옵티마이저 내 기울기 초기화
optimizer.zero_grad()
# 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
outputs = model(images)
# 손실 함수를 활용해 outputs와 labels의 손실값 계산
loss = criterion(outputs, labels)
# 현재 배치에서의 손실 추가 (훈련 데이터용)
epoch_train_loss += loss.item()
loss.backward() # 역전파 수행
optimizer.step() # 가중치 갱신
if scheduler != None: # <<<<<<<스케줄러 학습률 갱신
scheduler.step()
# 훈련 데이터 손실값 출력
print(f'\t훈련 데이터 손실값 : {epoch_train_loss/len(loader_train):.4f}')
저번 12장 코드와 같은데 다른 한 가지는 스케줄러를 전달한 경우에만 학습률을 갱신한다.
- 바깥쪽 for문에서 단위로 훈련과 검증 반복
- 훈련과 검증이 반복되므로 훈련할 때는 모델을 훈련상태로(model.train()) 설정
- 안쪽 for문은 반복횟수(데이터로더의 길이)만큼 반복되며 배치크기만큼 이미지와 레이블을 추출해 image와 labels 변수에 할당
- 에폭이 하나 끝날 때마다 손실값 출력
훈련 함수 작성3: 검증
# == [ 검증 ] ==============================================
model.eval() # 모델을 평가 상태로 설정
epoch_valid_loss = 0 # 에폭별 손실값 초기화 (검증 데이터용)
preds_list = [] # 예측값 저장용 리스트 초기화
true_list = [] # 실젯값 저장용 리스트 초기화
with torch.no_grad(): # 기울기 계산 비활성화
# 하나의 에폭만큼의 데이터를 미니배치 단위로 검증
for images, labels in loader_valid:
images = images.to(device)
labels = labels.to(device)
outputs = model(images) # 순전파
loss = criterion(outputs, labels) # 손실값 계산
epoch_valid_loss += loss.item() # 현재 배치에서의 손실 추가
# 예측값 및 실제값
preds = torch.max(outputs.cpu(), dim=1)[1].numpy()
true = labels.cpu().numpy()
preds_list.extend(preds)
true_list.extend(true)
# 정확도, 재현율, F1 점수 계산
val_accuracy = accuracy_score(true_list, preds_list)
val_recall = recall_score(true_list, preds_list)
val_f1_score = f1_score(true_list, preds_list)
# 검증 데이터 손실값 및 정확도, 재현율, F1점수 출력
print(f'\t검증 데이터 손실값 : {epoch_valid_loss/len(loader_valid):.4f}')
print(f'\t정확도 : {val_accuracy:.4f} / 재현율 : {val_recall:.4f} / F1 점수 : {val_f1_score:.4f}')
- 훈련과 검증이 반복되므로 훈련할 때는 모델을 검증상태로(model.eval()) 설정
- 데이터로더에서 검증 데이터를 미니배치 단위로 검증
- 예측값과 실젯값을 저장: 평가지표로 정확도, 재현율, F1점수를 사용하려면 예측값을 확률이 아닌 이산값으로 구해야한다. torch.max()는 최댓값을 구하는 메서드로, 예측 확률을 이산값으로 바꿔준다.
- for문을 다 돌면 현재 에폭의 검증 데이터 손실값을 출력하고, 정확도, 재현율, F1점수를 구해 출력한다.
훈련 함수 작성4: 최적 모델 가중치 찾기
# == [ 최적 모델 가중치 찾기 ] ==============================
# 현 에폭에서의 손실값이 최소 손실값 이하면 모델 가중치 저장
if epoch_valid_loss <= valid_loss_min:
print(f'\t### 검증 데이터 손실값 감소 ({valid_loss_min:.4f} --> {epoch_valid_loss:.4f}). 모델 저장')
# 모델 가중치를 파일로 저장
torch.save(model.state_dict(), save_file)
valid_loss_min = epoch_valid_loss # 최소 손실값 갱신
return torch.load(save_file) # 저장한 모델 가중치를 불러와 반환
- 현재 에폭의 손실값이 최소 손실값 이하면 모델 가중치를 저장하고 기존 최소 손실값을 현재 에폭의 손실값으로 갱신한다.
- torch.save()의 첫 번째 파라미터에 전달한 model.state_dict()가 모델 가중치를 뜻한다. 두 번째 save_file은 저장 파일명이다.
- 모든 훈련과 검증이 끝나면 저장한 파일을 읽어와 반환한다.
- -> 전체 에폭에서 가장 성능이 좋은(검증 데이터 손실값이 가장 작은) 모델 가중치를 반환하는 것이다.
훈련 및 성능 검증
train()함수로 실제로 모델을 훈현해본다. 다음 코드는 훈련을 진행하면서 손실값과 정확도, 재현율, F1점수를 출력한다.
# 모델 훈련
model_state_dict = train(model = model,
loader_train = loader_train,
loader_valid = loader_valid,
criterion = criterion,
optimizer = optimizer)
9번째 에폭에서 검증 데이터 손실값이 0.3724로 많이 줄어 가장 작으므로 이때 모델 가중치가 최적 모델 가중치라고 볼 수 있다. 그래서 최종적으로 9번째 에폭 모델 가중치가 저장되었다.
# 최적 가중치 불러오기
model.load_state_dict(model_state_dict)
train() 함수에서 반환한 최적 모델 가중치는 model_state_dict 변수에 저장되어있다. 이 가중치로 보델의 가중치를 갱신했다.
5. 예측 및 평가 결과
마지막으로 예측과 평가가 남았다. 테스트 데이터를 활용해 결과를 예측하고 실젯값과 비교해보도록 하자. 먼저 테스트 데이터셋과 데이터 로더를 생성한다.
datasets_test = ImageFolder(root=test_path, transform=transform_test)
loader_test = DataLoader(dataset=datasets_test, batch_size=batch_size,
shuffle=False, worker_init_fn=seed_worker,
generator=g, num_workers=2)
예측
훈련 과정 함수인 train()함수와 비슷하게 테스트 데이터로 결과를 예측하는 predict() 함수를 정의한다. 이 함수는 간단하게 모델과 테스트 데이터 로더를 인수로 입력받아 예측값을 반환한다.
def predict(model, loader_test, return_true=False):
model.eval() # 모델을 평가 상태로 설정
preds_list = [] # 예측값 저장용 리스트 초기화
true_list = [] # 실제값 저장용 리스트 초기화
with torch.no_grad(): # 기울기 계산 비활성화
for images, labels in loader_test:
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
preds = torch.max(outputs.cpu(), dim=1)[1].numpy() # 예측값
true = labels.cpu().numpy() # 실제값
preds_list.extend(preds)
true_list.extend(true)
if return_true:
return true_list, preds_list
else:
return preds_list
return_true라는 파라미터를 둬서 이 값이 참이면 실젯값과 예측값을 같이 반환하고 거짓이면 예측값만 반환한다.
이제 predict() 함수를 사용해서 예측값을 구하는데 return_true 파라미터에 False를 전달해서 실젯값과 함께 반환한다.
true_list, preds_list = predict(model=model,
loader_test=loader_test,
return_true=True)
true_list에는 실젯값, preds_list에는 예측값을 저장했다.
평가 결과
저장한 실젯값과 예측값으로 정확도, 재현율, F1점수를 출력하면
print('#'*5, '최종 예측 결과 평가 점수', '#'*5)
print(f'정확도 : {accuracy_score(true_list, preds_list):.4f}')
print(f'재현율 : {recall_score(true_list, preds_list):.4f}')
print(f'F1 점수 : {f1_score(true_list, preds_list):.4f}')
EfficientNet-B0를 이용한 베이스라인 모델 점수가 나온다. 이어서 또 다른 기법으로 성능 개선을 해볼 것이다.
6. 성능 개선
모델 세 개를 앙상블해서 베이스라인 모델보다 우수한 성능을 내보려고 한다.
EfficientNet은 B0에서 B7까지 있다고 했는데, B7로 갈수록 모델이 복잡해진다. 즉 모델 파라미터가 많아진다는 뜻이다. 모델 파라미터가 많을수록 성능이 좋아진다고 볼수 있기는 하지만 항상 그런 건 아니다. 단순한 이미지를 구분하기 위해 파라미터가 많은 모델을 사용하면 오히려 과대적합이 일어나 평가 점수가 떨어질 수도 있다.
이번에는 파라미터가 B7보다 적은 B1, B2, B3을 사용하는데, 세 모델을 각자 훈련한 뒤 결과를 앙상블하는 것이다.
데이터 준비 과정까지는 위의 베이스라인과 동일하게한 후 진행한다.
모델 생성
!pip install efficientnet-pytorch==0.7.1
models_list =[] # 모델 저장용 리스트
EfficientNet을 설치하고 모델들을 저장할 리스트를 만들었다. 모델 세 개를 앙상블하므로 세 모델을 한 번에 다루기 편하도록 리스트에 담아 관리한다.
이어서 EfficientNet-B1, EfficientNet-B2, EfficientNet-B3을 생성하고, 장비에 할당한 뒤 모델 저장 리스트에 저장한다.
from efficientnet_pytorch import EfficientNet
# 모델 생성
efficientnet_b1 = EfficientNet.from_pretrained('efficientnet-b1', num_classes=2)
efficientnet_b2 = EfficientNet.from_pretrained('efficientnet-b2', num_classes=2)
efficientnet_b3 = EfficientNet.from_pretrained('efficientnet-b3', num_classes=2)
# 장비 할당
efficientnet_b1 = efficientnet_b1.to(device)
efficientnet_b2 = efficientnet_b2.to(device)
efficientnet_b3 = efficientnet_b3.to(device)
# 리스트에 모델 저장
models_list.append(efficientnet_b1)
models_list.append(efficientnet_b2)
models_list.append(efficientnet_b3)
베이스라인에서 EfficientNet-B0을 사용했을 때 약 4백만 개가 나왔는데 이번에는 얼마나 나왔는지 각 모델의 파라미터 개수를 출력해보면
for idx, model in enumerate(models_list):
num_parmas = sum(param.numel() for param in model.parameters())
print(f'모델{idx+1} 파라미터 개수 : {num_parmas}')
B1, B2, B3순으로 파라미터가 늘어나는 것을 볼 수 있다.
손실 함수, 옵티마이저, 스케줄러 설정
베이스라인에서는 손실함수와 옵티마이저까지 설정했는데 이번에는 스케줄러까지 추가한다.
<손실함수 설정>
import torch.nn as nn
criterion = nn.CrossEntropyLoss()
<옵티마이저: AdamW(Adam에 가중치 감쇠 추가)>
optimizer1 = torch.optim.AdamW(models_list[0].parameters(), lr=0.0006, weight_decay=0.001)
optimizer2 = torch.optim.AdamW(models_list[1].parameters(), lr=0.0006, weight_decay=0.001)
optimizer3 = torch.optim.AdamW(models_list[2].parameters(), lr=0.0006, weight_decay=0.001)
모델을 세 개 만들었으므로 각각 설정한다.
<스케줄러 설정>
from transformers import get_cosine_schedule_with_warmup
epochs = 20 # 총 에폭
# 스케줄러
scheduler1 = get_cosine_schedule_with_warmup(optimizer1,
num_warmup_steps=len(loader_train)*3,
num_training_steps=len(loader_train)*epochs)
scheduler2 = get_cosine_schedule_with_warmup(optimizer2,
num_warmup_steps=len(loader_train)*3,
num_training_steps=len(loader_train)*epochs)
scheduler3 = get_cosine_schedule_with_warmup(optimizer3,
num_warmup_steps=len(loader_train)*3,
num_training_steps=len(loader_train)*epochs)
이번 스케줄러도 get_cosine_schedule_with_warmup()을 사용한다. 첫 번째 파라미터에 위에서 설정한 옵티마이저를 각각 전달하고, 에폭을 20으로 늘려보았다.
모델 훈련 및 성능 검증
먼저 train() 함수를 베이스라인과 동일하게 정의하고, train()함수를 이용해 세 모델을 순서대로 훈련시킨다.
<첫 번째 모델 훈련>
# 첫 번째 모델 훈련
model_state_dict = train(model=models_list[0],
loader_train=loader_train,
loader_valid=loader_valid,
criterion=criterion,
optimizer=optimizer1,
scheduler=scheduler1,
epochs=epochs)
# 첫 번째 모델에 최적 가중치 적용
models_list[0].load_state_dict(model_state_dict)
마지막 에폭인 20번째 검증 데이터 손실값이 가장 낮아서 첫 번째 모델에 저장되었다.
<두 번째 모델 훈련>
# 두 번째 모델 훈련
model_state_dict = train(model=models_list[1],
loader_train=loader_train,
loader_valid=loader_valid,
criterion=criterion,
optimizer=optimizer2,
scheduler=scheduler2,
epochs=epochs)
# 두 번째 모델에 최적 가중치 적용
models_list[1].load_state_dict(model_state_dict)
두 번째 모델에서는 15번째 에폭 값이 저장되었다.
<세 번째 모델 훈련>
# 세 번째 모델 훈련
model_state_dict = train(model=models_list[2],
loader_train=loader_train,
loader_valid=loader_valid,
criterion=criterion,
optimizer=optimizer3,
scheduler=scheduler3,
epochs=epochs)
# 세 번째 모델에 최적 가중치 적용
models_list[2].load_state_dict(model_state_dict)
마지막 세 번째 모델에서는 마지막 에폭 값이 저장되었다.
이제 세 모델 모두 훈련을 마쳤다. models_list에는 훈련이 완료된 최적 가중치로 갱신된 모델들이 저장되어 있다.
예측 및 평가 결과
훈련을 마친 모델들로 각각 예측하고 세 모델의 예측 결과를 앙상블하려고 한다. 앙상블 예측값과 실젯값을 비교해서 최종 평가 점수를 산출하려는 것이다.
<테스트 데이터셋, 데이터 로더 생성>
datasets_test = ImageFolder(root = test_path, transform=transform_test)
loader_test = DataLoader(dataset=datasets_test, batch_size = batch_size,
shuffle=False, worker_init_fn = seed_worker,
generator = g, num_workers=2)
모델별 예측
predict() 함수도 베이스라인과 동일하게 정의하고, 모델이 세 개이므로 세 번 예측한다.
<첫 번째 모델로 예측>
true_list, preds_list1 = predict(model=models_list[0],
loader_test=loader_test,
return_true=True)
첫 번째 모델은 실젯값과 예측값을 모두 반환하기 위해 return_true에 True를 전달한다. 실젯값이 있어야 평가 점수를 산출할 수 있기 때문이다.
true_list는 실젯값, preds_list1은 첫 번째 모델(EfficientNet-B1)로 예측한 값이다.
<두 번째 모델로 예측>
preds_list2 = predict(model=models_list[1],
loader_test=loader_test)
두 번째 모델은 실젯값을 또 구할 필요는 없으므로 return_true를 생략한다.
<세 번째 모델로 예측>
preds_list3 = predict(model=models_list[2],
loader_test=loader_test)
이렇게 해서 실젯값 true_list와 세 모델로 예측한 값 preds_list1~3이 구해졌다.
이제 각 모델별 평가 점수를 확인해보자.
모델별 평가점수
<첫 번째 모델(EfficientNet-B1) 평가 점수>
print('#'*5, 'efficientnet-b1 모델 예측 결과 평가 점수', '#'*5)
print(f'정확도 : {accuracy_score(true_list, preds_list1):.4f}')
print(f'재현율 : {recall_score(true_list, preds_list1):.4f}')
print(f'F1 점수 : {f1_score(true_list, preds_list1):.4f}')
<두 번째 모델(EfficientNet-B2) 평가 점수>
print('#'*5, 'efficientnet-b2 모델 예측 결과 평가 점수', '#'*5)
print(f'정확도 : {accuracy_score(true_list, preds_list2):.4f}')
print(f'재현율 : {recall_score(true_list, preds_list2):.4f}')
print(f'F1 점수 : {f1_score(true_list, preds_list2):.4f}')
<세 번째 모델(EfficientNet-B3) 평가 점수>
print('#'*5, 'efficientnet-b3 모델 예측 결과 평가 점수', '#'*5)
print(f'정확도 : {accuracy_score(true_list, preds_list3):.4f}')
print(f'재현율 : {recall_score(true_list, preds_list3):.4f}')
print(f'F1 점수 : {f1_score(true_list, preds_list3):.4f}')
B1과 B2의 점수는 거의 비슷했다.
이번 실습을 하며 출력된 평가 점수를 표로 정리해보았다.
B0 | B1 | B2 | B3 | |
정확도 | 0.8574 | 0.8494 | 0.8590 | 0.8910 |
재현율 | 0.9667 | 0.9974 | 0.9949 | 0.9949 |
F1점수 | 0.8944 | 0.8922 | 0.8981 | 0.9194 |
평가 점수를 모든 모델들과 비교해본 결과 B1~B3 모델이 모든 면에서 베이스라인인 B0보다 점수가 높지만, 숫자가 높다고(파라미터가 많다고) 성능이 반드시 잘 나오지 않는다는 것을 확인할 수 있엇다.
앙상블 예측
성능향상을 위해서 세 모델로 구한 예측값을 앙상블 해보았다.
ensemble_preds = []
for i in range(len(preds_list1)):
pred_element = np.round((preds_list1[i] + preds_list2[i] + preds_list3[i])/3)
ensemble_preds.append(pred_element)
앙상블 원리는 간단하게 세 예측값을 모두 합친 다음 3으로 나누고, np.round() 함수로 반올림해주었다.
이렇게 하면 과반수가 예측한 값을 최종 예측값으로 결정한다. 예를 들어
- 모델1: 1
- 모델2: 1
- 모델3: 0
으로 예측했을 때 두 모델이 1로 예측했으므로 앙상블 결과는 과반수 규칙으로 1로 나온다.
평가 결과
앙상블을 마친 후 실젯값과 앙상블 예측 결과를 비교해 정확도, 재현율, F1점수를 구해보았다. 성능이 얼마나 향상되었을까?
print('#'*5, '최종 앙상블 결과 평가 점수', '#'*5)
print(f'정확도 : {accuracy_score(true_list, ensemble_preds):.4f}')
print(f'재현율 : {recall_score(true_list, ensemble_preds):.4f}')
print(f'F1 점수 : {f1_score(true_list, ensemble_preds):.4f}')
앙상블 결과 앞의 모델들보다 높은 점수가 나왔을까? 앙상블 모델도 표에 추가해서 보자.
B0 | B1 | B2 | B3 | 앙상블 | |
정확도 | 0.8574 | 0.8494 | 0.8590 | 0.8910 | 0.8718 |
재현율 | 0.9667 | 0.9974 | 0.9949 | 0.9949 | 0.9974 |
F1점수 | 0.8944 | 0.8922 | 0.8981 | 0.9194 | 0.9068 |
나의 실습환경에서는 재현율만 앞의 모델보다 높은 점수가 나왔다. 교재에서는 모든 점수에서 개별 모델보다 높은 점수가 나왔다. 왜인지 고민해봤는데 이번 경우는 다수결 기반 방법을 사용해서 결합하는 앙상블 했는데 비교 모델이 세 가지라서 적었을 수도 있지 않을까..라는 생각이 든다.
참고 교재
머신러닝·딥러닝 문제해결 전략 | 신백균
[머신러닝·딥러닝 문제해결 전략 - chapter13]
'ML,DL' 카테고리의 다른 글
[머신러닝/딥러닝] 흉부 엑스선 기반 폐렴 진단: 분석정리 및 시각화 (0) | 2023.02.12 |
---|---|
[머신러닝/딥러닝] 병든 잎사귀 식별 경진대회: 베이스라인 모델 및 성능개선 (0) | 2023.02.05 |
[머신러닝/딥러닝] 병든 잎사귀 식별 경진대회: 분석정리 및 시각화 (0) | 2023.02.04 |
[머신러닝/딥러닝] 향후 판매량 예측 경진대회: 분석정리 및 시각화 (0) | 2022.11.19 |
[머신러닝/딥러닝] 안전 운전자 예측 경진대회: 분석정리 및 시각화 (0) | 2022.11.07 |