info
구현 깃허브: https://github.com/Ahn-Ssu/model-implementation
-
DenseNet 입니다. 특별한 내용은 없고, ResNet의 Residual connection( sum skip connection)을 concatenation으로 변경을 한 skip connection을 제안하였습니다. (dense connectivity pattern을 가진 skip connection은 U-Net 등 encoder-decoder 아키텍쳐에서 사용됩니다)
Dense connectivity pattern을 통해서 얻는 이점은 다음과 같습니다.
- Vanishing-gradient problem 완화 - backprop을 통해 gradient가 직접 흐르게 됩니다. 이는 deep supervision을 암시적으로 활용하는 효과를 가집니다.
- strengthen feature propagation - summation을 통해서 '희석되는' feature 없이 강한 신호를 가진 feature로 모두 전달됩니다.
- encourage feature reuse - 네트워크 중간에서 출력된 feature를 재 사용함으로써, 모델의 효율 증대 (중복되는 입력에 재학습이 필요하지 않습니다)
- substantially reduce the number of parameters - 네트워크 파라미터 감소로 더 적은 크기의 모델로 운용 가능; ResNet은 레이어마다 각자 weight가 존재합니다. DenseNet은 파라미터 수를 줄일 수 있기 때문에 feature dim을 작게 가져갈 수 있습니다.
위의 장점들을 통해 DenseNet은 비교적 더 간단하고, 효과적인 모델 구조라는 점을 이야기하며 DenseNet도 ResNet처럼 충분히 깊은 네트워크를 구조를 형성을 저자는 이야기 합니다.
재밌던 내용은 모든 레이어가 학습에 필요하지 않다는 점이 선행 연구 1202-layer ResNet을 통해서 밝혀진 점을 추가하며 깊은 네트워크에서는 중복되는 내용이 매우 크며, 그렇기 때문에 feature reuse가 가지는 이점이 좋다는 것이었습니다.
구현은 DenseNet121로 구현을 하였으나, Dense Block에 전달되는 파라미터 (num_conv)를 수정하면 다른 레이어 갯수를 가지는 모델도 생성할 수 있습니다.
Basic operation modules
Hyperparameter configuration
import torch
import torch.nn as nn
import torch.nn.functional as F
from easydict import EasyDict as edict
args = edict()
# net dim
args.in_dim = 1
args.init_dim = 32 # the growth rate
args.enc_depth = 5
args.net_dim = [args.init_dim*2**x for x in range(args.enc_depth)] # [64, 128, 256, 512, 1024]
args.out_dim = 2
# net operator
conv_kwargs = edict()
conv_kwargs.kernel_size = 3
conv_kwargs.stride = 1
conv_kwargs.padding = 1
down_kwargs = edict()
down_kwargs.kernel_size = (2,2,2)
down_kwargs.stride = (2,2,2)
down_kwargs.padding = 0
args.conv_kwargs = conv_kwargs
args.down_kwargs = down_kwargs
Convolution layer
- 본문에서는 Composite function $H_l(\cdot)$dmf BN-ReLU-Conv의 순서로 구성을 했기 때문에, convolution layer도 그에 맞추어 구성이 되어 있습니다.
class ConvLayer(nn.Module):
def __init__(self, in_dim, out_dim,
conv_kwargs=None) -> None:
super(ConvLayer, self).__init__()
if conv_kwargs is None:
conv_kwargs = edict()
conv_kwargs.kernel_size = 3
conv_kwargs.stride = 1
conv_kwargs.padding = conv_kwargs.kernel_size // 2 # padding=convKenrel_size//2 --> keep size of the input image
self.norm = nn.BatchNorm2d(in_dim)
self.act = nn.ReLU()
self.conv = nn.Conv2d(in_dim, out_dim, **conv_kwargs)
def forward(self, inputs):
# H() = BN - ReLU - Conv
h = self.norm(inputs)
h = self.act(h)
h = self.conv(h)
return h
The first Convolution module: initial convolution
- 이 즈음 나온 CNN 모델들이 가지는 공통점 같은데, 224x224 사이즈의 이미지를 우선 크기부터 줄이고자 하였는지 큰 커널사이즈의 Conv 연산 후에 pooling을 수행합니다.
- 덕분에 224x224 크기의 이미지는 112x112 사이즈가 됩니다.
class InitialConv(nn.Module):
def __init__(self, in_dim, out_dim,
convKernel_size=7, convStride=2, poolingKernel_size=3, poolingStride=2) -> None:
super(InitialConv, self).__init__()
self.composite = ConvLayer(in_dim, out_dim, convKernel_size, convStride)
self.pool = nn.MaxPool2d(poolingKernel_size, stride=poolingStride)
def forward(self, inputs):
h = self.composite(inputs)
h = self.pool
return h
Dense Blocks
- DenseNet의 핵심, Dense block. 1x1 convolution을 우선 통과시켜 bottleneck을 형성 model parameter의 수를 조절합니다. 그 후 3x3 convolution에 통과시키며, 1x1-3x3 convolution이 통과할 때마다 output은 input에 다시 concat되어 다음 레이어로 전달이 됩니다.
class DenseBlock(nn.Module):
def __init__(self, in_dim, out_dim, num_conv=6,
conv_kwargs=None) -> None:
super(DenseBlock, self).__init__()
self.num_conv = num_conv
self.consecutive = nn.ModuleList([])
for idx in range(self.num_conv):
# 1x1 convolution
self.consecutive.append(ConvLayer(in_dim+(out_dim*(idx+1)), out_dim, convKernel_size=1, convStride=1))
# 3x3 convlotuon
self.consecutive.append(ConvLayer(out_dim, out_dim, conv_kwargs=conv_kwargs))
def forward(self, inputs):
prev = inputs
for idx in range(self.num_conv):
inter = self.consecutive[idx*2](prev) # 1x1 conv
inter = self.consecutive[idx*2 +1](inter) # 3x3 conv
prev = torch.concat([prev, inter], dim=1)
return inter #the final layer output
Transition Layer
- pooling layer에 1x1 convolution을 추가하여, channel dimmension scaling을 진행 한 뒤에 pooling을 수행합니다.
class TransitionLayer(nn.Module):
def __init__(self, in_dim, out_dim,
down_kwargs=None) -> None:
super(TransitionLayer, self).__init__()
self.conv = ConvLayer(in_dim, out_dim, convKernel_size=1, convStride=1)
self.pool = nn.AvgPool2d(**down_kwargs) # used average pool (not global)
def forward(self, dense_out):
h = self.conv(dense_out)
h = self.pool(h)
return h
Model
- feature encoder와 classifier 밖에 존재하지 않기 때문에 간단한 구조입니다. 아이디어가 새롭고, 구현은 단순하고, 장점은 크다.
class DenseNet(nn.Module):
def __init__(self, args) -> None:
super(DenseNet, self).__init__()
assert not args is None, "invalid args passing"
self.growthRate = args.init_dim
self.init_conv = InitialConv(args.in_dim, self.growthRate, convKernel_size=7, convStride=2, poolingKernel_size=3, poolingStride=2)
self.dense1 = DenseBlock(self.growthRate*2, self.growthRate*2, num_conv=6, convKernel_size=3, convStride=2)
self.transition1 = TransitionLayer(self.growthRate*2, self.growthRate*4)
self.dense2 = DenseBlock(self.growthRate*4, self.growthRate*4, num_conv=12, convKernel_size=3, convStride=2)
self.transition2 = TransitionLayer(self.growthRate*4, self.growthRate*8)
self.dense3 = DenseBlock(self.growthRate*8, self.growthRate*8, num_conv=24, convKernel_size=3, convStride=2)
self.transition3 = TransitionLayer(self.growthRate*8, self.growthRate*16)
self.dense4 = DenseBlock(self.growthRate*16, self.growthRate*16, num_conv=16, convKernel_size=3, convStride=2)
self.pool = nn.AdaptiveAvgPool2d(1)
self.classifier = nn.Sequential(
nn.Linear(self.growthRate*16, self.growthRate*16),
nn.Dropout(),
nn.ReLU(),
nn.Linear(self.growthRate*16, args.out_dim)
)
def forward(self, inputs):
h0 = self.init_conv(inputs)
h1 = self.dense1(h0)
h1 = self.transition1(h1)
h2 = self.dense2(h1)
h2 = self.transition2(h2)
h3 = self.dense3(h2)
h3 = self.transition3(h3)
h4 = self.dense4(h3)
hg = self.pool(h4)
out = self.classifier(hg)
return out