Modeling - Aerial Cactus Identification - [ 머신러닝-딥러닝 문제해결 전략 ]
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
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
11.4.1 데이터 준비
import pandas as pd
# 데이터 경로
data_path = '/kaggle/input/aerial-cactus-identification/'
labels = pd.read_csv(data_path + 'train.csv')
submission = pd.read_csv(data_path + 'sample_submission.csv')
from zipfile import ZipFile
# 훈련 이미지 데이터 압축 풀기
with ZipFile(data_path + 'train.zip') as zipper:
zipper.extractall()
# 테스트 이미지 데이터 압축 풀기
with ZipFile(data_path + 'test.zip') as zipper:
zipper.extractall()
from sklearn.model_selection import train_test_split
# 훈련 데이터, 검증 데이터 분리
train, valid = train_test_split(labels,
test_size=0.1,
stratify=labels['has_cactus'],
random_state=50)
import cv2 # OpenCV 라이브러리
from torch.utils.data import Dataset # 데이터 생성을 위한 클래스
class ImageDataset(Dataset):
# 초기화 메서드(생성자)
def __init__(self, df, img_dir='./', transform=None):
super().__init__() # 상속받은 Dataset의 생성자 호출
# 전달받은 인수들 저장
self.df = df
self.img_dir = img_dir
self.transform = transform
# 데이터셋 크기 반환 메서드
def __len__(self):
return len(self.df)
# 인덱스(idx)에 해당하는 데이터 반환 메서드
def __getitem__(self, idx):
img_id = self.df.iloc[idx, 0] # 이미지 ID
img_path = self.img_dir + img_id # 이미지 파일 경로
image = cv2.imread(img_path) # 이미지 파일 읽기
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 이미지 색상 보정
label = self.df.iloc[idx, 1] # 이미지 레이블(타깃값)
if self.transform is not None:
image = self.transform(image) # 변환기가 있다면 이미지 변환
return image, label
이미지 변환기 정의
from torchvision import transforms # 이미지 변환을 위한 모듈
# 훈련 데이터용 변환기
transform_train = transforms.Compose([transforms.ToTensor(),
transforms.Pad(32, padding_mode='symmetric'),
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
transforms.RandomRotation(10),
transforms.Normalize((0.485, 0.456, 0.406),
(0.229, 0.224, 0.225))])
# 검증 및 테스트 데이터용 변환기
transform_test= transforms.Compose([transforms.ToTensor(),
transforms.Pad(32, padding_mode='symmetric'),
transforms.Normalize((0.485, 0.456, 0.406),
(0.229, 0.224, 0.225))])
baseline 단계에서는 ToTensor()만 사용하며 이미지를 불러왔다. 이번에는 이미지 변환기를 직접 정의하며 과소적합을 해결해보자.
Pad는 패딩을 추가하는 것이다. symmetric을 썼으므로 상하좌우 대칭의 이미지가 32 두께로 패딩을 만들어 9배 큰 이미지가 된다.
HorizontalFlip, VerticalFlip은 말 그대로 상하좌우 대칭을 변환한다. 바라미터에는 변환 비율을 설정할 수 있다.
Rotation은 이미지를 회전히킨다. 파라미터로 10을 전달하면 -10 ~ 10 도의 값만큼 무작위로 회전한다. Mnist의 경우엔 9와 6이 구분되기 힘듦으로 조심해서 사용해야 한다.
Normalize는 데이터를 지정한 평균과 분산에 맞게 정규화해준다. 평균과 분산은 RGB 데이터이므로 3개씩 있다.
코드에서 지정된 해당 값은 백만 개 이상의 이미지를 보유한 imageNet의 데이터로부터 얻은 값인데, 평균과 분산을 직접 구하기보다 대개 이 값을 그대로 사용하는 편이고, 성능도 잘 나오는 편이다.
데이터셋 및 데이터 로더 생성
dataset_train = ImageDataset(df=train, img_dir='train/', transform=transform_train)
dataset_valid = ImageDataset(df=valid, img_dir='train/', transform=transform_test)
from torch.utils.data import DataLoader # 데이터 로더 클래스
loader_train = DataLoader(dataset=dataset_train, batch_size=32, shuffle=True)
loader_valid = DataLoader(dataset=dataset_valid, batch_size=32, shuffle=False)
11.4.2 모델 생성
import torch.nn as nn # 신경망 모듈
import torch.nn.functional as F # 신경망 모듈에서 자주 사용되는 함수
class Model(nn.Module):
# 신경망 계층 정의
def __init__(self):
super().__init__() # 상속받은 nn.Module의 __init__() 메서드 호출
# 1 ~ 5번째 {합성곱, 배치 정규화, 최대 풀링} 계층
self.layer1 = nn.Sequential(nn.Conv2d(in_channels=3, out_channels=32,
kernel_size=3, padding=2),
nn.BatchNorm2d(32), # 배치 정규화
nn.LeakyReLU(), # LeakyReLU 활성화 함수
nn.MaxPool2d(kernel_size=2))
self.layer2 = nn.Sequential(nn.Conv2d(in_channels=32, out_channels=64,
kernel_size=3, padding=2),
nn.BatchNorm2d(64),
nn.LeakyReLU(),
nn.MaxPool2d(kernel_size=2))
self.layer3 = nn.Sequential(nn.Conv2d(in_channels=64, out_channels=128,
kernel_size=3, padding=2),
nn.BatchNorm2d(128),
nn.LeakyReLU(),
nn.MaxPool2d(kernel_size=2))
self.layer4 = nn.Sequential(nn.Conv2d(in_channels=128, out_channels=256,
kernel_size=3, padding=2),
nn.BatchNorm2d(256),
nn.LeakyReLU(),
nn.MaxPool2d(kernel_size=2))
self.layer5 = nn.Sequential(nn.Conv2d(in_channels=256, out_channels=512,
kernel_size=3, padding=2),
nn.BatchNorm2d(512),
nn.LeakyReLU(),
nn.MaxPool2d(kernel_size=2))
# 평균 풀링 계층
self.avg_pool = nn.AvgPool2d(kernel_size=4)
# 전결합 계층
self.fc1 = nn.Linear(in_features=512 * 1 * 1, out_features=64)
self.fc2 = nn.Linear(in_features=64, out_features=2)
# 순전파 출력 정의
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.layer5(x)
x = self.avg_pool(x)
x = x.view(-1, 512 * 1 * 1) # 평탄화
x = self.fc1(x)
x = self.fc2(x)
return x
1. 신경망을 2층에서 5층으로 더 깊게 만들었다. 신경망이 깊을 수록 예측력이 좋아지는 편이다. 다만 과적합을 유의하자.
2. Sequential에 배치 정규화를 추가하여 학습속도 상승, 기울기 소실/폭발 문제, 과적합 방지, 가중치 초기화 민감도 해소를 기대할 수 있다.
3. 활성화 함수를 Relu가 아닌 LeakyRelu를 썼다. Relu는 x=0 이하의 값을 0으로 반환하지만, LeakyRelu는 약한 경사가 있다. 기술기 소실을 방지한다.
4. 전결합 계층을 두 개로 늘렸다.
model = Model().to(device)
11.4.3 모델 훈련
손실 함수와 옵티마이저
# 손실 함수
criterion = nn.CrossEntropyLoss()
# 옵티마이저
optimizer = torch.optim.Adamax(model.parameters(), lr=0.00006)
baseline 단계에서는 SGD를 사용했지만, 이번에는 Adamax를 사용했다. Adam의 개선 버전이다. Adam은 adagrad와 momentum을 합친 옵티마이저라고 볼 수 있다.
모델 훈련
epochs = 70 # 총 에폭
# 총 에폭만큼 반복
for epoch in range(epochs):
epoch_loss = 0 # 에폭별 손실값 초기화
# '반복 횟수'만큼 반복
for images, labels in loader_train:
# 이미지, 레이블 데이터 미니배치를 장비에 할당
images = images.to(device)
labels = labels.to(device)
# 옵티마이저 내 기울기 초기화
optimizer.zero_grad()
# 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
outputs = model(images)
# 손실 함수를 활용해 outputs와 labels의 손실값 계산
loss = criterion(outputs, labels)
# 현재 배치에서의 손실 추가
epoch_loss += loss.item()
# 역전파 수행
loss.backward()
# 가중치 갱신
optimizer.step()
print(f'에폭 [{epoch+1}/{epochs}] - 손실값: {epoch_loss/len(loader_train):.4f}')
Cuda의 사용률이 저조하고 CPU는 빠르게 돌아가는 이유를 알아봐야할 것 같다.
CPUI에서 전처리를 해 Batch를 만들면 GPU가 학습한다. 이 때 GPU가 Batch의 연산을 끝냈는데 CPU가 다음 Batch를 준비 못 했기 때문일 것이다. Batch size를 줄이거나 CPU 연산을 최적화할 방법을 찾아야 할 것 같다.
코드로 신경망을 구현하는 것도 익혀야겠지만, 최적화 방법을 미리 알고 있어야 한정된 시간에 더 많은 시도를 해볼 수 있을 것 같다.
1 epoch를 돌리는 데 로컬에서는 평균값 34초가 걸렸다. [ 파이토치 교과서 ] 에서 구현한 MNIST 훈련보다 훨씬 더 많은 시간이 소요된다. 훈련 시간을 줄이는 방법을 앞으로 배울 기회가 생긴다면 기억해둬야겠다.
kaggle 에서 GPU T4 2X를 사용했을 때, 1 epoch 당 평균 54초가 걸렸다. GPU 가속을 해도 CPU가 처리할 정보가 많아서 병목이 생긴 것 같다.
11.4.4 성능 검증
from sklearn.metrics import roc_auc_score # ROC AUC 점수 계산 함수 임포트
# 실제값과 예측 확률값을 담을 리스트 초기화
true_list = []
preds_list = []
model.eval() # 모델을 평가 상태로 설정
with torch.no_grad(): # 기울기 계산 비활성화
for images, labels in loader_valid:
# 이미지, 레이블 데이터 미니배치를 장비에 할당
images = images.to(device)
labels = labels.to(device)
# 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
outputs = model(images)
preds = torch.softmax(outputs.cpu(), dim=1)[:, 1] # 예측 확률값
true = labels.cpu() # 실제값
# 예측 확률값과 실제값을 리스트에 추가
preds_list.extend(preds)
true_list.extend(true)
# 검증 데이터 ROC AUC 점수 계산
print(f'검증 데이터 ROC AUC : {roc_auc_score(true_list, preds_list):.4f}')
11.4.5 예측 및 결과 제출
dataset_test = ImageDataset(df=submission, img_dir='test/',
transform=transform_test)
loader_test = DataLoader(dataset=dataset_test, batch_size=32, shuffle=False)
# 예측 수행
model.eval() # 모델을 평가 상태로 설정
preds = [] # 타깃 예측값 저장용 리스트 초기화
with torch.no_grad(): # 기울기 계산 비활성화
for images, _ in loader_test:
# 이미지 데이터 미니배치를 장비에 할당
images = images.to(device)
# 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
outputs = model(images)
# 타깃값이 1일 확률(예측값)
preds_part = torch.softmax(outputs.cpu(), dim=1)[:, 1].tolist()
# preds에 preds_part 이어붙이기
preds.extend(preds_part)
submission['has_cactus'] = preds
submission.to_csv('submission.csv', index=False)
import shutil
shutil.rmtree('./train')
shutil.rmtree('./test')