안녕하세요, 오늘은 딥러닝을 사용해 닮은 꼴 연예인을 찾아보려고 합니다. Convolution Neural Network는 보통 이미지를 분류하는데 사용하는데요, 조금 생각을 바꿔보면 학습시킨 데이터 외의 사진이 주어졌을 때, 즉 연예인 사진으로 학습을 시키고, 일반인의 사진을 넣었을 때 얻은 결과로 이 일반인과 연예인이 닮았다고 생각할 수 있지 않을까요? 이러한 생각에서 프로젝트를 시작했습니다.

사용한 모델의 기본적인 구조는 VGG라는 모델을 사용했구요. 본격적으로 들어가보기 전에 Convolution Neural Network(이하 CNN)에 대한 간단한 설명부터 하고 시작해봅시다.

Before Convolution

아주아주 기초적인 Fully-Connected Layer 혹은 MLP를 생각해봅시다.

image

아주아주 기본적인 3층짜리 네트워크

image

모든 연산을 하나씩 하는 대신 행렬로 한번에 연산, 또한 batch라는 개념을 사용해 데이터를 여러개 쌓아 동시에 계산한다.

전체적인 연산은 행렬을 통해 이루어집니다. 중요한 것은 입력이 1차원의 벡터로써 주어진다는 부분입니다. 사람 얼굴 사진을 가지고 있다고 해봅시다. 주변의 여러 픽셀들이 모여서 눈을 구성하게 될 텐데요, 이 픽셀들을 그냥 일렬로 쭉 늘어놓게 되는 것입니다. 이렇게 되면 우리는 더 이상 입력의 어느부분이 눈이라고 얘기할 수 없게 되겠죠. 즉 입력의 위치 정보 가 사라지게 되는 겁니다. 이러한 문제를 어느정도 해결할 수 있는 방법이 CNN이 됩니다.

CNN 또한 완전한 위치정보를 활용하는 것은 아닙니다.

image

주변 의 위치정보만을 활용하기 때문에 왼쪽처럼 이상한 조합의 데이터도 얼굴이라고 판단할 수 있는 여지가 생깁니다. 이러한 문제를 해결하기 위해서 등장한 것이 Capsule Network입니다.

Convolution

CNN에서 핵심은 Convolution Layer입니다. 이전의 MLP처럼 입력을 쭉 늘어놓는 대신 원래 형태를 그대로 유지합니다.

image

Andrew Ng의 Coursera 강의에서

Convolution 연산에는 필터라는 것이 필요합니다. 가장 먼저 이 필터의 좌상단과 입력의 좌상단이 겹치도록 설정해줍니다. 그 다음 필터 안의 element들과 대응하는 입력의 element들을 곱하고, 이들 모두를 더한 값이 출력의 한 element가 됩니다.(초록색 사각형을 보시면 됩니다) 이러한 과정을 필터를 움직여주면서 반복해주면 끝입니다. 혹시 이해가 잘 안되신다면, CNN에 대해서 잘 설명해준 글들이 많으니 참조하시면 좋을 것 같습니다.

image

이외에도 CNN에는 Pooling layer가 존재합니다. 이 또한 Convolution 연산처럼 우리가 설정해준 필터의 크기만큼을 움직여가며 값들을 골라주는 과정이라고 생각하시면 됩니다. 보통 영역의 최대값을 선택하는 Max Pooling을 많이 사용합니다. 핵심 특징만을 골라내는 과정과 유사하죠.

VGG

CNN에 대한 설명은 이쯤에서 마무리하고 제가 사용한 모델인 VGG에 대한 설명으로 넘어가봅시다. ImageNet challenge라고 들어보신 적이 있으신가요? 정말 많은 사진들을 분류하는 대회인데요, 이 대회에서 좋은 성능을 거둔 모델입니다. 논문을 직접 읽어보고 싶으시다면 이 링크로 가보세요. 논문에서는 다양한 구조를 시도했습니다

image

이 중에서 저는 D에 있는 모델 구조를 사용하기로 했습니다. Convolution연산과 Max Pooling, 마지막에는 FC로 이루어진 전형적인 CNN의 구조를 갖고 있죠. Pytorch의 경우에는 이런 VGG 모델들이나 다른 유명한 모델들을 바로 불러올 수도 있습니다. 여기를 가보시면 VGG 이외에도 다른 모델들을 불러올 수 있죠. 하지만 저는 모델의 구조를 조금 변형시켰기 때문에 이 모델들을 바로 가져오는 대신 직접 구현했습니다.

class VGG(nn.Module):
    def __init__(self):
        super(VGG, self).__init__()
        self.cnn = nn.Sequential(
            # 3 x 128 x 128
            nn.Conv2d(3, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            # 64 x 64 x 64
            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            # 128 x 32 x 32
            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            # 256 x 16 x 16
            nn.Conv2d(256, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            # 512 x 8 x 8
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)
            # 512 x 4 x 4
        )
        self.fc = nn.Sequential(
            nn.Linear(512 * 4 * 4, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),

            nn.Linear(4096, 2048),
            nn.ReLU(),
            nn.Dropout(0.5),

            nn.Linear(2048, 30)
        )
    def forward(self, x):
        output = self.cnn(x)
        output = output.view(output.size(0), -1)
        output = self.fc(output)
        return output

pytorch로 구현한 변형 VGG, pytorch의 경우, Cross Entropy Loss를 구하는 과정에서 Softmax를 수행하므로 모델에서는 제외했습니다.

코드를 보시면 아시겠지만 전체적인 구조는 동일합니다. 그러나 입력(데이터)의 크기가 ImageNet과 조금 달라 구조를 조금 변형했습니다. 또한 갖고 있는 데이터의 수가 적은 편이라 생각해 뒷단 FC의 숫자도 조금 줄여 오버피팅을 조금이라도 줄여보고자 했습니다.

데이터

가장 중요한 것은 데이터를 구하는 것입니다. 기본적으로는 구글에서 크롤링을 하고자 했습니다. 크롤링 과정에 대해서 이런저런 고민을 되게 많이 했는데요, 좋은 라이브러리가 있어 이 라이브러리를 사용했습니다. 이미지 크롤러, multi threading까지 지원하기 때문에 이 라이브러리를 사용해 데이터를 모았습니다. 하지만 이렇게 크롤링해서 모은 데이터의 경우 얼굴을 제대로 판단할 수 없는 각도의 사진(옆모습)이나 지나치게 옛날 사진이 섞여 있는 등, 여러 문제가 있었습니다. 이러한 데이터들은 제가 직접 보면서 휴리스틱한 방법을 사용해 골라냈습니다.

어느정도 앞모습만 찍혀있는 사진을 골라냈는데요, 문제는 사진마다 크기가 제각각이라는 것이었습니다. 이를 해결하기 위해서 저와 다른 학회원들이 구현한 모듈을 사용했습니다(링크) 이 모듈을 사용하면 이런 결과를 얻을 수 있습니다.

image

얼굴을 인식해 잘라내주는 것뿐만 아니라 사진마다 다를 수 있는 눈, 코, 입의 위치 또한 함꼐 조정해주도록 했습니다. 이런 식으로 남자연예인 30명, 여자연예인 30명에 대한 사진을 모았고, 각각 연예인마다 100~150장 정도의 사진을 갖도록 했습니다.

결과

모은 데이터에 구현한 모델을 학습시켰고 얻은 결과는 다음과 같습니다.

image

image

얻은 결과들, 물론 가장 유사한 사진을 찾아주지는 못한다. 입력(왼쪽)이 주어졌을 때, 모델에서 가장 유사한 연예인을 얻어내고, 이 연예인의 사진 중에서 입력과 가장 유사한 사진을 직접 골랐다.

이런 식으로 흔히 “닮은 꼴” 연예인들의 사진을 넣어보니 그럭저럭 비슷한 연예인을 찾아줬습니다.

Futhermore

기회가 된다면 Activation Map을 사용해 얼굴의 어느 부분 때문에 이러한 결과를 얻게 되었는지를 찾아내도록 구현하려고합니다. 또한 가능하다면 가장 유사한 사진을 찾아주는데까지…