딥러닝 이론 : 4.2 Modern CNN

milkbuttercheese·2023년 2월 1일
0

Dive_into_Deeplearning

목록 보기
5/7
post-custom-banner

LeNet

  • 다음과 같은 구조를 갖는 네트워크이다
  • 이는 2개의 합성곱 층으로 이루어진 Convolutional Block와 3개의 fully-connected layer로 구성된 Dense Block 층으로 나뉘어진다
  • Convolutional Block
    - 6개 채널 ->16개의 채널
    - 5×55 \times 5 , padding=2 커널
    - 시그모이드 활성화 함수
    - 2×22 \times 2 , strides=2의 평균-풀링 (현재 ReLU가 더 좋은 성능임을 알고 있지만, 이 모델이 개발될 당시에 ReLU가 개발되지 않았음)
  • Dense Block
    - Dense Block에 데이터가 입력되기 위하여, 직전의 출력값을 flatten으로 만든다

LeNet 구현

CNN 초기화 함수

def init_cnn(module):  #@save
    """Initialize weights for CNNs."""
    if type(module) == nn.Linear or type(module) == nn.Conv2d:
        nn.init.xavier_uniform_(module.weight)

LeNet 클래스

class LeNet(d2l.Classifier):  #@save
    def __init__(self, lr=0.1, num_classes=10):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential(
            nn.LazyConv2d(6, kernel_size=5, padding=2), nn.Sigmoid(),
            nn.AvgPool2d(kernel_size=2, stride=2),
            nn.LazyConv2d(16, kernel_size=5), nn.Sigmoid(),
            nn.AvgPool2d(kernel_size=2, stride=2),
            nn.Flatten(),
            nn.LazyLinear(120), nn.Sigmoid(),
            nn.LazyLinear(84), nn.Sigmoid(),
            nn.LazyLinear(num_classes))

네트워크 구조 요약 메소드

@d2l.add_to_class(d2l.Classifier)  #@save
def layer_summary(self, X_shape):
    X = torch.randn(*X_shape)
    for layer in self.net:
        X = layer(X)
        print(layer.__class__.__name__, 'output shape:\t', X.shape)

model = LeNet()
model.layer_summary((1, 1, 28, 28))

"""result)
Conv2d output shape:         torch.Size([1, 6, 28, 28])
Sigmoid output shape:        torch.Size([1, 6, 28, 28])
AvgPool2d output shape:      torch.Size([1, 6, 14, 14])
Conv2d output shape:         torch.Size([1, 16, 10, 10])
Sigmoid output shape:        torch.Size([1, 16, 10, 10])
AvgPool2d output shape:      torch.Size([1, 16, 5, 5])
Flatten output shape:        torch.Size([1, 400])
Linear output shape:         torch.Size([1, 120])
Sigmoid output shape:        torch.Size([1, 120])
Linear output shape:         torch.Size([1, 84])
Sigmoid output shape:        torch.Size([1, 84])
Linear output shape:         torch.Size([1, 10])
"""

학습

trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128)
model = LeNet(lr=0.1)
model.apply_init([next(iter(data.get_dataloader(True)))[0]], init_cnn)
trainer.fit(model, data)

AlexNet : Deep Convolutional Network

  • 좌 : LeNet, 우 :AlexNet
  • Convolutional Block
    - 11×1111 \times 11 strides=4 Conv
    - 3×33 \times 3 strides=2 NaxPool
    - 5×55 \times 5 padding=2 Conv
    - 3×33\times 3 stride=2 MaxPool
    - 3×33 \times 3 paddding=1 Conv 레이어 3개
    - 3×33 \times 3 stride=2 MaxPool
    - 이때 활성화함수로 simpler ReLU를 사용하였다
  • Fully-Connected Block
    - 4096 MLP, nn.dropout(p=0.5) 2개
    - 1000 MLP, nn.dropout(p=0.5): 아웃풋 레이어
  • 통상적으로 1GB 정도의 모델 파라미터 용량이 요구된다

VGG : Visual Geomery Grup

Networks Using Blocks

  • 좌 : AlexNet, 중앙: VGG의 한 블록, 우:VGG
  • CNN의 빌딩 블록은 다음의 시퀀스를 따른다
    1. Convolution layer가 Padding을 통하여 해상도(이미지 사이즈)를 유지한채로 convolution 하는 것
    2. ReLU등의 활성화 함수를 통해 비선형성이 부여되는것
    3. Pooling Layer를 통하여 해상도(이미지 사이즈)가 축소되는 것
    - 이러한 방식의 문제점은 공간적 해상도spatial resolution이 급격하게 줄어드는 것에 문제가 있다
  • VGG의 핵심아이디어는 다운샘플링(해상도를 줄이는 것)을 하나의 블록형태로 하는 여러개의 Convolution layer를 활용하여 하는 것이다.
    -
    - 연속 3×33 \times 3 의 Convolution은 단일 5×55 \times 5 Convolution과 같이 하나의 출력층 픽셀에 대응되는 입력층 픽셀영역의 크기가 같다
    - 그러면서 동시에 전자의 파라미터는 (2×32×c2)(2 \times 3 ^{2} \times c ^{2}) 인데 반해, 후자의 파라미터수는 (52×c2)(5 ^{2} \times c ^{2}) 이다. 즉 연산상의 이점이 존재하는 것이다
    - 또한 각 필터 사이사이 활성화함수를 삽입함으로써 비선형성을 높여, 모델의 식별능력을 향상시킬 수 있다
    - VGG Block 코드
def vgg_block(num_convs, out_channels):
    layers = []
    for _ in range(num_convs):
        layers.append(nn.LazyConv2d(out_channels, kernel_size=3, padding=1))
        layers.append(nn.ReLU())
    layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
    return nn.Sequential(*layers)

VGG 네트워크 구조

  • Convolution Block
    - 위 그림과 같이 구성된다
  • Fully-Connected Block : AlexNet과 동일함
    - 4096 MLP, nn.dropout(p=0.5) 2개
    - 1000 MLP, nn.dropout(p=0.5): 아웃풋 레이어
  • VGG 코드
class VGG(d2l.Classifier):
    def __init__(self, arch, lr=0.1, num_classes=10):
        super().__init__()
        self.save_hyperparameters()
        conv_blks = []
        """
        arch에서 받아온 convolution수,channel수 정보를 활용하여
        Convolution Block의 구조가 설계된다.  
        Convolution 레이어는 kernel_size=3 padding=1으로,
        MaxPool 레이어는 kernel_size=2, stride=2로 구성된다
        """
        for (num_convs, out_channels) in arch:
            conv_blks.append(vgg_block(num_convs, out_channels))
        self.net = nn.Sequential(
            *conv_blks, nn.Flatten(),
            """Fully-Connected Block 영역
            마지막 아웃풋은 num_classes로 받는다"""
            nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(0.5),
            nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(0.5),
            nn.LazyLinear(num_classes))
        self.net.apply(d2l.init_cnn)
  • VGG-11 모델
    - 기본 MLP 블록 3층에 Convolution BLock(1+1+2+2+2)가 추가되어 11층으로 구성된 네트워크이다
model = VGG(arch=((1, 16), (1, 32), (2, 64), (2, 128), (2, 128)), lr=0.01)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
model.apply_init([next(iter(data.get_dataloader(True)))[0]], d2l.init_cnn)
trainer.fit(model, data)

Network in Network

  • LeNet, AlexNet, VGG는 공간 구조속에서 합성곱,풀링 과정을 통하여 피처를 추출한다는 공통점을 갖고 있다.
  • 기본 구조들은 두가지의 문제점을 갖고 있다
    - 첫번째론 마지막 Fully-connected 블록 영역이 엄청난 수의 파라미터를 갖고 있다는 것이다. 단순한 VGG-11 조차 25088×409625088 \times 4096 개의 파라미터가 존재하여, single precision 한번당 400MB의 램을 사용한다. 이는 연산에 엄창난 부담이 되어, 모바일이나 기타 임베디드 장치에서 구동을 어렵게 한다
    - 또한 비선형성을 증가시키기 위해 Fully-connected 레이어를 추가하는 것 또한 어렵다. 이는 공간 구조를 파괴하고, 더 많은 메모리가 필요하게 될 것이기 때문이다
  • NiN : Network in Network 블록은 다음과 같은 전략으로 위 문제를 해결한다
    1. 채널 활성화 사이 1×11\times 1 합성곱을 활용하여, 비선형성을 증가시킨다
    2. 마지막 표현 계층에 Global average pooling을 사용한다.

NiN Blocks

  • 합성곱 층의 입력값과 출력값은 4차원 텐서 (데이터포인트 순서,채널,높이,폭) 으로 존재한다
  • 또한 완전연결 층의 입력값과 출력값은 데이터포인트 순서와 피처로 이루어진 2차원 텐서로 존재한다
  • 이 개념으로 부터 발생된 NiN의 아이디어는 각 픽셀에다 1×11 \times 1 convolution 을 시행하는 것이다
  • NiN 블록 코드
import torch
from torch import nn
from d2l import torch as d2l


def nin_block(out_channels, kernel_size, strides, padding):
    return nn.Sequential(
        nn.LazyConv2d(out_channels, kernel_size, strides, padding), nn.ReLU(),
        nn.LazyConv2d(out_channels, kernel_size=1), nn.ReLU(),
        nn.LazyConv2d(out_channels, kernel_size=1), nn.ReLU())
  • NiN 모델 코드
class NiN(d2l.Classifier):
    def __init__(self, lr=0.1, num_classes=10):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential(
            nin_block(96, kernel_size=11, strides=4, padding=0),
            nn.MaxPool2d(3, stride=2),
            nin_block(256, kernel_size=5, strides=1, padding=2),
            nn.MaxPool2d(3, stride=2),
            nin_block(384, kernel_size=3, strides=1, padding=1),
            nn.MaxPool2d(3, stride=2),
            nn.Dropout(0.5),
            nin_block(num_classes, kernel_size=3, strides=1, padding=1),
            nn.AdaptiveAvgPool2d((1, 1)),
            nn.Flatten())
        self.net.apply(d2l.init_cnn)
  • 모델 요약
NiN().layer_summary((1, 1, 224, 224))
"""result)
Sequential output shape:     torch.Size([1, 96, 54, 54])
MaxPool2d output shape:      torch.Size([1, 96, 26, 26])
Sequential output shape:     torch.Size([1, 256, 26, 26])
MaxPool2d output shape:      torch.Size([1, 256, 12, 12])
Sequential output shape:     torch.Size([1, 384, 12, 12])
MaxPool2d output shape:      torch.Size([1, 384, 5, 5])
Dropout output shape:        torch.Size([1, 384, 5, 5])
Sequential output shape:     torch.Size([1, 10, 5, 5])
AdaptiveAvgPool2d output shape:      torch.Size([1, 10, 1, 1])
Flatten output shape:        torch.Size([1, 10])
"""
  • 학습
model = NiN(lr=0.05)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
model.apply_init([next(iter(data.get_dataloader(True)))[0]], d2l.init_cnn)
trainer.fit(model, data)

Ref. 1x1 합성곱 효과

  • 채널의 수를 줄여 연산량을 줄이는데 활용된다
  • 연산량이
    - 위 같은 경우 480×5×5×48×14×14112.9M480\times 5 \times 5 \times 48 \times 14 \times 14\sim 112.9M 이고
    - 아래의 경우 (16×1×1×480×14×14)+(5×5×48×16×14×14)5.3M(16 \times 1 \times 1 \times 480 \times 14 \times 14)+(5\times 5 \times 48 \times 16 \times 14\times 14) \sim 5.3M

GoogLeNet : Multi-Branch Network

  • CNN에서 stem(data ingest), body(data processing), head(prediction) 3개 구조로 구분되는 최초의 네트워크 구조이다
    - stem은 2~3개의 합성곱망으로 이루어져 이미지 내에 있는 low-level 특징들을 추출한다
    - body는 convolutional blocks으로 구성되어 stem의 과정을 이어 받는다
    - head는 위 과정에서 추출된 feature를 활용하여classification,segemnetation,detection,tracking problem 등의 문제를 해결한다

Inception Block

  • "We need to go deeper" 라는 대사가 나온 인셉션의 명칭을 따와 인셉션 블록이라 명명되었다
  • 그림에서 묘사되었듯이 인셉션 블록은 4개의 평행한 브랜치로 구성되어 있다.
    - 1번째 브랜치는 1×11 \times 1 합성곱 레이어이다
    - 2번째 브랜치는 1×11\times 1 합성곱 레이어 후 padding=1 인 3×33 \times 3 합성곱을 통하여 크기가 보존된다
    - 3번째 브랜치는 1×11 \times 1 합성곱 레이어 후 padding=2인 5×55 \times 5 인 합성곱을 통하여 크기가 보존된다
    - 4번째 브랜치는 padding=1 인 3×33 \times 3 MaxPool 후 1×11 \times 1 합성곱을 통하여 크기가 보존된다
    - 즉 모든 브랜치는 height,width의 크기를 보존하며, 1×11 \times 1 합성곱을 통하여 채널의 수만이 변경된다. 그 후 각 브랜치의 피처맵 채널차원을 따라 합친다concatenate
  • 왜 GoogLeNet은 1×11 \times 1 Convolution을 사용하는가?
    -
    1x1 filter 128개 적용했을 때 : 28x28x128 -> parameter 연산 : 28x28x128x1x1x256
    3x3 filter 192개 적용했을 때 : 28x28x192 -> parameter 연산 : 28x28x192x3x3x256**
    5x5 filter 96개 적용했을 때 : 28x28x96 -> parameter 연산 : 28x28x96x5x5x256*
    Maxpooling 적용했을 때 : 28x28x256
    이를 concat하면 28x28x(128+192+96+256)이 된다. 이 때 파라미터연산을 모두 더하면 854M 개로 매우 많게 정도 나온다.
    -
    	-  ![](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBrb0r%2FbtrcM1G9gYV%2FKp0MqbXQ2HZ9haKx5LYIpk%2Fimg.png)
    	- 이렇게 보이듯이 $1 \times 1$ 합성곱은 연산량을 줄이는 효과를 갖고 있다. GoogLeNet에선 대략 절반 이상의 파라미터 수를 줄이는 연산상의 이득을 제공하였다
    	- 이렇게 $1 \times 1$ 합성곱을 통해 채널수를 줄인뒤, 다시 채널을 늘리는 형태를 Bottleneck Layer라고 부른다
    	- [ref.](https://bigdata-analyst.tistory.com/290?category=953758)
  • GoogLeNet Inception 블록 클래스
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


class Inception(nn.Module):
    # `c1`--`c4` are the number of output channels for each branch
    def __init__(self, c1, c2, c3, c4, **kwargs):
        super(Inception, self).__init__(**kwargs)
        # Branch 1
        self.b1_1 = nn.LazyConv2d(c1, kernel_size=1)
        # Branch 2
        self.b2_1 = nn.LazyConv2d(c2[0], kernel_size=1)
        self.b2_2 = nn.LazyConv2d(c2[1], kernel_size=3, padding=1)
        # Branch 3
        self.b3_1 = nn.LazyConv2d(c3[0], kernel_size=1)
        self.b3_2 = nn.LazyConv2d(c3[1], kernel_size=5, padding=2)
        # Branch 4
        self.b4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.b4_2 = nn.LazyConv2d(c4, kernel_size=1)

    def forward(self, x):
        b1 = F.relu(self.b1_1(x))
        b2 = F.relu(self.b2_2(F.relu(self.b2_1(x))))
        b3 = F.relu(self.b3_2(F.relu(self.b3_1(x))))
        b4 = F.relu(self.b4_2(self.b4_1(x)))
        return torch.cat((b1, b2, b3, b4), dim=1)
  • GoogLeNet 모델
    -
  • 구조
    1. 7×77 \times 7 합성곱 층
    2. 3×33 \times 3 MaxPool 층
    3. 1×11\times 1 합성곱 층
    4. 3×33\times 3 합성곱 층
    5. 3×33\times 3 합성곱 층
    6. 인셉션 층 2개
    7. 3×33 \times 3 MaxPool 층
    8. 인셉션 층 5개
    9. 3×33 \times 3 MaxPool 층
    10. 인셉션 층 2개
    11. Global AvgPool 층
    12. Fully-connected 층
class GoogleNet(d2l.Classifier):
	"""1.channel=64, padding=3 stride=2 kernel_size=7 합성곱층 
	   2.padding=1 stride=2 kernel_size=3 MaxPool층""" 
    def b1(self):
        return nn.Sequential(
            nn.LazyConv2d(64, kernel_size=7, stride=2, padding=3),
            nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

@d2l.add_to_class(GoogleNet)
def b2(self):
	"""4.channel=64 kernel_size=1 합성곱층 
	   5.channel=192 padding=1 kernel_size=3 합성곱층 
	   6.padding=1 stride=2 kernel_size=3 MaxPool층""" 
    return nn.Sequential(
        nn.LazyConv2d(64, kernel_size=1), nn.ReLU(),
        nn.LazyConv2d(192, kernel_size=3, padding=1), nn.ReLU(),
        nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

@d2l.add_to_class(GoogleNet)
def b3(self):
	"""7.인셉션 레이어 2개
	   8.padding=1 stride=2 kernel_size=3 MaxPool층""" 
    return nn.Sequential(Inception(64, (96, 128), (16, 32), 32),
                         Inception(128, (128, 192), (32, 96), 64),
                         nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

@d2l.add_to_class(GoogleNet)
def b4(self):
	"""9.인셉션 레이어 5개
	   10.padding=1 stride=2 kernel_size=3 MaxPool층""" 
    return nn.Sequential(Inception(192, (96, 208), (16, 48), 64),
                         Inception(160, (112, 224), (24, 64), 64),
                         Inception(128, (128, 256), (24, 64), 64),
                         Inception(112, (144, 288), (32, 64), 64),
                         Inception(256, (160, 320), (32, 128), 128),
                         nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
                         
@d2l.add_to_class(GoogleNet)
def b5(self):
	"""11.인셉션 레이어 2개
	   12.AdaptiveAvgPool 층 후 Flatten""" 
    return nn.Sequential(Inception(256, (160, 320), (32, 128), 128),
                         Inception(384, (192, 384), (48, 128), 128),
                         nn.AdaptiveAvgPool2d((1,1)), nn.Flatten())

@d2l.add_to_class(GoogleNet)
	"""12.num_class 만큼의 출력노드를 갖는 Fully-Connected 층"""
def __init__(self, lr=0.1, num_classes=10):
    super(GoogleNet, self).__init__()
    self.save_hyperparameters()
    self.net = nn.Sequential(self.b1(), self.b2(), self.b3(),self.b4(), self.b5(), nn.LazyLinear(num_classes))
    self.net.apply(d2l.init_cnn)

배치 정규화 Batch Normalization

  • 후술될 layer normalization이 각 관측치 단위로 정규화를 하는데 반해 batch normalization은 각 채널 단위로 이루어지는 것이다 ref

  • 벡터를 정규화하는 것은 함수의 복잡도를 제한하는 것과 더불어 파라미터를 비슷한 스케일로 만들어주는 효과를 갖는다

  • MLP 또는 CNN에서 중간층의 변수들은 다양한 범위의 값을 가질 수 있다. 배치 정규화는 이러한 배치의 분포하는 경향성이 네트워크 수렴에 방해가 될수 있다는 가정에서 만들어졌다
    - 만약 이전 레이어에 비해 현재 레이어의 변수값이 100배정도 크다고 한다면, 학습률에 대하여 적절한 조정이 필요할 것이라고 직관적으로 알 수 있을 것이다.
    - Adam이나 AdaGrad, Yogi등은 이러한 연유로 제작된 optimizer이다. 그러나 대안책으로 정규화를 통해서도 이러한 문제를 예방할 수 있을 것이다

  • 네트워크가 더 복잡한 구조를 가질수록 오버피팅에 빠질 위험성은 커진다. 그렇기 때문에 정규화는 더욱 중요해진다
    - 정규화를 위한 기본적인 테크닉은 노이즈 삽입이다.
    - 또 다른 방법으론 dropout이 있다

  • 배치 정규화는 각각의 레이어에 대하여 적용된다. 트레이닝의 반복iteration동안
    1. 맨 처음 입력값에 대하여 정규화를 시행한다. 미니배치를 통해 평균과 표준편차를 추정하고, 각 입력값에 평균을 빼고 표준편차로 나눠주는 것이다.
    2. 다음 scale coefficient와 offset을 적용하여 잃어버린 자유도를 회복한다
    - BN(x)=γxμ^Bσ^B+β\boldsymbol{BN}(\boldsymbol{x})=\boldsymbol{\gamma}\,\,\odot \,\,\displaystyle\frac{\boldsymbol{x}-\hat{\boldsymbol{\mu}}_{\mathcal{B}}}{\hat{\boldsymbol{\sigma}}_{\mathcal{B}}}+\boldsymbol{\beta}
    - μ^B1BxBx\hat{\boldsymbol{\mu}}_{\mathcal{B}}\equiv \displaystyle\frac{1}{|\mathcal{B}|}\displaystyle\sum_{\boldsymbol{x} \in \mathcal{B}}^{}{\boldsymbol{x}} : 표본 평균
    - σ^B1BxB(xμ^B)2+ϵ\hat{\boldsymbol{\sigma}}_{\mathcal{B}} \equiv \displaystyle\frac{1}{|\mathcal{B}|}\displaystyle\sum_{\boldsymbol{x} \in \mathcal{B}}^{}{(\boldsymbol{x}-\hat{\boldsymbol{\mu}}_{\mathcal{B}}) ^{2}+\boldsymbol{\epsilon}} : 미니배치 B\mathcal{B} 의 표본 표준편차. 이때 division by zero 에러를 막기 위해 아주 작은 값 ϵ\boldsymbol{\epsilon} 를 추가한 것이다.

  • 배치 정규화는 미니배치의 사이즈가 1일 경우 계산이 불가능하다. 배치 정규화가 안정적이고 효과적이기 위해선 각 배치의 크기가 충분히 커야한다. 보통의 경우 50~100개의 미니배치가 좋은 성능을 보여주었다. 너무 작은 경우 유용한 시그널이 높은 분산값에 의해 손실되고, 너무 큰 배치에 경우 지나치게 안정적인 추정치로 인해 일반화가 잘 이루어지지 않았다

  • 학습과정, 학습모드training mode에서는 샘플링하는 대상이 미니배치이므로 같은 입력값에 대하여 다르게 분류되는 현상이 발생할 수 있지만, 학습 종료후 예측모드prediction mode 에서는 샘플링 대상이 데이터셋 전체이므로 이러한 문제가 발생하지 않는다

FC 레이어와 Convolution 레이어의 배치 정규화

  • fully-connected layer의 경우
    - 오리지널 논문에서는 배치 정규화는 아핀변형과 활성화함수 사이에서 이루어진다.
    - 이는 입력값이 x\boldsymbol{x} , 가중치 파라미터 W\boldsymbol{W} , 편향 파라미터 b\boldsymbol{b} , 활성화 함수 ϕ\phi , 출력값 f\boldsymbol{f} 가 있을 때 다음과 같이 계산된다 h=ϕ(BN(Wx+b))\boldsymbol{h}=\phi(BN(\boldsymbol{W}\boldsymbol{x}+\boldsymbol{b}))
  • convolutional Layer인 경우
    - convolutional layer인 경우도 Fully-connected Layer와 유사하게 배치 정규화는 합성곱과 비선형 활성화 함수 사이에서 이루어진다
    - 미니배치가 mm 개의 데이터포인트들을 갖고, 각각의 채널에 대하여 출력의 합성곱의 결과가 높이 pp, 폭 qq 를 갖고 있다고 하자. 각 합성곱 레이어 각 출력값 채널 마다 m×p×qm \times p \times q 개 원소의 배치 정규화를 시행하게 된다. 그 결과 우리는 평균과 분산에 대하여 계산할때 모든 공간적 위치에 대한 값을 수집하게 되고, 주어진 채널에 대하여 동일한 평균값과 분산을 갖고 정규화하게 된다는 것이다

레이어정규화 Layer Normalization

  • fully-Connected Layer와 convolutional layer에 정규화를 하는 것에는 약간의 차이점이 존재한다
  • 레이어 정규화의경우
    - 배치 정규화를 한 관측치마다 적용시키는 방법. n차원에 대한 벡터 x\boldsymbol{x} 는 다음과 같이 정규화된다$$ \boldsymbol{x} \to LN(\boldsymbol{x})=\displaystyle\frac{\boldsymbol{x}-\hat{\mu}}{\hat{\sigma}} - 레이어 정규화를 했을 때의 이점은 발산을 막는다는 것이다. 레이어 정규화의 결과값은 스케일-독립scale-independent한 특성을 가지고 있다. 이는 다시 말해 $$LN(\boldsymbol{x}) \sim LN(\alpha \boldsymbol{x}) for any choice of α0\alpha \neq 0 라는 것이다.
    - 또 다른 이점으론 레이어 정규화는 미니배치 사이즈에 의존하지 않는다는 것이다.

배치 정규화와 레이어 정규화 코드 구현

배치 정규화 함수

  • Training set을 모집단으로 보자. 앞으로 prediction을 시행하고자 한다면 Training set의 모평균, 모표준편차를 필요로 할 것이다. 그러한 계산을 하기 위한 공식들은 존재한다. 하지만 연산을 간단히 하기 위하여 EMA(Exponential moving average/Exponentially weighted average)라는 방식을 활용한다

  • EMA는 다음과 같이 계산된다
    - Vt=βVt1+(1β)×ΘtV _{t}=\beta V _{t-1}+(1-\beta) \times \Theta _{t}
    - 이때 β\beta 는 0과 1사이 하이퍼파라미터, Θ\Theta 는 새로들어온 데이터, VtV _{t} 현재 상태를 나타내는 변수로 볼 수 있다
    - Vt1=βVt2+(1β)×Θt1V _{t-1}=\beta V _{t-2}+(1-\beta) \times \Theta _{t-1} 인것을 고려하면
    - Vt=β(βVt2+(1β)×Θt1)+(1β)Θt2V _{t}=\beta(\beta V _{t-2}+(1-\beta) \times \Theta _{t-1})+(1-\beta)\Theta _{t-2} 인데 이를 보면
    - Vt2V _{t-2} 의 계수는 β2\beta ^{2} 로 현재상태 VtV _{t} 를 계산할 때 ll 만큼의 이전상태 VtlV _{t-l}βl\beta ^{l} 만큼 가중치를 갖는다는 것을 알 수 있다. 이는 오래된 데이터일수록 기하급수적으로 영향력이 감소하게 되는 결과를 낳는다. 그러한 이유로 Exponentially 라는 뜻이 붙게 되었다
    - 활용 : 이 VV 값은 근사적으로 11β\displaystyle\frac{1}{1-\beta} 단계의 데이터만을 활용하여 평균을 취한것과 같다고 알려져있다. 예컨데 β=0.98\beta=0.98 이면 위 식의 값은 50으로, 대략 50일간의 데이터를 갖고 가중평균을 구한것과 유사해지는 것이다.

  • 현재 알고리즘에선 다음과 같이 작성되었다
    - moving_mean: μ^(1η)μ^+ημB(i)\hat{\mu}\leftarrow (1-\eta)\hat{\mu}+\eta \mu _{\mathcal{B}}^{(i)}
    - moving_var :σ^(1η)σ^+ησB(i)\hat{\sigma} \leftarrow (1- \eta)\hat{\sigma }+\eta \sigma_{\mathcal{B}}^{(i)}

import torch
from torch import nn
from d2l import torch as d2l


def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
		"""
		torch.is_grad_enabled()는 gradient 계산이 가능한 상태인지 T or F로 
		반환한다.만약 False라면 prediction mode로 간주한다
		training 과정속에서 moving_mean과 moving_var를 학습시키는데 이 결과는
		prediction에서 변수들을 정규화하는데 사용된다
		"""
    if not torch.is_grad_enabled():
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        assert len(X.shape) in (2, 4)
        """
        len(X.shape)==2라면 fully-connected layer로 간주하고
        아닐경우 Convolutional layer로 간주한다
        이때 FC layer의 경우 평균값과 분산을 feature 축(axis=0)에서 계산한다
        convolutional layer일 경우 channel 축(axis=1)에서 계산한다
         """
        if len(X.shape) == 2:
            mean = X.mean(dim=0)
            var = ((X - mean) ** 2).mean(dim=0)
        else:
            mean = X.mean(dim=(0, 2, 3), keepdim=True)
            var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
        """training 모드라면, 현재 저장된 mean과 var를 통해 정규화를 진행한다
        . 그리고 moving_average 와 moving_var를 업데이트하는데 활용한다"""
        X_hat = (X - mean) / torch.sqrt(var + eps)
        moving_mean = (1.0 - momentum) * moving_mean + momentum * mean
        moving_var = (1.0 - momentum) * moving_var + momentum * var
        """각 피처or채널마다 배치 정규화가 된다. 
        이는 tensor-contradiction을 통해 구현된다"""
    Y = gamma * X_hat + beta  # Scale and shift
    return Y, moving_mean.data, moving_var.data

배치 정규화 클래스

class BatchNorm(nn.Module):
	    """ 
	    num_features: FC-출력층의 출력 수 또는 Convolutional 층의 출력 채널 수
	    를 의미한다
	    num_dims : 2차원인경우 FC층, 4차원인경우 Convolutional층을 제작한다
	    """
    def __init__(self, num_features, num_dims):
        super().__init__()
        if num_dims == 2:
            shape = (1, num_features)
        else:
            shape = (1, num_features, 1, 1)
		""" 
		scale 파라미터와 shift 파라미터는 각각 1과 0 으로 초기화된다
		moving_mean과 moving_var는 각각 0과 1로 초기화된다
		"""
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.ones(shape)

    def forward(self, X):
	    """ 
	    my_tensor=my_tensor.to(device)를 한다면 
	    GPU에 my_tensor의 복사본이 저장되게 된다.
	    따라서 'X'란 텐서가 메인 메모리에 올라와있지 않을 경우, 메모리에
	    올리는 작업을 시행한다.

		메모리에 올라왔다면, 앞에서 정의한 batch_norm 함수를 활용하여 
		해당 레이어에 대한 배치 정규화를 시행한다.
	    """
        if self.moving_mean.device != X.device:
            self.moving_mean = self.moving_mean.to(X.device)
            self.moving_var = self.moving_var.to(X.device)
        # Save the updated `moving_mean` and `moving_var`
        Y, self.moving_mean, self.moving_var = batch_norm(
            X, self.gamma, self.beta, self.moving_mean,
            self.moving_var, eps=1e-5, momentum=0.1)
        return Y

배치정규화가 적용된 LeNet

class BNLeNetScratch(d2l.Classifier):
    def __init__(self, lr=0.1, num_classes=10):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential(
            nn.LazyConv2d(6, kernel_size=5), BatchNorm(6, num_dims=4),
            nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2),
            nn.LazyConv2d(16, kernel_size=5), BatchNorm(16, 
										            num_dims=4),
            nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2),
            nn.Flatten(), 
            nn.LazyLinear(120),BatchNorm(120, num_dims=2), 
										            nn.Sigmoid(), 
            nn.LazyLinear(84),
            BatchNorm(84, num_dims=2), nn.Sigmoid(),
            nn.LazyLinear(num_classes))

ResNet

  • ResNet은 기존의 딥러닝 모델들이 더 깊은 네트워크 구조를 만들면서 성능을 향상시키는 방향으로 연구가 되자, 기울기 소실Gradient Vanishing 문제가 대두되면서 탄생하였다.
  • ResNet은 기울기 소실을 최소화하면서 더 깊은 네트워크 구조를 설계하기 위해 개발되었다

Function Class

  • F\mathcal{F} 를 (특정한 학습률과 기타 다른 하이퍼파라미터가 포함된) 특정 네트워크 구조를 만들수있는 함수들의 클래스라고 하자.
  • 이 말은 모든 fFf \in \mathcal{F} 에 대하여, ff 는 적절한 데이터셋의 의한 교육을 통해 특정 파라미터 집합을 가질수 있다는 것이다
    - ff ^{*} 가 우리가 얻고자 하는 "truth" 함수라고 할때 만약 fFf ^{*} \in \mathcal{F} 라고 한다면 가장 좋은 상태이지만, 그렇지 않는 경우가 더 일반적이다. 이 경우 fFFf _{\mathcal{F}}^{*} \in \mathcal{F} 라는 함수, 즉 F\mathcal{F} 에서 ff ^{*} 에서 가장 근접한 함수를 찾아야 한다.
    - 이를 수식으로 표현하자면 다음과 같다. 피처 X\boldsymbol{X} , 레이블 y\boldsymbol{y} 의 데이터셋이 존재할 때 우리는 다음과 같은 최적화 문제로 표현할 수 있다
    - fFargminf[L(x,y,f)],fFf ^{*}_{F} \equiv argmin _{f} [L(\boldsymbol{x},\boldsymbol{y},f)] \,\,,f \in \mathcal{F}
  • fFf _{\mathcal{F}}^{*} 찾기 전략
    -
    1. 정규화를 통하여 F\mathcal{F} 의 복잡도complexity 를 제어하는 방법이 있다.
    2. 훈련 데이터셋의 사이즈가 큰 경우 일반적으로 더 나은 fFf _{\mathcal{F}}^{*} 를 찾을 가능성이 높다
    3. 더 강력한 아키텍처인 F\mathcal{F}' 을 설계한다면, fFf _{\mathcal{F}'}^{*}fFf _{\mathcal{F}}^{*} 보다 더 좋은 근사일 가능성이 높다
    - 그러나 만약 F⊄F\mathcal{F} \not\subset \mathcal{F}' 이라면 반드시 더 좋은 근사일 것이라는 보장은 없다 오히려 더 나쁠수도 있다
    - 그림에서 묘사되었듯이, 왼쪽의 함수 클래스들은, 함수 클래스가 점점 커진다고 하여서 반드시 "truth" 함수 ff ^{*} 에 더 가까워지지는 않는 모습을 보여준다. 이 경우 F3\mathcal{F}_{3} 에서 가장 ff ^{*} 에 가까운 모습을 보여준다.
    - 오른쪽 그림의 경우 F1F6\mathcal{F}_{1} \subset \cdots \subset \mathcal{F}_{6} 의 관계를 보여주는데 이 경우 점차적으로 ff ^{*} 에 가까워지는 모습을 보여주고 있다
    - 따라서 함수 클래스가 기존 함수 클래스를 포함하는 형태가 된다면, 이는 네트워크의 표현력이 점점 증가하는 것으로 볼 수 있다.
    - 만약 우리가 새로운 레이어를 identity 함수 f(x)=xf(\boldsymbol{x})=\boldsymbol{x} 로 훈련시키게 된다면, 새로운 모델은 기존 모델 만큼이나 효과적인 표현력을 가질 것이다. 새로운 모델이 더 훈련셋에 부합하는 더 나은 해solution을 찾을때마다, 추가된 레이어는 훈련 에러를 줄이기 더 나을것이다
    	- 이는 매우 깊은 컴퓨터 비전 모델을 설계할때 깊이 고려되었던 문제이다. ResNet의 아이디어는 모델에 새로운 레이어를 추가할때마다, 항등함수를 추가하는 것이다 
    	- 이러한 고려는 residual block이란 아이디어로 구체화 되었다. 

Residual Block

  • 위 그림을 보자. 입력값 x\boldsymbol{x} 가 있고, 우리가 학습을 통하여 얻고자 하는 함수는 f(x)f(\boldsymbol{x})이다.
  • 왼쪽의 기존모델은 블록의 출력값이 바로 f(x)f(\boldsymbol{x}) 인데 반해 오른쪽 모델은 합성곱 연산을 통해 얻은 결과 g(x)g(\boldsymbol{x}) 에 기존 입력값 x\boldsymbol{x} 를 더한 f(x)=g(x)+xf(\boldsymbol{x})=g(\boldsymbol{x})+\boldsymbol{x} 를 블록의 출력값으로 하고 있다.
  • 이는 기존의 학습한 정보x\boldsymbol{x} 를 보존하고, 거기에 추가로 학습g(x)g(\boldsymbol{x}) 을 하게 되는 방식으로 이해할 수 있다. 이 g(x)g(\boldsymbol{x}) 를 잔차 residual 이라고 부른다
  • 이는 레이어가 깊어져 많이 학습될수록 x\boldsymbol{x} 는 점점 출력값 f(x)f(\boldsymbol{x}) 에 가까워져 추가학습량 g(x)=f(x)x0g(\boldsymbol{x})=f(\boldsymbol{x})-x \to 0 이 되어야 한다는 의미이다.
    - 따라서 학습의 목표는 g(x)=f(x)x0g(\boldsymbol{x})=f(\boldsymbol{x})-x \to 0 로, residual을 0으로 가깝게 만드는 것이 목표가 된다
  • 이 방법은 역전파하게 되었을 때 f(x)f(\boldsymbol{x}) 를 미분하게 된다. 이는 g(x)+xg(\boldsymbol{x})+\boldsymbol{x} 를 미분하는 것인데, 이때 아무리 미분을 해도 1\boldsymbol{1} 은 남기 때문에 기울기 소실 문제를 예방할 수 있다

ResNet 모델 설계

Residual Block 클래스

  • 만약 1x1 convolution을 사용하지 않으면 출력값과 입력값 텐서모양이 그대로이고, 만약 1x1 convolution을 사용하였다면 텐서 모양이 서로 다르게 된다.
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

class Residual(nn.Module):  #@save
    """Residual Block 구조 
	    1. 출력 채널값=num_channels ,kerrnel_size=3, padding=1을 갖는 
		합성곱 레이어
		2. 출력 채널값 동일 ,kerrnel_size=3, padding=1을 갖는 
		합성곱 레이어
		(optional)3. 출력 채널값 동일, stride=strides 인 1x1 conv로 shape를 
		바꾸기 위해서 사용되는 레이어 """
    def __init__(self, num_channels, use_1x1conv=False, strides=1):
        super().__init__()
        self.conv1 = nn.LazyConv2d(num_channels, kernel_size=3, padding=1,
                                   stride=strides)
        self.conv2 = nn.LazyConv2d(num_channels, kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.LazyConv2d(num_channels, kernel_size=1,
                                       stride=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.LazyBatchNorm2d()
        self.bn2 = nn.LazyBatchNorm2d()

    def forward(self, X):
	    """ 전방향 전파 학습과정
	    1. conv1 -> 배치정규화 -> relu 활성함수
	    2. conv2 -> 배치정규화 -> relu 활성함수
	    3. conv3가 있으면 conv3 수행
	    4. 2번째 레이어 activation map 값에 입력값 X를 더하여
	    (conv3가 있었다면 아래의 식으로 바뀜)반환함
	    """
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X
        return F.relu(Y)

ResNet 모델

  • 첫번째에서 2개의 레이어는 GoogLeNet과 동일한 구조이다. 다만 중간사이 배치 정규화가 있다는 차이점은 존재한다
  • 그 다음부터는 GoogLeNet이 4개의 인셉션 모듈을 사용한것과는 달리, ResNet은 residual block을 사용했다는 점에서 차이가 있다.
class ResNet(d2l.Classifier):
	""" 
	1. 맨 앞에서 2개의 레이어. 7x7 Conv-> BN -> 3x3 MaxPoo로 구성되어 있다
	"""
    def b1(self):
        return nn.Sequential(
            nn.LazyConv2d(64, kernel_size=7, stride=2, padding=3),
            nn.LazyBatchNorm2d(), nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

@d2l.add_to_class(ResNet)
	""" 
	Residual Block 제작 함수.
	첫번째 레이어에서는 # of channels=num_channels 인데, max-pooling layer가 
	stride=2인 상태로 적용된 상태의 값을 받아 높이와 폭을 더 줄이지 않기 위해 
	1x1block을 사용하지 않는다. 이를 identity block이라 한다
	 
	그 다음 레이어에서 부터는 use_1x1conv=True, strides=2로서 크기가 
	floor((input+1)/2) 이 된다. 이를 convolution block이라 한다
	"""
def block(self, num_residuals, num_channels, first_block=False):
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.append(Residual(num_channels, use_1x1conv=True, strides=2))
        else:
            blk.append(Residual(num_channels))
    return nn.Sequential(*blk)

@d2l.add_to_class(ResNet)
def __init__(self, arch, lr=0.1, num_classes=10):
    super(ResNet, self).__init__()
    self.save_hyperparameters()
    self.net = nn.Sequential(self.b1())
    for i, b in enumerate(arch):
    """
    add_module(name, module) : 현재 모듈에 name이란 새로운 모듈을 추가한다
    
    for i,b in enumerate(arch) 를 하면
	0 (2, 64) 1 (2, 128) 2 (2, 256) 3 (2, 512) 로 출력되는데
	이것이 각각 b2,b3,b4,b5 라 명명하기 위해 f'b{i+2}'를 쓴듯
	여기서 튜플 첫번째 원소값은 residual layer수를, 두번째 원소값은 채널수를 의미한다
    """
        self.net.add_module(f'b{i+2}', self.block(*b, first_block=(i==0)))
    self.net.add_module('last', nn.Sequential(
        nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(),
        nn.LazyLinear(num_classes)))
    self.net.apply(d2l.init_cnn)

class ResNet18(ResNet):
    def __init__(self, lr=0.1, num_classes=10):
    """((2, 64), (2, 128), (2, 256), (2, 512))가 architecture
	    구조를 의미함 """
        super().__init__(((2, 64), (2, 128), (2, 256), (2, 512)),
                       lr, num_classes)

ResNet18().layer_summary((1, 1, 96, 96))
""" result)
Sequential output shape:     torch.Size([1, 64, 24, 24])
Sequential output shape:     torch.Size([1, 64, 24, 24])
Sequential output shape:     torch.Size([1, 128, 12, 12])
Sequential output shape:     torch.Size([1, 256, 6, 6])
Sequential output shape:     torch.Size([1, 512, 3, 3])
Sequential output shape:     torch.Size([1, 10])
"""
profile
안녕하세요!
post-custom-banner

0개의 댓글