Quantized Activation; ReLU example

이번에는 ReLU Activation을 Quantization 해보겠습니다.
Quantization 하기 이전에 ReLU Activation을 일반화된 표현으로 위와 같이 나타낼 수 있는데요,
어떤 z_{x}, z_{y} zx,zy 값과 slope에 해당하는 k k 값이 도입됩니다.
일반화된 ReLU는 오른쪽 그림처럼 그래프를 그릴 수 있습니다.

그럼 Quantized ReLU를 유도해보겠습니다.
이전 강의와 동일한 방식으로 유도 하는데요, De-Quantized 되었다고 가정을 합니다.
그래서 유도를 해보면 y y 는
- x_{q} < z_{x} xq<zx 일 때 0의 되고
- x_{q} \geq z_{x} xq≥zx 일 때 s_{x}(x_{q} - z_{x}) sx(xq−zx) 의 값을 갖습니다.
y y 또한 De-Quantized 되었다고 가정 하면 s_{y}(y_{q} - z_{y}) sy(yq−zy) 이처럼 표현할 수 있는데요,
그래서 y_{q} yq 에 대해서 다시 정리하면 아래와 같은 수식으로 Quantized ReLU를 정의하게 됩니다.
Layer Fusion
Layer Fusion은 학습을 더 이상 수행하지 않을 runtime 시점에, Conv layer 뒤에 추가되는 Activation, Batchnorm layer 들을 fuse하여, 하나의 Conv layer로 표현하는 것을 말합니다.

실제로 Layer 갯수가 줄어들기 때문에, 추론 시간에서 속도가 향상되며,
Quantization 관점에서도 Quant - decant 과정이 줄어드는 이점이 있습니다.
Batch Normalization

Batch Nomalization은 \gamma γ (scailing factor), \beta β (shift factor), \mu, \sigma μ,σ (mean, variance over a batch) 의 4개의 파라미터로 표현됩니다. Batch Nomalization의 과정은 아래와 같습니다.
- mini-batch 의 평균을 계산
- mini-batch 의 분산을 계산
- 정규화 수행
- Scale과 Shift 적용
위 과정을 다시 풀어보면, 평균 0, 분산 1로 정규화를 수행한 뒤, 추론이 잘 되도록 affine mapping 한다고 볼 수 있습니다.

추론을 수행할 때는 freezed BN 이라는 전체 학습 데이터셋의 평균, 분산을 저장해놓는 방법을 사용합니다. (실 구현에서는 효율을 위하여 지수 이동 평균(EMA)등의 기법을 적용하여 구현합니다)
그리고 Normarlze 또한 affine mapping 이므로, 두 affine mapping을 하나의 affine mapping으로 묶어 사용할 수도 있습니다.
그래서 위 그림의 11번째 줄의 식을 보면 하나로 묶인 수식을 확인할 수 있습니다.
Freezed Batchnorm as 1x1 Conv
Layer Fusion 이전에 Freezed BN은 1x1 Conv 으로도 표현할 수도 있다는 사실을 알아야 하는데요,
우선 Feature map에 BatchNorm을 적용한 수식을 살펴보면 아래와 같습니다.
- Feature map F F 의 크기 : C \times H \times W C×H×W (out channel, height, width)
- F_{c} Fc : 특정 out channel c c 의 Feature map matrix
- \gamma_{c}, \beta_{c} γc,βc : 각 out channel c에서 추론에서의 batch norm의 전체 affine mapping의 scale, shift

특정한 채널의 각 spatial 한 위치 i,j i,j 에 대한 연산은 아래와 같이 표현 가능한데요,
- \hat {F}_{C,i,j} = \gamma_{c} F_{C,i,j} + \beta_{C} F^C,i,j=γcFC,i,j+βC
이전 그림의 11번째 줄의 수식과 비교를 하면 동일한 형태임을 알 수 있습니다.
그리고, Channel wise한 연산(채널 내부의 전체 위치에 동일한 연산) 이므로, 1x1 Conv로도 표현이 가능합니다.
Vector unwrapping

실제 Conv 연산은 prev feature map output( F_{prev} Fprev )을 이번 연산의 입력이라 했을 때 F_{prev} Fprev 의 인접 k \times k k×k vector로 unwrapped한 결과인 f_{i,j} fi,j 를 사용합니다.

이 과정에서 입력을 좌측과 같이 vector로 unwrapping 하여 계산하게 되는데요,
vector unwrapping을 했기 때문에 Conv 연산은 행렬 곱의 조합으로 표현이 가능합니다.
Fusing BN Conv, original Conv


그래서 Batch Normalization과 Conv layer의 연산을 위와 같이 표현할 수 있습니다.
Fusing BN Conv, original Conv; Code
import torch
import torch.nn as nn
def fuse_convbn(conv: nn.Module, bn: nn.Module) -> nn.ModuleList:
"""Fuse conv + bn module into single conv."""
fused = torch.nn.Conv2d(
conv.in_channels,
conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=conv.padding,
bias=True,
)
w_conv = conv.weight.clone().view(conv.out_channels, -1)
w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps + bn.running_var)))
fused.weight.copy_(torch.mm(w_bn, w_conv).view(fused.weight.size()))
if conv.bias is not None:
b_conv = conv.bias
else:
b_conv = torch.zeros(conv.weight.size(0))
b_bn = bn.bias - bn.weight.mul(bn.running_mean).div(
torch.sqrt(bn.running_var + bn.eps)
)
fused.bias.copy_(bn.weight.mul(b_conv) + b_bn)
return fused
코드로 살펴보면 위와 같습니다.
생각해보기
- Conv 연산에서 빠른 연산을 위해 커널을 희소 행렬로 변환을 하게 됩니다. 이 때 커널에 대한 메모리 오버헤드가 발생 하는 것을 확인 할 수 있는데요요, 커널 대신에 이미지를 변환하는 형태로도 빠른 연산을 수행할 수 있을까요?
참고 자료
comment
activation 쪽에 질문이 있는데 동적 양자화에는 activation을 미리 양자화 하지 않는 이유가 궁금합니다.
1. 입력값이 가중치와는 다르게 실제 input 값이라서 인가요?
2. 만약 (1)이 맞다면 x와 y가 필요한 것 같은데, activation을 정적 양자화하기 위해서는 input, output이 모두 필요한 것인가요?