[딥러닝 알아듣기] 2.2. 딥 러닝 프레임워크

“딥러닝 알아듣기” 시리즈는 딥러닝의 기초 지식을 저만의 방식으로 쉽게 풀어내는 시리즈입니다. 이번 챕터에서는 딥러닝 프레임워크를 사용하는 이유와, 몇 가지 대표적인 프레임워크들을 알아봅니다.


2.2.1. 딥 러닝 프레임워크

딥 러닝이 본격적으로 뜨기 전에는, 기존에 개발되어 있던 수치 계산 및 행렬 연산 라이브러리들을 활용해 머신 러닝이 연구되었다. 모든 행렬 연산과 학습 과정의 코드를 개발자가 직접 작성하여 실행해야 했다. 시간이 흘러 딥 러닝의 학습 방법이 발전하고 딥 러닝을 실제 문제 해결에 도입하려는 시도가 증가하면서, 효율적으로 딥 러닝 모델의 학습부터 배포까지의 과정을 도와줄 수 있는 프레임워크의 중요성이 대두되었다. 이러한 필요에 따라 다양한 프로그래밍 언어와 실행 환경을 목표로 한 수많은 딥 러닝 프레임워크가 탄생했다. 이번 챕터에서는 몇 가지의 대표적인 딥 러닝 프레임워크를 알아보고, 어떤 상황에서 어떤 프레임워크를 이용하면 좋을지 알아보도록 하자.

딥 러닝 프레임워크가 처음부터 모델의 실제 운용(Serving) 까지 염두에 두고 개발되었던 것은 아니었다. 대부분의 딥 러닝 프레임워크들은 처음에 행렬 연산과 미분 연산의 지원으로 수치적인 모델을 만들고 최적의 목적 함수를 찾아주는 기능을 지원하는 소프트웨어 라이브러리로부터 시작했다. 여기서 수치적인 모델을 계산 그래프(Computational Graph) 라고 부른다. 인공 신경망 또한 입력 데이터와 연산의 순서가 정해져 있는 계산 그래프이므로 이 라이브러리들을 사용하면 훨씬 쉽게 딥 러닝 모델을 만들고 분석할 수 있었다. 이후로 딥 러닝의 수요가 늘어남에 따라, 개발자가 다양한 딥 러닝 기술들을 구현하기 쉽게 라이브러리의 기능들이 발전하였다. 최근에는 딥 러닝 라이브러리들이 모델의 학습과 평가 뿐만 아니라, 실제 서비스 환경에 모델을 쉽게 배포할 수 있도록 하는 환경까지 만들어 제공하기 시작하였다. 그 자체로 하나의 딥 러닝 프레임워크 가 된 것이다.

딥 러닝 프레임워크의 가장 중요한 특성은, 개발자가 작성한 동일한 코드를 CPU 뿐만 아니라 GPU를 활용해서도 실행할 수 있게 만들어주는 것이다. 개발자가 딥 러닝 모델을 학습하고 평가하기 위한 코드를 한 번만 작성해놓으면, 실행 환경에 따라 CPU의 연산만 활용할 수도 있고, GPU의 병렬 처리를 활용할 수도 있다. 대부분의 딥 러닝 프레임워크들이 내부적으로 CUDA를 이용하여 GPU 자원을 활용할 수 있게 구현되었기 때문이다. 덕분에 개발자들은 GPU 연산을 이용하기 위한 저수준 제어에 신경쓸 필요 없이 딥 러닝 모델의 학습 자체에 집중할 수 있게 되었다.

구현된 언어나 실행되는 환경, 그리고 목적에 따라 다양한 딥 러닝 프레임워크의 선택지가 존재한다. 특정 프레임워크가 무조건 좋은 것만은 아니다. 대표적인 딥 러닝 프레임워크의 장단점을 같이 살펴보고, 어떤 프레임워크가 어떤 상황에서 유용한지 살펴보자.

2.2.2. Caffe

Caffe는 버클리 AI 리서치(BAIR) 주도로 개발된 오픈소스 딥 러닝 프레임워크이다. 현재 딥 러닝 상용화 단계에서 많이 사용되고 있는 프레임워크로, 빠른 처리 속도를 주된 장점으로 갖는다. 코드가 CUDA C와 호환되도록 작성되어 있어 GPU를 효율적으로 활용할 수 있다.

<그림 1> Caffe

Caffe의 가장 두드러지는 특징은, 직접 코드를 작성하지 않고도 딥 러닝 모델을 구성하고 학습시킬 수 있다는 점이다. Caffe에서 하나의 딥 러닝 모델은 모델의 구조를 담고 있는 prototxt 파일과, 그 구조에 맞는 모델의 모든 파라미터들이 저장된 caffemodel 파일이 동시에 모여 표현된다. 개발자는 먼저 학습할 딥 러닝 모델의 구조 전체를 prototxt에 선언한다. prototxt는 대강 다음과 같이 생겼다.

name: "LeNet"
layer {
  name: "data"
  type: "Input"
  top: "data"
  input_param { shape: { dim: 64 dim: 1 dim: 28 dim: 28 } }
}
layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  convolution_param {
    num_output: 20
    kernel_size: 5
    stride: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}
...
layer {
  name: "prob"
  type: "Softmax"
  bottom: "ip2"
  top: "prob"
}

(출처 : https://github.com/BVLC/caffe/blob/master/examples/mnist/lenet.prototxt)

그 후에, 정의한 모델을 학습시키기 위한 방법을 역시 prototxt 형태로 정의한다. 학습을 위한 prototxt에는 입력 데이터와 정답 데이터의 형태, 손실 함수 계산 방법 등의 정보가 추가로 정의된다. solver 라는 이름으로 정의되는 특별한 prototxt에는 학습률 등의 하이퍼 파라미터들이 정의된다. prototxt의 내용들을 자세히 보고 싶으면 다음의 카페 공식 깃헙 레포지토리 예제를 천천히 구경해보는 것도 좋다.

모델과 학습 방법을 모두 정의한 후, 준비된 학습 / 평가 데이터셋과 prototxt 파일들을 Caffe 프로그램에 넣고 실행시키면 모델의 학습이 시작된다. 모델을 학습하기까지 어떠한 프로그래밍 언어로 작성된 코드도 없다. Caffe 프로그램이 이해할 수 있는 prototxt와, 그것에 적용할 수 있도록 정해진 포맷으로 준비된 데이터셋만 있으면 바로 모델을 학습할 수 있다.

학습된 Caffe 모델은 C++, 파이썬 등으로 구현되어 있는 Caffe 라이브러리를 이용해 실행해볼 수 있다. prototxt와 caffemodel 파일을 동시에 읽어들여서 모델을 메모리에 올려 놓고, 입력 데이터가 들어오면 정의된 모델 구조에 맞춰 계산한 후 사용자가 원하는 이름의 레이어 출력을 돌려준다. 위의 모델 정의 prototxt에 보면, 블럭으로 구분된 각 레이어마다 ‘name’ 이라는 문자열 필드를 찾을 수 있다. 모델을 실행했을 때, 입력 데이터에 따른 모델의 각 레이어별 출력을 ‘name’ 필드의 이름을 통해 가져올 수 있다.

위에서 볼 수 있었다시피 Caffe는 추가적인 프로그래밍 없이 모델을 간편하게 학습시켜볼 수 있다는 것이 장점이다. 또한 모델 학습과 실행의 과정에서 특정 프로그래밍 언어에 대한 의존성이 없기 때문에, Caffe 프레임워크를 설치할 수 있는 어디에서라도 모델을 실행시킬 수 있다. CPU 연산을 이용하는 Caffe는 순수 C++ 언어로 작성된 네이티브 라이브러리이므로 저수준의 최적화에 용이하며, 윈도우즈, 리눅스, 맥, 안드로이드, iOS 등 다양한 운영체제와 기기에서 Caffe만 설치한다면 모두 실행할 수 있다. 아직 많은 기업에서 딥 러닝 모델의 배포와 실행 프레임워크로 Caffe를 선택하고 있는 이유이다.

그러나 역설적으로 Caffe의 이러한 장점들 때문에 오히려 Caffe를 사용하기 어렵게 되었다. 먼저 학습 과정에서 개발자가 추가적으로 코드를 작성할 수 없다는 것은, 학습 과정의 정의가 Caffe가 허용하는 범위 내에서만 가능하다는 것을 의미한다. Caffe에서 지원하는 형태가 아닌 데이터셋은 학습에 사용할 수 없다. Caffe가 지원하는 데이터셋 포맷인 LMDB나 HDF5 형태로 변환해야만 데이터셋을 사용할 수 있다. 이는 오히려 학습 과정의 다양성을 줄이는 원인이 된다.

또한 마찬가지로 Caffe 프레임워크 자체가 지원하는 기능 내에서만 모델의 구조를 정의할 수 있기 때문에, Caffe에 구현되지 않은 레이어나 기능을 사용하려면 개발자가 직접 Caffe 라이브러리에 해당 기능을 개발해 넣어야 한다. 많은 경우 개발자가 직접 Caffe를 수정하고 빌드해 사용해야 한다는 뜻이다. 이는 오히려 개발자가 가지는 코드 작성에 대한 부담감을 늘릴 수 있다. 개발자가 작성해서 빌드한 코드가 기존 코드에 문제를 일으키지 않으면서 최적화되어 돌아가리라는 보장을 할 수 없다. 때문에 매번 새로운 기술을 개발하고 실험해야 하는 연구 목적으로써의 Caffe 프레임워크는 그다지 좋은 선택이 아니다.

2.2.3. TensorFlow

TensorFlow는 구글에서 개발한 오픈 소스 딥 러닝 프레임워크이다. 구글이라는 공룡의 지원을 받아 세계에서 가장 널리 쓰이는 프레임워크로, 전 세계적으로 매우 넓은 사용자 층을 가지고 있는 것이 큰 장점이다. 기본적으로 파이썬 API를 사용하여 모델을 학습시킬 수 있으며, C/C++ 및 Javascript 런타임에서 모델의 실행을 지원한다.

<그림 2> Tensorflow

첫 버전의 TensorFlow는 정적 계산 그래프(Static Computational Graph) 방식의 프레임워크였다. TensorFlow가 설계될 때 Theano와 Caffe같이 정적 그래프를 사용하는 프레임워크의 영향을 크게 받았다. 그래서 두 프레임워크와 같이 모델의 학습이나 추론을 시작하기 전에 모든 모델의 구조가 파이썬 코드로써 미리 정의되어있어야 한다. 모델에 데이터를 입력할 때부터 출력을 얻어낼 때까지의 실행 흐름을 프로그램 실행 중에 변경할 수 없다. 간단한 텐서플로우 코드를 보자.

# 계산 그래프와 손실 함수 정의
W = tf.Variable(tf.random_uniform([1], -1.0, 1.0))
b = tf.Variable(tf.zeros([1]))
Y = W * X + b
loss = tf.reduce_mean(tf.square(Y - label))

# 학습 방법 정의
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

# 계산 그래프 초기화
init = tf.initialize_all_variables()

sess = tf.Session()
sess.run(init)

for step in range(10000):
     sess.run(train)	# 미리 정의된 계산 그래프를 세션에 담아 실행

출력 데이터를 만들어내는 과정을 계산 그래프에 미리 정의하고, 모델의 손실 함수 또한 같이 정의한다. 이렇게 만들어진 계산 그래프는 고정이다. 계산 그래프는 그 실행 주체인 Session에 담겨 실행되므로 실행 중간에 계산 그래프를 변경할 수 없다. 그래서 정적(Static) 그래프 방식이라고 부른다.

정적 그래프 방식의 장단점은 명확하다. 정의된 계산 그래프가 중간에 수정될 일이 없으므로, TensorFlow가 실행시에 알아서 그래프의 계산을 최적화할 수 있다. 실행 중간에 사용자에 의해 계산 그래프가 수정될 수 없음이 보장되기 때문이다. 특히 딥 러닝 모델의 학습에는 모델 실행의 수많은 반복이 필요하므로, 정적 그래프 방식은 실행 속도의 향상을 가져올 가능성이 있다. 그러나 반대로 단점 또한 계산 그래프가 수정될 수 없음에서 생긴다. 파이썬 코드로써 계산 그래프를 정의하지만 정작 파이썬의 제어 구문을 사용할 수 없다. 제어 구문은 모델을 실행하는 시점에서 실행 흐름을 바꿀 수 있기 때문이다. 그래서 첫 버전의 텐서플로우는 파이썬 코드임에도 불구하고 파이썬스럽지 못한(Not pythonic) 코드 구조를 가져갈 수밖에 없었다. 파이썬 언어 자체의 실행 흐름을 따라가지 않으니 디버깅에도 힘이 들었으며, ‘tfdbg’ 라는 TensorFlow 모델 디버깅을 위한 도구를 따로 사용해야 했다.

TensorFlow 개발진에서도 이 문제를 인지하여, ‘Eager execution’ 이라는 이름으로 동적 계산 그래프(Dynamic Computational Graph) 방식을 지원하기 시작했다. TensorFlow 2.0 버전부터 동적 그래프 방식이 기본으로 사용된다. 기존에 TensorFlow는 배우고 적응하기 어렵다는 문제점이 항상 지적되어왔는데, 최근 TensorFlow의 업데이트로 진입 장벽이 많이 낮아진 것으로 보인다.

TensorFlow는 파이썬 API를 이용해 다양한 딥 러닝 모델을 쉽게 구현할 수 있도록 하면서, 다중 GPU의 동시 사용에 대한 지원도 강력해 2010년대 초중반 가장 널리 쓰이는 딥 러닝 프레임워크였다. 다른 파이썬 딥 러닝 프레임워크들보다 상대적으로 빠른 속도가 강점으로, 아직도 딥 러닝 모델의 연구 개발부터 배포까지 폭넓게 사용되는 프레임워크이다. 특히 최근에는 TensorFlow Serving의 꾸준한 업데이트로 실제 환경에 TensorFlow 모델을 사용할 수 있도록 지원이 강화되었다. 또한 Javascript 환경에서 TensorFlow 모델을 실행할 수 있는 API도 제공하면서, 다양한 플랫폼에서 효율적으로 모델을 배포할 수 있도록 진화중인 프레임워크이다.

연구 목적으로는 PyTorch와 같이 훨씬 모델 구현과 디버깅이 쉬운 라이브러리들이 뜨고 있지만, 최근 소프트웨어 배포의 대세 방법론인 클라우드 환경에서의 사용과 실행 속도의 이점을 생각한다면 TensorFlow는 여전히 강력한 선택지이다.

2.2.4. PyTorch

PyTorch는 또 다른 공룡 기업인 Facebook이 강력하게 지원하는 오픈 소스 딥 러닝 프레임워크이다. Lua 언어로 작성된 torch 라이브러리를 기반으로, 파이썬 API를 중점으로 개발되었다. PyTorch는 특히 2018년 이후 전 세계적으로 사용자가 크게 늘고 있으며, 관련 커뮤니티도 급성장하고 있다. 파이썬으로 딥 러닝 모델을 학습시키고 실행할 수 있으며, TorchScript라는 중간 단계의 PyTorch 모델 컴파일러를 사용해 C++로 작성된 저수준의 라이브러리에서도 실행 가능하다.

<그림 3> PyTorch

PyTorch는 기본적으로 동적 계산 그래프 방식으로 모델을 구현한다. 따라서 파이썬의 제어 구문을 모델 실행시에 활용할 수 있다. 다음의 PyTorch 예제 코드를 잠시 살펴보자.

model = torch.nn.Linear(10, 1)
l1_loss = torch.nn.L1Loss()
l2_loss = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.0005)
loss = None

for step in range(10000):
    prediction = model(data)
    if step % 2 == 0:
        loss = l1_loss(prediction, label)
    else:
        loss = l2_loss(prediction, label)
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

예제를 위해 만들어본 가상의 상황이다. 매 반복마다 두 가지의 손실 함수를 번갈아가며 사용하도록 코드를 작성했다. TensorFlow가 손실 함수의 계산 과정까지 모두 그래프상에 미리 정의한 후 반복을 돌렸던 반면에, PyTorch는 매 반복마다 계산 그래프를 새로 정의한다. 정확히 말하면 매 반복마다 파이썬 코드의 흐름에 따라 계산 그래프를 직접 만들며 실행하는 동적 계산 그래프 방식이다. 그래서 코드에서 보다시피 계산 그래프의 정의에 if문과 같은 파이썬의 제어 구문을 바로 활용할 수 있다. 이는 개발자로 하여금 더욱 직관적인 코드 설계와 쉬운 디버깅을 가능하게 한다. 또한 더 복잡하고 유연한 모델을 비교적 쉽게 구현할 수 있다는 장점이 있다.

그래서 특히 2018년부터 PyTorch가 대세 딥 러닝 프레임워크로 떠오르기 시작했다. 기존의 강호였던 TensorFlow에 비해 코드의 진입 장벽이 낮아 사용자가 크게 늘었으며, 점점 복잡해지는 새로운 딥 러닝 모델들을 학습하고 분석하기에 더 용이한 프레임워크이기 때문이다. 파이썬 언어 자체의 강력함을 활용하기 쉬운 것도 큰 장점으로 떠올랐다. 그래서 최근 초보자부터 현업의 전문가들까지, PyTorch 사용자 커뮤니티가 매우 활발해지고 있다. 프레임워크의 사용자 커뮤니티가 크고 활발하다는 점도 프레임워크의 선택에 있어 중요한 사항이 된다. 딥 러닝을 공부하는 사람들에게는 특히 더 그렇다.

딥 러닝 개발자들 사이에서 가장 뜨거운 논쟁거리 중 하나가 ‘TensorFlow와 PyTorch 중 무엇이 더 좋은가’ 하는 문제이다. 앞서 보았다시피 TensorFlow는 사용이 상대적으로 어려운 대신 배포와 최적화 면에서 더 강력하며, PyTorch는 상대적으로 사용이 쉬운 대신 모델의 배포 면에서 더 약하다. 프레임워크 선택에 정답은 없고, 최고로 좋은 프레임워크라는 것도 없다. 상황과 목적에 따라 더 적절한 프레임워크를 선택하면 된다. 이 시리즈에서는 PyTorch를 사용해 모든 내용을 진행하겠다. 딥 러닝을 어느 정도 공부하고 나면 다른 프레임워크에 익숙해지는 것도 어렵지 않다. 프로그래밍 언어를 배울수록 새로운 언어를 배우기 쉬워지는 것과 마찬가지다.

2.2.5. 모든 프레임워크를 아우르기 위한 노력, ONNX

마지막으로 최근 활발히 개발이 진행되고 있는 ONNX에 대해서 간략히 소개하고 챕터를 마치겠다. 앞서 여러 딥 러닝 프레임워크들이 각자 명확한 장단점을 가지고 있음을 보았다. 딥 러닝을 연구하거나 딥 러닝으로 제품을 개발하면서, 프레임워크의 상대적인 한계 때문에 기대한 결과를 보지 못하면 안 될 것이다. 그래서 서로 다른 딥 러닝 프레임워크 간의 호환 문제가 오래전부터 제기되었다. 예를 들면, PyTorch로 연구 분석해서 만든 딥 러닝 모델을 최적화에 강한 Caffe 프레임워크를 통해 모바일 디바이스에서 실행할 수 있다면 좋을 것이다. 서로 다른 프레임워크를 사용하는 개발자들 간의 소통이 힘들다는 것도 큰 문제였다.

<그림 4> ONNX

그런 고민의 결과로 ONNX(Open Neural Network eXchange) 가 등장했다. ONNX의 목표는 모든 딥 러닝 모델을 표현할 수 있는 하나의 통일된 표현 방식을 만드는 것이다. 각각의 프레임워크에서 이 통일된 표현 방식을 이해하여 모델을 실행시킬수만 있으면, 프레임워크에 상관없이 어디서든 딥 러닝 모델을 학습시키고 실행시킬 수 있을 것이다. 전 세계 다양한 기업의 지원을 바탕으로 오픈 소스로 만들어나가고 있다.

위에서 보았던 Caffe, TensorFlow, PyTorch의 모델을 모두 ONNX로 변환할 수 있고, 이외에도 수많은 프레임워크에서 이 모델을 실행할 수 있다. PyTorch로 개발한 모델을 모바일 디바이스의 Caffe로 실행하는 과정이 훨씬 간편해진 것이다.