2026. 7. 2. 19:57ㆍ카테고리 없음
이번에는 APTOS 2019 Blindness Detection 대회를 리뷰해보려고 합니다. 2019년은 구글의 EfficientNet이 발표된지 얼마 안된 시점이여서 상위권 팀들의 코드를 보면 EfficientNet 계열을 앙상블한 코드가 많이 보입니다. ViT 모델을 공부하고, 적용해볼만한 데이터셋을 찾던 와중 2019년에 캐글에서 3600장의 실제 안저 사진을 이용해 주최한 대회를 알게되었습니다. 대회가 끝난지는 오래되었지만, 의료데이터에 비전 트랜스포머 모델을 적용해보고 싶어서 늦게나마 참여하게 되었습니다.
아래는 해당 노트북 링크입니다.
- 주최: APTOS(Asia Pacific Tele-Ophthalmology Society, 아시아 태평양 원격안과 의학회)
- 목표: 인도의 Aravind Eye Hospital이 의료 스크리닝이 어려운 시골 지역에서 당뇨망막병증 환자를 발견하고 예방
대회 개요
https://www.kaggle.com/competitions/aptos2019-blindness-detection/overview
APTOS 2019 Blindness Detection
Detect diabetic retinopathy to stop blindness before it's too late
www.kaggle.com
Kernels-only competition이며, 여러 지역에서 얻은 retina images를 사용해 모델을 학습시키고, 예측하는 대회입니다.
label 0은 무증상, 4로 갈수록 증상이 심해집니다.
평가지표
Quadratic weighted kappa(이차 가중 카파)를 사용합니다.
자세한 내용은 이차 가중 카파를 읽어보시기 바랍니다.
간단하게 설명하자면, 예측과 정답의 차이가 클수록 점수를 더 크게 깎는 지표입니다. label 4데이터를 0으로 예측한것과 3으로 에측한 것이 똑같은 오답 처리가 되면 안되기 때문에 사용합니다.
또한, 그 평가 결과가 우연이 아니라 얼마나 실제로 일치하는지 평가하기도 합니다. 이는 데이터에 특정 클래스(무증상 등)이 많을때 모델이 무조건 무증상으로 예측하는 것을 막기 위함입니다.
탐색적 데이터 분석(Exploratory Data Analysis)
# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load
import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
# Input data files are available in the read-only "../input/" directory
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory
import os
count=0
for dirname, _, filenames in os.walk('/kaggle/input'):
for filename in filenames:
print(os.path.join(dirname, filename))
count+=1
if count==20:
break
if count==20:
break
# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All"
# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session
# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session
# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md
import kagglehub
# kagglehub.dataset_download('<owner>/<dataset-slug>')
from dataclasses import dataclass
@dataclass #__init__을 생략해줌. 기존 방식(클래스에 속성 부여)은 속성 하나 바꾸려면 클래스 자체가 바뀌어야 했는데, 이 방식은 a=BaseConfig(BATCH_SIZE=64)으로 변경 가능
class BaseConfig:
EPOCHS: int = 10
BATCH_SIZE: int = 32
LR: float = 3e-5
IMG_SIZE: int = 384
SEED: int = 42
dataclass모듈을 사용해서 Config를 정의합니다. 이렇게 하면 클래스 자체를 건들지 않고도 속성들을 조정할 수 있게 됩니다.
import matplotlib.pyplot as plt
from glob import glob #glob 라이브러리 안에 glob 함수 사용
from PIL import Image
import cv2
import numpy as np
import pandas as pd
import numpy as np
set_seed()
SEED=42
config=BaseConfig()
#학습/테스트 데이터 목록 확인
train_df=pd.read_csv('../input/competitions/aptos2019-blindness-detection/train.csv')
test_df=pd.read_csv('../input/competitions/aptos2019-blindness-detection/test.csv')
train_df = train_df.sample(frac=1, random_state=config.SEED).reset_index(drop=True) #df의 행을 랜덤 샘플링(frac은 샘플링 비율을 설정)-아렇게 해야 같은 병변끼리 모여있는 상황 최소화
#random state 안쓰면 넘파이 난수 엔진 참고, 따로 고정할 필요 없음
train_df.head(3)
데이터 분포 확인
from sklearn.model_selection import train_test_split
x=train_df['id_code']
y=train_df['diagnosis']
train_x, valid_x, train_y, valid_y=train_test_split(x, y, test_size=0.15, stratify=y)
print(train_x.shape, valid_x.shape)
fig, axes=plt.subplots(1,2, figsize=(10,4))
train_y.hist(ax=axes[0])
axes[0].set_title('Train Class Distribution')
valid_y.hist(ax=axes[1])
axes[1].set_title('Valid Class Distribution')

데이터 분포를 확인해보면 무증상(0) 데이터가 가장 많고, 1,3,4 클래스의 데이터는 많이 부족한 불균형 데이터입니다.
Image Plot
실제 이미지를 확인해보겠습니다.
#train_df에 이미지 path 만들기
all_image_path={os.path.basename(x).replace('.png',''):x for x in
glob(os.path.join('..','input','competitions','aptos2019-blindness-detection','train_images','*.png'))}
print('Scans Found:', len(all_image_path), 'Total image Index:', len(train_df))
train_df['path']=train_df['id_code'].map(all_image_path.get)
train_df.head()
#test_df에 이미지 path 만들기
all_image_path={os.path.basename(x).replace('.png',''):x for x in
glob(os.path.join('..','input','competitions','aptos2019-blindness-detection','test_images','*.png'))}
print('Scans Found:', len(all_image_path), 'Total image Index:', len(test_df))
test_df['path']=test_df['id_code'].map(all_image_path.get)
test_df.head()
#image plot 함수
def plot_images(df, rows, columns, figsize):
fig, axes=plt.subplots(rows, columns, figsize=figsize)
idx=0
for i in range(rows):
for j in range(columns):
image=np.array(Image.open(df['path'].values[idx]).convert('RGB'))
axes[i,j].imshow(image)
axes[i,j].set_title(f'Label: {df['diagnosis'].values[idx]}, id: {df['id_code'].values[idx]}')
idx+=1
plt.show()
plot_images(train_df, 6,6, (25,25))

주최 측 설명에 따르면, ' The images were gathered from multiple clinics using a variety of cameras over an extended period of time, which will introduce further variation.'라고 합니다. 여러 병원에서 가져온 데이터이기 때문에 장애물이 있을 수도 있고, 오염되어 있을 수도 있습니다. 실제 이미지를 보면 어떤건 검은 배경이 많고, 어떤건 잘려있고, 또 어떤 이미지는 모델이 학습하기 어려울 정도로 어두워보입니다.
이미지 전처리
#이미지에 여백 문제/조도, 색상 문제가 있는 것으로 확인
#Autocrop과 Ben Graham 전처리로 해결
#AutoCrop 코드
def AutoCrop(img, tol=7): #tol=tolerance(허용 오차)-밝기 7 이하인 픽셀은 크롭
gray_image=cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
mask=gray_image>tol
row_mask=mask.any(1) #열을 흝으며 해당 행에 true를 찾음-그래서 1
col_mask=mask.any(0)
check_shape=img[:,:,0][np.ix_(row_mask, col_mask)].shape
#np.ix_(mask.any(1), mask.any(0)) 브로드케스팅 계산이 가능한 형태로 튜플 반환((3,1),(1,3) 형태의 array)
if check_shape[0]==0:
return img
else:
img1=img[:,:,0][np.ix_(row_mask, col_mask)]
img2=img[:,:,1][np.ix_(row_mask, col_mask)]
img3=img[:,:,2][np.ix_(row_mask, col_mask)]
img=np.stack([img1, img2, img3], axis=-1)
return img
검은 배경이 많은 이미지의 배경을 잘라내는 Autocrop 함수입니다. mask를 이용해서 밝기 7 이하의 픽셀을 모조리 잘라냅니다.
#Ben Graham 전처리 코드
def ben_color(img):
blur=cv2.GaussianBlur(img, (0,0), sigmaX=20) #가우시안 커널 (0,0)으로 두면 시그마 값에 맞춰서 자동 설정
#조도 보정 공식 구현 (4*original-4*blur+128)
result=cv2.addWeighted(img, 4, blur, -4, 128)
return result
이 대회에서 유명한 전처리 중 하나인 Ben Graham의 전처리 코드입니다.
가우시안 블러를 이용해 배경 밝기만 있는 이미지를 만들어서 원본에서 제거해주면, 배경 이미지 전체에 깔린 '노란색/갈색의 불균일한 바탕 조명(배경)'을 평균값으로 상쇄시켜 배경의 영향력을 지워버리는 효과를 냅니다.
단순히 블러를 빼기만 하면 이미지가 너무 흐려지기 때문에, 4배의 가중치를 주어 빼줌으로써 배경 조명은 깔끔하게 소거하고, 남은 미세 혈관과 병변의 명암 대비는 4배로 강하게 증폭시킵니다.
#시각화 해보기
image=np.array(Image.open(train_df['path'].values[0]).convert('RGB'))
img1=AutoCrop(image)
img2=ben_color(img1)
fig, axes=plt.subplots(1,3, figsize=(10,4))
axes[0].imshow(image)
axes[0].set_title('Original')
axes[1].imshow(img1)
axes[1].set_title('Autocrop')
axes[2].imshow(img2)
axes[2].set_title('Autocrop+Ben Color')
for ax in axes:
ax.axis('off')

Train Valid split
train_df, valid_df=train_test_split(train_df, test_size=0.15, random_state=SEED, stratify=train_df['diagnosis'])
print(train_df.shape, valid_df.shape)
(3112, 3) (550, 3)
테스트 데이터는 3112개, 검증 데이터는 550개로 고정합니다.
시드 고정
import random
import torch
def set_seed(seed=42):
random.seed(seed) #파이선 기본 내장 random 모듈 시드
np.random.seed(seed) #numpy 랜덤 시드, albumentation가 numpy랜덤 모듈을 사용
torch.manual_seed(seed) #CPU에서 동작하는 pytorch 랜덤 시드(모델 가중치 초기화 등)
torch.cuda.manual_seed(seed) # 단일 GPU용
torch.cuda.manual_seed_all(seed) #멀티 GPU에서 동작하는 pytorch 랜덤 시드(dropout 등)
#worker init fn(서브 프로세스 시드 고정)
def seed_worker(worker_id):
worker_seed = torch.initial_seed() % 2**32
np.random.seed(worker_seed)
random.seed(worker_seed)
g = torch.Generator() # 제너레이터 생성
g.manual_seed(0) # 제너레이터 시드값 고정
#전역 시드 고정해도 프로세스들의 순서가 달라지면 학습되는 데이터 순서가 달라질 수 있으므로 shuffle을 통제
재현성을 위해 시드를 고정해줍니다.
seed_worker, generator는 num_workers=2와 같은 멀티 프로세스 환경에서 데이터 로딩의 재현성을 보장하기 위해 설정합니다. 각 CPU 코어가 독립적으로 데이터를 할당받을 때도 시드가 일정하게 유지됩니다.
Dataset
#훈련 데이터용 transform
import albumentations as A #cv2기반으로 만들어진 라이브러리
from albumentations.pytorch import ToTensorV2
#3000개의 데이터수는 ViT에게 너무 적음
train_transform=A.Compose([
A.Resize(384,384),
A.HorizontalFlip(p=0.5), #좌우 뒤집에도 질환 찾는데 문제 없음(좌안 우안 변경)
A.VerticalFlip(p=0.5), #상하 뒤집에도 질환 찾는데 문제 없음
A.RandomRotate90(p=0.5),
A.Normalize(), #ImageNet std/mean 으로 정규화
ToTensorV2()]
)
#검증/테스트 데이터용 transform #이미지 transform 안하는 이유-불필요한 왜곡 추가가 목적이라 원본 이미지 예측에는 사용 안함
test_transform=A.Compose([
A.Resize(384,384),
A.Normalize(),
ToTensorV2()]
)
대회에서 제공하는 3,112개의 데이터는 CNN과 달리 합성곱 특성(Inductive Bias)이 없는 ViT모델을 학습시키기에 턱없이 부족한 양입니다. 이를 극복하기 위해 Albumentations 라이브러리를 활용해 데이터 증강을 진행했습니다.
특히 일반적인 의료 영상과 달리 상하 반전(Vertical Flip) 증강을 적극적으로 활용했습니다. 당뇨망막병증은 미세혈관류, 망막 출혈, 삼출물, 신생 혈관 등의 병변이 특정 위치가 아닌 망막 전반에 무작위로 발생하여 진단 기준(존재 여부, 개수, 심각도)을 형성하기 때문입니다. 따라서 상하를 뒤집어도 질병의 특징적인 정보는 훼손되지 않으므로, 안전하면서도 효과적인 데이터 증강이 가능합니다
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
class APTOSData(Dataset):
def __init__(self, df, transform=None, is_test=False):
self.df=df
self.transform=transform
self.is_test=is_test
def __getitem__(self,idx):
image=np.array(Image.open(self.df['path'].iloc[idx]).convert('RGB'))
img=AutoCrop(image)
img=ben_color(img)
if self.transform:
img=self.transform(image=img)['image'] #딕셔너리 반환(mask같은거 넘기면 같이 반환)
if self.is_test:
return img
labels=self.df['diagnosis'].iloc[idx]
labels=torch.tensor(labels, dtype=torch.float) #Smooth L1 Loss를 손실함수로 사용
return img, labels
def __len__(self):
return len(self.df)
#dataset 생성
train_dataset=APTOSData(train_df, transform=train_transform)
valid_dataset=APTOSData(valid_df, transform=train_transform)
test_dataset=APTOSData(test_df, transform=test_transform, is_test=True)
#dataloader 생성
config=BaseConfig()
train_dataloader=DataLoader(train_dataset, batch_size=config.BATCH_SIZE, worker_init_fn=seed_worker, shuffle=True, generator=g, num_workers=2)
valid_dataloader=DataLoader(valid_dataset, batch_size=config.BATCH_SIZE, worker_init_fn=seed_worker, shuffle=True, generator=g, num_workers=2,
drop_last=False) #default도 false이다. valid랑 test는 모든 데이터 대상으로 적용되야 하기 때문에 False.
test_dataloader=DataLoader(test_dataset, batch_size=config.BATCH_SIZE, worker_init_fn=seed_worker, shuffle=True, generator=g, num_workers=2,
drop_last=False)
ViT
!pip install timm #torch image model 라이브러리(허깅페이스는 태생이 nlp라 비전에는 timm을 더 많이 사용)
import timm
model=timm.create_model('vit_base_patch16_384', pretrained=True, num_classes=1) #224모델을 384로 파인튜닝 한 모델, 헤드는 초기화
dummy_img=torch.randn(2,3,384,384)
output=model(dummy_img)
print(output.shape)
표준이 되는 224 사이즈의 이미지로 학습한 모델이 아닌 384 사이즈의 모델을 선택한 이유는 당뇨 망막병증 특유의 미세 병변을 포착하기 위해서입니다. 연산 부담을 감수하더라도 더 정확한 예측을 위해 이미지의 해상도를 384로 맞췄습니다.
for name, params in model.named_parameters():
print(name)
!pip install torchinfo
from torchinfo import summary
summary(model, input_size=(2,3,384,384))
파라미터 분리
from torch import optim
from torch import nn
device='cuda' if torch.cuda.is_available() else 'cpu'
model=model.to(device)
grad_params=[p for p in model.named_parameters() if p[1].requires_grad]
no_decay = ["bias", "norm"] # Bias나 LayerNorm은 weight decay 사용 안하고 보호(bias는 과적합에 영향x, layernorm은 모델 표현력을 올려주는 계층)
optimizer_grouped_parameters = [
{
"params": [p for n, p in grad_params if not any(nd in n for nd in no_decay)],
"weight_decay": 1e-2, #weight decay가 그래디언트 항하고 분리, lr과 곱해져서 빼지기 때문에 큰 값 아님.그리고 vit는 과적합에 취약
},
{
"params": [p for n, p in grad_params if any(nd in n for nd in no_decay)],
"weight_decay": 0.0,
},
]
optimizer = optim.AdamW(optimizer_grouped_parameters, lr=BaseConfig.LR)
ViT는 과적합되기 쉬운 모델이기 때문에 weight decay로 가중치에 패널티를 줍니다. 이때 bias, 정규화 계층에 weight decay로 패널티를 부여하지 않기 위해 파라미터를 분리해줍니다.
bias는 입력에 곱해지지 않고 그냥 더해지기 때문에 입력에 대한 민감도와 관련이 없고,
정규화 계층에서 에서 감마(scale)랑 베타(shift)는 정규화가 지나치게 강제한 표현력 제약을 풀어주는 것이기 때문에 weight decay의 타깃이 아닙니다.
손실함수
balance loss 만들기
범주형 데이터의 분류 문제에서 주로 사용하는 Cross Entropy Loss를 사용하지 않고 회귀 계열의 손실함수를 적용했습니다. CEE는 두 경우 모두 정답 클래스(4단계)의 확률만 보기 때문에, 두 예측이 오답 클래스에 확률을 전혀 다르게 분배했더라도, 정답 클래스 확률만 같다면 동일한 loss를 갖게 됩니다.
예를 들어 4단계가 정답인 데이터를 예측한다고 해봅시다.
첫번째 경우 조금의 오차로 예측을 잘못해서
- 예측 확률 분포 $\hat{Y}_A = [0.05, \; 0.05, \; 0.10, \; \mathbf{0.60}, \; \mathbf{0.20}]$
- 가장 높은 확률을 3단계(0.60)에 주었고, 정답인 4단계에는 0.20의 확률을 주었습니다.
- CEE 손실 계산: $-\log(0.20) \approx \mathbf{1.609}$
두번째 경우 완전히 예측을 잘못해서
- 예측 확률 분포 $\hat{Y}_B = [\mathbf{0.60}, \; 0.05, \; 0.05, \; 0.10, \; \mathbf{0.20}]$
- 가장 높은 확률을 0단계(0.60)에 주었고, 정답인 4단계에는 마찬가지로 0.20의 확률을 주었습니다.
- CEE 손실 계산: $-\log(0.20) \approx \mathbf{1.609}$
정답이 4단계인데 3단계로 틀린 loss와 0단계로 틀린 loss가 같아질 수도 있습니다.
그러기 때문에 이런 순서형(Ordinal) 데이터에서는 회귀 계열의 손실함수를 적용하는 것이 더 점수를 높여줄 것이라고 판단했습니다.
회귀계열의 손실함수로는 Smooth L1 loss를 선택했습니다. MSE는 오차가 클 때 제곱으로 패널티가 커져서 이상치(outlier)나 초반 학습 시 예측이 크게 틀렸을 때 그래디언트가 폭발적으로 커질 위험이 있고, MAE는 미분 불가능한 지점(0)이 있어 최적화가 불안정할 수 있습니다. Smooth L1은 오차가 작을 땐 MSE처럼(부드러운 그래디언트), 오차가 클 땐 MAE처럼(그래디언트가 일정하게 유지) 동작해서 두 손실의 장점을 절충한 선택입니다.
또한, EDA에서 데이터 불균형이 심한 것을 확인하여 커스텀 손실 함수를 제작해주었습니다. loss를 구할때 개수의 역수를 곱해 적은 개수의 데이터가 더 큰 가중치를 가지도록 설계했습니다. 이때 역수는 단순 역수($w_c= \frac{1}{n_c}$)를 사용했는데, 단순 역수를 그대로 쓰면 클래스별 가중치 비율이 과도하게 벌어져서 학습이 불안정해질 수 있다고 합니다. 다음번에는 Focal Loss의 balance 방법을 고려해보려고 합니다.
$$\mathcal{L} = w_c \cdot \text{SmoothL1}(\hat{y}, y)$$
$w_c$는 클래스 $c$의 가중치, $\text{SmoothL1}$은 순서 정보를 반영하는 손실입니다.
#클래스별 불균형이 심해서 Smooth L1 Loss에 클래스 개수 역수 곱한 가중치 추가(balanced loss)
from collections import Counter
labels=train_df['diagnosis']
counter=Counter(labels)
total=len(labels)
class_weights={c: total/(len(counter)*count) for c, count in counter.items()}
#len(counter)을 곱해주는건 가중치 평균을 1로 맞추기 위함(가중치 너무 커지거나 작아지면 loss 영향)
print(class_weights)
sorted_weights=[class_weights[i] for i in range(len(counter))]
print(sorted_weights)
class BalancedLoss(nn.Module):
def __init__(self, class_weights):
super().__init__()
#weight 버퍼로 등록(device 변경할떄 필요), self.class_weights 속성으로 등록
self.register_buffer('class_weights', torch.FloatTensor(class_weights))
self.loss=nn.SmoothL1Loss(reduction='none')
def forward(self, pred, target):
# pred가 (32, 1)로 들어오면 (32,)로 짜부러뜨리기
if pred.dim() == 2 and pred.size(1) == 1:
pred = pred.squeeze(1)
loss=self.loss(pred, target)
weight=self.class_weights[target.long()]
return (loss*weight).mean()
criterion=BalancedLoss(sorted_weights).to(device)
scheduler은 ReduceLROnPlateau로 설정.
#스케쥴러 설정-0.5배씩 줄이기
scheduler=optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2)
Training
from tqdm.notebook import tqdm
best_val_loss=float('inf')
for epoch in range(BaseConfig.EPOCHS):
save_path='best_aptos_model.pt'
#모델 훈련
model.train()
train_loss=0.0
for images, labels in tqdm(train_dataloader):
images=images.to(device)
labels=labels.to(device)
output=model(images)
loss=criterion(output, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss+=loss.item()*images.shape[0]
train_loss=train_loss/len(train_dataloader.dataset)
print(f'Epoch: {epoch+1:4d}, Train Loss: {train_loss:.4f}')
model.eval()
val_loss=0
with torch.no_grad():
for images, labels in tqdm(valid_dataloader):
images=images.to(device)
labels=labels.to(device)
output=model(images)
loss=criterion(output, labels)
val_loss+=loss.item()*images.shape[0]
val_loss=val_loss/len(valid_dataloader.dataset)
scheduler.step(val_loss)
current_lr=optimizer.param_groups[0]['lr']
print(f'Validation Loss: {val_loss:.4f}, Current LR: {current_lr:.6f}')
if val_loss<best_val_loss:
print('가장 낮은 val loss 기록, 모델 저장')
best_val_loss=val_loss
torch.save(model.state_dict(), save_path)

추론
submission=pd.read_csv('../input/competitions/aptos2019-blindness-detection/sample_submission.csv')
print(submission.head())
test_df.head()
model.load_state_dict(torch.load('best_aptos_model.pt', map_location=device))
pred_list=[]
model.eval()
with torch.no_grad():
for images in tqdm(test_dataloader):
images=images.to(device)
output=model(images)
pred_list.append(output.cpu()) #VRAM용량이 부족해질 수 있으므로 cpu로 옮기기
pred=torch.cat(pred_list, dim=0)
pred=pred.squeeze()
pred=torch.clamp(pred, 0, 4)
pred=torch.round(pred).long()
submission['diagnosis']=pred.numpy()
submission.head()
소수로 나온 예측값을 단순히 반올림해서 결과를 제출했습니다.
submission.to_csv('submission.csv', index=False)
print('성공적으로 제출 완료')
최종 결과

첫번째 submission 때는 test_dataloader shuffle을 true로 해버려서 대참사가 발생해버렸습니다. 덕분에 QWK의 음수 결과를 실제로 경험해볼 수 있었습니다.
상위 리더보드는 QWK 0.93~0.94대가 나오던데, 저의 베이스라인에서 점수를 올리기 위한 추가적인 방법들로는 다음과 같은 것들이 있겠습니다.
- 반올림 대신 threshold 최적화-무조건 반올림 하는 것이 아니라 OptimizedRounder를 사용해서 최적의 올림/내림 지점을 찾기
- ViT는 일단 CNN보다 훨씬 많은 데이터를 학습에 필요로 하기 때문에 외부 데이터로 학습.논문 (An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale)에서도 ViT가 ImageNet-1k(120만 장) 정도 스케일에서는 ResNet보다 오히려 성능이 떨어지고, JFT-300M처럼 훨씬 큰 데이터로 사전학습해야 강점이 드러난다고 명시되어 있습니다.
- 손실함수 자체를 QWK로 바꿔버리기
실제 이미지 데이터에 ViT를 활용해서 학습시켜보며, 이미지 전처리, 손실함수 커스텀, 파라미터 분리 학습 등 많은 것을 배운 것 같습니다. 이후 시각화를 위해 attention flow/rollout논문을 읽고, ViT 시각화 기법 중 하나인 rollout을 사용해보았습니다.
보통 캐글 코드 리뷰나 대회 리뷰 같은 건 잘 안하는거 같던데, 왜 안하는지 이번 글을 쓰면서 알게 되었습니다. 대부분의 내용들은 주석으로 달면 되고, 굳이 블로그까지 코드를 들고 와서 리뷰를 하는 것은 꽤나 수고로운 과정 같습니다. 그래도 이미지의 어떤 특성 때문에 해당 전처리 방식을 사용했는지, 왜 그 손실함수를 사용했는지 기록하고, 설명하는 측면에서는 블로그 리뷰도 꽤 괜찮은 것 같습니다.