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)가 줄어드는 현상이 발생한다.
보면, 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%의 오차로 우승을 거머쥐었다.
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들에 대한 해석을 노력해왔다.
위 그림은 스탠포드 대학에서 온라인으로 제공하는 수업, 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라는것을 제안하였다.
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등이 없기 때문)
즉, 마지막 판별 레이어에서 가지는 Weight값을 convolutional layers 와 pooling layers 를 거친 n x n 행렬 에 곱하면, 판별식에서 어느 부분이 큰 값을 가졌는지를 알 수 있다.
이는, 마지막 판별 레이어가 각 레이어의 Average 값을 입력으로 받는 노드라 이론적으로 옳은 표현이 되는데, 뉴런 네트워크에서 각 노드의 결과는 `입력 x Weight` 들을 모두 더한 값 이기 때문이다. 또한 위 구조에서, 마지막 FC레이어의 노드 갯수는 Class의 갯수와 같으므로, 각 노드의 `입력 x Weight` 값 들의 합을 비교해서, 가장 큰 값을 가지는 노드의 클래스로 분류되는 것이다.
즉, 해당 값이 클수록 해당 클래스로 분류될 확율이 늘어난다. 따라서, `Weight` 값의 의미는 *분류를할 때 해당 채널의 중요도* 라고 볼 수 있을 것 이다. 이 때, 각 채널에서 높은 값을 가지는 부분은 해당 채널의 Average 값을 높히는데 가장 큰 역할을 하는 부분이다. 즉, 각 채널에서 가장 중요한 부분이라고 해석 가능하다.
따라서, CAM의 구현은, 해당 채널의 각 값에 Weight값을 곱한 수치를 중요도로 해석한다. 이후, 위와 같이 계산된 모든 채널을 더하여서 나온 n x n 행렬을 Heat-map 으로 그린다. 그려진 Heat-map은 이미지에서의 위치 정보를 여전히 포함하고 있으므로, 가장 높은 값을 가지는 위치가, 이미지에서 CNN의 Classification에 있어서 가장 영향력이 높은 부분이라 할 수 있다.
이를 수식으로 표현하면 아래와 같다.(M 은 CAM, x,y는 좌표 c 는 판별 클래스, k는 각 채널)
[^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/)
[^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.
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 한 라이브러리 들이, 같은 형태를 취한다면, 이용법을 최대한 맞추게 되어 있는데, 그렇지 못해서 이 부분이 다소 아쉬웠다.
이 부분은, 데이터셋을 가져올 때, 형태를 변환해주는 코드로, 위 부터 설명하면 아래와 같다. 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 imagedefimshow(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 labelsprint(''.join('%5s'% classes[labels[j]] for j inrange(4)))
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에서 돌아가는 네트워크로 지정하게 된다.
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를 설명할때 자주 참고하고 있다.
VGG 모델 실행
for epoch inrange(100): # loop over the dataset multiple times
running_loss =0.0for i, data inenumerate(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-batchesprint('[%d, %5d] loss: %.3f'%
(epoch +1, i +1, running_loss /50))
running_loss =0.0print('Finished Training')
class_correct =list(0. for i inrange(10))
class_total =list(0. for i inrange(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 inrange(4):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] +=1for i inrange(10):
print('Accuracy of %5s : %2d%%'% (
classes[i], 100* class_correct[i] / class_total[i]))
[이터레이션 횟수, 미니배치 횟수] 의 구성이며, 실제로는 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 inrange(10))
class_total =list(0. for i inrange(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 inrange(4):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] +=1for i inrange(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 inrange(10))
class_total =list(0. for i inrange(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 inrange(4):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] +=1for i inrange(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.
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
defimshow(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 이 생기는 듯 하다.
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 "
ANS : monkey REAL : monkey 1
ANS : dog REAL : dog 2
ANS : ship REAL : airplance 3
ANS : cat REAL : cat 4
ANS : bird REAL : bird 5
ANS : horse REAL : bird 6
ANS : bird REAL : bird 7
ANS : deer REAL : deer 8
ANS : horse REAL : deer 9
ANS : bird REAL : airplance 10
ANS : airplance REAL : airplance 11
ANS : deer REAL : deer 12
ANS : airplance REAL : airplance 13
ANS : cat REAL : bird 14
ANS : cat REAL : dog 15
ANS : deer REAL : deer 16
ANS : truck REAL : truck 17
ANS : airplance REAL : airplance 18
ANS : horse REAL : horse 19
ANS : horse REAL : dog 20
ANS : cat REAL : cat 21
ANS : dog REAL : bird 22
ANS : cat REAL : cat 23
ANS : car REAL : car 24
ANS : horse REAL : horse 25
ANS : bird REAL : dog 26
ANS : cat REAL : dog 27
ANS : truck REAL : car 28
ANS : dog REAL : monkey 29
ANS : truck REAL : bird 30
ANS : horse REAL : horse 31
ANS : dog REAL : cat 32
ANS : monkey REAL : monkey 33
ANS : truck REAL : truck 34
ANS : truck REAL : car 35
ANS : deer REAL : deer 36
ANS : monkey REAL : monkey 37
ANS : truck REAL : truck 38
ANS : horse REAL : dog 39
ANS : bird REAL : bird 40
ANS : airplance REAL : truck 41
ANS : airplance REAL : airplance 42
ANS : horse REAL : horse 43
ANS : deer REAL : deer 44
ANS : truck REAL : truck 45
ANS : bird REAL : bird 46
ANS : bird REAL : bird 47
ANS : cat REAL : cat 48
ANS : car REAL : car 49
ANS : cat REAL : dog 50
ANS : cat REAL : deer 51
ANS : airplance REAL : airplance 52
ANS : airplance REAL : airplance 53
ANS : car REAL : truck 54
ANS : ship REAL : ship 55
ANS : horse REAL : horse 56
ANS : monkey REAL : monkey 57
ANS : car REAL : car 58
ANS : ship REAL : airplance 59
ANS : airplance REAL : airplance 60
ANS : dog REAL : bird 61
ANS : bird REAL : bird 62
ANS : monkey REAL : monkey 63
CAM을 구현하였다.
CAM의 구현은 앞에서 적은 저자들의 코드를 참고하였다. (CAM의 구현은 Pytorch로 구현되어있음)
params[-2] 는 뒤에서 부터 두번째 레이어 이다. 이는, 512x1 input을 받는 FC레이어의 weight값들 이다. 즉 512x1 의 크기를 가진다. 이것과 matmul(행렬곱)을 하는 f[num]은 512개의 채널을 가지는 7x7 행렬들 이다. 이를 512x49로 리쉐잎을 하는 이유는 행렬곱이 2차원 행렬 끼리의 곱만 지원하기 때문이다.
마지막으로 이것을 cpu로 바꿔주고 넘파이로 바꿔주며 끝을 낸다. cpu로 바꿔주지 않고 numpy로 바꿀 경우 오류가 나게 된다.