본문 바로가기

공부방/Upstage AI Lab 4기

[CV 문서 분류 대회] 두 번째 날 정리

대회가 시작되고, 첫째 날에는 베이스라인을 토대로 모듈화와 mlflow를 덧붙여서 (편리하길 바라는) 실험 환경을 세팅했다. 그리고 간단한 어그멘테이션을 넣어서 기존 이미지의 5배 크기로 늘리고, rasnet34, efficientnet_b0, densenet121 3가지 모델을 한번 돌려봤다. 결과는 그닥 좋지 않았다. 시험삼아 올린건데 0.39, 0.51ㅋㅋㅋㅋ 

 

둘째 날, 베이스라인 코드를 찬찬히 뜯어봤다. 그리고 정확한 모델 검증을 하기 위해 테스트 전에 거칠 validatino set을 만들었다. 그 다음에 mixup 과 어그맨테이션 다시 만들어보았다. 

Validation set을 잘 만드는 게 중요한데, 테스트 데이터의 분포를 잘 반영해야 얘를 믿고 쓸 수가 있다고 한다.(어떻게..? 이 부분이 잘 이해가 안가서 어디 물어봐야 할 것 같다. 테스트 데이터를 모르는 경우도 있을거고, 이번 대회의 경우는 테스트 데이터를 눈으로 직접 확인할 수 있긴 하지만 이걸 분석해서 테스트 데이터랑 비슷하게 밸리데이션 데이터셋을 나눠야 한다는 말인감.ㅎㅎ 일단 드는 생각은 어그멘테이션을 테스트 데이터랑 최대한 비슷하게 만들어서 밸리데이션 세트에 포함되게 만들어야 겠다는 것.) 

이번 대회는 문서 이미지를 클래스로 분류하는 것이 목표이므로, 테스트 이미지를 눌러서 직접 눈으로 확인할 수가 있었다. 그리고 트레인 데이터셋은 1570장, 테스트 데이터셋은 3140개 이미지로, 테스트가 더 많다. (현실성을 반영했다고..ㅋㅋㅋ) 트레인 데이터가 적으니 일단 무조건 augmentation이 필요했고, 이렇게 늘린 것을 클래스별로 골고루 들어가게 해서 모델이 잘 학습할 수 있도록 만들어주자! 

 

1. Augmentation

1) 맨 처음에 했던 Augmentation:
PyTorch의 torchvision.transforms 라이브러리를 사용한 이미지 어그멘테이션. 트레인데이터 * 5배

    complex_transform = transforms.Compose([
        transforms.RandomHorizontalFlip(), #좌우 반전
        transforms.RandomRotation(15), #-15도에서 +15도 사이의 랜덤한 각도로 회전
        transforms.ColorJitter(brightness=0.2, contrast=0.2), # 밝기와 대비를 랜덤하게 조정
        transforms.GaussianBlur(kernel_size=3), #이미지 흐리게
    ])

특강에서도 강사님이 소개해주셨던 가장 기본적인 라이브러리였지만 자주 쓰이는 건 아니라고 한다. 비전 데이터에 가장 쉽게 적용할 수 있는 어그멘테이션이다. 저기서 5개 요소(반전, 약한 회전, 밝기, 대비, 흐리게) 중에서 (기본값에 세팅된 확률로) 효과들이 적용되어 한 이미지당 5개의 추가 이미지를 얻었다. 

2) 두 번째 Augmentation
토치 비전보다 속도가 더 빠르고, 기능도 다양한 albumentations 라이브러리. 트레인데이터 * 10배 생성

    transform = A.Compose([
        A.OneOf([
            A.Rotate(limit=270, p=1.0),  # 270도까지 회전
            A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.15, rotate_limit=270, p=1.0),  # 이동, 스케일, 회전 동시 적용
            A.Affine(shear=45, p=1.0),  # 기울이기
        ], p=0.99),
        
        A.OneOf([
            A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=1.0),
            A.HueSaturationValue(hue_shift_limit=30, sat_shift_limit=40, val_shift_limit=30, p=1.0),
            A.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.1, p=1.0),
        ], p=0.7),
        
        A.OneOf([
            A.GaussNoise(var_limit=(30.0, 80.0), p=1.0),
            A.MultiplicativeNoise(multiplier=[0.8, 1.2], p=1.0),
            A.ISONoise(p=1.0),
        ], p=0.6),
        
        A.OneOf([
            A.GaussianBlur(blur_limit=(3, 7), p=1.0),
            A.MotionBlur(blur_limit=7, p=1.0),  # 모션 블러
            A.ImageCompression(quality_lower=60, quality_upper=100, p=1.0),  # JPEG 압축 효과
        ], p=0.5),
        
        A.OneOf([
            A.OpticalDistortion(distort_limit=0.5, shift_limit=0.5, p=1.0),  # 광학 왜곡
            A.GridDistortion(num_steps=5, distort_limit=0.3, p=1.0),  # 격자 왜곡
            A.ElasticTransform(alpha=1, sigma=50, alpha_affine=50, p=1.0),  # 탄성 변형
        ], p=0.3),
        
        A.HorizontalFlip(p=0.2),
    ])

 

 

A.Oneof([효과1(), 효과2(), 효과3()], p= )

이런 구조인데, 이미지 하나가 들어갈 때마다 99%의 확률로 첫 번째 효과그룹이 실행되고(Rotate, ShiftScaleRotate, Affine 중 하나), 70% 확률로 두 번째 그룹의 효과가 선택되고(RandomBrightnessContrast, HueSaturationValue, ColorJitter 중 하나, ... 이런 식으로 구성해서 여러 효과가 동시에 적용될 수 있게 했다. 총 6가지 효과 그룹이 있다. 어떤 이미지는 6가지 효과가 모두 적용된 이미지가 될 수도 있고, 어떤 건 2~3개만 적용될 수도 있고, 하나만 적용되는 것도 있을 수 있다. p값으로 확률을 조절할 수 있다. 이렇게 해서 기존 트레인셋의 10배만큼 데이터를 늘렸다. 

다시 해야겠다

+ 추가)
내가 서버에서 처음 시도할 때는 cv2인가 의존성 문제가 있다고 에러가 떳다. opencv, albumentation에 필요한 시스템 라이브러리를 설치해야한다고 해서 설치 후에 시도하니 실행됐다.

apt-get update
apt-get update && apt-get install -y libglib2.0-0 libsm6 libxext6 libxrender-dev libgl1-mesa-glx

 

3) 세 번째 Augmentation
mixup 어그멘테이션

def mixup_data(x, y, alpha=1.0):
    if alpha > 0:
        lam = np.random.beta(alpha, alpha)
    else:
        lam = 1

    batch_size = x.size()[0]
    index = torch.randperm(batch_size)
    mixed_x = lam * x + (1 - lam) * x[index, :]
    y_a, y_b = y, y[index]
    return mixed_x, y_a, y_b, lam

def mixup_loss(loss_fn, pred, labels_a, labels_b, lam):
    return lam * loss_fn(pred, labels_a) + (1 - lam) * loss_fn(pred, labels_b)

사진 두 개를 겹쳐서 하나로 만드는 게 mixup인데, 굳이 믹스업이 필요한가 싶긴 하다. 테스트 이미지 중에서 뭔가 믹스된 듯한 느낌의 이미지가 있기는 한데, 그렇게 많지도 않고. 이건 온라인 어그맨테이션으로 진행했다. (일반적으로 온라인이 권장된다. 메모리 효율과 에폭마다 더 다양한 조합을 생성하는 게 학습 효과에 좋아서.) 그리고 트레인에 5번 배치가 돌 때마다 한번 mixup이 적용되도록 만들었다. 

def train_one_epoch(self):
        self.model.train()
        train_loss = 0
        preds_list = []
        targets_list = []

        pbar = tqdm(self.train_loader)

        for batch_idx, (image, targets) in enumerate(pbar):
                image = image.to(self.device)
                targets = targets.to(self.device)

                self.optimizer.zero_grad()

                # 5배치마다 한 번씩 mixup 적용 (베이스라인 코드와 동일한 비율)
                if (batch_idx + 1) % 5 == 0:
                    mixed_images, targets_a, targets_b, lam = mixup_data(image, targets)
                    preds = self.model(mixed_images)
                    loss = mixup_loss(self.criterion, preds, targets_a, targets_b, lam)
                    
                    # mixup된 예측은 평가에서 제외 (정확한 평가를 위해)
                    with torch.no_grad():
                        real_preds = self.model(image)
                        preds_list.extend(real_preds.argmax(dim=1).detach().cpu().numpy())
                        targets_list.extend(targets.detach().cpu().numpy())
                else:
                    # 일반적인 학습
                    preds = self.model(image)
                    loss = self.criterion(preds, targets)
                    
                    preds_list.extend(preds.argmax(dim=1).detach().cpu().numpy())
                    targets_list.extend(targets.detach().cpu().numpy())

                loss.backward()
                self.optimizer.step()

                train_loss += loss.item()
                preds_list.extend(preds.argmax(dim=1).detach().cpu().numpy())
                targets_list.extend(targets.detach().cpu().numpy())

                pbar.set_description(f"Loss: {loss.item():.4f}")

        train_loss /= len(self.train_loader)
        train_acc = accuracy_score(targets_list, preds_list)
        train_f1 = f1_score(targets_list, preds_list, average='macro')

        return {
            "train_loss": train_loss,
            "train_acc": train_acc,
            "train_f1": train_f1,
        }

https://arxiv.org/pdf/1905.04899.pdf

 

4) 지금 수정하려고 하는 방향
만들어진 어그맨테이션 결과를 보니 생각보다 다양하지 않고 기울어진 각도가 일률적이고 반복된다. 더 다양한 각도로 기울여질 수 있게 바꾸고, 온라인 어그멘테이션에 cutout, mixup, cutmix도 추가해봐야겠다. 

 

2. Validation Set

이건 다른 포스팅에 자세히 정리해야할 듯. 간단히 요약하면 Stratified K fold cross validation을 사용했다. ->

2024.11.09 - [프로젝트] - [CV 문서 분류 대회] 신뢰성 있는 validation set은 어떻게 만들지?