본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 인공지능 고급-시각 강의의 CNN알고리즘 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.
MobielNet은 스마트폰 및 임베디드 기기와 같이 리소스가 제한된 환경에서 아래 그림에서여 표현한 Recognition task를 수행할 목적으로 설계한 경량 심층 신경망이다.
MobielNet이 경량화된 뉴럴 네트워크를 만들기 위해 사용한 기법은 크게 3가지로
1) Depthwise Separable Convolutions
2) Width Multiplier()
3) Resolution Multiplier()
위 기법을 적용함으로써 비교대상인 VGG-16, GoogLeNet와 유사한 성능을 내면서도 연산량 및 Param 개수를 획기적으로 줄일 수 있었다.
Multi-Adds
: 모델이 추론을 수행하는데 필요한 연산량
Million-Param
: 모델을 구성하는데 사용되는 파라미터 개수
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가
Conv layer
의 커널 개수 : , 커널 사이즈 :
라 정의했을 때
로 연산 Cost를 정의할 수 있다.
이 연산 과정을 공간 방향(Depth-wise
), 채널방향(Point-wise
) 두 방향으로 나누고(Separate
) 각각 따로 Convolution
을 수행하는 것이
Depthwise Separable Convolutions 이다.
위 과정의 도식도는 아래와 같다.
위와 같이 in_feature
Separate
Depth-wise Conv
Concat
Point-wise Conv out_feature
단계를 수행함으로써 얻을 수 있는 연산 Cost를 각각 확인해보자
Depth-wise Conv Cost
:
Point-wise Conv Cost
:
따라서 Standard Conv 대비 Depth-wise Conv + Point-wise Conv의 cost
감소 비율을 확인하자면 아래와 같아진다.
단어가 좀 어려워 보이는 것이지 위 Depthwise Separable Convolutions 기법이 적용된 MobielNet의 모든 레이어 커널 개수를 일정 비율로 감소시키는 것을 의미한다.
커널의 개수가 줄어들면 전체적으로 네트워크의 Width
에 해당하는 채널 개수가 감소하고, 이것이 모델의 경량화로 이어짐을 의미한다.
이 계수는 0~1
사이의 값을 가져서
필터 개수를 일정 비율로 감소시킨다.
이 Width Multiplier 를 적용하면 Depthwise Separable Convolutions의 연산 Cost는 아래와 같이 감소한다.
이 과정은 네트워크에 입력되는 이미지의 크기(해상도)를 감소시키는 기법이다. 이것도 를 곱해서 일정 비율로 감소시키며, 0~1
사이의 값을 갖는다.
이 Resolution Multiplier을 적용한 Depthwise Separable Convolutions의 연산 Cost는 아래와 같다.
우선 기본이 되는 ConvBlock
는 오른편의 Origin ConvBlock에서
우편의 Depthwise Separable ConvBlock으로 변화한 것을 알 수 있다.
위 사진만 본다면
in_feature
Separate
Depth-wise Conv
Concat
Point-wise Conv 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
Separate
Depth-wise Conv
Concat
Point-wise Conv 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
에 다뤄 보고자 한다.
우선 임의의 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
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
의 초기화를 수행한다.. 라고 보면 될 듯 하다.
1) 모델 스펙 확인
대조군이랑 같이 놓고 비교한다면 MobielNet의 연산복잡성(FLOPs), 모델 무게(Params) 둘다 대조군 Net에 비해 꽤 가벼운 모델에 속하는 것을 확인할 수 있다.
2) 모델 성능 평가
다음으로는 위 9가지 경우에 수에 따른 모델의 성능 평가이다.
성능 평가에 사용할 Image Dataset은 Animals-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 경량버전을 생성해 탑재하면 될 것이다.
이 여러개의 모델을 돌리는데 코드가 딕셔너리 형태로 좀 지저분해 지다보니
전체 코드를 포스팅하지는 않았다.
코드에 대해 알고싶다면
https://github.com/tbvjvsladla/metacodeM_pytorch_bootcamp/blob/main/mobilenet.ipynb
를 참조하기 바란다.