본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 인공지능 고급-시각 강의의 CNN알고리즘 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.
GoogLeNet는 ILSVRC-2014에서 우승을 차지한 Google 연구팀이 개발한 네트워크로 Inception Module이란 아키텍쳐의 도입을 통해 네트워크의 깊이(Depth)와 너비(Width)를 효율적으로 확장할 수 있는 방법론을 제시하였다.
향후 Inception Module을 도입하여 발전시킨 다양한 네트워크가 발표되면서 GoogLeNet는 Inception V1으로 불린다.
네트워크의 성능을 향상시키는 가장 직관적인 방법은 깊이
, 너비
를 확장시키는 것이지만, 확장시킨 만큼 파라미터 개수가 늘어나 네트워크가 무거워진다.
이렇게 네트워크가 무거워지면 Overfitting의 가능성, 요구 리소스가 높아지는 단점이 있는데
이를 GoogLeNet은 Inception Module의 도입으로 문제를 해결해 냈다.
Inception Module은 위 사진처럼 1x1
, 3x3
, 5x5
같이 다양한 크기를 갖는 Kernel이 적용된 Layer을 병렬로 적용하여 여러 종류의 특징정보를 추출하는 것이다.
이렇게 이미지에 대해 다양한 시각적 정보를 동시에 포착하고 이후 Filter Concat
과정을 통해 다음 레이어에서 여러 수준이 통합된 정보를 학습 풍부한 표현정보의 학습효과를 노린다.
위 1.1에서 설계한 Inception Module은 Prev Layer Next Layer로 넘어가는 과정에서 차원이 기하급수로 늘어나는 문제점이 발생하기에 이를 방지하기 위한 목적으로 [1x1] Conv
를 각각 아래의 그림처럼 배치하여 Next Layer의 차원을 축소, 이로 인한 연산량 감소 효과를 꾀한다.
GoogLeNet에는 위 사진처럼 네트워크의 중간마다 Auxiliary classifier 모듈이 붙어 중간 결과값을 추출해낸다.
이 보조분류기(Auxiliary classification)이 추가적인 Gradient 정보를 제공하여 역전파 때 Gradient Vanishing 문제를 억제해준다.
하지만 후기 Inception계열 네트워크에는 해당 기법을 사용하지 않는데 Batch Normalization(배치정규화), ResNet의 Residual Connection 개념 도입, Inception 모듈 개선등으로 더이상 Auxiliary classification
을 사용하지 않는다.
이는 Auxiliary classification
을 구현해 보면 알 수 있듯이 해당 모듈을 도입하면 디버깅 및 최적화에 어려운 점이 있다.
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만개의 파라미터가 생성된다.
이정도면 꽤 경량한 모델이라 보면 된다.
데이터셋은 이전 포스트인 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를 사용한다.
Dataset
을 DataLoader
로 변환하기 전
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
하이퍼 파라미터 설정 전 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
로 놓고 모델을 객체화 한다.
# 옵티마이저 설정 (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)
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()
이다.
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_2
는 Auxiliary 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
을 적용하면 된다.
데이터 증강의 적용 유/무에 따른 실험 결과이다.
증강을 적용된 항목이 좀 더 성능이 잘 나오는 것처럼 보이는데
이것은 더 가벼운 CNN계열 모델로 훈련시켰을 때 차이가 더 극명하게 드러난다
데이터를 증강시키면 훈련 결과가 덜 정확하게 보이는데
이것은 데이터 증강이 더 많은 데이터 다양성을 만들어내기에
학습이 더 어려워진다
따라서 데이터 증강을 시켯으면
학습을 더 시켜야함으로 보면 된다.
전반적으로 데이터 증강이 적용되면 해당 모델의 overfitting
에 도달하는 값이 뒤로 더 밀려났다...
이렇게 생각하면 된다.
그래서 결론은
이다.