
이 글은 Szegedy et al.(2014)의 Going Deeper with Convolutions 을 원 논문의 전개(문제 제기 → 관련 연구 → 동기/설계 원칙 → Inception 모듈 설계 → GoogLeNet 구체화 → 학습 방법론 → ILSVRC 분류/검출 결과 → 결론)를 가능한 한 그대로 따라가며 촘촘히 풀어쓴 상세 리뷰다.
이 논문에서 제안하는 아키텍처는 Inception이고, ILSVRC14 제출에 사용한 22-layer 구체 인스턴스가 GoogLeNet이다. 즉 GoogLeNet은 Inception 아키텍처의 한 구현(incarnation) 이다. 논문이 강조하는 핵심은 단지 더 깊게 만들었다가 아니라, 동일하거나 제한된 연산 예산(computational budget) 안에서 depth/width를 늘릴 수 있도록, 네트워크 내부의 계산 자원을 더 효율적으로 쓰는 구조를 만들었다는 점이다.
논문 초반에서 이미 정량적 메시지를 던진다.
또한 이 논문은 분류(classification)뿐 아니라 검출(detection)에서도 ILSVRC14에서 강력한 성능을 냈다고 보고한다. 특히 검출에서는 R-CNN류 파이프라인과 결합한 딥러닝과 고전 CV의 시너지가 성능 향상의 핵심이었다고 정리한다.
논문은 지난 몇 년간(2012~2014) 이미지 인식/검출 성능이 급격히 향상된 배경을 짚는다. 중요한 주장 하나는 이 진전이 단지 더 좋은 하드웨어/더 큰 데이터/더 큰 모델 때문만은 아니라는 점이다. ILSVRC14에서 상위권 팀들 역시 detection을 위해 새로운 외부 데이터 소스를 추가로 쓰지 않았고, 주된 개선은 아이디어, 알고리즘, 네트워크 아키텍처의 발전에서 나왔다고 말한다.
이 문맥에서 GoogLeNet은 두 가지 축을 동시에 잡는다.
특히 모바일/임베디드 환경에서 메모리/전력 제약이 중요해지고 있으므로, 알고리즘 효율이 점점 더 중요한 설계 목표가 된다고 강조한다.
논문은 대부분의 실험에서 inference 시 1.5 billion multiply-adds 수준의 연산 예산을 유지하려고 했다고 명시한다. 여기서 multiply-add는 흔히 MAC(Multiply–Accumulate)처럼 생각하면 되고, conv 연산에서 대략
크기의 곱셈-덧셈이 발생한다는 점을 떠올리면 된다.
즉 이 논문이 하고자 하는 건 단순히 모델을 키우자가 아니라,
이 문제 설정이 이후의 희소 구조 → 조밀 블록 근사 → Inception 모듈로 자연스럽게 이어진다.
논문은 LeNet-5 이후 CNN이 보통 따르는 구조를 먼저 요약한다.
그리고 대규모 데이터셋(ImageNet)에서는 최근 트렌드가
로 요약된다고 말한다.
또한 max-pooling이 공간 정보를 잃는다는 우려가 있음에도, 동일한 CNN 구조가 localization/detection/pose 추정 등으로 확장되어 성공적으로 쓰였다는 점을 들며, CNN 표현이 다양한 과제에 강력하다는 배경을 깔아준다.
이 섹션에서 논문이 직접 연결하는 실마리들은 다음이다.
Network-in-Network (Lin et al.)
1×1 conv를 추가해 representational power를 늘리는 접근이다. 논문은 여기서 한 발 더 나아가, 1×1 conv를 표현력 증가뿐 아니라 차원 축소(dimension reduction) 로써 계산 병목 제거 목적에 매우 적극적으로 사용한다고 밝힌다. 즉 이 논문에서 1×1 conv는 중요한 모듈이다.
멀티스케일 처리(Serre et al.)
고정 Gabor 필터를 여러 크기로 두는 방식과 유사하게, Inception은 여러 스케일의 conv를 병렬로 두어 멀티스케일을 다루지만, Inception에서는 필터가 학습되고, 모듈이 반복되어 22-layer 수준의 깊은 모델이 된다는 점이 차별점이다.
이 섹션의 결론은 기존 아이디어(1×1 conv, 멀티스케일, 검출 파이프라인)를 가져오되, 계산 효율 관점에서 재조합해 더 깊고 넓은 네트워크를 만들겠다는 방향으로 정리할 수 있다.
논문은 성능을 올리는 가장 직관적인 방법으로 네트워크를 키우는 것을 먼저 말한다.
하지만 이 단순 해법에는 두 가지 큰 단점이 있다고 한다.
즉 논문이 말하는 더 크게의 문제는 단순히 느리다/메모리 크다를 넘어, 제한된 예산을 성능 향상에 가장 잘 쓰는 구조가 무엇인가라는 자원 배분 문제로 넘어간다.
논문은 이상적으로는 fully-connected 대신 희소(sparse) 연결을 네트워크 내부에도 더 적극적으로 도입하는 것이 두 문제(과적합, 연산 낭비)를 동시에 해결할 근본 해법이라고 본다.
여기서 Arora et al.를 핵심 이론적 근거로 끌어온다. 논문이 요약하는 메시지는 대략 다음이다.
이 이야기는 Hebbian 원리 (같이 발화하는 뉴런은 같이 연결된다)와도 직관적으로 맞닿아 있다고 말한다.
하지만 문제는 당시 하드웨어/소프트웨어 인프라가 비균일 희소 데이터 구조에서의 수치 연산에 매우 비효율적이라는 점이다. 희소 연산의 산술량이 100× 줄어도, 메모리 접근/캐시 미스/인덱싱 오버헤드가 지배적이라 실제 속도가 잘 안 나올 수 있다는 것이다.
그래서 이 논문은 희소 구조를 직접 쓰는 대신, 희소 구조가 요구할 법한 최적 로컬 토폴로지를 조밀한(dense) 구성요소로 덮어서(cover) 근사하자는 현실적 전략을 선택한다. 그 조밀한 구성요소가 바로 다음 섹션의 Inception 모듈이다.
이 섹션에서 논문은 translation invariance를 전제로, 네트워크가 convolutional building block들로 구성된다고 본다. 핵심 질문은 최적의 로컬 희소 구조를 어떻게 찾고, 이를 공간적으로 반복할 것인가다.
논문은 상관 기반 클러스터링 직관을 가져와 다음 같은 그림을 그린다.
그래서 한 stage에서 서로 다른 receptive field를 가진 연산들을 병렬로 두고, 그 출력 채널들을 depth 방향으로 concat해서 다음 stage의 입력으로 넘기는 구조를 제안한다. 여기에 pooling 경로도 병렬로 추가하면 도움이 될 것이라고 말한다.
하지만 (Fig. 2(a)) 같은 naive 구조에는 큰 문제가 있다.
여기서 논문이 제시하는 두 번째 핵심 아이디어가 차원 축소를 병목 구간에 전략적으로 넣자는 것이다. 즉 expensive conv(3×3, 5×5) 전에 1×1 conv로 채널 수를 줄여서 계산량을 낮춘다.
논문은 이를 embeddings의 성공에 기대어 설명한다. 낮은 차원 임베딩도 많은 정보를 담을 수 있다는 직관을 받아들이되, 너무 이른 압축은 표현을 너무 조밀하게 만들어 모델링이 어려울 수 있으니 많이 합쳐야 하는 구간에서만 압축하자고 말한다.
여기서 1×1 conv는 두 가지 역할을 한다.
이 결과가 (Fig. 2(b))이며, 오늘날 우리가 흔히 떠올리는 Inception 모듈의 표준 형태다.
입력 feature map을 라 하자(배치 , 채널 ). Inception 모듈은 서로 다른 변환 를 병렬로 적용하고 채널 축으로 이어붙인다.
각 는 (1×1), (1×1→3×3), (1×1→5×5), (pool→1×1) 같은 연산 그래프로 구성된다. 이때 reduction이 들어가면 (1×1)에서 출력 채널 수를 줄여 로 만든 뒤 큰 커널을 적용하므로, 연산량이 크게 줄어든다.
Inception 모듈이 하는 일은 사실상 같은 입력에서 서로 다른 receptive field로 정보를 뽑아 동시에 제공하고, 다음 stage가 그들 위에서 더 추상적인 특징을 만든다는 것이다. 이전 CNN들이 보통 한 stage에 한 종류의 커널을 쓰던 것과 달리, Inception은 stage 내부에서 멀티스케일을 결합한다.
논문은 Inception 모듈을 여러 번 쌓고, 중간중간 stride 2 max-pooling으로 해상도를 반으로 줄이는 구조를 기본으로 한다고 말한다. 다만 기술적 이유(훈련 시 메모리 효율)로 Inception 모듈을 아주 초반부터 쓰지 않고, 낮은 레벨은 전통적인 conv 스택으로 시작하는 것이 유리해 보였다고 설명한다. 이는 원리적 필연이 아니라 당시 구현 인프라의 제약에 가까운 선택이라는 점도 덧붙인다.
논문은 Inception이 아키텍처 패밀리이고, 그중 ILSVRC14 제출에서 쓴 구체 인스턴스를 GoogLeNet이라고 부른다고 명확히 한다. 이름은 LeNet-5에 대한 오마주라고 말한다.
또한 아래 표에 가장 성공한 particular instance(GoogLeNet)를 제시하며, 앙상블 7개 중 6개 모델은 동일한 토폴로지(sampling 방법만 다름)를 썼다고 한다.
#1×1#3×3 reduce, #3×3#5×5 reduce, #5×5pool proj아키텍처 전체 흐름은 아래 도표에서 보조 분류기까지 포함해 도식화된다.
논문은 Network-in-Network를 따라 classifier 앞에 average pooling을 두는 것이 도움이 된다고 말한다. 다만 GoogLeNet 구현은 average pooling 뒤에 추가 linear layer를 하나 두는데, 이는 다른 label set으로 fine-tuning 할 때 편의성 때문이며 성능에 큰 영향을 주진 않을 것으로 본다고 말한다.
흥미로운 정량적인 수치 언급도 있다.
즉 GoogLeNet은 거대한 FC 스택을 줄이고, 글로벌 평균 pooling 성격을 강화하면서도, regularization(dropout)을 유지하는 설계를 택했다.
22-layer 깊이에서 gradient가 잘 전파될지(backprop signal) 우려가 있었고, 논문은 중간 레이어의 특징도 충분히 discriminative할 것이라는 관찰에 기대어, 중간에 보조 분류기를 달아 학습을 돕는 아이디어를 사용한다.
핵심 목적은 세 가지다.
논문은 보조 분류기를 Inception (4a)와 (4d) 출력 위에 붙이며, 학습 시 이 보조 손실을 전체 손실에 더하되 가중치 0.3으로 discount한다고 말한다. inference 때는 보조 분류기를 버린다.
보조 분류기의 구조는 본문에서 bullet로 명시된다.
메인 분류기 손실을 , 보조 분류기 손실을 라고 하면, 학습에서 최종 손실은 논문 설명을 그대로 옮기면 다음처럼 정리할 수 있다.
이 식의 통찰은 보조 분류기를 메인 목적(정답 분류)을 바꾸지 않는 선에서, 학습을 돕는 정규화/gradient 보조 신호로 쓰자는 것이다. 가중치 은 보조 손실이 메인 손실을 지배하지 않도록 조절하는 장치다.
논문은 학습을 DistBelief 분산 학습 시스템으로 수행했다고 한다. CPU 기반 구현만 썼지만, 고급 GPU 몇 개면 1주 이내에 수렴시킬 수 있을 것이라 추정하며, 주된 제한은 메모리 사용량이라고 덧붙인다.
학습 최적화는 다음처럼 요약된다.
모멘텀 업데이트는 보통
로 이해하면 되고, 여기서 다. 비동기 SGD는 worker들이 서로 다른 배치로 업데이트를 병렬로 수행하는 형태로 떠올리면 된다.
Polyak averaging은 학습 과정에서 여러 시점의 가중치를 평균해 더 안정적인 모델을 얻는 방법으로 이해할 수 있다.
논문은 흥미롭게도 경쟁까지 몇 달 동안 이미지 샘플링 방법이 계속 바뀌었고, 이미 수렴한 모델을 다른 옵션으로 재학습(fine-tune)하기도 했으며, dropout/lr 같은 하이퍼파라미터도 함께 바뀌었다고 말한다. 그래서 가장 효과적인 단 하나의 학습법을 딱 잘라 말하기 어렵다고 솔직하게 적는다.
그럼에도 경쟁 후 검증된 레시피로 다음을 제시한다.
이 문단은 학습 레시피가 성능에 매우 중요하지만, 실전에서는 여러 선택이 얽혀 있어 clean한 ablation이 어렵다는 현실을 보여준다.
논문 내용을 그대로 절차로 정리하면 다음과 같다.
Algorithm: GoogLeNet training (Sect. 6)
Inputs:
- Training set (ImageNet ILSVRC)
- GoogLeNet/Inception architecture with auxiliary classifiers
- Loss weights: aux_weight = 0.3
Optimizer:
- Asynchronous SGD with momentum 0.9
- Learning rate decays by 4% every 8 epochs
- Polyak averaging for final inference model
Augmentation/Sampling (one recommended recipe):
- Sample random patches with area in [8%, 100%] of image
- Random aspect ratio in [3/4, 4/3]
- Photometric distortions
- Random resize interpolation method
Training loop:
- Forward main + aux heads
- Compute L = L_main + 0.3*L_aux1 + 0.3*L_aux2
- Backprop + async SGD update
Output:
- Polyak-averaged weights
논문은 ILSVRC14 classification을 다음처럼 요약한다.
top-5 error는 정답이 상위 5개 예측 안에 없을 비율이다. 즉 GT가 top-5에 있으면 정답으로 친다.
논문은 외부 데이터 없이 참가했다고 강조한다. 그 위에서 성능을 끌어올리기 위해 테스트 시점에 다음을 수행했다고 말한다.
동일 토폴로지의 GoogLeNet 7개를 독립 학습하고 앙상블
Krizehvsky et al.보다 더 공격적인 cropping
{256, 288, 320, 352}로 리사이즈(4 scales)softmax 확률을 crops와 모델들에 대해 평균
이 절차를 알고리즘으로 정리하면 아래처럼 쓸 수 있다.
Algorithm: GoogLeNet classification inference (Sect. 7)
Input: image I, ensemble models {M_j}
1) For each scale s in {256, 288, 320, 352}:
a) Resize I so shorter side = s
b) Take 3 squares (left/center/right or top/center/bottom)
c) For each square:
- take 4 corners + center 224x224 crops (5)
- take the whole square resized to 224x224 (1)
- add mirrored versions (x2)
2) For each crop c and each model M_j:
p_{j,c} = softmax(M_j(c))
3) Average probabilities:
p = mean_{j,c} p_{j,c}
Output: predicted class ranking from p
논문은 최종 제출 결과로 val/test 모두 top-5 error 6.67% 를 얻어 1위를 했다고 말한다. 또한 2012 SuperVision 대비 상대적으로 56.5% error 감소라고 강조한다.
여기서 경쟁 전체 결과 비교가 다음 표로 제시된다.
그리고 crops/모델 수에 따른 성능/비용 변화가 아래 표로 제시된다. 텍스트 추출본에서 표가 완전히 정렬되진 않지만, 핵심 메시지는 다음이다.
이 구간의 해석을 한 문장으로 요약하면 GoogLeNet 자체 아키텍처가 강한 것은 기본이고, ILSVRC 순위 수준의 최종 성능은 test-time computation(멀티크롭/앙상블)과 강하게 결합되어 있다.
이 논문의 의의는 Inception 모듈이라는 블록을 만든 것 이상으로, 제약(연산 예산)을 명시한 상태에서 아키텍처를 설계하는 사고 방식을 대중화했다는 점에 있다.
또한 Inception 모듈은 한 stage에서 멀티스케일을 병렬로 처리하고 concatenate한다는 관점으로 이후 수많은 변형(v2/v3/v4, Xception, Inception-ResNet 등)으로 확장된다. 즉 GoogLeNet은 단일 모델이라기보다 아키텍처 패밀리의 출발점이다.
한계도 있다.
그럼에도 이 논문은 효율을 고려한 깊은 네트워크 설계라는 주제를 강하게 제시했고, 이후의 아키텍처 연구가 단순한 더 크고 더 깊게에서 벗어나 자원 배분/모듈 설계/병렬 경로/차원 축소 같은 키워드로 전개되게 만든 대표적 전환점이라고 볼 수 있다.
논문 본문에서는 GoogLeNet이라는 이름을 쓰지만, lucid 구현에서는 같은 모델을 inception_v1(InceptionNet-v1)로 지칭한다. 이 파트에서는 inception.py를 직접 읽고, 논문이 코드에서 어떻게 구현되는지 단계적으로 연결한다.
먼저 파일 상단은 import와 공개 심볼, 그리고 공통 베이스 클래스를 정의한다.
class Inception(nn.Module):
def __init__(self, num_classes: int, use_aux: bool | None = True) -> None:
super().__init__()
self.num_classes = num_classes
self.use_aux = use_aux
@override
def forward(self, x: Tensor) -> Tuple[Tensor | None, ...]:
return super().forward(x)
여기서 핵심은 Inception이 nn.Module을 상속하는 공통 베이스이고, num_classes와 use_aux를 저장한다는 점이다. forward는 Tuple[Tensor | None, ...] 형태를 반환하는 것으로 타입을 넓혀두고, 실제 forward는 하위 클래스들이 구현한다.
논문의 Inception 모듈(병렬 1×1 / 3×3 / 5×5 / pool 경로 + concat)은 _InceptionModule로 구현되어 있다.
class _InceptionModule(nn.Module):
def __init__(
self,
in_channels: int,
out_channels_1x1: int,
reduce_3x3: int,
out_channels_3x3: int,
reduce_5x5: int,
out_channels_5x5: int,
out_channels_pool: int,
) -> None:
super().__init__()
self.branch1 = nn.ConvBNReLU2d(in_channels, out_channels_1x1, kernel_size=1)
self.branch2 = nn.Sequential(
nn.ConvBNReLU2d(in_channels, reduce_3x3, kernel_size=1),
nn.ConvBNReLU2d(reduce_3x3, out_channels_3x3, kernel_size=3, padding=1),
)
self.branch3 = nn.Sequential(
nn.ConvBNReLU2d(in_channels, reduce_5x5, kernel_size=1),
nn.ConvBNReLU2d(reduce_5x5, out_channels_5x5, kernel_size=5, padding=2),
)
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
nn.ConvBNReLU2d(in_channels, out_channels_pool, kernel_size=1),
)
def forward(self, x: Tensor) -> Tensor:
return lucid.concatenate(
[self.branch1(x), self.branch2(x), self.branch3(x), self.branch4(x)],
axis=1,
)
논문과의 대응을 branch 단위로 읽으면:
branch1: 1×1 conv 경로 (Fig. 2의 1×1 path)branch2: 1×1 reduction → 3×3 conv (Fig. 2(b)의 #3×3 reduce → 3×3)branch3: 1×1 reduction → 5×5 conv (Fig. 2(b)의 #5×5 reduce → 5×5)branch4: 3×3 maxpool(stride 1, padding 1) → 1×1 projection (pool proj)그리고 마지막 lucid.concatenate([...], axis=1)이 논문에서 말하는 filter bank concatenation(DepthConcat) 이다. 즉 Inception 모듈의 본질인 병렬 경로의 출력 채널을 쌓아서 다음 stage 입력으로 만든다가 코드에서 그대로 보인다.
여기서 nn.ConvBNReLU2d는 (conv + batchnorm + ReLU) 계열의 블록이며, 논문 GoogLeNet은 LRN을 언급하는 반면, Lucid 구현은 BN을 포함한 conv 블록을 기본 단위로 사용한다는 점이 논문과 구현의 차이로 남는다.
논문에서 보조 분류기는 avgpool → 1×1 conv(128) → FC 1024 → dropout 0.7 → linear로 정의됐다. Lucid의 _InceptionAux는 이를 매우 직접적으로 구현한다.
class _InceptionAux(nn.Module):
def __init__(
self, in_channels: int, num_classes: int, pool_size: tuple[int, int]
) -> None:
super().__init__()
self.avgpool = nn.AdaptiveAvgPool2d(pool_size)
self.conv = nn.ConvBNReLU2d(in_channels, 128, kernel_size=1)
self.fc1 = nn.Linear(128 * pool_size[0] * pool_size[1], 1024)
self.fc2 = nn.Linear(1024, num_classes)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(p=0.7)
def forward(self, x: Tensor) -> Tensor:
x = self.avgpool(x)
x = self.conv(x)
x = x.reshape(x.shape[0], -1)
x = self.fc1(x)
x = self.dropout(self.relu(x))
x = self.fc2(x)
return x
논문에서는 average pooling 5×5 stride 3로 4×4 output을 만들었다고 설명하지만, Lucid는 AdaptiveAvgPool2d(pool_size)로 출력을 (4,4)로 강제한다. 결과적으로 4×4 feature를 만든다는 목적은 같고, 구현은 입력 크기 변화에도 robust하게 만든 형태로 볼 수 있다.
또한 dropout이 p=0.7로 설정되어 있어 논문 bullet(70% dropped outputs)과 정합된다.
이제 핵심인 Inception-v1(GoogLeNet) 전체 네트워크는 Inception_V1에 구현되어 있다.
class Inception_V1(Inception):
def __init__(self, num_classes: int = 1000, use_aux: bool = True) -> None:
super().__init__(num_classes, use_aux)
in_channels = 3
self.conv1 = nn.ConvBNReLU2d(
in_channels, 64, kernel_size=7, stride=2, padding=3
)
self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.conv2 = nn.Sequential(
nn.ConvBNReLU2d(64, 64, kernel_size=1),
nn.ConvBNReLU2d(64, 192, kernel_size=3, stride=1, padding=1),
)
self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.incep_3 = nn.Sequential(
_InceptionModule(192, 64, 96, 128, 16, 32, 32),
_InceptionModule(256, 128, 128, 192, 32, 96, 64),
)
self.maxpool3 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.incep_4a = _InceptionModule(480, 192, 96, 208, 16, 48, 64)
self.incep_4bcd = nn.Sequential(
_InceptionModule(512, 160, 112, 224, 24, 64, 64),
_InceptionModule(512, 128, 128, 256, 24, 64, 64),
_InceptionModule(512, 112, 144, 288, 32, 64, 64),
)
self.incep_4e = _InceptionModule(528, 256, 160, 320, 32, 128, 128)
self.maxpool4 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.incep_5 = nn.Sequential(
_InceptionModule(832, 256, 160, 320, 32, 128, 128),
_InceptionModule(832, 384, 192, 384, 48, 128, 128),
)
if use_aux:
self.aux1 = _InceptionAux(512, num_classes, pool_size=(4, 4))
self.aux2 = _InceptionAux(528, num_classes, pool_size=(4, 4))
else:
self.aux1 = None
self.aux2 = None
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.dropout = nn.Dropout(p=0.4)
self.fc = nn.Linear(1024, num_classes)
def forward(self, x: Tensor) -> Tuple[Tensor, Optional[Tensor], Optional[Tensor]]:
x = self.maxpool1(self.conv1(x))
x = self.maxpool2(self.conv2(x))
x = self.maxpool3(self.incep_3(x))
x = self.incep_4a(x)
if self.aux1 is not None and self.training:
aux1 = self.aux1(x)
else:
aux1 = None
x = self.incep_4bcd(x)
if self.aux2 is not None and self.training:
aux2 = self.aux2(x)
else:
aux2 = None
x = self.maxpool4(self.incep_4e(x))
x = self.avgpool(self.incep_5(x))
x = x.reshape(x.shape[0], -1)
x = self.dropout(x)
x = self.fc(x)
return x, aux2, aux1
이 코드는 논문 GoogLeNet의 전통적 stem → Inception 스택 → 평균풀링+드롭아웃+선형 분류기 흐름을 매우 직접적으로 담고 있다.
논문 Table 1/Fig 3에서 앞단은 전통적 conv로 시작한다고 했고, Lucid도 동일하게 7×7 conv로 시작한다.
conv1: 7×7, stride 2, padding 3 → 공간 해상도를 빠르게 줄이는 AlexNet류 stemmaxpool1: 3×3, stride 2conv2: 1×1 후 3×3(padding 1) → 채널 재구성 + 지역 문맥maxpool2: 3×3, stride 2_InceptionModule(in_channels, out1, red3, out3, red5, out5, poolproj)의 숫자들은 논문 Table 1에 나오는 각 경로의 채널 수를 코드로 직접 박아 넣은 것이다. 예를 들어 stage3 첫 모듈은
로 읽히며, 이는 차원 축소를 먼저 하고 큰 커널을 적용한다는 Fig. 2(b)의 원칙을 그대로 구현한 형태다.
self.training 조건논문은 보조 분류기를 (4a)와 (4d)에 붙인다고 했다. Lucid는
incep_4a 출력에서 aux1incep_4bcd(4b,4c,4d의 묶음) 출력에서 aux2를 뽑는다. 즉 aux2는 4d 이후 출력에 해당한다고 볼 수 있다.
또한 중요한 구현 디테일이 있다.
self.training일 때만 계산한다.None이 된다.즉 논문에서 말한 inference time에 auxiliary network discard가
if self.aux1 is not None and self.training
같은 조건으로 구현되어 있다.
(main, aux2, aux1) 순서forward가 return x, aux2, aux1 형태로 반환한다는 점은 일반적인 (main, aux1, aux2) 순서와 다를 수 있다. 따라서 학습 코드에서 이 반환 순서를 어떻게 소비하는지(예: loss 가중치 0.3 적용)를 맞춰야 한다.
파일 하단의 팩토리 함수들은 Lucid의 모델 레지스트리에 각 Inception 변형을 등록한다.
@register_model
def inception_v1(
num_classes: int = 1000,
use_aux: bool = True,
**kwargs,
) -> Inception:
return Inception_V1(num_classes, use_aux, **kwargs)
즉 Lucid에서 GoogLeNet을 쓰려면 inception_v1()을 호출하면 된다.
GoogLeNet 논문은 "더 깊고 더 넓게" 라는 슬로건을 무작정 밀어붙이는 대신, 희소 구조의 아이디어(상관 기반 클러스터링, Hebbian 원리)과 멀티스케일 처리를 바탕으로, dense 블록 조합으로 희소 구조를 근사하는 Inception 모듈을 제안했다. 이 모듈은 1×1 reduction을 통해 3×3/5×5의 비용을 제어하면서, 병렬 경로의 출력을 concat해 다음 stage가 다양한 스케일의 정보를 동시에 활용할 수 있게 만든다. 실전에서는 보조 분류기(auxiliary classifier) 로 학습 신호를 보강하고, ILSVRC에서는 멀티크롭/앙상블 같은 테스트 기법과 결합해 매우 낮은 top-5 error를 달성했다. Lucid의 구현은 이 논문의 핵심 아이디어를 _InceptionModule(Fig. 2), _InceptionAux(보조 분류기), Inception_V1(Fig. 3의 전체 네트워크)로 직접 대응시키며, 논문에서 GoogLeNet이라 부른 모델을 inception_v1이라는 이름으로 제공한다는 점만 기억하면 논문-코드 연결이 깔끔해진다.
Szegedy, Christian, et al. "Going Deeper with Convolutions." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 2015.