경진대회 소개
이번 글에서는 Image Classification 경진대회에 대해서 그간 해온 방법을 정리해보려고 한다.
이번 경진대회에서는 아날로그 문서를 디지털화할 때 그 이미지를 보고 어떤 종류의 문서인지 분류하는 CV 경진대회였다.
현재 학습 데이터는 1570장의 정상 이미지 데이터고 테스트 데이터는 3140장의 노이즈가 심한 이미지 데이터다.
class는 총 17개로 문서도 있고 자동차 대시보드같은 문서가 아닌 일반 이미지 데이터도 들어있다.
평가지표는 Macro F1를 사용했다.
이제 여러가지 기법을 적용해 보면서 모델의 성능을 높여보자!
데이터 증강 기법 적용
우선 일차원적으로 생각했을때 테스트 데이터셋에 노이즈가 많기 때문에 학습할 때도 노이즈가 많은 데이터를 학습하면 좋은 성능을 낼 것 같기도 하고, 학습 데이터가 2000장도 안 돼서 학습 데이터수가 부족하다고 생각했다.
그래서 학습 데이터에 노이즈를 추가하는 데이터 증강 기법을 사용해봤다.
img_size = 224
LR = 1e-3
EPOCHS = 1
BATCH_SIZE = 32
num_workers = 0
# augmentation을 위한 transform 코드
trn_transform = A.Compose([
# 이미지 크기 조정
A.Resize(height=img_size, width=img_size),
A.RandomRotate90(p=0.5), # 90도 단위로 랜덤 회전
A.HorizontalFlip(p=0.5), # 수평 뒤집기
A.VerticalFlip(p=0.5), # 수직 뒤집기
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=30, p=0.5),
# 이미지 밝기, 대비, 채도 조정
A.RandomBrightnessContrast(p=0.2),
# images normalization
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
# numpy 이미지나 PIL 이미지를 PyTorch 텐서로 변환
ToTensorV2(),
])
그리고 모델은 작은 데이터셋에서도 성능이 좋은 efficientnet_b0를 사용했다. 이 모델은 image size 244에 최적화되어있기 때문에 하이퍼파라미터도 그렇게 맞춰줬다.
이렇게 해서 성능은 0.6999로 나왔다. (기존 baseline 코드의 기본 성능은 0.16 정도 나왔다.)
최적의 하이퍼파라미터 찾기
위 코드에서 꽤 높은 성능을 보인 것 같아서 위 코드에서 하이퍼파라미터를 수정해 줬다.
그런데 생각보다 잘 나오지 않았다.
- epoch 늘려서 했을 때 성능 급감
- 그리드 서치로 찾은 최적의 하이퍼파라미터로 적용했을 때 성능 급감
- 스케줄러 적용했을때 성능 미미하게 올랐지만 그래도 0.7을 넘진 못했다.
이것저것 해보다가 EDA도 중요한 거 같아서 학습 데이터셋의 분포를 확인해 봤다.
이렇게 데이터에 불균형도 있고 그래서 데이터 증강에 대해서 조금 더 파헤쳐봤다.
데이터 증강
이미지 증강은 원본 이미지 파일 자체를 새로 생성하여 파일 수를 늘리는 것이 아니라 모델이 학습할 때마다 원본 이미지를 변형하여 다양한 형태의 데이터를 제공하는 방식으로 작동한다.
다시 말하자면, image1.jpg라는 파일이 있을 때, 증강을 통해 image1_1.jpg, image1_2.jpg 등의 파일이 실제로 생성되는 것이 아니다. 대신, DataLoader에서 매번 이미지를 불러올 때마다 증강을 적용하여 이미지의 모양이나 색상, 구도가 달라진 데이터를 모델에 입력한다.
새로운 이미지 파일을 생성하지 않고 메모리 상에서만 변형하기 때문에 디스크 용량을 추가로 차지하지 않는다.
image1.jpg가 여러 가지 방식으로 변형되어 모델에 제공되기 때문에 모델이 다양한 상황에 대한 이미지 변형을 학습할 수 있어 일반화 성능이 향상된다.
그리고 데이터 증강은 데이터셋의 원본 크기를 증가시키지 않지만 학습 과정에서는 실질적으로 더 많은 데이터가 모델에 입력되는 효과를 낸다. 예를 들어, DataLoader가 image1.jpg를 불러올 때마다 밝기, 회전, 크기 조정 등의 변형을 다르게 적용하여 매번 새로운 형태로 모델에 전달된다. 그래서 이 이미지 증강을 더 효과적으로 적용하기 위해서는 epoch를 늘려야겠다는 생각이 들었다.
왜냐하면 DataLoader는 각 에포크마다 trn_loader에서 배치를 불러올 때마다 증강을 적용한다. 따라서 에포크를 반복할 때마다 같은 이미지를 다양한 형태로 변형된 상태로 모델에 전달하게 된다.
데이터 증강과 에포크의 관계
- 에포크가 1일 때: 모든 데이터에 대해 증강이 단 한 번만 적용되어 모델은 각 이미지를 한 번만 변형된 상태로 학습한다. 이 경우 데이터 증강의 효과가 제한적이다.
- 에포크가 증가할 때: 에포크마다 같은 이미지를 새롭게 증강하여 매번 다른 변형으로 모델에 입력된다. 에포크를 늘리면 모델이 같은 이미지를 여러 번 보면서 다양한 변형을 학습하므로 일반화 성능이 향상될 가능성이 높아진다.
따라서 증강의 효과를 제대로 활용하려면 에포크 수를 1보다 크게 설정하여 여러 번 학습하는 것이 좋다. 이를 통해 모델이 같은 이미지를 다양한 변형으로 학습할 수 있다. 증강이 에포크마다 다르게 적용되는지 확인하고 싶다면 각 에포크에서 불러오는 이미지를 출력해 보는 방식으로 확인할 수도 있다.
데이터 증강 기법 추가
위 코드에서 더 다양한 노이즈를 심어주고 epoch도 3으로 늘려주고 실험을 돌려봤다.
LR = 1e-3
EPOCHS = 5
trn_transform = A.Compose([
# 이미지 크기 조정
A.Resize(height=img_size, width=img_size),
# 회전, 뒤집기, 스케일 조정 등 추가적인 증강 기법
A.Rotate(limit=45, p=0.5), # -45도에서 45도 사이의 각도로 랜덤 회전
A.HorizontalFlip(p=0.5), # 수평 뒤집기
A.VerticalFlip(p=0.5), # 수직 뒤집기
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=45, p=0.5),
# 밝기, 대비, 채도, 색조 조정
A.RandomBrightnessContrast(p=0.2), # 밝기 및 대비 조정
A.HueSaturationValue(hue_shift_limit=20, sat_shift_limit=30, val_shift_limit=20, p=0.3), # 색조 및 채도 조정
# Gaussian 노이즈 추가
A.GaussNoise(var_limit=(10.0, 50.0), p=0.3),
A.MotionBlur(blur_limit=3, p=0.2),
A.GaussianBlur(blur_limit=3, p=0.2), # 가우시안 블러 추가
A.OpticalDistortion(distort_limit=0.05, shift_limit=0.05, p=0.3), # 비뚤어짐 적용
A.GridDistortion(num_steps=5, distort_limit=0.1, p=0.3), # 그리드 왜곡
A.ChannelShuffle(p=0.1), # 채널 순서를 랜덤으로 변경
# 이미지 정규화 및 텐서 변환
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2(),
])
이렇게 하니 과적합 발생했다...
Train - Loss: 0.2784, Acc: 0.8999, F1: 0.8932
Val - Loss: 13.7569, Acc: 0.0615, F1: 0.0068
이런식으로 train에서는 F1 score가 0.89로 높게 나왔는데 예측에서는 0.0068로 사실상 옳은 예측을 못하고 있다.
그래서 lr를 줄이고 스케줄러를 적용해봤는데 더 안좋아지는걸 경험했다...ㅠㅠ
이것저것 또 엄청 해보다가 아예 새로운 마음으로 다시 시작해봤다.
- EarlyStopping 적용(기준 val loss)
- 모델 efficientnet_b3 적용
- optimizer AdamW 적용
- scheduler = CosineAnnealingLR(optimizer, T_max=10, eta_min=1e-5)
img_size = 224
LR = 1e-4
EPOCHS = 10
BATCH_SIZE = 32
num_workers = 0
for epoch in range(EPOCHS):
# 학습 단계
ret = train_one_epoch(trn_loader, model, optimizer, loss_fn, device=device)
ret['epoch'] = epoch
# 검증 단계
val_metrics, preds_list = validate_one_epoch(tst_loader, model, loss_fn, device=device)
val_metrics['epoch'] = epoch
scheduler.step()
log = f"Epoch {epoch+1}/{EPOCHS}\n"
for k, v in ret.items():
log += f"Train {k}: {v:.4f}\n"
for k, v in ret.items():
log += f"Val {k}: {v:.4f}\n"
print(log)
# Early Stopping 체크
if early_stopping(val_metrics["val_loss"]):
print("Early stopping triggered")
break
이렇게 진행하니 그래도 성능이 꽤 좋아졌다!
현재 로컬에서는 0.8416이 나왔다.
이 기세를 몰아 실제 서버에 제출하니 F1 score가 0.6657이 나왔따 ㅠㅠㅠ 대체 왜이럴까....
다시한번 더 데이터 증강 MixUP & CutMix
이전에 시도한 데이터 증강에서 더 다양한 방법을 시도해봤다.
epoch을 돌면서 MixUp과 CutMix 중 하나를 랜덤하게 선택해서 적용하게끔 해봤다. 각각의 기법은 alpha 파라미터로 제어되며 학습 데이터 다양성을 극대화할 수 있다.
💡 alpah 파라미터란?
MixUp과 CutMix에서 ‘alpha 파라미터로 제어된다’라는 것은 이 두 기법이 데이터를 섞는 정도를 조절하는 데 alpha라는 파라미터를 사용한다는 뜻이다.
구체적으로는, MixUp과 CutMix 모두 베타 분포(Beta distribution)에서 샘플링한 값을 통해 두 이미지 또는 이미지 영역을 섞는데, 이 베타 분포의 모양을 결정하는 파라미터가 alpha이다.
• MixUp: 두 이미지를 lambda라는 가중치 비율로 섞어 하나의 새로운 이미지를 만든다. lambda 값은 Beta(α, α) 분포에서 뽑히며, alpha 값이 크면 두 이미지가 거의 비슷한 비율로 섞이고, alpha 값이 작으면 한 이미지에 더 많이 치우쳐진 비율로 섞인다.
• CutMix: 한 이미지의 특정 부분을 잘라서 다른 이미지 위에 덮어씌우는데, 이때도 lambda 값이 Beta(α, α) 분포에서 결정된다. alpha 값이 클수록 두 이미지가 고르게 섞이고, 작을수록 한 이미지의 특정 영역만 섞이는 경향이 생긴다.
즉, alpha 값을 조정함으로써 두 이미지가 섞이는 정도를 조절하여 학습 데이터의 다양성을 극대화할 수 있는 것이다.
MixUp과 CutMix가 적용될 때는 mixup_criterion 함수를 사용하여 손실 함수를 계산하고 적용이 안되었을대는 일반 손실 계산을 하게끔 했다.
# Mixup 적용 함수
def mixup_data(x, y, alpha=0.4):
"""Mixup 데이터 생성 함수"""
if alpha > 0:
lam = np.random.beta(alpha, alpha)
else:
lam = 1
batch_size = x.size()[0]
index = torch.randperm(batch_size).to(x.device)
mixed_x = lam * x + (1 - lam) * x[index, :]
y_a, y_b = y, y[index]
return mixed_x, y_a, y_b, lam
# CutMix 적용 함수
def cutmix_data(x, y, alpha=1.0):
"""CutMix 데이터 생성 함수"""
if alpha > 0:
lam = np.random.beta(alpha, alpha)
else:
lam = 1
batch_size = x.size()[0]
index = torch.randperm(batch_size).to(x.device)
# 이미지의 사각형 영역을 자르고 섞기
bbx1, bby1, bbx2, bby2 = rand_bbox(x.size(), lam)
x[:, :, bbx1:bbx2, bby1:bby2] = x[index, :, bbx1:bbx2, bby1:bby2]
y_a, y_b = y, y[index]
return x, y_a, y_b, lam
def rand_bbox(size, lam):
"""CutMix bounding box 생성"""
W = size[2]
H = size[3]
cut_rat = np.sqrt(1. - lam)
cut_w = int(W * cut_rat) # np.int 대신 int로 변경
cut_h = int(H * cut_rat) # np.int 대신 int로 변경
# 무작위 위치에서 시작하는 좌표
cx = np.random.randint(W)
cy = np.random.randint(H)
bbx1 = np.clip(cx - cut_w // 2, 0, W)
bby1 = np.clip(cy - cut_h // 2, 0, H)
bbx2 = np.clip(cx + cut_w // 2, 0, W)
bby2 = np.clip(cy + cut_h // 2, 0, H)
return bbx1, bby1, bbx2, bby2
# 손실 함수에서 Mixup/CutMix용 가중치를 적용
def mixup_cutmix_criterion(criterion, pred, y_a, y_b, lam):
"""Mixup/CutMix 손실 함수"""
return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)
for images, targets in pbar:
images, targets = images.to(device), targets.to(device)
# MixUp 또는 CutMix를 적용할 확률 설정
use_mixup = random.random() < 0.5
use_cutmix = not use_mixup and (random.random() < 0.5)
if use_mixup:
# MixUp 적용
images, y_a, y_b, lam = mixup_data(images, targets, alpha=mixup_alpha)
preds = model(images)
loss = mixup_criterion(loss_fn, preds, y_a, y_b, lam)
elif use_cutmix:
# CutMix 적용
images, y_a, y_b, lam = cutmix_data(images, targets, alpha=cutmix_alpha)
preds = model(images)
loss = mixup_criterion(loss_fn, preds, y_a, y_b, lam)
else:
# MixUp과 CutMix를 적용하지 않을 때
preds = model(images)
loss = loss_fn(preds, targets)
이렇게 했는데 더 안좋아진것같다...
일단 train f1값도 좋지 않아서 모델 차제가 학습이 잘 안되고 있는것같다..
일단 모델의 크기를 줄여봤다. EfficientNet_b3 모델은 작은 데이터셋에서 학습이 어려울 수 있다 그래서 일단 모델을 EfficientNet_b0 이걸로 수정해본 결과는 다음과 같다.
여전히 학습이 잘 되지 않고 있는것같다.
테스트 데이터셋에 노이즈를 너무 많이 줬나? 싶어서 강력한 왜곡은 줄이고 다시 해봤다.
trn_transform에서 Cutout, CoarseDropout, OpticalDistortion 를 제거하고 다시 실행해봤다.
여전히 이전만큼 성능이 잘 나오지 않고 있다.,
아예 새로운 시도 - 모델 변경
더이상 데이터 증강으로는 뭘 못할거같아서 새로운 접근을 해봤다.
ViT모델을 적용해보았다. ViT 모델은 데이터셋이 엄청 많아야한다고 하지만 일단 트라이해보자
# 모델 및 Feature Extractor 설정
feature_extractor = ViTFeatureExtractor.from_pretrained('google/vit-base-patch16-224-in21k')
model = ViTForImageClassification.from_pretrained('google/vit-base-patch16-224-in21k', num_labels=17)
model = model.to(device)
- ViTFeatureExtractor 이 객체는 ViT 모델에 필요한 입력 형식으로 이미지를 전처리하는 역할을 한다. from_pretrained 메서드를 통해 사전 학습된 모델에 적합한 feature_extractor를 로드하며, img_size, normalize, standardize 등의 설정을 자동으로 가져온다.
- ViTForImageClassification은 ViT 모델을 이미지 분류 작업에 사용하기 위한 클래스다. google/vit-base-patch16-224-in21k는 사전 학습된 모델 가중치를 가져오며, 224x224 크기의 이미지를 16x16 패치 단위로 변환하여 입력한다. num_labels=17로 설정해 17개 클래스 분류를 목표로 한다.
# 학습 설정
img_size = 224
LR = 1e-4 # ViT의 경우 더 낮은 학습률이 적합할 수 있음
EPOCHS = 10 # 더 많은 에폭으로 설정
BATCH_SIZE = 16 # ViT는 메모리 사용량이 높으므로 배치를 작게 설정
num_workers = 4 # 시스템에 따라 조정
- LR: ViT는 파라미터가 많고 더 높은 표현력을 가지고 있어서 일반적으로 EfficientNet보다 낮은 학습률을 사용해야 안정적인 학습이 가능하다.
- EPOCHS: ViT 모델이 일반적으로 많은 양의 데이터와 충분한 반복 학습을 필요로하기때문에 기본적으로 에폭 수를 높게 설정한다.
- BATCH_SIZE: ViT는 높은 메모리 사용량을 요구하기때문에 배치 크기를 작게 설정해야 메모리 초과를 방지할 수 있다.
# 손실 함수 및 옵티마이저 설정
loss_fn = nn.CrossEntropyLoss()
optimizer = AdamW(model.parameters(), lr=LR, weight_decay=1e-4)
- AdamW는 Adam 옵티마이저의 변형으로 weight_decay를 통해 가중치 감소(L2 정규화)를 추가한 방식이다. 이는 ViT 모델처럼 파라미터가 많고 복잡한 모델에 유용하다. weight_decay는 모델의 복잡도를 제한하여 과적합을 방지하는 데 도움을 준다.
이렇게 하고 보니 드디어 로컬에서도 그럴듯한 값이 나왔다.
이렇게 학습을 완료하고 예측한 결과를 서버에 제출하니 드디어 최초 시도한 0.6999를 뛰어넘은 0.7718가 나왔따!!
아직 4등이지만 더 올라갈일만 남았다!!!
'Upstage AI Lab 4기' 카테고리의 다른 글
[CV 경진대회] TTA 실패기,,, (0) | 2024.11.04 |
---|---|
[CV 경진대회] K-fold 적용 (0) | 2024.11.04 |
[Upstage AI Lab 4기] '아파트 실거래가 예측' 경진대회 Private Rank 3등 후기 (1) | 2024.09.17 |
가설 검정 - 유의수준, 검정통계량, 임계값, 기각역 (0) | 2024.08.23 |
집합의 크기 (Cardinality) (0) | 2024.08.22 |