본문 바로가기

공부/AI

[Kaggle] PetFinder.my - Pawpularity Contest 후기 (Top 3%, Silver Medal)

 

캐글 PetFinder.my - Pawpularity Contest (AI 경진대회) 리뷰 

 

 
 

📌 대회 설명

 
1. 개요 : Analyze raw images and metadata to predict the “Pawpularity” of pet photos. (이미지와 메타데이터를 기반으로 반려동물 사진의 인기도를 예측)
 
2. 평가 기준 : RMSE 
 
3. 대회 기간 : 2021.09.23 ~ 2022.01.14 
 
4. 대회 링크 : https://www.kaggle.com/competitions/petfinder-pawpularity-score/overview/description
 
 
 

📌 점수 기록

 
Leaderboard Private 17.03501 으로 90등 / 3537 (Top 3%)
Silver Medal 실버메달 획득 
 
 
 

📌 데이터 설명

 
 train 디렉토리의 jpg 형식 이미지들
 
모델 학습에 사용하는 반려동물 이미지들을 포함. 
 
 
 test 디렉토리의 jpg 형식 이미지들
 
모델 추론에 사용하는 반려동물 이미지들을 포함. kaggle 특성상 test 데이터에 접근 불가능. 
 
 
 train.csv
 
1. Id : 반려동물 이미지의 파일명에 대응하는 고유한 profile id
2. Focus : Pet stands out against uncluttered background, not too close / far. 동물이 배경에 묻히지 않고 지나치게 가깝거나 멀지 않게 돋보이는지 여부.
3. Eyes : Both eyes are facing front or near-front, with at least 1 eye / pupil decently clear. 두개의 눈이 정면을 바라보고 있고 적어도 한개의 눈, 동공이 선명한지 여부.
4. Face : Decently clear face, facing front or near-front. 얼굴이 충분히 선명하고 정면을 바라보고 있는지 여부.
5. Near : Single pet taking up significant portion of photo (roughly over 50% of photo width or height). 각 동물이 사진 내에서 특정한 위치에 있는지 여부(너비와 높이에서 50% 이상 차지).
6. Action : Pet in the middle of an action (e.g., jumping). 동물이 어떤 행동을 하고 있는지 여부(예를 들어 점핑을 하고 있는 등).
7. Accessory : Accompanying physical or digital accessory / prop (i.e. toy, digital sticker), excluding collar and leash. 물리적인 악세서리(장난감 등)를 하고 있거나 디지털 스티커 등으로 꾸미고 있는지 여부 (목줄, 목걸이 제외)
8. Group : More than 1 pet in the photo. 사진에서 동물이 1마리 이상인지 여부.
9. Collage : Digitally-retouched photo (i.e. with digital photo frame, combination of multiple photos). 디지털로 리터치된 사진인지 여부(디지털 사진 프레임, 여러 사진이 콜라쥬된 것 등).
10. Human : Human in the photo. 사진 내에 인간이 있는지 여부.
11. Occlusion : Specific undesirable objects blocking part of the pet (i.e. human, cage or fence). Note that not all blocking objects are considered occlusion. 특정한 물체(사람, 케이지, 펜스 등)가 동물과 맞불려서 가리고 있는지 여부.
12. Info : Custom-added text or labels (i.e. pet name, description). 텍스트(동물의 이름, 설명)이 추가되어 있는지 여부.
13. Blur : Noticeably out of focus or noisy, especially for the pet’s eyes and face. For Blur entries, “Eyes” column is always set to 0. 동물(특히 동물의 눈이나 얼굴)이 눈에 띄게 포커스에서 나가거나 흐린지 여부. Blur 가 1 이면 Eye 컬럼은 항상 0이다. 
14. train.shape: (9912, 14)
 
 
 test.csv
 
1. features는 train 데이터와 동일하므로 설명 생략.
2. test.shape: kaggle 특성상 test 데이터에 접근 불가능
 
 
 

📌 문제 접근

  
YOLOv5 object detection 를 통한 사진 속 pets의 개체 수, 고양이 여부, 강아지 여부 정보 및 이미지의 사이즈 정보 등을 추가 
 
1. 대회에서 제공한 이미지를 살펴보니 반려동물의 종류와 개체 수가 유의미할 것이라는 판단 하에 YOLOv5를 통한 object detection으로 n_pets(이미지 내 반려동물의 개체 수), cat(고양이 여부), dog(개 여부), etc (기타 개체 여부)정보를 추가하였다.

!git clone https://github.com/ultralytics/yolov5

yolov5 = torch.hub.load('ultralytics/yolov5', 'yolov5x6')

def add_obj_info(df):
    df[['n_pets', 'cat', 'dog', 'etc']] = 0

    n_pets = []
    cat = []
    dog = []
    etc = [] 

    for idx, path in enumerate(df['path'].values):
        img = imageio.imread(path)
        result = yolov5(img)
        
        num, cat_flag, dog_flag = 0, 0, 0

        for xmin, ymin, xmax, ymax, confidence, label in result.xyxy[0].cpu().detach().numpy():
            label = result.names[int(label)] 

            if label == 'cat':
                cat_flag = 1
                num += 1
        
            elif label == 'dog':
                dog_flag = 1 
                num += 1 

        n_pets.append(num)

        cat.append(cat_flag)
        dog.append(dog_flag)

        if cat_flag == 0 & dog_flag == 0:
            etc.append(1)
        else:
            etc.append(0) 
    
    df[['n_pets', 'cat', 'dog', 'etc']] = np.vstack([n_pets, cat, dog, etc]).T

    return df
    
    df = add_obj_info(df)

 
2. 이 대회에서는 이미지의 인기도를 판단하는 대회이므로 이미지의 사이즈 및 비율도 인기도에 영향을 줄 것이라는 가정 하에 width, height, hw_ratio, wh_ratio, size 정보를 추가하였다. 

from sklearn.preprocessing import MinMaxScaler

def size_and_shape(x):
    img = Image.open(x['path'])
    return pd.Series([img.size[0], img.size[1], img.size[1]/img.size[0], img.size[0]/img.size[1], os.path.getsize(x['path'])]) 

mms = MinMaxScaler()
df[['width', 'height', 'hw_ratio', 'wh_ratio', 'size']] = pd.DataFrame(mms.fit_transform(df.apply(size_and_shape, axis=1).values))
df.head()

 
 
✔ 연속형 종속변수를 비닝하여 StratifiedKFold를 통한 cross validation 진행
 
Pawpularity를 normalize한 score에 대하여 Sturges' rule을 사용하여 구간화한 후, 이를 기준으로 StratifiedKfold를 진행하여 데이터를 split하였다. 참고로 Sturges' rule 이란 히스토그램에서 데이터를 표현하기에 가장 적절한 구간화 개수를 구하는 방법이다.

from sklearn.model_selection import StratifiedKFold

num_bins = int(np.floor(1+np.log2(len(df))))
df['bins'] = pd.cut(df['norm_score'], bins=num_bins, labels=False)

df['fold'] = -1

skf = StratifiedKFold(n_splits=args.n_folds, random_state=args.seed, shuffle=True)
for i, (_, train_index) in enumerate(skf.split(df.index, df['bins'])):
    df.loc[train_index, 'fold'] = i
    
df['fold'] = df['fold'].astype('int')

 
 
Image Data Augmentation 
 
1. albumentations 라이브러리를 활용한 transform을 진행하였다.
 
2. 이미지 데이터 증강을 하되, 이 대회는 단순히 이미지 분류가 아니라 이미지의 인기도를 예측하는 과제이므로 이미지 변형이 많이 되면 인기도 예측에 방해가 될 것이라는 판단 하에 기본적인 transform을 적용하였다.

def train_transforms():
    return A.Compose([
                      A.Resize(args.image_size, args.image_size),
                      A.HueSaturationValue(
                          hue_shift_limit=0.2, sat_shift_limit=0.2, 
                          val_shift_limit=0.2, p=0.5
                          ),
                      A.RandomBrightnessContrast(
                          brightness_limit=(-0.1, 0.1),
                          contrast_limit=(-0.1, 0.1), p=0.5
                      ),
                      A.Normalize(
                          mean=[0.485, 0.456, 0.406], 
                          std=[0.229, 0.224, 0.225], 
                          max_pixel_value=255.0, p=1.,
                          ),
                      ToTensorV2(p=1.),
    ], p=1.)
    
def valid_transforms():
    return A.Compose([
                      A.Resize(args.image_size, args.image_size),
                      A.Normalize(
                          mean=[0.485, 0.456, 0.406], 
                          std=[0.229, 0.224, 0.225], 
                          max_pixel_value=255.0, p=1.,
                          ),
                      ToTensorV2(p=1.),
    ], p=1.)

 
 
TTA (Test Time Augmentation)
 
fast.ai의 Learner에서 기본적으로 제공하는 tta 함수를 사용하여 test time augmentation를 진행하였다. inference 과정에서 test 데이터에 대해 augmentation을 적용한 후 증강된 데이터들에 대한 예측 확률을 평균하여 최종 예측값을 도출하기 때문에 성능이 향상되었다. 

preds, _ = learn.tta(dl=test_dataloader, 
                        n=6, 
                        beta=0)

 

 

 Regression 문제를 Binary Classification 문제로 전환 
 
1. 이 대회는 반려동물 이미지의 pawpularity(인기도)를 1~100까지로 예측하는 regression 문제이지만, 이에 대하여  normalize해줄 경우에는 '해당 이미지를 선호하느냐 아니냐'에 대한 binary classification 문제영역으로 바뀌게 된다. 즉 target 자체를 점수 예측이 아닌 '선호'라는 label에 대한 확률값으로 인식할 수 있게끔 하여 성능이 향상된다.

 
2. regression 문제를 binary classification 문제로 변환하기 위해 targets를 100으로 나누어 0~1 사이로 normalize하여 확률값처럼 사용하였다. 

df['norm_score'] = df['Pawpularity']/100

 

 
3. targets을 normalize했기 때문에 metric 값을 구할 때에는 이를 보정해주기 위해 outputs에는 sigmoid 적용 후 100 곱하기를 해주고 targets에는 100을 곱해서 비교해주어야 한다. 그리고 loss를 계산할 때에도 regression 문제로 접근할 땐 MSELoss를 사용하지만, classification 문제로 접근할 경우 BCEWithLogitsLoss (Binary Cross Entropy with logits)을 사용한다.

outputs = outputs.cpu().detach().numpy()
targets = targets.cpu().detach().numpy() 

outputs = [sigmoid(x) * 100 for x in outputs]
targets = [x * 100 for x in targets]

rmse = metrics.mean_squared_error(targets, outputs, squared=False)

 


 이미지 데이터와 메타 데이터를 모두 학습하는 multi-modal learning 사용 
 
1. 이 대회에서는 이미지 데이터와 메타 데이터가 함께 제공되고 있기 때문에 multi-modal learning을 진행하였다. 
 
2. input으로 이미지 데이터와 함께 대회에서 제공한 meta 데이터('Subject Focus', 'Eyes', 'Face', 'Near', 'Action', 'Accessory', 'Group', 'Collage', 'Human', 'Occlusion', 'Info', 'Blur')를 학습한 모델과, 이미지 데이터 및 대회에서 제공한 meta 데이터 중 일부('Eyes', 'Action', 'Accessory', 'Group', 'Blur'만 사용)와 YOLOv5를 통하여 새롭게 얻은 정보를 학습한 모델, 이미지 데이터만 사용한 모델 총 3가지를 블렌딩 앙상블하였다. 


 
✔ 3가지 모델을 블렌딩 앙상블 
 
1. Model 1 :  PyTorch, swin_large_patch4_window12_384 
input : 이미지 데이터와 대회에서 제공된 meta 데이터('Subject Focus', 'Eyes', 'Face', 'Near', 'Action', 'Accessory', 'Group', 'Collage', 'Human', 'Occlusion', 'Info', 'Blur') 사용

2. Model 2 : fast.ai, YOLOv5, swin_large_patch4_window7_224 
input : 이미지 데이터와 대회에서 제공된 meta 데이터 중 일부('Eyes', 'Action', 'Accessory', 'Group', 'Blur'만 사용)와 YOLOv5를 통해 사진 속 object detection을 하여 새롭게 얻은 features('n_pets', 'cat', 'dog', 'etc', 'width', 'height', 'hw_ratio', 'wh_ratio', 'size') 사용 

3. Model 3 : fast.ai, swin_large_patch4_window7_224
input : 이미지 데이터만 사용

4. Blending Ensemble : 모델1, 2, 3의 weight를 0.2, 0.3, 0.5로 설정하여 블렌딩 앙상블을 진행하였다.
 
 
 

📌 pyTorch를 사용한 모델링 

 
✔ swin transformer 384 모델을 사용한 모델링 
 
swin_large_patch4_window12_384 모델을 사용하여 이미지 데이터의 embeddings을 추출한다. 이렇게 추출한 embeddings를 tabular features와 결합한 뒤, FC layer에서 예측을 진행하게 된다.

class PawModel(nn.Module):
    def __init__(self):
        super().__init__() 
        self.embedder =  timm.create_model('swin_large_patch4_window12_384',
                                           pretrained=True,
                                           in_chans=3) 
        self.embedder.head = nn.Linear(self.embedder.head.in_features, 128)
        self.dropout = nn.Dropout(0.1)
        self.fc1 = nn.Linear(128+12, 64) 
        self.fc2 = nn.Linear(64, 1)
        
    def forward(self, image, meta):
        x = self.embedder(image) 
        x = self.dropout(x) 
        x = torch.cat([x, meta], dim=1) 
        output = self.fc1(x) 
        output = self.fc2(output)

        return output

 
 

📌 fast.ai를 사용한 모델링 

 
✔ swin transformer 224 모델을 사용한 모델링 
 
swin_large_patch4_window7_224 모델을 사용하여 이미지 데이터의 embeddings을 추출한다. 이렇게 추출한 embeddings를 tabular features와 결합한 뒤, FC layer에서 예측을 진행하게 된다. fast.ai를 사용하여 학습을 진행할 것이므로 PawModel()을 fast.ai의 Learner에 넣어준다.
또한 모델 내부에서 dropout을 서로 다르게 한 결과를 블렌딩하였다. self.dropouts = nn.ModuleList([nn.Dropout(0.5) for _ in range(5)]) 을 통해 dropout rate이 0.5인 layer 5개가 담긴 리스트인 dropouts을 생성하고, forward() 함수에서 dropouts 리스트에 iterable하게 접근해서 각각 dropout을 다르게 한 5개 결과들을 블렌딩해주었다. 

class PawModel(nn.Module):
    def __init__(self):
        super().__init__() 
        self.model = timm.create_model('swin_large_patch4_window7_224',
                                       pretrained=True,
                                       in_chans=3) 
        self.dropouts = nn.ModuleList([nn.Dropout(0.5) for _ in range(5)])
        self.meta = nn.Sequential(
            nn.Linear(len(tabular_features), 256),
            nn.BatchNorm1d(256), 
            nn.SiLU(),
            nn.Dropout(p=0.3),
            nn.Linear(256, 128),
            nn.BatchNorm1d(128),
            nn.SiLU(),
        )
        self.fc = nn.Linear(self.model.head.in_features + 128, 1)
        self.model.head = nn.Identity() 
    
    def forward(self, image, tabular):
        x = self.model(image)
        x = x.squeeze(-1).squeeze(-1) 
        x_meta = self.meta(tabular)
        x = torch.cat([x, x_meta], dim=1) 

        for i, dropout in enumerate(self.dropouts):
            if i == 0:
                output = self.fc(dropout(x))
            else:
                output += self.fc(dropout(x))

        output /= len(self.dropouts) 
        return output
        
        
 def get_learner(fold):
    dataloader = get_data(fold) 
    model = PawModel()
    learn = Learner(dataloader, 
                    model, 
                    loss_func=BCEWithLogitsLossFlat(), 
                    metrics=petfinder_rmse, 
                    ).to_fp16()
    
    return learn

 

반응형