깔끔하게 파이토치 모델을 C++ 완성된 애플리케이션에 넣어 사용하는 방법을 정리하는 글이다

개발 환경은 다음과 같다

OS: Ubuntu 16.04

Pytorch 1.0

CMAKE 3.13.3

libtorch cpu버전

C++편집기: Dev C++

우선 새 폴더를 만들어 CMakeLists.txt와 Cpp 소스코드를 만든다


CMakeLists.txt:

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)

project(custom_ops)


find_package(Torch REQUIRED)


add_executable(example-app example-app.cpp)

target_link_libraries(example-app "${TORCH_LIBRARIES}")

set_property(TARGET example-app PROPERTY CXX_STANDARD 11)



C++ 코드:


C++ 코드는, 실행시 모델 경로를 같이 전달하면 확인 후 load하며 잘 load가 되었다면 cout을 통해 ok를 출력하는 코드이다

두 파일이 있는 폴더에 새폴더를 만들어 build로 이름짓고 폴더로 들어가 터미널을 실행한다


그리고 CMAKE를 만드는 명령어들을 실행한다


cmake -S '소스코드있는 경로' -DCMAKE_PREFIX_PATH='libtorch 경로'

make

순서대로 입력 후 빌드가 성공적으로 끝나면 ./example-app '모델경로' 명령어로 완성된 애플리케이션을 실행하낟

ok가 출력되면 성공



이제 실제 load한 모델에 인풋값을 통과시켜보는 작업을 한다

C++ 코드를 아래와 같이 수정한다

이때 자신이 사용하는 모델의 인풋 shape에 맞게 inputs을 생성해준다

터미널로 돌아가 

make

./example-app '모델경로'

순서대로 명령어를 실행하면 된다 

그 결과 아래와 같은 결과를 얻었다. 


드디어 cmake 파일을 빌드하는데 성공했다

이거땜에 얼마나 많은 시간을 날리고 낭비했는지 ..ㅂㄷㅂㄷ


결론은 파이토치 튜토리얼에 나오것을 그대로 따라하면 안된다


일단 cmake가 버전업되면서 명령어가 바뀌었다

또한, libtorch를 cuda버전이 아닌 cpu버전으로 하니까 성공했다


빌드명령어는 cmake -S '소스코드있는 경로' -DCMAKE_PREFIX_PATH='cpu버전 libtorch폴더의 경로'


이렇게하면 빌드가 되고

이후에 make 명령어를 입력하면 exe파일을 만들어준다


아 그리고 그 사이 우분투로 OS를 바꿔 환경은 16.04, python 3.6, pytorch 1.0, cuda 9.0, libcudnn 7.4.2.24-1 이 되었다

(단 libtorch는 언급한대로 cpu 버전이다)

터미널에 출력 결과는 다음과 같다


***@******:~/example-app/build$ cmake -S ~/example-app -DCMAKE_PREFIX_PATH=~/libtorch-cpu/libtorch

-- The C compiler identification is GNU 5.4.0

-- The CXX compiler identification is GNU 5.4.0

-- Check for working C compiler: /usr/bin/cc

-- Check for working C compiler: /usr/bin/cc -- works

-- Detecting C compiler ABI info

-- Detecting C compiler ABI info - done

-- Detecting C compile features

-- Detecting C compile features - done

-- Check for working CXX compiler: /usr/bin/c++

-- Check for working CXX compiler: /usr/bin/c++ -- works

-- Detecting CXX compiler ABI info

-- Detecting CXX compiler ABI info - done

-- Detecting CXX compile features

-- Detecting CXX compile features - done

-- Looking for pthread.h

-- Looking for pthread.h - found

-- Looking for pthread_create

-- Looking for pthread_create - not found

-- Looking for pthread_create in pthreads

-- Looking for pthread_create in pthreads - not found

-- Looking for pthread_create in pthread

-- Looking for pthread_create in pthread - found

-- Found Threads: TRUE  

-- Found torch: /home/lab/libtorch-cpu/libtorch/lib/libtorch.so  

-- Configuring done

-- Generating done

-- Build files have been written to: /home/lab/example-app/build

***@******:~/example-app/build$ make

Scanning dependencies of target example-app

[ 50%] Building CXX object CMakeFiles/example-app.dir/example-app.cpp.o

[100%] Linking CXX executable example-app

[100%] Built target example-app



우선 C++(VS)에서 LibTorch를 쓰기위해 몇가지 설치,설정을 해야한다


참고링크: https://zhuanlan.zhihu.com/p/52806730


VS2017이 필요하고 Cmake는 3.0 이상의 버전으로 설치한다


설치는 https://cmake.org/download/ 에서 

Windows win64-x64 ZIPcmake-3.13.2-win64-x64.zip

를 다운받아서 설치하였다.

다운받아서 압축 푼 뒤 bin폴더 경로를 시스템 환경 변수에 설치하면 된다

제대로 설치되었는지 확인은 cmd에서 cmake --version 명령어를 입력해보면 된다. 아래와 같이 잘 설치된것을 확인하면 끝


C:\Users\10127>cmake --version

cmake version 3.13.2


또한, 현재 타입 변경에 대해 narrow conversion 문제가 있다 아래 관련 링크

https://github.com/pytorch/pytorch/pull/15333



공식홈페이지 튜토리얼 정리글입니다

https://pytorch.org/tutorials/advanced/cpp_export.html



STEP 1: 파이토치 모델을 Torch script로 컨버팅

 Torch Script 을 이용해서 파이토치모델을 C++에서 불러올 수 있는데 여기엔 두가지 방법이 있습니다.


첫번째는 tracing 이라고 불리는 것으로 예제 인풋을 이용해서 그 인풋이 모델을 통과하면서 변화과정을 기록하는 것으로 사용 방법(control flow)이 제한적인 모델들에 적합한 방법

두번째는 모델에 소위 '주석'을 달아 Torch scrip 컴파일러가 직접적으로 파싱하고 컴파일할 수 있도록 하는 방법으로 위 Torch Script 링크를 누르면 자세한 방법이 나옴



Converting to Torch Script via Tracing

torch.jit.trace 함수에 모델 인스턴스와 예제 인풋값을 전달해주면 됨. 이러면 torch.jit.ScriptModule 오브젝트가 생성되며 모듈의 forward 메소드를 추적함

import torch
import torchvision

# An instance of your model.
model = torchvision.models.resnet18()

# An example input you would normally provide to your model's forward() method.
example = torch.rand(1, 3, 224, 224)

# Use torch.jit.trace to generate a torch.jit.ScriptModule via tracing.
traced_script_module = torch.jit.trace(model, example)

trace된 ScriptModule은 이제 일반적인 파이토치 모듈로 사용가능

In[1]: output = traced_script_module(torch.ones(1, 3, 224, 224))
In[2]: output[0, :5]
Out[2]: tensor([-0.2698, -0.0381,  0.4023, -0.3010, -0.0448], grad_fn=<SliceBackward>)


Converting to Torch Script via Annotation

만약 모델이 특별한 형태나 흐름을 갖고있다면, Torch Script에 직접 기록하고 주석 달기를 원할 수 있다. 예를들어 모델이 아래와 같은 구조라면

import torch

class MyModule(torch.nn.Module):
    def __init__(self, N, M):
        super(MyModule, self).__init__()
        self.weight = torch.nn.Parameter(torch.rand(N, M))

    def forward(self, input):
        if input.sum() > 0:
          output = self.weight.mv(input)
        else:
          output = self.weight + input
        return output

이 모듈의 forward 함수는 인풋에 따라 다른 흐름을 보이기 때문에 tracing에 알맞지 않음. 따라서 torch.jit.ScriptModule 에 부분집합을 만들고 @torch.jit.script_method 주석을 forward 메소드에 추가하는 방법을 ScriptModule로 컨버팅함

import torch

class MyModule(torch.jit.ScriptModule):
    def __init__(self, N, M):
        super(MyModule, self).__init__()
        self.weight = torch.nn.Parameter(torch.rand(N, M))

    @torch.jit.script_method
    def forward(self, input):
        if input.sum() > 0:
          output = self.weight.mv(input)
        else:
          output = self.weight + input
        return output

my_script_module = MyModule()

MyModule 오브젝트를 만들면 바로 ScriptModule 인스턴스를 만들어줌 




STEP 2: Script Module을 파일로 쓰기


아래의 명령어를 이용해 파일로 저장하면, 파이썬에 의존하지안고 C++에서 모델을 사용 가능함

traced_script_module.save("model.pt")

파이썬파트 여기까지가 끝


STEP 3: C++에서 Script Module 불러오기


LibTorch라는 파이토치 C++ API를 이용해서 불러와야 함. CMake와 LibTorch를 이용해서 모델을 불러와 이용하는 간단한 예제를 만들것


모델 불러오는 코드

#include <torch/script.h> // One-stop header.

#include <iostream>
#include <memory>

int main(int argc, const char* argv[]) {
  if (argc != 2) {
    std::cerr << "usage: example-app <path-to-exported-script-module>\n";
    return -1;
  }

  // Deserialize the ScriptModule from a file using torch::jit::load().
  std::shared_ptr<torch::jit::script::Module> module = torch::jit::load(argv[1]);

  assert(module != nullptr);
  std::cout << "ok\n";
}

파일 불러와 sharedpoint로 받고(C++에서의 torch.jit.ScriptModule) null 포인터인지 확인하는 코드임



Depending on LibTorch and Building the Application

위 코드가 example-app.cpp라는 스크립트에 적혀있다고 할때 CMakeLists.txt는 다음과 같음

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
project(custom_ops)

find_package(Torch REQUIRED)

add_executable(example-app example-app.cpp)
target_link_libraries(example-app "${TORCH_LIBRARIES}")
set_property(TARGET example-app PROPERTY CXX_STANDARD 11)

마지막으로 필요한것은 LibTorch distribution이다.  download page 에서 최신버전 다운 가능함

받아서 압축 풀면 아래와 같음

libtorch/
  bin/
  include/
  lib/
  share/

lib/ 폴더는 공유 라이브러리 포함

include/ 폴더는 헤더파일 포함

share/ 폴더는 위에 find_package 명령어를 실행하기위한 필요한 CMake configuration을 포함


예제 경로가 다음과 같을때:

example-app/
  CMakeLists.txt
  example-app.cpp

다음의 명령어들을 실행하여 애플리케이션을 빌드함

mkdir build
cd build
cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
make

/path/to/libtorch 는 LibTorch distribution을 압축 푼 풀 경로여야함. 만약 잘 됐다면 아래와 같아야함

root@4b5a67132e81:/example-app# mkdir build
root@4b5a67132e81:/example-app# cd build
root@4b5a67132e81:/example-app/build# cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Looking for pthread.h
-- Looking for pthread.h - found
-- Looking for pthread_create
-- Looking for pthread_create - not found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Configuring done
-- Generating done
-- Build files have been written to: /example-app/build
root@4b5a67132e81:/example-app/build# make
Scanning dependencies of target example-app
[ 50%] Building CXX object CMakeFiles/example-app.dir/example-app.cpp.o
[100%] Linking CXX executable example-app
[100%] Built target example-app

ResNet18모델을 성공적으로 만들었다면 아래와 같은 메세지가 뜸

root@4b5a67132e81:/example-app/build# ./example-app model.pt
ok


STEP 4: Executing the script module in C++

명령어 조금만 넣으면 바로 실행 가능 아래의 코드를 main() 함수에 추가

// Create a vector of inputs.
std::vector<torch::jit::IValue> inputs;
inputs.push_back(torch::ones({1, 3, 224, 224}));

// Execute the model and turn its output into a tensor.
at::Tensor output = module->forward(inputs).toTensor();

std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n';

모델의 인풋과 아웃풋은 IValue 타입이다 이후 forward 를 실행하고 텐서로 변환함

마지막줄은, 아웃풋의 처음 5개만 출력하는 것







관련 링크

https://www.degruyter.com/downloadpdf/j/itms.2017.20.issue-1/itms-2017-0003/itms-2017-0003.pdf

https://stats.stackexchange.com/questions/164876/tradeoff-batch-size-vs-number-of-iterations-to-train-a-neural-network

http://forums.fast.ai/t/batch-size-effect-on-validation-accuracy/413

https://www.quora.com/Intuitively-how-does-mini-batch-size-affect-the-performance-of-stochastic-gradient-descent

http://goodtogreate.tistory.com/entry/Batch-%ED%81%AC%EA%B8%B0%EC%9D%98-%EA%B2%B0%EC%A0%95-%EB%B0%A9%EB%B2%95

https://blog.lunit.io/2018/08/03/batch-size-in-deep-learning/



1. "Impact of Training Set Batch Size on the Performance of Convolutional Neural Networks for Diverse Datasets" 에 따르면(VIII 결론 부분)


CNN의 퍼포먼스를 늘리는 문제(image recognition의 정확도)에 있어서 CNN 파라미터 중 배치사이즈는 치명적인(crucial) 영향력을 갖고 있다


이 값이 높을수록 인식 정확도가 높아진다. 반면 계산 양이 많아진다


대략 200부터 다양한 크기의 배치사이즈가 적절하다고 말하고있다.



2. From Nitish Shirish Keskar, Dheevatsa Mudigere, Jorge Nocedal, Mikhail Smelyanskiy, Ping Tak Peter Tang. On Large-Batch Training for Deep Learning: Generalization Gap and Sharp Minima. https://arxiv.org/abs/1609.04836  에 내용


large-batch 에서 generalization이 부족한데 그 이유는 sharp minimizers로 수렴하는 경향 때문이라고 한다.


이런 minimizer들은 


2f(x)


의 eigenvalue가 큰 양수인 특징이 있다고 한다. 반면 flat minimizer들은 그게 작은 양수 값들임


그들의 관찰에 의하면 large-batch의 딥러닝들은 주로 sharp minima에 빠지는 경우가 많당



그리고 Ian Goodfellow가 '왜 그래디언트 계산할때 모든 트레이닝셋을 쓰지않는지 즉 미니배치를 왜쓰는지'에 대해 답변하길


러닝레이트(learning rate)는 코스트 함수(cost function)가 얼마나 굽어져있는지(curved) 등에 의해 제한된다. gradient descent가 코스트 함수의 linear approximation을 만든 것을 생각할때 기울기를 보고 아래쪽으로 움직이지. 만약에 코스트 함수가 상당히(highly) non-linear하면(즉 highly curved) 그 근사치(approximation)이 충분히 좋지 않아 lr이 작아야 안전하다.


미니배치에 m개를 넣으면 시공간 복잡도가 각각 O(m)이다. 하지만 O(sqrt(m))으로 줄일수있다(이부분 의역)

다른말로, 미니배치에 더 많은 예시를 넣게 되면 약해진 미미한(diminishing marginal) 결과를 얻는다


또한 전체 트레이닝 셋을 다 쓴다고해서 정확한 기울기값을 알수있는게 아니다. 데이터 분포에 영향받기 때문에 기대값을 갖게될수밖에 없다.




그밖에 

http://goodtogreate.tistory.com/entry/Batch-%ED%81%AC%EA%B8%B0%EC%9D%98-%EA%B2%B0%EC%A0%95-%EB%B0%A9%EB%B2%95

https://blog.lunit.io/2018/08/03/batch-size-in-deep-learning/


위 두 글에서도 잘 정리가 되어있는데 한국어이니 여기에 정리는 안하고 나중에 필요하면 다시 읽어볼것이다



근데 결론이 좀 어렵다. 

http://blog.outcome.io/pytorch-quick-start-classifying-an-image/

Data Loading and Processing Tutorial

AuthorSasank Chilamkurthy


튜토리얼 시작하기에 앞서 다음의 패키지를 다운받아야합니다

  • scikit-image: For image io and transforms
  • pandas: For easier csv parsing

다운방법

scikit-image:

https://anaconda.org/anaconda/scikit-image 참고

1. anaconda3 실행하여 가상환경 실행(activate '환경이름')

2. conda install -c anaconda scikit-image 입력하여 설치



pandas:

http://pandas.pydata.org/ 참고

1. anaconda3 실행하여 가상환경 실행(activate '환경이름')

2. conda install -c anaconda scikit-image 입력하여 설치



Learning PyTorch with Examples


파이토치 기본 컨셉을 소개하는 튜토리얼이다


파이토치가 제공하는 2가지 메인 특징:


n-차원 텐서는 넘파이와 비슷하지만 GPU들에서 돌릴 수 있다

신경망 형성과 학습에서 자동으로 differentiation


fully-connected ReLU 네트워크를 예시로 사용할 것이며


1개의 히든레이어가 있고, gradient descent 로 학습하여 임의의 데이터를 정답 결과와의 유클리안 거리를 최소화하는 방법으로 학습할 것



Tensors


파이토치에 앞서, 넘파이를 이용한 네트워크에 대한 내용


넘파이는 n차원 배열 오브젝트로, 이런 배열들을 계산하는 함수를 제공함 딥러닝이나 기울기 등에 대한 직접적인 것을 포함하진 않지만


넘파이 계산들을 이용하여 2개 레이어의 네트워크를 만들 수 있음


import numpy as np

N, D_in, H, D_out = 64, 1000, 100, 10

x = np.random.randn(N, D_in)
y = np.random.randn(N, D_out)

w1 = np.random.randn(D_in, H)
w2 = np.random.randn(H, D_out)

learning_rate = 1e-6

for t in range(500):
#forward pass: predicted y 계산
h = x.dot(w1) #dot product? maybe
h_relu = np.maximum(h, 0) #normal ReLU
y_pred = h_relu.dot(w2)

#Compute and pritn loss
loss = np.square(y_pred - y).sum()
print(t, loss)

#backprop to compute gradients of w1 and w2 with respect to loss
grad_y_pred = 2.0 * (y_pred - y)
grad_w2 = h_relu.T.dot(grad_y_pred)
grad_h_relu = grad_y_pred.dot(w2.T)
grad_h = grad_h_relu.copy()
grad_h[h < 0] = 0
grad_w1 = x.T.dot(grad_h)

#update weights
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2




PyTorch: Tensors

넘파이는 좋은 프레임웤이지만 GPU를 사용할 수 없음 최근 딥러닝 네트워크들은 GPU 사용시 50배 또는 그 이상 빨라지기도 하기때문에 넘파이 ㄴㄴ


파이토치 텐서는 넘파이랑 같음. n차원 배열이며 딥러닝에 대한 건 아니지만 배열에 대한 계산도 있고 쉽게 구현가능



하지만 넘파이와 달리 GPU를 이용하여 가속화할 수 있음 새로운 변수타입으로 cast하기만 하면 됨


다음은 텐서를 이용하여 2개 레이어 네트워크의 임의 데이터에 대한 학습을 하는 코드임


import torch

dtype = torch.float
device = torch.device("cpu")
# dtype = torch.device("cuda:0") #GPU로 돌리려면 주석 해제

# N은 배치사이즈 D_in은 인풋 차원
# H는 히든 디멘션 D_out은 아웃풋 차원
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random input and output data
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)

# Randomly initialize weights
w1 = torch.randn(D_in, H, device=device, dtype=dtype)
w2 = torch.randn(H, D_out, device=device, dtype=dtype)

learning_rate = 1e-6
for t in range(500):
# Forward pass: compute predicted y
h = x.mm(w1)
h_relu = h.clamp(min=0)
y_pred = h_relu.mm(w2)

# Compute and print loss
loss = (y_pred - y).pow(2).sum().item()
print(t, loss)

# Backprop to compute gradients of w1 and w2 with respect to loss
grad_y_pred = 2.0 * (y_pred - y)
grad_w2 = h_relu.t().mm(grad_y_pred)
grad_h_relu = grad_y_pred.mm(w2.t())
grad_h = grad_h_relu.clone()
grad_h[h < 0] = 0
grad_w1 = x.t().mm(grad_h)

# Update weights using gradient descent
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2




Autograd

위 코드에서 수동적으로 네트워크 학습을 진행시켰는데 좀만 복잡해져도 힘들어짐. 


다행히도 automatic differentiation 을 이용해서 신경망 backward pass를 자동으로 계산할 수 있음 파이토치에서는 autograd 패키지를 이용


네트워크 정방향 진행하면 computational 그래프가 정의됨. 그래프의 노드는 텐서들이며 엣지는 아웃풋 텐서를 만드는 계산식. Backpropagation


시 기울기 계산하기가 쉬워짐



복잡한 소리처럼 들리지만, 사용하기엔 간단함


x 텐서 변수가 x.requires_grad=True라면 x.grad는 이 x의 기울기값을 담는 텐서임


아래 코드는 이전 코드에서 필요한부분만 autograd 패키지를 이용하는쪽으로 수정한 코드임:


import torch

dtype = torch.float
device = torch.device("cpu")
# dtype = torch.device("cuda:0") #GPU로 돌리려면 주석 해제

# N은 배치사이즈 D_in은 인풋 차원
# H는 히든 디멘션 D_out은 아웃풋 차원
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random input and output data
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)

# Randomly initialize weights
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(500):
# # Forward pass: compute predicted y
# h = x.mm(w1)
# h_relu = h.clamp(min=0)
# y_pred = h_relu.mm(w2)
y_pred = x.mm(w1).clamp(min=0).mm(w2)

# Compute and print loss
#loss = (y_pred - y).pow(2).sum().item()
loss = (y_pred - y).pow(2).sum()
print(t, loss.item())

# # Backprop to compute gradients of w1 and w2 with respect to loss
# grad_y_pred = 2.0 * (y_pred - y)
# grad_w2 = h_relu.t().mm(grad_y_pred)
# grad_h_relu = grad_y_pred.mm(w2.t())
# grad_h = grad_h_relu.clone()
# grad_h[h < 0] = 0
# grad_w1 = x.t().mm(grad_h)
loss.backward()

with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad

#수동으로 기울기를 0으로 다시 해줌
w1.grad.zero_()
w2.grad.zero_()

# # Update weights using gradient descent
# w1 -= learning_rate * grad_w1
# w2 -= learning_rate * grad_w2




PyTorch: Defining new autograd functions

각각의 오토그래드 기본 계산은 사실 텐서를 계산하는 두개의 함수이다. 


forward 함수는 인풋 텐서로부터 아웃풋 텐서를 계산한다.


backward 함수는 아웃풋 텐서의 기울기를 받아 인풋 텐서의 기울기를 계산한다.



torch.autograd.Function 의 서브클래스를 정의하고 forward와 backward 함수를 시행하여 우리는 우리만의 autograd 연산을 만들 수 있다. 


그리고 인스턴스를 생성하고 함수처럼 생성하여 우리의 새로운 연산을 이용해서 인풋데이터를 포함하는 텐서를 넘긴다



아래 예제에서 우리의 커스텀 오토그래드 함수를 정의하여 ReLU nonlinearity를 수행하고 2개 레이어 네트워크를 실행한다


# -*- coding: utf-8 -*-

import torch

class MyReLU(torch.autograd.Function):
"""
서브클래스를 생성하여 고유의 오토그래드 함수를 만듦
"""
@staticmethod
def forward(ctx, input):
"""
forward 패스에서 인풋이 담긴 텐서를 받아 아웃풋이 담긴 텐서를 반환한다. ctx는 context object이며 backward 계산을
위한 정보를 숨기는데 사용한다. ctx.save_for_backward를 이용해서 backward pass에 사용하기위해 임의의 오브젝트를 캐시
"""
ctx.save_for_backward(input)
return input.clamp(min=0)

@staticmethod
def backward(ctx, grad_output):
"""
backward 패스에서 아웃풋쪽 loss의 gradient를 담는 텐서를 받아 input쪽 기울기를 계산해야함
"""
input, = ctx.saved_tensors
grad_input = grad_output.clone()
grad_input[input < 0] = 0
return grad_input

dtype = torch.float
device = torch.device("cpu")

# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random Tensors to hold input and outputs.
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)

# Create random Tensors for weights.
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(500):
# 우리가 만든 함수를 사용하기 위해 .apply를 사용함 우리는 이걸 relu라 부름
relu = MyReLU.apply

# forward 패스: y를 예상하여 계산함; 우리가 계산함
# ReLu 는 우리 고유 함수를 이용한것
y_pred = relu(x.mm(w1)).mm(w2)

# loss 계산하고 출력
loss = (y_pred - y).pow(2).sum()
print(t, loss.item())

loss.backward()

# gradient descent를 이용하여 weights 갱신
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad

w1.grad.zero_()
w2.grad.zero_()




TensorFlow: Static Graphs

파이토치는 텐서플로우와 매우 비슷함. 둘다 computational 그래프를 만들고, automatic differentiation 을 사용하여 기울기 계산함


가장 큰 차이점은 그래프가 텐서플로우는 static이고 파이토치는 dynamic



텐서플로우에서 그래프를 생성하고 이걸 계속 같은걸 쓰고 또 씀. 인풋데이터만 다르게 줘서말이지.


파이토치는 각각의 forward pass가 각각의 그래프를 생성함



static 그래프는 개발자가 optimize하는 측면에서 더 나이스함. 예를들어 프레임워크는 일부 그래프 연산을 효율을 위해 융합(fuse)시키거나


여러 GPU나 머신들에게 분배하는 전력을 쓸 수 있음. 만약 같은 그래프를 계속 반복해서 다시 사용한다면 이건 비용적으로 선행 optimization이


분할상환될 수 있음(이 부분 한정 직역)



static과 dynamic 그래프가 다른 것 중 하나는 control flow이다. 일부 모델들에서는 각각의 데이터 포인트에 따라 다른 계산을 하길 바랄 수도 있다.


예를들어 반복네트워크(Recurrent Network - RNN)에서 각각의 데이터포인트에서 시간 단계에 따라 다르기 때문에 펼쳐질 수 있다


이 펼치는건 loop 반복으로 시행될 수 있다. static 그래프에서 반복되는 부분은 그래프의 일부여야 한다. 이러한 이유에서 텐서플로우에서는 tf.scan 같은 연산을 제공하여 그래프에 내장된 루프를 시행한다.


dynamic 그래프에선 더 간단하다. 각각의 예시에 대해 그래프를 생성하기때문에 일반적인 긴요한 플로우 컨트롤을 사용하여 각각의 인풋에 맞게


연산을 할 수 있다.



이전의 autograd 예시와 비교하기 위해 여기서는 텐서플로우를 이용하여 2개 레이어 네트워크를 학습하였다


# -*- coding: utf-8 -*-
import tensorflow as tf
import numpy as np

# First we set up the computational graph:

# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10

# Create placeholders for the input and target data; these will be filled
# with real data when we execute the graph.
x = tf.placeholder(tf.float32, shape=(None, D_in))
y = tf.placeholder(tf.float32, shape=(None, D_out))

# Create Variables for the weights and initialize them with random data.
# A TensorFlow Variable persists its value across executions of the graph.
w1 = tf.Variable(tf.random_normal((D_in, H)))
w2 = tf.Variable(tf.random_normal((H, D_out)))

# Forward pass: Compute the predicted y using operations on TensorFlow Tensors.
# Note that this code does not actually perform any numeric operations; it
# merely sets up the computational graph that we will later execute.
h = tf.matmul(x, w1)
h_relu = tf.maximum(h, tf.zeros(1))
y_pred = tf.matmul(h_relu, w2)

# Compute loss using operations on TensorFlow Tensors
loss = tf.reduce_sum((y - y_pred) ** 2.0)

# Compute gradient of the loss with respect to w1 and w2.
grad_w1, grad_w2 = tf.gradients(loss, [w1, w2])

# Update the weights using gradient descent. To actually update the weights
# we need to evaluate new_w1 and new_w2 when executing the graph. Note that
# in TensorFlow the the act of updating the value of the weights is part of
# the computational graph; in PyTorch this happens outside the computational
# graph.
learning_rate = 1e-6
new_w1 = w1.assign(w1 - learning_rate * grad_w1)
new_w2 = w2.assign(w2 - learning_rate * grad_w2)

# Now we have built our computational graph, so we enter a TensorFlow session to
# actually execute the graph.
with tf.Session() as sess:
    # Run the graph once to initialize the Variables w1 and w2.
    sess.run(tf.global_variables_initializer())

    # Create numpy arrays holding the actual data for the inputs x and targets
    # y
    x_value = np.random.randn(N, D_in)
    y_value = np.random.randn(N, D_out)
    for _ in range(500):
        # Execute the graph many times. Each time it executes we want to bind
        # x_value to x and y_value to y, specified with the feed_dict argument.
        # Each time we execute the graph we want to compute the values for loss,
        # new_w1, and new_w2; the values of these Tensors are returned as numpy
        # arrays.
        loss_value, _, _ = sess.run([loss, new_w1, new_w2],
                                    feed_dict={x: x_value, y: y_value})
        print(loss_value)





nn module

Computational graph나 autograd는 복잡한 연산을 정의하고 자동으로 기울기를 구하기위해 매우 파워풀한 패러다임. 하지만


거대한 신경망에서 autograd는 약간 low-level일 수 있음



텐서플로우에서 Keras, TensorFlow-Slim, 등과 같은 패키지는 좀더 높은 레벨(higher-level)의 관념을 제공하여 신경망 만들때 더 유용함



파이토치에서는 nn 패키지가 같은 목적으로 이용됨. Modules의 집합을 정의하며 이건 신경망의 레이어에 해당함. 하나의 모듈은 인풋 텐서를 받아


아웃풋 텐서를 계산하지만 학습할 파라미터를 포함한 텐서같은 것을 내부 state로 가지고있기도 함


nn 패키지는 또한 신경망 학습 시 일반적으로 많이 사용하는 loss 함수들도 정의함



아래 예시는 2개 레이어 네트워크를 nn패키지를 이용하여 구성한것


import torch

N, D_in, H, D_out = 64, 1000, 100, 10

x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# nn 패키지를 이용하여 레이어를 나열하여 모델을 만듬. nn.Sequential 은 다른 모듈을 포함하는 모듈이고
# 이를 적용하여 output을 얻어냄. 각각의 선형 모듈은 인풋으로부터 선형 함수를 이용하여 아웃풋을 계산
# 그리고 weight과 bias를 홀드
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out),
)

# nn 패키지는 유명한 몇개의 loss 함수도 갖고있음
loss_fn = torch.nn.MSELoss(size_average=False)

learning_rate=1e-4
for t in range(500):
# x를 모델로 통과시켜서 y를 예측하여 게산함. 모듈 오브젝트는 __call__ 연산을 오버라이드하여 함수처럼 호출하게함
# 인풋과 아웃풋은 텐서임
y_pred = model(x)

# 로스 계산과 출력. 계산된 y와 실제 y를 포함하는 텐서들을 넘기면 계산해줌
loss = loss_fn(y_pred, y)
print(t, loss.item())

# 기울기 0으로 만들기 backward pass 하기 전에
model.zero_grad()

# backward pass: 모델의 학습가능한 모든 파라미터에 대해 기울기 계산하고, 각각 모듈(레이어)의 파라미터는 텐서에
#저장되어있음 requires_grad=True 속성을 호출하면 모든 기울기를 계산할것임
loss.backward()

# gradient descent 이용하여 weight들 업데이트함
with torch.no_grad():
for param in model.parameters():
param -= learning_rate * param.grad




PyTorch: optim

파이토치의 optim 패키지는 optimization 알고리즘의 아이디어를 추상화하고 널리 사용되는 알고리즘을 제공한다.


아래 코드는 nn패키지로 모델을 정의하고 optim패키지가 제공하는 Adam 알고리즘으로 optimize 하는것이다


import torch

# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random Tensors to hold inputs and outputs
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# Use the nn package to define our model and loss function.
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out),
)
loss_fn = torch.nn.MSELoss(size_average=False)

# optim 패키지의 Adam 알고리즘을 이용하여 optimize함. 다른 알고리즘도 많음
# Adam 알고리즘의 첫번째 인자는 어떤 텐서를 아담이 업데이트해야하는지 알려줌

learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(500):
# Forward pass: compute predicted y by passing x to the model.
y_pred = model(x)

# Compute and print loss.
loss = loss_fn(y_pred, y)
print(t, loss.item())

# backward pass 하기 전에, 모든 기울기를 0으로 만들어야하는데 왜냐하면 .backward()가 호출이되면 기본값으로
# 기울기값들은 버퍼에 축적되기때문
optimizer.zero_grad()

# backward pass: 모델 파라미터에 따른 loss 기울기 계산
loss.backward()

#step 함수는 파라미터를 갱신함
optimizer.step()




PyTorch: Custom nn Modules

기존에 존재하는 모듈 외에 좀더 복잡한 모델을 원할 경우가 있을텐데 이때는 nn.Module이라는 서브클래스로 고유한 모듈을 만들 수 있다. 


import torch

class TwoLayerNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
"""
2개의 nn.Linear 모듈(레이어)를 초기화하고 멤버 변수 할당화
:param D_in:
:param H:
:param D_out:
"""
super(TwoLayerNet, self).__init__()
self.linear1 = torch.nn.Linear(D_in, H)
self.linear2 = torch.nn.Linear(H, D_out)

def forward(self, x):
"""
인풋 텐서를 받아서 아웃풋 텐서를 반환해야함 생성자에 정의된 모듈이나 텐서의 임의 연산을 사용할 수 있음
:param x:
:return:
"""
h_relu = self.linear1(x).clamp(min=0)
y_pred = self.linear2(h_relu)
return y_pred

# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random Tensors to hold inputs and outputs
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# Construct our model by instantiating the class defined above
model = TwoLayerNet(D_in, H, D_out)

# Loss 함수와 Opimizer를 정의함. model.parameters()를 호출하여 SGD를 사용하면 모델의 레이어들의 학습해야할 파라미터가 넘겨짐
criterion = torch.nn.MSELoss(size_average=False)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4)
for t in range(500):
# forward pass: y 예상값을 계산함 x를 모델에 통과시켜서
y_pred = model(x)

loss = criterion(y_pred, y)
print(t, loss.item())

# 기울기 0, backward pass, weights 갱신
optimizer.zero_grad()
loss.backward()
optimizer.step()




PyTorch: Control Flow + Weight Sharing

dynamic 그래프와 weight 공유의 예로 상당히 이상한 모델을 돌려봅시다. fully-connected ReLU 네트워크이며 각각의 패스에서 1부터 4 사이의 숫자를 고르고 그 숫자만큼의(?) 히든레이어를 사용하며 같인 weights를 여러번 사용하여 가장 안쪽에있는 히든레이어를 계산함


루프를 돌리기 위해 일반적인 파이썬 플로우 컨트롤을 사용하며 forward pass 선언 시 같은 모듈을 여러번 사용함으로써 weights를 공유함


모듈 서브클래스를 이용하면 쉽게 시행할 수 있음



import random
import torch

class DynamicNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
"""
생성자에서 3개 nn.Linear 인스턴스를 생성하여 forward pass에서 사용할것
:param D_in:
:param H:
:param D_out:
"""
super(DynamicNet, self).__init__()
self.input_linear = torch.nn.Linear(D_in, H)
self.middle_linear = torch.nn.Linear(H, H)
self.output_linear = torch.nn.Linear(H, D_out)

def forward(self, x):
"""
forward 패스에서 0~3까지 골라 그 숫자만큼 중간 레이어를 반복하여 히든레이어를 계산함
각각의 pass가 그래프를 생성하므로 파이썬의 반복문이나 조건문 등의 일반적인 컨트롤 플로우 연산자를
사용하여 모델의 forward 패스를 구현 가능함

또한 computational graph를 생성하는데 있어 같은 모듈을 여러번 사용해도 안전함 LuaTorch 대비 가장 발전한 부분
:param x:
:return:
"""
h_relu = self.input_linear(x).clamp(min=0)
for _ in range(random.randint(0, 3)):
h_relu = self.middle_linear(h_relu).clamp(min=0)
y_pred = self.output_linear(h_relu)
return y_pred

# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10

# Create random Tensors to hold inputs and outputs
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# Construct our model by instantiating the class defined above
model = DynamicNet(D_in, H, D_out)

# 이 이상한 모델을 vanilla stochastic gradient descent로 하기엔 너무 터프하니 momentum 이용
# SGD - stochastic gradient descent
# 최적화 관련 용어 : http://ikaros0909.tistory.com/3
criterion = torch.nn.MSELoss(size_average=False)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9)
for t in range(500):
# Forward pass: Compute predicted y by passing x to the model
y_pred = model(x)

# Compute and print loss
loss = criterion(y_pred, y)
print(t, loss.item())

# Zero gradients, perform a backward pass, and update the weights.
optimizer.zero_grad()
loss.backward()
optimizer.step()




AuthorsSung Kim and Jenny Kang



2개이상의 GPU를 DataParallel을 이용하여 사용하는 법


우선 GPU에 모델을 전달하는 방법은 매우 쉬움


device = torch.device("cuda:0")
model.to(device)


그리고, 모든 텐서를 GPU로 복사


mytensor = my_tensor.to(device)


my_tensor.to(device)는 mytensor를 새로 쓰는게 아니라 값을 복사해서 넘김


따라서 이걸 mytensor라는 새로운 변수에 할당해야하는듯




모델의 학습과정을 여러대의 GPU로 이용하는것은 자연스러워 보이지만 파이토치는 1개만 사용하는것을 디폴트로함


따라서 DataParallel 을 이용하여 여러대의 GPU 로 돌릴 수 있음


model = nn.DataParallel(model)


이게 이번 강의의 핵심




Imports and parameters

모듈 임포트 및 변수 정의


import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader

input_size = 5
output_size = 2

batch_size = 30
data_size = 100

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")


Dummy DataSet

임의의 데이터셋을 만듬. getitem만 추가해주면 됨


class RandomDataset(Dataset):
def __init__(self, size, length):
self.len = length
self.data = torch.randn(length, size)

def __getitem(self, index):
return self.data[index]

del __len__(self):
return self.len

rand_loader = DataLoader(dataset=RandomDataset(input_size, 100), batch_size=batch_size, shuffle=True)



Simple Model

이번 강의에 쓸 모델은 단순히 인풋을 넣으면 선형 연산을 하고 아웃풋을 줌 하지만 CNN, RNN, 등 어떤 모델에서도 DataParallel을 쓸 수 있음


print 메소드는 인풋 아웃풋 텐서 사이즈를 알아보기 위해 넣었음. batch rank 0 일때 뭐가 출력되는지 주의해서 볼것


모델 정의:

class Model(nn.Module):
def __init__(self, input_size, output_size):
super(Model, self).__init__()
self.fc = nn.Linear(input_size, output_size)

def forward(self, input):
output = self.fc(input)
print("\tIn Model: input size", input.size(), "output size", output.size())

return output



Create Model and DataParallel

이번 강의의 핵심 내용. 먼저, 모델 인스턴스를 만들고 GPU가 여러개인지 체크함

여러개라면 DataParallel로 모델을 덮어쓰고 GPU로 보냄


model = Model(input_size, output_size)
if torch.cuda.device_count() > 1:
print("Let's use", torch.cuda.device_count(), "GPUs!")
model = nn.DataParallel(model)

model.to(device)


Run the Model

인풋,아웃풋 텐서들의 사이즈를 볼 수 있음


GPU가 하나라 하지 않았음



+ Recent posts