AlexNet 모델 구성

사실 르넷 부터 공부해야되는 것 같긴 하지만 !

alexnet 2

softmax 까지 취한 후에 최종 아웃풋이 나온다.

Alexnet 에선 input 으로 227 * 227 * 3 이미지가 들어갔다. 그리고 Max pooling 의 stride = 2 이므로 filter 가 overlapping 된다.

각 layer 통과 후 feature map 크기

마찬가지로 o = (n - f + 2p / s) + 1 공식을 따른다.

여기서 n 은 각 layer 에 input 으로 들어오는 feature map 의 width를 뜻한다. f 는 filter 개수를 뜻한다.

아침에 일어나서 잠을 깨려고 오랜만에 산수를 조금 해서 점검해보았다.

ㄴㅇㄹㄴㅇㄹ

input 으로 들어간 feature map , 연산 결과 나온 feature map size 가 동일한 층들의 경우 padding 이 들어갔기 때문이다. (padding = ‘same’)

  • padding=’same’ ; 필터의 사이즈가 k 이면 사방으로 k/2 만큼의 패딩을 준다

AlexNet 스펙

분명 어떠한 대회에서 우승했으니까 유명해졌을 것이다. AlexNet 의 스펙을 쳐봤더니 , 2012 년에 ILSVRC image classification 대회에서 당시 오차율 16.4% 로 우승한 모델이라고 한다. 해당 대회에서는 1000개의 이미지를 분류했어야 했다. 그나저나 2012년이면 갤럭시 3 막 쓰고 애니팡 하고 다니던 그 시절이 아닌가 ? 대단하구만

AlexNet 포인트 1 - relu 활성화 함수 사용

저번에 sigmoid 를 활성화 함수로 쓴 linear layer 층들로 쌓인 모델의 오차 역전파가 어떻게 진행되는지 간단하게 공부한 적이 있다. z 는 sigmoid 취하기 전이고, ㅣ은 l 번째 layer 임을 뜻한다고 할 때,

sigmoid 관련된 부분만 보면, 오차역전파 과정에서 sigmoid’ (z[l-1]) 연산이 반복된다 .

backpropagation

sigmoid’(z) = sigmoid(z) * ( 1 - sigmoid (z)) 이다.

sigmoid 는 0 과 1 사이의 값만을 가지므로 한마디로 1보다 작은 분수들이 계속해서 곱해지는 것이다. 당연히 미분값들은 점점 작아지다가 곧 update 를 하지 못할 정도로 소실되어 버릴 것이다.

cnn 에서도 layer 을 깊게 쌓게 되는 경우 동일한 고질적인 문제들이 발생할 수 있다.

반면 relu 활성화 함수의 경우 relu(z) = max (0,z) 이다. 즉 값이 0 보다 클 경우 relu’(z) = 1 , 1x1x1… 계속 연산해도 gradient 가 소실될 우려가 없다.

AlexNet 포인트 2 - dropout 사용

dropout 은 over fitting 을 맞기 위해서 몇 뉴런들은 아예 꺼버리는 것이다. 코드 작성 시 어느 정도의 뉴런들만 사용할지 그 비율을 인자로 넣어줄 수 있다.

haha

AlexNet 포인트 3 - overlapping pooling

가령 4 * 4 feature map 을 stride 2 로 2 * 2 max pooling 한다고 치고, width 만 봐주면

1 2 3 4

이렇게 가로 길이 만큼의 숫자 값이 존재하는데

stride 가 2 이므로 max pooling 은

1 2 에서 한 번, (2,3) 은 stride 2 이므로 생략, 3 4 에서 한 번 이렇게 일어날 것이다.

즉 maxpooling 을 구할 때 overlapping 되지 않는다. = 매번 전혀 다른 숫자 모음집에서 제일 큰 값을 꺼내어온다.

그런데 alexnet 에서는 55 * 55 feature map 에서 stride 2 로 3 * 3 max pooling 을 하기 때문에, width 만 봐주면

1 2 3 4 5 …. 55

1 2 3 에서 한 번, (2,3,4) 는 stride 2 이므로 생략, 3 4 5 에서 한 번 , … 이런 식으로 일어날 것이다.

즉 max pooling 할 때 이전 숫자 모음집에 있던 숫자가 또 다른 max(숫자 3개 모음) 에도 중복되어서 포함된다. = overlapping pooling 이라고 할 수 있다.

overlapping 풀링을 하면 over fitting 이 방지되어서 에러를 줄이면 효과가 있다고 한다.

AlexNet 포인트 4 - local response normalization

Difference between Local Response Normalization and Batch Normalization by Aqeel Anwar Towards Data Science

해당 부분은 위 링크를 통해서 이해에 큰 도움을 받을 수 있었다. batch normalization 이전에 존재하던, (alexnet 에서 사용된) normalization 같은데

batch

공식을 사용한다.

가령 하이퍼 파라미터들을 (k,α, β, n)=(0,1,1,2) 으로 지정하면

맨 처음 파란색 (0,0,0) 의 normalization 의 경우 (-1,0,0) 은 없고, (1,0,0) 은 1이므로 1 / (0+1^2+1^2)^1= 0.5 가 나온다.

bn an

AlexNet 에서 이러한 normalization 을 사용한 이유는 activation 에 relu 함수를 쓰기 때문에 큰 값의 경우 스케일링 되지 않고 그대로 다시 나오기 때문이다. 이대로 max pooling 했다가 다시 들어가고 .. 계속 반복된다면 결국 이 두드러진 큰 feature 값만 살아남을 가능성이 높다.

반면 위 그림에서 normalization 한 결과를 보면 특정 값만 지나치게 두드러지지 않게 바뀌었다.

이 밖에도 data augmentaion 의 기법들도 사용했다고 하는데, 순수히 모델 레이어 구성 관련된 부분들 위주로만 살펴보았다.

AlexNet 코드 구현

데이터 셋 : 캐글에 귀엽고 재밌는 데이터 셋이 올라왔다. binary classification task 인데 해당 사진이 ‘동물의 숲’ 인지 ‘doom’ ? 인지 게임을 구별하는 것이다. mnist 에 질린 사람들은 해보라는 문구에 끌려서 다운받았다.

Doom or Animal Crossing?

목표 1 ) pytorch 에서 pretrained 된 AlexNet 으로 해당 data classification 해보기

목표 2) AlexNet ver 1 직접 만들어보기

pytorch 에서 pretrained 된 AlexNet 으로 해당 data classification 해보기

모듈 import

import torch
import torch.nn as nn
from torchvision import transforms, datasets
import torchvision
from google.colab import drive
import torch.nn.functional as F

코랩에서 돌렸는데 data 가 공간 부족인지 계속 안 올라가서 구글 드라이브를 연동하였다.

구글 드라이브에 ‘img_alex’ 폴더를 만들어주고 ‘animal_crossing’ , ‘doom’ 폴더 각각에 해당하는 데이터들을 넣어주었다.

drive.mount('/content/gdrive')

train / test 분할 (animal crossing vs doom)

from torch.utils.data.dataset import random_split
root='/content/gdrive/My Drive/img_alex/'
transform_img=transforms.Compose([
                                  transforms.Resize((227,227)),
                                  transforms.ToTensor()    #값이 0,1 사이로 변경(standardization)
])
dataset=torchvision.datasets.ImageFolder(
    root=root,
    transform=transform_img
)

print(len(dataset)) 을 해보면 총 1597개가 나온다. 데이터 수가 매우 적으므로 파인튜닝은 필수적이다.

train_dataset, test_dataset = random_split(dataset, [1277,320])
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=20)
test_loader  = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=20)

데이터 확인하기

count=0
for t,s in train_loader:
    if count==0:
        print(t.shape)
        print(s.shape)
        count+=1
    else:
        break

torch.Size([20, 3, 227, 227])

torch.Size([20])

print(len(train_loader))

64

print(len(test_loader))

16

Alexnet (pretrained)

가져오기

alexnet=torchvision.models.alexnet(pretrained=True)

alexnet 의 마지막에 나오는 feature 수를 바꿔주어야한다.

num_ftrs=alexnet.classifier[1].out_features
print(num_ftrs)

4096

alexnet.classifier[-1]=nn.Linear(in_features=num_ftrs,out_features=2)
alexnet

AlexNet( (features): Sequential( (0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2)) (1): ReLU(inplace=True) (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False) (3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2)) (4): ReLU(inplace=True) (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False) (6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (7): ReLU(inplace=True) (8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (9): ReLU(inplace=True) (10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (11): ReLU(inplace=True) (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False) ) (avgpool): AdaptiveAvgPool2d(output_size=(6, 6)) (classifier): Sequential( (0): Dropout(p=0.5, inplace=False) (1): Linear(in_features=9216, out_features=4096, bias=True) (2): ReLU(inplace=True) (3): Dropout(p=0.5, inplace=False) (4): Linear(in_features=4096, out_features=4096, bias=True) (5): ReLU(inplace=True) (6): Linear(in_features=4096, out_features=2, bias=True) ) )

gpu 에 올려준다.

device='cuda'
alexnet.to(device)

optimizer, loss function 정의

optimizer=torch.optim.SGD(alexnet.parameters(),lr=0.001,momentum=0.9)
criterion=nn.CrossEntropyLoss()

tensorboard 에 값 적어줄 준비

from torch.utils.tensorboard import SummaryWriter
writer=SummaryWriter()

train, evaluate 함수 정의

def train(model,train_loader,optimizer,log_interval):
    for batch_idx,(t,l) in enumerate(train_loader):
        t, l = t.to(device), l.to(device)
        model.train()
        optimizer.zero_grad()
        outputs=model(t)
        loss=criterion(outputs,l)
        writer.add_scalar("Loss/train",loss,Epoch)
        loss.backward()
        optimizer.step()
        if batch_idx % log_interval==0 and batch_idx!=0:
          print(batch_idx)
          print("Train Epoch is {}, Train loss is {}".format(Epoch, loss.data))
def evaluate(model,test_loader,optimizer,log_interval):
    val_loss=0
    with torch.no_grad():
          for batch_idx,(t,l) in enumerate(test_loader):
                t, l = t.to(device), l.to(device)
                model.eval()
                output=model(t)
                val_loss+=criterion(output,l).item()
                
    val_loss/=len(test_loader.dataset)
    writer.add_scalar("Loss/valid",val_loss,Epoch)
    return val_loss

학습 돌려주기

for Epoch in range(1,11):
    train(alexnet,train_loader,optimizer,log_interval=1)
    vali_loss=evaluate(alexnet,test_loader,optimizer,log_interval=1)
    print("Val loss is {}".format(vali_loss))

tensorboard 에서 그래프를 확인해보았다.

alex_net

매 에폭 당 validation 차이가 너무 미미하긴 하지만, overfitting 이 일어난 것 같다. early stopping 파이토치에서 적용하는 법을 서서히 알아보아야겠다.

val_loss 가 가장 낮았을 때 값은 0.012793236458674074 가 나왔고

train_loss 가 가장 낮았을 때 값은 0.04112085700035095 가 나왔다

AlexNet ver 1 직접 만들어보기

pretrained 된 모델 구성은 내가 공부한 맨 날 것 구성과 조금 다른 것을 보니까 이후 버전으로 학습된 것 같다. 나는 AlexNet 맨 처음 논문에 있는 구성 그대로 만들어보았다.

사실 이것도 돌려보고 싶었는데, 그러기엔 데이터 수가 너무 적었고, imagenet data 를 다운받아서 쓰기에는 다운받는 로딩 시간 기다리는게 싫었다.

그래서 돌려보지는 못했다.

class AlexNet(nn.module):
    def __init__(self,num_features):
        super(AlexNet,self).__init__()
        self.l1=nn.Conv2d(3,96,11,4)
        self.m1=nn.MaxPool2d(3,stride=2)
        self.l2=nn.Conv2d(96,256,5,1)
        self.m2=nn.MaxPool2d(3,stride=2)
        self.l3=nn.Conv2d(256,384,3,1,padding=1)
        self.l4=nn.Conv2d(384,384,3,1,padding=1)
        self.l5=nn.Conv2d(384,256,3,1.padding=1)
        self.m3=nn.MaxPool2d(3,stride=2)
        self.l6=nn.Flatten()
        self.l7=nn.Linear(256,4096)
        self.l8=nn.Dropout(0.5)
        self.l9=nn.Linear(4096,4096)
        self.l10=nn.Dropout(0.5)
        self.l9=nn.Linear(4096,1000)
        self.l10=nn.Linear(1000,num_features)
    
    def forward(self,x):
        x=self.l1(x)
        x=F.relu(x)
        x=self.m1(x)
        x=self.l2(x)
        x=F.relu(x)
        x=self.m2(x)
        x=self.l3(x)
        x=F.relu(x)
        x=self.l4(x)
        x=F.relu(x)
        x=self.l5(x)
        x=F.relu(x)
        x=self.m3(x)
        x=self.l6(x)
        x=self.l7(x)
        x=F.relu(x)
        x=self.l8(x)
        x=self.l9(x)
        x=F.relu(x)
        x=self.l10(x)
        x=F.softmax(x)

F.relu(self.l1(x)) 이런 식으로 써줬으면 훨씬 깔끔하고 짧아졌을 것 같다. 😅

pytorch 에서 햇갈렸던 것들 정리

이 참에 현재를 기준으로 pytorch 에서 햇갈리는 것들을 정리해보았다.

  • .to(device) 사용의 필요성

=> It is necessary to have both the model, and the data on the same device, either CPU or GPU, for the model to process data. Data on CPU and model on GPU, or vice-versa, will result in a Runtime error.

You can set a variable device to cuda if it’s available, else it will be set to cpu, and then transfer data and model to device :

why to device

  • optimizer.zero_grad() 의 역할

optimizer.zero_grad() 가 필요한 이유

  • torch.no_grad() 의 역할

with torch.no_grad():로 코드 블럭을 감싸서 autograd가 .requires_grad=True인 Tensor들의 연산 기록을 추적하는 것을 멈출 수 있다.

print(x.requires_grad)
print((x**2).requires_grad)

with torch.no_grad():
    print((x**2).requires_grad)
True
True
False


출처: https://deepinsight.tistory.com/84 [Steve-Lee's Deep Insight]

출처: https://deepinsight.tistory.com/84 [Steve-Lee’s Deep Insight]