왜 CNN이 막강한가?
CNN은 이미지 분류, 객체 탐지, 객체 분할, 동영상 처리, 자연어 처리, 음성 인식 등 까다로운 문제를 푸는 데 가장 강력한 머신러닝 모델 중 하나이다.
- 가중치 공유 : CNN은 가중치를 공유함으로써 매개변수를 효과적으로 활요한다. 즉 동일한 가중치 또는 매개변수로 다양한 특징을 추출한다. 특징은 모델이 매개변수를 사용해 생성하는 입력 데이터의 고수준 표현이다.
- 자동 특징 추출 : 특징 추출 단계를 여럿 둠으로써 CNN은 데이터 셋에서 자동으로 특징 표현을 학습할 수 있다.
- 계층적 학습: 여러 계층으로 구성된 CNN 구조 덕분에 CNN은 저수준부터 고수준까지의 특징을 학습할 수 있다.
- 동영상 처리 작업처러므 데이터에서 공간적 혹은 시간적 상관관계를 탐색할 수 있다.
- 경사 소실 문제(Gradient Vanishing)를 극복하기 위해 ReLU 같은 더 나은 활성화 함수(activate function)와 손실 함수(Loss function)을 사용한다.
- 매개변수 최적화: 단순한 확률적 경사 하강법 대신 적응형 모멘트 추정(Adam, Adaptive Momentum) 기법에 기반한 Optimizer 등을 사용한다
- 정칙화 : L2 정칙화 외에 드롭아웃과 배치 정규화를 적용한다.
- 공간 탐색 기반 CNN : 입력 데이터에서 다양한 수준의 시각적 특징을 탐색하기 위해 다양한 커널 크기를 사용하는 것을 기본 아이디어로 삼는다.
- 깊이 기반 CNN : 깊이란 신경망 깊이, 즉 계층 수를 말한다. 따라서 여기서는 고도로 복합적인 시각 특징을 추출하기 위해 여러 개의 합성곱 계층을 두어 CNN 모델을 생성한다.
너비 기반 CNN : 너비는 데이터에서 채널이나 특징 맵 개수, 또는 데이터로부터 추출된 특징 개수를 말한다. 따라서 너비 기반 CNN은다음 그림에 나온 것처럼 입력 계층에서 출력 계층으로 이동할 때 특징 맵 개수를 늘린다.
다중 경로 기반 CNN : 지금까지 앞선 세 가지 유형의 아키텍처는 계층 간 단조롭게 연결돼 있다. 즉, 연이은 계층 사이에 직접 연결만 존재한다. 다중 경로 기반 CNN은 연이어 있지 않은 계층 간 숏컷 연결(shortcut connections) 또는 스킵 연결(skip connections) 등의 방식을 채택한다.
다중 경로 아키텍처의 핵심 장점은 스킵 연결 덕분에 여러 계층에 정보가 더 잘 흐르게 된다는 것이다. 이는 또한 너무 많은 손실 없이 경사가 입력 계층으로 다시 흐르도록 한다.
경사 소실 문제(Gradient Vanishing)란 무엇일까?
신경망에서 역전파는 미분의 연쇄 법칙을 기반으로 작동한다. 연쇄 법칙에 따르면 입력 계층 매개변수에 대한 손실 함수의 경사는 각 계층의 경사의 곱으로 나타낼 수 있다. 이 경사가 모두 1보다 작고, 게다가 0을 향하는 경향이 있는 경우 이 경사의 곱은 사라질 정도로 작은 값이 된다. 경사 소실 문제는 네트워크 매개변수의 값을 변경할 수 없게 만들어 최적화 프로세스에 심각한 문제를 일으키고 학습을 저해한다.
LeNet을 처음부터 구현하기
LeNet-5의 숫자 5는 이 모델의 전체 계층 수, 즉 2개의 합성곱 게층과 3개의 완전 연결 계층을 나타낸다.
- 라이브러리를 임포트
1
2
3
4
5
6
7
8
9
10
import numpy as np
import matplotlib.pyplot as plt
import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
torch.manual_seed(55)
- 모델 아키텍쳐를 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
# 3 input image channel, 6 output feature maps and 5x5 conv kernel
self.cn1 = nn.Conv2d(3, 6, 5)
# 6 input image channel, 16 output feature maps and 5x5 conv kernel
self.cn2 = nn.Conv2d(6, 16, 5)
# fully connected layers of size 120, 84 and 10
self.fc1 = nn.Linear(16 * 5 * 5, 120) # 5*5 is the spatial dimension at this layer
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
# Convolution with 5x5 kernel
x = F.relu(self.cn1(x))
# Max pooling over a (2, 2) window
x = F.max_pool2d(x, (2, 2))
# Convolution with 5x5 kernel
x = F.relu(self.cn2(x))
# Max pooling over a (2, 2) window
x = F.max_pool2d(x, (2, 2))
# Flatten spatial and depth dimensions into a single vector
x = x.view(-1, self.flattened_features(x))
# Fully connected operations
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
def flattened_features(self, x):
# all except the first (batch) dimension
size = x.size()[1:]
num_feats = 1
for s in size:
num_feats *= s
return num_feats
lenet = LeNet()
print(lenet)
"""
output:
LeNet(
(cn1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
(cn2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)
"""
- 훈련 루틴, 즉 실제 역전파 단계를 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def train(net, trainloader, optim, epoch):
# initialize loss
loss_total = 0.0
for i, data in enumerate(trainloader, 0):
# get the inputs; data is a list of [inputs, labels]
# ip refers to the input images, and ground_truth refers to the output classes the images belong to
ip, ground_truth = data
# 매개변수인 경사를 0으로 설정
optim.zero_grad()
# forward pass + backward pass + optimization step
op = net(ip)
loss = nn.CrossEntropyLoss()(op, ground_truth)
loss.backward()
optim.step()
# update loss
loss_total += loss.item()
# print loss statistics
if (i+1) % 1000 == 0: # print at the interval of 1000 mini-batches
print('[Epoch number : %d, Mini-batches: %5d] loss: %.3f' %
(epoch + 1, i + 1, loss_total / 200))
loss_total = 0.0
- 모델 성능을 평가하기 위해 사용되는 테스트 루틴 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
def test(net, testloader):
success = 0
counter = 0
with torch.no_grad():
for data in testloader:
im, ground_truth = data
op = net(im)
_, pred = torch.max(op.data, 1)
counter += ground_truth.size(0)
success += (pred == ground_truth).sum().item()
print('LeNet accuracy on 10000 images from test dataset: %d %%' % (
100 * success / counter))
AlexNet 모델 미세 조정하기
AlexNet은 LeNet 모델의 아키텍처를 증가시켜 만든 후속 모델이다. AlexNet 모델은 8개의 계층(5개 합성곱 계층, 3개 완전 연결 계층)에 6천만 개 모델 매개변수를 사용하고 최대 풀링 방식을 썼다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class AlexNet(nn.Module):
def __init__(self, number_of_classes):
super(AlexNet, self).__init__()
self.feats = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=11, stride=4, padding=5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.Conv2d(in_channels=64, out_channels=192, kernel_size=5, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.Conv2d(in_channels=192, out_channels=384, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,stride=2),
)
self.clf = nn.Linear(in_features=256,out_features=number_of_classes)
def foward(self, inp):
op = self.feats(inp):
op = op.view(op.size(0),-1)
op = self.clf(op)
return op
GoogLeNet과 Inception v3 살펴보기
- Inception 모듈 - 여러 병렬 합성곱 계층으로 구성된 모듈
- 모델 매개변수 개수를 줄이기 위해 1*1 합성곱을 사용
- 완전 연결 계층 대신 전역 평균 풀링을 사용해 과적합을 줄입
- 훈련 시 정착화 및 경사 안정성을 위해 보조 분류기(auxiliary classifier)를 사용
Inception Moudle 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import torch.nn as nn
class InceptionModule(nn.Module):
def __init__(self, input_planes, n_channels1x1, n_channels3x3red, n_channels3x3, n_channels5x5red, n_channels5x5, pooling_planes):
super(InceptionModule, self).__init__()
# 1x1 convolution branch
self.block1 = nn.Sequential(
nn.Conv2d(input_planes, n_channels1x1, kernel_size=1),
nn.BatchNorm2d(n_channels1x1),
nn.ReLU(True),
)
# 1x1 convolution -> 3x3 convolution branch
self.block2 = nn.Sequential(
nn.Conv2d(input_planes, n_channels3x3red, kernel_size=1),
nn.BatchNorm2d(n_channels3x3red),
nn.ReLU(True),
nn.Conv2d(n_channels3x3red, n_channels3x3, kernel_size=3, padding=1),
nn.BatchNorm2d(n_channels3x3),
nn.ReLU(True),
)
# 1x1 conv -> 5x5 conv branch
self.block3 = nn.Sequential(
nn.Conv2d(input_planes, n_channels5x5red, kernel_size=1),
nn.BatchNorm2d(n_channels5x5red),
nn.ReLU(True),
nn.Conv2d(n_channels5x5red, n_channels5x5, kernel_size=3, padding=1),
nn.BatchNorm2d(n_channels5x5),
nn.ReLU(True),
nn.Conv2d(n_channels5x5, n_channels5x5, kernel_size=3, padding=1),
nn.BatchNorm2d(n_channels5x5),
nn.ReLU(True),
)
# 3x3 pool -> 1x1 conv branch
self.block4 = nn.Sequential(
nn.MaxPool2d(3, stride=1, padding=1),
nn.Conv2d(input_planes, pooling_planes, kernel_size=1),
nn.BatchNorm2d(pooling_planes),
nn.ReLU(True),
)
def forward(self, ip):
op1 = self.block1(ip)
op2 = self.block2(ip)
op3 = self.block3(ip)
op4 = self.block4(ip)
return torch.cat([op1,op2,op3,op4], 1)
1x1 합성곱
Inception 모듈의 병렬 합성곱 계층 외에 각 병렬 계층의 맨 앞에는 1x1 합성곱 계층이 있다. 이것을 사용하는 이유는 차원 축소에 있다. 1x1 합성곱 계층은 이미지 표현의 넓이와 높이를 변경하지 않지만 이미지 표현의 깊이를 바꿀 수 있다. 이 기법은 1x1, 3x3, 5x5 합성곱을 병렬로 수행하기 전에 입력 시각 특징의 깊이를 축소하는데 사용된다.
전역 평균 풀링
GoogleLeNet 아키텍처를 보면, 모델 끝에서 두 번째 출력 계층 앞에 7x7 평균 풀링 계층이 있다. 이 계층은 다시 모델의 매개변수 개수를 줄이는데 도움이 되어 과적합을 줄인다. 이 계층이 없으면 모델은 완전 연결 계층의 조밀한 연결로 인해 수백만 개의 추가 매개변수를 갖게 된다.
보조 분류기
보조 분류기는 특히 입력에 가까운 계층인 경우, 역전파하는 동안 경사의 크기를 더함으로써 경사가 소실되는 문제를 해결해준다. 이러한 모델에는 계층이 많아서 경사가 소실되면 병목 현상이 발생할 수 있다. 따라서 보조 분류기를 사용하는 것이 이 22개 계층을 갖는 심층 모델에 유용한 것으로 입증됐다. 또한 보조 분류 분기는 정칙화에도 도움이 된다. 예측하는 동안에는 이 보조 분기가 꺼지거나 폐기된다.
GoogLeNet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class GoogLeNet(nn.Module):
def __init__(self):
super(GoogLeNet, self).__init__()
self.stem = nn.Sequential(
nn.Conv2d(3, 192, kernel_size=3, padding=1),
nn.BatchNorm2d(192),
nn.ReLU(True),
)
self.im1 = InceptionModule(192, 64, 96, 128, 16, 32, 32)
self.im2 = InceptionModule(256, 128, 128, 192, 32, 96, 64)
self.max_pool = nn.MaxPool2d(3, stride=2, padding=1)
self.im3 = InceptionModule(480, 192, 96, 208, 16, 48, 64)
self.im4 = InceptionModule(512, 160, 112, 224, 24, 64, 64)
self.im5 = InceptionModule(512, 128, 128, 256, 24, 64, 64)
self.im6 = InceptionModule(512, 112, 144, 288, 32, 64, 64)
self.im7 = InceptionModule(528, 256, 160, 320, 32, 128, 128)
self.im8 = InceptionModule(832, 256, 160, 320, 32, 128, 128)
self.im9 = InceptionModule(832, 384, 192, 384, 48, 128, 128)
self.average_pool = nn.AvgPool2d(7, stride=1)
self.fc = nn.Linear(4096, 1000)
def forward(self, ip):
op = self.stem(ip)
out = self.im1(op)
out = self.im2(op)
op = self.maxpool(op)
op = self.a4(op)
op = self.b4(op)
op = self.c4(op)
op = self.d4(op)
op = self.e4(op)
op = self.max_pool(op)
op = self.a5(op)
op = self.b5(op)
op = self.avgerage_pool(op)
op = op.view(op.size(0), -1)
op = self.fc(op)
return op
모델을 인스턴스화하는 것 외에도 코드 단 두 줄로 사전 훈련된 GoogLeNet을 로딩할 수 있다.
1
2
import torchvision.models as models
model = models.googlenet(pretrained=True)
ResNet과 DenseNet 아키텍처
ResNet은 스킵 연결 개념을 도입했다. 이 기법은 매개변수가 넘쳐나는 것과 경사가 소실되는 문제를 모두 해결한다. 입력은 먼저 비선형 변환(합성곱 다음에 비선형 활성화)을 통과한 다음 이 변환의 출력(잔차)을 원래 입력에 더한다. 이러한 계산이 포함된 각 블록을 잔차 블록(residual block)이라고 하며, 잔차 네트워크 또는 ResNet은 이 이름에서 비롯됐다.
ResNet 아키텍처에는 합성곱 블록(convolutional block)과 항등 블록(identity block), 두 종류의 잔차 블록이 있다. 이 두 블록 모두 스킵 연결이 있다. 합성곱 블록에는 1x1 합성곱 계층이 추가되어 차원을 축소하는데 도움이 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch.nn as nn
class BasicBlock(nn.Module):
multiplier=1
def __init__(self, input_num_planes, num_planes, strd=1):
super(BasicBlock, self).__init__()
self.conv_layer1 = nn.Conv2d(in_channels=input_num_planes, out_channels=num_planes, kernel_size=3, stride=stride, padding=1, bias=False)
self.batch_norm1 = nn.BatchNorm2d(num_planes)
self.conv_layer2 = nn.Conv2d(in_channels=num_planes, out_channels=num_planes, kernel_size=3, stride=1, padding=1, bias=False)
self.batch_norm2 = nn.BatchNorm2d(num_planes)
self.res_connnection = nn.Sequential()
if strd > 1 or input_num_planes != self.multiplier*num_planes:
self.res_connnection = nn.Sequential(
nn.Conv2d(in_channels=input_num_planes, out_channels=self.multiplier*num_planes, kernel_size=1, stride=strd, bias=False),
nn.BatchNorm2d(self.multiplier*num_planes)
)
def forward(self, inp):
op = F.relu(self.batch_norm1(self.conv_layer1(inp)))
op = self.batch_norm2(self.conv_layer2(op))
op += self.res_connnection(inp)
op = F.relu(op)
return op
ResNet을 빠르게 시작하려면 파이토치 레파지토리에서 사전 훈련된 ResNet 모델을 사용하면 된다.
1
2
3
import torchvision.models as models
model = models.resnet50(pretrained=True)
ResNet은 역전파하는 동안 경사를 보존하기 위해 항등 함수를 사용한다.
DenseNet
DenseNet은 밀집 블록(dense block) 내의 모든 합성곱 계층이 서로 연결된다. 게다가 모든 밀집 블록은 DenseNet 전체 안에서 다른 밀집 블륵과 모두 연결된다. 밀집 블록은 3x3으로 밀집 연결된 합성곱 계층 두 개로 구성된 모듈이다.
이렇게 밀집 연결하면 모든 계층은 네트워크에서 자기보다 앞선 계층 저체로부터 정보를 받는다. 이로써 마지막 계층에서 제일 처음에 위치한 계층까지 경사값을 크게 유지하며 흐를 수 있다.
DenseNet 아키텍처에는 밀집 블록과 전환 블록의 두 가지 유형의 블록이 포함된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch.nn as nn
class DenseBlock(nn.Module):
def __init__(self, input_num_planes, rate_inc):
super(DenseBlock, self).__init__()
self.batch_norm1 = nn.BatchNorm2d(input_num_planes)
self.conv_layer1 = nn.Conv2d(in_channels=input_num_planes, out_channels=4*rate_inc, kernel_size=1, bias=False)
self.batch_norm2 = nn.BatchNorm2d(4*rate_inc)
self.conv_layer2 = nn.Conv2d(in_channels=4*rate_inc, out_channels=rate_inc, kernel_size=3, padding=1, bias=False)
def forward(self, inp):
op = self.conv_layer1(F.relu(self.batch_norm1(inp)))
op = self.conv_layer2(F.relu(self.batch_norm2(op)))
op = torch.cat([op,inp], 1)
return op
class TransBlock(nn.Module):
def __init__(self, input_num_planes, output_num_planes):
super(TransBlock, self).__init__()
self.batch_norm = nn.BatchNorm2d(input_num_planes)
self.conv_layer = nn.Conv2d(in_channels=input_num_planes, out_channels=output_num_planes, kernel_size=1, bias=False)
def forward(self, inp):
op = self.conv_layer(F.relu(self.batch_norm(inp)))
op = F.avg_pool2d(op, 2)
return op
1
2
3
4
5
6
import torchvision.models as models
densenet121 = models.densenet121(pretrained=True)
densenet161 = models.densenet161(pretrained=True)
densenet169 = models.densenet169(pretrained=True)
densenet201 = models.densenet201(pretrained=True)