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

Miniconda(Anaconda) 사용법(conda 설치 및 사용법)

|

Anaconda는 Continuum Analytics라는 곳에서 만든 파이썬 배포판으로 수백 개의 파이썬 패키지를 포함하는 환경을 구성한다. Anaconda로는 virtualenv와 같은 여러 개의 가상환경을 만들어 각각의 환경을 따로 관리할 수 있다.
그 중 Miniconda는 이것저것 설치할 것이 많은 Anaconda에서 패키지를 다르게 설치할 여러 환경들을 관리한다는 최소한의 기능만 가진 부분만 포함하는 mini 버전이다. 따라서 이 글에서는 Miniconda를 설치하여 가상환경을 관리하는 법을 알아보겠다.


설치

Anaconda를 설치하거나, Miniconda를 설치한다. 설치하고 싶을 운영체제와 버전에 맞는 것을 골라 설치한다. 설치 방법은 공식 홈페이지에 따로 설명되어 있다.

01_install Not recommended라고 되어 있는 옵션이지만 체크하면 PATH에 등록하지 않아도 된다(이건 환경마다 조금 다르다). 02_install

설치 후 다음 명령을 명령창(cmd / 터미널)에 입력해본다.

conda list

만약 다음과 같이 오류가 뜬다면 conda가 System PATH에 등록되지 않은 것이므로 등록을 해 준다.

03_install 04_install

윈도우10, Miniconda3인 경우 C:\ProgramData\Miniconda3\Scripts를 PATH에 등록해 준다.

설치 패키지 목록은 다를 것이지만 다음과 같이 뜬다. conda list는 현재 환경(기본 환경의 이름은 base이다)에서 설치된 패키지 목록을 나타내는 명령이다.

05_conda_list


가상환경 목록 확인, 생성 및 삭제

다음을 명령창에 입력한다.

conda env list
# 또는,
conda info --envs

현재 활성화된 가상환경 옆에는 * 가 추가된다.

06_env_list

처음 설치했을 때는 기본값인 base 하나만 있을 것이다.

다음 명령을 통해 새 가상환경을 하나 생성한다.

# -n 옵션은 --name과 같은 것으로, 가상환경 이름을 myenv로 지정한다.
conda create -n myenv
# python=3.6 옵션은 가상환경 생성 시 파이썬 버전을 지정한다.
# 지정하지 않으면 conda에 기본 포함된 파이썬 버전으로 생성된다.
conda create -n condatorch python=3.6

# 특정 패키지 버전을 지정하면서, 그리고 패키지를 설치하면서 생성하는 것도 가능하다.
conda create -n myenv python=3.4 scipy=0.15.0 astroid babel

# 가상환경 생성 시 이것저것 깔리는 것이 싫다면 다음 옵션을 주면 된다.
conda create --no-default-packages -n myenv python

# 새 가상환경을 만들 때 특정 가상환경 안에 설치된 패키지 전부를 설치하면서 생성할 수 있다.
# base 가상환경에 있는 패키지를 전부 설치하면서 생성한다면, 
conda create -n myenv --clone base

# environment.yml 파일이 있다면 다음과 같이 생성할 수 있다.
# 생성 방법은 이후에 설명한다.
conda env create -f environment.yml

계속 진행하겠냐는 물음이 보이면 y를 입력한다.

07_env_create

다시 conda env list로 목록을 확인해보면 지정한 이름으로 가상환경이 생성되었음을 확인할 수 있다.

08_env_create

위 그림에서 activate condatorch, deactivate 등의 명령이 쓰여 있는 것을 확인할 수 있는데, 이는 특정 가상환경을 활성화 또는 비활성화할때 사용하는 명령이다(가상환경이 무엇에 쓰는 것인지 알면 무슨 말뜻인지 알 수 있을 것이다). 이는 다음 절에서 설명한다.

가상환경 삭제는 다음 명령을 통해 수행할 수 있다.

# 생성할 때와는 다르게 env를 앞에 적어주어야 한다.
# 생성 시에는 env를 앞에 적으면 실행이 되지 않는다.
# remove 앞에 env를 써 주지 않으면 가상환경 삭제가 아닌 패키지 삭제가 이루어진다.
# conda env remove -n <environment_name>
conda env remove -n condatorch
# 다음도 가능하다.
conda remove --name myenv --all

09_env_remove

Requirements.txt로 가상환경 생성하기

아래 명령들은 가독성을 위해 두 줄로 펼쳐 놓았다.

Windows 환경이라면 명령창에 다음과 같이 쓰는 것이 가능하다.

FOR /F "delims=~" %f in (requirements.txt) 
DO conda install --yes "%f" || pip install "%f"

Unix 환경이라면 다음과 같이 쓸 수 있다.

while read requirement; do conda install --yes $requirement; 
done < requirements.txt 2>error.log

conda로는 설치가 안 되고 pip으로는 설치가 되는 패키지가 있다면 다음과 같이 쓸 수 있다.

while read requirement; do conda install --yes $requirement 
|| pip install $requirement; done < requirements.txt 2>error.log

다음을 참조하였다: github 글, stackoverflow 글


가상환경 활성화, 비활성화

가상환경 활성화는 위에서도 설명했듯 다음과 같이 쓰면 된다.

activate <environment_name>
activate condatorch

Unix 등의 환경에서는 activate가 아닌 source activate를 써야 한다.

그러면 명령창의 맨 앞에 (condatorch)와 같이 활성화된 가상환경 이름이 뜬다.

비활성화는 다음 명령으로 할 수 있다.

deactivate
# 설치한 버전에 따라 deactivate는 deprecated되었다는 경고를 볼 수도 있다. 이 경우 conda deactivate이다.

10_activate

위 그림이 잘 이해가 되지 않는다면, activate를 여러 번 쓰지 않을 것을 권장한다.


가상환경 안에 패키지 설치

버전에 따라 조금씩 다른 경우도 있으나, 최신 버전(2019-02-01 기준)의 Miniconda3에서는 pip, whl, conda를 통한 설치 모두 현재 활성화된(없다면 base 또는 컴퓨터에 깔려 있는 다른 버전의 파이썬에) 가상환경에만 설치된다. 따라서 각 환경 간 거의 완전한 분리가 가능하다.

패키지 설치는 다음과 같다. pip과 거의 비슷하다.

conda install seaborn
# 여러 개를 동시에 설치할 경우 comma 없이 그냥 나열한다.
conda install numpy pandas

설치된 패키지 목록을 보고 싶으면 다음을 입력한다.

conda list

참고로 conda 환경에서도 pip 등을 통한 설치가 가능하다.

environment.yml 파일 생성 및 가상환경 생성

설치된 패키지 목록을 .yml 파일로 저장하는 명령이다.
pip freeze > requirements.txt와 같은 역할이다.

conda env export > environment.yml

만들어진 파일은 다음과 비슷하게 생겼다.

name: condatorch
channels:
  - pytorch
  - defaults
dependencies:
  - blas=1.0=mkl
  - certifi=2018.11.29=py36_0
  ...
  - zstd=1.3.7=h508b16e_0
  - pip:
    - cycler==0.10.0
    ...
    - six==1.12.0
prefix: C:\ProgramData\Miniconda3\envs\condatorch

만들어진 파일로 가상환경을 생성하는 방법은 위에서도 설명했지만 다음과 같다.

# 이 경우에는 env를 앞에 써 주어야 한다.
# -f는 --file을 의미한다.
conda env create -f environment.yml -n myenv

패키지 업데이트

특정 환경 안의 특정 패키지를 업데이트하려면 다음과 같이 하면 된다.

conda update -n <environment_name> spacy

특정 환경 안의 모든 패키지를 업데이트하려면 다음과 같이 하면 된다.

conda update -n <environment_name> --all
# 현재 환경 업데이트
conda update --all

Conda 버전 확인 및 update

명령창에서 Conda의 버전을 확인하는 방법은 다음과 같다.

conda -V
conda --version

Conda 자체를 업데이트하는 방법은 다음과 같다.

conda update conda
conda update anaconda

References

공식 홈페이지에서 더 자세한 사용법을 찾아볼 수 있다.

Comment  Read more

Jupyter Notebook 사용법(주피터 노트북 설치 및 사용법)

|

Jupyter notebook은 대화형 파이썬 인터프리터(Interpreter)로서 웹 브라우저 환경에서 파이썬 코드를 작성 및 실행할 수 있는 툴이다.
서버에 Jupyter notebook을 설치하여 포트를 개방한 후 해당 url에 접속하여 원격으로 사용하거나, 로컬 환경에서 브라우저를 띄워 대화형 환경에서 코드를 작성 및 실행할 수 있다.

설치 및 실행

설치

설치는 두 가지 방법이 있는데, 첫 번째는 Anaconda와 함께 설치하는 방법이 있다. Anaconda를 설치할 때 Jupyter Notebook도 같이 설치하면 된다.
Anaconda와 같은 역할을 하는 Miniconda 사용법은 여기를 참조하도록 한다.

Anadonda를 설치하는 방법 외에 기본적으로 pip은 Jupyter 패키지 설치를 지원한다. 설치 방법은 다른 패키지 설치 방법과 똑같다.

pip install jupyter

파이썬3과 2를 구분지어야 한다면 pip 대신 pip3를 사용한다.

실행 및 종료

jupyter notebook

01_jupyter_notebook

위 명령을 입력하면 자동으로 어떤 html 파일을 열면서 브라우저가 실행된다. 만약 실행되지 않는다면 http://localhost:8888 으로 접속하거나 위 그림의 맨 마지막 줄에 있는 url을 복사하여 브라우저에서 접속한다.

그러면 위 명령을 실행한 디렉토리 위치(위 그림에서 jupyter notebook을 실행한 줄에서 볼 수 있다. 필자의 경우 C:\JupyterTest)의 파일들이 브라우저에 보이게 된다.

02_broswer

Jupyter의 실행을 종료하려면 명령창에서 Ctrl + C를 입력한다.

03_terminate

고급: 실행 옵션

명령 옵션의 도움말을 표시한다.

jupyter notebook --help

실행 속도 상승을 위해 MathJax를 무효화할 수 있다. MathJax는 수식 입력을 위해 필요한 JavaScript 라이브러리이다.

jupyter notebook --no-mathjax

웹 브라우저를 지정하거나 실행시키지 않을 수 있다. 포트 번호 지정도 가능하다.

jupyter notebook --browser="safari"
jupyter notebook --no-browser
jupyter notebook --port=8889

노트북 실행 시 실행 디렉토리를 지정할 수 있다. 기본값은 현재 밍령창에서의 실행 위치이다.

jupyter notebook --notebook-dir=/user/define/directory

고급: 설정 파일 수정

매번 옵션을 지정해서 실행하기가 귀찮다면, Jupyter Notebook의 기본 설정을 변경하기 위해 다음 명령을 입력한다.

jupyter notebook --generate-config

그러면 Jupyter가 실행되는 대신 설정 파일이 열린다.
Linux에서는 기본적으로 /home/<username>/.jupyter/jupyter_notebook_config.py 파일로 생성되며, 윈도우에서는 C:\Users\<username>\.jupyter\jupyter_notebook_config.py로 생성된다.

설정 파일에서 필요한 옵션을 변경하여 사용하면 된다. 기본적으로 사용하지 않는 옵션은 모두 주석 처리되어 있다.

기본 설정 파일을 재지정하고 싶으면 다음과 같이 입력한다.

jupyter notebook --config=custom_config.py

임시로 설정 파일을 변경해서 실행하고 싶다면 일반 옵션 주듯이 하면 된다.

jupyter notebook --config custom_config.py

Jupyter notebook을 단순히 로컬 환경에서 실행하는 것이 아니라 서버로 띄워 놓고 원격 접속을 하려면, 위 방법으로 허용 포트나 접속 주소 등 설정 파일을 수정해야 한다.

고급: 원격 접속 설정

localhost(127.0.0.1) 말고 다른 컴퓨터에서 (서버로) 원격접속하고 싶을 때가 있다. 그럴 때는 다음 과정을 따른다.

  1. 명령창(cmd or terminal)에 python 또는 ipython을 입력하여 대화창을 연다.
    • 다음을 입력한다:
        >>> from notebook.auth import passwd
        >>> passwd()
        Enter password: 
        Verity password: 
        'sha1:c5b493745105:0d26dcd6e9cf868d3b49f43d'
      
    • 출력으로 나온 암호화된 비밀번호를 기억해 둔다.
    • 참고로 linux에서나 윈도우에서나 passwd() 등으로 비밀번호를 입력할 때에는 명령창에 입력하고 있는 문자가 전혀 표시되지 않는다. 별표(*)로도 표시되지 않으니 참고.
    • 대화창을 종료한다.
  2. 이제 조금 전에 생성한 jupyter_notebook_config.py를 편집기로 연다.
    • 아래처럼 주석처리된 부분을 다음과 같이 바꾼다. 물론 비밀번호는 조금 전 여러분이 생성한 문자열로 입력해야 한다.
        #c.NotebookApp.password = '' 
      
        c = get_config()
        c.NotebookApp.password = 'sha1:c5b493745105:0d26dcd6e9cf868d3b49f43d'
      
    • 필수: 비슷하게 다음 설정들을 바꿔주어야 한다. 모든 설정을 변경할 때에는 앞의 주석(#)을 지우도록 한다.
      • 외부접속 허용: c.NotebookApp.allow_origin = '*'
      • IP 설정: c.NotebookApp.ip = <여러분의 IP>
    • 옵션: 다음은 하고 싶으면 하도록 한다.
      • 작업경로 설정: c.NotebookApp.notebook_dir = <원하는 경로>
      • 포트 설정: c.NotebookApp.port = <원하는 port>
      • jupyter notebook으로 실행 시 브라우저 실행 여부: c.NotebookApp.open_browser = False

이제 외부접속을 할 때는 서버에서

  • jupyter notebook을 실행시킨 다음
  • <여러분의 IP="">:<원하는 port=""> 형식을 브라우저의 주소창에 입력하면 된다.
    • 예시: 123.212.321.14:8888
  • 여러분이 설정한 비밀번호를 입력한다. 암호화된 문자열이 아니라 passwd() 에서 입력한 비밀번호면 된다.
  • 물론 일반 가정집에서는 그냥 ip를 할당할 수 없기 때문에 공유기 설정을 해주거나, 회사 컴퓨터 등이라면 따로 접속 허용하는 절차를 거쳐야 한다. 이 부분은 여기서는 ~pass~
    • 그냥 되는 경우도 있다. 안 되는 경우에만 검색해서 해 보기 바람.

Jupyter의 기본 사용법

새 파일 생성

04_new

오른쪽 부분의 New 버튼을 클릭하면 Python 3, Text File, Folder, Terminal 등의 옵션이 있다(파이썬 버전에 따라 Python 2가 있을 수 있다). 우선 Python 3을 클릭하여 Python 3 코드를 입력할 수 있는 창을 열도록 한다.

05_python3

생성하면 맨 위에 기본적으로 Untitled라는 제목으로 생성이 된다. 파일 탐색기나 Finder 등에서도 Untitled.ipynb라는 파일을 확인할 수 있다.

06_ipynb

위의 checkpoints 디렉토리는 자동으로 생성된다. Jupyter는 자동저장이 되고(맨 위의 autosaved), 체크포인트를 따로 설정할 수 있다.

제목은 Untitled 부분을 클릭하면 수정할 수 있다.

편집 / 명령 모드

편집 모드에서는 셀의 내용을 편집할 수 있고(셀의 테두리가 초록색), 명령 모드는 편집중이 아닌 상태 또는 셀 자체에 조작을 가하는 상태(셀의 테두리가 파란색)이다.
명령 모드에서 편집 모드로 들어가려면 Enter키를, 반대로는 Esc 키를 누르면 된다.

셀의 타입

Code 타입, Markdown 타입이 있다.
Code 타입은 일반 코드를 실행할 수 있는 셀이다. 기본적으로 셀을 생성하면 Code 타입으로 생성된다.
Markdown 타입은 Markdown으로 셀의 내용을 작성할 수 있다. 코드로 실행되지는 않으며, 수식을 작성할 수 있다. 수식은 MathJax에 의해 지원된다. 수식 작성 방법은 여기를 참고한다.

Markdown 타입으로 변경하면 Markdown 코드를 작성할 수 있다. Shift + Enter 키를 누르면 마크다운이 실제 보여지는 방식으로 변경되며, 다시 수정하려면 Enter 또는 더블 클릭하면 편집 가능하다.

07_markdown

셀 실행

실행하고 싶은 셀의 아무 곳에나 커서를 위치시킨 후 Shift + Enter 키를 누른다.
실행하면 셀 아래쪽에는 실행 결과가 표시되고, 셀 옆의 ‘In [ ]’과 ‘Out [ ]’에 몇 번째로 실행시켰는지를 나타내는 숫자가 표시된다. 여러 번 실행하면 계속 숫자가 올라간다.

08_run

강제 중단 / 재실행

제목 아래 줄의 탭에 Kernel 탭이 있다. 커널은 IPython 대화창 아래에서 백그라운드 비슷하게 실행되는 일종의 운영체제 같은 개념이다. IPython 대화창을 관리한다고 보면 된다.

09_interrupt

Kernel 탭의 모든 버튼은 코드를 삭제하지는 않는다. 각 버튼의 기능을 설명하면,

  • Interrupt: 실행 중인 코드를 강제 중지한다. 중지하면 위 그림과 같은 에러가 뜨며 실행이 중지된다.
  • Restart: 실행 중인 코드가 중지되며 재시작된다. 코드나 실행 결과는 삭제되지 않는다.
  • Restart & Clear Output: 코드는 중지되며 실행 결과도 삭제한다.
  • Restart & Run All: 재시작 후 모든 셀의 코드를 위에서부터 순차적으로 한 번씩 실행한다.
  • Reconnect: 인터넷 연결이 끊어졌을 때 연결을 재시도한다.
  • Shutdown: 커널을 종료한다. 이 버튼을 누르면 실행 결과는 삭제되지 않으나 완전 종료된 상태로 더 이상 메모리를 잡아먹지 않는다.

Shutdown되었거나, 인터넷 연결이 끊어졌거나, 기타 문제가 있으면 아래와 같이 탭 옆에 알림이 표시된다. Shutdown 된 경우 No kernel이라고 뜬다.

10_shutdowned

현재 실행중인 커널이 있는지 확인하는 방법은 두 가지다. 첫 번째는 Home 화면에서 ipynb 파일의 아이콘이 초록색이면 실행중, 회색이면 중단된 또는 시작되지 않은 상태이다. 여기서는 해당 디렉토리에서 실행중인 것만 확인할 수 있다.

11_shutdowned

또 하나는 Home 화면에서 Files 탭 대신 Running 탭을 클릭하면 실행 중인 IPython과 터미널의 목록을 확인할 수 있다. 이 탭에서는 전체 디렉토리에서 실행중인 파일 또는 터미널을 전부 볼 수 있다.

12_list

Text File 생성

New 버튼에서 Text File을 생성하면 .txt 파일이나 .py 파일 등을 만들 수 있다. 이렇게 만든 파일은 대화 형식으로 실행되지 않고, 터미널에서 실행시켜야 한다. 읽는 것은 IPython 창에서도 가능하다.

Folder 생성

디렉토리를 생성할 때 사용한다. 폴더랑 같은 것이다.

터미널

New 버튼으로 Terminal을 클릭하면, 터미널을 하나 새로 연다. 이것은 윈도우나 맥 등의 명령창(cmd 또는 terminal)과 같다. 여기서 .py 파일을 실행시킬 수 있고, 파일의 목록을 보거나 삭제하는 등의 명령이 모두 가능하다. Running 탭에서 중지시킬 수 있다.

13_terminal

파일 이름 변경 또는 삭제

파일 맨 왼쪽의 체크박스를 클릭하면 복제, 수정, 삭제 등이 가능하다. 물론 로컬 파일 탐색기에서 수정이나 삭제를 해도 되며, 서버가 연결에 문제가 없으면 바로 반영된다.

14_name

자동완성

웬만한 IDE에는 다 있는 자동완성 기능이다. 변수나 함수 등을 일부만 입력하고 Tab 키를 누르면 된다. 따로 설명할 필요는 없을 듯 하다.


단축키

단축키 정보는 [Help] - [Keyboard Shortcuts] 또는 명령 모드에서 H를 눌러서 표시할 수 있다.

공용 단축키 설명
Shift + Enter 액티브 셀을 실행하고 아래 셀을 선택한다.
Ctrl + Enter 액티브 셀을 실행한다.
Alt + Enter 액티브 셀을 실행하고 아래에 셀을 하나 생성한다.
편집 모드 단축키 설명
Ctrl + Z Undo 명령이다.
Ctrl + Shift + Z Redo 명령이다.
Tab 자동완성 또는 Indent를 추가한다.
Shift + Tab 툴팁 또는 변수의 상태를 표시한다.
Ctrl + Shift + - 커서의 위치에서 셀을 잘라 두 개로 만든다.

참고로 명령 모드 단축키 중 콤마(,)로 되어 있는 것은 연속해서 누르라는 의미이다. 예로 D를 두 번 누르면 액티브 코드 셀을 삭제한다.

명령 모드 단축키 설명
↑, ↓ 셀 선택
A 액티브 코드 셀의 위(Above)에 셀을 하나 생성한다.
B 액티브 코드 셀의 위(Below)에 셀을 하나 생성한다.
Ctrl + S Notebook 파일을 저장한다.
Shift + L 줄 번호 표시를 토글한다.
D, D (D 두번 연속으로 타이핑)액티브 코드 셀을 삭제한다.
Z 삭제한 셀을 하나 복원한다.
Y 액티브 코드 셀을 Code 타입(코드를 기술하는 타입)으로 한다.
M 액티브 코드 셀을 Markdown 타입으로 한다.
O, O 커널을 재시작한다.
P 명령 팔레트를 연다.
H 단축키 목록을 표시한다. Enter 키로 숨긴다.

Jupyter의 기능

DocString의 표시

선언한 변수 뒤에 ?를 붙여서 셀을 실행하는 것으로 해당 변수의 상태를 확인할 수 있다.

약간 다른 방법으로 변수를 타이핑한 후 Shift + Tab을 누르면 툴팁이 표시된다.
툴팁에는 DocString의 일부 내용이 표시된다.

이미지 첨부하기

Drag & Drop으로 첨부할 수 있다.

shell(명령 프롬프트)의 이용

명령창에서 쓰는 명령을 그대로 쓰되, 맨 앞에 !를 입력하여 사용 가능하다.

!cd Documents

매직 명령어 이용

맨 앞에 %를 붙이고 특정 명령을 수행할 수 있다. 이는 파이썬 문법에는 포함되지 않은, Jupyter notebook만의 기능이다.

15_magic

매직 명령어 설명
%pwd 현재 디렉토리 경로 출력
%time 코드 코드의 실행 시간을 측정하여 표시
%timeit 코드 코드를 여러 번 실행한 결과를 요약하여 표시
%history -l 3 최근 3개의 코드 실행 이력 취득
%ls 윈도우의 dir, Linux의 ls 명령과 같음
%autosave n 자동저장 주기를 설정한다. 초 단위이며, 0이면 무효로 한다.
%matplotlib 그래프를 그리는 코드 위에 따로 설정한다. %matplotlib inline으로 설정하면 코드 셀의 바로 아래에, %matplotlib tk로 설정하면 별도 창에 그래프가 출력된다. %matplotlib notebook으로 하면 코드 셀 바로 아래에 동적으로 그래프를 조작할 수 있는 그래프가 생성된다.
# 코드 실행 시간 측정
%time sum(range(10000))
# 결과:
# CPU times: user 225 us, sys: 0 ns, total: 225 us
# Wall time: 228 us
# 499950000

# 1000회 반복, 3회 실행
%timeit sum(range(10000))
# 결과: 
# 1000 loops, best of 3: 238 us for loop

# 옵션 지정하기
%timeit -n 2000 -r 5 sum(range(10000))

# 셀 전체의 시간 측정
%%timeit -n 1000 -r 3
s = 0
for i in range(10000):
    s += i
Comment  Read more

PyTorch 사용법 - 03. How to Use PyTorch

|

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


2020.02.04 Updated

이 글에서는 PyTorch 프로젝트를 만드는 방법에 대해서 알아본다.

사용되는 torch 함수들의 사용법은 여기에서 확인할 수 있다.

Pytorch의 학습 방법(loss function, optimizer, autograd, backward 등이 어떻게 돌아가는지)을 알고 싶다면 여기로 바로 넘어가면 된다.

Pytorch 사용법이 헷갈리는 부분이 있으면 Q&A 절을 참고하면 된다.

예시 코드의 많은 부분은 링크와 함께 공식 Pytorch 홈페이지(pytorch.org/docs)에서 가져왔음을 밝힌다.

주의: 이 글은 좀 길다. ㅎ


Import

# preprocess, set hyperparameter
import argparse
import os

# load data
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# train
from torch import nn
from torch.nn import functional as F

# visualization
import matplotlib.pyplot as plt

argparse

python train.py --epochs 50 --batch-size 64 --save-dir weights

Machine Learning을 포함해서, 위와 같은 실행 옵션은 많은 코드에서 볼 수 있었을 것이다. 학습 과정을 포함하여 대부분은 명령창 또는 콘솔에서 python 파일 옵션들...으로 실행시키기 때문에, argparse에 대한 이해는 필요하다.

argparse에 대한 내용은 여기를 참조하도록 한다.


Load Data

전처리하는 과정을 설명할 수는 없다. 데이터가 어떻게 생겼는지는 직접 봐야 알 수 있다.
다만 한 번 쓰고 말 것이 아니라면, 데이터가 추가되거나 변경점이 있더라도 전처리 코드의 대대적인 수정이 발생하도록 짜는 것은 본인 손해이다.

단순한 방법

data = pd.read_csv('data/02_Linear_Regression_Model_Data.csv')
# Avoid copy data, just refer
x = torch.from_numpy(data['x'].values).unsqueeze(dim=1).float()
y = torch.from_numpy(data['y'].values).unsqueeze(dim=1).float()

pandascsv 패키지 등으로 그냥 불러오는 방법이다. 데이터가 복잡하지 않은 형태라면 단순하고 유용하게 쓸 수 있다. 그러나 이 글에서 중요한 부분은 아니다.

torch.utils.data.DataLoader

참조: torch.utils.data.DataLoader

Pytorch는 DataLoader라고 하는 괜찮은 utility를 제공한다. 간단하게 생각하면 DataLoader 객체는 학습에 쓰일 데이터 전체를 보관했다가, train 함수가 batch 하나를 요구하면 batch size 개수만큼 데이터를 꺼내서 준다고 보면 된다.

  • 실제로 [batch size, num]처럼 미리 잘라놓는 것은 아니고, 내부적으로 Iterator에 포함된 Index가 존재한다. train() 함수가 데이터를 요구하면 사전에 저장된 batch size만큼 return하는 형태이다.

사용할 torch.utils.data.Dataset에 따라 반환하는 데이터(자연어, 이미지, 정답 label 등)는 조금씩 다르지만, 일반적으로 실제 DataLoader를 쓸 때는 다음과 같이 쓰기만 하면 된다.

for idx, (data, label) in enumerate(data_loader):
    ...

DataLoader 안에 데이터가 어떻게 들어있는지 확인하기 위해, MNIST 데이터를 가져와 보자. DataLoader는 torchvision.datasetstorchvision.transforms와 함께 자주 쓰이는데, 각각 Pytorch가 공식적으로 지원하는 dataset, 데이터 transformation 및 augmentation 함수들(주로 이미지 데이터에 사용)를 포함한다.
각각의 사용법은 아래 절을 참조한다.

input_size = 28
batch_size = 64

transform = transforms.Compose([transforms.Resize((input_size, input_size)),
                                transforms.ToTensor()])
data_loader = DataLoader(
    datasets.MNIST('data/mnist', train=True, download=True, transform=transform),
    batch_size=batch_size,
    shuffle=True)

print('type:', type(data_loader), '\n')

first_batch = data_loader.__iter__().__next__()
print('{:15s} | {:<25s} | {}'.format('name', 'type', 'size'))
print('{:15s} | {:<25s} | {}'.format('Num of Batch', '', len(data_loader)))
print('{:15s} | {:<25s} | {}'.format('first_batch', str(type(first_batch)), len(first_batch)))
print('{:15s} | {:<25s} | {}'.format('first_batch[0]', str(type(first_batch[0])), first_batch[0].shape))
print('{:15s} | {:<25s} | {}'.format('first_batch[1]', str(type(first_batch[1])), first_batch[1].shape))

결과:

type: <class 'torch.utils.data.dataloader.DataLoader'> 

name            | type                      | size
Num of Batch    |                           | 938
first_batch     | <class 'list'>            | 2
first_batch[0]  | <class 'torch.Tensor'>    | torch.Size([64, 1, 28, 28])
first_batch[1]  | <class 'torch.Tensor'>    | torch.Size([64])
# 총 데이터의 개수는 938 * 28 ~= 60000(마지막 batch는 32)이다.

Custom Dataset 만들기

nn.Module을 상속하는 Custom Model처럼, Custom DataSet은 torch.utils.data.Dataset를 상속해야 한다. 또한 override해야 하는 것은 다음 두 가지다. python dunder를 모른다면 먼저 구글링해보도록 한다.

  • __len__(self): dataset의 전체 개수를 알려준다.
  • __getitem__(self, idx): parameter로 idx를 넘겨주면 idx번째의 데이터를 반환한다.

위의 두 가지만 기억하면 된다. 전체 데이터 개수와, i번째 데이터를 반환하는 함수만 구현하면 Custom DataSet이 완성된다.
다음에는 완성된 DataSet을 torch.utils.data.DataLoader에 인자로 전달해주면 끝이다.

완전 필수는 아니지만 __init__()도 구현하는 것이 좋다.

1차함수 선형회귀(Linear Regression)의 를 들면 다음과 같다.
데이터는 여기에서 받을 수 있다.

class LinearRegressionDataset(Dataset):

    def __init__(self, csv_file):
        """
        Args:
            csv_file (string): Path to the csv file. 
        """
        data = pd.read_csv(csv_file)
        self.x = torch.from_numpy(data['x'].values).unsqueeze(dim=1).float()
        self.y = torch.from_numpy(data['y'].values).unsqueeze(dim=1).float()

    def __len__(self):
        return len(self.x)

    def __getitem__(self, idx):
        x = self.x[idx]
        y = self.y[idx]

        return x, y

dataset = LinearRegressionDataset('02_Linear_Regression_Model_Data.csv')

torchvision.datasets

참조: torchvision.datasets

Pytorch가 공식적으로 다운로드 및 사용을 지원하는 datasets이다. 2020.02.04 기준 dataset 목록은 다음과 같다.

  • MNIST
    • MNIST(숫자 0~9에 해당하는 손글씨 이미지 6만(train) + 1만(test))
    • Fashion-MNIST(간소화된 의류 이미지),
    • KMNIST(일본어=히라가나, 간지 손글씨),
    • EMNIST(영문자 손글씨),
    • QMNIST(MNIST를 재구성한 것)
  • MS COCO
    • Captions(이미지 한 장과 이를 설명하는 한 영문장),
    • Detection(이미지 한 장과 여기에 있는 object들을 segmantation한 정보)
  • LSUN(https://www.yf.io/p/lsun),
  • ImageFolder, DatasetFolder
  • Image:
    • ImageNet 2012,
    • CIFAR10 & CIFAR100,
    • STL10, SVHN, PhotoTour, SBU
  • Flickr8k & Flickr30k, VOC Segmantation & Detection,
  • Cityscapes, SBD, USPS, Kinetics-400, HMDB51, UCF101

각각의 dataset마다 필요한 parameter가 조금씩 다르기 때문에, MNIST만 간단히 설명하도록 하겠다. 사실 공식 홈페이지를 참조하면 어렵지 않게 사용 가능하다.

01_MNIST

  • root: 데이터를 저장할 루트 폴더이다. 보통 data/data/mnist/를 많이 쓰는 것 같지만, 상관없다.
  • train: 학습 데이터를 받을지, 테스트 데이터를 받을지를 결정한다.
  • download: true로 지정하면 알아서 다운로드해 준다. 이미 다운로드했다면 재실행해도 다시 받지 않는다.
  • transform: 지정하면 이미지 데이터에 어떤 변형을 가할지를 transform function의 묶음(Compose)로 전달한다.
  • target_transform: 보통 위의 transform까지만 쓰는 것 같다. 쓰고 싶다면 이것도 쓰자.

torchvision.transforms

참조: torchvision.transforms

  1. 이미지 변환 함수들을 포함한다. 상대적으로 자주 쓰이는 함수는 다음과 같은 것들이 있다. 더 많은 목록은 홈페이지를 참조하면 된다. 참고로 parameter 중 transforms는 변환 함수들의 list 또는 tuple이다.

    • transforms.CenterCrop(size): 이미지의 중앙 부분을 크롭하여 [size, size] 크기로 만든다.
    • transforms.Resize(size, interpolation=2): 이미지를 지정한 크기로 변환한다. 직사각형으로 자를 수 있다.
      • 참고: transforms.Scale는 Resize에 의해 deprecated되었다.
    • transforms.RandomCrop(size, padding=None, pad_if_needed=False, fill=0, padding_mode=’constant’): 이미지의 랜덤한 부분을 [size, size] 크기로 잘라낸다. input 이미지가 output 크기보다 작으면 padding을 추가할 수 있다.
    • transforms.RandomResizedCrop(size, scale=(0.08, 1.0), ratio=(0.75, 3/4), interpolation=2): 이미지를 랜덤한 크기 및 비율로 자른다.
      • 참고: transforms.RandomSizedCrop는 RandomResizedCrop에 의해 deprecated되었다.
    • transforms.RandomRotation(degrees, resample=False, expand=False, center=None): 이미지를 랜덤한 각도로 회전시킨다.
    • transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0): brightness, contrast 등을 변화시킨다.
  2. 이미지를 torch.Tensor 또는 PILImage로 변환시킬 수 있다. 사용자 정의 변환도 가능하다.
    • transforms.ToPILImage(mode=None): PILImage로 변환시킨다.
    • transforms.ToTensor(): torch.Tensor로 변환시킨다.
    • transforms.Lambda(lambd): 사용자 정의 lambda function을 적용시킨다.
  3. torch.Tensor에 적용해야 하는 변환 함수들도 있다.
    • transforms.LinearTransformation(transformation_matrix): tensor로 표현된 이미지에 선형 변환을 시킨다.
    • transforms.Normalize(mean, std, inplace=False): tensor의 데이터 수치(또는 범위)를 정규화한다.
  4. brightness나 contrast 등을 바꿀 수도 있다.
    • transforms.functional.adjust_contrast(img, contrast_factor) 등
  5. 위의 변환 함수들을 랜덤으로 적용할지 말지 결정할 수도 있다.

    • transforms.RandomChoice(transforms): transforms 리스트에 포함된 변환 함수 중 랜덤으로 1개 적용한다.
    • transforms.RandomApply(transforms, p=0.5): transforms 리스트에 포함된 변환 함수들을 p의 확률로 적용한다.
  6. 위의 모든 변환 함수들을 하나로 조합하는 함수는 다음과 같다. 이 함수를 dataloader에 넘기면 이미지 변환 작업이 간단하게 완료된다.

    • transforms.Compose(transforms)
      transforms.Compose([
       transforms.CenterCrop(14),
       transforms.ToTensor(),
       transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))
      ])
      

변환 순서는 보통 resize/crop, toTensor, Normalize 순서를 거친다. Normalize는 tensor에만 사용 가능하므로 이 부분은 순서를 지켜야 한다.

torchtext

자연어처리(NLP)를 다룰 때 쓸 수 있는 좋은 라이브러리가 있다. 이는 자연어처리 데이터셋을 다루는 데 있어서 매우 편리한 기능을 제공한다.

  • 데이터셋 로드
  • 토큰화(Tokenization)
  • 단어장(Vocabulary) 생성
  • Index mapping: 각 단어를 해당하는 인덱스로 매핑
  • 단어 벡터(Word Vector): word embedding을 만들어준다. 0이나 랜덤 값 및 사전학습된 값으로 초기화할 수 있다.
  • Batch 생성 및 (자동) padding 수행

설치는 다음과 같다.

pip install torchtext
# conda 환경에선 다음과 같다.
conda install -c pytorch torchtext

Define and Load Model

Pytorch Model

gradient 계산 방식 등 Pytorch model의 작동 방식은 Set Loss function(creterion) and Optimizer 절을 보면 된다.

Pytorch에서 쓰는 용어는 Module 하나에 가깝지만, 많은 경우 layer나 model 등의 용어도 같이 사용되므로 굳이 구분하여 적어 보았다.

Layer : Model 또는 Module을 구성하는 한 개의 층, Convolutional Layer, Linear Layer 등이 있다.
Module : 1개 이상의 Layer가 모여서 구성된 것. Module이 모여 새로운 Module을 만들 수도 있다.
Model : 여러분이 최종적으로 원하는 것. 당연히 한 개의 Module일 수도 있다.

예를 들어 nn.Linear는 한 개의 layer이기도 하며, 이것 하나만으로도 module이나 Model을 구성할 수 있다. 단순 Linear Model이 필요하다면, model = nn.Linear(1, 1, True)처럼 사용해도 무방하다.

PyTorch의 모든 모델은 기본적으로 다음 구조를 갖는다. PyTorch 내장 모델뿐 아니라 사용자 정의 모델도 반드시 이 정의를 따라야 한다.

import torch.nn as nn
import torch.nn.functional as F

class Model_Name(nn.Module):
    def __init__(self):
        super(Model_Name, self).__init__()
        self.module1 = ...
        self.module2 = ...
        """
        ex)
        self.conv1 = nn.Conv2d(1, 20, 5)
        self.conv2 = nn.Conv2d(20, 20, 5)
        """

    def forward(self, x):
        x = some_function1(x)
        x = some_function2(x)
        """
        ex)
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        """
        return x

PyTorch 모델로 쓰기 위해서는 다음 조건을 따라야 한다. 내장된 모델들(nn.Linear 등)은 당연히 이 조건들을 만족한다.

  1. torch.nn.Module을 상속해야 한다.
  2. __init__()forward()를 override해야 한다.
    • 사용자 정의 모델의 경우 init과 forward의 인자는 자유롭게 바꿀 수 있다. 이름이 x일 필요도 없으며, 인자의 개수 또한 달라질 수 있다.

이 두 가지 조건은 PyTorch의 기능들을 이용하기 위해 필수적이다.

따르지 않는다고 해서 에러를 내뱉진 않지만, 다음 규칙들은 따르는 것이 좋다:

  1. __init__()에서는 모델에서 사용될 module을 정의한다. module만 정의할 수도, activation function 등을 전부 정의할 수도 있다.
    • 아래에서 설명하겠지만 module은 nn.Linear, nn.Conv2d 등을 포함한다.
    • activation function은 nn.functional.relu, nn.functional.sigmoid 등을 포함한다.
  2. forward()에서는 모델에서 행해져야 하는 계산을 정의한다(대개 train할 때). 모델에서 forward 계산과 backward gradient 계산이 있는데, 그 중 forward 부분을 정의한다. input을 네트워크에 통과시켜 어떤 output이 나오는지를 정의한다고 보면 된다.
    • __init__()에서 정의한 module들을 그대로 갖다 쓴다.
    • 위의 예시에서는 __init__()에서 정의한 self.conv1self.conv2를 가져다 썼고, activation은 미리 정의한 것을 쓰지 않고 즉석에서 불러와 사용했다.
    • backward 계산은 PyTorch가 알아서 해 준다. backward() 함수를 호출하기만 한다면.

nn.Module

여기를 참고한다. 요약하면 nn.Module은 모든 PyTorch 모델의 base class이다.

nn.Module 내장 함수

nn.Module에 내장된 method들은 모델을 추가 구성/설정하거나, train/eval(test) 모드 변경, cpu/gpu 변경, 포함된 module 목록을 얻는 등의 활동에 초점이 맞춰져 있다.

모델을 추가로 구성하려면,

  • add_module(name, module): 현재 module에 새로운 module을 추가한다.
  • apply(fn): 현재 module의 모든 submodule에 해당 함수(fn)을 적용한다. 주로 model parameter를 초기화할 때 자주 쓴다.

모델이 어떻게 생겼는지 보려면,

  • children(), modules(): 자식 또는 모델 전체의 모든 module에 대한 iterator를 반환한다.
  • named_buffers(), named_children(), named_modules(), named_parameters(): 위 함수와 비슷하지만 이름도 같이 반환한다.

모델을 통째로 저장 혹은 불러오려면,

  • state_dict(destination=None, prefix='', keep_vars=False): 모델의 모든 상태(parameter, running averages 등 buffer)를 딕셔너리 형태로 반환한다.
  • load_state_dict(state_dict, strict=True): parameter와 buffer 등 모델의 상태를 현 모델로 복사한다. strict=True이면 모든 module의 이름이 정확히 같아야 한다.

학습 시에 필요한 함수들을 살펴보면,

  • cuda(device=None): 모든 model parameter를 GPU 버퍼에 옮기는 것으로 GPU를 쓰고 싶다면 이를 활성화해주어야 한다.
    • GPU를 쓰려면 두 가지에 대해서만 .cuda()를 call하면 된다. 그 두 개는 모든 input batch 또는 tensor, 그리고 모델이다.
    • .cuda()는 optimizer를 설정하기 전에 실행되어야 한다. 잊어버리지 않으려면 모델을 생성하자마자 쓰는 것이 좋다.
  • eval(), train(): 모델을 train mode 또는 eval(test) mode로 변경한다. Dropout이나 BatchNormalization을 쓰는 모델은 학습시킬 때와 평가할 때 구조/역할이 다르기 때문에 반드시 이를 명시하도록 한다.
  • parameters(recurse=True): module parameter에 대한 iterator를 반환한다. 보통 optimizer에 넘겨줄 때 말고는 쓰지 않는다.
  • zero_grad(): 모든 model parameter의 gradient를 0으로 설정한다.

사용하는 법은 매우 간단히 나타내었다. Optimizer에 대한 설명은 여기를 참조하면 된다.

import torchvision
from torch import nn

def user_defined_initialize_function(m):
    pass

model = torchvision.models.vgg16(pretrained=True)
# 예시는 예시일 뿐
last_module = nn.Linear(1000, 32, bias=True)
model.add_module('last_module', last_module)
last_module.apply(user_defined_initialize_function)
model.cuda()

# set optimizer. model.parameter를 넘겨준다.
optimizer = optim.Adam(model.parameters(), lr=0.0001, betas=(0.5, 0.999))

# train
model.train()
for idx, (data, label) in dataloader['train']:
    ...

# test
model.eval()
for idx, (data, label) in dataloader['test']:
    ...

Pytorch Layer의 종류

참조: nn.module

참고만 하도록 한다. 좀 많다. 쓰고자 하는 것과 이름이 비슷하다 싶으면 홈페이지를 참조해서 쓰면 된다.

  1. Linear layers
    • nn.Linear
    • nn.Bilinear
  2. Convolution layers
    • nn.Conv1d, nn.Conv2d, nn.Conv3d
    • nn.ConvTranspose1d, nn.ConvTranspose2d, nn.ConvTranspose3d
    • nn.Unfold, nn.Fold
  3. Pooling layers
    • nn.MaxPool1d, nn.MaxPool2d, nn.MaxPool3d
    • nn.MaxUnpool1d, nn.MaxUnpool2d, nn.MaxUnpool3d
    • nn.AvgPool1d, nn.AvgPool2d, nn.AvgPool3d
    • nn.FractionalMaxPool2d
    • nn.LPPool1d, nn.LPPool2d
    • nn.AdaptiveMaxPool1d, nn.AdaptiveMaxPool2d, nn.AdaptiveMaxPool3d
    • nn.AdaptiveAvgPool1d, nn.AdaptiveAvgPool2d, nn.AdaptiveAvgPool3d
  4. Padding layers
    • nn.ReflectionPad1d, nn.ReflectionPad2d
    • nn.ReplicationPad1d, nn.ReplicationPad2d, nn.ReplicationPad3d
    • nn.ZeroPad2d
    • nn.ConstantPad1d, nn.ConstantPad2d, nn.ConstantPad3d
  5. Normalization layers
    • nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d
    • nn.GroupNorm
    • nn.InstanceNorm1d, nn.InstanceNorm2d, nn.InstanceNorm3d
    • nn.LayerNorm
    • nn.LocalResponseNorm
  6. Recurrent layers
    • nn.RNN, nn.RNNCell
    • nn.LSTM, nn.LSTMCell
    • nn.GRU, nn.GRUCell
  7. Dropout layers
    • nn.Dropout, nn.Dropout2d, nn.Dropout3d
    • nn.AlphaDropout
  8. Sparse layers
    • nn.Embedding
    • nn.EmbeddingBag

Pytorch Activation function의 종류

참조: Activation functions

  1. Non-linear activations
    • nn.ELU, nn.SELU
    • nn.Hardshrink, nn.Hardtanh
    • nn.LeakyReLU, nn.PReLU, nn.ReLU, nn.ReLU6, nn.RReLU
    • nn.Sigmoid, nn.LogSigmoid
    • nn.Softplus, nn.Softshrink, nn.Softsign
    • nn.Tanh, nn.Tanhshrink
    • nn.Threshold
  2. Non-linear activations (other)
    • nn.Softmin
    • nn.Softmax, nn.Softmax2d, nn.LogSoftmax
    • nn.AdaptiveLogSoftmaxWithLoss

Containers

참조: Containers

여러 layer들을 하나로 묶는 데 쓰인다.
종류는 다음과 같은 것들이 있는데, Module 설계 시 자주 쓰는 것으로 nn.Sequential이 있다.

  • nn.Module
  • nn.Sequential
  • nn.ModuleList
  • nn.ModuleDict
  • nn.ParameterList
  • nn.ParameterDict

nn.Sequential

참조: nn.Sequential

이름에서 알 수 있듯 여러 module들을 연속적으로 연결하는 모델이다.

# Example of using Sequential
model = nn.Sequential(
          nn.Conv2d(1,20,5),
          nn.ReLU(),
          nn.Conv2d(20,64,5),
          nn.ReLU()
        )
"""
이 경우 model(x)는 nn.ReLU(nn.Conv2d(20,64,5)(nn.ReLU(nn.Conv2d(1,20,5)(x))))와 같음.
"""

# Example of using Sequential with OrderedDict
model = nn.Sequential(OrderedDict([
          ('conv1', nn.Conv2d(1,20,5)),
          ('relu1', nn.ReLU()),
          ('conv2', nn.Conv2d(20,64,5)),
          ('relu2', nn.ReLU())
        ]))

조금 다르지만 비슷한 역할을 할 수 있는 것으로는 nn.ModuleList, nn.ModuleDict가 있다.


모델 구성 방법

크게 6가지 정도의 방법이 있다. nn 라이브러리를 잘 써서 직접 만들거나, 함수 또는 클래스로 정의, cfg파일 정의 또는 torchvision.models에 미리 정의된 모델을 쓰는 방법이 있다.

단순한 방법

model = nn.Linear(in_features=1, out_features=1, bias=True)

이전 글에서 썼던 방식이다. 매우 단순한 모델을 만들 때는 굳이 nn.Module을 상속하는 클래스를 만들 필요 없이 바로 사용 가능하며, 단순하다는 장점이 있다.

nn.Sequential을 사용하는 방법

sequential_model = nn.Sequential(
    nn.Linear(in_features=1, out_features=20, bias=True),
    nn.ReLU(),
    nn.Linear(in_features=20, out_features=1, bias=True),
)

여러 LayerActivation function들을 조합하여 하나의 sequential model을 만들 수 있다. 역시 상대적으로 복잡하지 않은 모델 중 모델의 구조가 sequential한 모델에만 사용할 수 있다.

함수로 정의하는 방법

def TwoLayerNet(in_features=1, hidden_features=20, out_features=1):
    hidden = nn.Linear(in_features=in_features, out_features=hidden_features, bias=True)
    activation = nn.ReLU()
    output = nn.Linear(in_features=hidden_features, out_features=out_features, bias=True)
    
    net = nn.Sequential(hidden, activation, output)
    
    return net

model = TwoLayerNet(1, 20, 1)

바로 위의 모델과 완전히 동일한 모델이다. 함수로 선언할 경우 변수에 저장해 놓은 layer들을 재사용하거나, skip-connection을 구현할 수도 있다. 하지만 그 정도로 복잡한 모델은 아래 방법을 쓰는 것이 낫다.

nn.Module을 상속한 클래스를 정의하는 방법

가장 정석이 되는 방법이다. 또한, 복잡한 모델을 구현하는 데 적합하다.

from torch import nn
import torch.nn.functional as F

class TwoLinearLayerNet(nn.Module):
    
    def __init__(self, in_features, hidden_features, out_features):
        super(TwoLinearLayerNet, self).__init__()
        self.linear1 = nn.Linear(in_features=in_features, out_features=hidden_features, bias=True)
        self.linear2 = nn.Linear(in_features=hidden_features, out_features=out_features, bias=True)
        
    def forward(self, x):
        x = F.relu(self.linear1(x))
        return self.linear2(x)

model = TwoLinearLayerNet(1, 20, 1)

역시 동일한 모델을 구현하였다. 여러분의 코딩 스타일에 따라, ReLU 등의 Activation function을 forward()에서 바로 정의해서 쓰거나, __init__()에 정의한 후 forward에서 갖다 쓰는 방법을 선택할 수 있다. 후자의 방법은 아래와 같다.
물론 변수명은 전적으로 여러분의 선택이지만, activation1, relu1 등의 이름을 보통 쓰는 것 같다.

from torch import nn

class TwoLinearLayerNet(nn.Module):
    
    def __init__(self, in_features, hidden_features, out_features):
        super(TwoLinearLayerNet, self).__init__()
        self.linear1 = nn.Linear(in_features=in_features, out_features=hidden_features, bias=True)
        self.activation1 = nn.ReLU()
        self.linear2 = nn.Linear(in_features=hidden_features, out_features=out_features, bias=True)
        
    def forward(self, x):
        x = self.activation1(self.linear1(x))
        return self.linear2(x)

model = TwoLinearLayerNet(1, 20, 1)

두 코딩 스타일의 차이점 중 하나는 import하는 것이 다르다(F.relu와 nn.ReLU는 사실 거의 같다). Activation function 부분에서 torch.nn.functionaltorch.nn의 Module에 거의 포함되는데, forward()에서 정의해서 쓰느냐 마느냐에 따라 다르게 선택하면 되는 정도이다.

cfg(config)를 정의한 후 모델을 생성하는 방법

처음 보면 알아보기 까다로운 방법이지만, 매우 복잡한 모델의 경우 .cfg 파일을 따로 만들어 모델의 구조를 정의하는 방법이 존재한다. 많이 쓰이는 방법은 대략 두 가지 정도인 것 같다.

먼저 PyTorch documentation에서 찾을 수 있는 방법이 있다. 예로는 VGG를 가져왔다. 코드는 여기에서 찾을 수 있다.

class VGG(nn.Module):

    def __init__(self, features, num_classes=1000, init_weights=True):
        super(VGG, self).__init__()
        self.features = features
        self.classifier = nn.Sequential(...)
        if init_weights:
            self._initialize_weights()

    def forward(self, x):...

    def _initialize_weights(self):...

def make_layers(cfg, batch_norm=False):
    layers = []
    in_channels = 3
    for v in cfg:
        if v == 'M':
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        else:
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            if batch_norm:
                layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
            else:
                layers += [conv2d, nn.ReLU(inplace=True)]
            in_channels = v
    return nn.Sequential(*layers)

cfg = {
    'A': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'B': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'D': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    'E': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}

def vgg16(pretrained=False, **kwargs):
    """VGG 16-layer model (configuration "D")"""
    if pretrained:
        kwargs['init_weights'] = False
    model = VGG(make_layers(cfg['D']), **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['vgg16']))
    return model

여기서는 .cfg 파일이 사용되지는 않았으나, cfg라는 변수가 configuration을 담당하고 있다. VGG16 모델을 구성하기 위해 cfg 변수의 해당하는 부분을 읽어 make_layer 함수를 통해 모델을 구성한다.

더 복잡한 모델은 아예 따로 .cfg 파일을 빼놓는다. YOLO의 경우 수백 라인이 넘기도 한다.

.cfg 파일은 대략 다음과 같이 생겼다.

[net]
# Testing
batch=1
subdivisions=1
# Training
# batch=64
# subdivisions=8
...

[convolutional]
batch_normalize=1
filters=32
size=3
stride=1
pad=1
activation=leaky

[maxpool]
size=2
stride=2
...

이를 파싱하는 코드도 있어야 한다.

def parse_cfg(cfgfile):
    blocks = []
    fp = open(cfgfile, 'r')
    block =  None
    line = fp.readline()
    while line != '':
        line = line.rstrip()
        if line == '' or line[0] == '#':
            line = fp.readline()
            continue        
        elif line[0] == '[':
            if block:
                blocks.append(block)
            block = dict()
            block['type'] = line.lstrip('[').rstrip(']')
            # set default value
            if block['type'] == 'convolutional':
                block['batch_normalize'] = 0
        else:
            key,value = line.split('=')
            key = key.strip()
            if key == 'type':
                key = '_type'
            value = value.strip()
            block[key] = value
        line = fp.readline()

    if block:
        blocks.append(block)
    fp.close()
    return blocks

이 방법의 경우 대개 depth가 수십~수백에 이르는 아주 거대한 모델을 구성할 때 사용되는 방법이다. 많은 수의 github 코드들이 이런 방식을 사용하고 있는데, 그러면 그 모델은 굉장히 복잡하게 생겼다는 뜻이 된다.

torchvision.models의 모델을 사용하는 방법

torchvision.models에서는 미리 정의되어 있는 모델들을 사용할 수 있다. 이 모델들은 그 구조뿐 아니라 pretrained=True 인자를 넘김으로써 pretrained weights를 가져올 수도 있다.

2019.02.12 시점에서 사용 가능한 모델 종류는 다음과 같다.

  • AlexNet
  • VGG-11, VGG-13, VGG-16, VGG-19
  • VGG-11, VGG-13, VGG-16, VGG-19 (with batch normalization)
  • ResNet-18, ResNet-34, ResNet-50, ResNet-101, ResNet-152
  • SqueezeNet 1.0, SqueezeNet 1.1
  • Densenet-121, Densenet-169, Densenet-201, Densenet-161
  • Inception v3

모델에 따라 train mode와 eval mode가 정해진 경우가 있으므로 이는 주의해서 사용하도록 한다.

모든 pretrained model을 쓸 때 이미지 데이터는 [3, W, H] 형식이어야 하고, W, H는 224 이상이어야 한다. 또 아래 코드처럼 정규화된 이미지 데이터로 학습된 것이기 때문에, 이 모델들을 사용할 때에는 데이터셋을 이와 같이 정규화시켜주어야 한다.

transforms.Normalize(mean=[0.485, 0.456, 0.406],
                     std=[0.229, 0.224, 0.225])

사용법은 대략 다음과 같다. 사실 이게 거의 끝이고, 나머지는 다른 일반 모델처럼 사용하면 된다.

import torchvision.models as models

# model load
alexnet = models.alexnet()
vgg16 = models.vgg16()
vgg16_bn = models.vgg16_bn()
resnet18 = models.resnet18()
squeezenet = models.squeezenet1_0()
densenet = models.densenet161()
inception = models.inception_v3()

# pretrained model load
resnet18 = models.resnet18(pretrained=True)
vgg16 = models.vgg16(pretrained=True)
...

Set Loss function(creterion) and Optimizer

Pytorch Loss function의 종류

참조: Loss functions

Loss function은 모델이 추측한 결과(prediction 또는 output)과 실제 정답(label 또는 y 등)의 loss를 계산한다. 이는 loss function을 어떤 것을 쓰느냐에 따라 달라진다. 예를 들어 regression model에서 MSE(Mean Squared Error)를 쓸 경우 평균 제곱오차를 계산한다.

사용법은 다른 함수들도 아래와 똑같다.

import torch
from torch import nn
criterion  = nn.MSELoss()
prediction = torch.Tensor([12, 21, 30, 41, 52]) # 예측값
target     = torch.Tensor([10, 20, 30, 40, 50]) # 정답
loss       = criterion(prediction, target)
print(loss)
# tensor(2.)
# loss = (2^2 + 1^2 + 0^2 + 1^2 + 2^2) / 5 = 2

criterion_reduction_none = nn.MSELoss(reduction='none')
loss = criterion_reduction_none(prediction, target)
print(loss)
# tensor([4., 1., 0., 1., 4.])

여러 코드들을 살펴보면, loss function을 정의할 때는 보통 creterion, loss_fn, loss_function등의 이름을 사용하니 참고하자.

홈페이지를 참조하면 각 함수별 설명에 ‘Creates a criterion that measures…‘라 설명이 되어 있다. 위의 예시를 보면 알겠지만 해당 함수들이 당장 loss를 계산하는 것이 아니라 loss를 계산하는 기준을 정의한다는 뜻이다.
또 많은 함수들은 reducesize_average argument를 갖는다. loss를 계산하여 평균을 내는 것이 아니라 각 원소별로 따로 계산할 수 있게 해 준다. 그러나 2019.02.16 기준으로 다음과 비슷한 경고가 뜬다.

reduce args will be deprecated, please use reduction=’none’ instead.

따라서 reduction argument를 쓰도록 하자. 지정할 수 있는 종류는 ‘none’ | ‘mean’ | ‘sum’ 세 가지이다. 기본값은 mean으로 되어 있다.

  • nn.L1Loss: 각 원소별 차이의 절댓값을 계산한다. L1
  • nn.MSELoss: Mean Squared Error(평균제곱오차) 또는 squared L2 norm을 계산한다. MSE
  • nn.CrossEntropyLoss: Cross Entropy Loss를 계산한다. nn.LogSoftmax() and nn.NLLLoss()를 포함한다. weight argument를 지정할 수 있다. CE
  • nn.CTCLoss: Connectionist Temporal Classification loss를 계산한다.
  • nn.NLLLoss: Negative log likelihood loss를 계산한다. NLL
  • nn.PoissonNLLLoss: target이 poission 분포를 가진 경우 Negative log likelihood loss를 계산한다. PNLL
  • nn.KLDivLoss: Kullback-Leibler divergence Loss를 계산한다. KLDiv
  • nn.BCELoss: Binary Cross Entropy를 계산한다. BCE
  • nn.BCEWithLogitsLoss: Sigmoid 레이어와 BCELoss를 하나로 합친 것인데, 홈페이지의 설명에 따르면 두 개를 따로 쓰는 것보다 이 함수를 쓰는 것이 조금 더 수치 안정성을 가진다고 한다. BCE
  • 이외에 MarginRankingLoss, HingeEmbeddingLoss, MultiLabelMarginLoss, SmoothL1Loss, SoftMarginLoss, MultiLabelSoftMarginLoss, CosineEmbeddingLoss, MultiMarginLoss, TripletMarginLoss를 계산하는 함수들이 있다. 필요하면 찾아보자.

Pytorch Optimizer의 종류

참조: torch.optim

여기에도 간략하게 언급했었지만, GPU CUDA를 사용할 계획이라면 optimizer를 정의하기 전에 미리 해놓아야 한다(model.cuda()). 공식 홈페이지에 따르면,

If you need to move a model to GPU via .cuda(), please do so before constructing optimizers for it. Parameters of a model after .cuda() will be different objects with those before the call. In general, you should make sure that optimized parameters live in consistent locations when optimizers are constructed and used.

이유를 설명하자면

  1. optimizer는 argument로 model의 parameter를 입력받는다.
  2. .cuda()를 쓰면 모델의 parameter가 cpu 대신 gpu에 올라가는 것이므로 다른 object가 된다.
  3. 따라서 optimizer에 model parameter의 위치를 전달한 후 .cuda()를 실행하면, 학습시켜야 할 parameter는 GPU에 올라가 있는데 optimizer는 cpu에 올라간 엉뚱한 parameter 위치를 참조하고 있는 것이 된다.

그러니 순서를 지키자.

optimizer 정의는 다음과 같이 할 수 있다.

optimizer = optim.SGD(model.parameters(), lr = 0.01, momentum=0.9)
optimizer = optim.Adam([var1, var2], lr = 0.0001)

optimizer에 대해 알아 두어야 할 것이 조금 있다.

  1. optimizer는 step() method를 통해 argument로 전달받은 parameter를 업데이트한다.
  2. 모델의 parameter별로(per-parameter) 다른 기준(learning rate 등)을 적용시킬 수 있다. 참고
  3. torch.optim.Optimizer(params, defaults)는 모든 optimizer의 base class이다.
  4. nn.Module과 같이 state_dict()load_state_dict()를 지원하여 optimizer의 상태를 저장하고 불러올 수 있다.
  5. zero_grad() method는 optimizer에 연결된 parameter들의 gradient를 0으로 만든다.
  6. torch.optim.lr_scheduler는 epoch에 따라 learning rate를 조절할 수 있다.

Optimizer의 종류:

  • optim.Adadelta, optim.Adagrad, optim.Adam, optim.SparseAdam, optim.Adamax
  • optim.ASGD, optim.LBFGS
  • optim.RMSprop, optim.Rprop
  • optim.SGD

LBFGS는 per-parameter 옵션이 지원되지 않는다. 또한 memory를 다른 optimizer에 비해 많이 잡아먹는다고 한다.

Pytorch LR(Learning Rate) Scheduler의 종류

LR(Learning Rate) Scheduler는 미리 지정한 횟수의 epoch이 지날 때마다 lr을 감소(decay)시켜준다.
이는 학습 초기에는 빠르게 학습을 진행시키다가 minimum 근처에 다다른 것 같으면 lr을 줄여서 더 최적점을 잘 찾아갈 수 있게 해주는 것이다.

종류는 여러 개가 있는데, 마음에 드는 것을 선택하면 된다. 아래쪽에 어떻게 lr이 변화하는지 그림을 그려 놓았다.

lr Scheduler의 종류:

  • optim.lr_scheduler.LambdaLR: lambda 함수를 하나 받아 그 함수의 결과를 lr로 설정한다.
  • optim.lr_scheduler.StepLR: 특정 step마다 lr을 gamma 비율만큼 감소시킨다.
  • optim.lr_scheduler.MultiStepLR: StepLR과 비슷한데 매 step마다가 아닌 지정된 epoch에만 gamma 비율로 감소시킨다.
  • optim.lr_scheduler.ExponentialLR: lr을 지수함수적으로 감소시킨다.
  • optim.lr_scheduler.CosineAnnealingLR: lr을 cosine 함수의 형태처럼 변화시킨다. lr이 커졌다가 작아졌다가 한다.
  • optim.lr_scheduler.ReduceLROnPlateau: 이 scheduler는 다른 것들과는 달리 학습이 잘 되고 있는지 아닌지에 따라 동적으로 lr을 변화시킬 수 있다. 보통 validation set의 loss를 인자로 주어서 사전에 지정한 epoch동안 loss가 줄어들지 않으면 lr을 감소시키는 방식이다.

각 scheduler는 공통적으로 last_epoch argument를 갖는다. Default value로 -1을 가지며, 이는 초기 lr을 optimizer에서 지정된 lr로 설정할 수 있도록 한다.

10_Scheduler

코드는 아래와 같이 작성하였다.

from torch import optim
from torch import nn

import re
import random
from matplotlib import pyplot as plt


class Model(nn.Module):

    def __init__(self):
        super(Model, self).__init__()
        self.linear1 = nn.Linear(5, 3)

    def forward(self, x):
        return self.linear1(x)


model = Model()

optimizer = optim.Adam(model.parameters(), lr=1.0)


scheduler_list = [
    optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer,
                                         mode='min',
                                         factor=0.5,
                                         patience=3, ), # 이외에도 인자가 많다. 찾아보자.
    optim.lr_scheduler.LambdaLR(optimizer=optimizer,
                                lr_lambda=lambda epoch: 1 / (epoch+1)),
    optim.lr_scheduler.StepLR(optimizer=optimizer,
                              step_size=5,
                              gamma=0.5),
    optim.lr_scheduler.MultiStepLR(optimizer=optimizer,
                                   milestones=[2, 5, 10, 11, 28],
                                   gamma=0.5),
    optim.lr_scheduler.ExponentialLR(optimizer=optimizer,
                                     gamma=0.5),
    optim.lr_scheduler.CosineAnnealingLR(optimizer=optimizer,
                                         T_max=10,
                                         eta_min=0),
]

reObj = re.compile(r'<torch\.optim\.lr_scheduler\.(.+) object.*>')


for i, scheduler in enumerate(scheduler_list):
    scheduler_name = reObj.match(str(scheduler)).group(1)
    print(scheduler_name)

    lr_list = []

    for epoch in range(1, 30+1):
        if str(scheduler_name) == 'ReduceLROnPlateau':
            scheduler.step(random.randint(1, 50))
        else:
            scheduler.step()

        lr = optimizer.param_groups[0]['lr']
        # print('epoch: {:3d}, lr={:.6f}'.format(epoch, lr))
        lr_list.append(lr)

    plt.subplot(3, 2, i + 1)

    plt.title(scheduler_name)
    plt.ylim(0, 1.1)
    plt.plot(lr_list)

plt.show()
# plt.savefig('scheduler')

조금 더 자세한 설명은 홈페이지를 참조하자.

from torch import optim
from torch import nn


class Model(nn.Module):

    def __init__(self):
        super(Model, self).__init__()
        self.linear1 = nn.Linear(5, 3)

    def forward(self, x):
        return self.linear1(x)


model = Model()
optimizer = optim.Adam(model.parameters(), lr=1.0) # 1.0은 보통 너무 크다. 하지만 예시이므로 1을 주었다.

# Learning Rate가 scheduler에 따라 어떻게 변하는지 보려면 이곳을 바꾸면 된다.
scheduler = optim.lr_scheduler.LambdaLR(optimizer=optimizer,
                                        lr_lambda=lambda epoch: 0.95 ** epoch)

for epoch in range(1, 100+1):
    for param_group in optimizer.param_groups:
        lr = param_group['lr']
    print('epoch: {:3d}, lr={:.6f}'.format(epoch, lr))
    scheduler.step()

결과:

epoch:   1, lr=1.000000
epoch:   2, lr=1.000000
epoch:   3, lr=0.950000
epoch:   4, lr=0.902500
epoch:   5, lr=0.857375
epoch:   6, lr=0.814506
epoch:   7, lr=0.773781
epoch:   8, lr=0.735092
epoch:   9, lr=0.698337
epoch:  10, lr=0.663420
...

Train Model

일반적인 machine learning의 학습 방법은 다음과 같다. 입력은 input, 모델의 출력은 output, 정답은 target이라고 하자.

  1. model structure, loss function, optimizer 등을 정한다.
  2. forward-propagation: input을 모델에 통과시켜 output을 계산한다.
  3. loss function으로 output과 target 간 loss를 계산한다.
  4. back-propagation: loss와 chain rule을 활용하여 모델의 각 레이어에서 gradient($\Delta w$)를 계산한다.
  5. update: $ w \leftarrow w - \alpha\Delta w $식에 의해 모델의 parameter를 update한다.

Pytorch의 학습 방법은 다음과 같다.

  1. model structure, loss function, optimizer 등을 정한다.
  2. optimizer.zero_grad(): 이전 epoch에서 계산되어 있는 parameter의 gradient를 0으로 초기화한다.
  3. output = model(input): input을 모델에 통과시켜 output을 계산한다.
  4. loss = loss_fn(output, target): output과 target 간 loss를 계산한다.
  5. loss.backward(): loss와 chain rule을 활용하여 모델의 각 레이어에서 gradient($\Delta w$)를 계산한다.
  6. optimizer.step(): $w \leftarrow w - \alpha\Delta w$식에 의해 모델의 parameter를 update한다.

거의 일대일 대응되지만 다른 점이 하나 있다.

  • optimizer.zero_grad(): Pytorch는 gradient를 loss.backward()를 통해 계산하지만, 이 함수는 이전 gradient를 덮어쓴 뒤 새로 계산하는 것이 아니라, 이전 gradient에 누적하여 계산한다.
    • 귀찮은데? 라고 생각할 수는 있다. 그러나 이러한 누적 계산 방식은 RNN 모델을 구현할 때는 오히려 훨씬 편하게 코드를 작성할 수 있도록 도와준다.
    • 그러니 gradient가 누적될 필요 없는 모델에서는 model에 input를 통과시키기 전 optimizer.zero_grad()를 한번 호출해 주기만 하면 된다고 생각하면 끝이다.

Pytorch가 대체 어떻게 loss.backward() 단 한번에 gradient를 자동 계산하는지에 대한 설명도 하면,

  • 모든 Pytorch Tensor는 requires_grad argument를 가진다. 일반적으로 생성하는 Tensor는 기본적으로 해당 argument 값이 False이며, 따로 True로 설정해 주면 gradient를 계산해 주어야 한다. nn.Linear 등의 module은 생성할 때 기본적으로 requires_grad=True이기 때문에, 일반적으로 모델의 parameter는 gradient를 계산하게 된다.
    • 참고(3번 항목): Pytorch 0.4.0 버전 이전에는 Variable class가 해당 역할을 수행하였지만, deprecated되었다.
  • 마지막 레이어만 원하는 것으로 바꿔서 그 레이어만 학습을 수행하는 형태의 transfer learning을 requires_grad를 이용해 손쉽게 구현할 수 있다. 이외에도 특정 레이어만 gradient를 계산하지 않게 하는 데에도 쓸 수 있다. 아래 예시는 512개의 class 대신 100개의 class를 구별하고자 할 때 resnet18을 기반으로 transfer learning을 수행하는 방식이다.
model = torchvision.models.resnet18(pretrained=True)
for param in model.parameters():
    param.requires_grad = False
# Replace the last fully-connected layer
# Parameters of newly constructed modules have requires_grad=True by default
model.fc = nn.Linear(512, 100)

# Optimize only the classifier
optimizer = optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9)
  • requires_grad=True인 Tensor로부터 연산을 통해 생성된 Tensor도 requires_grad=True이다.
  • with torch.no_grad(): 범위 안에서는 gradient 계산을 하지 않는다.
  • with torch.no_grad(): 안에서 선언된 with torch.enable_grad(): 범위 안에서는 다시 gradient 계산을 한다. 이 두 가지 기능을 통해 국지적으로 gradient 계산을 수행하거나 수행하지 않을 수 있다.
import torch

x = torch.Tensor(1)
print('x.requires_grad:', x.requires_grad)

y = torch.ones(1, requires_grad=True)
print('y.requires_grad:', y.requires_grad)

z = 172 * y + 3
print('z.requires_grad:', z.requires_grad)

with torch.no_grad():
    print('z.requires_grad:', (z ** 2).requires_grad)
    with torch.enable_grad():
        print('z.requires_grad:', (z ** 2).requires_grad)

print('z.grad_fn:', z.grad_fn)

print('x:', x, '\ny:', y, '\nz:', z)

z.backward()
print('y.grad:', y.grad)
print('z.grad:', z.grad)
x.requires_grad: False
y.requires_grad: True
z.requires_grad: True
z.requires_grad: False
z.requires_grad: True
z.grad_fn: <AddBackward0 object at 0x0000028634614780>
x: tensor([1.4013e-45]) 
y: tensor([1.], requires_grad=True) 
z: tensor([175.], grad_fn=<AddBackward0>)
y.grad: tensor([172.])
z.grad: None

튜토리얼이 조금 더 궁금하다면 여기를 참고해도 좋다.

학습할 때 알아두면 괜찮은 것들을 대략 정리해보았다. 어떤 식으로 학습하는 것이 좋은지(learning rate 선택 기준 등)는 양이 너무 방대하기에 여기에는 적지 않는다.

CUDA: use GPU

CUDA

  • torch.cuda.is_available(): 학습을 시킬 때는 GPU를 많이 사용한다. GPU가 사용가능한지 알 수 있다.
  • torch.cuda.device(device): 어느 device(GPU나 CPU)를 쓸 지 선택한다.
  • torch.cuda.device_count(): 현재 선택된 device의 수를 반환한다.
  • torch.cuda.init(): C API를 쓰는 경우 명시적으로 호출해야 한다.
  • torch.cuda.set_device(device): 현재 device를 설정한다.
  • torch.cuda.manual_seed(seed): 랜덤 숫자를 생성할 시드를 정한다. multi-gpu 환경에서는 manual_seed_all 함수를 사용한다.
  • torch.cuda.empty_cache(): 사용되지 않는 cache를 release하나, 가용 메모리를 늘려 주지는 않는다.

간단한 학습 과정은 다음 구조를 따른다.

# 변수명으로 input을 사용하는 것은 비추천. python 내장 함수 이름이다.
for data, target in dataloader: 
    optimizer.zero_grad() # RNN에서는 생략될 수 있음
    output = model(data)
    loss = loss_fn(output, target)
    loss.backward()
    optimizer.step()

Visualize and save results

Visualization Library

Visualization은 이 글에서 설명하지 않겠다. 기본적으로 python의 그래프 패키지인 matplotlib을 많이 쓰며, graphviz, seaborn 등의 다른 라이브러리도 잘 보이는 편이다.

Save & Load Model

모델을 저장하는 방법은 여러 가지가 있지만, pytorch를 사용할 때는 다음 방법이 가장 권장된다. 아주 유연하고 또 간단하기 때문이다.

Save:

torch.save(model.state_dict(), PATH)

Load:

model = TheModelClass(*args, **kwargs)
model.load_state_dict(torch.load(PATH))
# model.eval() # 테스트 시

# 참고로 model.load_state_dict(PATH)와 같이 쓸 수는 없다.

epoch별로 checkpoint를 쓰면서 저장할 때는 다음과 같이 혹은 비슷하게 쓰면 좋다. checkpoint를 쓸 때는 단순히 모델의 parameter뿐만 아니라 epoch, loss, optimizer 등을 저장할 필요가 있다.

Save:

torch.save({
            'epoch': epoch,
            'model_state_dict': model.state_dict(),
            'optimizer_state_dict': optimizer.state_dict(),
            'loss': loss,
            ...
            }, PATH)

Load:

model = TheModelClass(*args, **kwargs)
optimizer = TheOptimizerClass(*args, **kwargs)

checkpoint = torch.load(PATH)
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
epoch = checkpoint['epoch']
loss = checkpoint['loss']
# model.train() or model.eval()

일반적으로 저장한 모델 파일명은 .pt.pth 확장자를 쓴다. 모델을 포함하여 여러 가지를 같이 저장할 때는 .tar 확장자를 자주 쓰는 편이다.

모델을 불러오고 나서 계속 학습시킬 것이라면 model.train(), 테스트를 할 것이라면 model.eval()으로 모드를 설정하도록 한다. 이유는 이 글에 설명이 있다.

모델이 여러 개라면

torch.save({
            'modelA_state_dict': modelA.state_dict(),
            'modelB_state_dict': modelB.state_dict(),
            ...

처럼 쓰면 그만이다.

구조가 조금 다른 모델에다가 parameter를 load하고 싶을 경우 load할 때 다음처럼 쓴다.

model.load_state_dict(torch.load(PATH), strict=False)

load_state_dict 함수는 기본적으로 strict=True 옵션을 갖고 있으며, 이는 불러올 모델과 저장된 모델의 레이어의 개수와 이름 등이 같아야만 오류 없이 불러온다.

따라서 transfer learning이나, 복잡한 모델을 새로 학습시키고 싶을 때 모델의 일부라도 parameter를 불러오고 싶다면 strict=False argument를 설정하면 된다.
이는 레이어들이 정확히 일치하지 않아도 매칭이 되는 레이어가 일부라도 있다면 그 레이어들에 한해서 parameter를 load한다.
또 parameter 개수는 같지만 이름은 다른 레이어에 parameter를 불러오고 싶을 때는, state_dict는 딕셔너리이기 때문에 그냥 해당 딕셔너리의 이름만 바꿔서 load하면 그만이다.

pickle 또는 torch.save를 통해 model 전체를 통째로 저장하는 방법은 간편하기는 하지만 이후 불러올 때는 해당 모델과 완전히 똑같이 생긴 모델에만 사용 가능하기 때문에 확장성과 재사용성이 떨어진다.
layer 이름과 parameter를 mapping하여 저장하는 state_dict를 쓰는 것이 transfer learinng을 쉽게 할 수 있는 등 범용성이 더 좋다.

device를 바꿔서 저장하고 싶다면, load_state_dict에서 map_location argument를 설정하거나, model.to(device) 함수를 사용하면 된다. 자세한 것은 홈페이지를 참조한다. GPU를 사용할 때 바꿔줘야 하는 부분은 여기의 cuda 부분을 참고한다.

torch.save & torch.load

내부적으로 pickle을 사용하며, 따라서 모델뿐 아니라 일반 tensor, 기타 다른 모든 python 객체를 저장할 수 있다.

nn.Module.state_dict & nn.Module.load_state_dict

우선 state_dict는 간단히 말해 모델의 상태를 딕셔너리 형태로 표현하는 것이다. 그러면 모델의 상태는 어떻게 정의되는가?
state_dict로 저장되는 모델의 상태는 learnable parameters이며, state_dict{레이어 이름: parameter tensor}의 형태를 갖는 딕셔너리이다.
딱 그뿐이다. 간단하지 않은가?

Optimizer도 state_dict를 갖고 있는데, 이 경우는 사용된 hyperparameter 등의 상태가 저장된다.

공식 홈페이지의 예시를 일부 가져오면 다음과 같다.

# 모델이 이렇게 생겼으면, 
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)

# 이 코드에 의해
for param_tensor in model.state_dict():
    print(param_tensor, "\t", model.state_dict()[param_tensor].size())

# 이렇게 출력된다.
conv1.weight     torch.Size([6, 3, 5, 5])
conv1.bias       torch.Size([6])
conv2.weight     torch.Size([16, 6, 5, 5])
conv2.bias       torch.Size([16])
fc1.weight       torch.Size([120, 400])
fc1.bias         torch.Size([120])

# optimizer는
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
for var_name in optimizer.state_dict():
    print(var_name, "\t", optimizer.state_dict()[var_name])

# 이렇다.
state    {}
param_groups     [{'lr': 0.001, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 
'nesterov': False, 'params': [4675713712, 4675713784, ..., 4675714720]}]

Q & A

  • model.train()model.eval()은 모델이 학습 모드인지, 테스트 모드인지를 정하는 것이다. 이는 dropout이나 batchnorm이 있는 모델의 경우 학습할 때와 테스트할 때 모델이 달라지기 때문에 세팅하는 것이다(또한 필수이다). torch.no_grad()는 (대개 일시적으로) 해당 범위 안에서 gradient 계산을 중지시킴으로써 메모리 사용량을 줄이고 계산 속도를 빨리 하는 것이다. 참고
  • optimizer.zero_grad()를 사용하는 이유. 참고
  • Pytorch 코드들 중에는 torch.autograd.Variable을 사용한 경우가 많다. Pytorch 0.4.0 버전 이후로는 Tensor 클래스에 통합되어 더 이상 쓸 필요가 없다. 참고(3번 항목)
  • 역시 Pytorch 코드들 중에는 loss를 tensor가 아닌 그 값을 가져올 때 loss.data[0] 등의 표현식은 에러를 뱉는 경우가 많다. 이는 0.4 이후 버전의 PyTorch에서는 loss.item()으로 그 값을 가져오도록 변경되었기 때문이다.
    • Pytorch의 loss는 이전에는 Variable에 할당된 size=(1, )의 tensor였지만 이제는 scalar 형태이다.

댓글로 문의하시면 확인 후 포스팅에 추가 가능합니다.

Comment  Read more

Bootstrap, Bagging, Boosting

|

Bootstrap: 부트스트랩의 개념

통계학에서의 부트스트랩과 기계학습에서의 부트스트랩은 그 의미가 다른 점도 있지만 본질적으로는 같다고 할 수 있다. 통계학적으로는 정확한 분포를 모르는 데이터의 통계치의 분포를 알아내기 위하여 Random Sampling을 하는 경우를 말하며, 종종 측정된 샘플이 부족한 경우에도 사용된다.
기계학습에서는 기본적으로 Random Sampling을 통해 데이터의 수를 늘리는 것을 말한다.


Decision Tree: 의사 결정 나무

내용을 입력합시당


Bagging: 배깅

Bagging은 Bootstrap Aggregatint의 줄임말이다. 특별히 부트스트랩이 over-fitting을 줄이는 데에 사용될 때를 말한다. 주어진 데이터에 대해 여러 번의 Random Sampling을 통해 Training Data를 추출하고 (여러 개의 부트스트랩을 생성), 독립된 모델로서 각각의 자료를 학습시키고 이를 앙상블로서 결합하여 최종적으로 하나의 예측 모형을 산출하는 방법이라고 할 수 있다.

예를 들어 단일 Decision Tree는 변동성이 매우 크다. 이러한 단일 Decision Tree를 여러 개 결합하여 모델을 형성한다면 과적합을 방지할 수도 있고 안정된 결과를 산출할 수 있을 것이다.

대표적인 예가 Random Forest이며, Sample의 예측변수들의 결합 시 Target Variable이 연속형일 때는 평균을, 범주형일 때는 다중 투표를 사용한다.


Boosting: 부스팅

배깅이 독립적으로 모델을 학습시킨다면, 부스팅은 이전의 잘못을 파악하고 이를 이용하여 다음 번에는 더 나은 모델을 만들어 내자는 목표를 추구하면서 학습하는 방법이다. 분류 문제로 예를 들면, 잘못 분류된 개체들을 다음 번에는 더 잘 분류하고 싶은 것이 당연하다. 부스팅은 잘못 본류된 개체들에 집중하여 새로운 분류 규칙을 만드는 것을 반복하는 방법이며, 이는 결국 약한 예측모형들을 결합하여 강한 예측모형을 만드는 과정으로 서술할 수 있다.


XGBoost 이론

XGBoost는 Extreme Gradient Boosting의 줄임말로, 2014년에 등장하여 이후 지금까지 널리 쓰이고 있는 강력한 기계학습 알고리즘이다.
본 글에서는 XGBoost의 창시자인 Tianqi Chen과 Carlos Guestrin이 2016년 publish한
[XGBoost: A Scalable Tree Boosting System] 논문과 Chen의 관련 강연을 기초로 하여
알고리즘에 대해 설명하도록 하겠다.

알고리즘에 대한 설명이 끝난 이후에는 XGBoost Python의 메서드와 패키지의 주요 기능에 대해 알아본 뒤, Hyperparameter들을 튜닝하는 법에 대해 설명할 것이다.

XGBoost의 강점

  1. Regularization: 복잡한 모델에 대하여 페널티를 주는 Regularization 항이 있기 때문에 과적합을 방지할 수 있다.
  2. Handling Sparse Data: XGB는 원핫인코딩이나 결측값 등에 의해 발생한 Sparse Data(0이 많은 데이터) 또한 무리 없이 다룰 수 있다.
  3. Weighted Quantile Sketch: 가중치가 부여된 데이터 또한 Weighted Percentile Sketch 알고리즘을 통해 다룰 수 있다.
  4. Block Structure for parallel learning: 데이터는 정렬되어 in-memory units (blocks)에 저장된다. 이 데이터는 이후에 계속 반복적으로 재사용이 가능하기 때문에 다시 계산할 필요가 없다. 이를 통해 빠르게 Split Point를 찾아낼 수 있고 Column Sub-sampling을 진행할 수 있다.
  5. Cache Awarness: 하드웨어를 최적으로 사용하도록 고안되었다.
  6. Out-of-core computing: 거대한 데이터를 다룰 때 디스크 공간을 최적화하고 사용 가능 범위를 최대화한다.

[1] Regularized Learning Objective
n개의 example과 m개의 feature(변수)로 이루어진 데이터셋이 있다고 할 때,

[D = {(x_i, y_i)} ( D = n, x_i \in \mathbb{R^m}, y_i \in \mathbb{R})]

앙상블 모델은 output을 예측하기 위해 K개의 additive functions(가법 함수)를 이용한다. 즉, f(x)는 q(x)라는 Tree 구조의 weight을 의미하는데,

$ \vec{x_i} $라는 i번째 데이터가 Input으로 들어왔을 때, 각각의 Tree가 Decision Rule을 통해 산출한 score = output = $ f_k(x_i) $ 을 모두 더한 값을 아래의 식과 같이 최종 output = $ \hat{y_i} $ 으로 출력하게 된다.

[\hat{y_i} = \phi(\vec{x_i}) = \sum_{k=1}^K f_k(\vec{x_i}), f_k \in \mathbb{F}]

[\mathbb{F} = { f(\vec{x}) = w_{q(x)} } (q:\mathbb{R} \rightarrow T, w \in \mathbb{R}^T)]

여기서 K는 Tree의 개수를, T는 Tree 안에 있는 leaf의 개수를, w는 leaf weights를, $ w_i $는 i번째 leaf의 score를 의미한다.
q는 example을 leaf index에 매핑하는 Tree 구조를 말하는데 이 안에는 물론 Tree 내부의 수많은 Decision Rule을 포함한다.
F는 모든 Regression Trees를 포함하는 space of functions를 의미하며,
여기서 Classification and Regression Trees의 경우 CART라고도 한다.

이러한 함수들을 학습하기 위해서는 다음과 같은 Objective Function을 상정할 필요가 있다.
아래의 Regularized Objective는 예측 값과 실제 값 사이의 차이와 Regularized Term으로 구성된다.

[L(\phi) = \sum_{i}^{n} l(\hat{y_i}, y_i) + \sum_{k=1}^{K} \Omega(f_k)]

여기서 $ \Omega(f) = \gamma T + \frac{1}{2} \lambda \Vert{w}\Vert^2 $

물론 위의 $ l $은 미분 가능한 convex loss function이 될 것이며,
간단한 예로는 Square Loss나 Log Loss를 생각할 수 있을 것이다.

오른쪽 부분인 $ \Omega $의 역할은 모델이 너무 복잡해지는 것을 막는 페널티 항이다.
이 항은 과적합을 방지하기 위해 final learnt weights을 부드럽게 만들어줄 것이다. (Smoothing)

항을 자세히 보면, Tree 개수가 너무 많아지거나 leaf weights의 L2 norm이 너무 커지면 전체 Loss를 증가시키는 것을 알 수 있다.

[2] Gradient Tree Boosting
위에서 본 전체 Loss는 각각의 Tree 구조 자체( f(x) )를 포함하고 있기 때문에 최적화하기가 까다롭다. 따라서 아래의 방법으로 최적화 과정에 논의해볼 것이다.

일단 $ \hat{y_i}^{(t)} $를 t번 째 iteration(t번 째 Tree)에서의 i번 째 Instance(실제 개체)의 예측 값이라고 해보자,

이 값은 아래의 과정에 의해 표현될 수 있다.

\(\hat{y_i}^{(0)} = 0\)
\(\hat{y_i}^{(1)} = f_1(x_i) + \hat{y_i}^{(0)}\)
\(...\)
\(\hat{y_i}^{(t)} = \sum_{k=1}^{t} f_k(x_i)\)

따라서 전체 Loss를 아래와 같이 표현할 수 있다.

[L^{(t)} = \sum_{i=1}^{n} l({y_i}, \hat{y_i}^{(t-1)} + f_t(\vec{x_i})) + \Omega(f_t)]

이 단계에서 위의 $ l $ 부분을 2차항까지 사용한 테일러 전개에 의해 근사적으로 구하면, 다시 아래와 같이 표현할 수 있다.

예를 들어 $ l $을 Square Loss로 사용하였다면, 아래와 같은 전개가 가능할 것이다.

일반화된 식으로 다시 보면 상수항을 제거하고 남은 step t에서의 근사한 전체 Loss는 아래와 같다.

여기서 잠시 $ I_j = {i|q(\vec{x_i} = j)} $를
instance set of leaf j (leaf j의 할당 결과물)이라고 정의하겠다.

위의 식에서 정규화 항을 확장하여 정리해보면,

[\tilde{L}^{(t)} = \sum_{i=1}^{n} [g_i f_i(\vec{x_i}) + \frac{1}{2}h_i f_t^2(\vec{x_i})] + \gamma T + \frac{1}{2} \sum_{j=1}^{T} w_j^2]

[= \sum_{i=1}^{n} [g_i w_q(\vec{x_i}) + \frac{1}{2}h_i w_q^2(\vec{x_i})] + \gamma T + \frac{1}{2} \sum_{j=1}^{T} w_j^2]

example 단위에서 leaf 단위로 식을 재표현해주면,

[= \sum_{j=1}^{T} [ (\sum_{i \in I_j} g_i)w_j + \frac{1}{2} (\sum_{i \in I_j} h_i + \lambda) w_j^2 ] + \gamma T]

식을 보기 좋게 표현하기 위해 아래와 같은 정의를 사용하겠다.

[G_j = \sum_{i \in I_j} g_i, H_j = \sum_{i \in I_j} h_i]

고정된 $ q(\vec{x}) $에 대하여 위의 식 = 0으로 놓고 계산하면,
leaf j의 최적 weight을 계산할 수 있다.

[w_j^* = - \frac{G_j} {H_j + \lambda}]

이 때의 전체 Loss는 아래와 같다.

[\tilde{L}^{(t)}(q) = - \frac{1}{2} \sum_{j=1}^{T} \frac{G_{j}^2} {H_j + \lambda} + \gamma T]

정리하자면, 위의 식은 사실상 q라는 Tree 구조의 성능(quality)을 측정하는 Scoring Function의 역할을 수행하게 된다. 이 Score는 Decision Tree에서의 불순도와 같은 역할을 한다.

그런데 다만 여기서 생각해야 할 점은, 발생가능한 수많은 Tree의 구조를 일일히 다 평가할 수는 없다는 것이다. 이를 위해 Greedy 알고리즘이 사용되는데, 이 알고리즘은 단일 Leaf에서 시작하여 가지를 반복적으로 확장해 나가는 방법을 말한다.

$ I_L, I_R $을 각각 split 이후의 좌측, 우측 노드의 Instance Sets라고 할 때,
Gain 혹은 Loss reduction이라고 불리는 아래의 식은,

[Gain = L_{split} = \frac{1}{2} [ \frac{G_{L}^2} {H_L + \lambda} + \frac{G_{R}^2} {H_R + \lambda} - \frac{ (G_{L} + G_{R})^2 } {H_L + H_R + \lambda} ] - \gamma]

Left Child의 스코어 + Right Child의 스코어 - Split 안했을 때의 스코어 - Complexity cost by introducing additional leaf로 표현된다.

이는, split을 했을 때의 이득 (loss reduction)이 $ \gamma $로 표현되는 어떤 상수보다 작으면, split을 하지 말라는 뜻이다.
즉, Training Loss Reduction < Regularization Constant라면 split을 중지하게 되며,
이는 Pruning 시스템이라고 할 수 있다.

Pruning에는 2가지 방법이 있다.

  1. Pre-Stopping: Best Split이 음수 Gain을 가지면 Stop한다. 다만 Future Split에서의 이득을 고려하지 못하므로 주의가 요구된다.
  2. Post-Pruning: Max_depth까지 확자한 후에 Negative Gain을 가진 Split 모두를 가지치기 한다.

[3] Efficient Findings of the Best Split

Exact Greedy Algorithm에서는 데이터를 Feature Value에 따라 정렬한 후 이와 같은 Gain을 반복적으로 계산하여 가장 높은 Gain을 바탕으로 Split을 결정하게 된다.
(In order to do so efficiently, the algorithm must first sort the data according to feature values and visit the data in sorted order to accumulate the gradient statistics for the structure score)
(자세한 내용은 논문을 참고할 것)

Approximate Algorithm에서는 적절한 Split 후보들을 선정한 후 그 중에서만 찾게 된다.

직관적으로는 다음과 같은 과정을 거친다고 말할 수 있다.

  1. Split할 양 쪽의 g, h의 합을 계산하는 것
  2. 정렬된 Instances(개체)를 left -> right 방향으로 스캔하면 feature 속에서 best split을 결정하기에 충분하다.

[3] Shrinkage and Colums Subsampling
위에서 설명된 정규화 과정 외에도 추가적으로 과적합을 막기 위한 방법이 도입된다.

Shrinkage는 Tree Boosting의 각 단계를 실행한 이후 $ \eta $라는 factor를 도입하여 새로 추가된 weight을 스케일링해주는 기법이다. tochastic optimization과 유사한 방법인데, shrinkage는 모델을 향상시키기 위해 각각의 개별 Tree와 미래의 Tree의 leaves space의 영향력을 감소시킨다.

Column(Feature) Subsampling은 Random Forest에서도 사용된 기법이다. 이 기법은 전통적인 Row- subsampling에 비해 더욱 효과적이고 빠르다고 알려져 있다.

[4] Weighted Quantile Sketch
위에서 언급하였듯이 근사 알고리즘에서는 후보 Split을 제안하는데, 보통 이 때 feature의 percentil은 후보들이 데이터 상에서 고르게 분포하도록 만든다.
그런데 XGBoost는 가중치가 부여된 데이터에 대해서도 효과적인 Handling이 가능하다.
(Weighted quantile sketch algorithm can handle weighted data with a provable theoretical guarantee)

논문의 4페이지를 살펴보면, Rank Function과 전체 Loss 식의 재표현을 통해서 위의 설명을 간단히 증명하고 있다.

[5] Sparsity-aware Split Finding
현실에서 데이터를 다룰 때 직면하게 되는 가장 큰 문제는 input인 $ \vec{x} $가 매우 sparse하다는 것이다. 이 현상에는 대표적으로 3가지 원인이 있다.

  1. 결측값
  2. 통계학에서의 빈번한 zero entries
  3. 원 핫 인코딩

XGB는 내재적으로 이러한 현상을 효과적으로 Handling할 수 있다.
왜냐하면 데이터를 통해 Optimal Default Direction이 학습되기 때문이다.

[6] System Design
글의 서두에서 언급하였는데, 하드웨어 측면에서도 XGB는 우수한 성능을 보인다.

  • Block Structure for parallel learning: 데이터는 정렬되어 in-memory units (blocks)에 저장된다. 이 데이터는 이후에 계속 반복적으로 재사용이 가능하기 때문에 다시 계산할 필요가 없다. 이를 통해 빠르게 Split Point를 찾아낼 수 있고 Column Sub-sampling을 진행할 수 있다.
  • Cache Awarness: 하드웨어를 최적으로 사용하도록 고안되었다.
  • Out-of-core computing: 거대한 데이터를 다룰 때 디스크 공간을 최적화하고 사용 가능 범위를 최대화한다.

XGB는 또한 Early Stopping 기능도 갖고 있다.
XGb는 참고로 Feature Engineering이나 Hyper Parameter 자동 튜닝 등의 기능은 갖고 있지 못하다.

이로써 XGBoost의 이론적 배경에 대해 살펴보았다.


XGBoost 패키지 Methods

지금부터는 XGBoost Python을 효과적을 Implement하는 방법에 대해 설명한다.


XGBoost Parameter Tuning

파라미터 튜닝의 세부사항을 설명하기 전에, 가장 전반적인 2가지 사항에 대해 설명한다.

  1. Control Overfitting
    일차적으로는 모델 Complexity를 직접적으로 조절할 수 있는데, 이는 max_depth, min_child_weight, gamma 파라미터 조정에 해당한다.

이후에 학습 과정을 Noise에 Robust하게 만들기 위해 Randomness를 추가하는 방법이 있는데, 이는 subsample, colsample_bytree 파라미터 조정에 해당한다.
또는 stepsize eta를 줄일 수도 있는데, 이 때는 num_round를 늘려야만 한다.

link: [https://xgboost.readthedocs.io/en/latest/tutorials/param_tuning.html]

  1. Handle Imbalanced Dataset
    불균형 데이터를 효과적으로 다루기 위해서는 scale_pos_weight 파라미터 조정을 통해 positive & negative weights를 균형적으로 맞출 수 있다.

만약 오직 right probability를 예측하는 것에만 관심이 있다면,
Dataset을 균형적으로 맞추기 힘드므로, max_delta_step 파라미터를 1과 같은 유한 실수로 세팅하면 효과적인 convergence(수렴)을 가능하게 할 수 있다.

XGBoost Parameters

XGB 파라미터는 크게 3가지로 구분된다.

  • General, Booster, Task paramers

General Parameters는 부스팅을 위해 어떤 부스터를 쓰는지와 관련이 있다.
Booster Parameters는 선택한 Booster에 의존한다.
Task Paramters는 학습 시나리오를 결정한다. 예를 들어, Regression tasks는 ranking tasks와 관련하여 다른 파라미터를 사용할 수 있다.

참고로 R에서는 _대신 .를 사용하면 된다.

1. General Parameters

  • booster [default=gbtree]
    gbtree(기본값), gblinear(선형), dart(tee based model 사용)

  • silent [default=0]
    0은 학습 과정을 출력해라, 1은 출력하지 마라.

  • nthread [default=최대치]
    XGB를 돌리기 위해 사용될 병렬 스레드의 개수

  • disable_default_eval_metric [default=0]
    flag to disable default metric. Set to >0 to disable

  • num_pbuffer, num_feature는 자동적으로 설정됨

2. Booster Parameters

  • eta [default=0.3, alias=learning_rate]
    과적합을 방지하기 위해 업데이트 과정에서 사용되는 shrinkage의 step size이다. 각 부스팅 단계이후 우리는 새로운 features에 대한 weights를 얻을 수 있는데, eta는 부스팅 과정을 더욱 보수적으로 만들기 위해 feature weights를 축소한다. 결론적으로 과적합을 방지하는 파라미터다!

  • gamma [default=0, alias=min_split_loss]
    Tree의 leaf split을 진행하기 위해 필요한 최소 Loss Reduction을 뜻한다.
    gamma가 커질수록, 알고리즘은 더욱 보수적으로 만들어질 것이다.
    min_loss_reduction이라는 다른 이름을 생각해볼 때, 이 파라미터는 아래의 식에서 $ \gamma $를 뜻한다.

[Gain = L_{split} = \frac{1}{2} [ \frac{G_{L}^2} {H_L + \lambda} + \frac{G_{R}^2} {H_R + \lambda} - \frac{ (G_{L} + G_{R})^2 } {H_L + H_R + \lambda} ] - \gamma]

  • max_depth [default=6]
    Tree구조의 최대 깊이이다. 0을 입력하면 한계치를 설정하지 않음을 뜻한다.

  • min_child_weight [default=1]
    Child Node에 필요한 Instance weight(hessian)의 최소합.
    만약 Tree의 Split 과정이 진행되면서 instance weight의 합이 min_child_weight보다 작은 leaf node가 나타난다면, Tree는 계속해서 Split을 진행하도록 설정하는 것이다.
    결론적으로 min_child_weight가 커질수록, 알고리즘은 더욱 보수적으로 변화한다.

  • max_delta_step [default=0]
    Maximum delta step we allow each leaf output to be.
    디폴트로 설정된 0은 제한이 없음을 뜻한다.
    양수로 설정이 되면, update step을 더욱 보수적으로 만들어준다.
    일반적으로 이 파라미터는 불필요한데, Logistic Regression에서 데이터셋이 심각하게 불균형한 경우 [1-10]에 해당하는 값을 설정한다면 도움이 될 수도 있다.

  • subsample [default=1]
    Training Instances의 Subsample 비율을 말한다.
    예를 들어 0.5로 설정될 경우, XGB가 학습 데이터의 절반을 랜덤하게 샘플링한다는 것을 뜻한다. Dropout과 유사한 측면이 있다.
    수치가 작아질 수록 과적합을 방지하지만 학습이 더뎌질 수 있다.

  • colsample_bytree [default=1]
    Subsample ratio of columns when constructing each tree.

  • colsample_bylevel [default=1]
    Subsample ratio of columns for each split, in each level.

  • lambda [default=1, alias:reg_lambda]
    Weight에 대한 L2 정규화항. 커질수록 모델을 보수적으로 만든다.

[L(\phi) = \sum_{i}^{n} l(\hat{y_i}, y_i) + \sum_{k=1}^{K} \Omega(f_k) \leftarrow \Omega(f) = \gamma T + \frac{1}{2} \lambda \Vert{w}\Vert^2]

  • alpha [default=0, alias: reg_alpha]
    Weight에 대한 L1 정규화항.

  • tree_method [defaul=auto]
    Tree 구성 구조 방법을 말한다. 단, Distributed and external memory 버전은 오직 approx만 지원한다.
    auto외에는 exact(Exact Greedy 알고리즘), approx(Approximate Greedy 알고리즘), hist(Fast Histo Optimized Approxmate Greedy 알고리즘), gpu_exact, gpu_hist 등이 있다.
    auto로 두면 적당한 크기의 데이터셋에 대해서는 exact를 선택하고, 데이터셋이 매우 커지면 approx를 자동으로 선택한다.
    이 방법들에 대한 간략한 설명은 위 논문 리뷰에서 다루었다.

  • scale_pos_weight [default=1]
    positive & negative weights의 밸런스를 조정하므로, 불균형 데이터에 대해 유용한 파라미터이다.

3. Task Parameters

  • objective [default=reg:linear]
    reg:linear, reg:logistic, binary:logistic, binary:logitraw, binary:hinge 위의 것에서 gpu:추가 가능
    count:poisson, survival:cos, multi:softmax, multi:softprob, rank:pairwise, rank:ndcg, rank:map, reg:gamma

  • base_score [default=0.5]
    The initial prediction score of all instances, global bias.
    바꿀 필요 없다.

  • eval_metric
    rmse, mae, logloss, error, auc, mlogloss, …

  • seed

Dart Booster이나 Linear Booster를 선택하였을 때 따라오는 추가적인 파라미터 조정은 Documentation을 참조할 것.
Console에서만 사용가능한 Line Parameter들도 있다.
Link: [https://xgboost.readthedocs.io/en/latest/parameter.html]


AdaBoost

AdaBoost는 Additive Boosting의 줄임말로, 1995년에 등장하였지만 빠르고 정확한 성능으로 좋은 평가를 받고 있는 알고리즘이다.
간결한 설명을 위해 본 논문의 설명은 m개의 training data에 대하여 Y는 = {-1, +1}로,
Binary Classification 문제로 범위를 제한한다.

AdaBoost는 t개의 weak(base) learning algorithm을 반복적으로 호출하여 학습을 진행한다. (t = 1 ~ T)

여기서 t번 째 round에서의 training example i에 대한 weight distribution을 $ D_t(i) $이라고 하자.

초기에 weight은 균일 분포로서 초기화되어 모두 동일하게 설정되지만,
잘못 분류된 example에 대한 weights는 증가하게 된다. 이렇게 되면 weak learner가 다음 round에서 학습을 진행할 때 이러한 example에 대해 더욱 집중하게 하는 효과를 낼 수 있다.

Weak Learner의 일은 $ D_t $ 분포에 적합한 weak hypothesis $ h_t : X \rightarrow {-1, +1} $을 찾는 것이다.
Weak Learner는 $ D_t $를 다시 학습할 때 사용하거나 $ D_t $에 따라 다시 표본이 추출될 수 있다.
그 weak hypothesis의 성능은 다음과 같이 Error를 계산하여 평가할 수 있다.

[\epsilon_t = P_{i \sim D_t} [ h_t(x_i) \neq y_i] = \sum_{i:h_t(x_i) \neq y_i} D_t(i)]

Adaboost의 부스팅 알고리즘은 아래와 같다. 사실 그리 어렵지는 않다.

$ \alpha_t $는 결국 $ h_t $에 배정된 가중치라고 볼 수 있다.

Analyzing the training error
$ \gamma_t $를 모델의 예측이 Random Guess보다 얼마나 나은지를 나타낸다고 하면,
$ h_t $의 Error인 $ \epsilon_t $은 $ \frac{1}{2} - \gamma_t $로 표현할 수 있다.

아래 식은, 최종 hypothesis H의 Training Error는 일정 수치보다 작을 수 밖에 없음을 나타내는데,
이는 만약 Weak Hypothesis가 적어도 Random Guess보다는 낫다면, 결과적으로 Training Error는 지수적으로 빠르게 감소할 수 밖에 없음을 나타낸다.

이전 알고리즘들도 이와 유사한 과정을 거쳤지만 이들은 $ \gamma_t $의 하한선인 $ \gamma $라는 상수에 대한 사전 정의가 필요했다. AdaBoost는 그러한 과정이 필요 없으며, 각각의 Weak Hypothesis의 Error rates에 adapt하는 모습을 보여준다.
이 때문에 AdaBoost는 Adaptive Boosting이다.

Generalization Error
기존의 연구는 최종 Hypothesis의 Generalization에러를 Training Error의 관점에서 설명할 때,
아래와 같은 식으로 나타냈었는데, 이는 T가 커질 때, boosting 모델은 결국 과적합한다는 것을 의미한다.

Sign Description
T boosting round 수
d hypotheseis의 공간의 compexity의 standard measure인 VC-차원
m example 수
$ \hat{Pr(.)} $ empirical probability on the training example

그런데 이후의 연구를 보면 이는 종종 사실이 아닌 것으로 나타났다.
특히 AdaBoost의 경우 Training Error가 0에 도달한 이후에 지속적인 학습을 진행한 결과,
(Generalization Error)Test Error가 점차적으로 감소한 것을 알 수 있었다.

이를 설명하기 위해 다른 개념이 도입되었는데, 아래를 Maring of exmaple(x, y)라고 한다.

[{Margin} = \frac{y * \sum_t \alpha_t h_t(x)} {\sum_t \alpha_t}]

이 식은 [-1, +1]에 속하며 H가 example을 적절히 분류했을 때 0의 값을 가진다.
이 Margin의 Magnitude는 prediction의 confidence를 측정한다고 해석할 수 있다.

이후에 증명된 바에 따르면,
Training Set에서 Margin이 더욱 증가하면 이는 Generalization Error의 상위의 상한선으로 변환된다고 한다.

식으로 표현하면 아래와 같은데, $ \theta $로 표현되는 상한선(Upper Bound)가 클 수록 Prediction에 자신이 있다는 뜻이고, 이 $ \theta $는 T에 독립적이기 때문에 반복 횟수가 증가해도 Error가 증가하지 않는다.

AdaBoost의 이와 같은 기재는 game-theoretic setting과 같은 방식으로도 이해될 수 있다.
이는 Boosting이 어떤 특정 게임의 반복 play라고 할 때,
AdaBoost는 이 게임에 반복적으로 참여하여 근사적으로 게임을 푸는 General한 알고리즘의 특별한 케이스라고 해석하는 것이다.

Experimetns and Applications

  1. AdaBoost는 간단하고 사용하기 쉽다. weak learner에 대한 사전지식이 필요없으며 여러 method와 결합하여 사용이 가능하다.
  2. T 빼고는 튜닝할 Hyperparameter가 없다.
  3. Noise에 민감하다.
  4. AdaBoost의 operation은 선형 번류기의 coordinate-wise gradient descent로 해석할 수 있다.
  5. AdaBoost는 Outlier를 찾아내는 데에 뛰어난 성능을 보인다. (높은 Weight은 Outlier일 확률이 높다.)
Comment  Read more

PyTorch 사용법 - 02. Linear Regression Model

|

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


이 글에서는 가장 기본 모델인 Linear Regression Model의 Pytorch 프로젝트를 살펴본다.

사용되는 torch 함수들의 사용법은 여기에서 확인할 수 있다.


프로젝트 구조

  • 02_Linear_Regression_Model/
    • main.py
    • data/
      • 02_Linear_Regression_Model_Data.csv
    • results/
  1. 일반적으로 데이터는 data/ 디렉토리에 넣는다.
  2. 코드는 git에 두고, data/.gitignore 파일에 추가하여 데이터는 git에 올리지 않는다. 파일은 다른 서버에 두고 필요할 때 다운로드한다. 일반적으로 dataset은 그 크기가 수 GB 혹은 그 이상도 될 수 있기 때문에 upload/download 시간이 굉장히 길어지기도 하고, Git이 100MB 이상의 큰 파일은 업로드를 지원하지 않기 때문이기도 하다.

물론 이 예제 프로젝트는 너무 간단하여 그냥 data/ 디렉토리 없이 해도 상관없다.
그리고 output/ 또는 results/ 디렉토리를 만들도록 한다.


Import

import pandas as pd

import torch
from torch import nn

import matplotlib.pyplot as plt

다음 파일을 다운로드하여 data/ 디렉토리에 넣는다.

02_Linear_Regression_Model_Data.csv

  1. torch: 설명이 필요없다.
  2. from torch import nn: nn은 Neural Network의 약자이다. torch의 nn 라이브러리는 Neural Network의 모든 것을 포괄하며, Deep-Learning의 가장 기본이 되는 1-Layer Linear Model도 nn.Linear 클래스를 사용한다. 이 예제에서도 nn.Linear를 쓴다.
    • nn.Module은 모든 Neural Network Model의 Base Class이다. 모든 Neural Network Model(흔히 Net이라고 쓴다)은 nn.Module의 subclass이다. nn.Module을 상속한 어떤 subclass가 Neural Network Model로 사용되려면 다음 두 메서드를 override해야 한다.
      • __init__(self): Initialize. 여러분이 사용하고 싶은, Model에 사용될 구성 요소들을 정의 및 초기화한다. 대개 다음과 같이 사용된다.
        • self.conv1 = nn.Conv2d(1, 20, 5)
        • self.conv2 = nn.Conv2d(20, 20, 5)
        • self.linear1 = nn.Linear(1, 20, bias=True)
      • forward(self, x): Specify the connections. __init__에서 정의된 요소들을 잘 연결하여 모델을 구성한다. Nested Tree Structure가 될 수도 있다. 주로 다음처럼 사용된다.
        • x = F.relu(self.conv1(x))
        • return F.relu(self.conv2(x))
    • 다른 말로는 위의 두 메서드를 override하기만 하면 손쉽게 Custom net을 구현할 수 있다는 뜻이기도 하다.
  3. 참고: torch.autograd.Variable은 이전에는 auto gradient 계산을 위해 tensor에 필수적으로 씌워 주어야 했으나, PyTorch 0.4.0 버전 이후로 torch.Tensortorch.autograd.Variable 클래스가 통합되었다. 따라서 PyTorch 구버전을 사용할 예정이 아니라면 Variable은 쓸 필요가 전혀 없다.

Load Data

데이터 준비

지금의 경우는 전처리할 필요가 없으므로 그냥 데이터를 불러오기만 하면 된다. 데이터가 어떻게 생겼는지도 확인해 보자.
데이터가 어떤지 살펴보는 것은 모델을 결정하는 데 있어 매우 중요하다.

다운로드는 여기에서 할 수 있다.

data = pd.read_csv('data/02_Linear_Regression_Model_Data.csv')
# Avoid copy data, just refer
x = torch.from_numpy(data['x'].values).unsqueeze(dim=1).float()
y = torch.from_numpy(data['y'].values).unsqueeze(dim=1).float()

plt.xlim(0, 11);    plt.ylim(0, 8)
plt.title('02_Linear_Regression_Model_Data')
plt.scatter(x, y)

plt.show()

02_Linear_Regression_Model_Data

from_numpy로 불러오는 이유는 데이터를 복사하여 새로 텐서를 생성하는 대신 원 데이터와 메모리를 공유하는 텐서를 쓰기 위함이다. 지금은 상관없지만 대용량의 데이터를 다룰 때에는 어떤 함수가 데이터를 복사하는지 아닌지를 확실하게 알아둘 필요가 있다.
물론, 정말 대용량의 데이터의 경우는 read_csv로 한번에 불러오지 못한다. 이는 데이터를 batch로 조금씩 가져오는 것으로 해결하는데, 이에 대해서는 나중에 살펴보자.

참고: 이 데이터는 다음 코드를 통해 생성되었다.

x = torch.arange(1, 11, dtype=torch.float).unsqueeze(dim=1)
y = x / 2 + 1 + torch.randn(10).unsqueeze(dim=1) / 5

data = torch.cat((x, y), dim=1)
data = pd.DataFrame(data.numpy())

data.to_csv('data/02_Linear_Regression_Model_Data.csv', header=['x', 'y'])

Define and Load Model

매우 간단한 모델이므로 코드도 짧다.
여기서는 여러분의 편의를 위해 함수들의 parameter 이름을 명시하도록 한다.

PyTorch에서 Linear 모델은 torch.nn.Linear 클래스를 사용한다. 여기서는 단지 x를 y로 mapping하는 일차원 직선($ y = wx + b $)을 찾고 싶은 것이므로, in_featuresout_features는 모두 1이다.
nn.Linearnn.Module의 subclass로 in_features개의 input을 선형변환을 거쳐 out_features개의 output으로 변환한다. parameter 개수는 $ (in _ features \times out _ features [ + out _ features]) $ 개이다. 마지막 항은 bias이다.

from torch import nn

model = nn.Linear(in_features=1, out_features=1, bias=True)
print(model)
print(model.weight)
print(model.bias)

"""
Linear(in_features=1, out_features=1, bias=True)
Parameter containing:
tensor([[-0.9360]], requires_grad=True)
Parameter containing:
tensor([0.7960], requires_grad=True)
"""

별다른 utility 함수가 필요 없으므로 따로 utils.py는 만들지 않는다.


Set Loss function(creterion) and Optimizer

적절한 모델을 선정할 때와 마찬가지로 loss function과 optimizer를 결정하는 것은 학습 속도와 성능을 결정짓는 중요한 부분이다.
지금과 같이 간단한 Linear Regression Model에서는 어느 것을 사용해도 학습이 잘 된다. 하지만, 일반적으로 성능이 좋은 AdamOptimizer를 사용하도록 하겠다.

criterion = nn.MSELoss()
optimizer = torch.optim.Adam(params=model.parameters(), lr=0.01)

print(model(x))

"""
tensor([[-0.1399],
        [-1.0759],
        [-2.0119],
        [-2.9478],
        [-3.8838],
        [-4.8197],
        [-5.7557],
        [-6.6917],
        [-7.6276],
        [-8.5636]], grad_fn=<ThAddmmBackward>)
"""

참고: 보통 변수명은 criterion 혹은 loss_function 등을 이용한다.


Train Model

Train은 다음과 같이 이루어진다.

  1. 모델에 데이터를 통과시켜 예측값(현재 모델의 weights로 prediction)을 얻은 뒤
  2. 실제 정답과 loss를 비교하고
  3. gradient를 계산한다.
  4. 이 값을 통해 weights를 업데이트한다(backpropagation).
for step in range(500):
    prediction = model(x)
    loss = criterion(input=prediction, target=y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if step % 20 == 0:
        """
        Show your intermediate results
        """
        pass

코드의 각 라인을 설명하면 다음과 같다.

  1. prediction: 모델에 데이터(x)를 집어넣었을 때 예측값(y). 여기서는 $ y = wx + b $의 결과들이다.
  2. loss: criterion이 MSELoss로 설정되어 있으므로, prediction과 y의 평균제곱오차를 계산한다.
  3. optimizer.zero_grad(): optimizer의 grad를 0으로 설정한다. PyTorch는 parameter들의 gradient를 계산해줄 때 grad는 계속 누적되도록 되어 있다. 따라서 gradient를 다시 계산할 때에는 0으로 세팅해주어야 한다.
  4. loss.backward(): gradient 계산을 역전파(backpropagation)한다.
  5. optimizer.step(): 계산한 gradient를 토대로 parameter를 업데이트한다($ w \leftarrow w - \alpha \Delta w, b \leftarrow b - \alpha \Delta b $)
  6. 학습 결과를 중도에 확인하고 싶으면 그래프를 중간에 계속 그려주는 것도 한 방법이다.

Visualize and save results

결과를 그래프로 보여주는 부분은 matplotlib.pyplot에 대한 내용이므로 여기서는 넘어가도록 하겠다.

def display_results(model, x, y):
    prediction = model(x)
    loss = criterion(input=prediction, target=y)
    
    plt.clf()
    plt.xlim(0, 11);    plt.ylim(0, 8)
    plt.scatter(x.data.numpy(), y.data.numpy())
    plt.plot(x.data.numpy(), prediction.data.numpy(), 'b--')
    plt.title('loss={:.4}, w={:.4}, b={:.4}'.format(loss.data.item(), model.weight.data.item(), model.bias.data.item()))
    plt.show()
    # plt.savefig('results/02_Linear_Regression_Model_trained.png')

display_results(model, x, y)

02_Linear_Regression_Model_Trained

모델을 저장하려면 torch.save 함수를 이용한다. 저장할 모델은 대개 .pt 확장자를 사용한다.

torch.save(obj=model, f='02_Linear_Regression_Model.pt')

참고: .pt 파일로 저장한 PyTorch 모델을 load해서 사용하려면 다음과 같이 한다. 이는 나중에 Transfer Learning과 함께 자세히 다루도록 하겠다.

loaded_model = torch.load(f='02_Linear_Regression_Model.pt')

display_results(loaded_model, x, y)

정확히 같은 결과를 볼 수 있을 것이다.


전체 코드는 여기에서 살펴볼 수 있다.


Comment  Read more