AlexNet(ImageNet Classification with Deep Convolutional Neural Networks)논문 요약
Abstract
ImageNet LSVRC-2010 대회에서 120만개의 고해상도 이미지, 1000개의 라벨을 분류하는 CNN 모델을 만들었다. 그 결과 테스트 데이터에서 기존의 SOTA보다 뛰어난 top1, top5 에러율을 37.5%, 17.0%로 기록했다. 훈련 속도를 높이기 위해 non-staurating 뉴런과 GPU를 사용했고, 드롭아웃을 통해 과대적합을 방지했다. 이를 통해 ILSVRC-2012대회에서 15.3%의 top-5 테스트 에러율을 기록하며 2위인 26.2%보다 좋은 성능을 가진 모델을 만들었다.
Details
도입부
- CNN을 통해 기존 대회의 기록보다 높은 점수를 기록했다.
- 2D 합성곱에 GPU를 사용한 연산이 가능하다는 것을 보여줬다.
- 과대적합을 막기 위한 다양한 기술(드롭 아웃, 데이터 증강 등)을 사용했다.
- 합성곱의 깊이가 매우 중요하다는 것을 깨달았다.
- ILSVRC 데이터: 학습 120만개, 검증 5만개, 테스트 15만개, 클래스 1000개
- 평가지표로 사용된 top-5 에러율은 모델이 예측한 상위 5가지의 결과 중 정답이 없는 비율로 낮을수록 좋은 성능
- Ex) 하나의 이미지가 바나나 0.4, 배 0.2, 사과 0.15, 포도 0.1, 수박 0.05로 top 5를 추렸고, 정답이 사과라면 top 5 안에 사과가 있으니까 에러가 발생한 것이 아님
- 만약, 정답이 오렌지라면 top 5안에 오렌지가 없으니까 에러 발생
- top-5 error = top 5 error 발생횟수 / 테스트 데이터 수
- 이미지를 256 x 256 사이즈로 줄였고, 각 픽셀값에서 평균을 뺀 전처리 외에는 특별한 전처리를 수행하지 않았다.
모델 구조와 활용 기술
1. ReLU Nonlinearity & GPU
하이퍼볼릭 탄젠트나 시그모이드 함수를 활성화 함수로 사용하여 발생할 수 있는 기울기 소실문제를 해결하기 위해 ReLU를 사용했다. ReLU를 사용하게 됨으로써 기울기 소실 문제를 해결하고 학습 시간을 크게 단축시킬 수 있었고 에러도 CIFAR-10데이터셋에서 아래 그래프의 실선과 같이 6 에포크만에 25% 에러율을 달성했다. 점선은 하이퍼볼릭 탄젠트를 활성화 함수로 사용했을 때의 에러이다.
또한, 2개의 GPU를 사용하여 각 GPU에 특정 layer의 절반의 뉴런들만을 할당시켰다. 예를 들어, layer3의 입력을 layer2에서 받을 때 layer2의 정보를 하나의 GPU에서 전부 다 받는 것이 아닌 2개의 GPU에 절반씩 나누어서 전달하는 방식이다. 이를 통해, 에러가 줄어들었을 뿐만 아니라 연산 속도가 증가하여 학습 속도를 크게 감소시켰다.
2. Local Response Nomalization & Overlapping Pooling
위에서 설명한대로 활성화 함수로 ReLU를 사용하게됨으로써 기울기 소실과 학습 속도 문제를 해소했지만 활성화 함수를 통과한 층의 출력값이 매우 크다는 문제가 존재했다. 어떤 layers에 여러개의 필터가 존재하는데 만약 특정 필터만 활성화값이 매우 크다면(특성맵의 수치가 큼)다른 특성맵의 의견은 크게 반영하지 않은채 하나의 필터에 의해 가중치가 좌지우지 될 것이다. LRN은 이를 해결하기 위해 크게 활성화된 뉴런을 다른 특성 맵에 있는 같은 위치의 뉴런을 활용해서 억제시키는 정규화 방법이다. Local Response Nomalization은 지역적인 영향을 줄이고 정규화를 통해 일반화 능력을 향상시키는데 도움이 되었다. 모든 layer가 아닌 특정 layer에만 적용되었고 ReLU를 거친 후의 결과값에 사용했다. 해당 정규화 기법을 통해 top-1 에러는 1.4%, top-5 에러는 1.2% 감소시켰다. 하지만, 최근에는 Batch Normalization의 등장으로 잘 사용하지 않는 방법이다.
또한, 아래의 이미지처럼 기존에는 인접한 픽셀들이 중복되지 않게 Pooling 했다면 AlexNet에서는 Pooling 시에 픽셀들이 중복되어(Overlapping Pooling) 특성맵을 구성할 수 있도록 했다. 이 방법을 통해 top-1 에러 0.4%, top-5 에러 0.3%를 감소시켰다.
출처 https://bskyvision.com/421
3. Overall Architecture
모델의 전체 구조는 위와 같다. Convolution Network 5개, Fully Connetected Network 3개로 총 8개의 층으로 이루어져있고, 가장 마지막에 있는 Fully Connected Network는 1000개의 클래스를 softmax함수를 사용하여 예측할 수 있도록 했다. 2,4,5번째의 합성곱 층에서는 이전 층의 결과를 각각 다른 GPU를 통해 전달받도록 구현되었고, 1,2번째 층에서는 위에서 설명한 Local Response Nomalization과 Max Pooling이, 5번째 층에서는 별다른 정규화 없이 Max Pooling만이 적용된다. 활성화 함수인 ReLU는 모든 Layer에 적용되었다.
각 Layer마다의 출력 크기를 계산하기 위해서는 아래와 같은 특성맵의 크기를 구하는 식을 사용할 수 있다.
- I: 입력 이미지의 크기
- K: 커널의 크기
- P: 패딩의 크기
- S: stride 크기
첫번째층
논문에 표시된 224x224 크기의 입력 이미지는 227x227로 수정되었고, 첫번째 층의 출력값의 갯수는 다음과 같이 표현할 수 있다.
55x55의 사이즈를 갖는 특성맵에 stride를 2, 커널의 크기를 3으로 하는 Max Pooling을 적용하면 최종적으로
27x27x96 크기에 해당하는 출력값을 갖게 된다.
- 입력: 227 x 227 x 3
- 커널: 11 x 11 x 3, 채널 갯수 96(48 x 2), stride 4, padding 0
- 합성곱: 55 x 55 x 96
- ReLU
- Local Response Nomalization
- Max Pooling: 27 x 27 x 96, stride 2, 커널 3
두번째층
- 입력: 27 x 27 x 96
- 커널: 5 x 5 x 48, 채널 갯수 256(128 x 2), stride 1, padding 2
- 합성곱: 27 x 27 x 256
- ReLU
- Local Response Nomalization
- Max Pooling: 13 x 13 x 256, stride 2, 커널 3
세번째층
- 입력: 13 x 13 x 256 x 2
- 커널: 3 x 3 x 256, 채널 갯수 384(192 x 2), stride 1, padding 1
- 합성곱: 13 x 13 x 384
- ReLU
네번째층
- 입력: 13 x 13 x 384
- 커널: 3 x 3 x 192, 채널 갯수 384(192 x 2), stride 1, padding 1
- 합성곱: 13 x 13 x 384
- ReLU
다섯번째층
- 입력: 13 x 13 x 384
- 커널: 3 x 3 x 192, 채널 갯수 256(128 x 2), stride 1, padding 1
- 합성곱: 13 x 13 x 256
- ReLU
- Max Pooling: 6 x 6 x 256, stride 2, 커널 3
여섯,일곱번째층
- 입력: 6 x 6 x 256
- ReLU
- 출력: 4096
- 입력: 4096
- ReLU
- 출력: 4096
8번째층
- 입력: 4096
- Softmax
- 출력: 1000
4. 과대적합 방지
이미지 증강
- 학습 시에는 기존 이미지를 특정 크기(224 x 224)의 패치로 랜덤하게 잘라서 데이터셋의 크기를 늘렸다. 256 x 256 이미지에 좌우반전과 넓이, 높이 각 변당 랜덤하게 32번의 224 x 224 패치로 잘라 최종적으로는 2048(32 x 32 x 2)배 많은 데이터셋을 입력으로 사용할 수 있다.
- 테스트셋은 기존 이미지에서 좌상단, 우상단, 좌하단, 우하단, 중앙을 각각 자르고 좌우반전을 시킨 이미지에도 크롭을 똑같이 적용하여 총 10개의 이미지의 예측값을 평균내어 최종 output을 만들었다.
- 훈련데이터의 RGB 값에 PCA를 수행해서 평균이 0이고, 표준 편차가 0.1의 가우시안값에 비례하는 크기의 랜덤 변수와 원래의 픽셀 값을 곱하여 색상에 변형을 준다. 라벨의 변화없이 색상만 변화시킬 수 있는 방법이다.
Dropout
- 각 층에 있는 모든 뉴런을 사용한다면 학습 시간이 매우 오래 걸릴 것이므로 특정 층에서 50%에 해당하는 뉴런들만 랜덤하게 활성화시켜 가중치들이 너무 훈련 데이터에만 fit하지 않도록했다.
5. 하이퍼파라미터 & 결과
- 학습시에는 batch_size = 128, momentum = 0.9, weight_decay = 0.0005로 설정하고 SGD를 optimizer로 사용했다. 이후 수동으로 학습률과 같은 파라미터들을 조정하면서 성능을 향상시켰다.
- ILSVRC-2010 데이터셋에서는 아래와 같이 기존보다 top-1, top-5 에러율이 크게 감소했다. 다른 데이터셋에서도 다른 모델들보다 훨씬 좋은 성능을 보였다.
개인적인 생각
- 2D 합성곱층과 GPU를 연결하여 학습 시간을 크게 단축시켰다는 점에서 굉장히 의미있었던 연구였던 것 같다.
- Local Response Nomalization을 사용하여 ReLU를 사용했을 때 발생할 수 있는 문제를 해결했다는 점이 인상깊었다.
- 합성곱층의 깊이를 매우 조금 수정했을 때 성능이 작지 않게 하락했는데 이 깊이라는 수치가 어떤 의미를 갖는지 궁금증이 생겼다. 오히려 더 깊이를 늘렸을 때의 학습 속도와 성능 변화는 어떻게 될 것인가?다른 논문에서 이를 해소했지 않을까?
- AlexNet에서는 과대적합을 해소하기 위해 이미지 증강, Dropout 기법을 활용했는데 이외에도 최근에 나온 모델들은 어떤 시도들을 했는지 궁금증이 생겼다.
구현
pytorch로 AlexNet을 구현해보자(참고).
import torch
import torch.nn as nn
class AlexNet(nn.Module):
# 모델 구조
def __init__(self, num_classes = 1000):
super(AlexNet, self).__init__() # nn.Module 클래스 정보 상속
# 합성곱층
self.net = nn.Sequential(
# 첫번째층
nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4), # output: 55 x 55 x 96
nn.ReLU(),
nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=2), # 논문에 구현된 파라미터. ReLU이후 정규화
nn.MaxPool2d(kernel_size=3, stride=2), # output: 27 x 27 x 96
# 두번째층
nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2), # output: 27 x 27 x 256
nn.ReLU(),
nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=2),
nn.MaxPool2d(kernel_size=3, stride=2), # output: 13 x 13 x 256
# 세번째층
nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1), # output: 13 x 13 x 384
nn.ReLU(),
# 네번째층
nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1), # output: 13 x 13 x 384
nn.ReLU(),
# 다섯번째층
nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1), # output: 13 x 13 x 256
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2) # output: 6 x 6 x 256
)
# 완전 결합층. 분류기
self.classifier = nn.Sequential(
# 여섯번째층
nn.Dropout(p=0.5, inplace=True), # 해당 층에 50%의 뉴련만 활성화. inplace = True -> Dropout 결과를 기존 input 값에 반영. 메모리 절약
nn.Linear(in_features=(256 * 6 * 6), out_features=4096), # 다차원의 입력을 1차원으로 Flatten. input shape은 tensor 형식
nn.ReLU(),
#일곱번째층
nn.Dropout(p=0.5, inplace=True),
nn.Linear(in_features=4096, out_features=4096),
nn.ReLU(),
#여덟번째층
nn.Linear(in_features=4096, out_features=num_classes)
)
self.init_bias() # 가중치, 편향 초기화
def init_bias(self): # 가중치, 편향 초기화 함수
for layer in self.net:
if isinstance(layer, nn.Conv2d): # 합성곱층에 대해
nn.init.normal_(layer.weight, mean=0, std=0.01) # 평균0, 표준편차 0.01로 가중치 초기화
nn.init.constant_(layer.bias, 0) # 편향 초기화
# 합성곱 2,4,5 층 편향은 1로 초기화
nn.init.constant_(self.net[4].bias, 1)
nn.init.constant_(self.net[10].bias, 1)
nn.init.constant_(self.net[12].bias, 1)
def forward(self, x): # 순전파. Layer 결합.
x = self.net(x)
x = x.view(-1 ,256 * 6 * 6) # Linear한 분류기에 넣기위해 차원 Flatten
return self.classifier(x)