23. 주요 CNN알고리즘 구현 : MobileNet (1) - 인공지능 고급(시각) 강의 복습

안상훈·2024년 7월 30일
0

인공지능-시각

목록 보기
43/54
post-thumbnail

개요

본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 인공지능 고급-시각 강의의 CNN알고리즘 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.


1. MobileNet 개요

MobielNet은 스마트폰 및 임베디드 기기와 같이 리소스가 제한된 환경에서 아래 그림에서여 표현한 Recognition task를 수행할 목적으로 설계한 경량 심층 신경망이다.

MobielNet이 경량화된 뉴럴 네트워크를 만들기 위해 사용한 기법은 크게 3가지로

1) Depthwise Separable Convolutions
2) Width Multiplier(α\alpha)
3) Resolution Multiplier(ρ\rho)

위 기법을 적용함으로써 비교대상인 VGG-16, GoogLeNet와 유사한 성능을 내면서도 연산량 및 Param 개수를 획기적으로 줄일 수 있었다.

Multi-Adds : 모델이 추론을 수행하는데 필요한 연산량
Million-Param : 모델을 구성하는데 사용되는 파라미터 개수


1.1 Depthwise Separable Convolutions

Depthwise Separable Convolutions기법은 Inception계열의 net에 적용된 Factorized Convolutions(합성곱 분해)의 아이디어를 한 단계 더 발전시킨 것으로
Factorized Convolutions이 적용된 Inception Module이 큰 필터를 여러개의 작은 필터로 분해하여 연산 비용을 감소시킨 것처럼
Depthwise Separable Convolutions는 합성곱의 연산 단계를 2단계로 분해하여 연산 비용을 감소시킨다.

이 과정을 이해하려면 먼저 합성곱의 연산 과정을 숙지할 필요성이 있다.

위 gif처럼 청록색의 in_feature가 Conv 레이어를 만나면
Conv layer에 무지개색의 다양한 필터를 거치면서
무지개색의 out_feature가 출력된다. 이 과정을 gif로 표현한 것이다.

따라서 위 Standard Conv Cost를 한번 확인해보자

위 그림처럼 in_feature의 shape가 [M,DF,DF][M, D_F, D_F]
Conv layer의 커널 개수 : NN, 커널 사이즈 : DK×DKD_K \times D_K

라 정의했을 때

연산 Cost를 정의할 수 있다.

이 연산 과정을 공간 방향(Depth-wise), 채널방향(Point-wise) 두 방향으로 나누고(Separate) 각각 따로 Convolution을 수행하는 것이

Depthwise Separable Convolutions 이다.
위 과정의 도식도는 아래와 같다.

위와 같이 in_feature \rightarrow Separate \rightarrow Depth-wise Conv \rightarrow
Concat \rightarrow Point-wise Conv \rightarrow out_feature

단계를 수행함으로써 얻을 수 있는 연산 Cost를 각각 확인해보자

Depth-wise Conv Cost :


Point-wise Conv Cost :


따라서 Standard Conv 대비 Depth-wise Conv + Point-wise Convcost 감소 비율을 확인하자면 아래와 같아진다.



1.2 Width Multiplier (α)\alpha)

단어가 좀 어려워 보이는 것이지 위 Depthwise Separable Convolutions 기법이 적용된 MobielNet의 모든 레이어 커널 개수를 일정 비율로 감소시키는 것을 의미한다.

커널의 개수가 줄어들면 전체적으로 네트워크의 Width에 해당하는 채널 개수가 감소하고, 이것이 모델의 경량화로 이어짐을 의미한다.

α\large \alpha 계수는 0~1 사이의 값을 가져서
필터 개수를 일정 비율로 감소시킨다.

이 Width Multiplier 를 적용하면 Depthwise Separable Convolutions연산 Cost는 아래와 같이 감소한다.


1.3 Resolution Multiplier(ρ\rho)

이 과정은 네트워크에 입력되는 이미지의 크기(해상도)를 감소시키는 기법이다. 이것도 ρ\large \rho를 곱해서 일정 비율로 감소시키며, 0~1사이의 값을 갖는다.

이 Resolution Multiplier을 적용한 Depthwise Separable Convolutions연산 Cost는 아래와 같다.



2. MobileNet 아키텍쳐


우선 기본이 되는 ConvBlock는 오른편의 Origin ConvBlock에서
우편의 Depthwise Separable ConvBlock으로 변화한 것을 알 수 있다.

위 사진만 본다면
in_feature \rightarrow Separate \rightarrow Depth-wise Conv \rightarrow
Concat \rightarrow Point-wise Conv \rightarrow out_feature

이 과정에서 Separate, Concat 이 빠진것처럼 묘사되고 있는데
이 부분은 코드 구현에서 설명을 진행하겠다.

다음으로 MobielNet의 전체 구조를 살펴보면 군데군데 Stride=2으로 설정된 Depthwise Separable ConvBlock이 존재하는데 이는

Conv 레이어와 Pool 레이어를 합친 레이어라 보면 된다.

Pool 레이어의 주요 기능인 DownSampling을 모델을 경량화 해야하니 Conv 레이어에서 Stride=2 옵션으로 자체적으로 해결했다
라고 보면 될 듯 하다.

이제 코드로 모델을 구현해보자.

import torch
import torch.nn as nn
class BasicConv(nn.Module):
    def __init__(self, in_ch, out_ch, kernel_size, **kwargs):
        super(BasicConv, self).__init__()

        self.conv_block = nn.Sequential(
            nn.Conv2d(in_ch, out_ch, kernel_size=kernel_size, bias=False, **kwargs),
            nn.BatchNorm2d(out_ch),
            nn.ReLU6(inplace=True) #Relu랑 같은데 상한이 6으로 제한된 레이어
            # 기존 Relu보다 고정 소수점 연산(fixed-point arithmetic)에 더 유리함
            # 따라서 모바일 및 임베디드 디바이스에 대하여 유리함
        )

    def forward(self, x):
        x = self.conv_block(x)

        return x

여기서 살짝 바뀐 부분이 있는데
nn.ReLU6()의 적용이다.

기존 nn.ReLU()와 주요 차이점은 상한값이 6으로 제한된 것으로 nn.ReLU6()이 고정소수점 연산에 더 유리하여
경량화된 모델에 Activation Function을 적용한다면 nn.ReLU6()을 적용하는게 일반적이라고 한다.

class DepthSep(nn.Module):
    def __init__(self, in_ch, out_ch, stride=1):
        super(DepthSep, self).__init__()

        self.depthwise = BasicConv(in_ch, in_ch, kernel_size=3, stride=stride, padding=1,
                                   groups = in_ch)
        # 여기서 groups 입력 채널과 출력 채널사이의 관계를 나타냄
        # default=1 => 모든 입력은 모든 출력과 conv 연산이 됨
        # 2, 3, 4 => 입력을 2, 3, 4 그룹으로 나누어서 각각 conv연산 후 concat
        # group = in_ch --> 이게 Depthwise의 `Separable`에 해당하는 항목임

        self.pointwise = BasicConv(in_ch, out_ch, kernel_size=1, stride=1, padding=0)

    def forward(self, x):
        x = self.depthwise(x)
        x = self.pointwise(x)

        return x

다음으로 Depthwise Separable ConvBlock인데 여기에서
groups = in_ch 라는 특이한 옵션 하나가 더 붙어있다.

groups라는 인자값은 default=1이지만
지금처럼 groups = in_ch으로 옵션을 조정하면

in_feature \rightarrow Separate \rightarrow Depth-wise Conv \rightarrow
Concat \rightarrow Point-wise Conv \rightarrow out_feature

위 과정에서
Separate, Concat을 한큐에 수행해주는 옵션이라 보면 된다.

class MobileNetV1(nn.Module):
    def __init__(self, width_multiplier, num_classes=1000, init_weight=True):
        super(MobileNetV1, self).__init__()

        self.alpha = width_multiplier #네트워크 각 층의 필터 개수를 조정하는 인자값

        self.stem = BasicConv(3, int(32*self.alpha), kernel_size=3, stride=2, padding=1)

        self.feature_ext = nn.Sequential(
            DepthSep(int(32*self.alpha), int(64*self.alpha)),
            DepthSep(int(64*self.alpha), int(128*self.alpha), stride=2),
            DepthSep(int(128*self.alpha), int(128*self.alpha)),
            DepthSep(int(128*self.alpha), int(256*self.alpha), stride=2),
            DepthSep(int(256*self.alpha), int(256*self.alpha)),
            DepthSep(int(256*self.alpha), int(512*self.alpha), stride=2),
            DepthSep(int(512*self.alpha), int(512*self.alpha)),
            DepthSep(int(512*self.alpha), int(512*self.alpha)),
            DepthSep(int(512*self.alpha), int(512*self.alpha)),
            DepthSep(int(512*self.alpha), int(512*self.alpha)),
            DepthSep(int(512*self.alpha), int(512*self.alpha)),
            DepthSep(int(512*self.alpha), int(1024*self.alpha), stride=2),
            DepthSep(int(1024*self.alpha), int(1024*self.alpha))
        )

        self.classfier = nn.Sequential(
            nn.AdaptiveAvgPool2d((1, 1)),
            nn.Flatten(),
            nn.Linear(int(1024*self.alpha), num_classes)
        )

        if init_weight: #초기화 구동함수 호출
            self._initialize_weight()

    def forward(self, x):
        x = self.stem(x)
        x = self.feature_ext(x)
        x = self.classfier(x)

        return x
    
    #모델의 초기 Random을 커스터마이징 하기 위한 함수
    def _initialize_weight(self):
        for m in self.modules(): #설계한 모델의 모든 레이어를 순회
            if isinstance(m, nn.Conv2d): #conv의 파라미터(weight, bias)의 초가깂설정
                # Kaiming 초기화를 사용한 이유:
                # Kaiming 초기화는 ReLU 활성화 함수와 함께 사용될 때 좋은 성능을 보임
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            
            elif isinstance(m, nn.BatchNorm2d): #BN의 파라미터(weight, bias)의 초가깂설정
                # BatchNorm 레이어의 가중치와 바이어스를 간단한 값으로 초기화
                nn.init.constant_(m.weight, 1) # 1로 다 채움
                nn.init.constant_(m.bias, 0) # 0으로 다 채움

            elif isinstance(m, nn.Linear): #FCL의 파라미터(weight, bias)의 초기값 설정
                # 선형 레이어의 가중치를 정규 분포로 초기화
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.constant_(m.bias, 0)

MobielNet의 전체 구조는
Conv block의 구성이 AlexNet이나 VGG랑 상당히 유사하게 설계됨을 알 수 있다.

이제 그동안 필자가 이악물고 모른척 했던
_initialize_weight에 다뤄 보고자 한다.


2.1 def _initialize_weight()

우선 임의의 Net을 설계한다면 아래의 그림처럼 Net가 인스턴스화 되면서 초기화 과정을 자동으로 수행한다

따라서 대부분의 경우에는 def _initialize_weight() 함수를 따로 설계할 필요가 없다.

알아서 자동으로 각 레이어별 할당된 Parameter의 초기값이 Pytorch라이브러리에서 알아서 잘 초기값을 지정하기 때문이다.

그러나, 이 Parameter의 가중치 초기화에 대하여
어떻게 하면 더 적합한 초기값을 부여할 수 있을까?

에 대한 연구가 진행되었고
대표적으로
Lecun, Xavier, Kaiming He 3가지 초기화 방법론이 존재한다.
그리고 여기서 적합한 초기화 방법론을 PyTorch 라이브러리가
알아서 잘 선택해서 초기화를 수행한다.

각각의 수식은 위와 같은데 음... 궂이 이해할 필요는 없을것 같다.
모두 다 신경망의 학습 성능을 향상시키기 위해 초기 Param을 어떻게 지정할까? 에 대한 연구이고

Lecun 보다는 Xavier, Kaiming He 초기화 방법론이 더 많이 쓰이며
PyTorch에서는 Kaiming He을 Defalut 초기화 방법론으로 주로 사용하는 듯 하다.

초기화 방법론에 대한 기본 개념이 숙지되었으니

이제 코드를 보도록 하자

def _initialize_weight(self):
    for m in self.modules(): #설계한 모델의 모든 레이어를 순회
        if isinstance(m, nn.Conv2d): #conv의 파라미터(weight, bias)의 초가깂설정
        
        elif isinstance(m, nn.BatchNorm2d): #BN의 파라미터(weight, bias)의 초가깂설정

        elif isinstance(m, nn.Linear): #FCL의 파라미터(weight, bias)의 초기값 설정

먼저 기본코드는 위와 같다.

.modules()메서드를 통해 현재 설계된 Net의 모든 레이어(모듈)을 탐색하며,

해당 모듈이 초기값 지정이 필요한 레이어
nn.Conv2d, nn.BatchNorm2d, nn.Linear인 경우에 한하여
초기값 지정을 수행하겠다는 뜻이다.

1) nn.Conv2d 인 경우

if isinstance(m, nn.Conv2d): #conv의 파라미터(weight, bias)의 초가깂설정
    # Kaiming 초기화를 사용한 이유:
    # Kaiming 초기화는 ReLU 활성화 함수와 함께 사용될 때 좋은 성능을 보임
    nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')

	if m.bias is not None:
        nn.init.constant_(m.bias, 0)

Conv Layer의 weight는 앞서 소개한 가중치 초기화 방법론 중
Kaiming He \rightarrow Uniform(균등분포)모드로 초기화를 하겠다는 의미이며,
여기서 추가 인자값으로 mode, nonlinearity를 지정하는데

mode='fan_in' 인 경우에는 입력 연결 수에 기반한 초기화
mode='fan_out' 인 경우에는 출력 연결 수에 기반한 초기화 로 나뉜다.

일반적으로 mode='fan_in'이지만, 모델을 경량화, 그리고 활성화 함수에 더 잘 조정되게 하려면 mode='fan_out'을 써라.. 뭐 이정도가 되겠다.

nonlinearity는 Conv Layer 다음에 붙는 Activation Function이 어떤 것인지 묻는 인자다.
leaky_relu, relu 둘 중 하나를 고를 수 있다.


2) nn.BatchNorm2d 인 경우

elif isinstance(m, nn.BatchNorm2d): #BN의 파라미터(weight, bias)의 초가깂설정
    # BatchNorm 레이어의 가중치와 바이어스를 간단한 값으로 초기화
    nn.init.constant_(m.weight, 1) # 1로 다 채움
    nn.init.constant_(m.bias, 0) # 0으로 다 채움

BN레이어는 Feature의 분포를 표준화 하는 기능을 수헹하는 레이어 이기에 초기값은 가장 간편하게
weight는 다 1로 채우고
bias는 0으로 채우고 시작하게 설정한다.


3) nn.Linear 인 경우

elif isinstance(m, nn.Linear): #FCL의 파라미터(weight, bias)의 초기값 설정
    # 선형 레이어의 가중치를 정규 분포로 초기화
    nn.init.normal_(m.weight, 0, 0.01)
    nn.init.constant_(m.bias, 0)

마지막 FCL레이어는 균등분포, bias는 0으로 초기화한다.


이렇게 MobielNet은 성능이 제한된 임베디드, 모바일 환경에서 API의 구동을 전제로 하고 있고, 이 임베디드, 모바일 기기에서 가장 껄끄러운 항목이
Random, 난수값 생성이 있다.

음.. 그러니까 난수값 생성하는데 난수처럼 보이는 값을 만들어 내는거지 이걸 진짜 난수랑 비슷하게 만들게끔 하려면 리소스를 많이 잡아먹는다.. 뭐 이렇게 이해하고 넘어가자

아무튼 이렇게 각 레이어별로 경량화에 초점을 맞춰서 Param의 초기화를 수행한다.. 라고 보면 될 듯 하다.



3. Mobile 성능실험

1) 모델 스펙 확인

대조군이랑 같이 놓고 비교한다면 MobielNet의 연산복잡성(FLOPs), 모델 무게(Params) 둘다 대조군 Net에 비해 꽤 가벼운 모델에 속하는 것을 확인할 수 있다.

2) 모델 성능 평가

다음으로는 위 9가지 경우에 수에 따른 모델의 성능 평가이다.

성능 평가에 사용할 Image DatasetAnimals-10

데이터셋은 Train = 85%, Val = 15% 로 자료 비율을 조정한다

import splitfolders

input_folder = './raw-img'
output_folder = './Animals-10'

# 데이터셋을 85% , 검증, 15% 세트로 나눔
splitfolders.ratio(input_folder, output=output_folder, seed=1337, ratio=(.85, .15, 0.0))

그리고 데이터 전처리 방법론은 아래와 같이
수평반전, 색상&밝기&채도, 아핀 3가지만 적용한다

# 데이터 전처리 방법론 정의
from torchvision.transforms import v2

animals_val = {'mean' : [0.5177, 0.5003, 0.4126],
                'std' : [0.2133, 0.2130, 0.2149]
}

def define_transform(img_size, normal_val, augment=False):
    transform_list = []

    if augment:
        transform_list += [ #데이터 증강은 반전, 색상밝기채도, 아핀 3가지
            v2.RandomHorizontalFlip(p=0.5),
            v2.ColorJitter(brightness=0.4,
                            contrast=0.4,
                            saturation=0.4,
                            hue=0.1),
            v2.RandomAffine(degrees=(30, 70),
                            translate=(0.1, 0.3),
                            scale=(0.5, 0.75)),
        ]

    transform_list += [
        v2.Resize((img_size, img_size)), #이미지 사이즈별로 리사이징
        v2.ToImage(),  #이미지를 Tensor 자료형으로 변환
        v2.ToDtype(torch.float32, scale=True), #텐서 자료형을 [0~1]로 정규화
        v2.Normalize(mean=normal_val['mean'], std=normal_val['std']) #데이터셋 표준화
    ]

    return v2.Compose(transform_list)

마지막으로 하이퍼 파라미터는
Adam(lr=0.001), epoch = 15로 놓고
Train / Val의 성능 결과를 비교 분석한다.

모델이 경량화 되면서 동시에 성능하락이 발생하는것은 필연적이니 대상 모바일 기기의 연산성능을 참조하여
적합한 MobielNet 경량버전을 생성해 탑재하면 될 것이다.


이번 포스트의 경우 MobielNet은 필자가 다음에 사용할 `Net`을 공부하는데 있어 선행학습이 필요하여 공부한 항목이고..

이 여러개의 모델을 돌리는데 코드가 딕셔너리 형태로 좀 지저분해 지다보니
전체 코드를 포스팅하지는 않았다.

코드에 대해 알고싶다면
https://github.com/tbvjvsladla/metacodeM_pytorch_bootcamp/blob/main/mobilenet.ipynb
를 참조하기 바란다.

profile
자율차 공부중

0개의 댓글