<머신러닝. 딥러닝 문제해결 전략> 3부 11장 실습하고 해당 내용을 정리한 내용입니다.
https://www.kaggle.com/c/aerial-cactus-identification
Aerial Cactus Identification | Kaggle
www.kaggle.com
1. 성능 개선
베이스라인에서는 간단한 CNN모델을 사용했었습니다. 이번 절에서는 다음 네 가지를 개선해 성능을 높여보겠습니다.
1. 다양한 이미지 변환을 수행합니다.
2. 더 깊은 CNN 모델을 만듭니다.
3. 더 뛰어난 옵티마이저를 사용합니다.
4. 훈련 시 에폭 수를 늘립니다.
이상의 네 가지를 제외하고는 베이스라인과 코드가 비슷합니다. 절차는 다음과 같습니다.
시드값 고정 및 GPU 장비 설정 | |
데이터 준비 | 1. 훈련/검증 데이터 분리 2. 데이터셋 클래스 정의 3. 이미지 변환기 정의 4. 데이터셋 생성 5. 데이터 로더 생성 |
모델 생성 (깊은 CNN) | 배치 정규화 Leaky ReLU |
모델 훈련 | 1. 손실 함수와 (더 나은) 옵티마이저 설정 2. 모델 훈련 (에폭 수 증가) |
성능 검증 | |
예측 및 제출 |
2. 데이터 준비
시드값 고정부터 '데이터 준비'의 '2. 데이터셋 클래스 정의'까지는 베이스라인과 동일합니다.
- 이미지 변환과 데이터 증강
앞서 ImageDataset 클래스로 데이터셋을 만들 때 이미지 변환기를 적용할 수 있다고 했습니다. 다음은 많이 쓰이는 이미지 변환의 예입니다.
이렇게 원본 이미지를 다양한 방법으로 하나의 데이터로 더 많은 데이터를 생성이 가능해집니다. 딥러닝 모델을 대체로 훈련 데이터가 많을수록 정확해지므로 확보한 데이터가 부족할 때 특히 유용합니다. 이렇게 이미지를 변환하여 데이터 수를 늘리는 방식을 데이터 증강이라고 합니다.
다음은 파이토치용 컴퓨터 비전 라이브러리인 torchvision의 transforms 모듈이 제공하는 주요 변환기들입니다.
● Compose() : 여러 변환기를 묶어줌
● ToTensor() : PIL이미지나 ndarray를 텐서로 변환
● Pad() : 이미지 주변에 패딩 추가
● RandomHorizontalFlip() : 이미지를 무작위로 좌우 대칭 변화
● RandomVerticalFlip() : 이미지를 무작위로 상하 대칭 변화
● RandomRotation() : 이미지를 무작위로 회전
● Normalize() : 텐서 형태의 이미지 데이터를 정규화
이처럼 다양한 변환기들을 Compose()로 묶어 하나의 변환기처럼 사용할 수 있습니다.
- 이미지 변환기 정의
데이터를 증강해줄 이미지 변환기를 직접 정의해보죠. 성능을 개선하기 위해 다양한 이미지 변환기를 활용할 텐데, 훈련 데이터용과 검증 및 테스트 데이터용을 따로 만듭니다. 훈련 시에는 모델을 다양한 상황에 적응시키는 게 좋지만, 평가 및 테스트 시에는 원본 이미지와 너무 달라지면 예측하기 어려워질 수 있기 때문입니다.
transforms.Compose()로 여러 변환기를 하나로 묶었습니다. 사용된 변환기들을 하나씩 살펴보죠.
ⓐ transforms.ToTensor() : 이미지를 텐서 객체 만듭니다. 이어서 수행되는 다른 transforms 변환기들이 텐서 객체를 입력받기 때문에 가장 앞단에 추가했습니다.
ⓑ transforms.Pad() : 이미지 주변에 패딩을 추가합니다. 여기서는 32를 전달했으므로 32 x 32 크기인 원본 이미지 주변에 32 두께의 패딩을 두른 것입니다. 이미지의 가로, 세로 크기가 각각 세 배가 되겠네요. 한편, padding_mode='symmetric'은 패딩 추가 시 원본 데이터를 상하, 좌우 대칭이 되는 모양으로 만들어줍니다. 상하, 좌우 대칭한 선인장 이미지가 8개나 더 추가된 셈이니 원본 이미지 하나일 때보다 선인장을 더 잘 식별할 거라 기대해볼 수 있습니다.
ⓒ transforms.RandomHorizontalFlip(), ⓓ transforms.RandomVerticalFlip() : 각각 무작위로 이미지를 좌우, 상하 대칭 변환합니다. 파라미터에는 변환할 이미지의 비율을 설정할 수 있으며, 기본값은 0.5입니다. 기본값이면 전체 이미 중 50%를 무작위로 뽑아 대칭을 변환합니다.
ⓔ tranforms.RandomRotation() : 이미지를 회전시킵니다. 파라미터로 10을 전달하면 -10도 ~ 10도 사이의 값만큼 무작위 회전합니다.
ⓕ transforms.Normalize() : 데이터를 지정한 평균과 분산에 맞게 정규화해줍니다. 0 ~ 1 사이 값으로 설정해주면 되는데, 여기서는 평균을 (0.485, 0.456, 0.406)으로, 분산을 (0.229, 0.224, 0.225)로 정규화했습니다.
평균과 분산이 세 개씩 있는 이유와 값이 다음과 같은 이유는 아래와 같습니다.
첫째, 평균과 분산이 각각 세 개씩 있는 이유는 이미지 데이터의 색상을 빨강(R), 초록(G), 파랑(B)으로 구성돼 있기 때문입니다. 따라 빨강, 초록, 파랑을 각각 정규화해야 해서 평균과 분산에 값을 세 개씩 전달한 겁니다.
둘째, 평균은 (0.485, 0.456, 0.406)이고 분산은 (0.229, 0.224, 0.225)인 이유는 다른 값으로 해도 상관없지만 이미지를 다룰 때는 보통 이 값들로 정규화합니다. 이 값들은 백만 개 이상의 이미지를 보유한 이미지넷의 데이터로부터 얻은 값이기에 내가 사용할 이미지들로부터 평균과 분산을 직접 구해도 되지만 번거롭기 때문에 대게 이 값을 그대로 사용합니다. 일반적으로 성능도 잘 나옵니다.
- 데이터셋 및 데이터 로더 생성
ImageDataset 클래스로 훈련 및 검증 데이터셋을 만듭니다. 전달하는 변환기를 제외하면 베이스라인 코드와 똑같습니다. 훈련 데이터셋을 만들 때는 훈련용 변환기를, 검증 데이터셋을 만들 때는 검증/테스트용 변환기를 전달합니다.
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)
앞서 데이터셋을 만들 때 이미지 변환기를 전달했습니다. 그러면 데이터 로더로 데이터를 불러올 때마다 이미지 변환을 수행합니다. 이때 변환기 중 RandomHorizontalFlip(),RandomVerticalFlip(),RandomRotation()은 변환을 무작위로 가하기 때문에 매번 다르게 변환합니다. 즉, 원본 이미지는 같지만 첫 번째 에폭과 두 번째 에폭에서 서로 다른 이미지로 훈련하는 효과를 얻을 수 있는 거죠. 이것이 바로 '데이터 증강' 기법입니다.
3. 모델 생성
데이터가 준비되었으니 이제 CNN모델을 설계해봅시다. 베이스라인에는 합성곱과 최대 풀링 계층이 두 개씩이고, 이어서 평균 풀링 계층과 전결합 계층이 하나씩 있었습니다.
이번에는 더 깊은 CNN을 만들겠습니다. 신경망 계층이 깊어지면 대체로 예측력이 좋아집니다. 다만 지나치게 깊으면 과대적합될 우려가 있으니 유의해야 합니다. 배치 정규화를 적용하고 활성화 함수를 Leaky ReLU로 바꿔서 성능을 더 높이겠습니다. 또한 {합성곱, 배치 정규화, 최대 풀링 계층} 계층이 총 다섯 개에, 전결합 계층도 두 개로 늘리겠습니다.
nn.Sequential()을 활용해 신경망 계층을 설계하겠습니다.
베이스라인보다 복잡하지만, nn.Sequential()을 이용한 점과 계층이 더 많아진 점, 그리고 배치 정규화와 LeakyReLU()를 추가한 점 빼고 전체적인 구조는 비슷합니다. 달라진 부분만 간단히 설명하겠습니다.
ⓐ 의 nn.BatchNorm2d(32)가 배치 정규화를 적용하는 코드입니다. 파라미터로 채널 수를 전달하면 되는데, 바로 앞의 합성곱 연산(nn.Conv2d)을 거치면 채널이 32개가 됩니다.
ⓑ 에서는 활성화 함수를 Leaky ReLU로 지정합니다. ReLU를 적용할 때보다 성능이 조금 더 좋아질 수 있습니다.
ⓒ 에서 전결합 계층 두 개를 정의합니다. nn.Linear()의 파라미터 in_features에 512 * 1* 1 을 전달했네요. 평균 풀링 계층을 거친 후 데이터 개수가 512개여서 그렇습니다. 공식을 통해 하나하나 계산하면 구할 수 있습니다. 또한 크기가 32인 패딩을 추가했기 때문에 초기 이미지의 형상이 (32, 3, 32, 32)가 아니라 (32, 3, 96, 96)입니다.
마지막으로 방금 정의한 Model 클래스를 활용해 CNN 모델을 만든 뒤, 장비에 할당하겠습니다.
model = Model().to(device)
4. 모델 훈련
베이스라인 때와 마찬가지로 손실 함수와 옵티마이저를 설정한 후 훈련에 돌입하겠습니다.
- 손실 함수와 옵티마이저 설정
손실 함수는 베이스라인과 동일하게 CrossEntropyLoss()로 하겠습니다.
# 손실 함수
criterion = nn.CrossEntropyLoss()
옵티마이저는 Adamax로 바꿔보겠습니다. Adamax가 SGD나 Adam보다 항상 나은 결과를 보장하는 건 아니며, 실제로 테스트해보기 전까진 어떤 옵티마이저가 더 좋은지 판단하기 쉽지 않으니 여러 차례 실험을 해보는 게 좋습니다.
학습률을 0.00006으로 낮게 설정해줬습니다.
# 옵티마이저
optimizer = torch.optim.Adamax(model.parameters(), lr=0.00006)
- 모델 훈련
데이터를 증강시켜 훈련할 데이터가 더 많이졌기에 훈련을 더 많이 하기 위해 에폭 수를 10에서 70으로 늘리겠습니다. 나머지 코드는 베이스라인과 동일합니다.
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}')
총 70 에폭만큼 훈련하며 손실값을 출력했습니다. 출력 로그는 아래와 같이 정상적으로 나오는 것을 확인할 수 있습니다.
![]() |
![]() |
![]() |
5. 성능 검증
검증 데이터로 모델 성능을 평가해보겠습니다. 역시 코드는 베이스라인과 다를 바 없습니다.
베이스라인의 ROC AUC는 0.9900이었는데, 개선 작업이 효과를 발휘해 0.9998이 되었습니다. ROC AUC의 최댓값이 1이니 거의 완벽에 가까운 점수입니다. 검증 데이터를 거의 완벽히 분류해냈다는 뜻입니다.
6. 예측 및 결과 제출
이제 테스트 데이터로 예측해보겠습니다.이번에도 transform_test 변환기를 이용해 데이터셋을 만들었습니다.
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입니다. 0.9811을 기록한 베이스라인보다 0.0187 오른 것을 확인할 수 있습니다.
- 한 걸음 더
점수를 조금 더 높일 수 있는 간단한 방법이 있습니다. 전체 모델링 절차는 그대로 두고 '훈련 데이터 전체'로 모델을 훈련하는 것입니다.
최종 점수가 0.9999로 0.9998보다도 0.0001 오른 것을 확인할 수 있습니다.
'IT > 머신러닝&딥러닝' 카테고리의 다른 글
[머신러닝/딥러닝] 항공 사진 내 선인장 식별 경진대회 (2)_베이스라인 모델 (0) | 2023.01.30 |
---|---|
[머신러닝/딥러닝] 항공 사진 내 선인장 식별 경진대회 (1)_탐색적 데이터 분석 (0) | 2023.01.30 |
[머신러닝/딥러닝] 향후 판매량 예측 (3)_모델 성능 개선 (0) | 2022.11.28 |
[머신러닝/딥러닝] 향후 판매량 예측 (2)_베이스라인 모델 (0) | 2022.11.28 |
[머신러닝/딥러닝] 안전 운전자 예측 (1) (0) | 2022.11.07 |