논문 제목 : Very Deep Convolutional Networks For Large-Scale Image Recognition
논문 링크 : https://arxiv.org/pdf/1409.1556.pdf
이번에 리뷰할 논문은 VGGNet이다.
원래 AlexNet-VGGNet-GoogLeNet.. 등의 순으로 소개했어야 하는데
아무래도 구조가 간단해서 미루고 미루다보니 이제 한다.
사실 구조만 보면 단순하기도 하고, ILSVRC 2등이라 크게 매력적이지 않을 수 있다.
하지만 단순하기에 여러 model들의 BackBone으로 자주 쓰이고,
당시 1등인 GoogLeNet보다 훨씬 간단한데 어떻게 저런 좋은 성적을 냈는지 살펴봐야 한다.
그래서 이 논문을 리뷰하게 되었다.
간략하게 핵심 위주로 살펴보자.
이미지 인식 성능에 Convolutional network의 깊이가 얼마나 영향을 미치는지 조사했다.
3x3 size의 작은 convolutional filter를 사용해 16-19까지 깊이를 늘렸고,
이 덕분에 ILSVRC의 classification 분야에서 2등을 차지했다.
Large public image repository가 사용 가능해지고, GPU같이 높은 컴퓨팅 시스템의 등장으로 이미지 인식 분야에서 다양한 성공을 이뤄냈다.
AlexNet(2012) 이후로 accuracy를 높이기 위한 다양한 시도가 이루어졌다.
First convolutional layer를 바꾸고나, training과 testing을 바꾸는 등 여러 방법이 제시되었지만,
이 논문에서는 깊이에 집중할 것이다.
3x3 size의 작은 filter 덕분에 convolutional layer의 깊이를 차근차근 늘렸고, parameter를 조정했다.
그 덕분에 accuracy가 크게 증가했다.
ConvNet의 깊이 증가로 인한 성능 향상을 비교하기 위해,
모든 ConvNet layer 세팅은 같은 원칙으로 설정되었다.
Table 1, ConvNet 구조이다.
A-E는 깊이만 다르다.
깊이는 11(8 conv, 3 FC)~19(16 conv, 3 FC)이다.
너비(channel 수)는 max-pooling layer 뒤에서 2배로 커지며, 64부터 시작해서 512까지 커진다.
Table 2, parameter 수이다.
더 얕은 기존의 net과 비교하여, 깊이가 더 깊어졌음에도 weight 수는 더 커지지 않았다.
(vs 144M weights in (Sermanet et al., 2014))
기존의 ILSVRC-2012, 2013에서 top-performing 하던 구조들과 이 ConvNet은 꽤 다르다.
큰 receptive field(filter, 11x11/4 or 7x7/2)를 첫 layer에 쓰는 대신,
3x3/1 의 작은 receptive field를 전체 net에 적용했다.
3x3 conv layer가 2개면 5x5의 효과를, 3개면 7x7의 효과를 낸다.
이에 대한 장점은 2개가 있다.
즉 이러한 상황에서 7x7 conv layer에 대해 정규화를 도입해 3x3으로 decomposition(분해)했다고 볼 수 있다.
1x1 conv layer는 크기는 변하지 않게 하면서 input, output channel 수를 동일하게 설정한다.
이는 같은 차원에 linear projection을 하며, conv에 딸려오는 ReLU가 또 비선형성을 추가한다.
GoogLeNet에서는 1x1 conv layer를 dimension change(parameter 줄이기)의 목적으로 사용했는데, 여기서는 linear projection과 ReLU를 쓰기 위해 사용한다.
학습 과정은 AlexNet의 방법을 대체로 따른다(단 input crop의 sampling만 빼고).
학습은 370K의 반복(74 epoch)에서 멈췄다.
AlexNet보다 더 적은 epoch에서 멈춘 것은
(a) 깊은 깊이와 작은 conv filter size로 인한 정규화로 더 쉽게 수렴함
(b) 각 layer의 pre-initialization
이 2개가 원인일 것으로 추정한다.
네트워크의 pre-initialization은 중요하다.
Table 1의 Configuration A에는 얕아서 random initialization으로 학습되기에 충분했다.
그리고 더 깊은 구조는 A의 layer weight로 첫 4개의 conv layer, 3개의 FC layer를 초기화했다(중간은 random).
Pre-initialization된 layer도 learning rate를 줄이지 않아 바뀔 수 있게 했다.
Random initialization은 평균 0, 분산 의 정규분포이며 편향은 0이다.
가중치를 pre-training없이 random하게 할 수 있던건 Xavier Glorot의 논문에 나온 방법을 쓴 덕이다.
224x224의 input 이미지 크기를 고정하기 위해, training 이미지를 SGD 반복 당 이미지 당 한 번씩 random하게 crop하여 사용했다.
Augmentation을 위해 random horizontal flipping과 random RGB color shift를 했다.
S를 isotropically-rescaled된 image의 가장 짧은 side라 하자.
(isotropically-rescaled란 기존 이미지의 비율을 유지하며 rescaled된 것)
근데 crop size가 224x224니까 S는 224보다 커야한다.
즉 S=224라면 이미지 전체를 crop하고(짧은 쪽은 다 포함됨), 224보다 크다면 잘라낼 것이다.
기존 image와 같은 비율로 rescale하고, 224x224로 crop한다.
세 가지 방법으로 S를 실험했다.
빠른 학습을 위해 S=384를 고정하고 pre-train했다.
Test할 때, ConvNet과 input image는 아래의 과정을 거친다.
Fully-convolutional network가 전체 이미지에 적용되므로, test에는 컴퓨팅적으로 비효율적인 multi-crop을 할 필요가 없다.
(그치만 crop을 많이하면 성능이 올라간다 by Szegedy et.al(2014))
Multi-crop 평가는 dense 평가와 상호보완적이다(conv의 경계 조건때문에).
(dense 평가는 위에서 class 수에 맞게 channel을 만들고 score를 계산하는 것이다.
multi-crop에 비해 연산량이 훨씬 줄어든다!)
: ConvNet을 crop에 적용하면, feature-map은 zero padding된다.
반면 dense 평가는 같은 crop에 대해 옆 이미지를 쓰기 때문에 결과적으로 receptive field를 크게 키워주고 더 많은 context를 잡을 수 있다.
multi-crop : 이미지를 "처음"부터 자르기에, 0으로 padding함.
dense : conv을 통해 class만큼 channel만 만들면 됨, 여기서 conv할 때 multi-crop이 강제적으로 0으로 padding한 것과 다르게 convolution 연산은 한 픽셀의 주위값을 받아오기에 receptive field를 높여주는 효과를 줌)
C++ Caffe toolbox를 사용했지만 몇 가지 수정을 거쳐 full-size 이미지를 다양한 scale에 대해 train했다.
Multiple-GPU로 데이터를 병렬적으로 학습했다.
각 GPU에서 gradient를 병렬적으로 계산한 후, 평균내어 전체 batch의 gradient를 구했다.
4-GPU는 GPU 1개보다 약 3.75배 빠르다.
ILSVRC 대회에서 쓰이는 dataset으로 classification 결과를 얻었다.
즉 scale jittering을 통한 training set augmentation은 single scale로 test할 때 성능 향상에 도움이 된다.
Test에 single scale 대신 scale jittering으로 multi-scale test를 해보자.
Test 이미지를 rescale한 후, network를 통과시켜 나온 결과를 average했다.
Training, test 이미지의 scale 차이가 크면 오히려 성능이 떨어지므로, 비슷하게 설정했다.
Table 3는 test를 single scale로 한 결과이며, Table 4는 test를 scale jitter해서 사용했다.
앞서 single scale에서도 깊이가 깊고(D, E), training을 scale jitter한 것이 fixed S보다 성능이 좋았다.
Talbe 4를 보면 알 수 있듯이, 여기서도 D, E, train with scale jitter가 성능이 좋다.
Validation set에 대해 top-1/top-5 error가 각각 24.8%/7.5% (공교롭게도 D, E가 성능이 완전 똑같다)이다.
E의 경우 test set에서 7.3%의 top-5 error를 달성했다.
앞서 test할 때 network를 fully-convolutional로 바꾸었으므로 multi-crop도 적용 가능하다.
Dense(그냥 이미지 통째로 사용)와 multi-crop(한 이미지에 대해 crop 여러 번)을 비교해보자.
Multi-crop이 dense보다 약간 더 낫다.
여러 model을 soft-max class posterior를 평균내는 방식으로 합쳤다.
ILSVRC 대회 기간에는 single-scale network와 multi scale model D만 훈련했다.
D는 fine-tunning을 모든 layer 말고 FC layer만 하는 방식이었다.
7개의 network ensemble은 7.3%의 ILSVRC test error를 보여줬다.
대회 후, 두개의 best-performing model(D, E)만 합치는 방법이 생각나서 실행했다.
Dense evaluation으로는 7.0%, dense와 multi-crop을 같이 쓴 경우 6.8%의 test error를 얻었다.
참고로 single model의 최고 성능은 7.1%의 test error이다(model E, Table 5).
SOTA 결과물들과 비교해보자.
Table 7을 보면, ILSVRC-2014에서 7.3%의 test error로 2등을 수상했다.
또 대회 이후 2개의 net을 ensemble해 6.8%의 test error를 만들었다.
이는 ILSVRC-2012, 2013의 우승자를 훨씬 압도하는 결과이다.
2013년에 1등을 한 GoogLeNet(6.7%)과 거의 비슷한 결과이다(GoogLeNet은 7 net을 ensemble했다!).
단일 model로는 1등이다(7.0%, GoogLeNet보다 0.9% 앞섬).
주목할만한 점은, 구조는 LeCun이 제안한 ConvNet에서 크게 벗어나지 않고 깊이를 늘려 달성했다는 점이다.
이 논문에서 저자들은 19 weight layers라는 매우 깊은 convolutional network(당시..^^)를 만들었다.
Representation 깊이가 classification accuracy 향상에 도움되고, SOTA 성능을 달성할 수 있었다.
(Network의 표현력 깊이이므로 layer 수를 말하는듯)
이 연구는 다시 한 번 representation 깊이의 중요성을 확인시켜준다.
이 논문의 핵심을 요약해보자.
이번 코드는 거의 100% 스스로 짠 첫 코드이다.
FC layer 부분이 약간 오류가 나서 그것만 좀 찾아봤다.
뿌듯쓰 ^_^
여태 더 복잡한 구조를 짜다보니 이번 것은 간단했다.
class BasicConv2d(nn.Module):
def __init__(self, in_channels, out_channels, **kwargs):
super(BasicConv2d, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, bias=False, stride = 1, padding="same", **kwargs),
nn.ReLU()
)
def forward(self, x):
return self.conv(x)
먼저 기본 Conv2d를 정의한다.
여기서 filter size는 3이고, stride는 1이며, padding=1(=same)으로 한다했으니 그대로 적용한다.
모든 hidden layer는 ReLU가 있다고 하였으므로 ReLU 역시 적용해준다.
class VGGNet(nn.Module):
def __init__(self, nblocks, num_classes=10, init_weights=True):
# nblocks = number of conv3, 4, 5 layer
# if nblocks = 3 -> VGGNet16, if nblocks=4 -> VGGNet19
super(VGGNet, self).__init__()
# conv1
self.conv1 = nn.Sequential(
BasicConv2d(3, 64),
BasicConv2d(64, 64),
nn.MaxPool2d(kernel_size = 2, stride = 2)
)
# conv2
self.conv2 = nn.Sequential(
BasicConv2d(64, 128),
BasicConv2d(128, 128),
nn.MaxPool2d(kernel_size = 2, stride = 2)
)
# conv3
layers = []
layers.append(BasicConv2d(128, 256))
for i in range(nblocks-1):
layers.append(BasicConv2d(256, 256))
layers.append(nn.MaxPool2d(kernel_size = 2, stride = 2))
self.conv3 = nn.Sequential(*layers)
# conv4
layers = []
layers.append(BasicConv2d(256, 512))
for i in range(nblocks-1):
layers.append(BasicConv2d(512, 512))
layers.append(nn.MaxPool2d(kernel_size = 2, stride = 2))
self.conv4 = nn.Sequential(*layers)
# conv5, number of channels don't change
layers = []
for i in range(nblocks):
layers.append(BasicConv2d(512, 512))
layers.append(nn.MaxPool2d(kernel_size = 2, stride = 2))
self.conv5 = nn.Sequential(*layers)
# classifier
self.fc = nn.Sequential(
# since 224x224 -> 7x7, 512x7x7 must be input
# paper does not use average pooling, so I didn't use adaptive average pooling
nn.Linear(512*7*7, 4096),
nn.Dropout(p=0.5),
nn.ReLU(),
nn.Linear(4096, 4096),
nn.Dropout(p=0.5),
nn.ReLU(),
nn.Linear(4096, num_classes)
)
if init_weights:
self._initialize_weights()
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.conv5(x)
x = nn.Flatten()(x)
x = self.fc(x)
return x
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
# according to paper, random initialization = (mean=0, variance=10^-2)
nn.init.normal_(m.weight, mean=0.0, std=0.1)
if m.bias is not None:
# if bias exist, bias = 0
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
VGGNet 본체이다.
VGGNet D, E만 구현했는데 여기서 차이는 저번에 여러 모델을 구현할 때처럼 nblocks를 활용했다.
Conv3, 4, 5를 보면 VGG-16은 3개, VGG-19는 4개이다.
따라서 이 숫자로 for문을 돌려 layer를 만들어줬다.
마지막에 MaxPool(kernel=2, stride=2) 넣어주는 것을 잊지 말아야 한다.
Conv3, 4는 channel수가 바뀌기에 이것도 유의해야 한다.
Classifier 부분은 앞에 FC layer 2개에 Dropout(0.5)가 적용된다.
모든 hidden layer에 ReLU를 쓰므로 여기서도 사용했다.
가중치 초기화는 논문에서 나온대로 평균 0, 분산 0.01(=표준편차 0.1)인 정규분포로 초기화했다.
다른 논문은 보통 global average pooling을 사용해서 nn.AdaptiveAvgPool2d
를 쓰면 되지만,
여기서는 따로 없어서 nn.Linear(512*7*7, 4096)
이라는 무지막지한 크기의 linear layer를 만들었다.
그래서 단순한 구조에 비해 model 크기가 상당히 크다.
참고로 동시대에 출품한 GoogLeNet이
이정도니..
모델이 커서 한 epoch만 학습했다.
이 당시 initialization 기술이 제대로 없어 첫 accuracy가 처참하다..
논문에서도 74 epoch나 학습했다 했으니..
Test accuracy도 처참하다..ㅋㅋ
학습하면서 epoch마다 출력하긴 하는데 대체 현재 어디쯤인지 궁금할때가 있다.
그래서 구글링하며 코드를 찾아본 결과 이렇게 하면 된다.
print(f"\rcurrent batch: {batch_idx} \t Total batch: {len(train_loader)}", end="")
\r
과 end=""
의 조합을 하면, 맨 처음부터 쓰는 효과가 있다.
참고로 \n
이 없으니 epoch 출력할 때는
print(f"\n[*] Epoch: {epoch} \tTrain accuracy: {correct/count} \tTrain Loss: {train_loss/count}")
이렇게 맨 앞에 \n
을 넣어줘야 한다.
전체 코드는 github에 있습니다~~.
https://github.com/Parkyosep/Papers/tree/main/VGGNet
Paper
[1] https://deep-learning-study.tistory.com/398
[2] https://wikidocs.net/118514
Code
[3] https://deep-learning-study.tistory.com/521