본문 바로가기

Programming Project/Pytorch Tutorials

Pytorch 머신러닝 튜토리얼 강의 11 (Advanced CNN)


투명한 기부를 하고싶다면 이 링크로 와보세요! 🥰 (클릭!)

바이낸스(₿) 수수료 평생 20% 할인받는 링크로 가입하기! 🔥 (클릭!)

2018/07/02 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 1 (Overview)

2018/07/02 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 2 (Linear Model)

2018/07/03 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 3 (Gradient Descent)

2018/07/03 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 4 (Back-propagation and Autograd)

2018/07/07 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 5 (Linear Regression in the PyTorch way)

2018/07/09 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 6 (Logistic Regression)

2018/07/10 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 7 (Wide and Deep)

2018/07/11 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 8 (PyTorch DataLoader)

2018/07/11 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 9 (Softmax Classifier)

2018/07/13 - [Programming Project/Pytorch Tutorials] - Pytorch 머신러닝 튜토리얼 강의 10 (Basic CNN)




이제 좀더 심화된 CNN에 대해서 공부해보도록 합시다.


우리는 Conv FIlter등을 이용해서 이전 이미지 혹은 activation map으로부터 새로운 속성을 뽑아냈습니다.


하지만 어떤 filter을 쓸 것인지 정하는 것은 이것 저것 해보면서 어떤게 좋은지 경험 해 보는 수 밖에 없었습니다.


우리의 선조들은 ㅋㅋ! 이런 저런 것들을 시도 해 보시다가 그 중 괜찮은걸 하나 쓰고는 하셨답니다.





그던 와중, 특이점이 오게 되는데.. 바로 쓰까라 쓰까 입니다.







Inception Modules



Inception Module은 여러 필터의 결과를 합치는데요


이걸 보시므는 풀링도 하고, 1x1conv도 하고 3x3 conv 5x5conv등등


이 많은 친구들을 모두 써서 나온 결과들을 모두 합쳐주고 있습니다.


이렇게 입맛대로 한 데이터에 대해서 많은 필터의 종류를 먹인 후 쭉 이어붙이는(Concatenate) 것입니다.


강의에 나온 그림이 살짝 애매한 것 같아서, Andrew Ng 아조씨의 Cosera 강의에서 스샷 하나를 따왔습니다.




( Inception modules에 대한 좋은 강의이니 들으셔도 좋을 것 같습니다

https://www.coursera.org/lecture/convolutional-neural-networks/inception-network-piR0x )


저 그림의 해석을 조금 디테일하게 하자면


맨 윗줄은 1*1 Conv layer을 그냥 이전 Activation map에서 가져와서 28*28*64의 output activation map을 만들고


그 아랫줄은 이전 Activation map에서, 1x1 conv layer을 통해서 28*28*192 체널을 28*28*96으로 줄여주고, 28 * 28 * 96 에서 3*3 Conv 필터를 이용해 패딩을 적절히 줘서 28*28*128 로 내보내고..


등등으로 해석하시면 됩니다.


(결과가 다르면 그냥 쓰까서 붙일 때는 불가능하기 때문에 output을 28*28로 유지시켜주려고 패딩을 줘서 같은 모양으로 맞추는 것 같습니다.)





자 그럼 포괄적인 개념을 이해했다면, 이제 조금 더 세부적인 내용으로 들어가 봅시다.


저기에 보면 특이한 부분이 있습니다. 바로 1x1 Conv layer인데요



1x1 conv layer은 어떤 '범위의 영역'을 훑어본다는 의미는 없지만, 주된 의미는 그 '체널'을 줄이거나 늘릴 수 있다는 데에 있습니다.



일반적인 체널에서 그대로 쓰는 것 보다, 체널을 줄이고 쓴다면 그 다음 layer의 파라미터를 줄일 수 있다는 장점이 있으면서도, conv layer에서 의미있는 필터끼리 한번 다시 조합 해 주기 때문에 한 레이어에서 조금 더 깊은 논리를 처리할 수 있다는 장점이 있습니다.


예를 들어, 28*28의 192체널로부터 5*5 conv layer 32체널을 만든다면


5*5*192*32의 파라미터가 만들어지겠지만, 


1x1로 32체널로 줄인다면 줄이는 데에는 1*1*192*32의 파라미터가,


conv layer엔 5*5*32*32의 파라미터가 필요합니다.


둘을 합해도 확실히 많이 줄었죠.


또한 연산량으로 본다면 


기존에는 5*5*28*28*192*32 개의 연산이 필요 한 상황이라면


이제는 1*1*28*28*192*32 + 28*28*5*5*32*32 개의 연산이 필요하죠.



음.. 여튼 확실히 꽤나 줄어든다는 것을 알 수 있습니다.


이러한 이유로 1*1 Conv layer로 체널을 줄인다 라는 것을 알 수 있습니다.





그럼 이 복잡한 모델은 코드로 어떻게 짤 수 있을까요?



일단 우리가 짤 모델을 이렇게 정의 해 봅시다.


conv layer 아래에 있는 괄호는 filter size입니다.


그리고 맨 아래 filter concat 가 input , 맨 위의 filter concat가 output이 되겠죠


자 그럼 이제 각 레이어를 어떻게 실제로 구현하는지 봅시다.



각각의 레이어를 다음과 같이 구현할 수 있습니다.

avg_pool2d는 average pooling해 주는 layer이고


그 외에 Conv2d는 이미 사용 해본 적 있는 함수이므로 아실것이라고 생각합니다.


윗 줄은 선언부분이고, 아랫 부분은 forward부분입니다.



(사진은 클릭하면 확대가 됩니다)



그럼 마지막으로 이 모든 결과값을 concatenation 하면 됩니다.  (이어붙이면 되어요)



그렇습니다.


이렇게 구현하면 됩니다.



자 그럼 이 Inception Module 단위로 거대한 네트워크를 구성하면 어떻게 될까요?



저런 빨간 원들이 각각 하나의 인셉션 모듈이고, 저 모듈을 이어붙일 수 있지 않을까요?


그러기 위해서는 


1. 모듈을 프로그래밍 한다.


2. 모듈을 이용해서 전체 네트워크를 프로그래밍 한다.


이렇게 두 가지 절차를 통해서 가능합니다.


그럼 한번 프로그래밍을 시작 해 봅시다.





Code


일단 우리가 구현 할 모델은, 

inception module 은 위에서 정의한대로 만들고,

input을 받아서 

1. 5*5 Conv layer 10채널 통과시킨 후 Max pool 2*2 (Stride = 2)

2. inception module 통과시키기

3. 5*5 Conv layer 20체널 통과시킨 후 Max pool 2*2 (Stride = 2)

4. inception module 통과시키기 

5. Fully-Connected Layer 통과시키기

6. Softmax해서 내보내기


+ ) Softmax 해서 내보내기 때문에 Cross Entropy Loss가 아닌 NLL Loss를 쓰게 됩니다. 그리고 optimizer 은 SGD optimizer을 써봅시다.



우선 import부터 해줍시다.


import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms


그리고 device setup , batch_size정하기, dataset 불러오기를 진행합시다.


device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
batch_size = 64

train_dataset = datasets.MNIST(root='./mnist_data/',train = True, transform = transforms.ToTensor(),download = True)
test_dataset  = datasets.MNIST(root='./mnist_data/',train = False, transform = transforms.ToTensor())

train_loader = torch.utils.data.DataLoader(dataset=train_dataset,batch_size=batch_size,shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,batch_size=batch_size,shuffle=False)

지난글에 다 나왔던 부분이라 추가적인 설명은 하지 않도록 하겠습니다.



자 이제 인셉션 모듈을 한번 구현 해 보도록 하겠습니다.


class InceptionModule(nn.Module):
    
    def __init__(self,in_channels):
        super(InceptionModule,self).__init__()
        self.branch1x1 = nn.Conv2d(in_channels,16,kernel_size=1)
        
        self.branch5x5_1 = nn.Conv2d(in_channels,16,kernel_size=1)
        self.branch5x5_2 = nn.Conv2d(16,24,kernel_size=5,padding=2)
        
        self.branch3x3_1 = nn.Conv2d(in_channels,16,kernel_size=1)
        self.branch3x3_2 = nn.Conv2d(16,24,kernel_size=3,padding=1)
        self.branch3x3_3 = nn.Conv2d(24,24,kernel_size=3,padding=1)
        
        self.branch_pool = nn.Conv2d(in_channels,24,kernel_size=1)
        
    def forward(self,x):
        branch1x1 = self.branch1x1(x)
        
        branch5x5 = self.branch5x5_1(x)
        branch5x5 = self.branch5x5_2(branch5x5)
        
        branch3x3 = self.branch3x3_1(x)
        branch3x3 = self.branch3x3_2(branch3x3)
        branch3x3 = self.branch3x3_3(branch3x3)
        
        branch_pool = F.avg_pool2d(x,kernel_size=3,stride=1,padding=1)
        branch_pool = self.branch_pool(branch_pool)
        
        outputs = [branch1x1,branch5x5,branch3x3,branch_pool]
        
        #torch.cat은 
        #x =tensor([[ 0.6580, -1.0969, -0.4614],
        #          [-0.1034, -0.5790,  0.1497]]) 일 때, 
        # torch.cat((x,x,x),0) -> (6, 3) 행렬
        # torch.cat((x,x,x),1) -> (2, 9) 행렬로 합쳐진다
        # 우리의 output은 ( n*n , 16) , ( n*n , 24) . ( n*n , 24)  ( n*n , 24) 를 
        # ( n*n , 88)로 합쳐야 하므로, outputs,1이 된다.
        return torch.cat(outputs,1)

우선 __init__부분에서 그림에서 보았던 친구들을 모두 선언 해 주었습니다.


그리고 branch를 하나의 x에 대해서 4개로 쪼개서 각각 forward 시켜주었습니다.


마지막에 그 모든 결과를 합쳐주는 것은 조금 복잡해서 주석으로 달아놓았습니다.



그럼 이제 전체 모델을 짜보도록 해요.


class MainNet(nn.Module):
    
    def __init__(self):
        super(MainNet,self).__init__()
        
        self.conv1 = nn.Conv2d(1,10,kernel_size=5)
        self.conv2 = nn.Conv2d(88,20,kernel_size=5)
        
        self.incept1 = InceptionModule(in_channels=10)
        self.incept2 = InceptionModule(in_channels=20)
        
        self.max_pool = nn.MaxPool2d(2)
        self.fc = nn.Linear(1408,10)
        
    def forward(self,x):
        in_size = x.size(0) # size(0)을 하면 (n, 28*28)중 n을 리턴함
        x = F.relu(self.max_pool(self.conv1(x)))
        x = self.incept1(x)
        x = F.relu(self.max_pool(self.conv2(x)))
        x = self.incept2(x)
        x = x.view(in_size,-1)
        x = self.fc(x)
        return F.log_softmax(x)

아까 말한대로 선언을 하고 쭉 돌렸어요.


Conv2 가 88 체널을 받는건 위 인셉션 모듈의 아웃풋이라 그렇고 (아까 계산했죠?)


Linear이 1408을 인풋으로 받는건... 넘나 복잡해서 그냥 한번 런타임 에러 내고 찾아내도록 하죠 ^_^7 


이번엔 저번과 다르게 log_softmax까지 해서 뽑아줍니다.



자 그럼 늘 하던대로, 모델과 (gpu로 옮기고) 옵티마이저, loss함수 정의를 해주고여


model = MainNet().to(device)

optimizer = optim.SGD(model.parameters(),lr=0.01,momentum=0.5)
criterion = nn.NLLLoss()

(우리 모델이 Softmax를 이미 해서 뽑아주기 때문에 NLL Loss를 써야해요~)


Train하는 함수랑 Test하는 함수를 따로 짜구요!


def train(epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data = data.to(device)
        target = target.to(device)
        
        output = model(data)

        optimizer.zero_grad()
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % 50 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.data[0]))
            
            

def test():
    model.eval()
    test_loss = 0
    correct = 0
    for data, target in test_loader:
        data = data.to(device)
        target = target.to(device)
        
        output = model(data)
        # sum up batch loss
        test_loss += criterion(output, target).data[0]
        # get the index of the max log-probability
        pred = output.data.max(1, keepdim=True)[1]
        correct += pred.eq(target.data.view_as(pred)).cpu().sum()

    test_loss /= len(test_loader.dataset)/batch_size
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))

돌려보도록 합시다~


for epoch in range(1, 10):
    train(epoch)
    test()

저는 


Test set: Average loss: 0.0536, Accuracy: 9822/10000 (98%)

까지 나오네여 ㅋㅋ


여튼 여기까지 완료하셨으면


숙제로, 한번 여기에서 더 깊은 레이어를 쌓아 보도록 합시다.


모듈을 두번쯤 더 이어붙이던가 하는 방법을 통해서 말이죠.



하지만. 계속 깊어만 진다고 과연 모델의 성능이 계속 올라갈까요?



그렇지는 않습니다.




cifar - 10 데이터를 분석하는 모델을 만들 때 


56layer을 쌓는 것 보다 20layer을 쌓았을 때가 error가 더 낮게 나왔다고 합니다.


layer을 겹겹히 쌓는 것에는 근본적인 몇 문제가 있습니다.


Vanishing gradients problem이 발생합니다.


Back propagation을 하는 동안 gradient가 점점 줄어들거나 섞여버려서 맨 끝으로부터 학습된 정보가 첫 레이어에 올 때 쯤에는 별 의미없는 값이 되어버리는 것입니다.


네트워크의 깊이를 늘렸을 때, 정확도가 어느 수준에서 머무르다가 그 후 조금씩 감소하기 시작합니다.


이를 해결하기 위한 Solution으로 , Deep Residual Neural Network가 고안되었습니다.




Deep Residual Neural Network





기존의 네트워크는 단순히 forward로 weight layer을 보내는 것인 반면에, ResNet은 Residual Connection 을 남겨주어 이전 input x를 그대로 layer의 결과와 합쳐줍니다.


이렇게 되면, 맨 끝으로부터 흘러들어오는 Loss에 대한 Gradient가 직접적으로 다른 weight를 거치지 않고도 최상위까지 흘러갈 수 있는 길이 생기기 때문에, 훨씬 효과적으로 Model 에 대한 학습이 이루어 집니다.




이렇게 깊은 레이어에 대해서 아래에서 위로 바로 gradient가 흘러가니까 학습 효과가 짱짱이다. 라고 할 수 있겠습니다.








Deep Residual Neural Network 관련해 더 자세한 설명을 해 놓은 글을 찾아 링크 첨부합니다


http://incredible.ai/artificial-intelligence/2017/10/28/Deep-Residual-Neural-Network/







그럼 숙제로 한번 


ResNet을 구현해보도록 하구요.


이전에서 그냥 한번에 가져오는게 아니라 여러 이전 state에서 가져오는


DenseNet 또한 한번 구현 해 보도록 합시다.