캐글의 항공 사진 내 선인장 식별 경진대회 'Aerial Cactus Identification' compeition에 참가해 딥러닝 모델을 다루는 방법을 연습해 보았다.
드론이 보호 구역을 돌아다니며 찍은 항공사진에서 딥러닝 기술로 선인장을 식별하는 작업을 해내야 한다.
csv파일 뿐 아니라 이미지 파일 또한 활용해 이미지 데이터에 선인장이 있을 확률을 예측해야 한다.
데이터를 불러와 살펴본다.
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')
labels.head()
타깃값 분포
파이그래프로 타깃값 분포를 살펴보면 1:3정도의 비율을 갖는 것을 알 수 있다.
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
mpl.rc('font', size=15)
plt.figure(figsize=(7, 7))
label = ['Has cactus', 'Hasn\'t cactus'] # 타깃값 레이블
# 타깃값 분포 파이 그래프
plt.pie(labels['has_cactus'].value_counts(), labels=label, autopct='%.1f%%');
이미지 출력
ZipFile 클래스를 활용해 압축파일을 푼다.
from zipfile import ZipFile
# 훈련 이미지 데이터 압축 풀기
with ZipFile(data_path + 'train.zip') as zipper:
zipper.extractall()
# 테스트 이미지 데이터 압축 풀기
with ZipFile(data_path + 'test.zip') as zipper:
zipper.extractall()
os.listdr()로 디렉터리에 파일이 몇개나 들어있는지 알아본다.
import os
num_train = len(os.listdir('train/'))
num_test = len(os.listdir('test/'))
print(f'훈련 데이터 개수: {num_train}')
print(f'테스트 데이터 개수: {num_test}')
OpenCV 라이브러리로 선인장을 포함하는 이미지 파일을 읽어온다. 길쭉한 물체인 선인장을 볼 수 있다.
import matplotlib.gridspec as gridspec
import cv2 # OpenCV 라이브러리 임포트
mpl.rc('font', size=7)
plt.figure(figsize=(15, 6)) # 전체 Figure 크기 설정
grid = gridspec.GridSpec(2, 6) # 서브플롯 배치(2행 6열로 출력)
# 선인장을 포함하는 이미지 파일명(마지막 12개)
last_has_cactus_img_name = labels[labels['has_cactus']==1]['id'][-12:]
# 이미지 출력
for idx, img_name in enumerate(last_has_cactus_img_name):
img_path = 'train/' + img_name # 이미지 파일 경로
image = cv2.imread(img_path) # 이미지 파일 읽기
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 이미지 색상 보정
ax = plt.subplot(grid[idx])
ax.imshow(image) # 이미지 출력
선인장을 포함하지 않는 이미지또한 불러와 형태를 살펴본다.
plt.figure(figsize=(15, 6)) # 전체 Figure 크기 설정
grid = gridspec.GridSpec(2, 6) # 서브플롯 배치
# 선인장을 포함하지 않는 이미지 파일명(마지막 12개)
last_hasnt_cactus_img_name = labels[labels['has_cactus']==0]['id'][-12:]
# 이미지 출력
for idx, img_name in enumerate(last_hasnt_cactus_img_name):
img_path = 'train/' + img_name # 이미지 파일 경로
image = cv2.imread(img_path) # 이미지 파일 읽기
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 이미지 색상 보정
ax = plt.subplot(grid[idx])
ax.imshow(image) # 이미지 출력
image.shpae()으로 이미지 형상을 출력해보면 (32,32,3)의 결과를 얻을 수 있는데, 가로, 세로 크기가 32 x 32, 채널 수가 3개인 것을 알 수 있다. R,G,B로 이루어진 컬러 이미지이기에 채널이 3개이다.
시드값 고정 및 GPU 장비 설정 -> 데이터 준비 (훈련/검증 데이터 분리, 데이터셋 클래스 정의, 데이터셋 생성, 데이터 로더 생성) -> 모델 생성 -> 모델 훈련 (손실함수, 옵티마이저 설정, 모델 훈련) -> 성능 검증 -> 예측 및 제출
의 순서로 진행한다.
pytorch를 import하고 시드값을 고정한다. 머신러닝에서의 random_state와 같은 역할로, pytorch 딥러닝 모델링에서는 맨 처음에 고정한다.
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) # 파이토치 난수 생성기 시드 고정 (CPU 사용 시)
torch.cuda.manual_seed(seed) # 파이토치 난수 생성기 시드 고정 (GPU 사용 시)
torch.cuda.manual_seed_all(seed) # 파이토치 난수 생성기 시드 고정 (멀티GPU 사용 시)
torch.backends.cudnn.deterministic = True # 확정적 연산 사용
torch.backends.cudnn.benchmark = False # 벤치마크 기능 해제
torch.backends.cudnn.enabled = False # cudnn 사용 해제
GPU 장비 설정
훈련, 검증 데이터 분리
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
데이터셋 생성
ImageDataset 클래스를 이용해 데이터셋을 만드는데, pytorch 모델로 이미지를 다루려면 이미지 데이터를 Tensor 타입으로 바꾸어야 한다.
from torchvision import transforms # 이미지 변환을 위한 모듈
transform = transforms.ToTensor()
앞에서 정의한 ImageDataset() 클래스를 사용해 훈련, 검증 데이터셋을 만든다.
dataset_train = ImageDataset(df=train, img_dir='train/', transform=transform)
dataset_valid = ImageDataset(df=valid, img_dir='train/', transform=transform)
데이터 로더 생성
데이터 로더는 지정한 배치 크기만큼 데이터를 불러오는 개체로, 딥러닝 모델을 훈련할 때는 주로 배치 단위로 데이터를 가져와 훈련한다.
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)
CNN(합성곱 신경망) 모델을 만드는데, nn.Module을 상속해 정의하고 순전파 후 결과를 반환하는 forward()를 재정의한다.
import torch.nn as nn # 신경망 모듈
import torch.nn.functional as F # 신경망 모듈에서 자주 사용되는 함수
class Model(nn.Module):
# 신경망 계층 정의
def __init__(self):
super().__init__() # 상속받은 nn.Module의 __init__() 메서드 호출
# 첫 번째 합성곱 계층
self.conv1 = nn.Conv2d(in_channels=3, out_channels=32,
kernel_size=3, padding=2)
# 두 번째 합성곱 계층
self.conv2 = nn.Conv2d(in_channels=32, out_channels=64,
kernel_size=3, padding=2)
# 최대 풀링 계층
self.max_pool = nn.MaxPool2d(kernel_size=2)
# 평균 풀링 계층
self.avg_pool = nn.AvgPool2d(kernel_size=2)
# 전결합 계층
self.fc = nn.Linear(in_features=64 * 4 * 4, out_features=2)
# 순전파 출력 정의
def forward(self, x):
x = self.max_pool(F.relu(self.conv1(x)))
x = self.max_pool(F.relu(self.conv2(x)))
x = self.avg_pool(x)
x = x.view(-1, 64 * 4 * 4) # 평탄화
x = self.fc(x)
return x
정의한 Model 클래스로 CNN 모델을 생성하여 device 장비에 할당해주고, model을 출력하면 모델의 전체 구조를 볼 수 있다.
model = Model().to(device)
model
손실 함수로 CrossEntropy를 설정해준다.
# 손실함수
criterion = nn.CrossEntropyLoss()
최적 가중치를 찾아주는 옵티마이저로는 SGD를 사용해준다.
# 옵티마이저
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
모델 훈련 절차
epochs = 10 # 총 에폭
# 총 에폭만큼 반복
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}')
훈련이 끝난 후 valid data로 평가지표인 ROC AUC 값을 구한다.
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}')
test data를 담은 데이터셋과 데이터 로더를 만들어준다. 배치크기는 32로 한다.
dataset_test = ImageDataset(df=submission, img_dir='test/', transform=transform)
loader_test = DataLoader(dataset=dataset_test, batch_size=32, shuffle=False)
test data에서 타깃값이 1일 확률을 예측해 본다. 모델 성능을 검증하는 코드와 비슷하지만 test data에는 타깃값이 없어 for 문에 labels 변수를 할당하지 않았다. tolist()를 호출해 tensor를 list 타입으로 변경해 최종 제출 할 수 있게끔 해준다.
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)
커밋 후 제출하면 0.9837점 정도로 낮은 등수를 기록하기에 성능을 개선하도록한다.
다양한 이미지 변환 수행, 깊은 CNN 모델, 뛰어난 옵티마이저 사용, 에폭 수 증가 등을 통해 성능을 개선시킬 수 있다.
데이터셋 클래스를 정의하는 부분까지 Baseline의 코드와 같고, 이미지 변환기를 정의하는 부분부터 다르다.
이미지의 변환을 통해 데이터 수를 늘리는 '데이터 증강(data augmentation)'을 해준다.
Pad(), RandomHorizontalFlip(), RandomRotation() 등 다양한 변환기를 Compose()로 묶어 하나의 변환기처럼 사용한다.
다양한 이미지 변환기를 활용할 때 훈련 데이터용과 검증 및 테스트 데이터용은 따로 만드는데, 훈련 시에는 다양한 변환을 적용하는 것이 좋지만 검증 및 테스트 시에는 원본 이미지와 너무 달라지면 예측이 어려워지기 때문이다.
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))])
데이터셋 및 데이터 로더를 생성해준다.
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)
모델의 예측력을 높이기 위해 신경망 계층을 Baseline 보다 더 깊게 만들어주고, 배치 정규화를 적용하고 활성화 함수를 LeakyRelu로 바꾸어준다.
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
정의한 Model 클래스를 활용해 CNN 모델을 만든 뒤 device에 할당한다.
model = Model().to(device)
손실 함수와 옵티마이저를 설정해준다.
# 손실 함수
criterion = nn.CrossEntropyLoss()
# 옵티마이저
optimizer = torch.optim.Adamax(model.parameters(), lr=0.00006)
데이터를 증강해 훈련할 데이터가 많아졌으니 에폭을 늘려준다.
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}')
검증 데이터로 모델 성능을 평가한다. Baseline 코드와 같다.
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}')
transform_test 변환기를 이용해 데이터셋을 만들고 test data로 예측한다.
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')
커밋 후 제출하면 최종 점수는 0.9998로 1221명 중 455등으로 상위 37% 정도를 기록했다.
성능을 조금 더 높이기 위해서는 훈련 데이터를 9:1로 나눠 9로만 모델을 훈련하고 1은 검증용으로 쓰고 훈련에 사용하지 않는 방법을 사용하면 된다. 이렇게 하면 성능을 매우 개선하여 최종 점수 0.9999를 기록할 수 있다.
pytorch를 활용해 딥러닝 모델을 구축하는 연습을 할 수 있었고, 이미지 변환, 옵티마이저 등을 다뤄볼 수 있었다.
github에 해당 코드를 올려두었다.
참고: 머신러닝·딥러닝 문제해결 전략 (캐글 수상작 리팩터링으로 배우는 문제해결 프로세스와 전략)
참고: https://www.kaggle.com/code/bonhart/simple-cnn-on-pytorch-for-beginers/notebook