- 이번 포스팅에서는 Vision Transformer 가 아닌, CNN 모델에서 사용되는 Attention 기법에 대해 정리하겠습니다.
사람 혹은 생명채라면 시각 데이터의 특정 부분에 위치적으로 중요하다 아니다를 판별할 수 있고, 이로써 판단의 중요한 근거로 사용하므로, 시각 신경 구조를 토대로 만들어진 CNN 의 Attention 개념을 이해한다면 보다 근본적인 비젼 어텐션이 가능해질 것이라 생각합니다.
(SEBlock(Squeeze-and-Excitation Block))
- 논문 : Squeeze-and-Excitation Networks (2017)
- SE Block 의 핵심 아이디어는 채널의 중요도를 표현하는 것에 있습니다.
쉽게 설명드리겠습니다.
CNN 은 KxK 크기의 커널을 통해 값의 위치적 특성에 집중하는 모델입니다.
C 개의 커널로 C 개의 위치적 특성을 뽑아내는 것이죠.
그런데, 위치적 특성이 모두 동등하게 최종 결과에 영향을 끼칠까요?
당연히 아닙니다.
추출된 특징 중 어떤 것은 결과에 영향이 클테지만 어떤것은 영향이 별로 없을 수도 있죠.
앞서 CNN 커널로 C 개의 형태적 특성들을 추출했다면, 중요한 값은 증폭시키고, 중요치 않은 값은 침묵시키는...
이런 개념이 SE Block 이고, 보시다시피 CNN 모델의 채널 단위 Attention 을 구하는 기법이라 볼 수 있습니다.
- SE Block 의 작업 프로세스는 아래와 같습니다.
1. 압축
입력 Feature 가 (B, C, H, W) 크기가 있다면, 이를 채널단위로 평균 풀링을 하여 압축하여 (B, C, 1, 1) 로 만듭니다.
즉, 채널별로 평균을 구하는 것이죠.
2. 중요도 검출
앞서 구한 (B, C, 1, 1) 을 펼치면, Batch 개수의 Channel 크기만큼의 벡터입니다.
이를 FC Layer Block(Fc -> Relu -> Fc -> Sigmoid) 에 넣어서 각 채널간 중요도를 검출하도록 합니다.
3. 중요도 반영
앞서 채널간 중요도를 구했으므로, 압축 하기 전의 입력 Feature 인 (B, C, H, W) 의 각 채널별로 중요도를 곱해줍니다.
이렇게 되면 중요한 채널은 강조되고, 덜 중요한 채널은 억제되어 결과 도출에 긍정적인 영향을 줍니다.
- 위와 같은 SE Block 은 가벼운 연산으로 채널간 중요도라는 개념을 반영할 수 있기에 매우 효과가 좋은 기법입니다.
구조 자체도 모듈 단위로 나누기 쉬운 뚜렷한 구조이므로 어느 CNN 모델에도 쉽게 적용이 가능하고요.
- SE Block 예시 코드
import torch
import torch.nn as nn
import torch.nn.functional as F
class SEBlock(nn.Module):
def __init__(self, channels, reduction=16):
super(SEBlock, self).__init__()
self.fc1 = nn.Linear(channels, channels // reduction)
self.fc2 = nn.Linear(channels // reduction, channels)
def forward(self, x):
b, c, h, w = x.size()
z = F.adaptive_avg_pool2d(x, 1).view(b, c) # Squeeze
s = torch.sigmoid(self.fc2(F.relu(self.fc1(z)))).view(b, c, 1, 1) # Excitation
return x * s.expand_as(x) # Scale
(CBAM (Convolutional Block Attention Module))
- 논문 : CBAM: Convolutional Block Attention Module (2018)
- SE Block 에서 발전한 개념의 CNN Attention 모듈입니다.
핵심적인 변화는,
어텐션 개념을 채널뿐 아니라 공간까지 확장했다는 것입니다.
즉, SE 보다 무겁지만 성능은 좋은 SE Block 의 개선 버전이라 생각하시면 됩니다.
- CBAM 은 두가지 단계로 구성됩니다.
1. 채널 어텐션
앞서 설명한 SE 의 채널 어텐션 개념과 동일합니다.
다른 점으로는, 어텐션 계산을 위한 겂을 AvgPool 뿐만 아니라, MaxPool 도 같이 사용한다는 것입니다.
참고로 평균 값은 데이터 전체적 평균값을 반영하기 위한 것이고, 최대 값은 가장 튀는 값의 정보를 반영하기 위한 것인데,
통계적 관점에서 봤을 때, 이상치가 될 수 있는 최대값보다는 분산값을 적용하는 것이 더 좋아보일수도 있지만, 어텐션에서는 강한 자극인 최대값이 더 효과적이라고 합니다.
어쨌건 채널간 두 풀링 값을 구하고, MLP 통과 후 Sigmoid 를 하여 채널간 어텐션 값을 구합니다.
class ChannelAttention(nn.Module):
def __init__(self, in_planes, ratio=16):
super(ChannelAttention, self).__init__()
self.shared_mlp = nn.Sequential(
nn.Linear(in_planes, in_planes // ratio),
nn.ReLU(),
nn.Linear(in_planes // ratio, in_planes)
)
def forward(self, x):
b, c, h, w = x.size()
avg_pool = torch.mean(x, dim=(2,3), keepdim=True).view(b, c)
max_pool, _ = torch.max(x, dim=(2,3), keepdim=True)
max_pool = max_pool.view(b, c)
out = self.shared_mlp(avg_pool) + self.shared_mlp(max_pool)
scale = torch.sigmoid(out).view(b, c, 1, 1)
return x * scale
2. 공간 어텐션
앞서 채널 어텐션이 반영된 값을 받아 공간 단위 어텐션을 반영하는 과정입니다.
class SpatialAttention(nn.Module):
def __init__(self, kernel_size=7):
super(SpatialAttention, self).__init__()
self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size//2)
def forward(self, x):
avg_pool = torch.mean(x, dim=1, keepdim=True)
max_pool, _ = torch.max(x, dim=1, keepdim=True)
concat = torch.cat([avg_pool, max_pool], dim=1)
scale = torch.sigmoid(self.conv(concat))
return x * scale
위에서 보시다시피 채널단위가 아닌 채널간의 평균과 최대값을 구해서(B, 1, H, W), 이를 합친 값(B, 2, H, W)을 선형 변환 후 sigmoid 로 중요도 값으로 변환하여 x 에 곱하는 것입니다.
채널간 어텐션과 다른점으로는, 선형 변환을 conv2d 로 하기 때문에, 전 구간을 순회하며 공간단위 어텐션을 구하는 것입니다.
그렇기에 커널 크기가 크면 큰 범위를 기준으로 판단하기에 정확도가 높아지지만 그만큼 연산량이 많아집니다.
- 위와 같은 두 어텐션을 조합한 CBAM 의 최종 코드는,
class CBAM(nn.Module):
def __init__(self, channels):
super(CBAM, self).__init__()
self.channel_att = ChannelAttention(channels)
self.spatial_att = SpatialAttention()
def forward(self, x):
x = self.channel_att(x)
x = self.spatial_att(x)
return x
위와 같이 단순합니다.
(ECA(Efficient Channel Attention))
- 논문 : ECA-Net: Efficient Channel Attention for Deep Convolutional Neural Networks (2020)
- SE 의 대표적인 단점인,
1. 두개의 FC 레이어를 사용함으로써 파라미터 수 증가
2. 전체 채널 관계를 학습하므로 복잡도 상승으로 인한 오버피팅 가능성 증가
이를 해결하기 위하여 보다 가벼운 채널 Attention 구조를 만들기 위해 고안된 채널 어텐션 기법입니다.
- 코드 구현
import torch
import torch.nn as nn
import torch.nn.functional as F
class ECABlock(nn.Module):
def __init__(self, channels, k_size=3):
"""
Args:
channels (int): 입력 feature의 채널 수
k_size (int): 1D convolution의 커널 크기 (보통 3, 5, 7 중 하나)
"""
super(ECABlock, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1) # (B, C, 1, 1)
self.conv = nn.Conv1d(1, 1, kernel_size=k_size, padding=(k_size - 1) // 2, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
# x: (B, C, H, W)
b, c, _, _ = x.size()
y = self.avg_pool(x) # (B, C, 1, 1)
y = y.view(b, 1, c) # (B, 1, C)
y = self.conv(y) # (B, 1, C)
y = self.sigmoid(y) # (B, 1, C)
y = y.view(b, c, 1, 1) # (B, C, 1, 1)
return x * y.expand_as(x) # 채널별 attention 적용
코드상으로 파악했을 때,
SE 와 다른점으로는, FC 레이어가 사라지고, 인근 N개의 채널들과의 관계만을 단순한 conv 연산(1d conv)으로 구하여 sigmoid 로 확률화 한 것 뿐입니다.
확실히 모든 특징들에 대하여 연산을 수행하는 SE 보다 훨씬 가벼워 졌습니다.