반응형

얼굴인식 분야에서 지금까지도 많이 사용되고 있는 ArcFace (ArcFace: Additive Angular Margin Loss for Deep Face Recognition) 논문에서 사용한 기존 ResNet을 얼굴인식 모델 학습에 최적화 되도록 변형한 ResNetFace / SE-LResNet50E-IR에 대해 알아보았다.

 

논문에는 we investigate a more advanced residual unit setting for the training of face recognition model 라고 나와있다.

 

1. Block Setting (IR Block = Improved Residual Unit)

기존 ResNet에서 쓰는 Residual Network 기본 구조에서 조금 변형한 BN-Conv-BN-PReLu-Conv-BN 구조의 Residual Unit를 썼다. 기본 구조와 달라진 점은 두번째 Conv Layer에서 stride=2 를 줬고 ReLu activation function 대신에 PReLu를 썼다. 아래 두 사진을 보면 차이점을 알 수 있다.

 

기존 Residual Block (https://d2l.ai/chapter_convolutional-modern/resnet.html)
얼굴인식 모델용으로 발전된 Improved Residual Block

 

+ 추가적으로 위에 사진에 기존 Residual Block은 ResNet-18, ResNet-34에서만 쓰고, ResNet의 깊이가 점점 깊어지면 경우 parameter의 수가 너무 많아지기 때문에 레이어가 50층 이상인 ResNet에서는 (ResNet-50, ResNet-101, ResNet-152) residual block으로 다른 구조를 사용한다. Bottlenect block 이라고 하는데, 1x1 convolution을 통해 filter size를 줄인 후에 3x3 convolution을 하면 파라메터의 수를 아낄 수 있다고 한다. 이렇게 Bottleneck 구조를 사용해 ResNet의 내부 구조를 바꾸면 152 layer까지 쌓아도 vgg보다 모델 크기가 작다고 한다. 

 

* ResNet에 대한 자세한 설명 : dnddnjs.github.io/cifar10/2018/10/09/resnet/

(왼쪽) Basic Block , (오른쪽) Bottleneck Block

 

2. Backbones - SE-ResNet (Squeeze and excitation networks)

논문에서는 Backbone으로 SE-ResNet을 썼다. SE-ResNet은 위에 설명한 IR Block에서 마지막 BN를 거쳐서 나온 output를 Residual connection 전에 SE block 에 넣어준다. SE block의 목적은 한마디로 컨볼루션을 통해 생성된 특성을 채널당 중요도를 고려해서 재보정(recalibration)하는 것이다. 이러한 SE block을 컨볼루션 연산 뒤에 붙여줌으로써 성능 향상을 도모한다.

 

* Residual connection : 연산 후에 input x 를 더하는 것 (Residual Block). Skip connection를 통해 각각의 Layer (Block)들이 작은 정보들을 추가적으로 학습하도록 함. 즉, 기존에 학습한 정보를 보존하고 거기에 추가적으로 학습하는 정보를 의미한다. (자세한 설명 : itrepo.tistory.com/36)

 

Squeeze (압축)

 

Excitation을 거쳐, Channel 사이의 Feature들에 대한 중요도를 Recalibaration(재조정)

1) Squeeze

Squeeze는 짜낸다, 즉 각 채널들의 중요한 정보만 추출해서 가져가겠다라는 뜻으로 볼 수 있다. Global average pooling (GAP)을 통해 각 2차원의 특성맵을 평균내어 하나의 값을 얻는 방식으로 스퀴즈(squeeze)를 실행한다. GAP가 Feature들을 1차원 벡터로 바꿔서 HxWxC 크기의 특성맵을 1x1xC로 압축한다. 

(논문상에선 편리상 Global Average Pooling을 사용했지만, 압축하는 방법은 정해져있지 않다.)

 

2) Excitation 활성화

이제 중요한 정보들을 압축(Squeeze)했다면 재조정(Recalibration)을 하는 과정이다. 두 개의 Fully-connected(FC) 층을 더해줘서 각 채널의 상대적 중요도를 알아낸다. (채널 간 의존성(channel-wise dependencies)을 계산)

GP -> FC -> ReLu -> FC -> Sigmoid 로 SE Block 이 이뤄져 있는데,

channel descriptor vector를 선형 변환 시키고, ReLU를 씌우고, 또 선형 변환시킨 뒤 sigmoid 함수를 pointwise로 적용한 것이다. 마지막에 Sigmoid 함수를 지남으로 excitation operation을 거친 스케일 값들이 모두 0과 1사이의 값을 가지기 때문에 채널들의 중요도에 따라 스케일 된다.

 

아래 사진에서 색깔은 각 Feature의 0~1까지의 중요도를 나타냄. 이 재조정된 중요도를 담은 Feature를 원래 Feature Map에 곱해주어 중요도가 학습된 새로운 Feature Map이 탄생한다.

 

<자세한 설명 - wolfy.tistory.com/246>

추출된 1x1xC의 피처맵을 FC Layer를 거쳐 C/r (r = reduction ratio) 만큼 1차적으로 차원을 축소해줍니다.
ReLU를 거쳐 FC Layer에서 C만큼 다시 차원을 늘립니다.
이렇게 추출된 중요도 Map을 원래 Feature Map과 곱해 Feature Map을 재조정 해줍니다.

Excitation은 C에서 C/r로 축소된다음, 다시 C가 되는 과정이라고 할 수 있습니다.



출처: 

https://wwiiiii.tistory.com/entry/SqueezeandExcitation-Networks

jayhey.github.io/deep%20learning/2018/07/18/SENet/

*** wolfy.tistory.com/246

*** bskyvision.com/640

 

3) 기존 모델에 결합

ResNet의 경우에는 Residual 모듈 뒤에 SE Block을 붙인다.

 

 

 

3. Input Setting 

ArcFace 논문에서는 Input으로 들어가는 얼굴 이미지는 cropped and resized to 112x112 이라고 되어있다. 보통 ImageNet 분류문제에 쓰이는 컨볼루션 네트워크들을 보면 이미지 사이즈가 224x224, 또는 더 큰 것을 알 수가 있는데, 얼굴 이미지는 224X224보다 작은 112x112이다. 그래서 피처맵의 높은 해상도를 보존하기 위해서 (To preserve higher feature map resolution) 기존 ResNet에서 첫번째 컨볼루션 레이어로 쓰이는 conv7x7, stride 2 를 conv3x3 stride 1로 바꿔줬다고 한다. 이렇게 첫번째 컨볼루션 레이어를  conv3x3 stride 1로 바꾼 네트워크를 네트워크 이름 앞에 "L"를 붙여서 실험해봤다. 아래에 실험 결과를 보면, 네트워크 이름 앞에 L이 붙은 네트워크들이 Accuracy 가 더 높게 나오는 것을 알 수가 있다.

 

기존 ResNet

 

Input Setting 실험결과

 

4. Output Setting

Output Layer는 embedding 차원 512로 아래와 같이 여러 옵션을 테스트 해봤을 때 (코사인 거리로 두 이미지의 피처 백터 (차원 512) 비교해서 점수 산출), option-E가 가장 성능이 좋았다고 한다. 

 

5. 결론

결론은 데이터 셋마다 결과가 조금 다르지만 보통 SE-LResNet100E-IR 네트워크를 쓰는 것 같다.코드에는 ResNet-Face (SE-LResNetE-IR) 18 layer만 구현이 되어있는데 간단히 layer 수만 바꿔주면 SE-LResNet100E-IR를구현할 수 있다.

 

SE-LResNet100E-IR :

ResNet-100에

+ 기존 ResNet의 Bottleneck Block -> IR Block

+ SE Block

+ Input layer를 conv7x7, stride 2 -> conv3x3 stride 1

+ Output layer는 option A (GP) -> option E (BN-Dropout-FC-BN)

 

로 변경했다.

 

코드

1. github.com/foamliu/InsightFace-v2/blob/master/models.py

2. github.com/ronghuaiyang/arcface-pytorch/blob/master/models/resnet.py

class IRBlock(nn.Module):
    expansion = 1

    def __init__(self, inplanes, planes, stride=1, downsample=None, use_se=True):
        super(IRBlock, self).__init__()
        self.bn0 = nn.BatchNorm2d(inplanes)
        self.conv1 = conv3x3(inplanes, inplanes)
        self.bn1 = nn.BatchNorm2d(inplanes)
        self.prelu = nn.PReLU()
        self.conv2 = conv3x3(inplanes, planes, stride)
        self.bn2 = nn.BatchNorm2d(planes)
        self.downsample = downsample
        self.stride = stride
        self.use_se = use_se
        if self.use_se:
            self.se = SEBlock(planes)

    def forward(self, x):
        residual = x
        out = self.bn0(x)
        out = self.conv1(out)
        out = self.bn1(out)
        out = self.prelu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        if self.use_se:          # SE Block
            out = self.se(out)

        if self.downsample is not None:
            residual = self.downsample(x)

        out += residual
        out = self.prelu(out)

        return out
class SEBlock(nn.Module):
    def __init__(self, channel, reduction=16):
        super(SEBlock, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Sequential(
                nn.Linear(channel, channel // reduction),
                nn.PReLU(),
                nn.Linear(channel // reduction, channel),
                nn.Sigmoid()
        )

    def forward(self, x):
        b, c, _, _ = x.size()
        y = self.avg_pool(x).view(b, c)
        y = self.fc(y).view(b, c, 1, 1)
        return x * y
class ResNetFace(nn.Module):
    def __init__(self, block, layers, use_se=True):
        self.inplanes = 64
        self.use_se = use_se
        super(ResNetFace, self).__init__()
        self.conv1 = nn.Conv2d(1, 64, kernel_size=3, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.prelu = nn.PReLU()
        self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
        self.bn4 = nn.BatchNorm2d(512)
        self.dropout = nn.Dropout()
        self.fc5 = nn.Linear(512 * 8 * 8, 512)
        self.bn5 = nn.BatchNorm1d(512)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.xavier_normal_(m.weight)
            elif isinstance(m, nn.BatchNorm2d) or isinstance(m, nn.BatchNorm1d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.xavier_normal_(m.weight)
                nn.init.constant_(m.bias, 0)

    def _make_layer(self, block, planes, blocks, stride=1):
        downsample = None
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(planes * block.expansion),
            )
        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample, use_se=self.use_se))
        self.inplanes = planes
        for i in range(1, blocks):
            layers.append(block(self.inplanes, planes, use_se=self.use_se))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.prelu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.bn4(x)
        x = self.dropout(x)
        x = x.view(x.size(0), -1)
        x = self.fc5(x)
        x = self.bn5(x)

        return x

 

참고자료:

ArcFace 논문 version 1 : arxiv.org/pdf/1801.07698v1.pdf

반응형