세상의 변화에 대해 관심이 많은 이들의 Tech Blog search

PyTorch 사용법 - 01. 소개 및 설치

|

PyTorch 사용법 - 00. References
PyTorch 사용법 - 01. 소개 및 설치
PyTorch 사용법 - 02. Linear Regression Model
PyTorch 사용법 - 03. How to Use PyTorch


2021.07.12 updated

간단한 소개

PyTorch는 유연성과 속도를 모두 갖춘 딥러닝 연구 플랫폼이다. GPU 사용이 가능하기 때문에 속도가 상당히 빠르다.
또 입문 난이도가 높지 않은 편이고 코드가 간결하다.


설치 방법

여기를 참조한다. 자신에게 맞는 OS, package manager, Python 버전, CUDA 버전 등을 선택하면 그에 맞는 명령어 집합이 나온다. 이를 명령창에 실행하면 설치가 진행된다.
torchvision을 설치할 경우에 무슨 라이브러리가 없다면서 에러 메시지가 뜨긴 하는데, 사용하는 데 별 문제는 없을 것이다. 만약 자신이 그 부분을 꼭 써야 한다면 에러를 해결하고 넘어가자.

설치를 완료했으면, 명령창에 다음과 같이 입력해보자. Anadonda를 플랫폼으로 사용한다면 conda 설정은 직접 해 주어야 한다.

python

# 이 부분은 Python Interpreter에서 입력함.
import torch  
x = torch.randn(3,5)  
print(x)

결과가 대략 다음과 같이 나오면 설치가 완료되었다. 숫자가 다른 것은 랜덤이니 신경 쓰지 말자.

01_run_pytorch.PNG


GPU 사용을 위한 설치

GPU 사용을 위한 필수 절차는 다음과 같다.

Ubuntu의 경우 여기를 참조해도 된다.

  1. 호환성 체크
    1. 컴퓨터에 있는 GPU의 compute capability 확인
    2. compute capability에 맞는 CUDA SDK 버전 확인
    3. CUDA, nvidia-driver 호환 확인
      • 여기에서 확인
      • CUDA toolkit 호환성 확인은 여기에서
    4. Pytorch와 CUDA의 호환성 확인
      • 설치하고자 하는 PyTorch(또는 Tensorflow)가 지원하는 최신 CUDA 버전이 있다. 이보다 상위 버전의 CUDA를 설치하면 PyTorch 코드가 제대로 돌아가지 않는다.
      • Pytorch 홈페이지에서 정해주는 CUDA 버전을 설치하는 쪽이 편하다. 2020.02.13 기준 최신 버전은 10.1이다.
    5. CUDA에 맞는 cuDNN 버전 확인
      • 여기에서 확인할 수 있다.
  2. 이전 버전의 CUDA 제거
    1. CUDA를 여러 개 쓸 수도 있지만, 이전 버전의 CUDA를 제거해 주면 좋다.
      1. Windows의 경우 NVIDIA 관련 프로그램 제거를 해 주면 된다.
      2. Ubuntu의 경우 살짝 까다로운데, 터미널에 다음 코드를 입력한다.
         sudo apt-get purge nvidia*
         sudo apt-get autoremove
         sudo apt-get autoclean
         sudo rm -rf /usr/local/cuda*
        
      3. 혹시 오류가 뜨면 아래 7. 오류 해결법을 참조하자.
    2. 예전엔 어땠는지 잘 모르겠지만 최근 CUDA 설치 시 그에 맞는 nvidia-driver가 같이 설치된다. 따로 특정 버전의 driver를 요구하는 것이 아니라면 그대로 설치하자.
  3. Nvidia Driver 설치
    1. Windows의 경우 Geforce Experience 혹은 Nvidia에서 적절한 버전의 Driver를 설치한다.
    2. Ubuntu의 경우 다음 코드를 입력해 본다.
       # 가능 드라이버 확인 
       sudo apt search nvidia-driver 
       # 특정 드라이버 설치 
       sudo apt-get install nvidia-driver-455
      
  4. CUDA 설치
    1. Windows
      1. CUDA toolkit archive에서 원하는 CUDA를 다운받는다. 운영체제와 버전 등을 체크하고, 가능하면 Installer Type은 network가 아닌 local로 받는다. 인터넷으로 설치하면서 받는 것이 아닌 한번에 설치파일을 받는 식이다.
        • 같은 버전인데 update가 추가된 버전이 있다. 보통은 이것까지 추가로 설치해 주는 쪽이 좋다. base installer를 먼저 설치한 뒤에 추가로 설치해 주도록 하자.
      2. 설치 파일로 CUDA를 설치한다. 설치 시에는 다른 프로그램을 설치하거나 제거하는 중이면 실행이 되지 않으니 주의하자.
      3. cuda visual studio integration 관련해서 설치 실패가 뜨는 경우가 많은데, 이 부분이 필요한 코드를 실행할 일이 있다면 이 단계에서 다시 설치해 주는 것이 좋다. Visual Studio를 설치하면 해결이 되는 경우가 많다.
      4. C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0\bin을 등록하자. 버전에 따른 경로를 유의하자.
        • 실행이 잘 안 되는 경우 상위 또는 하위 폴더 몇 개를 추가 등록하면 되는 경우도 있다.
    2. Ubuntu 18.04(CUDA 11.0 기준)
      1. 역시 CUDA toolkit archive에 접속한다. Linux 버전에 따라서 알맞게 선택한다. Ubuntu 18.04를 선택한다면, 일반적으로 Linux - x86_64 - Ubuntu - 18.04를 따른다.
      2. 다음으로 Installer Type이 있는데, runfile의 경우는 .exe 파일처럼 실행이 가능하고, deb(local)은 터미널에 코드를 몇 줄 입력하면 되는 방식이다.
      3. runfile을 선택하면 다음 비슷한 코드를 실행하라고 안내가 뜬다.
         wget http://developer.download.nvidia.com/compute/cuda/11.0.2/local_installers/cuda_11.0.2_450.51.05_linux.run
         chmod +x cuda_11.0.2_450.51.05_linux.run
         sudo sh cuda_11.0.2_450.51.05_linux.run
        
      4. 아래 줄까지 실행하면 안내 페이지가 뜬다.
        1. 드라이버가 이전에 설치된 게 있다고 뜨는데, 미리 제거해 두는 것이 편하긴 하다. 그러나 제거하지 않아도 될 때도 있다. 엔터키를 누르면 X 표시가 토글된다.
        2. 다음으로 계약 동의를 위해 accept를 입력하고 엔터키를 누른다.
        3. 그냥 기본으로 두고 Install을 해도 된다. 그러나 Driver 설치 단계에서 오류가 나면(설치 실패시 로그를 확인하라고 뜬다), Driver을 엔터키를 눌러 체크 해제한다. CUDA symbolic link를 대체하고 싶지 않다면 역시 symbolic link 부분을 체크 해제한다.
        4. 정상적으로 설치가 된다면 다음과 비슷한 것을 볼 수 있다.
           ===========
           = Summary =
           ===========
          
           Driver:   Not Selected
           Toolkit:  Installed in /usr/local/cuda-11.0/
           Samples:  Installed in /root/, but missing recommended libraries
          
           Please make sure that
           -   PATH includes /usr/local/cuda-11.0/bin
           -   LD_LIBRARY_PATH includes /usr/local/cuda-11.0/lib64, or, 
           add /usr/local/cuda-11.0/lib64 to /etc/ld.so.conf and run ldconfig as root
          
        5. sudo vim /etc/bash.bashrc을 실행한 다음, 파일의 가장 아래쪽에 다음 코드를 추가하자. 버전에 따른 경로를 유의하자.
           export PATH=/usr/local/cuda-11.0/bin:$PATH
           export LD_LIBRARY_PATH=/usr/local/cuda-11.0/lib64:$LD_LIBRARY_PATH
           export LD_LIBRARY_PATH=/usr/local/cuda-11.0/extras/CUPTI/lib64:$LD_LIBRARY_PATH
          
        6. 다음 코드를 실행하여 변경 사항을 적용한다.
           source /etc/bash.bashrc
          
      5. deb(local)을 선택하면 터미널에 코드 몇 줄을 입력하면 된다. 이전에 CUDA 설치가 꼬인 것이 아니라면 보통은 에러 없이 설치된다. 버전에 따라 경로가 달라지므로 유의하자.
         wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-ubuntu1804.pin
         sudo mv cuda-ubuntu1804.pin /etc/apt/preferences.d/cuda-repository-pin-600
         wget http://developer.download.nvidia.com/compute/cuda/11.0.2/local_installers/cuda-repo-ubuntu1804-11-0-local_11.0.2-450.51.05-1_amd64.deb
         sudo dpkg -i cuda-repo-ubuntu1804-11-0-local_11.0.2-450.51.05-1_amd64.deb
         sudo apt-key add /var/cuda-repo-ubuntu1804-11-0-local/7fa2af80.pub
         sudo apt-get update
         sudo apt-get -y install cuda
        
  5. cuDNN 설치
    1. Windows
      1. 우선 cudnn-archive에서 사용하고자 하는 CUDA에 맞는 버전(cuDNN Library for Windows (x86))을 찾아 다운받는다. login이 필요하다.
      2. 압축을 풀어 CUDA 설치 폴더(C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0)에 붙여넣기 하면 된다. 폴더 경로는 설치한 CUDA 버전에 따라 달라진다.
    2. Ubuntu 18.04(cudnn 8.0.3 기준)
      1. 비슷하게 여기에서 cuDNN Library for Linux (x86_64)을 받는다. 이유는 잘 모르겠으나 wget으로 잘 받아지지 않는 경우가 있으니 브라우저로 접속하여 다운로드하자.
      2. 받고 나서 tar xvf cudnn-11.0-linux-x64-v8.0.3.33.tgz으로 압축을 해제한다.
      3. 생성된 CUDA 폴더로 이동하여 파일들을 복사한다.
         cd cuda
         sudo cp include/cudnn* /usr/local/cuda-11.0/include
         sudo cp lib64/libcudnn* /usr/local/cuda-11.0/lib64/
         sudo chmod a+r /usr/local/cuda-11.0/lib64/libcudnn*
        
      4. 설치되었는지 확인하자.
         cat /usr/local/cuda-11.0/include/cudnn_version.h | grep CUDNN_MAJOR -A 2
        

        결과:

         R -A 2
         #define CUDNN_MAJOR 8
         #define CUDNN_MINOR 0
         #define CUDNN_PATCHLEVEL 3
         --
         #define CUDNN_VERSION (CUDNN_MAJOR * 1000 + CUDNN_MINOR * 100 + CUDNN_PATCHLEVEL)
        
         #endif /* CUDNN_VERSION_H */
        

        8.2.0임을 확인할 수 있다.

  6. 설치 확인
    1. NVCC를 통해 CUDA 설치를 확인해보자.
       nvcc -V
       dpkg - l | grep CUDA 
       dpkg - l | grep cuDNN
      

      만약 nvcc가 없으면 다음을 입력하자.

       sudo apt install nvidia-cuda-toolkit
      
    2. 다음 코드를 python을 실행하여 입력해보고 True가 뜨면 성공한 것이다.
       import torch
       torch.cuda.is_available()
      
  7. 에러 해결법
    1. E: sub process /usr/bin/dpkg returned an error code (1)의 에러가 뜬다면 다음을 터미널에 입력한다.
       sudo rm /var/lib/dpkg/info/*
       sudo dpkg --configure -a
       sudo apt update -y
      
    2. NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver. Make sure that the latest NVIDIA driver is installed and running. 에러가 뜨면 nvidia-driver를 재설치해야 한다.
       # 설치된 driver 확인
       sudo apt --installed list | grep nvidia-driver
       # 문제 있는 driver 삭제(보통 전부 삭제)
       sudo apt remove nvidia-driver-<version>
       sudo apt autoremove
       # 재설치 & 재부팅
       sudo apt-get install nvidia-driver-<원래 driver 버전>
       sudo reboot now
      
    3. nvidia-smi를 쓰려고 하는데 VNML: Driver/library version mismatch라는 에러가 날 때가 있다. 그런 경우 lsmod | grep nvidia를 터미널에 입력하고 nvidia kernel을 unload하면 된다. 오른쪽 column에 무언가 있다면 unload하면 된다.
       sudo rmmod nvidia_drm
       sudo rmmod nvidia_modeset
       sudo rmmod nvidia_uvm
       sudo rmmod nvidia
      

      위의 작업 도중 rmmod:ERROR: Module nvidia is in use라는 에러가 뜨면 nvidia 관련 process를 종료시켜주자.

       sudo lsof /dev/nvidia*
       sudo kill -9 <process_id>
      

      다시 lsmod | grep nvidia를 하고 아무 것도 안 뜬다면 완료된 것이다.


참고: Ubuntu Python 설치

python 3.7 이후 버전은 (그 이전 버전도 있을 수 있다) apt-get 설치를 지원한다.

sudo apt update
sudo apt install python3.7 
sudo apt install python3.8
sudo apt install python3.9
# sudo apt-get install python3 python3-pip python3-dev python3-setuptools

Python이 여러 개 설치되어 터미널에 python을 입력했을 때 원하는 버전이 나오지 않는다면 vim ~/.bashrc로 파일을 열고 맨 아래에 다음과 비슷하게 추가하자.

# 명확하게 하길 원한다면 경로를 직접 지정하는 것이 편하다. 
# 최신 버전의 경우 '/usr/bin/python*` 또는 `/usr/local/bin/python*` 경로에 존재한다.
alias python='/usr/bin/python3.9'

# python 2 대신 3을 사용하고 싶은 경우
alias python=python3
alias pip=pip3

:wq를 입력하여 저장하고 나온 뒤 터미널에 source ~/.bashrc를 입력하여 변경사항을 적용하자.

Python 버전의 우선순위를 쓰고 싶다면 Python을 먼저 선택지에 추가해야 한다.

sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 1
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.9 2
# 마지막 숫자는 우선순위로 클수록 높은 우선권을 갖는다.

우선순위를 변경하고자 하는 경우 다음 명령을 입력하여 숫자를 누르고 엔터키를 누른다.

sudo update-alternatives --config python3

다시 auto mode로 돌아가려면 sudo update-alternatives --auto python3을 입력한다.

재설치 방법

sudo python3 -m pip uninstall pip 
sudo apt-get install python3-pip --reinstall

오류 해결법

ModuleNotFoundError: No module named ‘pip._internal’ 오류

다음 두 가지를 시도해본다.

# 재설치
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python get-pip.py --force-reinstall
# pip upgrade
python -m pip install --user --upgrade pip

pip3 on python3.9 fails on ‘HTMLParser’ object has no attribute ‘unescape’ 오류

pip3 install --upgrade setuptools
# 안 되면 다음을 시도한다.
pip3 install --upgrade pip
pip3 install --upgrade distlib

PyTorch Project 구조

프로젝트의 구조는 코딩하는 사람 마음대로이긴 하나, 기본적으로는 다음과 같은 구조를 따른다.

  1. Set HyperParameter and Run
  2. Load Data
  3. Define and Load Models 3-1. Define util functions
  4. Set Loss function(creterion) and Optimizer
  5. Train Model
  6. Visualize and save results

PyTorch는 각 단계에서 다음의 장점을 갖는다.

  1. PyTorch가 아닌 Python의 특징인데, 여러분은 많은 Machine Learning 코드를 보면서 python train.py --epochs 50 --batch-size 16 등 많은 옵션을 설정할 수 있는 것을 보았을 것이다. Python의 argparse 패키지는 이것을 가능하게 해 준다.
  2. 데이터 로드 시 DataLoader라는 클래스를 제공한다. DataLoader를 통해 데이터를 불러오면, 이 안에서 데이터 처리에 대한 거의 모든 것을 쉽게 수행할 수 있다.
    • 이를테면 Data Augmentation 같은 것도 전부 제공된다.
    • 여러 종류의 Data Transformation이 지원된다.
  3. 일반적인 모델을 불러올 때는 다른 Deep Learning Framework도 대체로 간결하지만, PyTorch는 torchvision이라는 패키지에서 따로 pretrain까지 된 모델들을 제공하므로 다른 곳에서 모델을 다운로드할 필요 없이 이를 바로 쓸 수 있다. 2-1. 많은 프로그래머들이 utils.py에 유틸리티 함수(예를 들면 YOLO에서 IoU를 구하는 함수)를 따로 빼내어 여러 가지를 한번에 정의한다. 프로젝트에서 부가적인 부분은 따로 관리하는 것이 가독성이 좋다.
  4. 이 부분은 다른 Deep Learning Framework와 비슷하다.
  5. Tensorflow와는 달리 Session을 설정할 필요가 없다.
  6. 이 부분도 역시 비슷하다.

다음 글에서는 Linear Regression Model을 예로 들어서 간단한 프로젝트의 구조를 설명하도록 하겠다.


References

PyTorch에서 자주 사용되는 함수들을 정리한 글이다.

Comment  Read more

PyTorch 사용법 - 00. References

|

PyTorch 사용법 - 00. References
PyTorch 사용법 - 01. 소개 및 설치
PyTorch 사용법 - 02. Linear Regression Model
PyTorch 사용법 - 03. How to Use PyTorch


본 글의 일부 예제는 Pytorch Documentation에서 가져왔음을 밝힙니다.


데이터 타입(dtype)

모든 텐서는 기본적으로 dtype을 갖고 있다. 데이터 타입(dtype)이란 데이터가 정수형인지, 실수형인지, 얼마나 큰 범위를 가질 수 있는지 등을 나타낸다.
종류는 아래 표와 같다.

Data type dtype CPU tensor GPU tensor
32-bit floating point torch.float32 or torch.float torch.FloatTensor torch.cuda.FloatTensor
64-bit floating point torch.float64 or torch.double torch.DoubleTensor torch.cuda.DoubleTensor
16-bit floating point torch.float16 or torch.half torch.HalfTensor torch.cuda.HalfTensor
8-bit integer (unsigned) torch.uint8 torch.ByteTensor torch.cuda.ByteTensor
8-bit integer (signed) torch.int8 torch.CharTensor torch.cuda.CharTensor
16-bit integer (signed) torch.int16 or torch.short torch.ShortTensor torch.cuda.ShortTensor
32-bit integer (signed) torch.int32 or torch.int torch.IntTensor torch.cuda.IntTensor
64-bit integer (signed) torch.int64 or torch.long torch.LongTensor torch.cuda.LongTensor

사용법은 어렵지 않다. 텐서 생성시 dtype=torch.float과 같이 parameter를 지정해 주기만 하면 된다.


Tensor Creation

torch.arange

# torch.arange(start=0, end, step=1, out=None, dtype=None, 
#              layout=torch.strided, device=None, requires_grad=False) → Tensor

start 이상 end 미만까지 step 간격으로 dtype 타입인 1차원 텐서를 생성한다.

out parameter로 결과 텐서를 저장할 변수(텐서)를 지정할 수 있다.

>>> torch.arange(start=1, end=9, step=2)
tensor([1, 3, 5, 7])

torch.linspace

# torch.linspace(start, end, steps=100, out=None, dtype=None, 
#                layout=torch.strided, device=None, requires_grad=False) → Tensor

start 이상 end 미만까지 총 steps 개수의 dtype 타입인 1차원 텐서를 생성한다.
torch.arange에서 step은 간격을, torch.linspace에서 steps는 개수를 의미한다.

>>> torch.linspace(-10, 10, steps=5)
tensor([-10.,  -5.,   0.,   5.,  10.])
>>> torch.linspace(0, 10, steps=10)
tensor([ 0.0000,  1.1111,  2.2222,  3.3333,  4.4444,  
         5.5556,  6.6667,  7.7778,  8.8889, 10.0000])

torch.from_numpy

# torch.from_numpy(ndarray) → Tensor

numpy array인 ndarray로부터 텐서를 만든다. 이 함수는 데이터를 복사가 아닌 참조를 한다.
from_numpy로 만들어진 텐서는 해당 ndarray와 메모리를 공유하며, 어느 한쪽의 데이터를 변경 시 둘 다 변경된다.

>>> a = numpy.array([1, 2, 3])
>>> t = torch.from_numpy(a)
>>> print(t)
tensor([ 1,  2,  3])
>>> t[0] = -1
>>> print(a)
array([-1,  2,  3])

torch.randn

# torch.randn(*sizes, out=None, dtype=None, 
#             layout=torch.strided, device=None, requires_grad=False) → Tensor

N(0, 1) 정규분포를 따르는 sizes 크기의 텐서를 생성한다.

>>> torch.randn(2, 3)
tensor([[ 1.5954,  2.8929, -1.0923],
        [ 1.1719, -0.4709, -0.1996]])

Tensor Reshape

torch.unsqueeze(Tensor.unsqueeze)

# torch.unsqueeze(input, dim, out=None) → Tensor

dim parameter 위치에 길이 1짜리 차원을 추가한 텐서를 만든다. 이 함수는 데이터를 복사가 아닌 참조를 한다. 원본 텐서와 메모리를 공유하며, 어느 한쪽의 데이터를 변경 시 둘 다 변경된다.

dim은 [ -input.dim() - 1, input.dim() + 1] 범위를 갖는다. 음수 dim은 dim + input.dim() + 1과 같다.
원본 텐서의 size가 (2, 3, 4)라면, unsqueeze(1) 버전은 (2, 1, 3, 4), unsqueeze(2) 버전은 (2, 3, 1, 4)이다.

>>> x = torch.tensor([1, 2, 3])
>>> x
tensor([1, 2, 3])
>>> y = x.unsqueeze(1)
>>> y
tensor([[1],
        [2],
        [3]])
>>> x.size(), y.size()
(torch.Size([3]), torch.Size([3, 1]))

>>> y[0][0] = -1
>>> y
tensor([[-1],
        [ 2],
        [ 3]])
>>> x
tensor([-1,  2,  3])

Tensor Operation

torch.cat

# torch.cat(seq, dim=0, out=None) → Tensor

두 텐서를 이어 붙인다(concatenate). 데이터를 복사한다.
concatenate하는 차원을 제외하고는 size가 같거나 empty여야 한다. 즉 shape=(2, 3, 4)인 텐서는 shape=(2, 1, 4)와는 dim=1일 때만 concatenate가 가능하다.

>>> x = torch.arange(0, 6).reshape(2, 3)
>>> y = torch.arange(100, 104).reshape(2, 2)
>>> x
tensor([[0, 1, 2],
        [3, 4, 5]])
>>> y
tensor([[100, 101],
        [102, 103]])
>>> torch.cat((x, y), dim=1)
tensor([[  0,   1,   2, 100, 101],
        [  3,   4,   5, 102, 103]])

torch.Tensor.backward


torch.nn

torch.nn.Linear

# class torch.nn.Linear(in_features, out_features, bias=True)

Linear 모델 클래스를 생성한다.
in_features 길이의 데이터를 Linear Transformation을 통해 out_features 길이의 데이터로 변환할 수 있다.

>>> from torch import nn
>>> model = nn.Linear(in_features=3, out_features=2, bias=True)

>>> print(model)
Linear(in_features=3, out_features=2, bias=True)
>>> print(model.weight)
Parameter containing:
tensor([[-0.3469,  0.1542, -0.4830],
        [-0.2903,  0.4949,  0.4592]], requires_grad=True)
>>> print(model.bias)
Parameter containing:
tensor([-0.0965,  0.5427], requires_grad=True)

torch.nn.MSELoss


torch.optim

torch.optim.Adam

torch.optim.Optimizer.zero_grad

torch.optim.Optimizer.step


Save and Load

torch.save


01_new_repository


Comment  Read more

Self Attention

|

A Structured Self-Attentive Sentence Embedding

본 글은 Zhouhan Lin, Minwei Feng, Cicero Nogueira dos Santos, Mo Yu, Bing Xiang, Bowen Zhou, Yoshua Bengio가 2017년에 Publish한 위 논문을 리뷰한 것이다.
특별히 본 글은 Presentation에 사용된 PDF 발표자료로 대체하겠다.

Comment  Read more

YOLOv2

|

You Only Look Once: Unified, Real-Time Object Detection

본 글은 Joseph Redmon, Santosh Divvala, Ross Girshick, Ali Farhadi가 2016년에 Publish한 위 논문을 리뷰한 것이며, 추가적으로 구현을 위한 Loss Function 설명과 코드를 첨부하였다.

Unified Detection

YOLO version2는 기본적으로 end-to-end training 방법을 취하고 있으며 학습이 완료되었을 때 실시간으로 테스트가 가능할 만큼 빠른 속도를 보이는 강점을 갖고 있다.
본 글은 416 X 416 이미지를 기준으로 설명을 진행하도록 하겠다.
YOLO의 핵심은 이미지를 Grid Cell로 나누어 각각의 Cell이 Object Detection을 위한 정체성을 갖게끔 만든다는 것이다.
예를 들어 416 X 416의 이미지는 아래와 같은 Darknet이라는 CNN 구조를 거치게 된다.
(참고로 아래는 Pytorch 기준으로 Channels_first를 적용하였다.)

layer size
0 [None, 3, 416, 416]
1 [None, 32, 208, 208]
2 [None, 64, 104, 104]
5 [None, 128, 52, 52]
8 [None, 256, 26, 26]
13 [None, 512, 13, 13]
18 [None, 1024, 13, 13]
19 [None, 1024, 13, 13]
20 [None, 1024, 13, 13]
skip: [None, 64, 26, 26] ->
skip: [None, 256, 13, 13]
21 [None, 1280, 13, 13]
22 [None, 1024, 13, 13]
23 [None, 35, 13, 13]

Output으로 나온 [35, 13, 13]에서 13 X 13은 Grid의 size이다. 35는 추후에 설명하겠다.

출처: https://blog.paperspace.com/how-to-implement-a-yolo-object-detector-in-pytorch/

위 그림과 같이 정리된 13 X 13 = 169개의 Cell은 이제 각각 Object을 detect하기 위한 정체성을 갖게 된다. 만일 실제(Ground-truth: GT) Object의 중심 좌표(center coordinate)가 Cell 내부에 위치한다면, 그 Cell은 해당 Object를 detect할 책임을 지게 되는 것이다.
(If the center of an object falls into a grid cell, that grid cell is reponsible for detecting that object)

각각의 Grid Cell은 이제 5개의 bbox를 예측하게 되고, 각각의 box에 대해 confidence score를 계산하게 된다. 5개는 YOLOv2에서 정한 숫자이고, YOLOv3에선 총 9개가 등장하게 된다.

자세한 설명을 위해 35라는 숫자에 대해 부연 설명을 하도록 하겠다.

[35 = 5 * (1 + 4 + 2)]

5 = bbox 개수
1 = box_confidence = P(Object) * IOU_truth_pred
4 = boxes = box coordinates (bounding box 좌표 4개: x, y, w, h)
2 = box_class_probs (예측하고자 하는 class의 개수와 길이가 같다.)

box_confidence는 그 Cell에 Object가 있을 확률에 IOU_truth_pred를 곱하게 되는데, P(Object)는 당연히 0 또는 1이다. 이 값에 GT_bbox(truth)와 pred_bbox(pred)의 IOU를 계산하여 곱해주면 box_confidence가 되는 것이다. P(Object)가 0일 경우 이 값은 물론 0이 된다.

boxes의 경우 bbox 좌표를 뜻하는데, 후에 IOU를 계산할 때에는 이와 같이 중심 좌표(x, y)와 box 길이(w, h)를 기준으로 계산하는 것이 불편하기 때문에 왼쪽 상단 좌표(x1, y1)과 오른쪽 하단 좌표(x2, y2)로 고쳐주도록 한다.

def box_to_corner(box1, box2):
    """
    abs_coord 형식인 bbox의 [x, y, w, h]를 [x1, y1, x2, y2]로 변환한다.
    :param box1: [..., 4]
    :param box2: [..., 4]
    :return: [..., 1] X 8
    """
    b1_x, b1_y, b1_w, b1_h = box1[..., 0], box1[..., 1], box1[..., 2], box1[..., 3]
    b2_x, b2_y, b2_w, b2_h = box2[..., 0], box2[..., 1], box2[..., 2], box2[..., 3]

    b1_x1, b1_x2 = b1_x - b1_w/2, b1_x + b1_w/2
    b1_y1, b1_y2 = b1_y - b1_h/2, b1_y + b1_h/2

    b2_x1, b2_x2 = b2_x - b2_w/2, b2_x + b2_w/2
    b2_y1, b2_y2 = b2_y - b2_h/2, b2_y + b2_h/2

    return b1_x1, b1_x2, b1_y1, b1_y2, b2_x1, b2_x2, b2_y1, b2_y2

참고로 현재 https://github.com/KU-DIA/BasicNet에서 관련 코드를 확인할 수 있다.

이제 box_class_probs를 보면 이 값은 P(Class_i | Object)를 뜻하는데, Object가 있을 경우 i번째 Class일 조건부 확률을 뜻한다. 사실 이 값만으로는 추후 과정 진행이 어렵기 때문에 위에서 구한 box_confidence와 이 box_class_probs를 브로드캐스팅 곱을 통해 계산해주면,

class_scores = box_confidence * box_class_probs
             = P(Object) * IOU * P(Class_i | Object)
             = P(Class_i) * IOU

위와 같이 class_scores를 구할 수 있다.
이 class_scores 텐서는 본인이 설정한 Class 수만큼의 길이를 가지는데(정확히 “길이”는 아니지만), 본 예에서는 2로 설정하였기 때문에 앞으로도 2라는 숫자를 계속 사용하도록 하겠다.

이 class_scores는 각각의 box가 가지는 class-specific confidence score를 의미하게 된다. 만약 설정한 Class를 사람, 고양이라고 한다면, 각 class_scores 텐서는 그 Cell이 “사람”을 detect할 확률, “고양이”를 detect할 확률을 담고 있는 것이다.

이후에 여러 과정이 추가되기는 하지만, 본질적으로 이렇게 표현된 class_scores에서 가장 높은 값을 갖는 class_index를 찾아 output으로 반환하게 된다.

다시 위로 돌아가서 [None, 35, 13, 13] 구조에서, 35는 5 X 7이라는 것을 확인하였다.
바로 위에서 7은 1(box_confidence), 4(bbox 좌표), 2(box_class_probs)로 분해되는 것을 보았는데,
1과 2가 브로드캐스팅 곱을 통해 길이 2의 class_scores로 정리되었다.
위 class_scores는 cell 기준으로 존재하는데, cell 하나당 5개의 bbox를 갖고, 이러한 cell은 총 13 X 13개 있으므로, 이제 우리는 13 X 13 X 5개의 class_scores 텐서를 갖게 되었다.

그런데 그냥 이런식으로 진행하게 되면, 845개의 텐서가 등장하는데, 너무 많다.
이제부터는 텐서의 수를 효과적으로 줄여 학습을 준비하는 과정에 대해 설명하겠다.
사실 아예 삭제하는 것은 아니고, 필요없는 텐서의 값들을 죄다 0으로 바꿔주는 작업이다.

  1. filter_by_confidence
    def filter_by_confidence(confidence, boxes, class_probs, threshold=0.6):
     """
     confidence가 threshold보다 낮은 경우 제거해준다.
     남은 confidence와 class_probs를 곱하여 class_scores를 생성한다.
     :param confidence: (None, 1)
     :param boxes: (None, 4)
     :param class_probs: (None, C)
     :param threshold: 필터링 threshold
     """
     confidence[confidence < threshold] = 0.0
        
     (하략: class_scores를 계산하여 반환함)
    

용어를 정리하자면, confidence = box_confidence, class_probs = box_class_probs이다.
위 함수는 일정 threshold보다 작은 box_confidence를 갖는 box_confidence를 아예 0으로 바꿔준다.
왜냐하면 P(Object)가 0에 근접할 경우, background(Object가 없음)라는 의미인데, 이들의 bbox를 찾는 것은 의미가 없기 때문이다.

  1. Non_max_suppression
    def nms(boxes, class_scores, threshold=0.6):
     """
     :param boxes: bbox 좌표, (None, 4)
     :param class_scores: confidence * class_prob_scores, 클래스 별 score, (None, C)
     :param threshold: NMS Threshold
     """
    
     for class_number in range(C):
         target = class_scores[..., class_number]
    
         # 현재 class 내에서 class_score를 기준으로 내림차순으로 정렬한다.
         sorted_class_score, sorted_class_index = torch.sort(target, descending=True)
    
         # idx: 아래 bbox_max의 Index
         # bbox_max_idx: 정렬된 class_scores를 기준으로 가장 큰 score를 가지는 bbox Index
         for idx, bbox_max_idx in enumerate(list(sorted_class_index.numpy())):
             # 기준 class_score가 0이라면 비교할 필요가 없다.
             # 아래 threshold 필터링에서 0으로 바뀐 값이기 때문이다.
             if class_scores[bbox_max_idx, class_number] != 0.0:
                 # 0이 아니라면 순서대로 criterion_box(bbox_max)로 지정된다.
                 bbox_max = boxes[bbox_max_idx, :]
    
                 # criterion_box(bbox_max)가 아닌 다른 box들을 리스트로 미리 지정한다.
                 #others = [index for index in list(sorted_class_index.numpy()) if index != i]
                 others = list(sorted_class_index.numpy())[idx+1:]
    
                 # 비교 대상 box들에 대해서
                 # bbox_cur_idx: 비교 대상 bbox Index
                 for bbox_cur_idx in others:
                     bbox_cur = boxes[bbox_cur_idx, :]
                     iou = get_iou(bbox_max, bbox_cur)
                     # print(bbox_max_idx, bbox_cur_idx, iou)
    
                     # iou가 threshold를 넘으면 (기준 box와 비교 대상 box가 너무 많이 겹치면)
                     # 그 해당 box의 현재 class의 class_score를 0으로 만들어 준다.
                     if iou > threshold:
                         class_scores[bbox_cur_idx, class_number] = 0.0
    
     return boxes, class_scores
    

사실 Cell 별로 각각 bbox를 5개씩 갖게 되면 인접한 Cell들끼리는 bbox가 마구 겹칠 것이다. 또, bbox의 크기가 충분히 클 경우 이미지 바깥으로 벗어나기도 할 것인데, 한 이미지에 Object 수가 수백개 있는 것이 아닌 이상, 이렇게 많은 bbox는 필요하지 않은 것이 자명하다.

NMS 작업은 이 문제를 효과적으로 해결해준다. 쉽게 말해서 “왕”을 뽑는 느낌인데, 특정 class_scores가 높은 bbox와 과하게 겹치는 (IOU가 높은) 다른 녀석들을 제거하는 것이다.

“사람”을 detect하는 class_scores를 기준으로 class_scores를 내림차순으로 정렬한다. 제일 큰 첫 번째 값과 나머지 값들을 쌍으로 IOU를 계산하여 과하게 겹치는 (기준 threshold)를 넘는 값은 0으로 바꿔준다.

이 과정이 끝나면 [None, 35, 13, 13]이라는 크기 자체는 바뀌진 않지만, 중간중간에 많은 숫자가 0으로 바뀌어 있을 것이다. (1, 2번 기준을 충족하지 못한 값들)
이제 이를 바탕으로 training을 시키면 된다.

Training

위 그림은 YOLOv2의 Loss Function이다. YOLO의 최대 장점은 이렇게 Loss Function을 하나로 통합하여 효과적인 학습을 가능하게 했다는 점이다.

https://curt-park.github.io/2017-03-26/yolo/에서 Loss Function과 관련한 기본적인 설명을 얻을 수 있는데, 추가적으로 설명을 하도록 하겠다.

먼저 1, 2줄은 bbox 좌표에 관한 Loss Function이다. 앞에 있는 $ \lambda_{coord} $는 5로 설정되었다.
$ S^2 $은 Grid Cell의 개수를 의미하며 본 예에서는 13 X 13을 의미한다. $ B $는 정해둔 bbox (anchors) 개수이며 본 예에서는 5를 의미한다. 이렇게 모든 Cell에서 5개 씩의 bbox를 계산하여 GT_bbox와 차이를 좁혀나가는 것이다.

여기서 앵커에 대해 잠깐 설명하자면, 이 앵커는 빠른 학습을 위해 설정된 bbox의 초기값이라고 생각하면 된다. 그냥 무작정 Cell에다가 bbox를 그린다면 그 크기의 편차가 매우 심할 것이다. 미리 object들의 크기를 대략적으로 계산하여 가장 많이 등장할 법한, 가장 유사한 크기의 bbox 크기를 미리 계산해두어 저장한 것이 바로 앵커인데, 이는 보통 Clustering 기법을 통해 미리 계산된다.

이렇게 미리 계산된 앵커를 초기값으로 투입하고, GT_bbox 좌표와의 차이를 빠르게 줄여 나가는 것이 1, 2번째 줄의 목표라고 하겠다.

그리고 $ 1_{i, j}^{obj} $ 라는 Indicator Function의 기능이 매우 중요한데, 이 지시함수는 i번째 Grid Cell에서 j번째 bbox에 Object가 존재할 경우 1이고, 아니면 0이다.

아래의 $ 1{i, j}^{noobj} $ 는 반대의 의미를 가지며, $ 1{i}^{obj} $ 는 오직 Cell 소속 여부와 관련이 있다.

3, 4번째 줄은 Object가 있는지 없는지에 대한 Loss를 계산하게 되고,
5번째 줄은 P(Class_i | Object) = Conditional Class Probability의 Loss를 계산하게 된다.

Conclusion

빠른 속도와 괜찮은 정확도를 가졌지만 YOLOv2의 단점은 작은 물체나 겹치는 물체들을 효과적으로 Localization하지 못한다는 것이다. 이는 version3에서 상당부분 업그레이드 된다.

Comment  Read more

Attention

|

Neural Machine Translation by Jointly Learning to Align and Translate

본 글은 Dzmitry Bahdanau, Kyunghyun Cho, Yoshua Bengio가 2014년에 Publish한 위 논문을 리뷰한 것이다.

Introduction

Basic Encoder-Decoder 모델은 source sentence의 모든 정보를 fixed-length vector로 압축하는 방식을 골자로 한다.
그러나 이 모델은 긴 문장을 대상으로 할 때 어려움을 겪는 것이 일반적이다.
이를 해결하기 위해 본 논문에서 제안된 새로운 모델은 target word를 예측하는 것과 관련된 source sentence의 부분을 자동적으로 soft-search하여 이와 같은 문제를 해결해낸다.
(learns to align and translate jointly)

encodes the input sentence into sequence of vectors and
chooses a subset of these vectors adaptively while decoding the translation.

Background: NMT

translation의 핵심은 source sentence x가 주어졌을 때의 y의 조건부확률을 최대화하는 target sentence y를 찾는 것이다.

[arg\max_{y} p(\vec{y} x)]

번역 모델에 의해 이러한 조건부 분포가 학습되면, source sentence가 주어졌을 때 상응하는 번역된 문장은 위의 조건부 확률을 최대화하는 문장을 찾음으로써 generate된다.

RNN Encoder-Decoder는 이전 리뷰에서 다룬 적이 있으므로 생략하도록 한다.

Learning to align and translate

모델의 구성 요소를 하나하나 살펴보기 이전에 notation에 관한 정리를 진행하겠다.

$y_i$: i time에서의 target word
$s_i$: i time에서의 디코더 Hidden State
$c_i$: i time에서의 context vector = annotations의 가중합
$\alpha_{ij}$: attention weight = normalized score = 연결 확률
$h_j$: j time에서의 인코더 Hidden State = annotations
$e_{ij}$: attention score = unnormalized score
$f, g$ = 비선형 함수

명확히 하자면, subscript i는 디코더를 명시하며, subscript j는 인코더를 명시한다.

이제 모델의 구성 요소를 살펴볼 것이다.
먼거 타겟 word $y_i$를 예측하기 위한 조건부 확률은 아래와 같이 정의된다.

[p(y_i y_1, …, y_{i-1}, \vec{x}) = g(y_{i-1}, s_i, c_i)]

이 중 디코더의 i time Hidden State인 $s_i$를 먼저 살펴보면,

[s_i = f(s_{i-1}, y_{i-1}, c_i)]

Basic Encoder-Decoder 모델과 달리 target word를 예측하기 위한 조건부 확률은 분리된 context vector $c_i$에 의존한다.

Context Vector $c_i$는 annotations $h_j$의 가중합이다.

[c_i = \sum_{j=1}^{T_x} \alpha_{ij} h_j]

여기서 $h_j$는 j time annotation으로, input sequence의 i번째 단어 주위 부분에 강하게 집중하여 input sequence에 대한 정보를 담게 된다.

Bidirectional RNN
이 $h_j$는 forward RNN의 Hidden States와 backward RNN의 Hidden States를 세로로 합친 열벡터이다.

[h_j = [\overrightarrow{h_j}^T \overleftarrow{h_j}^T]^T]

이러한 방식으로 $h_j$는 두 방향 모두로 words들을 요약한 정보를 담게 된다.

이제 attention weight $a_{ij}$가 어떻게 계산되는지 살펴보겠다.

[a_{ij} = \frac{ exp(e_{ij}) } {\sum_{k=1}^{T_x} exp(e_{ik}) }]

이 $a_{ij}$는 Normalized Score라고 할 수 도 있다. 왜냐하면 softmax함수의 확률로서 계산되기 때문이다.

Unnormalized Score인 $e_{ij}$는 아래와 같이 계산된다.

[e_{ij} = a(s_{i-1}, h_j)]

여기서 a함수는 alignment model이다. 이 a를 다른 component와 함께 학습되는 순전파 신경망으로서 모수화한다.
이 alignment model은 j time 인풋이 i time 아웃풋과 얼마나 유사한지를 평가하게 된다.
또한 이 모델은 잠재변수로 설정되지 않고, soft alignment를 직접적으로 계산하여 cost function의 gradient가 역전파될 수 있도록 하게 만든다.
계산 방법은 마지막 부분에서 설명하도록 하겠다.

위 설명을 보면, 결국 i번째 Context Vector인 $c_i$는 expected annotation over all the annotations with probabilities $\alpha_{ij}$라고 할 수 있다.
이 $\alpha_{ij}$는 다음 Hidden State인 $s_i$를 결정하고 target word $y_i$를 generate하는 데에 있어 $h_j$의 중요성을 결정하는 역할을 하게 된다.

즉 이는 일종의 attention이라는 개념으로 설명될 수 있다.
디코더는 source sentence의 어떤 부분에 집중해야 하는지 결정하게 되는 것이다.

Conclusion

제안된 모델은 다음 target word를 generate하는 데에 관련이 있는 정보에만 집중하며 이 때문에 source sentence의 길이에 상당히 robust하다.
다만 unknown or rare words를 다루는 데 있어서는 좀 더 보완이 필요하다.

Details about Model Architecture

이 부분에서는 Appendix에 나와 있는 수식들을 종합하여, 본 논문에서 제안한 RNNSearch라는 모델의 구조에 대해 정리하도록 하겠다.
논문이 굉장히 친절하여 Matrix의 차원이 정확하고 자세하게 나와있으므로 반드시 참고할 필요가 있다.

  1. 상수에 대한 설명은 아래와 같다.
    $m$: word embedding 차원, 본 모델에선 620
    $n$: 인코더/디코더 Hidden Units의 수, 본 모델에선 1000
    $n’$: Alignment Model 내에서의 Hidden Units의 수, 본 모델에선 1000
    $l$: , 본 모델에선 500
    $T_x$: source sentence의 길이
    $K_x$: source language의 vocab_size

  2. 벡터들의 크기는 아래와 같다.
    $y_i$: (k, 1)
    $s_i$: (n, 1)
    $h_i$: (n, 1)
    $v_a$: (n’, 1)
    $z_i$: (n, 1)
    $r_i$: (n, 1)

  3. 행렬들의 크기는 아래와 같다. W, U, C는 모두 Parameter Matrix이다.
    $X$: ($T_x$, $K_x$)
    $Y$: ($T_y$, $K_y$)
    $E$: (m, K), x와 결합할 때는 $K_x$, y와 결합할 때는 $K_y$
    $W$: (n, m), $W, W_z, W_r$에 한정
    $W_a$: (n’, n), Alignment 모델에서 사용
    $U$: (n, n), $U, U_z, U_r$에 한정
    $U_a$: (n’, 2n), Alignment 모델에서 사용
    $C$: (n, 2n), $C, C_z, C_r$에 한정

Encoder
source sentence Matrix X는 번역 대상인 하나의 문장을 뜻한다.
각각의 열벡터는 $\vec{x_i}$로 표기되며 이 벡터의 크기는 $K_x$로,
source language의 vocab_size를 의미한다.

인코더의 Bidirectional RNN은 아래와 같이 계산된다.
(Bias항은 잠시 삭제한다.)

[h_j = (1 - z_i) \odot h_{j-1} + z_i \odot \tilde{h_j}]

위에서 $z_i$가 Update Gate이며, 각 Hidden State가 이전 activation을 유지하느냐 마느냐를 결정한다.

[\tilde{h_j} = tanh(W*Ex_j + U[r_j \odot h_{j-1}])]

위에서 $r_j$가 Reset Gate이며, 이전 State의 정보를 얼마나 Reset할지 결정한다.

[z_j = \sigma(W_z * Ex_j + U_z * h_{j-1})]

[r_j = \sigma(W_r * Er_j + U_x * h_{j-1})]

위에서 계산한 식은 $\overrightarrow{h_j}$, $\overleftarrow{h_j}$ 모두에게 통용되며,
이를 stack하여 annotation $h_j$를 만들게 되는 것이다.

Decoder
디코더의 Hidden State인 $s_i$ 역시 계산 방식은 유사하다.

[s_i = (1 - z_i) \odot s_{i-1} + z_i \odot \tilde{s_i}]

[\tilde{s_i} = tanh(WEx_i + U[r_i \odot s_{i-1}] + Cc_i)]

[z_i = \sigma(W_z * Ex_i + U_z * s_{i-1} + C_zc_i)]

[r_i = \sigma(W_r * Er_j + U_x * s_{i-1} + C_rc_i)]

[c_i = \sum_{j=1}^{T_x}a_{ij}h_j]

[a_{ij} = \frac{ exp(e_{ij}) } {\sum_{k=1}^{T_x} exp(e_{ik}) }]

[e_{ij} = v_a^T tanh(W_a * s_{i-1} + U_a * h_j)]

최종적으로 Decoder State $s_{i-1}$, Context Vector $c_i$, 마지막 generated word $y_{i-1}$을 기반으로, target word $y_i$의 확률을 아래와 같이 정의한다.

[p(y_i s_i, y_{i-1}, c_i) \propto exp(y_i^T W_o t_i)]

즉 오른쪽 편에 있는 스칼라값에 정비례한다는 뜻이다.
잠시 행렬의 차원을 정의하고 진행하겠다.

$W_o$: ($K_y$, $l$)
$U_o$: ($2l$, n)
$V_o$: ($2l$, m)
$C_o$: ($2l$, 2n)
이들은 모두 Parameter이다.

이제 $t_i$를 정의할 것인데, 그 전에 두 배 크기인 candidate $\tilde{t_i}$를 먼저 정의하겠다.

[\tilde{t_i} = U_o * s_{i-1} + V_o * Ey_{i-1} + C_oc_i]

차원을 맞춰보면 위 벡터는 크기가 ($2l$, 1)인 것을 알 수 있을 것이다.
이제 이 벡터에서 아래와 같은 maxout과정을 거치면,

$t_i$는 아래와 같이 정의된다.

[t_i = [ max(\tilde{t_{i, 2j-1}}, \tilde{t_{i, 2j}}) ]_{j=1, …, l}^T]

아주 멋지다.
The End

Comment  Read more