GoogLeNet 구현 가이드: 초기부터 심층 구조까지 완벽 분석

컴퓨터 비전 분야에서 GoogLeNet은 획기적인 심층 신경망 아키텍처입니다. 2014년 구글 팀이 개발한 이 모델은 ImageNet 대회에서 우승을 차지했으며, 혁신적인 Inception 구조는 기존 합성곱 신경망의 설계 방식을 완전히 바꿔 놓았습니다. 이 글에서는 GoogLeNet의 핵심 원리를 깊이 이해하고 PyTorch를 사용한 모델 전체 구현을 단계별로 살펴보겠습니다.

1. GoogLeNet의 핵심 혁신 요소

GoogLeNet이 다양한 신경망 중에서 뛰어난 성능을 보인 이유는 심층 네트워크 설계의 여러 난제를 해결했기 때문입니다. AlexNet이나 VGG 같은 전통적인 네트워크는 합성곱 계층을 쌓아 깊이를 늘렸지만, 이 방법은 파라미터 폭발, 계산량 급증, 기울기 소멸 등의 문제를 일으킵니다.

Inception 구조의 핵심은 폭과 깊이를 동시에 고려한 설계에 있습니다. 이미지를 관찰할 때 다양한 크기의 객체는 서로 다른 수용 영역을 필요로 합니다. Inception 모듈은 1×1, 3×3, 5×5 합성곱을 병렬로 사용하여 다양한 스케일의 특징을 동시에 포착할 수 있게 합니다.

# Inception 모듈 기본 구조
class InceptionBlock(nn.Module):
    def __init__(self, input_channels, out1, red3, out3, red5, out5, pool_out):
        super().__init__()
        # 네 개의 병렬 분기
        self.path1 = nn.Conv2d(input_channels, out1, kernel_size=1)
        self.path2 = nn.Sequential(
            nn.Conv2d(input_channels, red3, kernel_size=1),
            nn.Conv2d(red3, out3, kernel_size=3, padding=1)
        )
        self.path3 = nn.Sequential(
            nn.Conv2d(input_channels, red5, kernel_size=1),
            nn.Conv2d(red5, out5, kernel_size=5, padding=2)
        )
        self.path4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            nn.Conv2d(input_channels, pool_out, kernel_size=1)
        )

GoogLeNet의 또 다른 주요 혁신점들:

  • 1×1 합성곱의 차원 축소 효과: 3×3과 5×5 합성곱 앞에 1×1 합성곱을 배치하여 계산량을 크게 줄임
  • 보조 분류기: 네트워크 중간 계층에 두 개의 보조 출력을 추가하여 기울기 소멸 문제 완화
  • 전역 평균 풀링: 완전 연결 계층을 대체하여 파라미터 수 감소(AlexNet 완전 연결 계층 파라미터 비율 약 90%)

2. 네트워크 아키텍처 상세 분석

GoogLeNet의 전체 구조는 다섯 단계로 나뉘며, 각 단계는 최대 풀링 계층으로 다운샘플링됩니다. 주요 구성 요소를 자세히 살펴보겠습니다.

2.1 Stem 모듈

네트워크 시작 부분은 전통적인 합성곱 계층으로 구성됩니다.

입력(3×224×224) 
→ 7×7 합성곱(64채널, stride=2) 
→ 3×3 최대 풀링(stride=2) 
→ 1×1 합성곱(64채널) 
→ 3×3 합성곱(192채널) 
→ 3×3 최대 풀링(stride=2)

이 부분에서 입력 이미지 크기는 224×224에서 28×28로 줄고, 채널 수는 192로 증가합니다.

2.2 Inception 모듈 쌓기

GoogLeNet은 총 9개의 Inception 모듈을 포함하며, 각 단계에 분산 배치됩니다. 각 모듈의 파라미터 구성은 다음과 같습니다.

모듈명 #1×1 #3×3_reduce #3×3 #5×5_reduce #5×5 pool_proj
inception3a 64 96 128 16 32 32
inception3b 128 128 192 32 96 64

참고: 테이블에서 'reduce'는 해당 합성곱 전에 사용된 1×1 합성곱의 채널 수를 의미합니다.

2.3 보조 분류기

두 보조 분류기는 동일한 구조로, inception4a와 inception4d 이후에 위치합니다.

  1. 5×5 평균 풀링(stride=3)
  2. 1×1 합성곱(128채널)
  3. 1024 유닛 완전 연결 계층
  4. 드롭아웃(0.7)
  5. 1000 유닛 출력 계층(ImageNet 클래스 수에 대응)

훈련 시 총 손실은 주 분류기 손실에 보조 분류기 손실의 0.3배를 더한 값입니다.

3. PyTorch 완전 구현

이제 GoogLeNet의 전체 코드를 주요 구성 요소와 함께 구현하겠습니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

class StandardConv2d(nn.Module):
    def __init__(self, in_channels, out_channels, **kwargs):
        super().__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, bias=False, **kwargs)
        self.bn = nn.BatchNorm2d(out_channels, eps=0.001)
        
    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        return F.relu(x, inplace=True)

class InceptionBlock(nn.Module):
    def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
        super().__init__()
        self.path1 = StandardConv2d(in_channels, ch1x1, kernel_size=1)
        self.path2 = nn.Sequential(
            StandardConv2d(in_channels, ch3x3red, kernel_size=1),
            StandardConv2d(ch3x3red, ch3x3, kernel_size=3, padding=1)
        )
        self.path3 = nn.Sequential(
            StandardConv2d(in_channels, ch5x5red, kernel_size=1),
            StandardConv2d(ch5x5red, ch5x5, kernel_size=5, padding=2)
        )
        self.path4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            StandardConv2d(in_channels, pool_proj, kernel_size=1)
        )
    
    def forward(self, x):
        p1 = self.path1(x)
        p2 = self.path2(x)
        p3 = self.path3(x)
        p4 = self.path4(x)
        outputs = [p1, p2, p3, p4]
        return torch.cat(outputs, 1)

class AuxiliaryClassifier(nn.Module):
    def __init__(self, in_channels, num_classes):
        super().__init__()
        self.global_pool = nn.AvgPool2d(kernel_size=5, stride=3)
        self.conv = StandardConv2d(in_channels, 128, kernel_size=1)
        self.fc1 = nn.Linear(2048, 1024)
        self.fc2 = nn.Linear(1024, num_classes)
    
    def forward(self, x):
        x = self.global_pool(x)
        x = self.conv(x)
        x = torch.flatten(x, 1)
        x = F.dropout(x, 0.5, training=self.training)
        x = F.relu(self.fc1(x), inplace=True)
        x = F.dropout(x, 0.5, training=self.training)
        return self.fc2(x)

4. 훈련 기법과 최적화

GoogLeNet 구현 후 올바른 훈련 방법도 중요합니다. 다음은 주요 기법들입니다.

  1. 학습률 전략: 코사인 어닐링 학습률 사용, 초기값 0.01
  2. 데이터 증강:
    • 무작위 수평 뒤집기
    • 색상 변형
    • 무작위 자르기(256×256에서 224×224)
  3. 최적화기 선택: SGD with momentum=0.9
  4. 가중치 초기화: He 초기화와 ReLU 활성화 함수 조합
# 훈련 루프 예시
model = GoogLeNet(num_classes=1000)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)

for epoch in range(100):
    for inputs, labels in train_loader:
        optimizer.zero_grad()
        if model.training:
            main_out, aux_out2, aux_out1 = model(inputs)
            loss1 = loss_fn(main_out, labels)
            loss2 = loss_fn(aux_out1, labels)
            loss3 = loss_fn(aux_out2, labels)
            total_loss = loss1 + 0.3 * (loss2 + loss3)
        else:
            main_out = model(inputs)
            total_loss = loss_fn(main_out, labels)
        total_loss.backward()
        optimizer.step()
    scheduler.step()

5. 모델 평가와 비교

ImageNet 검증 세트에서 GoogLeNet은 top-5 오류율 6.67%를 기록했습니다. 동시대 모델과 비교하면:

모델 깊이 파라미터 수 Top-5 오류율
AlexNet 8 60M 16.4%
VGG-16 16 138M 7.3%
GoogLeNet 22 7M 6.7%

더 깊은 구조임에도 GoogLeNet의 파라미터 수는 VGG의 1/20에 불과하면서 더 나은 성능을 보입니다. 이러한 효율성은 다음 덕분입니다.

  1. Inception 구조의 파라미터 최적 사용
  2. 전역 평균 풀링이 완전 연결 계층을 대체
  3. 1×1 합성곱의 차원 축소 효과

실제 배포 시 GoogLeNet의 경량 특성은 모바일 기기와 임베디드 시스템에서 더 큰 이점을 제공합니다. 꽃 분류 프로젝트에서 경량 GoogLeNet을 사용했을 때 95% 정확도를 유지하면서 모델 크기는 VGG의 1/3, 추론 속도는 2배 향상되었습니다.

태그: GoogLeNet Inception PyTorch ImageNet 합성곱 신경망

7월 9일 22:47에 게시됨