※ 본 게시물에 사용된 내용의 출처는 대다수 <펭귄브로의 3분 딥러닝-파이토치맛>에서 사용된 자료이며, 개인적인 의견과 해석이 추가된 부분도 존재합니다. 그림의 경우 교재를 따라 그리거나, 제 임의대로 추가 수정한 부분도 존재합니다. 저 역시도 공부하고자 포스팅한 게시물이니, 잘못된 부분은 댓글로 알려주시면 감사하겠습니다.
- Simple CNN 코드 리뷰
- Deep CNN
- ResNet
- Batch Normalization
- VGG
Simple CNN 코드 리뷰
일전에 언급했던 CNN의 기본적인 구조를 구현해놓은 간단한 코드를 분석해본다.
- 사용코드
본 예제에서는 앞에서 사용한 패션 아이템을 CNN 네트워크를 사용하여, 분류 성능을 높여본다.
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import transforms, datasets
from torchsummary import summary # 모델 구조를 한번에 확인하기 위해 추가한 라이브러리
#torchsummary : 파라미터 개수, 레이어 구조를 정리하여 확인할 수 있다.
준비 코드는 이전과 거의 비슷한 필수 라이브러리들을 Import한다. 이때 추가적으로 torchsummary 라이브러리를 추가하여 네트워크 내 파라미터의 개수와 구조를 간단하게 보고자 한다.
# GPU 사용여부 확인 코드
# DEVICE 안에 cuda(gpu)가 할당되어야 함
USE_CUDA = torch.cuda.is_available()
DEVICE = torch.device("cuda" if USE_CUDA else "cpu")
EPOCHS = 40
BATCH_SIZE = 64
이전과 동일하게 GPU동작을 위해 DEVICE할당과 Epoch, batch_size등 필수적인 파라미터를 정의해준다.
데이터를 로딩하는 과정은 이전과 동일하므로, 넘어가도록 한다.
# Dataloader : 데이터셋을 batch 단위로 쪼개서 학습할 때 모델의 입력으로 주는 클래스.
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('./.data',
train=True,#학습
download=True,#없으면 다운로드
transform=transforms.Compose([#torchvision.transform = 입력 변환 라이브러리
transforms.ToTensor(),#이미지를 Tensor로
transforms.Normalize((0.1307,), (0.3081,))#이미지 정규화
])),
batch_size=BATCH_SIZE, shuffle=True)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./.data',
train=False, #테스트
download=True,#없으면 다운로드
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=BATCH_SIZE, shuffle=True)
본격적인 CNN모델을 구현해보도록 하자.
크게 CNN 클래스를 구현하는 Init함수와 실제 데이터가 지나가는 길인 forward함수로 나뉜다.
CNN모델은 일전에 말한대로 CNN-ReLU(Activation function)-Dropout(Overfitting방지) 와 같은 형태를 유지한다.
class Net(nn.Module):
# CNN Network Class 정의#
# kernel size 5x5, 2 Convolutional layer#
def __init__(self):
super(Net, self).__init__()
# 채널 수 : 1(흑백 이미지) / output volume_size : 10(필터의 개수==10개의 특징맵을 생성) / kernel_size(필터의 사이즈) = 5x5
# kernel size를 하나만 입력하면 NxN으로 간주. / NxM을 원할경우, (N,M)으로 정의
self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
# 입력 채널 수 : 10 (conv1의 결과물) / output volume size : 20 (20개의 특징맵을 생성) / kernel_size(필터의 사이즈) = 5x5
self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
#Dropout 적용
self.conv2_drop = nn.Dropout2d()
# 일반 신경망을 거치면서 이전 출력 크기인 320을 기준으로 50, 10순으로 작아지도록 설정
# 이때 50과 같은 중간 값은 임의로 설정한 값. 10은 분류해야 할 클래스의 개수(FASHION MNIST CLASS 개수)
self.fc1 = nn.Linear(320, 50)
self.fc2 = nn.Linear(50, 10)
#CNN Network의 동작(forward)함수, 본격적으로 데이터가 지나갈 길을 닦아준다 생각하면 됨.#
def forward(self, x):
# 각 레이어는 conv - pooling - relu를 하나의 묶음으로 간주
x = F.relu(F.max_pool2d(self.conv1(x), 2))# max pooling with (2x2)kernel
x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
# 데이터를 FC레이어에 넣기 위해 2차원에서 1차원 형태로 변경
x = x.view(-1, 320)
#RELU activation function+Dropout
x = F.relu(self.fc1(x))
x = F.dropout(x, training=self.training)
# 출력 클래스 10개인 Output생성
x = self.fc2(x)
return x
레이어는 크게 CNN 레이어 2개(Conv1, Conv2) FC 레이어 2개(fc1, fc2)로 구성되어 있으며, 중간중간 활성화 함수와 pooling(max)이 껴있다. 사실상 CNN 두개를 사용하여 이미지의 feature를 추출하고, fc1, fc2를 거치면서 실제 클래스 분류를 진행한다 보면 된다. 일전 예제와 비슷하게 fc2의 최종 결과물이 데이터의 클래스 개수와 동일해지면서 최종 예측값을 내보내는 레이어라 생각하면 된다.
각 라인별 주석을 추가해놓으니, 그대로 따라가면 되겠지만 중간에 있는 320이라는 feature의 개수는 CNN의 output 개수로, 이를 직접 수식으로 계산하는 방법이 존재하지만, 우리는 조금 영리하게 pdb 라이브러리를 사용하여 중간에 찍어보도록 하자. 나는 실제로 DNN를 구현할 때 pdb를 가장 많이 사용한다. pdb는 일전 포스팅에 적어놓았지만 아래와 같이 사용하면 된다. 자세한 pdb명령어는 다음 포스팅을 참고하자.
import pdb; pdb.set_trace()
모델 설계가 끝난 후, 실제 코드 수행은 일전과 동일하기 때문에 따로 추가하지 않도록 한다.
model = Net().to(DEVICE)
summary(model, (1, 28, 28)) #모델 구조를 보기 위하여 삽입한 코드
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
print(model)
summary 함수를 출력해보면 파라미터 개수 및 레이어를 확인할 수 있다.
최종적으로 실제 훈련 및 테스트를 진행하면 이전 FC에 비해 매우 높은 성능을 보이는 것을 알 수 있다.
Deep CNN
이전까지는 단순한 흑백, 사이즈가 작은 이미지의 데이터셋에 적용하기 때문에 비교적 간단한 모델로 학습 시킬 수 있었다. 하지만 이는 단순한 데이터 처리에만 적용되는 이야기고, 대규모 벤치마크 데이터셋, 즉 복잡한 데이터가 늘어나면서 깊게 쌓아 올린 딥러닝 모델(Deep Neural Network)이 훨씬 성능에 유리하다.
+) ImageNet : ImageNet 데이터셋은 구성하는 이미지가 1000만개가 넘는 대규모 데이터셋으로 약 1000개의 클래스로 구성된 데이터셋이다. 자세한 데이터셋은 링크를 통해 들여다보길 바란다.
아래 사진은 실제로 몇 년에 걸쳐 Deep Neural Network의 발전을 보여주는 그래프 자료이다.
좌측 이미지는 ImageNet 데이터셋을 이용한 ILSVRC(ImageNet Large Scale Visual Recognition Challenge)국제 대회에서의 네트워크 크기(size)와 성능(error)을 보여주는 자료로, 13년도까지는 8 layer로 약 11퍼센트까지 오차(error)를 낮추었으나, 14년도에 22 layer, 15년도 ResNet이 등장하면서 152 layer까지 증가하고 Error를 대폭 줄인 것을 확인할 수 있다.
하지만, 무턱대고 layer를 마구 늘린다고 성능이 좋아지는 것은 절대 아니다. 우측 자료를 보면 56 layer를 쌓은 DNN이 20 layer의 DNN보다 error율이 더 높은 것을 알 수 있다. 이는 layer의 연산 과정 중 gradient vanishing/exploding 문제로 학습이 올바르게 이루어지지 않기 때문이며, 높은 레이어를 쌓고 좋은 성능을 내기 위해선 특별한 방법이 요구됨을 알 수 있다.
이제, 상단의 모델 중 대표적인 Deep CNN으로 VGG와 ResNet을 간단히 살펴보도록 하자.
VGG
VGG Net은 2014년에 Oxford 대학 Visual Geometry Group팀이 개발한 모델(팀명 이름 따서 VGG 인듯?)로 앞서 언급한 ILSVRC에서 준우승을 한 모델이다.(우승은 GoogleNet.) 이때, VGG는 크게 16개, 19개의 layer로 구성된 모델이 소개되었는데, 사실상 VGG Net이 등장한 14년도부터 CNN 네트워크가 크게 깊어졌다해도 과언이 아니다.
+) VGG는 현재까지도 많이 쓰이는 네트워크로, 중요한 주제이지만 본 교재(3분딥러닝_파이토치맛)에서는 언급조차 되어있지 않아 부득이하게 간단하게만 정리하고 넘어가도록 한다.
VGG의 핵심은 네트워크를 깊게 많드는것이 성능에 얼마만큼의 영향을 미치는가를 알아보는 내용이다. 그런 것에 비해 네트워크는 상당히 단순하게 생겼는데, Abstract만 읽어봐도 생각보다 좀 심플함을 알 수 있다.
여기서의 핵심은 CNN을 구성하는 Kernel Filter를 (3 x 3)로 설정하여, 깊이를 16 - 19까지 늘렸다는 것이 제일 핵심이다. 필터의 사이즈를 최대한 작게 진행하면서, 이미지를 줄여나갔기 때문에 19까지 늘릴 수 있었다고 본다.
아래 사진이 실제 VGG 16 의 아키텍쳐이다.
- Input : 224 x 224 x 3 RGB Image (ImageNet 데이터셋 기준)
- 13개의 Convolution layer + 3 FC layer
: 16개의 layer로 구성된 VGG(Pooling, dropout 계층은 훈련을 안하니, 제외)
- Conv layer = (3 x 3 filter, stride = 1, padding = True)
: 이미지 사이즈 유지를 위해 padding을 적용했으며, conv layer개수에 변화를 주어가며, 실험
- Pooling layer(2 x 2 filter, stride = 2)
: max pooling으로 진행, feature map 사이즈를 1/4로 줄임
- FC layer (4096 > 4096 > 1000) : 마지막 1000은 클래스 개수
- Padding : CNN 연산, Max pooling 계층에서 Padding으로 "same"을 부여하여, 오로지 Mas pooling과정에서만 이미지 사이즈가 절반으로 줄어들게끔 조절하였고, 이로 인하여 Convolution 연산 과정에서는 이미지 크기가 줄어들지 않았다.
+) VGG 19는 VGG 16모델에 CNN은 3개 더 얹은 형태
여기서의 가장 큰 핵심은 앞에서 말한 (3x3) Filter인데, 다른 모델(GoogleNet, AlexNet)은 (5x5), (7x7), (11x11)사이즈의 필터를 사용하여 feature map 사이즈를 줄여나갔는데 VGG에서는 작은 필터를 여러번 사용하여, 큰 필터 하나 사용했을 때의 효과와 동일한 효과를 가져왔으며, 오히려 학습해야할 파라미터 수는 줄일 수 있었다.
아래 그림을 보면 조금 더 이해하기 쉬운데, 3 x 3 필터를 써서 2번 컨볼루션을 하는 과정과 5 x 5 필터로 한번 하는게 똑같은 사이즈의 특징맵을 만드는 것을 확인할 수 있다. 그리고 실제 VGG를 구성하는 파라미터 개수를 보면 1) 3 x 3필터 3개 = 3x3x3 = 27 vs 2) 7x7필터 1개 = 7x7x1 = 49로 필터의 개수가 늘어나더라도, 학습해야 할 파라미터 개수가 오히려 적어진다는 것을 알 수 있다.
이는 학습 속도가 빠르다는 장점과 최적화하기 쉬워진다는 장점이 있으며, 작은 레이어를 여러번 쌓아 비선형 함수를 자주 거치게 되면서, 복잡한 특징들을 학습할 수 있는 능력이 증대된다는 장점이 존재한다.
이것만 보면 VGG가 굉장히 단순하고 좋아보이지만, 15년도에 ResNet이 등장하면서 Residual block으로 관심이 쏠리게 된다.
ResNet
ResNet은 15년에 나온 모델로 ILSVRC 1위를 차지하며, 152 layer이라는 엄청난 크기의 모델을 제안하며 화제가 되었다. 또한, 단순히 신경망을 깊게 쌓으면 오히려 성능이 나빠진다는 문제에 대한 해결방안을 제시하였고, 이후 등장하는 모델들에게 영향을 준 연구라 할 수 있다.
ResNet의 핵심은 Residual block인데, 아래 그림이 Residual Block의 핵심 아이디어이다.
좌측이 기존의 신경망이라면, 오른쪽에 F(x)라는 신규 라인이 생긴게 Residual block의 핵심이다. 간단하게 설명하면 그냥 이전 입력값을 출력값과 함께 더하여 보내준다(skip connection)는 차이점이 존재한다.
여기서 ResNet논문에서 강조했던 부분이 Residual learning(잔차 학습), Identity mapping(function, 항등 맵핑(함수))이 등장하는데, 이 개념이 조금 헷갈리니 아래에서 자세히 다뤄보도록 한다.
기존의 CNN은 좌측처럼 단순히 입력값(x)가 실제 목적하는 값(y)로 mapping할 수 있도록 H(x)를 얻는 과정에 집중했다. 즉, H(x) = y여야 하고, H(x)-y = 0, H(x)-y가 최소화 하는 방향으로 CNN이 feature 추출법을 학습하는 과정이라 할 수 있다.
ResNet은 이거랑 조금 다른 이야기를 언급하는데, 입력값(x)를 넣고 y를 예측할 수 있는 최적의 함수 H(x)를 찾는 과정보다, 이전에 학습했던 정보(x)를 갖고오고 여기에 추가 학습(F(x))을 진행하는게 더 쉽지 않겠냐는 의미다. 이건 다른말로 보면 F(x) + x = H(x)라 두고, 이 F(x)가 점점 작아지도록 하여 결국엔 x = H(x)가 되도록, 즉 H(x) 입력값과 동등하게 보는 identify function을 주장하게 된다. 이렇게 되면 F(x) = H(x) - x 즉, H(x)라는 값과 x간의 차이(= residual 잔차를 학습)를 줄이는 방향으로 학습하는 과정인 것이다. 이렇게 되면 H(x) = x가 되면서 아무리 학습과정에서 미분을 진행하더라도 최소 gradient 로 1 이상의 값을 갖기 때문에 gradient vanishing 문제는 해결되게 된다.
여기까지 말했을 때 조금 복잡하게 보일 수 있다. 거두절미하고 진짜 간단하게 얘기하면 그냥 이전의 연산값을 추가해서 학습하자는 얘기다. 그리고 이렇게 했을 때 깊은 신경망의 단점인 gradient vanishing가 해결된다.
이로써, ResNet 연구팀은 18, 34, 50, 101, 152개의 레이어를 쌓아가면서 성능 개선을 이룰 수 있었고, 본 교재 (3분 딥러닝 파이토치맛)에서 Deep CNN으로 이 ResNet을 예제로 들었으므로, 아래 코드를 분석해보면서 Deep CNN의 성능을 확인해보자.
ResNet 코드 리뷰
- 사용 코드
앞선 코드들과 마찬가지로 하이퍼파라미터, 주요 라이브러리 임포트 과정은 동일하다.
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import transforms, datasets, models
# GPU 사용여부 확인 코드
# DEVICE 안에 cuda(gpu)가 할당되어야 함
USE_CUDA = torch.cuda.is_available()
DEVICE = torch.device("cuda" if USE_CUDA else "cpu")
EPOCHS = 300
BATCH_SIZE = 128
여기에 CIFAR data loader를 불러오도록 한다.
# Dataloader : 데이터셋을 batch 단위로 쪼개서 학습할 때 모델의 입력으로 주는 클래스.
#CIFAR10 : 10개의 클래스, (32 * 32 *3) RGB 이미지
# Train data loader
train_loader = torch.utils.data.DataLoader(
datasets.CIFAR10('./.data',
train=True,
download=True,
transform=transforms.Compose([ # 과적합 방지를 위해 노이즈 추가(RandomCrop, RandomHorizontalFlip)
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),
(0.5, 0.5, 0.5))])),
batch_size=BATCH_SIZE, shuffle=True)
# Test data loader
test_loader = torch.utils.data.DataLoader(
datasets.CIFAR10('./.data',
train=False,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),
(0.5, 0.5, 0.5))])),
batch_size=BATCH_SIZE, shuffle=True)
바로 이어서 모델에 대한 본격적인 설계에 들어가도록 하자. 우선, 코드에 들어가기 전에 본 논문에서 구성한 모델 사진은 아래와 같다. 보이는 것처럼 크게 3개의 layer로 구성되어 있으며, layer 1 -> layer 2 , layer 2-> layer 3으로 넘어갈때마다 shortcut이 생성되는 것을 알 수 있다. 이 과정에 앞에서 말한 residual 과정이라 보면 된다.
각 layer는 BasicBlock으로 구성되어 있으며, 아래 코드가 본격적인 BasicBlock 클래스이다.
class BasicBlock(nn.Module): # ResNet을 구성하는 기본 블록
# CNN + Batch Normalization + ReLU
def __init__(self, in_planes, planes, stride=1):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)# Batch Normalization(for. 학습 안정화)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
#nn.Sequential : 여러 모듈을 하나로 묶는 역할
self.shortcut = nn.Sequential() #shortcut = Con + BN
if stride != 1 or in_planes != planes:
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, planes,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes)
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x)
out = F.relu(out)
return out
보이는 것처럼 CNN 두개, 그 사이에 학습 안정화를 위한 배치 정규화가 포함되어 정의되어 있으며, 마지막 shortcut은 이후 뒤에서 나올 stride가 1이 아닌 경우, 또한 이전의 input feature와 달라질 경우 추가되도록 정의되어 있다. forward함수는 앞에서 정의한 conv, batch norm을 같이 정의해 놓았으며, shortcut이 필요한 경우, output에 추가로 더하여 사용할 수 있도록 정의되어 있다. 이때, 앞에서 말한 조건(stride가 1이 아닌 경우, 또한 이전의 input feature와 달라질 경우)이 아니면, shortcut은 빈 nn.sequential 모듈이 되어 그냥 지나가듯 동작할 수 있다.
+) 배치 정규화(Youtube 강의 추천)란 학습률(learning rate)을 너무 높게 잡으면 기울기가 소실되거나 발산하는 증상을 예방하여 학습 과정을 안정화하는 방법. 즉, 학습 중 각 계층에 들어가는 입력을 평 균과 분산으로 정규화함으로써 학습을 효율적으로 만들어줌. 이 계층은 자체적으로 정규화를 수행해 드롭아웃과 같은 효과를 내는 장점이 있다.
이제 실제 ResNet 클래스를 보자.
class ResNet(nn.Module):
def __init__(self, num_classes=10):
super(ResNet, self).__init__()
self.in_planes = 16
self.conv1 = nn.Conv2d(3, 16, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(16) # 배치 정규화 진행
self.layer1 = self._make_layer(16, 2, stride=1)
self.layer2 = self._make_layer(32, 2, stride=2)
self.layer3 = self._make_layer(64, 2, stride=2)
'''
layer1 : 16채널에서 16채널을 내보내는 BasicBlock 2개
layer2 : 16채널을 받아 32채널을 출력하는 BasicBlock 1개와 32채널에서 32채널을 내보내는 BasicBlock 1개
layer3 : 32채널을 받아 64채널을 출력하는 BasicBlock 1개와 64채널에서 64채널을 출력하는 BasicBlock 1개
16->32, 32->64로 증폭시키는 Basic Block은 shortcut모듈 보유
'''
self.linear = nn.Linear(64, num_classes)
def _make_layer(self, planes, num_blocks, stride): # nn.Sequential로 묶어서 여러 개의 Basic Block을 하나의 모듈로 묶어주는 역할
strides = [stride] + [1]*(num_blocks-1)
layers = []
for stride in strides:
layers.append(BasicBlock(self.in_planes, planes, stride))
self.in_planes = planes
return nn.Sequential(*layers)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = F.avg_pool2d(out, 8)
out = out.view(out.size(0), -1)
out = self.linear(out)
return out
간단히 코드를 보면, 각 3종류의 layer가 make_layer라는 함수에 의해 정의되어 있고, make layer는 또다시 앞에서 정의한 여러 개의 Basicblock을 묶어 모듈화를 진행시켜주는 함수라 보면 된다.
또한, init 함수 내 3종류의 layer 중 layer 1을 제외한 나머지 2, 3은 각각 16->32로 증폭하는 블록 1개, 32 ->32를 내보내는 block한개, 32->64로 증폭하는 블록 1개, 64->64로 진행하는 블록 한개로 정의되어 있다. 이때 앞에서 언급한 조건 (stride가 1이 아닌 경우, 또한 이전의 input feature와 달라질 경우)이 바로 16->32로 증폭하는 블록 , 32->64로 증폭하는 블록 두가지의 케이스이다. 두 개의 케이스에서 shortcut 내 Conv2d와 BatchNorm이 정의됨을 알 수 있고, 이는 각 layer가 바뀔때마다 진행된다.
마지막으로 forward함수를 보면, 실제 layer의 결과값을 받고 제일 마지막 출력을 내기 위해 feature 사이즈를 변경하는 코드 out = out.view(out.size(0), -1), 과 linear layer를 거치는 모습을 볼 수 있다.
이후, 실제 코드를 돌리고 이 과정에서 이전과 다르게 최적화 함수의 학습률 감소(learning rate decay)기법을 사용하여 조금 더 정교하게 최적화할 수 있도록 설정해준다. 이 과정에는 optim.lr_scheduler.StepLR 라이브러리로 적용한다.
model = ResNet().to(DEVICE)
optimizer = optim.SGD(model.parameters(), lr=0.1,
momentum=0.9, weight_decay=0.0005)
#optim.lr_scheduler.StepLR : Scheduler가 epoch마다 호출, step_size를 50으로 지정하여,
# 50번마다 learning rate * 0.1을 수행하여, 점점 낮춤
# gamma = 0.1 로 시작, 점점 0.01, 0.001,... 형태로 낮아짐
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.1)
실제 model을 print(model)함수로 찍어보면 아래와 같다.
ResNet(
(conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential()
)
(1): BasicBlock(
(conv1): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential()
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential(
(0): Conv2d(16, 32, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential()
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(32, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential(
(0): Conv2d(32, 64, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(shortcut): Sequential()
)
)
(linear): Linear(in_features=64, out_features=10, bias=True)
)
이후 학습과 테스트 함수는 이전과 거의 동일하게 진행한다.
def train(model, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(DEVICE), target.to(DEVICE)
optimizer.zero_grad()
output = model(data)
loss = F.cross_entropy(output, target)
loss.backward()
optimizer.step()
def evaluate(model, test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(DEVICE), target.to(DEVICE)
output = model(data)
# 배치 오차를 합산
test_loss += F.cross_entropy(output, target,
reduction='sum').item()
# 가장 높은 값을 가진 인덱스가 바로 예측값
pred = output.max(1, keepdim=True)[1]
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
test_accuracy = 100. * correct / len(test_loader.dataset)
return test_loss, test_accuracy
for epoch in range(1, EPOCHS + 1):
scheduler.step()# 앞서 언급한 학습률을 낮추는 단계
train(model, train_loader, optimizer, epoch)
test_loss, test_accuracy = evaluate(model, test_loader)
print('[{}] Test Loss: {:.4f}, Accuracy: {:.2f}%'.format(
epoch, test_loss, test_accuracy))
보면 Epoch 300이 될때 약 91퍼센트의 성능을 보임을 알 수 있다.
지금은 Residual Block을 직접 설계하여 ResNet의 개념을 넣은 예제였고, 실제로 ResNet을 사용할때는 다른 벤치마크 데이터셋으로 선행학습된 모델을 많이 사용할 것이다.
그럴땐 ResNet class를 대신하여 아래와 같은 코드 torchvision 에서 ILSVRC에서 우승한 Pre-trained 모델을 가져온 후, 클래스에 맞도록 맨 마지막 layer만 바꿔주면 된다. (경우에 따라선 맨 처음 Input도 바꿔줘야 할 수 있다. )
# ================================================================== #
# 6. Pretrained model #
# ================================================================== #
# ResNet-18은 ILSVRC에서 2015년 우승한 심층 신경망 모델.
model = torchvision.models.resnet18(pretrained=True)
# 미세 조정을 위해 상단 레이어를 교체
num_ftrs = model.fc.in_features
model.fc= nn.Linear(num_ftrs, 10) # 100 = 예시
model = model.to(DEVICE)
하지만, 선행학습된 모델을 가져왔다고 다 좋은 성능을 낼거란 기대는 하지 말길.. 학습하는 데이터가 너무 단순한 데이터, 또는 형식이 다를 경우엔 오히려 이전보다 낮은 성능을 가져올 수 있다.
'머신러닝 > Pytorch 딥러닝 기초' 카테고리의 다른 글
[Pytorch-기초강의] 6. 순차적인 데이터를 처리하는 RNN (0) | 2021.05.25 |
---|---|
[Pytorch-기초강의] 5. 사람의 지도 없이 학습하는 오토인코더(AutoEncoder) (0) | 2021.05.21 |
[Pytorch-기초강의] 4. 이미지 처리 능력이 탁월한 CNN(Convolution, kernel, Padding, Pooling) (0) | 2021.04.06 |
[Pytorch-기초강의] 3. 패션 아이템을 구분하는 DNN(Overfitting, Dropout) (0) | 2021.02.15 |
[Pytorch-기초강의] 3. 패션 아이템을 구분하는 DNN(Fashion MNIST, DNN, Classification Network) (0) | 2021.02.05 |