ResNet - Deep Residual Learning for Image Recognition - Preview

Deep Residual Learning for Image Recognition (Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun 저) Preview


는 바야흐로 2015년, VGG Net과 GoogLeNet(Inception v1)의 활약 이후 CNN세상은 춘추 전국시대를 맞고 있었다. VGGNet이 던져놓은 화두는 "깊은 망이 학습이 잘 되지만, 너무 깊으면 안되더라" 라는 내용이었다. 이때 혜성 같이 등장한 딥-러닝의 기린아, ResNet이 등장하게 된다.


ResNet은, 기존 모델들에서 망이 너무 깊으면 학습이 되지 않던 문제를 "Vanishing-Gradient" 이슈 때문으로 해석하였다.





여기서 잠깐! Vanishing-Gradient가 무엇일까?

역전파 과정에서, 뒤로 갈수록 오류의 기울기(Gradient)가 줄어드는 현상이 발생한다.


왜 일까?


(출처 : https://medium.com/machine-learning-world/how-to-debug-neural-networks-manual-dc2a200f10f2)


위 그림은, 다양한 Activation function들의 생긴 모양이다.

보면, Sigmoid의 경우, 일정 수치 이상의 x값에서는 1로 일정이하의 값 에서는 0 으로 값이 고정된다.

따라서, 누적되는 기울기 값들이 점점 흐려지게 된다.


그래서, 이를 해결하고자 나온 것이, ReLU이다. ReLU는 0부터는 일정 기울기로 값이 증가하게 된다.
f(x) = max(0,x)

하지만 역시, 값이 감소하는 상황에서는 0으로 고정 되므로, 기울기가 가려지게 된다.

(그렇다고, 기울기가 계속 일정하면, 딥러닝의 핵심인 Non-linearity가 깨어지게 됨)


그래서 다시 나온것이 Leaky ReLU이다.

이는 0 이하일때에도 일정량의 기울기를 유지해 준다.


하지만, 역시 과하게 깊은 경우에는 기울기가 잘 전달되지 않는 문제점이 남아있다.



ResNet은 이 문제를 해결하기 위해 독특한 방식을 제안하였다. 바로, 일정 노드 마다 앞의 결과를 뒤에 붙여 버려서, Gradient가 전달될 수 있는 지름길을 만들어 놓는 것 이다.



이를 통해 Gradient가 "더 빠르게/잘" 전달되게 되었다.

여기서, "더 빠르게" 도 상당히 집중해 볼 부분이다. 실제 논문에서도, ResNet 을 14층 쌓았을 때와, 일반 CNN을 14층 쌓았을 때, 최종 Accuracy의 차이는 없었지만, 더 빠르게 수렴하였음을 이야기 하고 있다. (Inception Net에서도 ResNet이 꼭 필요한가에 대해선 회의적이었지만, 수렴 속도가 빨라지는 것은 동의하였으며, Inception v4 가 제안된 논문에서 이를 적용한 Inception-ResNet 이라는 모델을 제안하였다.)


그리고, 맨 위 그림에서 처럼, 기존에는 일정 이상의 깊이의 모델이 학습이 되지 않는 현상이 발생하였지만(Over-fitting은 아님, Training set 에서도 오히려 Error가 높기 때문), 깊은 모델에서도 그런 문제가 생기지 않는것을 확인하였다.



결과적으로, 대회에 출품하는 버전에서는 152개의 레이어를 쌓은 모델을 사용하여, 3%의 오차로 우승을 거머쥐었다.


이 글을 통해, ResNet의 컨셉이 잘 이해되었길 바란다.


간단히 살펴보는 ResNet은 이만 마치며,

다음 편, ResNet 논문 리뷰 편 에서 다시 돌아오겠다.



댓글()

오버워치 PPT 템플릿 블로그가 되어버렸다.

Diary|2018. 11. 16. 13:00

오버워치 PPT 템플릿 블로그가 되어버렸다.

뭐랄까... 손님을 끌기 위해 서비스로 빙수를 내놓았는데, 정신 차려보니, 가게가 빙수집이 되어버린 느낌이다.




원래는, 개발 블로그이고 싶었는데 개발 글은 올리지도 않았고...

연구실 일상 글은 나름 연재 중인데, 생각보다 어그로가 안끌린다.


역시 요즘 핫한건 "딥러닝" 인가보다.


딥러닝 글을 올린건 최근인데, 그래도 오버워치를 이기면서, 나름 블로그의 정체성을 지켜주었다.


뭐 그래서 결론은,

오버워치 PPT 템플릿 블로그가 되어버렸다.



※사족: 다른 템플릿도 올리고 싶은데, 공개 할만한 퀄리티의 작품이 더이상 나오지 않는다. 이제 더이상 학부생이 아니다 보니, 저런 톡톡튀는 템플릿을 쓸 일이 별로 안생기기도 하고, 개인적으로 템플릿을 쓰기 보단, 한 페이지 한 페이지를 깎아 만드는 편이라서.


※사족2: 결국 관종인거라, 오버워치 템플릿이 관심을 끈다면, 오케이! 땡큐! 다. 심지어 다른 템플릿을 못만들어서 슬프다니까?


※사족3: 최근 블로그 스킨을 바꾸고, 가독성이 확 떨어진 느낌이다. 아 어쩌지.. 폰트 못바꾸나.

'Diary' 카테고리의 다른 글

디아블로 이모탈을 본 단상  (0) 2018.11.08

댓글()

CAM - Class Acvtivation Map 논문 (Learning deep features for discriminative localization) 리뷰

@markdown


# CAM - Class Activation Map

Learning deep features for discriminative localization, Zhou, B., Khosla, A., Lapedriza, A., Oliva, A., & Torralba, A (MIT, csail)[^1]


## 서론

서두에, 이 논문을 읽으며 YbigTa 강병규 님의 글[^참고] 을 다소 참고하였음을 밝힌다.


### CAM 이란?

CAM(Class Activation Map)은 CNN(Convolutional Neural Network)을 해석 하고자 하는 생각에서 시작했다. 일반적으로, CNN은 특정 지역의 정보를 알고 해석한다고 믿어져 왔지만 이 부분이 명확히 해석되지 못하고 있다. 이를 위해, 기존 연구들 에서는 각 Filter들에 대한 해석을 노력해왔다. 


![enter image description here](https://lh3.googleusercontent.com/Tw-s-2fv1o9lB06NW-192XiKzILO7XlG2KaISynKGxHlLYir3ps6aW47AJvkY4ULhlbWmxNRcAk)

<center>

출처 : http://cs231n.github.io/convolutional-networks/

</center>


위 그림은 스탠포드 대학에서 온라인으로 제공하는 수업, cs231n[^2] 의 강의 자료중 일부 이다. 위 그림에서는 CNN이 동작하는 과정을 설명하기 위해 각 레이어를 지날때 마다의 값을 이미지로 표현하였다. 위 그림에서, 낮은 부분의 Layer에서는 엣지를 Detect하고, 깊은 레이어 일수록 특정 Feature를 찾아낸다는 점은 알 수 있지만, 왜 특정 Class로 분류 되는지는 보이지 않는다.



이 논문은 그러한 부분은 해결하기 위한 노력을 기하였다. 즉, 이미지가 분류될 때, 어느 부분이 해당 이미지 분류에 영향을 미쳤는지를 분석하기 위해 노력하였다.



### 논문의 특징

이 논문은 앞서 적은, 이미지의 어느 영역이 Class 분류에 큰 영향을 끼쳤는지를 분석하기 위하여 특징적인 구현을 하였다. 우선, 첫번째로는 기존의 분류 방법 대신 GAP라는 것을 사용하였다. GAP는 Global Average Pooling의 약자로, 기존 CNN분류기에서 사용하던 3층의 FC레이어(Fully connected Layer) 대신, 1층의 Average Pooling과 FC레이어를 사용하려는 아이디어 이다.


이는, NIN(Network In Network[^NIN])논문에서 처음 제안되었으며, 이후 이미지넷 첼린지에서 우승한 구글의 Inception Net(a.k.a GoogLeNet)[^GoogLeNet] 에도 적용되는 등 많은 참조가 되고 있는 구조 이다.


![GAP vs Ori](https://lh3.googleusercontent.com/Gc5xt-cXepMHzGrr1ThDhkGzcKzav6AmKI-OIdivbIJfhmilc2tuKqfyfnO-oBPw67iDMu6-Kx0)


 저자들은 CAM을 제안하면서 기존에 잘 알려진 모델들을 GAP가 적용되도록 재구현 하여 사용하였으며, GAP를 사용하였을 때에도 사용하지 않은 모델과 유사한 성능이 나옴을 실험적으로 증명하였다.


## 특징

### GAP

논문은 CAM을 구현하기 위해 GAP를 사용하였다. 기존 FC방식은, 최종 Convolution layer 의 결과를 Flatten(최종 Conv레이어의 결과는, 최종 채널 수 x 최종 W x 최종 H 로 3차원이다. 이를 1차원 행렬로 늘어놓아 Fully connected layer(이하 FC)의 인풋으로 바꿔주는 것을 Flatten 이라고 한다) 한 이후, 해당 데이터를 3번 정도의 FC레이어를 거쳐 Class별로 분류하는 방식이다. 이 과정은 많은 레이어를 소모 하므로, Network 의 속도가 늘어나고, 소모 메모리가 늘어나고, Over-fitting에 불리한 등의 문제를 가지고 있다. 따라서, NIN[^NIN] 논문 에서는 GAP라는것을 제안하였다.


![NIN 구조](https://lh3.googleusercontent.com/WXfY72DC3cxec_Qzc24-sGYql30xwPe0rhVogqYnlMtJgFOsxr4F82ExXegTer06SNIfcscPjkg)


GAP는 이를 위해, 1x1 사이즈의 filter를 가진 Convolutional network를 기존 CNN모델 마지막에 추가하였다. 이 때 해당 Conv레이어의 Out channel 사이즈는 분류하기 원하는 Class의 갯수와 같게 하였다. 이후 각 채널을 평균낸 값을 기존에 FC로 얻은 값 대신에 SoftMax 계층에 넣도록 하였다. 이렇게 하면, FC레이어 없이 학습 하므로 계수가 없어서 Over-fitting에 보다 안전하다.


하지만, CAM논문의 구현은 위와 같은 NIN의 구현과 다소 다르게 보였다. 공개된 저자들의 CAM구현[^3]을 보면 마지막 레이어가, 3x3 filter를 쓰는 out channerl이 1024인 Conv레이어 이며, 마지막이 1024x1000 인 FC로 끝나는 것을 확인 가능하다. 


따라서, 본 논문을 구현해 보는데 있어서는 저자들의 구현을 우선시 하여, 마지막 레이어에서average pooling --> channel x class FC 로 이어지는 구현을 GAP의 목적으로 사용하였다.


### Class Activation Map

CAM(Class Activation Map)은 CNN을 하는데 있어서, 이미지의 어느 부분이 결정에 큰 영향을 주었는지를 분석하고자 하는 목적에서 시작되었다. 이를 위해 저자들은 모델에 GAP을 적용시켰는데, 이는 GAP의 경우 마지막 판별 전 까지 데이터의 위치 정보가 훼손되지 않기 때문이다.(이전 까진, Flatten또는 FC등이 없기 때문)


![ "cam 구조"](https://lh3.googleusercontent.com/yesbxG9mqnP8uJ10y7zKJ4MRpKjtWuJbBwHzudTxuFW1rrXykjNQFF8GPHsyy74jLiZHrkq69WA)


즉, 마지막 판별 레이어에서 가지는 Weight값을 convolutional layers 와 pooling layers 를 거친 n x n 행렬 에 곱하면, 판별식에서 어느 부분이 큰 값을 가졌는지를 알 수 있다.


이는, 마지막 판별 레이어가 각 레이어의 Average 값을 입력으로 받는 노드라 이론적으로 옳은 표현이 되는데, 뉴런 네트워크에서 각 노드의 결과는 `입력 x Weight` 들을 모두 더한 값 이기 때문이다.  또한 위 구조에서, 마지막 FC레이어의 노드 갯수는 Class의 갯수와 같으므로, 각 노드의 `입력 x Weight` 값 들의 합을 비교해서, 가장 큰 값을 가지는 노드의 클래스로 분류되는 것이다.


![Neural의 구조](https://lh3.googleusercontent.com/6T9tzO3zShM6o5-ZT2CmLG_xztHe5hQCtUxwSYIf8GHR02P_FggmqeqhF1e-n0b88oLQT8DHZObV)


즉, 해당 값이 클수록 해당 클래스로 분류될 확율이 늘어난다. 따라서, `Weight` 값의 의미는 *분류를할 때 해당 채널의 중요도* 라고 볼 수 있을 것 이다. 이 때, 각 채널에서 높은 값을 가지는 부분은 해당 채널의 Average 값을 높히는데 가장 큰 역할을 하는 부분이다. 즉, 각 채널에서 가장 중요한 부분이라고 해석 가능하다.


따라서, CAM의 구현은, 해당 채널의 각 값에 Weight값을 곱한 수치를 중요도로 해석한다. 이후, 위와 같이 계산된 모든 채널을 더하여서 나온 n x n 행렬을 Heat-map 으로 그린다. 그려진 Heat-map은 이미지에서의 위치 정보를 여전히 포함하고 있으므로, 가장 높은 값을 가지는 위치가, 이미지에서 CNN의 Classification에 있어서 가장 영향력이 높은 부분이라 할 수 있다.


이를 수식으로 표현하면 아래와 같다.(M 은 CAM, x,y는 좌표 c 는 판별 클래스, k는 각 채널)

![enter image description here](https://lh3.googleusercontent.com/Q_ly_K8eX5IHXyYmyRKe0J9WmMXLTxTk6eIcRcNU9R36DmFaxbF7ZCGDcxbLKtawGmRmB4H6hTLg)


[VGG19 + CAM을 직접 구현한 글](http://blog.ees.guru/49)의 마지막에는 논문을 따라 구현하면서 얻은 결과를 포함하고 있지만, 여기에는 논문에서 나온 결과를 첨부하며 마무리 짓도록 하겠다.


![CAM result](https://lh3.googleusercontent.com/fsaJHcLNZrcDimwE9BwaDid6IWUicJgWBDD4P7yy8eA4xgDkfM4zBdsqpKf0BiPgSyFEts2VnDeB)


위 결과에서 보이듯,  이미지의 특징적인 부분에서 높은 값을 가지는 Class Activation Map(CAM)이 만들어진 것을 확인 가능하다.


특히 아래 dome으로 판별된 이미지의 top 5 결과를 보면, 각 판별 결과에 따라 중요하게 판별된 부분이 다른것 또한 확인 가능하다.



[^참고]: https://kangbk0120.github.io/articles/2018-02/cam

[^1]: ZHOU, Bolei, et al. Learning deep features for discriminative localization. In: _Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition_. 2016. p. 2921-2929. 

[^2]: [Convolutional Neural Networks for Visual Recognition](http://cs231n.github.io/)

[^3]: https://github.com/metalbubble/CAM

[^NIN]:LIN, Min; CHEN, Qiang; YAN, Shuicheng. Network in network. _arXiv preprint arXiv:1312.4400_, 2013.

[^GoogLeNet]:SZEGEDY, Christian, et al. Going deeper with convolutions. In: _Proceedings of the IEEE conference on computer vision and pattern recognition_. 2015. p. 1-9.

댓글()

VGG Net 과 CAM (Class Activation Map) 구현

VGG Net 과 CAM 구현

몇 팀원과 같이 스터디 중인 리포지토리 홍보 : https://poddeeplearning.readthedocs.io/ko/latest/CNN/VGG19%20+%20GAP%20+%20CAM/

Initial

import torch
import torchvision
import torchvision.transforms as transforms

torch libary init

torch.cuda.is_available()
True

torch.cuda.is_available 값이 True 면, 현재 GPU를 사용한 학습이 가능하다는 뜻, False면 불가능

import torch
import torchvision
import torchvision.transforms as transforms

import torch.nn as nn
import torch.nn.functional as F

transform = transforms.Compose([
    transforms.Resize(224),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
])

trainset = torchvision.datasets.STL10(root='./data', split='train', download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)

testset = torchvision.datasets.STL10(root='./data', split='test', download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)
Files already downloaded and verified
Files already downloaded and verified

Pytorch tutorial 을 참고하여 구현하였다.


STL10 데이터셋을 가져오도록 했다.(원본 출처)

STL10 데이터셋은 Stanford 대학에서 만든 데이터 셋 으로, 다양한 이미지들을 미리 10개의 클래스로 나누어 놓고, 이미지 별 정답 데이터를 같이 제공해주는 데이터셋 이다. 

이미지 학습에 자주 사용되는 데이터셋 이며,(이외에도, Cifar10/100: 10개/100개 의 클래스로 나눠놓은 데이터 셋, MNIST: 0~9 까지의 숫자 손글씨 데이터 셋 등이 자주 사용된다.) pytorch에서는 torchvision이라는 라이브러리에서 쉬운 사용을 위한 방법을 제공한다.

torchbision.datasets.원하는 데이터 셋 이름 과 같은 명명 방식으로 가져오는 함수가 구성되어 있으며, 기본적으로는 각 함수별로 유사하지만 직접 수행해본 결과, STL10과 Cifar10/100 은 train set과 test set을 나누는 방법이 다르다.


STL10 의 경우는 위와 같이, argument 에 split='train' or split='test' 로 작성하여 나누게 되고, Cifar10/100 의 경우에는 argument에 train=True or train=False 와 같이 작성하여 나누는 방식을 취한다.

batch_size=64 는 미니배치(한번에 모델에 동시에 돌려 학습시킬 사이즈) 이다.


많은 pythonic 한 라이브러리 들이, 같은 형태를 취한다면, 이용법을 최대한 맞추게 되어 있는데, 그렇지 못해서 이 부분이 다소 아쉬웠다.

transform = transforms.Compose([ transforms.Resize(224), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ])


이 부분은, 데이터셋을 가져올 때, 형태를 변환해주는 코드로, 위 부터 설명하면 아래와 같다. ToTensor의 위치에 따라, Resize와 Normalize의 순서는 입력한 순서와 같아야 한다.

  • transforms.Resize(224) : 이미지의 크기를 224x224로 변환, 이는 VGG Net에서 대상으로 하는 이미지의 크기가 244x244 이기 때문

  • transforms.ToTensor() : 받아오는 데이터를 pytorch에서 사용하기 위한 Tensor 자료 구조로 변환

  • transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) : 받아오는 데이터를 노말라이징, 입력 전에 노말라이징이 필요한가 여부는 아직 논의가 되고있는 것으로 보이지만, 노말라이징을 하는 경우, 특정 부분이 너무 밝거나 어둡다거나 해서 데이터가 튀는 현상을 막아줌

import matplotlib.pyplot as plt
import numpy as np

# functions to show an image


def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))


# get some random training images
dataiter = iter(trainloader)
images, labels = dataiter.next()

# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

 deer  deer plane  frog

png

imshow는 다른 사람이 구현한 내용을 가져왔다. (Pytorch tutorial 참조)


간단하게 말하면, matplotlib에 있는 pyplot 을 써서 데이터 셋에 있는 이미지를 읽으면, 비교적 어두운 색으로 보이는 이미지가 출력된다. (이는 학습을 위한 데이터 셋 이므로, 미리 정규화가 되어있기 때문) 

이를 다시 정상적인 이미지로 만들기 위해, 이미지를 unnormalize 하고 plt.imshow에 넣어 출력해 주는 함수이다.

VGG 모델 구현

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv = nn.Sequential(
            #3 224 128
            nn.Conv2d(3, 64, 3, padding=1),nn.LeakyReLU(0.2),
            nn.Conv2d(64, 64, 3, padding=1),nn.LeakyReLU(0.2),
            nn.MaxPool2d(2, 2),
            #64 112 64
            nn.Conv2d(64, 128, 3, padding=1),nn.LeakyReLU(0.2),
            nn.Conv2d(128, 128, 3, padding=1),nn.LeakyReLU(0.2),
            nn.MaxPool2d(2, 2),
            #128 56 32
            nn.Conv2d(128, 256, 3, padding=1),nn.LeakyReLU(0.2),
            nn.Conv2d(256, 256, 3, padding=1),nn.LeakyReLU(0.2),
            nn.Conv2d(256, 256, 3, padding=1),nn.LeakyReLU(0.2),
            nn.MaxPool2d(2, 2),
            #256 28 16
            nn.Conv2d(256, 512, 3, padding=1),nn.LeakyReLU(0.2),
            nn.Conv2d(512, 512, 3, padding=1),nn.LeakyReLU(0.2),
            nn.Conv2d(512, 512, 3, padding=1),nn.LeakyReLU(0.2),
            nn.MaxPool2d(2, 2),
            #512 14 8
            nn.Conv2d(512, 512, 3, padding=1),nn.LeakyReLU(0.2),
            nn.Conv2d(512, 512, 3, padding=1),nn.LeakyReLU(0.2),
            nn.Conv2d(512, 512, 3, padding=1),nn.LeakyReLU(0.2),
            nn.MaxPool2d(2, 2)
        )
        #512 7 4

        self.avg_pool = nn.AvgPool2d(7)
        #512 1 1
        self.classifier = nn.Linear(512, 10)
        """
        self.fc1 = nn.Linear(512*2*2,4096)
        self.fc2 = nn.Linear(4096,4096)
        self.fc3 = nn.Linear(4096,10)
        """

    def forward(self, x):
        
        #print(x.size())
        features = self.conv(x)
        #print(features.size())
        x = self.avg_pool(features)
        #print(avg_pool.size())
        x = x.view(features.size(0), -1)
        #print(flatten.size())
        x = self.classifier(x)
        #x = self.softmax(x)
        return x, features

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

net = Net()
net = net.to(device)
param = list(net.parameters())
print(len(param))
for i in param:
    print(i.shape)
#print(param[0].shape)
28
torch.Size([64, 3, 3, 3])
torch.Size([64])
torch.Size([64, 64, 3, 3])
torch.Size([64])
torch.Size([128, 64, 3, 3])
torch.Size([128])
torch.Size([128, 128, 3, 3])
torch.Size([128])
torch.Size([256, 128, 3, 3])
torch.Size([256])
torch.Size([256, 256, 3, 3])
torch.Size([256])
torch.Size([256, 256, 3, 3])
torch.Size([256])
torch.Size([512, 256, 3, 3])
torch.Size([512])
torch.Size([512, 512, 3, 3])
torch.Size([512])
torch.Size([512, 512, 3, 3])
torch.Size([512])
torch.Size([512, 512, 3, 3])
torch.Size([512])
torch.Size([512, 512, 3, 3])
torch.Size([512])
torch.Size([512, 512, 3, 3])
torch.Size([512])
torch.Size([10, 512])
torch.Size([10])

VGG Net 논문 본문을 확인하여, VGG19 모델의 구조를 참고 하였다.

VGG net

Sequential 을 활용하여 구현하였다. Sequential을 이용할 경우, forward에서 각 레이어를 하나 하나 부르는 대신, 해당 Sequence의 이름을 불러서 한번에 이용 가능하다.

features = self.conv(x)

이후 기존 VGG19 모델과 달리 마지막에는 Average pooling을 하고, Fully Connected Layer를 사용하여 Classification을 하였다. 

이는 NIN논문에서 구현된 GAP(NIN논문에서는 Class의 갯수와 같은 채널 수 를 가지는 Conv모델을 마지막에 사용하였으며, 각 채널의 Average 값을 SoftMax에 입력하였다. 즉,FC레이어를 사용하지 않았다.)와 다소 다르지만, 논문의 caffe 버전 구현을 중심으로 하여 구현하였다.


모델에서 Pytorch는 filter의 크기에 맞춰서 적절한 패딩 값을 직접 입력해 주어야 한다. 일반적으로 패딩의 크기는 stride 값을 1을 사용할때, 필터의 크기의 절반을 사용한다. 이는 필터의 크기가 절반이 되어야 모든 픽셀이 한번 씩 중앙에 오게 되기 때문(:= 모든 픽셀을 체크하는 것이 되기 때문)이다.


self.avg_pool = nn.AvgPool2d(7)

AvgPool2d 의 필터 크기가 7x7인 이유는, MaxPooling 레이어를 거칠때 마다 이미지의 각 면의 길이는 절반이 된다.(nn.MaxPool2d(2, 2)) VGG19 에서 요구하는 이미지의 크기는 244x244 이고, 122,56,28,14,7 의 순서로 크기가 줄어들게 되기 때문이다.


device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

마지막으로 해당 코드는, GPU를 사용하기 위한 코드이다. device라는 변수명에 GPU 디바이스를 지정/저장 해 준다. 이후

net = Net() net = net.to(device)

위와 같이, 만든 Net을 초기화 해 주고, net.to(device) 구문을 통해 해당 네트워크를 GPU에서 돌아가는 네트워크로 지정하게 된다.


classes =  ('airplance', 'bird', 'car', 'cat', 'deer', 'dog', 'horse', 'monkey', 'ship', 'truck')
import torch.optim as optim

criterion = nn.CrossEntropyLoss().cuda()
optimizer = optim.Adam(net.parameters(),lr=0.00001)

criterion 과 optimizer를 정의한다. 기본적인 구조는 역시 Pytorch tutorial을 참고 하였다.


criterion 은 일반적으로 loss 라고도 부르며, 이를 통해 학습을 위한 loss값을 계산하게 된다. 현재 많은 경우에 CrossEntropyLoss가 winner로 취급받고 있으며, pytorch의 CrossEntropyLoss의 경우 내부에서 SoftMax를 내장하고 있으므로, 모델 구조에서 SoftMax를 할 필요가 없다.(SoftMax가 없는 CrossEntropyLoss를 사용하고 싶을 경우 NLLLoss를 사용하면 된다.


Adam optimizer는 역시 현재 자주 상요되는 옵티마이저로, 학습 시 현재의 미분값 뿐만이 아니라, 이전 결과에 따른 관성 모멘트를 가지고 있는것이 특징이다. 이는 paddle point(안장점)에 도달하더라도 빠져나갈 수 있으므로, adam optimizer뿐만이 아니라, 현재 자주사용되는 옵티마이저들 에서는 대부분 사용되는 개념이다. 아래 그림은 관성모멘트에 따라 paddle point를 빠져나가는 방법에 따른 도식도 이다. 이 그림은 자주 인용되는 그림인데, 나는 최초 출처를 찾지 못하고 있다. 내가 가져온 곳은 이곳이다. 이외에도 옵티마이저에 대한 설명이 잘 되어있어 다른 사람한테 optimizer를 설명할때 자주 참고하고 있다.

Optimizer

VGG 모델 실행

for epoch in range(100):  # loop over the dataset multiple times
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs
        inputs, labels = data
        inputs, labels = inputs.to(device), labels.to(device)
        # zero the parameter gradients
        optimizer.zero_grad()

        #print(inputs.shape)
        #print(inputs.shape)  
        # forward + backward + optimize
        outputs,f = net(inputs)
        #print(outputs.shape)
        #print(labels.shape)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        if(loss.item() > 1000):
            print(loss.item())
            for param in net.parameters():
                print(param.data)
        # print statistics
        running_loss += loss.item()
        if i % 50 == 49:    # print every 2000 mini-batches
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 50))
            running_loss = 0.0

print('Finished Training')

class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
    for data in testloader:
        images, labels = data
        images = images.cuda()
        labels = labels.cuda()
        outputs,_ = net(images)
        _, predicted = torch.max(outputs, 1)
        c = (predicted == labels).squeeze()
        for i in range(4):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1


for i in range(10):
    print('Accuracy of %5s : %2d %%' % (
        classes[i], 100 * class_correct[i] / class_total[i]))
[1,    50] loss: 0.001
[2,    50] loss: 0.001
[3,    50] loss: 0.001
[4,    50] loss: 0.000
[5,    50] loss: 0.000
[6,    50] loss: 0.000
[7,    50] loss: 0.000
[8,    50] loss: 0.000
[9,    50] loss: 0.000
[10,    50] loss: 0.000

학습을 하고 있는 과정이다.


[이터레이션 횟수, 미니배치 횟수] 의 구성이며, 실제로는 10회 보다 많은 횟수의 이터레이션을 돌렸지만, 다시 수행하면서 이전 정보가 지워졌다... 실제로는 약 100회의 이터레이션을 돌리고, loss 값이 0.000이 되었을 때 강제로 멈춰 버렸다.(조급증 때문에...)


학습 과정은 계속 참고 하고있는 pytorch tutorial을 참고하였으며,

inputs, labels = data inputs, labels = inputs.to(device), labels.to(device)

을 통해 데이터 들을 GPU들을 복사하는것 이외엔 사실 자세히 보지 않고 그냥 사용하였다.


class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
    for data in testloader:
        images, labels = data
        images = images.cuda()
        labels = labels.cuda()
        outputs, f = net(images)
        _, predicted = torch.max(outputs, 1)
        c = (predicted == labels).squeeze()
        for i in range(4):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1


for i in range(10):
    print('Accuracy of %5s : %2d %%' % (
        classes[i], 100 * class_correct[i] / class_total[i]))
Accuracy of airplance : 55 %
Accuracy of  bird : 54 %
Accuracy of   car : 84 %
Accuracy of   cat : 52 %
Accuracy of  deer : 59 %
Accuracy of   dog : 49 %
Accuracy of horse : 66 %
Accuracy of monkey : 66 %
Accuracy of  ship : 84 %
Accuracy of truck : 65 %

역시, 튜토리얼에서 참고한 코드이며, Test셋을 이용하여 정답율을 보았다.

생각보다 높지 않은 정답율이 나와 아쉬웠다.


CAM

torch.save(net, 'vgg19.pt')
/lvmdata/ees/anaconda3/envs/tensorflow35/lib/python3.5/site-packages/torch/serialization.py:241: UserWarning: Couldn't retrieve source code for container of type Net. It won't be checked for correctness upon loading.
  "type " + obj.__name__ + ". It won't be checked "
net2 = torch.load('vgg19.pt')
net2 = net2.to(device)

CAM을 하기에 앞서, GPU 메모리를 비우는 등의 작업을 위해 일단 저장하고 커널을 종료 후 다시 불러서 사용하였다. 역시 GPU를 사용하기 위해서, .to(device) 를 사용하였다.


class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
    for data in testloader:
        images, labels = data
        images = images.cuda()
        labels = labels.cuda()
        outputs, f = net2(images)
        _, predicted = torch.max(outputs, 1)
        c = (predicted == labels).squeeze()
        for i in range(4):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1


for i in range(10):
    print('Accuracy of %5s : %2d %%' % (
        classes[i], 100 * class_correct[i] / class_total[i]))

CAM 을 위해 다시 Prediction을 계산하였다. 만, 이 문서를 작성하다가 실수로 실행 결과 화면을 지워버렸다. 위의 정확도와 실행 결과는 동일.

params = list(net.parameters())

파라미터들을 가져오도록 한다. 역시 CAM의 구현을 위함이다.

CAM?

CAM에 대해서는 다른 게시물로 작성하고 있지만, 지금 간단하게 설명하자면, 마지막 FC layer에서 사용하는 각 채널의 weight값과 각 채널을 Average하기 전의 7x7 데이터를 곱한 값 들을 더한 값을 이용하여, 해당 수치가 가장 높은 부분이 모델에서 가장 중요하게 판단하는 부분일 것 이라고 판단하는 논문 이다.

 ZHOU, Bolei, et al. Learning deep features for discriminative localization. In: _Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition_. 2016. p. 2921-2929. 

CAM

import torch
import torchvision
import torchvision.transforms as transforms

import torch.nn as nn
import torch.nn.functional as F

import matplotlib.pyplot as plt

net = torch.load('vgg19.pt')
net = net.to(device)
import numpy as np
def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    
for data in testloader:    
    images, labels = data
    images = images.cuda()
    labels = labels.cuda()
    outputs, f = net(images)
    _, predicted = torch.max(outputs, 1)
    break

imshow를 다시 구현하였으며, predicted 1회 만 다시 하는 코드를 작성하였다.

사실 이 부분에서 이슈가 있는것 같다... testloader에서 1회만 하고 하고 싶었는데, 이 이후 Memory leak 이 생기는 듯 하다.

import skimage.transform
classes =  ('airplance', 'bird', 'car', 'cat', 'deer', 'dog', 'horse', 'monkey', 'ship', 'truck')
params = list(net.parameters())
num = 0
for num in range(64):
    print("ANS :",classes[int(predicted[num])]," REAL :",classes[int(labels[num])],num)
    
    #print(outputs[0])
    
    overlay = params[-2][int(predicted[num])].matmul(f[num].reshape(512,49)).reshape(7,7).cpu().data.numpy()

    overlay = overlay - np.min(overlay)
    overlay = overlay / np.max(overlay)

    imshow(images[num].cpu())
    skimage.transform.resize(overlay, [224,224])
    plt.imshow(skimage.transform.resize(overlay, [224,224]), alpha=0.4,cmap='jet')
    plt.show()
    imshow(images[num].cpu())
    plt.show()
    
#plt.imshow(params[-2][int(predicted[num])].matmul(f[num].reshape(512,49)).reshape(7,7).cpu().data.numpy(), alpha=0.5,cmap='jet');
ANS : horse  REAL : horse 0


/lvmdata/ees/anaconda3/envs/tensorflow35/lib/python3.5/site-packages/skimage/transform/_warps.py:105: UserWarning: The default mode, 'constant', will be changed to 'reflect' in skimage 0.15.
  warn("The default mode, 'constant', will be changed to 'reflect' in "
/lvmdata/ees/anaconda3/envs/tensorflow35/lib/python3.5/site-packages/skimage/transform/_warps.py:110: UserWarning: Anti-aliasing will be enabled by default in skimage 0.15 to avoid aliasing artifacts when down-sampling images.
  warn("Anti-aliasing will be enabled by default in skimage 0.15 to "

png

png

ANS : monkey  REAL : monkey 1

png

png

ANS : dog  REAL : dog 2

png

png

ANS : ship  REAL : airplance 3

png

png

ANS : cat  REAL : cat 4

png

png

ANS : bird  REAL : bird 5

png

png

ANS : horse  REAL : bird 6

png

png

ANS : bird  REAL : bird 7

png

png

ANS : deer  REAL : deer 8

png

png

ANS : horse  REAL : deer 9

png

png

ANS : bird  REAL : airplance 10

png

png

ANS : airplance  REAL : airplance 11

png

png

ANS : deer  REAL : deer 12

png

png

ANS : airplance  REAL : airplance 13

png

png

ANS : cat  REAL : bird 14

png

png

ANS : cat  REAL : dog 15

png

png

ANS : deer  REAL : deer 16

png

png

ANS : truck  REAL : truck 17

png

png

ANS : airplance  REAL : airplance 18

png

png

ANS : horse  REAL : horse 19

png

png

ANS : horse  REAL : dog 20

png

png

ANS : cat  REAL : cat 21

png

png

ANS : dog  REAL : bird 22

png

png

ANS : cat  REAL : cat 23

png

png

ANS : car  REAL : car 24

png

png

ANS : horse  REAL : horse 25

png

png

ANS : bird  REAL : dog 26

png

png

ANS : cat  REAL : dog 27

png

png

ANS : truck  REAL : car 28

png

png

ANS : dog  REAL : monkey 29

png

png

ANS : truck  REAL : bird 30

png

png

ANS : horse  REAL : horse 31

png

png

ANS : dog  REAL : cat 32

png

png

ANS : monkey  REAL : monkey 33

png

png

ANS : truck  REAL : truck 34

png

png

ANS : truck  REAL : car 35

png

png

ANS : deer  REAL : deer 36

png

png

ANS : monkey  REAL : monkey 37

png

png

ANS : truck  REAL : truck 38

png

png

ANS : horse  REAL : dog 39

png

png

ANS : bird  REAL : bird 40

png

png

ANS : airplance  REAL : truck 41

png

png

ANS : airplance  REAL : airplance 42

png

png

ANS : horse  REAL : horse 43

png

png

ANS : deer  REAL : deer 44

png

png

ANS : truck  REAL : truck 45

png

png

ANS : bird  REAL : bird 46

png

png

ANS : bird  REAL : bird 47

png

png

ANS : cat  REAL : cat 48

png

png

ANS : car  REAL : car 49

png

png

ANS : cat  REAL : dog 50

png

png

ANS : cat  REAL : deer 51

png

png

ANS : airplance  REAL : airplance 52

png

png

ANS : airplance  REAL : airplance 53

png

png

ANS : car  REAL : truck 54

png

png

ANS : ship  REAL : ship 55

png

png

ANS : horse  REAL : horse 56

png

png

ANS : monkey  REAL : monkey 57

png

png

ANS : car  REAL : car 58

png

png

ANS : ship  REAL : airplance 59

png

png

ANS : airplance  REAL : airplance 60

png

png

ANS : dog  REAL : bird 61

png

png

ANS : bird  REAL : bird 62

png

png

ANS : monkey  REAL : monkey 63

png

png

CAM을 구현하였다.

CAM의 구현은 앞에서 적은 저자들의 코드를 참고하였다. (CAM의 구현은 Pytorch로 구현되어있음)


overlay = params[-2][int(predicted[num])].matmul(f[num].reshape(512,49)).reshape(7,7).cpu().data.numpy()

이 부분 위와 아래는, 정규화 나 이미지 출력을 위한 부분이며, 해당 부분이 핵심이다.


params[-2] 는 뒤에서 부터 두번째 레이어 이다. 이는, 512x1 input을 받는 FC레이어의 weight값들 이다. 즉 512x1 의 크기를 가진다. 이것과 matmul(행렬곱)을 하는 f[num]은 512개의 채널을 가지는 7x7 행렬들 이다. 이를 512x49로 리쉐잎을 하는 이유는 행렬곱이 2차원 행렬 끼리의 곱만 지원하기 때문이다.


마지막으로 이것을 cpu로 바꿔주고 넘파이로 바꿔주며 끝을 낸다. cpu로 바꿔주지 않고 numpy로 바꿀 경우 오류가 나게 된다.

imshow(images[num].cpu()) skimage.transform.resize(overlay, [224,224]) plt.imshow(skimage.transform.resize(overlay, [224,224]), alpha=0.4,cmap='jet') plt.show()

이 부분을 통해 이미지를 출력하게 된다. 

먼저, imshow로 원본 이미지를 출력해 주고, 같은 팔레트에 위에서 계산한 overlay(cam 값)을 그려준다.


이를 위해, 이미지와 같은 크기(224x224)로 리사이징을 하고, imshow의 argument 값에 alpha 값을 0.4로 설정해 주어 반투명 하게 덮어씌워주도록 한다.


위 출력에서 아래에 원본 이미지가 출력되는 것은 아랫줄에서 원본 이미지만 다시 한번 출력하기 때문이다.


여기서 주의하여야 하는 점은, plt.show()를 해야 현재 팔래트를 화면에 출력하고 새로운 팔래트를 준비한다는 점 이다.

결과를 통해, 해당 모델(VGG19)에서 어느 부분을 통해 해당 클래스로 판별한 것 인지 확인 가능했다.(일부 결과의 경우 특이한 결과를 보여주는데, 사슴(deer)의 경우 사슴본체 보단 주변 풀숲에서 높은 값이 보이는 것이 한 예 이다.)

댓글()