GoogLeNet (Inception v1)GoogLeNet (Inception v1)
2014, Google (Szegedy et al.) · ILSVRC 2014 분류 부문 우승 (Top-5 error 6.67%)
1. 왜 나왔나? — Problem
2012년 AlexNet 이후 CNN 연구의 흐름은 “네트워크를 더 깊고 넓게 만들면 성능이 오른다” 였다. 하지만 이 방식에는 분명한 한계가 있었다.
- 연산량과 파라미터의 폭발: 층과 채널을 늘리면 연산량이 제곱 단위로 늘어난다. 모델이 커질수록 학습·추론 비용이 감당하기 어려워진다.
- 과적합: 파라미터가 많을수록 데이터가 충분하지 않으면 과적합되기 쉽다. 특히 마지막 Fully Connected 층에 파라미터가 몰려 있었다.
- 필터 크기 선택의 딜레마: 이미지 속 객체의 크기는 제각각이다. 작은 필터(3×3)는 세밀한 특징을, 큰 필터(5×5)는 넓은 문맥을 잘 잡는데, 층마다 어떤 크기를 쓸지 사람이 정해야 했다.
- 기울기 소실: 깊어질수록 앞쪽 층까지 gradient가 잘 전달되지 않는다.
즉, “깊게 쌓고 싶은데 비용은 감당할 수 있게” 라는 문제를 풀어야 했다.
2. 다른 모델과 뭐가 다른가?
| AlexNet (2012) | VGG-16 (2014) | GoogLeNet (2014) | |
|---|---|---|---|
| 깊이 | 8층 | 16층 | 22층 |
| 파라미터 수 | 약 6,000만 | 약 1억 3,800만 | 약 500만 |
| 설계 철학 | CNN의 가능성 증명 | 3×3 conv를 단순하게 깊이 쌓기 | 여러 크기 필터를 병렬로, 효율적으로 |
| 네트워크 형태 | 한 줄로 쌓음 | 한 줄로 쌓음 | 층 안에서 가지가 갈라졌다 합쳐짐 |
| 마지막 분류부 | 큰 FC 층 3개 | 큰 FC 층 3개 | Global Average Pooling + FC 1개 |
VGG가 “단순함”으로 깊이를 얻었다면, GoogLeNet은 “구조 설계”로 깊이와 효율을 동시에 얻었다. VGG보다 깊으면서도 파라미터는 약 1/27이다.
3. 그래서 어떻게 풀었나? — Solve
① Inception 모듈 → 필터 크기 딜레마 해결
한 층에서 필터 크기를 하나만 고르지 않고, 1×1 / 3×3 / 5×5 conv와 3×3 max pooling을 병렬로 수행한 뒤 채널 방향으로 이어 붙인다. 어떤 스케일의 정보가 중요한지는 네트워크가 학습으로 알아서 조합한다. 이 모듈을 9개 쌓아 네트워크 몸통을 만든다.
┌─ 1×1 conv ─────────────────┐
├─ 1×1 conv → 3×3 conv ──────┤
입력 ───────┤ ├─ Concat → 출력
├─ 1×1 conv → 5×5 conv ──────┤
└─ 3×3 maxpool → 1×1 conv ───┘
② 1×1 Convolution 병목 → 연산량 폭발 해결
병렬로 여러 필터를 돌리면 연산량이 커지므로, 3×3·5×5 연산 앞에 1×1 conv를 두어 채널 수를 먼저 줄인다.
| 입력 28×28×192 → 5×5 conv로 32채널 출력 | 곱셈 연산량 |
|---|---|
| 5×5 conv 바로 적용 | 약 1억 2,000만 회 |
| 1×1 conv로 16채널 축소 → 5×5 conv | 약 1,240만 회 |
약 10배 절감된다. 1×1 conv 뒤에도 ReLU가 붙어 비선형성이 추가되는 효과도 있다.
③ Global Average Pooling → 파라미터·과적합 해결
마지막 feature map을 채널별로 평균 내어 1×1×1024 벡터로 만든다. 거대한 FC 층을 없애서 파라미터를 크게 줄이고 과적합도 완화했다.
④ 보조 분류기(Auxiliary Classifier) → 기울기 소실 완화
깊은 신경망은 마지막 층에서 계산한 오차가 앞쪽 층까지 전달되는 과정에서 학습 신호가 약해질 수 있다. 이를 완화하기 위해 중간 층에 작은 분류기를 붙여 추가 loss를 계산한다.
전체 loss = 메인 loss + 0.3 × (보조 loss1 + 보조 loss2)
추론할 때는 떼어낸다. (이후 Batch Normalization이 등장하면서 필요성은 줄었다.)
4. 코드로 어떻게 활용하나?
① Inception 모듈 직접 구현 (PyTorch)
import torch
import torch.nn as nn
class Inception(nn.Module):
def __init__(self, in_ch, c1, c3_red, c3, c5_red, c5, pool_proj):
super().__init__()
# 1×1
self.b1 = nn.Sequential(nn.Conv2d(in_ch, c1, 1), nn.ReLU(inplace=True))
# 1×1 병목 → 3×3
self.b2 = nn.Sequential(
nn.Conv2d(in_ch, c3_red, 1), nn.ReLU(inplace=True),
nn.Conv2d(c3_red, c3, 3, padding=1), nn.ReLU(inplace=True))
# 1×1 병목 → 5×5
self.b3 = nn.Sequential(
nn.Conv2d(in_ch, c5_red, 1), nn.ReLU(inplace=True),
nn.Conv2d(c5_red, c5, 5, padding=2), nn.ReLU(inplace=True))
# 3×3 maxpool → 1×1
self.b4 = nn.Sequential(
nn.MaxPool2d(3, stride=1, padding=1),
nn.Conv2d(in_ch, pool_proj, 1), nn.ReLU(inplace=True))
def forward(self, x):
# 네 갈래의 출력을 채널 방향(dim=1)으로 이어 붙임
return torch.cat([self.b1(x), self.b2(x), self.b3(x), self.b4(x)], dim=1)
# 논문의 inception(3a) 설정
m = Inception(192, 64, 96, 128, 16, 32, 32)
x = torch.randn(1, 192, 28, 28)
print(m(x).shape) # torch.Size([1, 256, 28, 28]) ← 64+128+32+32
padding으로 공간 크기(28×28)를 맞춰두었기 때문에 채널 방향으로 concat이 가능하다.
② 사전학습 모델로 전이학습 (실무에서 가장 흔한 방식)
import torch.nn as nn
from torchvision import models
from torchvision.models import GoogLeNet_Weights
weights = GoogLeNet_Weights.DEFAULT
model = models.googlenet(weights=weights) # ImageNet 사전학습, 보조 분류기는 제거된 상태
preprocess = weights.transforms() # 학습 때와 같은 전처리
# 백본 고정 (데이터가 적을 때)
for p in model.parameters():
p.requires_grad = False
# 마지막 분류기만 내 클래스 수에 맞게 교체
model.fc = nn.Linear(model.fc.in_features, 10) # in_features = 1024
③ 처음부터 학습할 때: 보조 분류기 loss 활용
model = models.googlenet(weights=None, aux_logits=True,
num_classes=10, init_weights=True)
criterion = nn.CrossEntropyLoss()
model.train()
out = model(images) # GoogLeNetOutputs(logits, aux_logits2, aux_logits1)
loss = criterion(out.logits, labels) \
+ 0.3 * (criterion(out.aux_logits1, labels) + criterion(out.aux_logits2, labels))
model.eval()
pred = model(images) # 추론 시에는 메인 출력만 반환
한 줄 정리
GoogLeNet은 여러 크기의 필터를 병렬로 쓰는 Inception 모듈과 1×1 conv 병목으로, 깊이는 늘리면서 연산량과 파라미터는 줄인 모델이다. 이 병목 설계는 이후 ResNet의 bottleneck block, MobileNet 같은 경량 모델로 이어진다.
2014, Google (Szegedy et al.) · Winner of ILSVRC 2014 classification (Top-5 error 6.67%)
1. Why did it appear? — Problem
After AlexNet in 2012, the trend in CNN research was “make the network deeper and wider and performance goes up”. But that approach had clear limits.
- Exploding computation and parameters: adding layers and channels grows computation quadratically. The bigger the model, the harder training and inference costs are to bear.
- Overfitting: the more parameters, the easier it is to overfit without enough data. Parameters were especially concentrated in the final fully connected layers.
- The filter-size dilemma: objects in images come in all sizes. Small filters (3×3) capture fine details and large filters (5×5) capture wider context, but someone had to decide which size each layer would use.
- Vanishing gradients: the deeper the network, the worse the gradient reaches the early layers.
In short, the problem was “we want to stack deeply, but at a cost we can afford”.
2. How is it different from other models?
| AlexNet (2012) | VGG-16 (2014) | GoogLeNet (2014) | |
|---|---|---|---|
| Depth | 8 layers | 16 layers | 22 layers |
| Parameters | ~60M | ~138M | ~5M |
| Design philosophy | Prove what CNNs can do | Stack 3×3 convs simply and deeply | Several filter sizes in parallel, efficiently |
| Network shape | Stacked in a single line | Stacked in a single line | Branches split and merge inside a layer |
| Final classifier | Three large FC layers | Three large FC layers | Global Average Pooling + one FC |
If VGG gained depth through “simplicity”, GoogLeNet gained depth and efficiency at the same time through “architecture design”. It is deeper than VGG yet has about 1/27 of the parameters.
3. So how did it solve it? — Solve
① Inception module → solving the filter-size dilemma
Instead of picking one filter size per layer, it runs 1×1 / 3×3 / 5×5 convolutions and 3×3 max pooling in parallel, then concatenates them along the channel axis. The network learns on its own how to combine whichever scales matter. Nine of these modules are stacked to form the body of the network.
┌─ 1×1 conv ─────────────────┐
├─ 1×1 conv → 3×3 conv ──────┤
입력 ───────┤ ├─ Concat → 출력
├─ 1×1 conv → 5×5 conv ──────┤
└─ 3×3 maxpool → 1×1 conv ───┘
② 1×1 convolution bottleneck → solving the computation explosion
Running several filters in parallel increases computation, so a 1×1 conv is placed before the 3×3 and 5×5 operations to reduce the number of channels first.
| Input 28×28×192 → 5×5 conv producing 32 channels | Multiplications |
|---|---|
| Apply the 5×5 conv directly | ~120 million |
| Reduce to 16 channels with a 1×1 conv → 5×5 conv | ~12.4 million |
That is about a 10× reduction. A ReLU also follows the 1×1 conv, which adds extra non-linearity.
③ Global Average Pooling → solving parameters and overfitting
It averages the last feature map per channel into a 1×1×1024 vector. Removing the huge FC layers greatly reduced the parameters and also eased overfitting.
④ Auxiliary classifiers → easing vanishing gradients
In a deep network, the learning signal can weaken as the error computed at the last layer travels back to the early layers. To ease this, small classifiers are attached to intermediate layers to compute additional losses.
전체 loss = 메인 loss + 0.3 × (보조 loss1 + 보조 loss2)
They are removed at inference time. (Once Batch Normalization appeared, the need for them decreased.)
4. How do you use it in code?
① Implementing the Inception module yourself (PyTorch)
import torch
import torch.nn as nn
class Inception(nn.Module):
def __init__(self, in_ch, c1, c3_red, c3, c5_red, c5, pool_proj):
super().__init__()
# 1×1
self.b1 = nn.Sequential(nn.Conv2d(in_ch, c1, 1), nn.ReLU(inplace=True))
# 1×1 병목 → 3×3
self.b2 = nn.Sequential(
nn.Conv2d(in_ch, c3_red, 1), nn.ReLU(inplace=True),
nn.Conv2d(c3_red, c3, 3, padding=1), nn.ReLU(inplace=True))
# 1×1 병목 → 5×5
self.b3 = nn.Sequential(
nn.Conv2d(in_ch, c5_red, 1), nn.ReLU(inplace=True),
nn.Conv2d(c5_red, c5, 5, padding=2), nn.ReLU(inplace=True))
# 3×3 maxpool → 1×1
self.b4 = nn.Sequential(
nn.MaxPool2d(3, stride=1, padding=1),
nn.Conv2d(in_ch, pool_proj, 1), nn.ReLU(inplace=True))
def forward(self, x):
# 네 갈래의 출력을 채널 방향(dim=1)으로 이어 붙임
return torch.cat([self.b1(x), self.b2(x), self.b3(x), self.b4(x)], dim=1)
# 논문의 inception(3a) 설정
m = Inception(192, 64, 96, 128, 16, 32, 32)
x = torch.randn(1, 192, 28, 28)
print(m(x).shape) # torch.Size([1, 256, 28, 28]) ← 64+128+32+32
Because padding keeps the spatial size (28×28) the same, the outputs can be concatenated along the channel axis.
② Transfer learning with a pretrained model (the most common approach in practice)
import torch.nn as nn
from torchvision import models
from torchvision.models import GoogLeNet_Weights
weights = GoogLeNet_Weights.DEFAULT
model = models.googlenet(weights=weights) # ImageNet 사전학습, 보조 분류기는 제거된 상태
preprocess = weights.transforms() # 학습 때와 같은 전처리
# 백본 고정 (데이터가 적을 때)
for p in model.parameters():
p.requires_grad = False
# 마지막 분류기만 내 클래스 수에 맞게 교체
model.fc = nn.Linear(model.fc.in_features, 10) # in_features = 1024
③ When training from scratch: using the auxiliary classifier losses
model = models.googlenet(weights=None, aux_logits=True,
num_classes=10, init_weights=True)
criterion = nn.CrossEntropyLoss()
model.train()
out = model(images) # GoogLeNetOutputs(logits, aux_logits2, aux_logits1)
loss = criterion(out.logits, labels) \
+ 0.3 * (criterion(out.aux_logits1, labels) + criterion(out.aux_logits2, labels))
model.eval()
pred = model(images) # 추론 시에는 메인 출력만 반환
One-line summary
GoogLeNet uses Inception modules that run several filter sizes in parallel and 1×1 conv bottlenecks to increase depth while reducing computation and parameters. This bottleneck design carried on into ResNet’s bottleneck block and lightweight models like MobileNet.