17. 주요 CNN알고리즘 구현 : GoogLeNet - 인공지능 고급(시각) 강의 복습

안상훈·2024년 6월 16일
0

인공지능-시각

목록 보기
18/54
post-thumbnail

개요

본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 인공지능 고급-시각 강의의 CNN알고리즘 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.


1. GoogLeNet 배경

GoogLeNet는 ILSVRC-2014에서 우승을 차지한 Google 연구팀이 개발한 네트워크로 Inception Module이란 아키텍쳐의 도입을 통해 네트워크의 깊이(Depth)와 너비(Width)를 효율적으로 확장할 수 있는 방법론을 제시하였다.
향후 Inception Module을 도입하여 발전시킨 다양한 네트워크가 발표되면서 GoogLeNetInception V1으로 불린다.

1.1 Inception Module

네트워크의 성능을 향상시키는 가장 직관적인 방법은 깊이, 너비를 확장시키는 것이지만, 확장시킨 만큼 파라미터 개수가 늘어나 네트워크가 무거워진다.
이렇게 네트워크가 무거워지면 Overfitting의 가능성, 요구 리소스가 높아지는 단점이 있는데
이를 GoogLeNetInception Module의 도입으로 문제를 해결해 냈다.

Inception Module은 위 사진처럼 1x1, 3x3, 5x5 같이 다양한 크기를 갖는 Kernel이 적용된 Layer을 병렬로 적용하여 여러 종류의 특징정보를 추출하는 것이다.

이렇게 이미지에 대해 다양한 시각적 정보를 동시에 포착하고 이후 Filter Concat과정을 통해 다음 레이어에서 여러 수준이 통합된 정보를 학습 \rightarrow 풍부한 표현정보의 학습효과를 노린다.

1.2 Dimension Reduction

위 1.1에서 설계한 Inception Module은 Prev Layer \rightarrow Next Layer로 넘어가는 과정에서 차원이 기하급수로 늘어나는 문제점이 발생하기에 이를 방지하기 위한 목적으로 [1x1] Conv를 각각 아래의 그림처럼 배치하여 Next Layer의 차원을 축소, 이로 인한 연산량 감소 효과를 꾀한다.

1.3 Auxiliary classification

GoogLeNet에는 위 사진처럼 네트워크의 중간마다 Auxiliary classifier 모듈이 붙어 중간 결과값을 추출해낸다.
이 보조분류기(Auxiliary classification)이 추가적인 Gradient 정보를 제공하여 역전파 때 Gradient Vanishing 문제를 억제해준다.

하지만 후기 Inception계열 네트워크에는 해당 기법을 사용하지 않는데 Batch Normalization(배치정규화), ResNet의 Residual Connection 개념 도입, Inception 모듈 개선등으로 더이상 Auxiliary classification을 사용하지 않는다.

이는 Auxiliary classification을 구현해 보면 알 수 있듯이 해당 모듈을 도입하면 디버깅 및 최적화에 어려운 점이 있다.


2. GoogLeNet 아키텍쳐


GoogLeNet는 [224x224x3]의 입력을 받으며, 총 9종류의 Inception Module에 중간중간 차원축소용 MaxPool이 배치되고, 레이어 중간에 Auxiliary classifier이 측면에 붙는 구조로 설계되어 있다.

기본 모듈인 Inception Module은 각 Conv 모듈별로 출력되는 Output Cannel의 값이 각각 다르기에 조금 어려운 감이 있다.

따라서 channel 계수값을 잘 확인해서 코드작성을 수행해야 한다.

다음으로 Auxiliary classifier는 모델의 중간 부분에서 결과물을 출력해 이를 역전파에서 Gradient 보정값으로 사용하는 모듈로,

Training 과정에서만 동작하고 추론/검증과정에서는 동작하지 않는 Batch Normalization, Dropout와 동일하게 작동한다.

따라서 모델을 살펴보면 incpetion모듈이 반복적으로 붙으나 내부 인자값이 많이 다르기 때문에 이를 반복적으로 표현하지 아니하고 각각 독립된 모듈로 표현하면 위와 같은 구조를 띄는 것으로 도식화가 가능하다.

위 도식도를 코드로 구현하면 아래와 같다.

import torch
import torch.nn as nn
#red는 reduce의 약자임
class Inception_module(nn.Module):
    def __init__(self, in_channels, ch1x1, ch3x3_red, ch3x3, ch5x5_red, ch5x5, pool):
        super(Inception_module, self).__init__()

        #1번째 네트워크
        self.conv1x1 = nn.Conv2d(in_channels, ch1x1, kernel_size=1)

        #2번째 네트워크
        self.conv3x3 = nn.Sequential(
            nn.Conv2d(in_channels, ch3x3_red, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(ch3x3_red, ch3x3, kernel_size=3, padding=1),
            nn.ReLU(inplace=True)
        )

        #3번째 네트워크
        self.conv5x5 = nn.Sequential(
            nn.Conv2d(in_channels, ch5x5_red, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(ch5x5_red, ch5x5, kernel_size=5, padding=2),
            nn.ReLU(inplace=True)
        )

        #4번째 네트워크
        self.pool_net = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            nn.Conv2d(in_channels, pool, kernel_size=1),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        x1 = self.conv1x1(x)
        x2 = self.conv3x3(x)
        x3 = self.conv5x5(x)
        x4 = self.pool_net(x)

        return torch.cat([x1, x2, x3, x4], 1)
#보조분류기
class AuxModule(nn.Module):
    def __init__(self, in_channels, num_classes):
        super(AuxModule, self).__init__()

        self.avgpool = nn.AdaptiveAvgPool2d((4,4))

        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels, 128, kernel_size=1),
            nn.ReLU(inplace=True)
        )

        self.flatten = nn.Flatten()
        
        self.fc = nn.Sequential(
            nn.Linear(4*4*128, 1024),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.7),
            nn.Linear(1024, num_classes)
        )

    def forward(self, x):
        x = self.avgpool(x)
        x = self.conv1(x)
        x = self.flatten(x)
        x = self.fc(x)
        return x
#위 설계한 inception모듈을 받아서 전체 GoogLeNet설계하기
class GoogLeNet(torch.nn.Module):
    def __init__(self, in_channels=3, num_classes=1000):
        super(GoogLeNet, self).__init__()

        self.training = True
        
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
            nn.LocalResponseNorm(2)  #BatehNormalization 이전의 정규화 기법
        )

        self.conv2 = nn.Sequential(
            nn.Conv2d(64, 64, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 192, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.LocalResponseNorm(2),  #BatehNormalization 이전의 정규화 기법
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        )

        self.inception_3a = Inception_module(192, 64, 96, 128, 16, 32, 32)
        self.inception_3b = Inception_module(256, 128, 128, 192, 32, 96, 64)
        self.maxpool_3 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        self.inception_4a = Inception_module(480, 192, 96, 208, 16, 48, 64)
        self.aux1 = AuxModule(512, num_classes)

        self.inception_4b = Inception_module(512, 160, 112, 224, 24, 64, 64)
        self.inception_4c = Inception_module(512, 128, 128, 256, 24, 64, 64)
        self.inception_4d = Inception_module(512, 112, 144, 288, 32, 64, 64)
        self.aux2 = AuxModule(528, num_classes)

        self.inception_4e = Inception_module(528, 256, 160, 320, 32, 128, 128)
        self.maxpool_4 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        self.inception_5a = Inception_module(832, 256, 160, 320, 32, 128, 128)
        self.inception_5b = Inception_module(832, 384, 192, 384, 48, 128, 128)

        self.fc = nn.Sequential(
            nn.AdaptiveAvgPool2d((1,1)),
            nn.Flatten(),
            nn.Dropout(p=0.4),
            nn.Linear(1024, num_classes)
        )
        
    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        
        x = self.inception_3a(x)
        x = self.inception_3b(x)
        x = self.maxpool_3(x)

        x = self.inception_4a(x)
        if self.training:
            out1 = self.aux1(x)

        x = self.inception_4b(x)
        x = self.inception_4c(x)
        x = self.inception_4d(x)
        if self.training:
            out2 = self.aux2(x)

        x = self.inception_4e(x)
        x = self.maxpool_4(x)

        x = self.inception_5a(x)
        x = self.inception_5b(x)

        x = self.fc(x)

        if self.training:
            return [x, out1, out2]
        else:
            return x
    
    def set_train(self): # 훈련 모드 설정
        self.train()  # 내장 train 메서드를 호출
    def set_eval(self):	 # 평가 모드 설정
        self.eval()  # 내장 eval 메서드를 호출

이 GoogLeNet에서 Auxiliary classifier를 향후에 사용하지 않게 되는 이유는
Gradient Vanishing 을 억제하기 위해

if self.training:
	out1 = self.aux1(x)

def set_train(self): # 훈련 모드 설정
	self.train()  # 내장 train 메서드를 호출
def set_eval(self):  # 평가 모드 설정
	self.eval()  # 내장 eval 메서드를 호출

이렇게 자잘한 코드를 써야하고 나중에 훈련 모드 에서도 추가로 처리해줘야 할 게 많아서 잘 안쓰게 됬다.. 라고 보면 될 것 같다.

그리고 nn.LocalResponseNorm(2)라는 코드도 포함되어 있는데 BatchNormalization이 도입되기 이전에 사용된 정규화 기법이라 이해하고 넘어가면 된다.

아무튼 GoogLeNet을 설계했으니 요약 및 디버깅을 해보면 아래와 같다.

from torchsummary import summary #설계한 모델의 요약본 출력 모듈

debug_model = GoogLeNet()
summary(debug_model, input_size=(3, 224, 224), device='cpu')

설계한 GoogLeNet은 논문이 작성된 시점으로 num_classes=1000으로 할 시 약 1300만개의 파라미터가 생성된다.
이정도면 꽤 경량한 모델이라 보면 된다.


3. 데이터셋 설정

데이터셋은 이전 포스트인 17. 이미지 데이터 증강에서 사용한 Cats and Dogs 데이터셋을 사용한다.

import os

path = '../00_pytest_img/cats_and_dogs'
# 폴더 경로 설정
train_path = os.path.join(path, "train")
val_path = os.path.join(path, "val")
test_path = os.path.join(path, "test")
# 하위 폴더 리스트
subdirs = ['Cat', 'Dog']
# 함수: 폴더 내 이미지 파일 개수 출력
def count_images(directory):
    for subdir in subdirs:
        subdir_path = os.path.join(directory, subdir)
        if os.path.exists(subdir_path):
            image_files = [f for f in os.listdir(subdir_path) if os.path.isfile(os.path.join(subdir_path, f))]
            print(f"{directory}/{subdir} 폴더에는 {len(image_files)}개의 이미지 파일이 있습니다.")
        else:
            print(f"{subdir_path} 경로가 존재하지 않습니다.")
            
# 각 폴더의 이미지 파일 개수 출력
print("Train 폴더:")
count_images(train_path)

print("\nValidation 폴더:")
count_images(val_path)

print("\nTest 폴더:")
count_images(test_path)
Train 폴더:
../cats_and_dogs\train/Cat 폴더에는 8748개의 이미지 파일이 있습니다.
../cats_and_dogs\train/Dog 폴더에는 8748개의 이미지 파일이 있습니다.

Validation 폴더:
../cats_and_dogs\val/Cat 폴더에는 2500개의 이미지 파일이 있습니다.
../cats_and_dogs\val/Dog 폴더에는 2500개의 이미지 파일이 있습니다.

Test 폴더:
../cats_and_dogs\test/Cat 폴더에는 1251개의 이미지 파일이 있습니다.
../cats_and_dogs\test/Dog 폴더에는 1251개의 이미지 파일이 있습니다.

위 코드를 통해 데이터셋을 불러오고 잘 불러와졌는지 확인하는 코드까지 설계했으니

이제 Dataset으로 만들기 위한 코드를 설계한다.

# 함수: 폴더 내 이미지 파일 경로와 라벨 수집
def collect_image_paths_and_labels(directory):
    image_paths = []
    labels = []
    for subdir in subdirs:
        subdir_path = os.path.join(directory, subdir)
        if os.path.exists(subdir_path):
            image_files = [f for f in os.listdir(subdir_path) if os.path.isfile(os.path.join(subdir_path, f))]
            image_paths.extend([os.path.join(subdir_path, f) for f in image_files])
            labels.extend([0 if subdir == 'Cat' else 1 for _ in image_files])
        else:
            print(f"{subdir_path} 경로가 존재하지 않습니다.")
    return image_paths, labels

우선 위 함수로 데이터셋의 이미지 파일에 라벨을 붙이는데
Cat이면 0을 붙이고, Dog이면 1을 붙인다.

위 설계한 함수로

# 각 폴더의 이미지 파일 경로와 라벨 수집
train_images, train_labels = collect_image_paths_and_labels(train_path)
val_images, val_labels = collect_image_paths_and_labels(val_path)
test_images, test_labels = collect_image_paths_and_labels(test_path)

images, lables 변수들을 설계한 뒤

from torch.utils.data import Dataset
from PIL import Image

class CustomDataset(Dataset):
    def __init__(self, image_paths, labels, transform=None):
        self.image_paths = image_paths
        self.labels = labels
        self.transform = transform

    def __len__(self):
        return len(self.image_paths)

    def __getitem__(self, idx):
        image_path = self.image_paths[idx]
        image = Image.open(image_path).convert("RGB")
        label = self.labels[idx]

        if self.transform:
            image = self.transform(image)
            label = torch.tensor(label, dtype=torch.int64)
            # 라벨을 torch.int64로 변환

        return image, label

    @property
    def label(self):
        return self.labels

이전의 데이터셋 생성 클래스를 재사용한다.

# Custom Dataset 생성
train_dataset = CustomDataset(train_images, train_labels)
test_dataset = CustomDataset(val_images, val_labels)

test 데이터셋 보다는 val데이터셋이 좀 더 자료가 많기에 단어의 혼동이 좀 있지만 검증용 데이터셋으로는 test를 사용한다.

DatasetDataLoader로 변환하기 전
Cats and Dogs 데이터셋의 Normalization을 위한 [Mean, Std]을 구할 필요성이 있다.

이것에 대한 코드는 아래와 같다.

# 모든 이미지 경로 합치기
all_images = train_images + val_images + test_images

from torchvision.transforms import v2

transform = v2.Compose([
    v2.Resize((224, 224)), #이미지 크기를 224x224로
    v2.ToImage(),  # 이미지를 Tensor 자료형으로 변환
    v2.ToDtype(torch.float32, scale=True), #텐서 자료형을 [0~1]로 스케일링
])
from tqdm import tqdm #훈련 진행상황 체크

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 이미지 텐서로 변환 후 스택
image_tensors = []
for image_path in tqdm(all_images, desc="Processing images"):
    image = Image.open(image_path).convert("RGB")
    image_tensor = transform(image).to(device)
    image_tensors.append(image_tensor)
# 모든 이미지 스택 쌓기
image_tensors = torch.stack(image_tensors)

# 이미지 채널별 평균과 표준편차 계산
#텐서 자료형은 4차원이고 [N, C, H, W]이니
#각 채널(R, G, B)에 대한 평균/표준편차를 구해야 하니
#dim=[N, H, W]로 놓는다.
mean = image_tensors.mean(dim=[0, 2, 3])
std = image_tensors.std(dim=[0, 2, 3])

print(f"Mean: {mean}")
print(f"Standard Deviation: {std}")
Mean: tensor([0.4883, 0.4553, 0.4170], device='cuda:0')
Standard Deviation: tensor([0.2598, 0.2531, 0.2559], device='cuda:0'

위 코드를 사용하면 GPU연산을 통해 mean, std계산이 이뤄지니 결과값을 얻은 후에는 GPU메모리를 리셋해줄 필요성이 있다.

아무튼 Mean, std정보를 바탕으로 데이터 전처리방법론 설계 및 DataLoader자료형을 생성해주자

이때

train_dataset_not_aug = CustomDataset(train_images, train_labels)

위 코드를 하나 추가해서

훈련 데이터셋에
데이터 증강을 적용한 경우
데이터 증강을 적용하지 않은 경우

두가지로 나눠서 DataLoader을 만들자

데이터 증강 적용 유/무에 따른 성능평가를 함께 수행하기 위한 코드이다.

from torchvision.transforms import v2

# STL-10 데이터셋의 mean과 std 값
C_N_D_val = [[0.4883, 0.4553, 0.4170], [0.2598, 0.2531, 0.2559]]

#전처리 방법론 설계
train_transformation = v2.Compose([
	#훈련데이터용 데이터 증강기법
    v2.RandomResizedCrop((224,224)),
    v2.RandomHorizontalFlip(p=0.5),
    v2.ColorJitter(brightness=0.4,
                    contrast=0.4,
                    saturation=0.4,
                    hue=0.1),

    v2.Resize((224, 224)), #이미지 크기를 224x224로
    v2.ToImage(),  # 이미지를 Tensor 자료형으로 변환
    v2.ToDtype(torch.float32, scale=True), #텐서 자료형을 [0~1]로 스케일링
    v2.Normalize(mean=C_N_D_val[0], std=C_N_D_val[1]) #데이터셋 정규화
])

test_transforamtion = v2.Compose([
    v2.Resize((224, 224)), #이미지 크기를 224x224로
    v2.ToImage(),  # 이미지를 Tensor 자료형으로 변환
    v2.ToDtype(torch.float32, scale=True), #텐서 자료형을 [0~1]로 스케일링
    v2.Normalize(mean=C_N_D_val[0], std=C_N_D_val[1]) #데이터셋 정규화
])

#전처리 방법론 적용
train_dataset.transform = train_transformation
train_dataset_not_aug.transform = test_transforamtion
test_dataset.transform = test_transforamtion
from torch.utils.data import DataLoader

BATCH_SIZE = 128
#전처리가 완료된 데이터셋을 Mini-batch가 생성가능한 dataloader로 변환
train_loader = DataLoader(train_dataset,
                            batch_size=BATCH_SIZE,
                            shuffle=True)
train_not_aug_loader = DataLoader(train_dataset_not_aug,
                            batch_size=BATCH_SIZE,
                            shuffle=True)

test_loader = DataLoader(test_dataset,
                            batch_size=BATCH_SIZE,
                            shuffle=False)

여기까지 수행후 16. 주요 CNN알고리즘 구현 : ResNet에 기재한 데이터셋을 검증하는 코드를 수행하면
아래와 같이 원할하게 데이터로더의 자료형이 설계됨을 확인할 수 있다.


Train Loader 정보:
배치 인덱스: 0
이미지 크기: torch.Size([128, 3, 224, 224])
라벨 크기: torch.Size([128])
첫 번째 이미지의 라벨: 0
라벨의 데이터타입 : torch.int64

Test Loader 정보:
배치 인덱스: 0
이미지 크기: torch.Size([128, 3, 224, 224])
라벨 크기: torch.Size([128])
첫 번째 이미지의 라벨: 0
라벨의 데이터타입 : torch.int64

4. 하이퍼 파라미터 설정

하이퍼 파라미터 설정 전 GoogLeNet을 객체화+GPU로 이전을 수행하자

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model = GoogLeNet(in_channels=3, num_classes=1)
model.to(device)
summary(model, input_size=(3, 224, 224), device=device.type)

Cats and Dogs 데이터셋은 Label데이터가 Cat, Dog 두개이고 이진 분류에 따라
Cat=0, Dog=1이니 0 or 1로 생각해서
num_classes=1로 놓고 모델을 객체화 한다.

4.1 논문의 하이퍼 파라미터

# 옵티마이저 설정 (SGD + Momentum)
learning_rate = 0.01
momentum = 0.9
weight_decay = 0.0002

optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum, weight_decay=weight_decay)

# 학습률 스케줄러 설정 (8 에포크마다 학습률을 10배 감소)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=8, gamma=0.1)

4.2 실습 설정


from torch import optim

#LossFn, Optimizer, scheduler 정의
#라벨이 2개이면 BLELoss를 써야하나 이걸 쓰려면 출력값이 [0~1]로만 나와야함
#[0~1]로만 데이터가 나오려면 GoogLeNet의 마지막 레이어에 nn.Sigmoid()을 추가해야함
#따라서 이걸 추가하지 않고 쓰려면 BLELoss랑 같은 기능을 하면서
#출력값을 Sigmod처리해서 받아내는 nn.BCEWithLogitsLoss()을 쓴다.
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=75)

라벨이 0 or 1인 이진분류 문제이니
nn.BLELoss()함수를 써야 하나 현재 모델의 최종 출력값은
nn.BLELoss()가 받아들일 수 있는 [0 ~ 1]의 확률값이 출력되지 않는다.
이 값이 출력되려면 모델의 마지막에 nn.Sidmod()함수를 붙여야 한다.
(다중분류의 nn.Softmax()랑 같은 개념이라 보면 된다.)

따라서 손실함수중에 nn.Softmax() + LossFn역할을 수행하는 함수가 nn.CorssEntropyLoss()
인 것처럼 nn.Sigmod() + LossFn(BLELoss)역활을 수행하는 함수를 쓰면 되는데
그것이 nn.BCEWithLogitsLoss()이다.


5. 훈련/검증/실행

from tqdm import tqdm #훈련 진행상황 체크

#tqdm 시각화 도구 출력 사이즈 조절 변수
epoch_step = 5
def model_train(model, data_loader, 
                loss_fn, optimizer_fn, scheduler_fn, 
                processing_device, epoch):
    model.train()  # 모델을 훈련 모드로 설정

    global epoch_step

    # loss와 accuracy를 계산하기 위한 임시 변수를 생성
    run_size, run_loss, correct = 0, 0, 0

    # 특정 에폭일 때만 tqdm 진행상황 바 생성
    if (epoch + 1) % epoch_step == 0 or epoch == 0:
        progress_bar = tqdm(data_loader)
    else:
        progress_bar = data_loader

    for image, label in progress_bar:

        # 입력된 데이터를 먼저 GPU로 이전하기
        image = image.to(processing_device)

        # 이진 분류를 위한 레이블 형식 변환
        label = label.to(processing_device).float().view(-1, 1)

        # 전사 과정 수행
        output_0, output_1, output_2 = model(image)

        loss_0 = loss_fn(output_0, label)
        loss_1 = loss_fn(output_1, label)
        loss_2 = loss_fn(output_2, label)

        loss = loss_0 + 0.3*(loss_1 + loss_2)

        #backward과정 수행
        optimizer_fn.zero_grad()
        loss.backward()
        optimizer_fn.step()

        # 스케줄러 업데이트
        scheduler_fn.step()

        #이진 분류이기 때문에 0.5를 기준으로 클래스 예측
        pred = torch.round(torch.sigmoid(output_0))
        correct += pred.eq(label).sum().item()

        #현재까지 수행한 loss값을 얻어냄
        run_loss += loss.item() * image.size(0)
        run_size += image.size(0)

        #tqdm bar에 추가 정보 기입
        if (epoch + 1) % epoch_step == 0 or epoch == 0:
            progress_bar.set_description('[Training] loss: ' +
                                         f'{run_loss / run_size:.4f}, accuracy: ' +
                                         f'{correct / run_size:.4f}')

    avg_accuracy = correct / len(data_loader.dataset)
    avg_loss = run_loss / len(data_loader.dataset)

    return avg_loss, avg_accuracy
def model_evaluate(model, data_loader, loss_fn, 
                   processing_device, epoch):
    model.eval()  # 모델을 평가 모드로 전환 -> dropout 기능이 꺼진다
    # batchnormalizetion 기능이 꺼진다.
    global epoch_step

    # gradient 업데이트를 방지해주자
    with torch.no_grad():

        # 여기서도 loss, accuracy 계산을 위한 임시 변수 선언
        run_loss, correct = 0, 0

        # 특정 에폭일 때만 tqdm 진행상황 바 생성
        if (epoch + 1) % epoch_step == 0 or epoch == 0:
            progress_bar = tqdm(data_loader)
        else:
            progress_bar = data_loader

        for image, label in progress_bar:  # 이때 사용되는 데이터는 평가용 데이터
            # 입력된 데이터를 먼저 GPU로 이전하기
            image = image.to(processing_device)
            # 이진 분류를 위한 레이블 형식 변환
            label = label.to(processing_device).float().view(-1, 1)


            # 평가 결과를 도출하자
            output = model(image)
            #이진 분류이기 때문에 0.5를 기준으로 클래스 예측
            pred = torch.round(torch.sigmoid(output))

            # 모델의 평가 결과 도출 부분
            # 배치의 실제 크기에 맞추어 정확도와 손실을 계산
            correct += torch.sum(pred.eq(label)).item()
            run_loss += loss_fn(output, label).item() * image.size(0)

        accuracy = correct / len(data_loader.dataset)
        loss = run_loss / len(data_loader.dataset)

        return loss, accuracy

훈련/평가 코드도 살짝 달라진 부분이 있는데

label = label.to(processing_device).float().view(-1, 1)

0 or 1 이진분류 문제로 바뀌었으니
라벨의 2차원 데이터도 1차원 데이터로 차원 변형을 수행해야 하고

        output_0, output_1, output_2 = model(image)

        loss_0 = loss_fn(output_0, label)
        loss_1 = loss_fn(output_1, label)
        loss_2 = loss_fn(output_2, label)

        loss = loss_0 + 0.3*(loss_1 + loss_2)

GoogLeNet의 Auxiliary classifier으로 인해서 모델의 출력값이 복잡하게 연산되느 부분이 있다.

loss_1, loss_2Auxiliary classifier의 중간 출력 결과물이고
loss_0은 모델의 끝단에서 출력되는 최종 출력 결과물이다.

이 중간출력 결과물의 반영 비율이
loss = loss_0 + 0.3*(loss_1 + loss_2)에서 0.3이란 계수라 보면 된다.

Auxiliary classifier는 이렇게 Gradient Vanishing문제를 억제하겠다고
코드 이곳저곳에 복잡한 구현로직을 설계해야 하니
안쓰게 되는게 이해가 되는 부분이다...

마지막으로 0 or 1 이진분류 문제이기에

pred = torch.round(torch.sigmoid(output_0))

예측의 결과값을 맞추는 문제도 위 코드로 바뀌었다.

다중분류 문제라면 높은값의 idx를 맞추는

pred = output.argmax(dim=1) #예측값의 idx출력

의 코드가 들어가야 함은 이전 포스트
12. 주요 CNN알고리즘 구현 : LeNet에서 설명을 진행했다.

# 학습과 검증 손실 및 정확도를 저장할 리스트
his_loss, his_accuracy = [], []
num_epoch = 50

for epoch in range(num_epoch):
    # 훈련 손실과 정확도를 반환 받습니다.
    train_loss, train_acc = model_train(model, train_loader, 
                                        criterion, optimizer, scheduler, 
                                        device, epoch)

    # 검증 손실과 검증 정확도를 반환 받습니다.
    test_loss, test_acc = model_evaluate(model, test_loader, 
                                         criterion, device, epoch)

    # 손실과 정확도를 리스트에 저장
    his_loss.append((train_loss, test_loss))
    his_accuracy.append((train_acc, test_acc))

    #epoch가 특정 배수일 때만 출력하기
    if (epoch + 1) % epoch_step == 0 or epoch == 0:
        print(f"epoch {epoch+1:03d}, Training loss: " + 
              f"{train_loss:.4f}, Training accuracy: {train_acc:.4f}")
        print(f"Test loss: {test_loss:.4f}, Test accuracy: {test_acc:.4f}")

여기서 데이터 증강의 유/무에 대한 실습을 진행한다면

    train_loss, train_acc = model_train(model, train_loader, 
                                        criterion, optimizer, scheduler, 
                                        device, epoch)

이 함수의
train_loader를 데이터 증강이 적용되지 않은 훈련 데이터로더셋
train_not_aug_loader을 적용하면 된다.


6. 데이터 증강 유/무 실험결과

데이터 증강의 적용 유/무에 따른 실험 결과이다.
증강을 적용된 항목이 좀 더 성능이 잘 나오는 것처럼 보이는데
이것은 더 가벼운 CNN계열 모델로 훈련시켰을 때 차이가 더 극명하게 드러난다

데이터를 증강시키면 훈련 결과가 덜 정확하게 보이는데

이것은 데이터 증강이 더 많은 데이터 다양성을 만들어내기에
학습이 더 어려워진다

따라서 데이터 증강을 시켯으면
학습을 더 시켜야함으로 보면 된다.

전반적으로 데이터 증강이 적용되면 해당 모델의 overfitting에 도달하는 값이 뒤로 더 밀려났다...
이렇게 생각하면 된다.

그래서 결론은

데이터 증강 적용 -> epoch를 더 늘려주기

이다.

profile
자율차 공부중

0개의 댓글