티스토리 뷰

반응형

데이터를 불러오고, stratify를 통해 분리한다.

from sklearn.model_selection import train_test_split

# 훈련 데이터, 검증 데이터 분리
train, valid = train_test_split(train, 
                                test_size=0.1,
                                stratify=train[['healthy', 'multiple_diseases', 'rust', 'scab']],
                                random_state=42)

 

 

from PIL import Image
from torch.utils.data import Dataset
import numpy as np
import os

class CustomImageDataset(Dataset):
    def __init__(self, dataframe, image_dir='./', transform=None, is_test=False):
        super().__init__()
        self.dataframe = dataframe
        self.image_dir = image_dir
        self.transform = transform
        self.is_test = is_test
    
    def __len__(self):
        return len(self.dataframe)
    
    def __getitem__(self, idx):
        img_id = self.dataframe.iloc[idx, 0]
        img_path = os.path.join(self.image_dir, img_id + '.jpg')
        image = Image.open(img_path).convert("RGB")
        
        if self.transform is not None:
            image = self.transform(image)
        
        if self.is_test:
            return image
        else:
            label = np.argmax(self.dataframe.iloc[idx, 1:5])
            return image, label

 

이미지 변환기

이미지를 크게 조정했을 때 성능이 좋다.

 

import torchvision.transforms as transforms

# 훈련 데이터용 변환기
transform_train = transforms.Compose([
    transforms.Resize((450, 650)),                      # 이미지 크기 조절 
    transforms.ColorJitter(brightness=0.2, contrast=0.2),# 밝기 대비 조절
    transforms.RandomVerticalFlip(p=0.2),               # 상하 대칭 변환
    transforms.RandomHorizontalFlip(p=0.5),             # 좌우 대칭 변환 
    transforms.RandomAffine(                             # 이동, 스케일링, 회전 변환
        degrees=30, translate=(0.1, 0.1), scale=(0.8, 1.2)),
    transforms.RandomChoice([                           # 양각화, 날카로움, 블러 효과
        transforms.Emboss(),
        transforms.Sharpen(),
        transforms.GaussianBlur(kernel_size=3)]),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],     # 정규화 변환
                         std=[0.229, 0.224, 0.225]),
    transforms.ToTensor()                               # 텐서로 변환
])

# 검증 및 테스트 데이터용 변환기
transform_test = transforms.Compose([
    transforms.Resize((450, 650)),                      # 이미지 크기 조절 
    transforms.Normalize(mean=[0.485, 0.456, 0.406],     # 정규화 변환
                         std=[0.229, 0.224, 0.225]),
    transforms.ToTensor()                               # 텐서로 변환
])

 

# 이미지 변환을 위한 모듈
import albumentations as A
from albumentations.pytorch import ToTensorV2

# 훈련 데이터용 변환기
transform_train = A.Compose([
    A.Resize(450, 650),       # 이미지 크기 조절 
    A.RandomBrightnessContrast(brightness_limit=0.2, # 밝기 대비 조절
                               contrast_limit=0.2, p=0.3),
    A.VerticalFlip(p=0.2),    # 상하 대칭 변환
    A.HorizontalFlip(p=0.5),  # 좌우 대칭 변환 
    A.ShiftScaleRotate(       # 이동, 스케일링, 회전 변환
        shift_limit=0.1,
        scale_limit=0.2,
        rotate_limit=30, p=0.3),
    A.OneOf([A.Emboss(p=1),   # 양각화, 날카로움, 블러 효과
             A.Sharpen(p=1),
             A.Blur(p=1)], p=0.3),
    A.PiecewiseAffine(p=0.3), # 어파인 변환 
    A.Normalize(),            # 정규화 변환 
    ToTensorV2()              # 텐서로 변환
])

# 검증 및 테스트 데이터용 변환기
transform_test = A.Compose([
    A.Resize(450, 650), # 이미지 크기 조절 
    A.Normalize(),      # 정규화 변환
    ToTensorV2()        # 텐서로 변환
])

 

 

사전 훈련 및 전이 학습

1. torchvision.models 모듈 이용

2. pretrainedmodels 모듈 이용 - 사전 훈련 모델 제

3. 직접 구현한 모듈 이용

 

사전 훈련 모델을 구할 때는 EfficientNet github pytorch와 같이 찾아보면 된다.

 

EffcientNet은 우수한 CNN 모델이다.

 

EffcientNet은 Google Brain 팀에서 개발한 매우 우수한 CNN 모델입니다. 주요 특징은 다음과 같습니다:

  1. 모델 스케일링 전략 (Model Scaling) EffcientNet은 네트워크 깊이, 너비, 해상도를 균형있게 스케일링하는 효율적인 방법을 제안했습니다. 기존 모델들은 주로 단순히 레이어 수나 채널 수를 늘려 높은 성능을 얻으려 했지만, 자원 낭비가 컸습니다. EffcientNet은 이러한 3가지 차원을 동시에 고려하여 균형있게 스케일링하여 높은 정확도와 효율성을 달성했습니다.
  2. 화소 셔플 (Pixel Shuffling) EffcientNet은 고해상도 입력 이미지를 낮은 해상도로 다운샘플링했다가 다시 높은 해상도로 업샘플링하는 방식을 사용합니다. 이로써 네트워크가 고해상도와 저해상도 특징을 모두 잡아내어 성능이 향상됩니다.
  3. 스퀴즈-앤-엑사이트 (Squeeze-and-Excitation) EffcientNet은 채널 간 의존성을 고려하는 SE(Squeeze-and-Excitation) 블록을 사용합니다. 이를 통해 중요한 채널 정보에 가중치를 더 많이 줄 수 있습니다.
  4. 스와이시 활성화 함수(Swish Activation) 기존 ReLU 대신 swish라는 새로운 활성화 함수를 사용해 모델 성능을 개선했습니다.

EffcientNet은 이러한 혁신적인 아이디어로 CIFAR, ImageNet, COCO 등 다양한 데이터셋에서 상당히 높은 정확도를 달성하면서도 모델 크기와 연산량이 작아 효율적입니다. 현재 컴퓨터 비전 분야에서 SOTA(State-of-the-art) 모델 중 하나로 평가받고 있습니다.

 

그 중에서도 EfficientNet-b7은 타깃값이 1000개인 이미지넷 데이터로 사전 훈련한 모델이다.

 

EfficientNet 모델에서 B0, B1, B2 등의 접미사는 모델의 크기와 복잡성을 나타내는 지표입니다.

  • B0: 가장 작고 간단한 베이스라인 모델
  • B1, B2, ...: 점점 더 크고 복잡한 모델

숫자가 높아질수록 모델의 깊이(depth), 너비(width), 해상도(resolution)가 동시에 증가하여 더 많은 계산 자원을 필요로 하지만 정확도 또한 높아집니다.

구체적으로는 다음과 같습니다:

  • B0: 5.3M 파라미터
  • B1: 7.8M 파라미터
  • B2: 9.2M 파라미터
  • B3: 12M 파라미터
  • B4: 19M 파라미터
  • B5: 30M 파라미터
  • B6: 43M 파라미터
  • B7: 66M 파라미터

따라서 B7은 EfficientNet 계열 중 가장 큰 모델로, 높은 정확도를 기대할 수 있지만 많은 계산 자원이 필요합니다. 개발자는 모델 크기에 따른 정확도-효율성 트레이드오프를 고려하여 적절한 모델을 선택해야 합니다.

 

num_classes에 전달해야 최종 출력값이 4개가 된다.

 

from efficientnet_pytorch import EfficientNet # EfficientNet 모델
# 사전 훈련된 efficientnet-b7 모델 불러오기
model = EfficientNet.from_pretrained('efficientnet-b7', num_classes=4) 

model = model.to(device) # 장비 할당

 

optimizer = torch.optim.AdamW(model.parameters(), lr=0.00006, weight_decay=0.0001)

가중치 감쇠 = weight_decay로 미세하게 규제를 적용한다.

 

from sklearn.metrics import roc_auc_score # ROC AUC 점수 계산 함수
from tqdm.notebook import tqdm # 진행률 표시 막대 

epochs = 5

# 총 에폭만큼 반복
for epoch in range(epochs):
    # == [ 훈련 ] ==============================================
    model.train()        # 모델을 훈련 상태로 설정 
    epoch_train_loss = 0 # 에폭별 손실값 초기화 (훈련 데이터용)
    
    # '반복 횟수'만큼 반복 
    for images, labels in tqdm(loader_train):
        # 이미지, 레이블(타깃값) 데이터 미니배치를 장비에 할당 
        images = images.to(device)
        labels = labels.to(device)
        
        # 옵티마이저 내 기울기 초기화
        optimizer.zero_grad()
        # 순전파 : 이미지 데이터를 신경망 모델의 입력값으로 사용해 출력값 계산
        outputs = model(images)
        # 손실 함수를 활용해 outputs와 labels의 손실값 계산
        loss = criterion(outputs, labels)
        # 현재 배치에서의 손실 추가 (훈련 데이터용)
        epoch_train_loss += loss.item() 
        loss.backward() # 역전파 수행
        optimizer.step() # 가중치 갱신
    # 훈련 데이터 손실값 출력
    print(f'에폭 [{epoch+1}/{epochs}] - 훈련 데이터 손실값 : {epoch_train_loss/len(loader_train):.4f}')
    
    # == [ 검증 ] ==============================================
    model.eval()          # 모델을 평가 상태로 설정 
    epoch_valid_loss = 0  # 에폭별 손실값 초기화 (검증 데이터용)
    preds_list = []       # 예측 확률값 저장용 리스트 초기화 
    true_onehot_list = [] # 실제 타깃값 저장용 리스트 초기화 
    
    with torch.no_grad(): # 기울기 계산 비활성화
        # 미니배치 단위로 검증
        for images, labels in loader_valid:
            images = images.to(device)
            labels = labels.to(device)
            
            outputs = model(images)
            loss = criterion(outputs, labels)
            epoch_valid_loss += loss.item()
            
            preds = torch.softmax(outputs.cpu(), dim=1).numpy() # 예측 확률값
            # 실제값 (원-핫 인코딩 형식)
            true_onehot = torch.eye(4)[labels].cpu().numpy()  
            # 예측 확률값과 실제값 저장
            preds_list.extend(preds)
            true_onehot_list.extend(true_onehot)
    # 검증 데이터 손실값 및 ROC AUC 점수 출력 
    print(f'에폭 [{epoch+1}/{epochs}] - 검증 데이터 손실값 : {epoch_valid_loss/len(loader_valid):.4f} / 검증 데이터 ROC AUC : {roc_auc_score(true_onehot_list, preds_list):.4f}')

 

사전 학습 모델을 훈련 상태로 적용하고, 훈련하여 검증한다.

 

from keras.models import Sequential
from keras.layers import Input, Conv2D, BatchNormalization, MaxPooling2D, Dropout, Flatten, Dense
from keras.applications import EfficientNetB7

# EfficientNet-B7 모델 로드
base_model = EfficientNetB7(weights='imagenet', include_top=False, input_shape=(h, w, c))

# Sequential 모델 생성
model = Sequential()

# EfficientNet-B7을 Sequential 모델에 추가
model.add(base_model)

# Flatten 레이어 추가
model.add(Flatten())

# Fully Connected Layer 추가
model.add(Dense(1024, activation='relu'))

# BatchNormalization 추가
model.add(BatchNormalization())

# Dropout 추가
model.add(Dropout(0.35))

# Output Layer 추가
model.add(Dense(10, activation='softmax'))

# 모델 컴파일
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])

# 모델 요약
model.summary()

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\


from keras.layers import Input, Flatten, Dense, BatchNormalization, Dropout
from keras.models import Model
from keras.applications import EfficientNetB2

# EfficientNet-B0 모델 로드
base_model = EfficientNetB2(weights='imagenet', include_top=False, input_shape=(h, w, c))

# Functional API를 이용한 모델 정의
x = base_model.output
x = Flatten()(x)
x = Dense(1024, activation='relu')(x)
x = BatchNormalization()(x)
x = Dropout(0.35)(x)
predictions = Dense(10, activation='softmax')(x)

# 새로운 모델 생성
model = Model(inputs=base_model.input, outputs=predictions)

# 모델 컴파일
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])

# 모델 요약
model.summary()

 

성능 개선

스케줄러 설정

에폭 늘리기

TTA 기법

레이블 스무딩

 

스케줄러는 처음에 학습률을 강하게 주고, 후에는 약하게 주면서 조정한다.

 

get_cosine_schedule_with_warmup 함수

지정한 값만큼 학습률을 증가시켰다가 코사인 그래프 모양으로 감소시킨다.

여기선 0.00006까지 직선으로 증가 했다가, 그 후에는 감소한다.

from transformers import get_cosine_schedule_with_warmup

epochs = 39 # 총 에폭의 수

# 스케줄러 생성
scheduler = get_cosine_schedule_with_warmup(optimizer, 
                                            num_warmup_steps=len(train)*3, #3 에포크만에 지정한 수에 도달하도록 한다.
                                            num_training_steps=len(train)*epochs)

 

예측

TTA 기법

테스트 단계 데이터를 증강하는 기법이다.

훈련 데이터가 많으면 모델 성능이 좋아진다.

이런 데이터 증강 기법을 테스트 단계에서도 이용해서 예측 성능을 끌어올리는 방법이다.

 

TTA를 하면 테스트 데이터를 여러 차례 변형한 후에 예측한다.

테스트 데이터가 늘어난 효과를 얻는다.

 

TTA(Test Time Augmentation)는 테스트 시간에 데이터 증강(Data Augmentation) 기법을 적용하여 모델의 예측 성능을 향상시키는 기법입니다. TTA는 일반적으로 다음과 같은 단계별 방식으로 수행됩니다.

  1. 원본 이미지 준비 먼저 예측하려는 원본 이미지를 준비합니다.
  2. 데이터 증강 기법 적용 원본 이미지에 다양한 데이터 증강 기법(회전, 반전, 잡음 추가, 크롭 등)을 적용하여 여러 개의 변형된 이미지를 생성합니다.
  3. 모델 예측 생성된 모든 변형 이미지를 입력으로 해당 모델에 통과시켜 예측값을 얻습니다.
  4. 예측값 앙상블 각 변형 이미지에 대한 예측값을 특정 방식(예: 평균, 가중 평균, 다수결 투표 등)으로 결합하여 최종 예측값을 얻습니다.
  5. 결과 출력 결합된 최종 예측값을 출력합니다.

이 과정에서 중요한 점은 데이터 증강 기법과 예측값 결합 방식을 적절히 선택하는 것입니다. 일반적으로 다양한 데이터 증강 기법을 조합하고, 예측값을 평균하는 방식이 좋은 성능을 내는 것으로 알려져 있습니다.

TTA는 단일 모델의 약점을 보완하고 모델이 데이터의 다양한 변형에 강건해지도록 해주어 예측 성능을 향상시킬 수 있습니다. 다만 추가 연산 비용이 발생하므로 속도와 정확도 간 트레이드오프를 고려해야 합니다.

 

훈련 모델로 테스트 데이터 예측 값들을 평균, 가중 평균, 다수결 투표와 같이 앙상블을 할 수 있다.

 

from keras.preprocessing.image import ImageDataGenerator
import numpy as np

# 데이터 증강 기법 정의
data_generator = ImageDataGenerator(
    rotation_range=30,
    horizontal_flip=True,
    width_shift_range=0.2,
    height_shift_range=0.2,
    zoom_range=0.2
)

# TTA 함수 정의
def test_time_augmentation(model, image, augmentations=10):
    predictions = []

    # 원본 이미지에 대한 예측
    prediction = model.predict(np.expand_dims(image, axis=0))[0]
    predictions.append(prediction)

    # 증강된 이미지에 대한 예측
    for _ in range(augmentations):
        augmented_image = data_generator.random_transform(image)
        augmented_image = np.expand_dims(augmented_image, axis=0)
        prediction = model.predict(augmented_image)[0]
        predictions.append(prediction)

    # 예측값 평균 계산
    final_prediction = np.mean(predictions, axis=0)

    return final_prediction

# 예시 이미지와 모델 로드
test_image = ... # 예시 이미지 로드
model = ... # 모델 로드

# TTA 적용
tta_prediction = test_time_augmentation(model, test_image)
print(tta_prediction)

 

레이블 스무딩 기법

딥러닝 모델이 과잉 확신하면 일반화 성능이 떨어진다. 예시 : 특정 타깃을 1에 가깝게 예측하ㅡㄴ 경우

일반화 성능을 높이려면 과잉 확신한 예측값을 보정해줘야 한다.

 

(1 - a) * preds + a/k

 

k= 타깃값의 개수

1-a, a = 레이블 스무딩 강도

 

0,0,1,0이 있다고 하면, 4개 분류이므로

 

1-0.1 * 0 + 0.1/4 = 0.025가 된다.

1-0.1 * 1 + 0.1/4 = 0.925가 된다.

 

def apply_label_smoothing(df, target, alpha, threshold):
    # 타깃값 복사
    df_target = df[target].copy()
    k = len(target) # 타깃값 개수
    
    for idx, row in df_target.iterrows():
        if (row > threshold).any():         # 임계값을 넘는 타깃값인지 여부 판단
            row = (1 - alpha)*row + alpha/k # 레이블 스무딩 적용  
            df_target.iloc[idx] = row       # 레이블 스무딩을 적용한 값으로 변환
    return df_target # 레이블 스무딩을 적용한 타깃값 반환

 

 

레이블 스무딩은 신경망의 손실 함수에 사용되는 기법 중 하나로, 모델이 너무 확신을 갖지 않도록 하고 일반화 성능을 향상시키는 데 도움이 될 수 있습니다. 레이블 스무딩을 적용하기 위한 공식은 다음과 같습니다.

레이블 스무딩을 적용한 실제 타깃 레이블(soft label)을 계산하기 위해 레이블 스무딩 매개변수인 epsilon을 사용합니다. 이 매개변수는 일반적으로 0보다 크고 1보다 작은 값입니다. 예를 들어, epsilon을 0.1로 설정하면 레이블 스무딩이 적용됩니다.

 

 

실제 타깃 레이블이 원-핫 인코딩된 경우:

  • 각 클래스의 실제 타깃 레이블에 epsilon 값을 더합니다.
  • 나머지 클래스에 대해서는 epsilon 값을 빼고 나머지 값을 골고루 분배합니다.

수식으로 표현하면:여기서,

  • smoothed_labels은 레이블 스무딩이 적용된 실제 타깃 레이블입니다.
  • one_hot_labels은 원-핫 인코딩된 실제 타깃 레이블입니다.
  • num_classes는 클래스의 수입니다.
  • ones는 크기가 실제 타깃 레이블과 동일하고 모든 요소가 1인 벡터입니다.

 

 

실제 타깃 레이블이 정수로 표현된 경우:

  • 각 클래스의 실제 타깃 레이블에 epsilon 값을 더합니다.
  • 나머지 클래스에 대해서는 epsilon 값을 빼고 나머지 값을 골고루 분배합니다.

수식으로 표현하면:여기서,

  • integer_labels은 정수로 표현된 실제 타깃 레이블입니다.
  • 나머지 변수들은 위와 동일합니다.

 

이러한 레이블 스무딩 공식을 사용하여 손실 함수에 적용된 타깃 레이블을 부드럽게 만들어 모델이 너무 강한 확신을 가지지 않도록 합니다.

 

레이블 스무딩을 타깃값에 적용해서 submission을 만든다.

alpha = 0.001 # 레이블 스무딩 강도
threshold = 0.999 # 레이블 스무딩을 적용할 임계값

# 레이블 스무딩을 적용하기 위해 DataFrame 복사
submission_test_ls = submission_test.copy()
submission_tta_ls = submission_tta.copy()

target = ['healthy', 'multiple_diseases', 'rust', 'scab'] # 타깃값 열 이름

# 레이블 스무딩 적용
submission_test_ls[target] = apply_label_smoothing(submission_test_ls, target, 
                                                   alpha, threshold)
submission_tta_ls[target] = apply_label_smoothing(submission_tta_ls, target, 
                                                  alpha, threshold)

submission_test_ls.to_csv('submission_test_ls.csv', index=False)
submission_tta_ls.to_csv('submission_tta_ls.csv', index=False)

 

최종적으로 모든 x 데이터를 사용하면 성능이 오른다.

 

반응형