안녕하세요. 지난 포스팅의 [IC2D] Very Deep Convolutional Networks for Large-Scale Image Recognition (ICLR2015)에서는 ILSVRC 2014에서 2등을 차지한 VGGNet에 대해서 리뷰를 해보았습니다. 오늘은 ILSVRC 2014에서 1등을 차지한 GooLeNet에 대해서 리뷰를 해보도록 하겠습니다. 대부분의 사람들한테는 VGGNet이 좀 더 인기가 많은 편 입니다. 아무래도 GooLeNet을 설계하는 과정에서 filter size를 아주 다양하게 적용하였는데 이 부분이 조금 난해하기 때문이라고 생각이 드네요. 오늘은 GooLeNet의 동기와 전체 네트워크 구조, 그리고 실험 결과와 함께 실제로 구현한 결과를 말씀드리도록 하겠습니다.
ImageNet-1K의 등장은 이후에 딥러닝의 패러다임을 바꾸는 계기가 되었습니다. LeNet까지만 해도 MNIST와 같이 소규모 데이터만 사용할 수 밖에 없는 상황이였죠. 하지만, ImageNet-1K는 1.2M장에 1,000개의 클래스를 가지는 데이터셋으로 기존의 데이터셋보다는 훨씬 대규모 데이터셋으로 등장하였습니다. 이로 인해 AlexNet을 시작으로 다양한 심층학습 기반을 모델이 등장하였습니다. 이 대표적인 예가 VGGNet과 GooLeNet이죠. VGGNet과 GooLeNet 동일하게 강조한 것은 Efficiency입니다. 인간보다 높은 인지능력을 보유하는 것만으로도 중요하지만 주어진 네트워크를 더 적은 파라미터와 컴퓨터 자원을 이용해서 충분히 높은 성능을 이끌어내는 것도 중요한 요소입니다.
당장 예만 들어서 AlexNet과 GooLeNet의 파라미터는 약 12배가 차이가 납니다. 하지만, 그 성능은 GooLeNet이 훨씬 뛰어나죠. 대체 어떤 요소가 더 적은 파라미터를 가진 GooLeNet이 AlexNet보다 더 높은 성능을 가지게 만들었을까요?
단순하게만 생각해보면 모델의 성능은 깊이 (Depth)와 너비 (Width)가 클수록 성능이 좋아질 수 밖에 없습니다. 여기서, 깊이는 전체 합성곱 블럭의 개수, 너비는 1개의 합성곱 블럭에서 특징 맵의 개수를 의미하죠. 하지만, 깊이와 너비 모두 모델의 파라미터를 증가시키고 복잡도를 증가시킵니다. 이는 데이터가 적은 경우, 수많은 파라미터를 학습시키지 못하고 과적합 (overfitting)에 빠지게 할수도 있습니다. 따라서, GooLeNet은 이 부분에 집중하여 1개의 합성곱 블럭에서 다양한 filter size를 가지는 모듈이 병렬적으로 새로운 특징 맵을 만들어내게 함으로써 달성합니다.
이와 같은 모듈을 앞으로 Inception Module이라고 부르도록 하겠습니다. 이제 GooLeNet이 어떤 생각을 가지고 전체적으로 구성하였는지 이해는 하였지만 한 가지 문제는 (a)와 같이 네트워크를 구성하게 되면 $5 \times 5$ 합성곱 계층에서 고차원 특징맵을 계산할 때는 계산 복잡도가 꽤 높다는 점 입니다. 이를 완화하고자 $3 \times 3$ 합성곱 계층과 $5 \times 5$ 합성곱 계층을 통과하기 전에 $1 \times 1$ 합성곱 계층을 추가해줌으로써 차원을 축소시키는 효과를 얻을 수 있습니다. 또한 이는 고차원의 공간에서 중요한 특징 맵들을 선별해주는 역할을 한다는 점에서 깊을 수록 높은 효과가 날 것이라고 본 논문에서는 예상하고 있습니다.
왼쪽의 표는 GooLeNet의 입력 및 출력 특징맵의 개수를 명시해주고 있습니다. 오른쪽 그림은 이를 도식화 한 것 이죠.
본 논문에서는 ImageNet에서 실험을 진행하였습니다. VGGNet과 비교했을 때 약 0.7% 정도 성능이 향상되었네요.
제 깃허브에서 전체 학습 코드 및 GooLeNet의 학습코드를 확인할 수 있습니다.
import torch
import torch.nn as nn
class InceptionModule(nn.Module):
def __init__(self, in_channels, out_channels_list):
super(InceptionModule, self).__init__()
self.branch1 = nn.Sequential(nn.Conv2d(in_channels, out_channels_list[0], kernel_size=(1, 1), stride=(1, 1), padding=(0, 0)),
nn.BatchNorm2d(out_channels_list[0]), nn.ReLU(inplace=True))
self.branch2 = nn.Sequential(nn.Conv2d(in_channels, out_channels_list[1], kernel_size=(1, 1), stride=(1, 1), padding=(0, 0)),
nn.BatchNorm2d(out_channels_list[1]), nn.ReLU(inplace=True),
nn.Conv2d(out_channels_list[1], out_channels_list[2], kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
nn.BatchNorm2d(out_channels_list[2]), nn.ReLU(inplace=True))
self.branch3 = nn.Sequential(nn.Conv2d(in_channels, out_channels_list[3], kernel_size=(1, 1), stride=(1, 1), padding=(0, 0)),
nn.BatchNorm2d(out_channels_list[3]), nn.ReLU(inplace=True),
nn.Conv2d(out_channels_list[3], out_channels_list[4], kernel_size=(5, 5), stride=(1, 1), padding=(2, 2)),
nn.BatchNorm2d(out_channels_list[4]), nn.ReLU(inplace=True))
self.branch4 = nn.Sequential(nn.MaxPool2d(kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
nn.Conv2d(in_channels, out_channels_list[5], kernel_size=(1, 1), stride=(1, 1), padding=(0, 0)),
nn.BatchNorm2d(out_channels_list[5]), nn.ReLU(inplace=True))
def forward(self, x):
branch1 = self.branch1(x)
branch2 = self.branch2(x)
branch3 = self.branch3(x)
branch4 = self.branch4(x)
out = torch.cat([branch1, branch2, branch3, branch4], dim=1)
return out
class InceptionAux(nn.Module):
def __init__(self, in_channels, num_classes):
super(InceptionAux, self).__init__()
self.conv = nn.Sequential(
nn.AvgPool2d(kernel_size=5, stride=3),
nn.Conv2d(in_channels, 128, kernel_size=(1, 1), stride=(1, 1), padding=(0, 0)),
nn.BatchNorm2d(128), nn.ReLU(inplace=True))
self.fc = nn.Sequential(
nn.Linear(2048, 1024),
nn.ReLU(),
nn.Dropout(),
nn.Linear(1024, num_classes),
)
def forward(self,x):
x = self.conv(x)
x = x.view(x.shape[0], -1)
x = self.fc(x)
return x
class GoogLeNet(nn.Module):
def __init__(self, image_size=224, num_channels=3, num_classes=1000):
super(GoogLeNet, self).__init__()
self.num_classes = num_classes
self.stem_conv = nn.Sequential(nn.Conv2d(num_channels, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3)),
nn.BatchNorm2d(64), nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=(3, 3), stride=(2, 2), padding=(1, 1)),
nn.Conv2d(64, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
nn.BatchNorm2d(192), nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=(3, 3), stride=(2, 2), padding=(1, 1)))
self.inception_branch_a3 = InceptionModule(in_channels=192, out_channels_list=[64, 96, 128, 16, 32, 32])
self.inception_branch_b3 = InceptionModule(in_channels=256, out_channels_list=[128, 128, 192, 32, 96, 64])
self.maxpool3 = nn.MaxPool2d(kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
self.inception_branch_a4 = InceptionModule(in_channels=480, out_channels_list=[192, 96, 208, 16, 48, 64])
self.inception_branch_b4 = InceptionModule(in_channels=512, out_channels_list=[160, 112, 224, 24, 64, 64])
self.inception_branch_c4 = InceptionModule(in_channels=512, out_channels_list=[128, 128, 256, 24, 64, 64])
self.inception_branch_d4 = InceptionModule(in_channels=512, out_channels_list=[112, 114, 288, 32, 64, 64])
self.inception_branch_e4 = InceptionModule(in_channels=528, out_channels_list=[256, 160, 320, 32, 128, 128])
self.maxpool4 = nn.MaxPool2d(kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
self.inception_branch_a5 = InceptionModule(in_channels=832, out_channels_list=[256, 160, 320, 32, 128, 128])
self.inception_branch_b5 = InceptionModule(in_channels=832, out_channels_list=[384, 192, 384, 48, 128, 128])
self.avgpool5 = nn.AvgPool2d(7, 1)
self.fc = nn.Linear(1024, self.num_classes)
self.aux_fc1 = InceptionAux(512, self.num_classes)
self.aux_fc2 = InceptionAux(528, self.num_classes)
def forward(self, x):
stem_out = self.stem_conv(x)
out3 = self.inception_branch_a3(stem_out)
out3 = self.inception_branch_b3(out3)
out3 = self.maxpool3(out3)
out4 = self.inception_branch_a4(out3)
aux_out1 = self.aux_fc1(out4)
out4 = self.inception_branch_b4(out4)
out4 = self.inception_branch_c4(out4)
out4 = self.inception_branch_d4(out4)
aux_out2 = self.aux_fc2(out4)
out4 = self.inception_branch_e4(out4)
out4 = self.maxpool4(out4)
out5 = self.inception_branch_a5(out4)
out5 = self.inception_branch_b5(out5)
out5 = self.avgpool5(out5)
out5 = out5.view(out5.shape[0], -1)
out5 = self.fc(out5)
return out5, aux_out1, aux_out2
'논문 함께 읽기 > 2D Image Classification (IC2D)' 카테고리의 다른 글
[IC2D] Deep Networks with Stochastic Depth (ECCV2016) (0) | 2023.05.03 |
---|---|
[IC2D] Identity Mappings in Deep Residual Networks (ECCV2016) (0) | 2023.04.21 |
[IC2D] Deep Residual Learning for Image Recognition (CVPR2016) (0) | 2023.04.12 |
[IC2D] Very Deep Convolutional Networks for Large-Scale Image Recognition (ICLR2015) (0) | 2023.02.17 |
2D Image Classification Summary (0) | 2023.02.17 |