개요

VGGNet은 2014년 ILSVRC(ImageNet Large Scale Visual Recognition Challenge)의 Classification+Localization 분야에서 1, 2등을 했을 만큼 당시에 Classification 분야에서 좋은 성능을 낸 대표적인 모델 중 하나이다. VGG는 Visual Geometry Group의 약자로 연구팀 이름으로, VGG뒤의 숫자는 신경망 모델의 깊이(레이어 수)를 나타낸다.

모델의 구조

  • Input : 224 x 224 RGB image
  • 이미지 전처리 : train set의 RGB 평균값을 각 픽셀로 부터 계산하여 빼줌
  • filter : 3 x 3, 1 x 1(input channel 에서 linear transformation 할 때)
  • stride = 1, padding = same
  • max-pooling : 2 x 2, stride = 2
  • 3개의 FC Layer : 4096 → 4096 → 1000(classification, softmax)
  • activation function : ReLU
  • total 144M parameter(VGG-19)

VGG Architecture

모델의 구성

아래의 표 1에서 볼 수 있듯이, A-E라는 이름의 모델들을 11개의 layer(8 conv. + 3FC layers) 부터 최대 19개의 layer(16 conv. + 3FC layers)까지 모델의 깊이를 변화시켜가며 실험을 진행하였다.

VGGNet은 이전에 7x7의 filter를 사용하는 모델들과 달리 상하좌우의 feature를 추출할 수 있는 가장 작은 크기의 filter인 3x3을 사용하였다. filter의 크기를 줄이는 대신 layer를 늘려 7x7 filter와 동일한 receptive field를 갖도록 했다.

출처 : Research Gate

3x3 filter를 사용함으로써 얻는 이점은 다음과 같다.

  1. 결정 함수의 비선형성 증가 7x7 filter를 사용했을 땐 activation function을 한 번 밖에 통과하지 않지만, 3x3 filter의 3개 layer를 사용했을 땐 activation function을 3번 통과하므로 함수의 비선형성이 증가하게 된다. 따라서 모델이 더 복잡한 특징도 추출할 수 있게 된다.
  2. 학습 파라미터 수의 감소 7x7 filter를 사용한 C채널의 학습 파라미터 수는 $7^2C^2$로 총 $49C^2$이다. 그러나 3x3 filter를 사용한 3개 layer의 학습 파라미터 수는 $3(3^2C^2)$으로 총 $27C^2$이다. 7x7과 3x3의 3layer는 동일한 receptive field를 가지지만 parameter 수는 약 81% 감소했다. 따라서 학습시간에 있어서 더 유리하게 된다.

모델 학습

VGGNet은 다음과 같은 최적화 방법을 사용하여 모델을 학습시켰다.

  • mini-batch gradient descent
  • Momentum (0.9)
  • batch_size = 256
  • weight decay (L2 = 5e10-4)
  • Dropout (0.5)
  • Initial learning rate = 0.01
  • 모델을 학습하면서 validation set accuracy가 증가하지 않을 때 learning rate를 1/10으로 감소시켰으며, 370K iterations (74 epochs)를 진행하는 동안 3번의 learning rate 감소가 이루어졌다.
  • Initial weight는 N(0, 0.01^2)을 따르는 정규분포에서 sampling
  • Initial bias = 0
  • 고정된 224x224의 image를 얻기 위해 train image를 random하게 crop하거나 rescale하였고, 더 나아가 augmentation을 위해 random하게 수평으로 flip하거나 RGB color를 shift 하였다.

평가

  1. local response normalization (A-LRN network)을 사용한 것은 큰 효과가 없었다. 따라서 더 깊은 레이어(B-E)에서 normalization을 사용하지 않았다.
  2. ConvNet의 깊이가 증가함에 따라 classification error가 감소하는 것을 확인할 수 있었다. 해당 Dataset에서는 19개의 layer로도 error rate가 saturated 되었지만, 더 큰 Dataset에서는 더 깊은 layer가 유용할 수 있다.

결론

  • ConvNet에서 layer의 깊이를 증가시키면 classification accuracy를 향상시키는 데 도움이 되며, ImageNet challenge dataset에 대한 state-of-the-art 성능을 얻을 수 있음을 증명하였다.

해당 모델을 코드로는 구현 해보았으나, 실제로 학습을 시켜보진 않았다. 150GB나 되는 ImageNet Dataset을 받아서 학습을 시킬 엄두가 나질 않았기 때문이다. ImageNet을 학습시키는 데 걸리는 시간은 P100 256개를 병렬로 연결해도 1시간이나 걸리는 것으로 알고 있는데, 단일 GPU를 사용하면 아마 몇 주 단위의 시간이 소요될 것이기 때문에 어쩔 수 없었다.

 

이 논문을 봤을 때, 딥러닝은 역시 컴퓨팅 파워가 중요하다고 생각했다. 과연 2014년 이전에는 deep network에 대한 인식이 없었을까? 물론 이전에도 deep network를 위한 시도는 있었으나 컴퓨팅 파워의 부족으로 ILSVRC의 dataset를 학습시킬 수 없었기 때문에 할 수 없었던 것으로 보인다. 분명 2021년인 지금도 엄청나게 깊은 deep network에 대한 시도는 계속 되고 있을 것이라고 생각한다. 다만 해당 network를 학습시킬 만한 컴퓨팅 파워가 부족하기 때문에 해당 network의 성능을 확인할 수 없을 뿐이라고 생각한다. 만약 딥러닝의 'deep'이 정말로 우리가 생각도 못할 만큼 'deep' 해야 한다면? 지금의 딥러닝 모델들이 이제 겨우 시작이라고 한다면?

 

실제 구현 코드는 깃허브

  오늘 아침부터 오종인 멘토님과 몇 명의 카뎃들과 함께 개발자 이력서를 작성해보는 '켠김에 이력서까지'를 진행했다. 신청했을 당시에는 내가 뭘 한 게 없는데 이력서를 쓸 수 있나? 라고 생각했었는데, 정답이었다. 나는 정말 아무것도 한 게 없었던 것이다.

 

  먼저, 오전 10시부터 시작하여 한 시간 정도 이력서를 작성해본 후 피드백을 받기로 하였다. 지금까지 그래왔듯이 가벼운 마음으로 이력서를 작성하였다. 핵심 역량에는 평소에 내가 잘 한다고 생각했던 것들을 적었다. 이전 직장의 경력이 기술영업 직군이어서 쓸지 말지 고민했지만, 일단 쓰기로 했다. 쓰다보니 금세 한 시간이 지나 피드백을 받았다. 결과는 너무 탈탈 털려서 뼈는 커녕 살까지 분쇄되어 다짐육이 될 정도였다. 내가 적응력이 뛰어난 걸 증명할 수 있는가? 뛰어난 같은 수식어구가 굳이 필요한가? 다양한 개발 언어 활용 능력이 중요한가? 원만한 성격인 것은 어떻게 증명할 수 있는가?

 

  핵심 역량은 내가 증명할 수 없는 것이면 쓰지 않는 편이 좋다고 한다. 생각해보니 위 질문들에 대해 한 번도 대답을 생각해본 적이 없는 것 같다. 내가 잘 하는 것을 어떻게 증명해야 하는가. 답은 간단하다. 증명 가능한 사실만 적으면 되는 것이다. 본인의 어떤 경험을 살려 해당 역량을 증명할 수 있으면 적으면 된다.

 

  나는 영업직으로 일했던 경험이 커뮤니케이션 능력을 증명해줄 수 있다고 생각했는데 전혀 아니었다. 오히려 영업직이라고 하면 자신의 실적을 위해 수단과 방법을 가리지 않는 사람이라는 부정적인 시선으로 볼 수 있다는 이야기를 들었다. 물론 내가 실제로 그런 사람은 아니지만, 그런 생각을 가진 사람도 있을 수 있다는 생각에 말문이 막혔다. 어차피 저는 아닙니다 라고 해봤자 증명할 수도 없고, 변명거리만 될 뿐이니까. 그래서 내가 증명할 수 없는 것들을 모두 빼보았다.

 

텅 빈 이력서

    다 지우고 나니, 이력서가 텅텅 비어버렸다. 그렇다. 정말 아무것도 한 게 없었다는 걸 깨달았다. 그나마 AIFFEL에서 머신러닝 관련해서 배우는 게 있으니 관련 Framework를 쓸 수 있는 정도였다. 그러나 그마저도 AI 엔지니어를 목표로 하기 위해서는 AI 도메인보다 개발 지식이 더 중요한 것이었다.

 

  나도 AIFFEL을 시작하기 전에는 인공지능에 대해 공부하기만 하면 개발을 잘 못해도 설계만 잘 하면 충분히 취업시장에서 경쟁력이 있다고 생각했다. 그러나 공부를 하면 할 수록 인공지능에 대한 지식만 알아서는 할 수 있는게 크게 없다는 것을 깨달았다. 설계를 잘 하는 사람? AI Researcher가 있는데 굳이 AI Engineer를? 딥러닝 모델 설계를 잘 하는 사람은 내가 아니어도 많을 것이다. 딥러닝 모델을 설계하기 이전에 해당 문제를 딥러닝으로 해결할 수 있는지 부터 판단할 수 있어야 하며, 해당 모델을 어디에서 사용할 것인지, 자원은 얼만큼 있는지, 처리 속도는 얼마나 중요한지, 어느 정도의 정확도가 요구되는지 등은 모델만 잘 설계한다고 해서 알 수 있는 것들이 아니었다. 따라서 인공지능에 대해 공부하기 이전에 개발 지식을 먼저 습득하고, 해당 분야의 도메인을 어느 정도 파악한 후에 해당 문제를 딥러닝으로 해결할 수 있는지를 판단할 줄 알아야 하는 것이다. 이 부분은 인지하고 있었지만 멘토님께 팩트로 맞음 당해서 다시 한 번 깨닫게 되었다.

 

   프로젝트의 중요성에 대해서도 다시 깨닫게 되었다. 같이 켠김에 이력서를 진행했던 hyukim님의 이력서도 보게되었는데, 그 동안 진행했던 것들이 노션에 잘 정리되어 있었다. 그 노션을 보고 2차로 스스로 맞음 당했다. 그동안 진행했던 프로젝트들이 잘 정리되어 있었고(내 기준), 그걸 보고 더 열심히 해야겠다는 생각이 들었다. 물론 지금도 열심히 안 하고 있었던 것은 아니지만 아무튼 더 열심히 해야 한다고 생각했다. 멘토님께서는 프로젝트에서 뭘 했는지 보다 해당 프로젝트를 진행하면서 어떤 기술을 써봤고, 왜 이 기술을 사용했으며, 거기서 무엇을 배웠는지를 잘 정리하는게 중요하다고 하셨다. 이런 부분은 나중에 프로젝트를 할 때 까먹지 말고 잘 적어두어야겠다.

 

  결국 나는 하루 종일 후드려 맞음 당하기만 했으나, 자기 반성의 시간을 가지면서 내가 앞으로 무엇을 해야 할지에 대한 방향성을 정하는 날이었다. 비록 지금은 아무것도 적혀진 게 없는 null뿐인 이력서지만, 추후에는 멘토님께서 기업에 '이 카뎃 잘해요' 라고 당당하게 추천할 수 있는 이력서를 만들어야겠다.

 

 

카뎃 여러분, 이력서 꼭 미리미리 준비합시다!

'잡담' 카테고리의 다른 글

2021년 회고  (0) 2022.01.01
첫 입사, 수습기간, 정직원  (2) 2021.11.23
1년간의 회고  (0) 2021.05.20

11. 뉴스 요약봇 만들기

학습 목표


  • Extractive/Abstractive summarization 이해하기
  • 단어장 크기를 줄이는 다양한 text normalization 적용해보기
  • seq2seq의 성능을 Up시키는 Attention Mechanism 적용하기

텍스트 요약(Text Summarization)이란?

텍스트 요약(Text Summarization)이란 위 그림과 같이 긴 길이의 문서(Document) 원문을 핵심 주제만으로 구성된 짧은 요약(Summary) 문장들로 변환하는 것을 말한다. 상대적으로 큰 텍스트인 뉴스 기사로 작은 텍스트인 뉴스 제목을 만들어내는 것이 텍스트 요약의 대표적인 예로 볼 수 있다.

이때 중요한 것은 요약 전후에 정보 손실 발생이 최소화되어야 한다는 점이다. 이것은 정보를 압축하는 과정과 같다. 비록 텍스트의 길이가 크게 줄어들었지만, 요약문은 문서 원문이 담고 있는 정보를 최대한 보존하고 있어야 한다. 이것은 원문의 길이가 길수록 만만치 않은 어려운 작업이 된다. 사람이 이 작업을 수행한다 하더라도 긴 문장을 정확하게 읽고 이해한 후, 그 의미를 손상하지 않는 짧은 다른 표현으로 원문을 번역해 내야 하는 것이다.

그렇다면 요약 문장을 만들어 내려면 어떤 방법을 사용하면 좋을까? 여기서 텍스트 요약은 크게 추출적 요약(Extractive Summarization)과 추상적 요약(Abstractive Summarization)의 두가지 접근으로 나누어볼 수 있다.

(1) 추출적 요약(Extractive Summarization)


첫번째 방식인 추출적 요약은 단어 그대로 원문에서 문장들을 추출해서 요약하는 방식이다. 가령, 10개의 문장으로 구성된 텍스트가 있다면, 그 중 핵심적인 문장 3개를 꺼내와서 3개의 문장으로 구성된 요약문을 만드는 식이다. 그런데 꺼내온 3개의 문장이 원문에서 중요한 문장일 수는 있어도, 3개의 문장의 연결이 자연스럽지 않을 수는 있다. 결과로 나온 문장들 간의 호응이 자연스럽지 않을 수 있다는 것이다. 딥 러닝보다는 주로 전통적인 머신 러닝 방식에 속하는 텍스트랭크(TextRank)와 같은 알고리즘을 사용한다.

(2) 추상적 요약(Abstractive Summarization)


두번째 방식인 추상적 요약은 추출적 요약보다 좀 더 흥미로운 접근을 사용한다. 원문으로부터 내용이 요약된 새로운 문장을 생성해내는 것이다. 여기서 새로운 문장이라는 것은 결과로 나온 문장이 원문에 원래 없던 문장일 수도 있다는 것을 의미한다. 자연어 처리 분야 중 자연어 생성(Natural Language Generation, NLG)의 영역인 셈이다. 반면, 추출적 요약은 원문을 구성하는 문장 중 어느 것이 요약문에 들어갈 핵심문장인지를 판별한다는 점에서 문장 분류(Text Classification) 문제로 볼 수 있을 것이다.

인공 신경망으로 텍스트 요약 훈련시키기

seq2seq 모델을 통해서 Abstractive summarization 방식의 텍스트 요약기를 만들어보자. seq2seq은 두 개의 RNN 아키텍처를 사용하여 입력 시퀀스로부터 출력 시퀀스를 생성해내는 자연어 생성 모델로 주로 뉴럴 기계번역에 사용되지만, 원문을 요약문으로 번역한다고 생각하면 충분히 사용 가능할 것이다.

seq2seq 개요


원문을 첫번째 RNN인 인코더로 입력하면, 인코더는 이를 하나의 고정된 벡터로 변환한다. 이 벡터를 문맥 정보를 가지고 있는 벡터라고 하여 컨텍스트 벡터(context vector)라고 한다. 두번째 RNN인 디코더는 이 컨텍스트 벡터를 전달받아 한 단어씩 생성해내서 요약 문장을 완성한다.

LSTM과 컨텍스트 벡터


LSTM이 바닐라 RNN과 다른 점은 다음 time step의 셀에 hidden state뿐만 아니라, cell state도 함께 전달한다는 점이다. 다시 말해, 인코더가 디코더에 전달하는 컨텍스트 벡터 또한 hidden state h와 cell state c 두 개의 값 모두 존재해야 한다는 뜻이다.

시작 토큰과 종료 토큰


[시작 토큰 SOS와 종료 토큰 EOS는 각각 start of a sequence와 end of a sequence를 나타낸다]

seq2seq 구조에서 디코더는 시작 토큰 SOS가 입력되면, 각 시점마다 단어를 생성하고 이 과정을 종료 토큰 EOS를 예측하는 순간까지 멈추지않는다. 다시 말해 훈련 데이터의 예측 대상 시퀀스의 앞, 뒤에는 시작 토큰과 종료 토큰을 넣어주는 전처리를 통해 어디서 멈춰야하는지 알려줄 필요가 있다.

어텐션 메커니즘을 통한 새로운 컨텍스트 벡터 사용하기

기존의 seq2seq는 인코더의 마지막 time step의 hidden state를 컨텍스트 벡터로 사용했다. 하지만 RNN 계열의 인공 신경망(바닐라 RNN, LSTM, GRU)의 한계로 인해 이 컨텍스트 정보에는 이미 입력 시퀀스의 많은 정보가 손실이 된 상태가 된다.

이와 달리, 어텐션 메커니즘(Attention Mechanism)은 인코더의 모든 step의 hidden state의 정보가 컨텍스트 벡터에 전부 반영되도록 하는 것이다. 하지만 인코더의 모든 hidden state가 동일한 비중으로 반영되는 것이 아니라, 디코더의 현재 time step의 예측에 인코더의 각 step이 얼마나 영향을 미치는지에 따른 가중합으로 계산되는 방식이다.

여기서 주의해야 할 것은, 컨텍스트 벡터를 구성하기 위한 인코더 hidden state의 가중치 값은 디코더의 현재 스텝이 어디냐에 따라 계속 달라진다는 점이다. 즉, 디코더의 현재 문장 생성 부위가 주어부인지 술어부인지 목적어인지 등에 따라 인코더가 입력 데이터를 해석한 컨텍스트 벡터가 다른 값이 된다. 이와 달리, 기본적인 seq2seq 모델에서 컨텍스트 벡터는 디코더의 현재 스텝 위치에 무관하게 한번 계산되면 고정값을 가진다.

유용한 링크

https://stackoverflow.com/questions/19790188/expanding-english-language-contractions-in-python 불용어 사전

10. 인물사진 모드로 사진찍기

인물사진 모드란, 피사체를 촬영할 때 배경이 흐려지는 효과를 넣어주는 것을 이야기 한다. 물론 DSLR의 아웃포커스 효과와는 다른 방식으로 구현한다.

핸드폰 카메라의 인물사진 모드는 듀얼 카메라를 이용해 아웃포커싱 기능을 흉내낸다.

  • DSLR에서는 사진을 촬영할 때 피사계 심도(depth of field, DOF)를 얕게 하여 초점이 맞은 피사체를 제외한 배경을 흐리게 만든다.
  • 핸드폰 인물사진 모드는 화각이 다른 두 렌즈를 사용한다. 일반(광각) 렌즈에서는 배경을 촬영하고 망원 렌즈에서는 인물을 촬영한 뒤 배경을 흐리게 처리한 후 망원 렌즈의 인물과 적절하게 합성한다.

인물사진 모드에서 사용되는 용어

한국에서는 배경을 흐리게 하는 기술을 주로 '아웃포커싱'이라고 표현한다. 하지만 아웃포커싱은 한국에서만 사용하는 용어이고 정확한 영어 표현은 얕은 피사계 심도(shallow depth of field) 또는 셸로우 포커스(shallow focus) 라고 한다.

학습 목표


  • 딥러닝을 이용하여 핸드폰 인물 사진 모드를 모방해본다.

셸로우 포커스 만들기

(1) 사진준비


하나의 카메라로 셸로우 포커스(shallow focus)를 만드는 방법은 다음과 같다.

이미지 세그멘테이션(Image segmentation) 기술을 이용하면 하나의 이미지에서 배경과 사람을 분리할 수 있다. 분리된 배경을 블러(blur)처리 후 사람 이미지와 다시 합하면 아웃포커싱 효과를 적용한 인물사진을 얻을 수 있다.

# 필요 모듈 import
import cv2
import numpy as np
import os
from glob import glob
from os.path import join
import tarfile
import urllib

from matplotlib import pyplot as plt
import tensorflow as tf

# 이미지 불러오기
import os
img_path = os.getenv('HOME')+'/aiffel/human_segmentation/images/2017.jpg'  # 본인이 선택한 이미지의 경로에 맞게 바꿔 주세요. 
img_orig = cv2.imread(img_path) 
print (img_orig.shape)

(2) 세그멘테이션으로 사람 분리하기


배경에만 렌즈흐림 효과를 주기 위해서는 이미지에서 사람과 배경을 분리해야 한다. 흔히 포토샵으로 '누끼 따기'와 같은 작업을 이야기한다.

세그멘테이션(Segmentation)이란?

이미지에서 픽셀 단위로 관심 객체를 추출하는 방법을 이미지 세그멘테이션(image segmentation) 이라고 한다. 이미지 세그멘테이션은 모든 픽셀에 라벨(label)을 할당하고 같은 라벨은 "공통적인 특징"을 가진다고 가정한다. 이 때 공통 특징은 물리적 의미가 없을 수도 있다. 픽셀이 비슷하게 생겼다는 사실은 인식하지만, 우리가 아는 것처럼 실제 물체 단위로 인식하지 않을 수 있다. 물론 세그멘테이션에는 여러 가지 세부 태스크가 있으며, 태스크에 따라 다양한 기준으로 객체를 추출한다.

시멘틱 세그맨테이션(Semantic segmentation)이란?

세그멘테이션 중에서도 특히 우리가 인식하는 세계처럼 물리적 의미 단위로 인식하는 세그멘테이션을 시맨틱 세그멘테이션 이라고 한다. 쉽게 설명하면 이미지에서 픽셀을 사람, 자동차, 비행기 등의 물리적 단위로 분류(classification)하는 방법이라고 할 수 있다.

인스턴스 세그멘테이션(Instance segmentation)이란?

시맨틱 세그멘테이션은 '사람'이라는 추상적인 정보를 이미지에서 추출해내는 방법이다. 그래서 사람이 누구인지 관계없이 같은 라벨로 표현이 된다. 더 나아가서 인스턴스 세그멘테이션은 사람 개개인 별로 다른 라벨을 가지게 한다. 여러 사람이 한 이미지에 등장할 때 각 객체를 분할해서 인식하자는 것이 목표이다.

이미지 세그멘테이션의 간단한 알고리즘 : 워터쉐드 세그멘테이션(watershed segmentation)

이미지에서 영역을 분할하는 가장 간단한 방법은 물체의 '경계'를 나누는 것이다. 이미지는 그레이스케일(grayscale)로 변환하면 0~255의 값을 가지기 때문에 픽셀 값을 이용해서 각 위치의 높고 낮음을 구분할 수 있다. 따라서 낮은 부분부터 서서히 '물'을 채워 나간다고 생각할 때 각 영역에서 점점 물이 차오르다가 넘치는 시점을 경계선으로 만들면 물체를 서로 구분할 수 있게 된다.

(3) 시멘틱 세그멘테이션 다뤄보기


세그멘테이션 문제에는 FCN, SegNet, U-Net 등 많은 모델이 사용된다. 오늘은 그 중에서 DeepLab이라는 세그멘테이션 모델을 만들고 모델에 이미지를 입력할 것이다. DeepLab 알고리즘(DeepLab v3+)은 세그멘테이션 모델 중에서도 성능이 매우 좋아 최근까지도 많이 사용되고 있다.

DeepLabDemo : https://github.com/tensorflow/models/blob/master/research/deeplab/deeplab_demo.ipynb

# DeepLabModel Class 정의
class DeepLabModel(object):
    INPUT_TENSOR_NAME = 'ImageTensor:0'
    OUTPUT_TENSOR_NAME = 'SemanticPredictions:0'
    INPUT_SIZE = 513
    FROZEN_GRAPH_NAME = 'frozen_inference_graph'

    # __init__()에서 모델 구조를 직접 구현하는 대신, tar file에서 읽어들인 그래프구조 graph_def를 
    # tf.compat.v1.import_graph_def를 통해 불러들여 활용하게 됩니다. 
    def __init__(self, tarball_path):
        self.graph = tf.Graph()
        graph_def = None
        tar_file = tarfile.open(tarball_path)
        for tar_info in tar_file.getmembers():
            if self.FROZEN_GRAPH_NAME in os.path.basename(tar_info.name):
                file_handle = tar_file.extractfile(tar_info)
                graph_def = tf.compat.v1.GraphDef.FromString(file_handle.read())
                break
        tar_file.close()

        with self.graph.as_default():
            tf.compat.v1.import_graph_def(graph_def, name='')

        self.sess = tf.compat.v1.Session(graph=self.graph)

    # 이미지를 전처리하여 Tensorflow 입력으로 사용 가능한 shape의 Numpy Array로 변환합니다.
    def preprocess(self, img_orig):
        height, width = img_orig.shape[:2]
        resize_ratio = 1.0 * self.INPUT_SIZE / max(width, height)
        target_size = (int(resize_ratio * width), int(resize_ratio * height))
        resized_image = cv2.resize(img_orig, target_size)
        resized_rgb = cv2.cvtColor(resized_image, cv2.COLOR_BGR2RGB)
        img_input = resized_rgb
        return img_input

    def run(self, image):
        img_input = self.preprocess(image)

        # Tensorflow V1에서는 model(input) 방식이 아니라 sess.run(feed_dict={input...}) 방식을 활용합니다.
        batch_seg_map = self.sess.run(
            self.OUTPUT_TENSOR_NAME,
            feed_dict={self.INPUT_TENSOR_NAME: [img_input]})

        seg_map = batch_seg_map[0]
        return cv2.cvtColor(img_input, cv2.COLOR_RGB2BGR), seg_map

preprocess()는 전처리, run()은 실제로 세그멘테이셔는 하는 함수이다. input tensor를 만들기 위해 전처리를 하고, 적절한 크기로 resize하여 OpenCV의 BGR 채널은 RGB로 수정 후 run()함수의 input parameter로 사용된다.

# Model 다운로드
# define model and download & load pretrained weight
_DOWNLOAD_URL_PREFIX = 'http://download.tensorflow.org/models/'

model_dir = os.getenv('HOME')+'/aiffel/human_segmentation/models'
tf.io.gfile.makedirs(model_dir)

print ('temp directory:', model_dir)

download_path = os.path.join(model_dir, 'deeplab_model.tar.gz')
if not os.path.exists(download_path):
    urllib.request.urlretrieve(_DOWNLOAD_URL_PREFIX + 'deeplabv3_mnv2_pascal_train_aug_2018_01_29.tar.gz',
                   download_path)

MODEL = DeepLabModel(download_path)
print('model loaded successfully!')

위 코드는 구글이 제공하는 pretrained weight이다. 이 모델은 PASCAL VOC 2012라는 대형 데이터셋으로 학습된 v3 버전이다.

# 이미지 resize
img_resized, seg_map = MODEL.run(img_orig)
print (img_orig.shape, img_resized.shape, seg_map.max()) # cv2는 채널을 HWC 순서로 표시

# seg_map.max() 의 의미는 물체로 인식된 라벨 중 가장 큰 값을 뜻하며, label의 수와 일치

# PASCAL VOC로 학습된 label
LABEL_NAMES = [
    'background', 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus',
    'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike',
    'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tv'
]
len(LABEL_NAMES)

# 사람을 찾아 시각화
img_show = img_resized.copy()
seg_map = np.where(seg_map == 15, 15, 0) # 예측 중 사람만 추출 person의 label은 15
img_mask = seg_map * (255/seg_map.max()) # 255 normalization
img_mask = img_mask.astype(np.uint8)
color_mask = cv2.applyColorMap(img_mask, cv2.COLORMAP_JET)
img_show = cv2.addWeighted(img_show, 0.6, color_mask, 0.35, 0.0)

plt.imshow(cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB))
plt.show()

(4) 세그멘테이션 결과를 원래 크기로 복원하기


DeepLab 모델을 사용하기 위해 이미지 크기를 작게 resize했기 때문에 원래 크기로 복원해야 한다.

# 이미지 사이즈 비교
img_mask_up = cv2.resize(img_mask, img_orig.shape[:2][::-1], interpolation=cv2.INTER_LINEAR)
_, img_mask_up = cv2.threshold(img_mask_up, 128, 255, cv2.THRESH_BINARY)

ax = plt.subplot(1,2,1)
plt.imshow(img_mask_up, cmap=plt.cm.binary_r)
ax.set_title('Original Size Mask')

ax = plt.subplot(1,2,2)
plt.imshow(img_mask, cmap=plt.cm.binary_r)
ax.set_title('DeepLab Model Mask')

plt.show()

cv2.resize() 함수를 이용하여 원래 크기로 복원하였다. 크기를 키울 때 보간(interpolation)을 고려해야 하는데, cv2.INTER_NEAREST를 이용하면 깔끔하게 처리할 수 있지만 더 정확하게 확대하기 위해 cv2.INTER_LINEAR를 사용하였다.

결과적으로 img_mask_up 은 경계가 블러된 픽셀값 0~255의 이미지를 얻는다. 확실한 경계를 다시 정하기 위해 중간값인 128을 기준으로 임계값(threshold)을 설정하여 128 이하의 값은 0으로 128 이상의 값은 255로 만들었다.

(5) 배경 흐리게 하기


# 배경 이미지 얻기
img_mask_color = cv2.cvtColor(img_mask_up, cv2.COLOR_GRAY2BGR)
img_bg_mask = cv2.bitwise_not(img_mask_color)
img_bg = cv2.bitwise_and(img_orig, img_bg_mask)
plt.imshow(img_bg)
plt.show()

bitwise_not : not 연산하여 이미지 반전

bitwise_and : 배경과 and 연산하여 배경만 있는 이미지를 얻을 수 있다.

# 이미지 블러처리
img_bg_blur = cv2.blur(img_bg, (13,13))
plt.imshow(cv2.cvtColor(img_bg_blur, cv2.COLOR_BGR2RGB))
plt.show()

(6) 흐린 배경과 원본 영상 합성


# 255인 부분만 원본을 가져온 후 나머지는 blur 이미지
img_concat = np.where(img_mask_color==255, img_orig, img_bg_blur)
plt.imshow(cv2.cvtColor(img_concat, cv2.COLOR_BGR2RGB))
plt.show()

회고록

  • 평소에 사진을 취미로 하고 있어서 이번 과제를 이해하는 것이 어렵지 않았다.
  • 이미지 세그멘테이션에 대해 단어만 알고 정확히 어떤 역할을 하는 기술인지 잘 몰랐는데 이번 학습을 통해 확실하게 알 수 있었다.
  • 이미지를 블러처리 하는 과정에서 인물과 배경의 경계 부분도 같이 블러처리 되어버리기 때문에 나중에 이미지를 병합하는 과정에서 배경에 있는 인물 영역이 실제 사진의 인물 영역보다 넓어져 병합 후의 이미지에서 경계 주위로 그림자(?)가 지는 것 같은 현상을 볼 수 있었다.
  • 앞으로 자율주행이나 인공지능을 이용해서 자동화 할 수 있는 부분에는 어디든 사용될 수 있을 법한 그런 기술인 것 같다.
  • 이 기술을 이용해서 병, 캔, 플라스틱 과 같은 재활용 쓰레기를 구분할 수 있다고 하면, 환경 분야에서도 쓸 수 있지 않을까...? 그 정도로 구분하는 것은 어려울 것 같긴 하다.
  • jupyter notebook에서 생성된 이미지를 깔끔하게 보이도록 정리하려고 html을 이용하여 사진을 정렬했었는데 github에선 html style이 무시된다는 것을 알게되어 3시간 정도를 이미지 정렬하는 데 썼다... 결국 안 된다는 것을 깨닫고 pyplot을 이용해서 비교하였다.

유용한 링크

https://blog.lunit.io/2018/07/02/deeplab-v3-encoder-decoder-with-atrous-separable-convolution-for-semantic-image-segmentation/ DeepLab V3+

https://arxiv.org/abs/1610.02357 Xception: Deep Learning with Depthwise Separable Convolutions

https://github.com/tensorflow/models/blob/master/research/deeplab/g3doc/model_zoo.md pretrained weight

https://opencv-python.readthedocs.io/en/latest/doc/10.imageTransformation/imageTransformation.html opencv image transformation

https://stackoverflow.com/questions/32774956/explain-arguments-meaning-in-res-cv2-bitwise-andimg-img-mask-mask bitwise 사용하여 이미지 바꾸기

https://numpy.org/doc/stable/reference/generated/numpy.where.html

오늘은 지난 2월 2일에 진행했던 캐글 경진대회 첫 참가한 소감을 적어볼 예정이다.

 

www.kaggle.com/c/2019-2nd-ml-month-with-kakr/overview

 

2019 2nd ML month with KaKR

캐글 코리아와 함께하는 2nd ML 대회 - House Price Prediction

www.kaggle.com

이미 지난 대회였기 때문에 각종 솔루션들이 많이 올라와 있어서 컴페티션에 처음 참가하는 나와 같은 사람들에게는 입문하기 좋은 대회였던 것 같다. 인공지능 분야 공부를 시작하면서 꼭 달성하고 싶은 목표 중 하나가 캐글 컴페티션에서 메달을 얻어보는 것인데, 그 시작의 첫 걸음이라고 생각했다.

 

첫 참가라 어떻게 해야 할지 막막했지만, 우선 Baseline이 되는 노트북을 하나 지정하여 해당 Baseline을 기준으로 성능을 향상시켜 나가기로 하였다. 목표는 Private Score를 11만점 이하로 낮추는 것이다.

 

데이터를 먼저 살펴보면 다음과 같은 데이터들이 있다.

  1. ID : 집을 구분하는 번호
  2. date : 집을 구매한 날짜
  3. price : 집의 가격(Target variable)
  4. bedrooms : 침실의 수
  5. bathrooms : 화장실의 수
  6. sqft_living : 주거 공간의 평방 피트(면적)
  7. sqft_lot : 부지의 평방 피트(면적)
  8. floors : 집의 층 수
  9. waterfront : 집의 전방에 강이 흐르는지 유무 (a.k.a. 리버뷰)
  10. view : 집이 얼마나 좋아 보이는지의 정도
  11. condition : 집의 전반적인 상태
  12. grade : King County grading 시스템 기준으로 매긴 집의 등급
  13. sqft_above : 지하실을 제외한 평방 피트(면적)
  14. sqft_basement : 지하실의 평방 피트(면적)
  15. yr_built : 지어진 년도
  16. yr_renovated : 집을 재건축한 년도
  17. zipcode : 우편번호
  18. lat : 위도
  19. long : 경도
  20. sqft_living15 : 주변 15 가구의 거실 크기의 평균
  21. sqft_lot15 : 주변 15 가구의 부지 크기의 평균

먼저 데이터를 모델에 학습시키기 전에 모델이 잘 학습할 수 있도록 데이터를 가공해주는 것이 중요하다. 데이터를 가공하는 기법 중 하나인 FE(Feature Engineerning)는 그대로 사용하기에는 어려운 데이터의 변수를 가공하여 데이터를 비교적 간단명료하게 만드는 작업이다. 실제로 대회를 진행하다보니 이 FE을 얼만큼 잘 했느냐에 따라서 점수가 생각보다 많이 차이났었다.

target인 price의 분포를 확인해보니 선형으로 표현하긴 어려울 것 같아서 log scale로 변환하였다.

이제 어느 정도 선형성을 가지는 것 같다.

위의 그림은 스피어만 순위 상관계수를 히트맵으로 나타내보았다. 스피어만 순위 상관계수는 범주형 변수가 포함되어 있을 때, 목적변수와 가장 상관관계가 높은 순서대로 정렬해주는 식이다. 위의 히트맵에 따르면 가격과 가장 상관관계가 높은 변수들은 grade, sqft_living, sqft_living15 순으로 나타나는 것을 확인할 수 있다.

이상치를 제거하기 위하여 몇 가지 변수들을 살펴보았다. 그 중 grade에서는 다음과 같은 요소들을 확인해보고 이상치라고 생각되는 값들은 제거해주었다.

  • 등급 3의 값이 큰 이유
  • 등급 7, 8, 9에서 이상치가 많은 이유
  • 등급 8과 11에서 큰 이상치가 나타나는 이유

모델이 더 잘 학습할 수 있도록 기존에 있는 변수들을 활용하여 새로운 Feature들을 생성해주었다.

새로 생성한 Feature에는 방의 전체 갯수, 거실의 비율, 면적대비 거실의 비율, 재건축 여부, 평당 가격, zipcode를 이용하여 위치에 따른 가격 등이 있다.

실제로 zipcode를 기준으로 평균 가격정보를 시각화 해보았더니 어느 정도 유의미한 변수인 것으로 볼 수 있었다.

FE를 끝내고 나면, train data에서 target인 price를 분리하여 train data를 모델에 학습시킨다.

사용한 모델은 XGBoost와 LightGBM 여러 개의 모델을 VotingRegressor로 앙상블 하여 사용하였다.

xgboost = XGBRegressor(learning_rate=0.024, max_depth=8, n_estimators=1000, random_state=random_state)
lightgbm0 = LGBMRegressor(boosting='goss', learning_rate=0.015, max_depth=7, n_estimators=1600, random_state=random_state)
lightgbm1 = LGBMRegressor(boosting='gbdt', learning_rate=0.015, max_depth=13, n_estimators=1400, random_state=random_state)
lightgbm2 = LGBMRegressor(boosting='goss', learning_rate=0.015, max_depth=7, n_estimators=1400, random_state=random_state)
lightgbm3 = LGBMRegressor(boosting='gbdt', learning_rate=0.015, max_depth=11, n_estimators=1400, random_state=random_state)
ereg = VotingRegressor(estimators=[('xgb', xgboost), ('lgbm0', lightgbm0), ('lgbm1', lightgbm1), ('lgbm2', lightgbm2), ('lgbm3', lightgbm3)])

Hyperparameter는 GridSearch를 이용하여 찾은 값을 사용하였다.

결과는 최종 Public Score 108111.71518, Private Score 106922.61196 으로 마무리하였다.

대회를 진행하면서 느낀점은, 모델을 향상시킨다는 것은 생각보다 어렵고 복잡한 일이라는 것이었다. 처음에는 추가로 FE를 진행하지 않고 있는 데이터만으로 모델을 학습시켰었는데 원하는 만큼의 성능이 나오질 않아서 다른 노트북을 참고하여 추가로 FE를 진행하였더니 그것 만으로도 엄청나게 성능이 향상되어 한 번에 원하는 결과를 얻을 수 있었다. 그 이전까지는 Hyperparameter 튜닝만으로는 어떻게 해도 11만점 이하가 나오질 않았었다.

또한, 학습 시의 결과가 좋다고 항상 실전 결과가 좋은 것은 아니었다. 물론 어느 정도의 경향성은 있지만 절대적인 것은 아니었다. 테스트 시에는 XGB가 가장 좋은 결과를 보여줘서 제출했는데 Public Score는 좋았지만 Private Score는 좋지 못했다. 이 부분은 아마 너무 train dataset에만 Overfitting 되어있어서 Private Score를 측정하는 test dataset에는 잘 예측을 하지 못한 것 같다.

이번 대회를 진행하면서 확실히 성장했다고 느낄 수 있었다. 더 좋은 Hyperparameter를 찾기 위해 GridSearch가 아닌 Optuna라는 라이브러리를 이용하여 RandomSearch도 시도해보고, 노드에서 가르쳐주지 않은 VotingRegressor나 HistGradientBoostingRegressor와 같이 다른 앙상블 모델도 사용해보면서 모델 설계에 대한 이해도가 조금은 높아진 것 같다.

생각보다 11만점의 벽이 높아서 거의 2주 정도를 매달렸지만, 그래도 원하는 목표치를 달성할 수 있어서 기분도 좋고, 무엇보다 공부하면서 모델을 학습시키는 과정이 지루하지 않아서 재미있었다.

지금은 더 경험을 쌓기 위하여 다른 친구와 함께 Dacon에 참가하고 있는데, Baseline도 없이 데이터 처리부터 모델 설계를 하려고 하니 내가 무엇이 부족한지를 너무 뼈저리게 느낄 수 있어서 더 공부를 열심히 하는 계기가 되고 있다.

 

해당 노트북의 전체 코드 : github.com/ceuity/AIFFEL/blob/main/exploration_09/%5BE-09%5Dkaggle_kakr_housing.ipynb

8. 아이유팬이 좋아할 만한 다른 아티스트 찾기

학습목표

  • 추천시스템의 개념과 목적을 이해한다.
  • Implicit 라이브러리를 활용하여 Matrix Factorization(이하 MF) 기반의 추천 모델을 만들어 본다.
  • 음악 감상 기록을 활용하여 비슷한 아티스트를 찾고 아티스트를 추천해 본다.
  • 추천 시스템에서 자주 사용되는 데이터 구조인 CSR Matrix을 익힌다
  • 유저의 행위 데이터 중 Explicit data와 Implicit data의 차이점을 익힌다.
  • 새로운 데이터셋으로 직접 추천 모델을 만들어 본다.

추천시스템이란?

추천시스템이란, 데이터를 바탕으로 유저가 좋아할 만한 콘텐츠를 찾아서 자동으로 보여주거나 추천해주는 기능이다. 추천시스템의 원리는 간단하게 설명하면 나와 비슷한 다른 사용자들이 좋아하는 것과 비슷한 것을 내게 추천해준다는 것이다. 이러한 추천시스템은 크게 두 가지로 나뉜다.

http://www.kocca.kr/insight/vol05/vol05_04.pdf

(1) 협업 필터링


협업 필터링이란 대규모의 기존 사용자 행동 정보를 분석하여 해당 사용자와 비슷한 성향의 사용자들이 기존에 좋아했던 항목을 추천하는 기술이다.

  • 장점
    • 결과가 직관적이며 항목의 구체적인 내용을 분석할 필요가 없다.
  • 단점
    • 콜드 스타트(Cold Start)문제가 있다.
    • 계산량이 비교적 많은 알고리즘이므로 사용자 수가 많은 경우 효율적으로 추천할 수 없다.
    • 시스템 항목이 많다 하더라도 사용자들은 소수의 인기있는 항목에만 관심을 보이는 롱테일(Long tail)문제가 있다.
  • 행렬분해(Matrix Factorization), k-최근접 이웃 알고리즘 (k-Nearest Neighbor algorithm;
    kNN) 등의 방법이 많이 사용된다.

(2) 콘텐츠 기반 필터링


콘텐츠 기반 필터링은 항목 자체를 분석하여 추천을 구현한다. 예를 들어 음악을 추천하기 위해 음악 자체를 분석하여 유사한 음악을 추천하는 방식이다.

콘텐츠 기반 필터링을 위해서는 항목을 분석한 프로파일(item profile)과 사용자의 선호도를
추출한 프로파일(user profile)을 추출하여 이의 유사성을 계산한다. 유명한 음악 사이트인 판도
라(Pandora)의 경우, 신곡이 출시되면 음악을 분석하여 장르, 비트, 음색 등 약 400여 항목의 특
성을 추출한다. 그리고 사용자로부터는 ‘like’를 받은 음악의 특색을 바탕으로 해당 사용자의 프로
파일을 준비한다. 이러한 음악의 특성과 사용자 프로파일을 비교함으로써 사용자가 선호할 만한
음악을 제공하게 된다.

  • 군집분석(Clustering analysis), 인공신경망(Artificial neural network), tf-idf(term frequencyinverse document frequency) 등의 기술이 사용된다.

추천시스템은 아이템은 매우 많고, 유저의 취향은 다양할 때 유저가 소비할 만한 아이템을 예측하는 모델이다.

  • 유튜브 : 동영상이 매일 엄청나게 많이 올라오고 유저의 취향(게임 선호, 뷰티 선호, 지식 선호, 뉴스 선호)이 다양
  • 페이스북 : 포스팅되는 글이 엄청 많고 유저가 관심 있는 페이지, 친구, 그룹은 전부 다름
  • 아마존 : 카테고리를 한정해도 판매 품목이 엄청 많고 좋아하는 브랜드, 구매 기준이 다양

데이터 탐색과 전처리

(1) 데이터 준비


http://ocelma.net/MusicRecommendationDataset/lastfm-360K.html 데이터셋 홈페이지

# 파일 불러오기
import pandas as pd
import os

fname = os.getenv('HOME') + '/aiffel/recommendata_iu/data/lastfm-dataset-360K/usersha1-artmbid-artname-plays.tsv'
col_names = ['user_id', 'artist_MBID', 'artist', 'play']   # 임의로 지정한 컬럼명
data = pd.read_csv(fname, sep='\t', names= col_names)      # sep='\t'
data.head(10)

# 사용하는 컬럼 재정의
using_cols = ['user_id', 'artist', 'play']
data = data[using_cols]
data.head(10)

data['artist'] = data['artist'].str.lower() # 검색을 쉽게하기 위해 아티스트 문자열을 소문자로 변경
data.head(10)

# 첫 번째 유저 데이터 확인
condition = (data['user_id']== data.loc[0, 'user_id'])
data.loc[condition]

(2) 데이터 탐색


확인이 필요한 정보

  • 유저수, 아티스트수, 인기 많은 아티스트
  • 유저들이 몇 명의 아티스트를 듣고 있는지에 대한 통계
  • 유저 play 횟수 중앙값에 대한 통계
# 유저 수
data['user_id'].nunique()

# 아티스트 수
data['artist'].nunique()

# 인기 많은 아티스트
artist_count = data.groupby('artist')['user_id'].count()
artist_count.sort_values(ascending=False).head(30)

# 유저별 몇 명의 아티스트를 듣고 있는지에 대한 통계
user_count = data.groupby('user_id')['artist'].count()
user_count.describe()

# 유저별 play횟수 중앙값에 대한 통계
user_median = data.groupby('user_id')['play'].median()
user_median.describe()

# 이름은 꼭 데이터셋에 있는 것으로
my_favorite = ['black eyed peas' , 'maroon5' ,'jason mraz' ,'coldplay' ,'beyoncé']

# 'zimin'이라는 user_id가 위 아티스트의 노래를 30회씩 들었다고 가정
my_playlist = pd.DataFrame({'user_id': ['zimin']*5, 'artist': my_favorite, 'play':[30]*5})

if not data.isin({'user_id':['zimin']})['user_id'].any(): # user_id에 'zimin'이라는 데이터가 없다면
    data = data.append(my_playlist) # 위에 임의로 만든 my_favorite 데이터를 추가

data.tail(10) # 잘 추가되었는지 확인

(3) 모델에 활용하기 위한 전처리


데이터의 관리를 쉽게 하기 위해 indexing 작업을 해준다.

# 고유한 유저, 아티스트를 찾아내는 코드
user_unique = data['user_id'].unique()
artist_unique = data['artist'].unique()

# 유저, 아티스트 indexing 하는 코드 idx는 index의 약자
user_to_idx = {v:k for k,v in enumerate(user_unique)}
artist_to_idx = {v:k for k,v in enumerate(artist_unique)}

# 인덱싱이 잘 되었는지 확인해 봅니다. 
print(user_to_idx['zimin'])    # 358869명의 유저 중 마지막으로 추가된 유저이니 358868이 나와야 합니다. 
print(artist_to_idx['black eyed peas'])

# indexing을 통해 데이터 컬럼 내 값을 바꾸는 코드
# dictionary 자료형의 get 함수는 https://wikidocs.net/16 을 참고하세요.

# user_to_idx.get을 통해 user_id 컬럼의 모든 값을 인덱싱한 Series를 구해 봅시다. 
# 혹시 정상적으로 인덱싱되지 않은 row가 있다면 인덱스가 NaN이 될 테니 dropna()로 제거합니다. 
temp_user_data = data['user_id'].map(user_to_idx.get).dropna()
if len(temp_user_data) == len(data):   # 모든 row가 정상적으로 인덱싱되었다면
    print('user_id column indexing OK!!')
    data['user_id'] = temp_user_data   # data['user_id']을 인덱싱된 Series로 교체해 줍니다. 
else:
    print('user_id column indexing Fail!!')

# artist_to_idx을 통해 artist 컬럼도 동일한 방식으로 인덱싱해 줍니다. 
temp_artist_data = data['artist'].map(artist_to_idx.get).dropna()
if len(temp_artist_data) == len(data):
    print('artist column indexing OK!!')
    data['artist'] = temp_artist_data
else:
    print('artist column indexing Fail!!')

data

사용자의 명시적/암묵적 평가

  • 명시적 데이터(Explicit Data) : 좋아요, 평점과 같이 유저가 자신의 선호도를 직접(Explicit)표현한 데이터
  • 암묵적 데이터(Implicit Data) : 유저가 간접적(Implicit)으로 선호, 취향을 나타내는 데이터. 검색기록, 방문페이지, 구매내역, 마우스 움직임 기록 등이 있다.
# 1회만 play한 데이터의 비율을 보는 코드
only_one = data[data['play']<2]
one, all_data = len(only_one), len(data)
print(f'{one},{all_data}')
print(f'Ratio of only_one over all data is {one/all_data:.2%}')

이번에 만들 모델에서는 암묵적 데이터의 해석을 위해 다음과 같은 규칙을 적용한다.

  • 한 번이라도 들었으면 선호한다고 판단한다.
  • 많이 재생한 아티스트에 대해 가중치를 주어서 더 확실히 좋아한다고 판단한다.

Matrix Factorization(MF)

추천시스템의 다양한 모델 중 Matrix Factorization(MF, 행렬분해) 모델을 사용할 것이다.

MF는 평가행렬 R을 P와 Q 두 개의 Feature Matrix로 분해하는 것이다. 아래 그림에서는 P가 사용자의 특성(Feature) 벡터고, Q는 영화의 특성 벡터가 된다. 두 벡터를 내적해서 얻어지는 값이 영화 선호도로 간주하는 것이다.

벡터를 잘 만드는 기준은 유저i의 벡터 와 아이템j의 벡터 를 내적했을 때 유저i가 아이템j에 대해 평가한 수치 와 비슷한지 확인하는 것이다.

https://latex.codecogs.com/png.latex?U\_i%7B%5Ccdot%7DI\_j%20%3D%20M\_%7Bij%7D

이번에 사용할 모델은 Collaborative Filtering for Implicit Feedback Datasets 논문에서 제한한 모델을 사용한다.

CSR(Compressed Sparse Row) Matrix


유저 X 아이템 평가행렬을 행렬로 표현한다고 하면 36만 * 29만 * 1byte = 약 97GB가 필요하다. 이렇게 큰 용량을 메모리에 올려놓고 작업을 한다는 것은 거의 불가능 하기 때문에 CSR을 사용한다.

CSR은 Sparse한 matrix에서 0이 아닌 유효한 데이터로 채워지는 데이터의 값과 좌표 정보만으로 구성하여 메모리 사용량을 최소화 하면서도 Sparse한 matrix와 동일한 행렬을 표현할 수 있는 데이터 구조이다.

# 실습 위에 설명보고 이해해서 만들어보기
from scipy.sparse import csr_matrix

num_user = data['user_id'].nunique()
num_artist = data['artist'].nunique()

csr_data = csr_matrix((data.play, (data.user_id, data.artist)), shape= (num_user, num_artist))
csr_data

MF 모델 학습하기


Matrix Factorization 모델을 implicit 패키지를 사용하여 학습해보자.

  • implicit 패키지는 이전 스텝에서 설명한 암묵적(implicit) dataset을 사용하는 다양한 모델을 굉장히 빠르게 학습할 수 있는 패키지이다.
  • 이 패키지에 구현된 als(AlternatingLeastSquares) 모델을 사용한다. Matrix Factorization에서 쪼개진 두 Feature Matrix를 한꺼번에 훈련하는 것은 잘 수렴하지 않기 때문에, 한쪽을 고정시키고 다른 쪽을 학습하는 방식을 번갈아 수행하는 AlternatingLeastSquares 방식이 효과적인 것으로 알려져 있다.
from implicit.als import AlternatingLeastSquares
import os
import numpy as np

# implicit 라이브러리에서 권장하고 있는 부분
os.environ['OPENBLAS_NUM_THREADS']='1'
os.environ['KMP_DUPLICATE_LIB_OK']='True'
os.environ['MKL_NUM_THREADS']='1'

# Implicit AlternatingLeastSquares 모델의 선언
als_model = AlternatingLeastSquares(factors=100, regularization=0.01, use_gpu=False, iterations=15, dtype=np.float32)

# als 모델은 input으로 (item X user 꼴의 matrix를 받기 때문에 Transpose해줍니다.)
csr_data_transpose = csr_data.T
csr_data_transpose

# 모델 훈련
als_model.fit(csr_data_transpose)

# 벡터값 확인
zimin, black_eyed_peas = user_to_idx['zimin'], artist_to_idx['black eyed peas']
zimin_vector, black_eyed_peas_vector = als_model.user_factors[zimin], als_model.item_factors[black_eyed_peas]

zimin_vector
black_eyed_peas_vector

# zimin과 black_eyed_peas를 내적하는 코드
np.dot(zimin_vector, black_eyed_peas_vector) # 0.5098079

# 다른 아티스트에 대한 선호도
queen = artist_to_idx['queen']
queen_vector = als_model.item_factors[queen]
np.dot(zimin_vector, queen_vector) # 0.3044492

비슷한 아티스트 찾기 + 유저에게 추천하기

(1) 비슷한 아티스트 찾기


AlternatingLeastSquares 클래스에 구현되어 있는 similar_items 메서드를 통하여 비슷한 아티스트를 찾는다.

# 비슷한 아티스트 찾기
favorite_artist = 'coldplay'
artist_id = artist_to_idx[favorite_artist]
similar_artist = als_model.similar_items(artist_id, N=15)
similar_artist

# #artist_to_idx 를 뒤집어, index로부터 artist 이름을 얻는 dict를 생성합니다. 
idx_to_artist = {v:k for k,v in artist_to_idx.items()}
[idx_to_artist[i[0]] for i in similar_artist]

# 비슷한 아티스트를 찾아주는 함수
def get_similar_artist(artist_name: str):
    artist_id = artist_to_idx[artist_name]
    similar_artist = als_model.similar_items(artist_id)
    similar_artist = [idx_to_artist[i[0]] for i in similar_artist]
    return similar_artist

# 다른 아티스트 확인
get_similar_artist('2pac')
get_similar_artist('lady gaga')

특정 장르를 선호하는 사람들은 선호도가 집중되기 때문에 장르별 특성이 두드러진다.

(2) 유저에게 아티스트 추천하기


AlternatingLeastSquares 클래스에 구현되어 있는 recommend 메서드를 통하여 좋아할 만한 아티스트를 추천받는다. filter_already_liked_items 는 유저가 이미 평가한 아이템은 제외하는 Argument이다.

user = user_to_idx['zimin']
# recommend에서는 user*item CSR Matrix를 받습니다.
artist_recommended = als_model.recommend(user, csr_data, N=20, filter_already_liked_items=True)
artist_recommended

# index to artist
[idx_to_artist[i[0]] for i in artist_recommended]

# 추천 기여도 확인
rihanna = artist_to_idx['rihanna']
explain = als_model.explain(user, csr_data, itemid=rihanna)

[(idx_to_artist[i[0]], i[1]) for i in explain[1]]

(3) 마무리


추천시스템에서 Baseline으로 많이 사용되는 MF를 통해 아티스트를 추천하는 모델을 만들어보았다. 그러나 이 모델은 몇 가지 아쉬운 점이 있다.

  1. 유저, 아티스트에 대한 Meta정보를 반영하기 쉽지 않다. 예를 들어 연령대별로 음악 취향이 다를 수 있는데 이러한 부분을 반영하기 어렵다.
  2. 유저가 언제 play했는지 반영하기 어렵다. 10년 전에 재생된 노래랑 지금 재생되는 노래랑 비교해보자.

회고록

  • csr_data를 만드는데 왜인지 모르겠지만 unique로 뽑은 값을 shape에다 넣어줬더니 row index가 넘었다고 에러가 떴다. 그래서 shape를 넣지 않고 그냥 돌렸더니 생성이 되었고, 만들어진 csr_data의 shape를 확인해보니 unique값과 달랐다. 왜 그런지는 잘 모르겠다... NaN값이 있나..?
  • 오늘 한 과제가 지금까지 한 과제 중에서 가장 어려웠던 것 같다. 아직 데이터 전처리에 익숙하지 않아서 그럴 수도 있지만 데이터 전처리가 거의 전부인 것 같다.
  • 데이터 전처리만 잘 해도 반은 성공한다는 느낌이다. 애초에 데이터가 없으면 시작조차 할 수 없으니 데이터 전처리하는 방법과 pandas, numpy등 사용 방법에 대해 잘 익혀둬야 겠다.
  • 모델을 훈련하여 실제로 추천받은 목록을 보니 토이스토리를 골랐을 때 토이스토리2, 벅스라이프, 알라딘 등을 추천해주는 걸 보면 만화애니메이션 장르쪽을 추천해주고 있다. 따라서 훈련이 잘 이루어졌다고 평가할 수 있을 것 같다.
  • csr_data를 만들 때 shape 에서 Error가 발생하는 이유를 알아냈다. 그 원인은 csr_data의 (row_ind, col_ind) parameter가 max(row_ind), max(col_ind)로 작동하여 row_ind와 col_ind의 index의 최댓값을 사용하기 때문이다. 물론 row와 col이 index 순으로 잘 정렬되어 있다면 이렇게 해도 문제가 없지만, 실제로는 movie_id의 중간에 빠진 index들이 있기 때문에 movie_id의 총 갯수인 3628개 보다 큰 max(row_ind)의 3953개가 parameter로 사용되는 것이다. user_id도 마찬가지로 unique한 값은 6040개지만, index가 1부터 시작하여 끝값은 6041이므로 총 6042개를 col_ind로 사용하게 된다. 이 부분은 수정해보려고 했으나, movie_id마다 이미 할당된 title이 있기 때문에 ratings DataFrame에 다시 movie_id에 맞는 title column을 더해주고 movie_id순으로 중복을 제거하고 sort하여 title에 다시 movie_id를 할당해 주는 작업이 너무 번거로워서 그만뒀다.

유용한 링크

https://orill.tistory.com/entry/Explicit-vs-Implicit-Feedback-Datasets?category=1066301 명시적/암묵적 평가

https://lovit.github.io/nlp/machine learning/2018/04/09/sparse_mtarix_handling/#csr-matrix CSR

https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html CSR을 만드는 방법

https://danthetech.netlify.app/DataScience/evaluation-metrics-for-recommendation-system 추천시스템 평가 방법

Copy of 7. 나랑 닮은 연예인은 누구?

실습목표

  • 임베딩에 대해 이해하고 얼굴의 임베딩 벡터를 추출한다.
  • 얼굴의 임베딩 벡터로 닮은 꼴인 얼굴을 찾는다.
  • 나와 가까운 연예인을 찾아낸다.

임베딩이란?

컴퓨터에는 감각기관이 없어서 우리가 쉽게 인지하는 시각, 촉각, 청각 정보들을 즉각적으로 판단하기 어렵다. 오직 0과 1의 조합으로 데이터를 표현해주어야 한다. 우리는 어떻게 컴퓨터에게 다양한 형태의 정보를 표현해줄 수 있을까?

우리는 어떤 벡터 공간(Vector Space)에다가 우리가 표현하고자 하는 정보를 Mapping하는 방법을 사용할 수 밖에 없다. 단어나 이미지 오브젝트나 무언가를 벡터로 표현한다면, 그것은 원점에서 출발한 벡터 공간상의 한 점이 될 것이다. 따라서, 우리가 하고 싶은 것은 점들 사이의 관계를 알아보는 것이다.

오늘 다루어 보고자 하는 '두 얼굴이 얼마나 닮았나'하는 문제는 두 얼굴 벡터 사이의 거리가 얼마나 되나 하는 문제로 치환된다. 중요한 것은 두 점 사이의 거리가 실제로 두 오브젝트 사이의 유사도를 정확하게 반영해야 한다는 것이다.

만약 100x100픽셀의 컬러 사진이라면 RGB 3개의 채널까지 고려해 무려 3x100x100 = 30000차원의 벡터를 얼굴 개수만큼 비교해야 한다. 그러나 너무 높은 차원의 공간에서 비교는 사실상 무의미해지기 때문에 고차원 정보를 저차원으로 변환하면서 필요한 정보만을 보존하는 것이 바로 임베딩이다.

임베딩 벡터에 보존되어야 하는 정보는 상대적인 비교수치이다. 이 상대적인 비교수치를 딥러닝을 통해 학습할 수 있도록 해보자.

얼굴 임베딩 만들기

(1) 얼굴인식


이미지 속의 두 얼굴이 얼마나 닮았는지 알아보기 위해서는 우선 이미지 속에서 얼굴 영역만을 정확하게 인식해서 추출하는 작업이 필요하다.

이전에 dlib를 이용하여 얼굴 인식을 해보았기 때문에, 오늘은 dlib을 사용해서 만들어진 Face Recognition 라이브러리를 이용해서 과제를 진행할 예정이다. pip를 이용해 라이브러리를 설치한다.

$ pip install cmake
$ pip install dlib
$ pip install face_recognition --user

(2) FaceNet


2015년 구글에서 발표한 FaceNet은 일반적인 딥러닝 모델과 크게 다르지 않지만, 네트워크 뒤에 L2 Normalization을 거쳐 임베딩을 만들어내고 여기에 Triplet Loss를 사용하고 있다.

Triplet Loss는 유사한 것은 벡터를 가깝게 위치시키고, 다른 것은 벡터를 멀게 위치시키는 효과를 가져온다. 예를 들어, 같은 사람의 얼굴은 벡터를 가깝게 하고, 다른 사람의 얼굴은 벡터를 멀게 하는 것이다.

얼굴 임베딩 사이의 거리측정


얼굴 임베딩 공간의 시각화


임베딩 벡터를 numpy 배열로 보면 벡터들의 거리를 한 눈에 알기 어렵다. 따라서 시각화를 통해 쉽게 이해할 수 있다.

고차원의 데이터를 시각화하기 위해서는 차원을 축소해야 하는데, 그 방법에는 PCA, T-SNE 등이 있다. Tensorflow의 Projector는 고차원 벡터를 차원 축소 기법을 사용해서 눈으로 확인할 수 있게 해준다.

  • PCA(Principal Component Analysis, 주성분 분석) : 모든 차원 축에 따른 값의 변화도인 분산(Variance)를 확인한 뒤 그 중 변화가 가장 큰 주요한 축을 남기는 방법
  • T-SNE : 고차원 상에서 먼 거리를 저차원 상에서도 멀리 배치되도록 차원을 축소하는 방식. 먼저 random하게 목표하는 차원에 데이터를 배치 후, 각 데이터들을 고차원 상에서의 배치와 비교하면서 위치를 변경해준다.

가장 닮은 꼴 얼굴 찾아보기


우리가 만들고 싶은 함수는 name parameter로 특정 사람 이름을 주면 그 사람과 가장 닮은 이미지와 거리 정보를 가장 가까운 순으로 표시해주어야 한다.

나랑 닮은 연예인을 찾아보자

# 필요 module import
import os
import face_recognition
import numpy as np
import matplotlib.pyplot as plt

# directory의 file list 불러오기
dir_path = os.getenv('HOME')+'/aiffel/face_embedding/images/actor'
file_list = os.listdir(dir_path)
print ("file_list: {}".format(file_list))

# 얼굴 영역만 잘라서 출력하는 함수
def get_cropped_face(image_file):
    image = face_recognition.load_image_file(image_file)
    face_locations = face_recognition.face_locations(image)
    if face_locations:
        a, b, c, d = face_locations[0]
        cropped_face = image[a:c,d:b,:]
        return cropped_face
    else:
        return []

# 임베딩 벡터 구하기
image_file = os.path.join(dir_path, '내사진.jpg')
cropped_face = get_cropped_face(image_file)   # 얼굴 영역을 구하는 함수(이전 스텝에서 구현)

# 이미지 확인
%matplotlib inline

plt.imshow(cropped_face)

# 얼굴 영역을 가지고 얼굴 임베딩 벡터를 구하는 함수
def get_face_embedding(face):
    return face_recognition.face_encodings(face)

embedding = get_face_embedding(cropped_face)  
embedding

# embedding dict 만드는 함수
def get_face_embedding_dict(dir_path):
    file_list = os.listdir(dir_path)
    embedding_dict = {}
    cropped_dict = {}

    for file in file_list:
        img_path = os.path.join(dir_path, file)
        face = get_cropped_face(img_path)
        if face == []:
            continue
        embedding = get_face_embedding(face)
        if len(embedding) > 0:
            # splitext = file, extension
            embedding_dict[os.path.splitext(file)[0]] = embedding[0]
            cropped_dict[os.path.splitext(file)[0]] = face

    return embedding_dict, cropped_dict

# embedding_dict 만들기
embedding_dict, cropped_dict = get_face_embedding_dict(dir_path)
embedding_dict['내사진']

# 두 얼굴 사이의 거리 구하기
def get_distance(name1, name2):
    return np.linalg.norm(embedding_dict[name1]-embedding_dict[name2], ord=2)

# 내 사진으로 비교
get_distance('내사진','내사진1')

# name1과 name2의 거리를 비교하는 함수
def get_sort_key_func(name1):
    def get_distance_from_name1(name2):
        return get_distance(name1, name2)
    return get_distance_from_name1

# 거리를 비교할 name1 미리 지정
sort_key_func = get_sort_key_func('내사진')

# 순위에 맞는 이미지 출력
def get_nearest_face_images(sorted_faces, top=5):
    fig = plt.figure(figsize=(15, 5))
    fig.add_subplot(2, top, 1)
    plt.imshow(cropped_dict[sorted_faces[0][0]])
    for i in range(1, top+1):
        fig.add_subplot(2, top, i+5)
        plt.imshow(cropped_dict[sorted_faces[i][0]])

# 가장 닮은 꼴 찾기
def get_nearest_face(name, top=5):
    sort_key_func = get_sort_key_func(name)
    sorted_faces = sorted(embedding_dict.items(), key=lambda x:sort_key_func(x[0]))

    for i in range(top+1):
        if i == 0:
            continue
        if sorted_faces[i]:
            print(f'순위 {i} : 이름 ({sorted_faces[i][0]}), 거리({sort_key_func(sorted_faces[i][0])})')
    return sorted_faces

# 순위 출력
sorted_faces = get_nearest_face('내사진')

# 순위에 따른 이미지 출력
get_nearest_face_images(sorted_faces)

회고록

  • 임베딩이란 개념에 대해 지난 번에 한번 학습했었던 경험이 있다보니 이번 과제의 난이도가 조금 덜 어렵게 느껴진다.
  • 고차원을 가지는 이미지를 저차원에서 시각화 하는 개념에 대해 조금 생소하게 느껴졌지만 tensowflow의 Projector로 확인해보니 바로 이해할 수 있었다.
  • 한 사람에 한 장의 이미지 만으로는 아무래도 닮은 정도를 비교하기 어려운 것 같다. 나랑 거리가 가까운 연예인이라고 나온 결과를 내 사진과 비교해 보니 전혀 아닌듯 ㅎ...
  • 오늘은 혼자서 matplotlib을 이용하여 결과를 조금 더 보기 좋게 꾸며봤다.
  • 혹시나 해서 한국 연예인 얼굴 사진 Dataset이 있는지 찾아봤는데 보이질 않았다. 내가 못 찾는건지, 아니면 초상권 등으로 인해서 없는건지...
  • 오늘 진행한 과제는 따로 학습하거나 하는 과정이 크게 없어서 빠르게 진행할 수 있었다.

유용한 링크

https://cloud.google.com/solutions/machine-learning/overview-extracting-and-serving-feature-embeddings-for-machine-learning?hl=ko 임베딩

https://github.com/ageitgey/face_recognition

https://arxiv.org/abs/1503.03832 FaceNet: A Unified Embedding for Face Recognition and Clustering
https://www.youtube.com/watch?v=d2XB5-tuCWU Triplet Loss

https://huyhoang17.github.io/128D-Facenet-LFW-Embedding-Visualisation/ 시각화

https://bcho.tistory.com/1209?category=555440 PCA 분석

https://www.youtube.com/watch?v=NEaUSP4YerM T-SNE

문제 링크 : leetcode.com/problems/longest-substring-without-repeating-characters/

 

Longest Substring Without Repeating Characters - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 주어진 문자열에서 각 문자가 중복되지 않는 연속된 가장 긴 문자열의 길이를 반환하는 문제이다.

 

풀이방법

 

from collections import deque

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        dq = deque()
        ans = ""
        for char in s:
            if char not in dq:
                dq.append(char)
                if len(ans) < len(dq):
                      ans = "".join(dq)
            else:
                if len(ans) < len(dq):
                      ans = "".join(dq)
                while True:
                    if dq.popleft() == char:
                        break
                dq.append(char)
        return len(ans)

이 문제는 스택을 이용하여 해결하였다. 스택에 문자를 하나씩 넣어두고 길이를 계산하여 최대 길이일 때만 정답을 갱신하고, 만약 중복된 문자가 나오면 스택에서 해당 문자가 나올 때 까지 이전 문자들을 제거하여, 스택에는 문자가 중복되지 않게 하였다. 문자열을 다 순회했을 때, 가장 길었던 정답의 길이를 반환한다.

'Algorithm > Leetcode' 카테고리의 다른 글

[LeetCode] 771. Jewels and Stones  (0) 2021.02.14
[LeetCode] 23. Merge k Sorted Lists  (0) 2021.02.14
[LeetCode] 937. Reorder Data in Log Files  (0) 2021.01.27
[LeetCode] 344. Reverse String  (0) 2021.01.27
[LeetCode] 125. Valid Palindrome  (0) 2021.01.27

문제 링크 : leetcode.com/problems/jewels-and-stones/

 

Jewels and Stones - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 jewels에 있는 문자가 stones에 몇 개가 들어있는지 갯수를 반환하는 문제이다.

 

풀이방법

이 문제는 key, value 쌍 구조인 딕셔너리를 이용하여 해결하는 가장 기본적인 문제이다.
딕셔너리의 jewels를 dict에 입력해두고 stones에서 jewels에 있으면 count를 +1 하여 해결하였다.

 

class Solution:
    def numJewelsInStones(self, jewels: str, stones: str) -> int:
        dicts = {}
        for i in jewels:
            if i not in dicts:
                dicts[i] = 0
        for j in stones:
            if j in dicts:
                dicts[i] += 1
        return sum(dicts.values())

 

문제 링크 : leetcode.com/problems/merge-k-sorted-lists/

 

Merge k Sorted Lists - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 주어진 정렬된 linked list를 merge하여 하나의 linked list로 만드는 문제이다.

 

풀이방법

처음에는 n개의 linked list를 순회하여 가장 작은 값을 이전 값의 next로 연결하는 방식으로 구현하려고 했었는데 잘 되지 않아서 stack을 이용하는 방법으로 문제를 해결하였다. 처음엔 stack에 linked list의 node도 넣을 수 있나 싶었는데 역시 파이썬. 안되는 게 없다.

lists를 전부 순회하면서 각 linked list에 들어있는 요소들을 모두 stack에 저장한 후, lambda식을 이용하여 stack의 value를 기준으로 정렬한 후 각각의 node들을 연결시켜주었다.

class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        head = ListNode()
        temp = head
        stack = []
        for i in lists:
            while i:
                stack.append(i)
                i = i.next
        stack = sorted(stack, key=lambda x:x.val, reverse=True)
        while stack:
            temp.next = stack.pop()
            temp = temp.next
        temp.next = None
        return head.next

+ Recent posts