티스토리 뷰

이번에는 ShuffleNet v1을 직접 구현해보았다. 확실히 MobileNet v1과 비교했을 때 비슷한 학습속도에서 높은 성능을 보여줌을 확인할 수 있었다.

 

1. Setup

이전 구현들과 같다.

import torch.nn as nn
import torch
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import torch.optim as optim
import time
import numpy as np

import random
import torch.backends.cudnn as cudnn

seed = 2022
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
cudnn.benchmark = False
cudnn.deterministic = True
random.seed(seed)

 

2. GConv

 

ShuffleNet unit에 Group Convolution이 적용되며 unit에는 2개의 GConv가 존재한다. 첫번째 GConv는 채널 셔플이 적용되고 두번째는 적용되지 않기에 그것을 구분하는 인자를 두어 ReLU 적용 여부와 Channel Shuffle여부를 체크하도록 했다. 또한 Stage2에서는 첫번째 GConv에서 Channel Shuffle을 적용하지 않으며 input channel과 output channel의 차이가 2배가 아닌 큰 차이가 나타나므로 뒤에 코드에서 다시 언급하겠지만 skip connection 적용시 1x1 Conv로 channel dimension을 조정한 후 concat 해주는 형태로 dimension을 조정했다. 정답은 아니므로 참고만 하면 좋을 것 같다.

 

또한 Channel shuffle은 논문에서 언급한 방법대로 reshape / transpose / flatten 순으로 적용하였다.

논문에서 언급한 Channel Shuffle 방법
그림 (c) 처럼 channel shuffle이 이루어지며 모든 channel을 연관시키는 방법으로 논문에서 언급하고 있다.

class GConv(nn.Module):
    def __init__(self, groups, in_channels, out_channels, channel_shuffle = True, s2 = False):
        super(GConv, self).__init__()
        self.s2 = s2
        self.channel_shuffle = channel_shuffle
        self.groups = groups
        self.out_channels = out_channels
        self.gconv = nn.Conv2d(
            in_channels=in_channels,
            out_channels=self.out_channels,
            groups=self.groups,
            kernel_size=1,
            stride=1,
            padding=0
        )
        if s2:
            self.gconv = nn.Conv2d(
                in_channels=in_channels,
                out_channels=self.out_channels,
                kernel_size=1,
                stride=1,
                padding=0
            )
            self.channel_shuffle = False
        self.batch = nn.BatchNorm2d(num_features=self.out_channels)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.gconv(x)
        x = self.batch(x)
        if self.channel_shuffle:
            x_shape = x.shape
            x = self.relu(x)
            x = x.reshape(x_shape[0], self.groups, self.out_channels//self.groups, x_shape[-2], -1)
            x = torch.transpose(x, 1, 2)
            x = torch.flatten(x, start_dim=1, end_dim=2)
        if self.s2:
            x = self.relu(x)
        return x

 

3. DWConv

ShuffleNet Unit에는 DWConv도 적용되므로 클래스를 선언해주었다.

 

class DWConv(nn.Module):
    def __init__(self, in_channels, stride, multiplier = 1):
        super(DWConv, self).__init__()
        out_channels = in_channels * multiplier
        self.net = nn.Conv2d(
            in_channels=in_channels,
            out_channels=out_channels,
            groups=in_channels,
            kernel_size=3,
            padding=1,
            stride=stride
        )
        self.batch = nn.BatchNorm2d(num_features=out_channels)

    def forward(self, x):
        x = self.net(x)
        x = self.batch(x)
        return x

 

4. ShuffleNet Unit

 

앞의 GConv에서 언급한대로 Stage2에서는 stride2인 첫 unit에서 input dimension과 output dimension 차이가 다른 Stage간 차이와 다르므로 identity Conv를 활용해 dimension을 맞춰준 후 concat이 되도록했다. 나머지 stride2 인 부분은 그대로 avgpooling만 통과한 후 net통과한 값과 concat하는 연산을 했고, stride1인 부분은 pooling 없이 add 형태로 연산을 하도록 했다.

 

class ShuffleNetUnit(nn.Module):
    def __init__(self, stride, in_channels, out_channels, groups, s2=False):
        super(ShuffleNetUnit, self).__init__()
        self.s2 = s2
        self.stride = stride
        if stride == 1:
            d = 1
        else:
            d = 2
            self.avgpool = nn.AvgPool2d(kernel_size=3, stride=stride, padding=1)
        out_channels = out_channels // d
        self.gconv1 = GConv(groups=groups, in_channels=in_channels, out_channels=out_channels, s2=s2)
        self.DWConv = DWConv(in_channels=out_channels, stride = stride)
        self.gconv2 = GConv(groups=groups, in_channels=out_channels, out_channels=out_channels, channel_shuffle=False)
        if s2:
            self.identity = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1, padding=0)
        self.relu = nn.ReLU()

    def forward(self, x):
        x2 = self.gconv1(x)
        x2 = self.DWConv(x2)
        x2 = self.gconv2(x2)
        if self.stride == 2:
            x = self.avgpool(x)
            if self.s2:
                x = self.identity(x)
            x = torch.concat((x, x2), dim=1)
        else:
            x = torch.add(x, x2)
        x = self.relu(x)
        return x

 

5. ShuffleNet v1

 

논문에서 제시한 전체 아키텍쳐를 groups 인자에 따라 다르게 적용되도록 만들었다.

 

class ShuffleNetV1(nn.Module):
    def __init__(self, in_channels, groups, num_classes):
        super(ShuffleNetV1, self).__init__()
        self.channels_dict = {
            1 : 144,
            2 : 200,
            3 : 240,
            4 : 272,
            8 : 384
        }
        self.conv1 = nn.Conv2d(
            in_channels=in_channels,
            out_channels=24,
            kernel_size=3,
            stride=2,
            padding=1
        )
        self.maxpool = nn.MaxPool2d(
            kernel_size=3,
            stride=2,
            padding=1
        )
        channel_var = self.channels_dict[groups]
        stage2_list = [
            ShuffleNetUnit(
                stride=1,
                in_channels=channel_var,
                out_channels=channel_var,
                groups=groups
            ) for _ in range(3)
        ]
        s2 = [
                 ShuffleNetUnit(
                     stride=2,
                     in_channels=24,
                     out_channels=channel_var,
                     groups=groups,
                     s2=True

            )] + stage2_list
        self.stage2 = nn.Sequential(*s2)
        stage3_list = [
            ShuffleNetUnit(
                stride=1,
                in_channels=channel_var * 2,
                out_channels=channel_var * 2,
                groups=groups
            ) for _ in range(7)
        ]
        s3 = [
            ShuffleNetUnit(
                stride=2,
                in_channels=channel_var,
                out_channels=channel_var * 2,
                groups=groups
            )
        ] + stage3_list
        self.stage3 = nn.Sequential(*s3)
        stage4_list = [
            ShuffleNetUnit(
                stride=1,
                in_channels=channel_var * 4,
                out_channels=channel_var * 4,
                groups=groups
            ) for _ in range(3)
        ]
        s4 = [
            ShuffleNetUnit(
                stride=2,
                in_channels=channel_var * 2,
                out_channels=channel_var * 4,
                groups=groups
            )
        ] + stage4_list
        self.stage4 = nn.Sequential(*s4)
        self.avgpool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Linear(in_features=channel_var*4, out_features=num_classes, bias=True)
        self.soft = nn.Softmax(dim = 1)

    def forward(self, x):
        x = self.conv1(x)
        x = self.maxpool(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        x = self.avgpool(x)
        x = x.squeeze()
        x = self.fc(x)
        x = self.soft(x)
        return x

 

CIFAR-10로 테스트한 결과 MobileNet v1과 비교했을 때 같은 Epoch 20에서 비슷한 학습속도로 더 큰 성능을 나타냈다.

MobileNet v1 : 0.553

ShuffleNet v1 : 0.638

 

참고로 ResNet101은 0.691이지만 학습속도가 2배이상이다.

 

전체 코드는 다음 링크를 참고하면된다.

https://github.com/kkt4828/reviewpaper/blob/68b179ff6550744ea262306908be50eb386dd3cc/ShuffleNet/ShuffleNetV1.py

 

GitHub - kkt4828/reviewpaper: 논문구현

논문구현. Contribute to kkt4828/reviewpaper development by creating an account on GitHub.

github.com

 

참고논문 - https://arxiv.org/abs/1707.01083

 

ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices

We introduce an extremely computation-efficient CNN architecture named ShuffleNet, which is designed specially for mobile devices with very limited computing power (e.g., 10-150 MFLOPs). The new architecture utilizes two new operations, pointwise group con

arxiv.org