Stop Thinking, Just Do!

Sungsoo Kim's Blog

TensorFlow Introduction

tagsTags

20 June 2016


Article Source


소개

2011년에 구글 브레인 팀이 조직되어 딥 뉴럴 네트워크(deep neural network)에 대한 연구를 시작하였고 딥 뉴럴 네트워크 시스템인 디스트빌리프(DistBelief)를 개발했습니다. 구글은 디스트빌리프를 비감독학습(unsupervised learning), 언어[1, 2], 이미지인식[1, 2], 비디오분류, 음성인식[1, 2, 3], 보행자감지, 바둑, 시퀀스예측, 강화학습(reinforcement learning)과 그외[1, 2] 여러 분야에 사용해왔습니다. 구글 안의 50개가 넘는 팀과 알파벳 자회사들이 디스트빌리프를 이용해 다양한 제품에 딥 뉴럴 네트워크를 적용해왔습니다. 여기에는 검색, 광고, 음성인식시스템[1, 2, 3], 구글포토, 지도, 스트리트뷰, 번역, 유투브 등이 포함됩니다. 이러한 경험을 바탕으로 차세대 대규모 머신러닝 시스템인 텐서플로우(TensorFlow)를 개발했습니다.

텐서플로우는 상태 정보를 가지는 데이터플로우(dataflow) 그래프로 컴퓨터의 계산을 표현합니다. 안드로이드, iOS 같은 모바일 환경에서 추론(inference) 시스템을 만들수도 있고 한개 또는 여러개의 GPU를 가진 단일 서버에서는 중간 규모의 훈련(training)과 추론 시스템을 구축할 수 있습니다. 또는 수천개의 GPU가 탑재된 수백대의 서버에서 운영될 수도 있습니다. 텐서플로우를 이용하면 학습 시스템은 대규모로 하면서 실제 서비스는 소형화하여 운영할 수 있습니다.

텐서플로우는 매우 빠른 성능과 유연한 구조를 가지고 있어 새로운 모델을 빠르게 실험해 볼 수 있고 실제 서비스에도 안정적으로 사용할 수 있습니다. 디스트빌리프와 비교해 볼 때 텐서플로우의 프로그래밍 방식이 더 유연하고 성능은 더 뛰어납니다. 구글 내부의 여러 팀들이 이미 디스트빌리프에서 텐서플로우로 전환했습니다. 텐서플로우는 딥 뉴럴 네트워크를 만드는 데 사용되는 것이 일반적이지만 다른 머신러닝 알고리즘을 적용하거나 수치 계산 용으로도 사용할 수 있어서 다양한 산업 분야에 폭 넓게 활용될 수 있습니다. 텐서플로우는 2015년 11월 아파치 2.0 오픈소스 라이센스로 공개되었고 www.tensorflow.org에서 다운받을 수 있습니다.

프로그래밍 모델과 기초 개념

텐서플로우의 데이터플로우 그래프는 연산(operation)을 표현하는 노드(node)와 노드 사이의 데이터 흐름을 나타내는 엣지(edge)로 구성되어 있는 유향 그래프(directed graph, 방향성이 있는 그래프)로 상태 정보를 유지, 갱신하고 분기나 반복 제어를 위한 노드를 가지고 있습니다. 이 그래프는 C++이나 Python 프로그램을 이용하여 만들 수 있습니다. 아래 그림은 Python 코드(그림1)와 이를 통해 만들어진 그래프(그림2)의 예입니다.


그림1: 파이썬 코드 예제 (출처: 텐서플로우 페이퍼)


그림2: 계산 그래프 (출처: 텐서플로우 페이퍼)

텐서플로우의 그래프는 연산 노드외에 상태를 유지, 갱신하는 노드나 분기, 루프를 구성하는 제어 노드 등의 확장된 노드 타입을 가지고 있습니다. 연산 노드는 입력과 출력을 가질 수 있으며 엣지를 따라 각 노드들 사이들 이동하는 값을 텐서(Tensor)라고 하는데 일종의 다차원 배열입니다. 의존성 제어(control dependencies) 엣지는 데이터가 흐르지 않는 엣지로 이전 노드가 실행을 마쳐야만 다음 노드가 실행될 수 있도록 조절하는 엣지입니다. 의존성 제어 엣지는 연산의 우선순위를 지정하는데 사용할 수 있고 때로는 메모리 사용량을 조절하기 위해 관련 없는 연산 사이에서도 사용됩니다.

연산은 행렬곱셈, 덧셈 등을 나타내는 추상 계층으로 속성(attribute)을 가질 수 있고 그래프가 구성될 때 필요한 속성이 제공되어야 합니다. 커널은 연산의 기능이 각 디바이스(CPU 또는 GPU)에 맞게 구현된 것입니다. 연산과 커널은 추가 확장이 가능하며 아래는 텐서플로우에서 기본적으로 제공하는 연산의 일부 목록입니다.(사용자들이 제안한 새로운 연산 기능이 버전이 업그레이드되면서 추가되고 있습니다)

테이블1: 텐서플로우 연산함수 (출처: 텐서플로우 페이퍼)

클라이언트 프로그램은 세션(Session)을 통해 텐서플로우 시스템과 통신합니다. 세션은 현재 그래프에 노드와 엣지를 추가할 수 있는 Extend 메소드를 가지고 있고 Run 메소드는 노드의 의존성을 따라 순서대로 그래프 계산을 수행합니다. 보통 한 세션에서 그래프는 한번 구성되고 수천에서 수백만번 실행됩니다. 그래프 계산이 반복될 때 일반 텐서는 유지되지 않지만 Variable 메소드로 만들어진 텐서는 그래프 실행 후에도 유지되어 머신러닝의 모델 파라메타를 저장하고 업데이트하는데 주로 사용됩니다.

구현

텐서플로우의 주요 컴포넌트는 마스터(master) 프로세스, 세션 인터페이스를 통해 마스터 프로세스와 통신하는 클라이언트(client) 프로세스, 그리고 하나 이상의 워커(worker) 프로세스 입니다. 워커 프로세스는 CPU나 GPU에서 연산을 실행시키는 역할을 하며 마스터 프로세스는 워커 프로세스에게 연산을 할당합니다. 텐서플로우 로컬(local, 단일서버) 버전에서는 클라이언트, 마스터, 워커 프로세스가 서버의 한 프로세스 안에서 모두 작동되지만 분산(distributed) 버전에서는 클라이언트, 마스터, 워커가 각기 다른 프로세스(아래 그림에서 굵은 실선)로 다른 서버에서 실행될 수 있습니다. 아래 그림에서 두 경우의 예를 비교해서 나타내었습니다.


그림3: 단일 서버와 분산 시스템의 구조 (출처: 텐서플로우 페이퍼)

하나의 워커는 디바이스 한개 이상을 담당합니다. 디바이스는 타입과 이름을 가지고 있고 이름은 타입과 워커에서 부여한 일련번호로 구성됩니다. 분산 환경에서는 잡(job) 아이디와 워커의 태스크(task)가 추가 됩니다(로컬환경에서는 localhost로 대신합니다). 디바이스 이름의 예는 로컬 단일 서버에서는 “/job:localhost/device:cpu:0“과 같고 분산 환경에서는 “/job:worker/task:17/device:gpu:3“과 같습니다. CPU와 GPU에 대한 디바이스 인터페이스를 구현했으며 이 외에도 새로운 디바이스 타입을 추가할 수 있습니다(아마 TPU를 염두해 둔 언급인 것 같습니다). 각 디바이스 오브젝트는 디바이스 메모리의 할당, 해제를 책임지고 있고 커널 실행을 도와 줍니다.

텐서는 형이 있는 다차원 배열로 8비트(아마도 TPU를 위해)~64비트 정수, 부동소숫점, 배정도 실수(double), 복소수, 문자열 등을 지원합니다. 백킹 스토어(backing store) 버퍼는 텐서가 위치한 디바이스에 만들어지며 텐서의 참조 횟수를 카운트 합니다. 참조 횟수가 0이 되면 버퍼도 삭제됩니다.

단일 디바이스를 가진 하나의 워커 프로세스라면 단순하게 그래프 노드 간의 의존성에 따라 순서대로 노드가 실행됩니다. 노드마다 선행되어야 하지만 아직 실행되지 않은 노드의 수를 카운트하고 있습니다. 이 카운트가 0이 되면 이 노드는 실행될 준비가 된 것입니다. 노드에 대한 커널 실행은 디바이스 오브젝트에게 위임되고 실행이 마치면 이 노드에게 의존성을 가지고 있는 다른 모든 노드의 카운트 값을 1씩 감소시킵니다.

여러개의 디바이스가 있을 때는 어느 디바이스에서 노드의 계산을 담당할 것인지와 디바이스 간에 데이터 이동을 관리하는 것이 어려운 문제입니다. 배치(placement) 알고리즘은 노드의 입력, 출력 텐서의 사이즈와 계산에 필요한 시간을 예측하는 코스트(cost) 모델을 사용하여 연산 노드를 가능한 디바이스에 적절히 배치합니다. 이 코스트 모델은 연산의 종류에 따라 정적으로 계산되거나 이전 노드의 배치와 실행 결과를 기반으로 측정됩니다.

배치 알고리즘은 그래프 실행을 시뮬레이션 하여 노드에 적절한 디바이스를 찾습니다. 즉 실행될 순서대로 각 노드가 디바이스에서 연산을 처리할 때 걸리는 시간과 노드 입력값을 디바이스로 전송하는 시간을 예측하여 가장 빠르게 작업이 끝날 수 있는 디바이스를 선택하게 됩니다. 텐서플로우는 사용자가 그래프의 일부분을 특정 디바이스에서 실행시킬 수 있는 기능을 제공합니다. 배치 알고리즘 영역은 계속해서 개선이 될 것입니다.

노드가 어디에 배치될 지 계산하면 디바이스 별로 그래프를 나누어 서브 그래프로 분리합니다. 예를 들어 서브 그래프로 분리할 때 x 노드에서 y 노드로 디바이스를 가로지르는 엣지가 있다면 그 엣지를 삭제하고 대신 x 노드 다음에 Send 노드를 연결하고 y 노드 이전에 Receive 노드를 추가합니다. 아래 그림4를 참고하세요.


그림4: Send/Receive 노드 추가 전과 후 (출처: 텐서플로우 페이퍼)

Send 노드와 Receive 노드는 디바이스 간의 데이터 전송을 담당합니다. 노드 하나의 출력이 다른 디바이스의 두개의 노드에 입력으로 사용될 경우 두개의 Send/Receive 노드를 만들지 않고 하나로 통일하여 데이터는 한번만 전송됩니다(그림4에서 b,c 노드의 경우). 이런 기능은 텐서를 받는 디바이스에서 한번만 메모리를 할당할 수 있도록 해줍니다. 이런식으로 Send/Receive 노드가 워커와 디바이스 간의 동기화를 담당해 주므로 서브 그래프의 노드 스케줄링은 각 워커 프로세스로 위임되고 마스터 프로세스는 각 워커마다 그래프를 실행시키기 위해 Run 메소드를 한번씩만 호출하면 됩니다.

분산 환경에서도 이와 유사하게 Send/Receive 노드가 TCP나 RDMA 프로토콜을 사용하여 서버간의 데이터 전송을 담당합니다. 분산 환경에서는 Send/Receive 노드 사이의 통신에서 에러가 발생할 수 있고 마스터 프로세스가 각 워커 프로세스의 상태를 주기적으로 체크하여 에러를 감지합니다. 에러가 발생되면 전체 그래프 실행이 중지되고 처음부터 다시 실행됩니다. Variable 노드는 그래프 실행이 종료되어도 텐서를 유지할 수 있어 재 시작할 때 원래 값으로 복구시킬 수 있습니다. Variable 노드는 Save 노드에 연결되어 있는데 Save 노드는 N초 마다 혹은 N번의 반복마다 실행되어 Variable 노드의 값을 디스크로 저장합니다. 또 Variable 노드는 그래프가 재 시작될 때 딱 한번 실행되는 Restore 노드와도 연결되어 있습니다.

고급 기능

많은 최적화 알고리즘은 코스트 함수의 기울기(gradient)를 계산하는 것이 필요로 해서  텐서플로우는 기울기 계산을 자동으로 해주는 기능을 내장하고 있습니다. 텐서플로우는 텐서 C가 의존하고 있는 텐서 I에 대한 C의 변화율(기울기)를 계산하기 위해 다음과 같은 과정을 따릅니다. 먼저 I에서 C로의 그래프 경로를 찾은 후 체인룰(Chain Rule)을 사용해 편미분을 계산하기 위해 C에서 I로 역방향으로 되짚어 가면서 노드들을 차례대로 새 그래프에 추가합니다. 추가된 노드의 기울기 함수는 이전 노드에서 계산한 기울기 값과 선택적으로 정방향 그래프(원본 그래프)의 입출력 값을 사용할 수 있습니다. 아래 그림5는 그림2의 예를 이용해 기울기 계산을 위한 과정을 보여 줍니다. 회색 화살표는 기울기 함수에서 사용할 수도 있는 입력값을 표시한 것입니다.


그림5: 그림2의 기울기 계산 그래프 (출처: 텐서플로우 페이퍼)

그리고 그림1에 포함될 부분은 아래와 같은 코드가 될 것 입니다.

[db, dW, dx] = tf.gradients(C, [b,W,x])

어떤 연산 노드 O의 출력이 여러개(y_1, y_2)이고 C가 그중 일부에만 의존성이 있을 경우 dC/dy_1 = 0 이므로 O의 기울기 함수에서 y_1에 대응되는 입력 파라메타는 0이 됩니다.

기울기 자동 계산 기능은 메모리 관리 부분에서 특히 최적화하기 어렵습니다. 정방향 그래프 계산의 경우 이전 노드의 출력이 다음 노드에서 바로 사용되므로 메모리를 재사용하기에 좋습니다. 또 그래프의 구조가 효율적이지 않을 경우엔 그래프 순서를 변경하거나 의존성 제어 엣지를 사용할 수도 있습니다. 그러나 기울기 계산을 위해 노드가 자동으로 그래프에 추가될 때에는 사용자가 간섭하기가 어렵습니다. 기울기를 위한 그래프는 정방향 그래프의 반대 순서라 정방향 그래프의 초기에 사용된 텐서가 종종 기울기 그래프의 마지막 부분에 필요하게 되곤 합니다. 그런 텐서는 GPU 메모리 부족을 초래하고 계산 능력을 저하시키게 됩니다. 우리는 이런 부분의 메모리 관리 능력을 향상시키려고 많은 노력을 기울이고 있습니다. 대안으로는 좀 더 정교한 그래프 배치 알고리즘을 사용하거나 텐서를 메모리에 두지 않고 필요할 때 재 생성하거나 오래 보관해야할 텐서를 GPU에서 CPU 메모리로 옮겨 놓는 것 등이 있습니다.

종종 그래프 전체가 아니라 그래프 일부분만 실행 시키고자 할 때가 있습니다. Run 메소드에서 그래프의 특정 일부분을 실행시킬 수 있으며 그래프의 지정된 엣지에 특정 데이터를 주입할 수도 있고 엣지로 부터 데이터를 추출할 수 있습니다. 그래프의 각 노드는 이름을 가지고 있고 출력은 노드의 이름과 0에서 부터 시작되는 출력 포트로 구분할 수 있습니다(예를 들면 “bar:0“은 bar 노드의 첫번째 출력이고 “bar:1“은 2번째 출력 값입니다).

Run 호출을 할 때 두개의 파라메타를 사용하여 서브 그래프를 지정합니다. 첫번째는 name:port 이름으로 매핑할 수 있는 입력 텐서이고 두번째 outpus_names 파라메타에는 출력값 name[:port], 즉 실행시킬 서브 그래프의 출력 노드를 나열합니다. 포트가 지정되면 지정된 출력 텐서가 Run 호출이 완료될 때 클라이언트 프로그램으로 리턴됩니다.

입력과 출력에 맞춰 그래프가 변경되어 node:port에 지정된 입력값이 feed 노드로 바뀝니다. 그리고 포트가 지정된 출력은 fetch 노드에 연결되어 Run 호출이 완료되면 클라이언트 프로그램에게 출력결과 텐서를 되돌려 줍니다. feed, fetch 노드가 삽입되면 전체 그래프에서 출력값을 얻기위해 계산되어야 할 노드를 결정하고 그래프를 재 구성합니다. 그림6에서 왼쪽이 원래 그래프이고 오른쪽은 입력을 {b}, 출력은 {f:0}로 Run 메소드를 호출했을 때 변경된 그래프입니다. 노드 f를 계산하기 위해서 c, a 만 계산하면 되므로 d, e 노드는 제외되었습니다.


그림6: 그래프 변환 전후 (출처: 텐서플로우 페이퍼)

텐서플로우는 특정 노드를 실행시킬 디바이스를 지정할 수 있는 기능을 제공합니다. 예를 들면 “이 노드는 GPU에만 배치한다” 또는 “이 노드는 /job:worker/task:17에 있는 디바이스 중 하나에 배치한다” 또는 “이 노드는 Variable13 노드와 함께 배치한다” 등이 가능합니다. 이런 기능을 제공하려면 앞서 이야기한 배치 알고리즘이 조금 변경됩니다. 먼저 각 노드에 배치 가능한 디바이스들을 찾고 반드시 같이 배치될 노드들의 디바이스 중 겹치는 디바이스가 그 노드들의 배치 가능한 디바이스가 됩니다.

텐서플로우는 머신러닝 알고리즘을 효과적으로 표현하기 위해 조건 분기와 반복 기능을 제공합니다. SwitchMerge 연산자는 불리언 텐서의 값에 따라 서브 그래프의 실행을 건너 뛸 수 있습니다. Enter, Leave, NextIteration 연산자를 이용해서 반복 루프를 만들 수도 있습니다. MIT 태그드-토큰 머신(MIT Tagged-Token Machine)과 유사하게 텐서플로우에서 루프의 매 반복은 태그로 구분이 되고 실행 상태는 프레임으로 표현됩니다. 입력값이 준비되면 언제든지 루프 반복이 실행될 수 있으므로 동시에 여러 반복이 실행될 수도 있습니다.

이런 조건 제어 기능은 분산 환경에서도 가능합니다. 일반적으로 루프는 여러개의 디바이스에 배치된 노드를 포함할 수 있습니다. 그렇기 때문에 루프의 종료 상태를 결정하는 것이 중요한 문제가 됩니다. 텐서플로우는 그래프가 디바이스별로 나뉘어질 때 제어 노드를 나뉘어진 그래프마다 모두 추가합니다. 이 제어 노드는 작은 상태 머신과 같은 것으로 반복을 시작하고 끝내는 것을 관장하며 루프의 종료를 결정합니다. 루프 종료 노드를 가진 디바이스는 매 반복마다 루프에 참여한 디바이스에게 제어 메세지를 보냅니다.

그래디언트 디센트(gradient descent) 방법을 사용하는 머신러닝 모델을 훈련시킬 때 데이터플로우 그래프에 흐름 제어(control-flow, if 나 while) 연산이 포함되어 있으면 이를 고려하여 기울기가 계산되어야 합니다. 즉 if 조건일 경우 어떤 조건이 선택되었는지 기억하여야 하고 while 루프는 몇번 반복이 되었는지와 반복마다 생성된 임시 값들을 기억하고 있어야 합니다. 텐서플로우는 그래프를 수정하여 기울기 계산에 필요한 이런 값들을 기억할 수 있도록 만듭니다.

대규모의 머신러닝 시스템이라면 입력 데이터를 노드에 직접 주입하는 것 외에 특수한 입력노드를 사용하여 파일로 부터 필요한 만큼 배치 데이터를 읽어들일 수 있습니다. 클라이언트 프로세스가 워커 프로세스랑 분리되어 있는 환경에서는 입력 데이터가 저장 장치에서 클라이언트 프로세스로 이동한 후 워커 프로세스로 전달되어 네트워크를 두번 거치치만 입력 노드를 사용할 경우 데이터는 저장 장치에서 워커 프로세스로 직접 전달 됩니다.

텐서플로우의 큐는 EnqueueDequeue 연산을 사용하여 그래프의 일부분을 비동기적으로 실행될 수 있게 해줍니다. Enqueue 연산은 큐에 여유가 있을 때까지 멈추게되고 Dequeue 연산은 큐에 최소한의 데이터가 들어 있을 때까지 멈추게됩니다. 큐의 예로는 이전 배치 데이터가 처리되고 있는 동안 디스크 파일로 부터 입력 데이터를 읽는 경우나, 복잡한 기울기 조합을 계산하기 위해 많은 기울기 값을 누적하거나, 리커런트 랭기지(recurrent language) 모델에서 효율적인 처리를 위해 여러 입력 문장들을 비슷한 길이끼리 묶음으로 만드는 등의 그룹핑 작업에 사용됩니다. 이런 FIFO 큐 외에 무작위 순서로 샘플 데이터를 처리해야되는 머신러닝 알고리즘을 위해 메모리 버퍼에서 엘리먼트를 랜덤하게 섞는 셔플링(shuffling) 큐도 있습니다.

컨테이너(container)는 텐서플로우 안에서 오랜기간 지속되는(long-lived) 상태를 관리하데 사용됩니다. Variable을 위한 백킹 스토어가 컨테이너 안에 있습니다. 디폴트 컨테이너는 자동으로 생성되어 프로세스가 종료될 때까지 유지되지만 우리가 추가로 만들 수도 있습니다. 컨테이너를 사용하면 다른 세션에서 나누어 실행되는 그래프끼리도 상태를 공유할 수 있습니다.

최적화

클라이언트 프로그램에서 만들어진 그래프는 종종 동일한 계산을 하는 중복 부분이 발생합니다. Click의 알고리즘과 유사하게 동일한 입력과 출력을 가지는 중복된 연산의 엣지를 이 중 하나의 노드를 택해 이 노드로 향하게 함으로써 공통된 부분을 단일화 시키는 공통 부분식 경로(common subexpression pass)를 만들었습니다. 정교한 스케줄링은 시스템의 성능을 높입니다. 메모리 고갈을 일으키지 않도록 연산간에 메모리에 유지되어야 할 임시 결과 값들이 가장 짧은 시간동안 유지되도록 스케줄링 되어야 합니다. 거기에 더불어 디바이스 간의 데이터 이동을 줄여서 네트워크 병목 현상을 피할 수 있어야 합니다. 사전 조치가 없다면 Receive 노드는 그래프가 실행되자마자 필요이상 일찍 값을 읽으려 시도할 것입니다. ASAP/ALAP(as soon as possible/as late as possible) 비율을 계산하여 최선의 스케줄링 타임을 찾아 언제 Receive 노드를 시작할지 결정합니다. 그리고 결과값이 필요할 때까지 이 노드를 실행하지 않고 지연시킬 수 있도록 제어 엣지를 추가합니다.

Compute 메소드의 끝까지 실행을 지속시키는 동기 커널 이외에도 비동기(non-blocking) 커널도 지원합니다. 비동기 커널은 많은 스레드가 동작하는 것이 메모리 등의 자원 부분에 비효율적인 환경에서 주요합니다. I/O나 다른 이벤트를 기다리는 시간동안 스레드를 묶어 두는 상황을 피할 수 있기 때문입니다. 비동기 커널의 예로는 Receive, Enqueue, Dequeue 커널이 있습니다.

텐서플로우는 여러가지 잘 최적화된 수학 라이브러리를 사용합니다. 형렬 곱셈을 위해서 BLAS, cuBLAS를 사용하고 딥 뉴럴 네트워크의 콘볼루션 커널을 위한 GPU 라이브러리로는 cuda_convnet, cuDDN 을 사용합니다. 또 오픈소스 Eigen 선형대수 라이브러리를 사용하며 임의의 차원을 갖는 텐서 연산을 위해 이 라이브러리를 일부 확장하여 사용합니다.

뉴럴 네트워크 같은 일부 머신러닝 알고리즘은 오차나 계산 정밀도에 관대한 편입니다. 디스트빌리프 시스템처럼 텐서플로우는 디바이스간에 데이터를 전송할 때 손실 압축을 사용합니다. 예를 들어 특별한 전환(conversion) 노드를 추가해서 32비트 부동 소수점 수를 16비트 부동 소수점으로 바꿉니다(IEEE 16비트 부동 소수점 구조가 아니고 32비트 부동 소수점 구조에서 가수부분 16비트를 버리는 것입니다). 그리고 난 후 다시 데이터를 받은 쪽에서 32비트로 복원합니다(단순히 버려졌던 16비트 부분을 0으로 채웁니다).

현재 상황과 경험

텐서플로우는 아파치 2.0 라이센스로 오픈 소스화 되어 http://www.tensorflow.org에서 다운받을 수 있고 문서와 튜토리얼, 예제를 포함하고 있습니다. 포함된 예제에는 MNIST 데이터셋을 이용한 손글씨 인식, CIFAR-10 데이터셋을 이용한 이미지 분류, 리커런트 LSTM을 이용한 언어 모델링, 워드 임베딩 벡터(word embedding vector) 등이 있습니다. Python, C++ 인터페이스를 포함하고 있으며 앞으로 다른 언어로도 확장되기를 기대하고 있습니다.

디스트 빌리프에서 텐서플로우로 구글내의 많은 머신러닝 모델이 이전되었습니다. 여기서는 머신러닝 모델을 마이그레이션 하면서 주의해야 할 점과 경험에 대해 소개합니다. 특별히 이미지 인식을 위한 콘볼루션 네트워크의 정수로 불리는 인셉션(Inception) 모델을 포팅(porting)하는 것에 초점을 맞춰 설명합니다. 이 이미지 인식 문제는 224×224 픽셀 이미지를 1000개의 레이블 중 하나로 분류하는 것으로 텐서플로우 그래프로 나타내면 13.6백만개의 파라메타와 36,000개의 연산이 있습니다. 만들어진 모델로 하나의 이미지를 추론하는 데는 20억개의 곱셈과 덧셈이 필요합니다.

올바른 그래프가 만들어졌는지 36,000개의 연산을 검증하고 디버깅하는 것은 쉬운 일이 아닙니다. 특별히 이 시스템은 확률적인 성질(SGD, stochastic gradient descent)을 가지고 있어 검증하기 더욱 어렵습니다. 이런 환경을 감안할때 인셉션 모델을 텐서플로우로 포팅하는 데 중요한 전략은 아래와 같습니다.

  1. 만들어진 모델의 파라메타 수가 정확한지 조사할 수 있는 툴을 만듭니다. 이런 툴은 복잡한 모델에서 찾기 어려운 문제점을 발견하고 특별히 차원을 따라 연산이 브로드캐스팅(broadcasting)될 때 잘못 만들어진 연산이나 변수를 확인할 수 있습니다.
  2. 작게 시작해서 점차 확대합니다. 처음 포팅한 모델은 CIFAR-10 데이터셋을 이용한 비교적 작은 모델이었습니다. 작은 모델을 디버깅하여 개개의 연산의 버그를 수정한 후 좀 더 복잡한 모델을 포팅하는 것이 수월합니다.
  3. 두 시스템에서 학습이 끝났을 때 코스트 함수(loss function) 값이 같은 지를 확인합니다. 학습속도를 0으로 하면 랜덤하게 초기화된 값이 어떤 예상치 못한 영향을 미치는지 조사할 수 있습니다.
  4. 단일 서버 환경에서 포팅을 하고 난 후 분산 버전을 포팅합니다. 이 방식은 머신러닝 시스템 간의 학습 성능 차이를 확인하는 데 좋습니다. 특히 경쟁 상태(race condition)이나 원자성이 없는(non-atomic) 연산에서 버그를 찾을 수 있습니다.
  5. 수치 오류를 예방합니다. 수치 라이브러리들은 무한 소수를 다룰 때 일관적이지 않습니다. 콘볼루션 네트워크는 특별히 수치적으로 불안정한 것에 민감하여 실험할 때와 디버깅할 때 크게 달라지는 경향이 있습니다. 무한 소수를 체크하여 이런 현상을 예방하면 실시간 에러에 대비할 수 있습니다.
  6. 그래프의 일부분을 분석해서 수치 오류의 크기를 가늠합니다. 뉴럴 네트워크를 서브 그래프로 나누어 두대의 서버에서 실행하면 두 시스템의 수학 알고리즘이 동일한 것을 확인할 수 있습니다. 부동 소수점 연산을 가진 알고리즘의 경우 예상되는 수치 오류의 기준을 정하여 커널이 정확하게 구현되었는지를 판단합니다.

확률을 기반으로 한 시스템에서 복잡한 수치 연산을 검증하는 일은 매우 어렵습니다. 위와 같은 노력으로 시스템에 대한 신뢰도를 높였으며 모델을 학습하는 데 디스트빌리프 대비 6배나 빠른 성능 향상을 가져왔습니다.

프로그래밍 문제

텐서플로우를 이용해 여러가지 머신러닝 모델을 만들 수 있지만 특별히 대용량 데이터셋을 이용한 뉴럴 네트워크를 훈련시킬 때 속도를 높이는 방법에 대해 살펴보겠습니다. 여기서는 일반적으로 많이 사용하는 100~1000개 사이즈의 미니 배치(mini-batch) 데이터를 확률기반 그래디언트 디센트(stochastic gradient descent, SGD) 방식을 사용하여 훈련시키는 모델을 가정합니다.

SGD 속도를 높이기 위해 간단한 방법은 1000개의 미니 배치 데이터를 10개로 나누어 100개씩 동시에 학습시킨 후 기울기를 합치고 모델 파라메타를 한꺼번에 업데이트하는 것입니다. 이를 위해 그래프의 일부분을 여러개로 복제하고 하나의 클라이언트 스레드가 전체 훈련 과정의 루프를 관장하게 합니다. 그림7의 윗부분 그림이 이 방식을 나타내고 있습니다.


그림7: 동기 방식과 비동기 방식으로 데이터를 병렬화하여 훈련 (출처: 텐서플로우 페이퍼)

이 방법을 비동기적으로 수행할 수도 있습니다. 이 때는 모델의 복제본마다 클라이언트 스레드가 하나씩 할당되어 모델 파라메타를 각자 업데이트 할 수 있습니다. 그림7의 아래 부분의 그림입니다.

모델 병렬 학습(model parallel training)은 모델의 각기 다른 부분을 여러개의 디바이스에 할당하여 같은 배치 데이터로 동시에 학습시키는 것으로 텐서플로우에서 쉽게 구현할 수 있습니다. 그림8은 시퀀스-시퀀스 학습을 위해 리커런트(recurrent) 딥 LSTM 모델을 세개의 디바이스에서 병렬로 학습시키는 모습을 보여줍니다.


그림8: 모델을 병렬화하여 훈련 (출처: 텐서플로우 페이퍼)


그림9: 한 디바이스내에서 병렬화하여 훈련 (출처: 텐서플로우 페이퍼)

하나의 디바이스에서 모델의 훈련 단계를 조금씩 나누어 병렬로 학습시키는 방법은 그림9에서 볼수 있듯이 하나의 디바이스 안에서 일어난다는 것을 제외하고는 데이터를 병렬화하여 비동기적으로 업데이트하는 방식과 유사합니다. 이 방법은 디바이스에 나누어 병렬로 학습시키는 것으로 성능을 만족시킬 수 없을 경우 사용될 수 있습니다.

성능

단일 서버와 분산 환경에서의 성능 자료는 추후 공개합니다.

도구

텐서플로우 그래프의 구조와 머신러닝 모델의 작동 방식을 이해하는 것을 돕기 위해 텐서플로우에는 텐서보드(TensorBoard)라는 시각화 도구가 함께 제공됩니다. 인셉션 같은 모델의 경우 36,000개의 노드를 갖는 그래프이며 몇몇 딥 리커런트 LSTM 모델은 15,000개의 노드를 가지고 있습니다. 텐서보드는 노드를 묶음으로 표시하고 동일 구조를 가지는 그룹을 하이라이팅해 줍니다. 또  매우 복잡한 노드는 화면의 분리된 영역에 표시해서 그래프의 중요한 부분에 집중할 수 있도록 도와 줍니다. 사용자는 인터랙티브하게 확대, 축소를 할 수 있고 그룹핑된 노드를 안으로 더 상세하게 탐색할 수 있습니다. 그림 10은 딥 콘볼루션 뉴럴 네트워크 모델의 예입니다.


그림10: 콘볼루션 뉴럴 네트워크의 텐서보드 시각화 (출처: 텐서플로우 페이퍼)


그림 11: 텐서보드의 모델 서머리(summary) 시계열 통계 데이터 (출처: 텐서플로우 페이퍼)

텐서플로우는 여러 종류의 Summary 연산을 제공합니다. 여기에는 스칼라 서머리(에러 함수 값, 연산에 걸린 시간 같은 전반적인 통계), 히스토그램 기반 서머리(뉴럴 네트워크의 한 레이어에서 가중치 값의 분포 등), 이미지 기반 서머리(콘볼루션 뉴럴 네트워크 필터의 시각화 등)이 포함됩니다. 이런 데이터를 얻기위해 Summary 노드가 그래프에 추가되어 그래프가 실행될 때 같이 실행됩니다. 클라이언트 프로그램은 서머리 데이터를 로그파일에 기록하고 텐서보드는 이를 읽어서 시각화합니다. 그림 11은 서머리 데이터를 시각화한 스크린샷입니다.

또 EEG 툴을 이용하여 텐서플로우 그래프가 정확한 순서로 실행되었는지와 성능 지표에 대한 자세한 정보를 수집하고 시각화합니다(이 툴은 오픈소스로 공개되지 않았습니다). 이 툴은 텐서플로우 프로그램의 병목부분과 통신 패턴을 이해하는 데 매우 유용합니다.

리눅스 ftrace 에서부터 간단한 스레드 트레이싱 툴과 CUDA 프로파일링 툴 인터페이스(CUDA Profiling Tool Interface, CUPTI) 등에서 트레이스(trace) 데이터를 수집합니다. 이런 로그를 바탕으로하여 모델 훈련 단계의 스레드 스위칭, CUDA 커널 구동, DMA 연산등을 마이크로 초 수준에서 재 구성할 수 있습니다.

트레이스 데이터는 시각화를 담당하는 서버에 모여 이벤트를 추출하고 상세 정보를 요약하여 사용자에게 비주얼하게 제공합니다. 통신, 동기화나 DMA에 의한 심각한 지연은 화면에 화살표로 표시됩니다. 그림12는 멀티코어 CPU에서 훈련하는 모델의 EEG 화면입니다. 그림의 상단 부분은 텐서플로우 연산이 병렬로 처리되고 있는 것을 보여줍니다. 화면의 아랫부분은 대부분의 연산이 병렬로 실행되기 위해 여러개의 작업으로 쪼개져 스레드로 실행되는 것을 보여주고 있습니다.

whitepaper-figure12.png
그림12: 멀티스레드 CPU 연산의 EEG 화면. x축은 마이크로세컨드. (출처: 텐서플로우 페이퍼)


그림13: CPU와 GPU를 사용하는 인셉션 모델 훈련 EEG 화면 (출처: 텐서플로우 페이퍼)

대각선 방향의 오른쪽 화살표는 작업이 스레드풀로 이전되면서 지연되는 것을 보여줍니다. 그림13은 GPU를 사용한 모델의 EEG 화면입니다. 호스트 스레드는 실행 준비가 된 텐서플로우 GPU 연산을 큐에 넣고 있고(밝은 파랑색 스레드풀) 백그라운드 스레드는 다른 프로세스 코어들로 전환되어 컬러가 다르게 보여집니다. 즉 화살표는 GPU에서 지연된 스레드가 CPU로 옮겨지는 것을 나타내며 이로 인해 연산이 많이 지연됩니다. 그림14는 여러개의 GPU 스트림(코어)에 할당된 연산에 대한 자세한 정보를 보여주는 화면입니다.


그림14: 멀티 스트림 GPU 실행 타임라인 (출처: 텐서플로우 페이퍼)

향후 과제

앞으로의 작업에 대해 몇가지 계획을 가지고 있습니다. 먼저 텐서플로우를 사용하여 인공지능 분야의 머신러닝 모델을 계속 개발하고 그에따라 오픈소스 커뮤니티와 함께 텐서플로우를 확장하는 것 입니다. 그리고 사용자가 텐서플로우 그래프의 서브그래프를 재사용 가능한 컴포넌트로 만들 수 있도록 기본 프로그래밍 모델을 확장하는 함수형 구조를 고려하고 있습니다. 이 기능이 구현되면 텐서플로우의 파이썬 인터페이스로 만든 컴포넌트를 C++ 인터페이스를 이용하여 사용할 수 있을 것 입니다.

또 텐서플로우 성능 향상을 위해서 그래프를 부분적으로 실행시킬 수 있는 JIT(Just-In-Time) 컴파일러를 개발하고 있습니다. 그리고 어느 디바이스에 노드를 배치하고 언제 실행시킬지를 판단하는 노드 스케줄링과 배치 알고리즘을 향상시킬 계획입니다.

관련 연구

텐서플로우와 비교할 수 있는 시스템이 많이 있습니다. 씨아노(Theano), 토치(Torch), 카페(Caffe), 체이너(Chainer), 컴퓨테이셔널 네트워크 툴킷(Computational Network Toolkit) 들은 뉴럴 네트워크를 전문적으로 훈련시키기 위한 몇안되는 시스템입니다. 텐서플로우와는 달리 이 시스템들은 단일 머신에서만 작동합니다. 씨아노나  체이너처럼 텐서플로우는 그래디언트 기반의 최적화 알고리즘을 쉽게 쓸수 있도록 함수 미분을 지원합니다. 텐서플로우는 카페처럼 C++로 작성되었으며 훈련된 모델을 쉽게 운영환경으로 배포시킬 수 있습니다. 텐서플로우는 설계 측면에서 디스트빌리프, 아담(Adam), 파라메타 서버 프로젝트(Parameter Server Project)를 참고했으며 디스트빌리프나 아담처럼 여러 서버의 디바이스로 연산을 분배시킬 수 있습니다.

그러나 디스트빌리프나 아담과는 달리 텐서플로우의 데이터플로우 그래프 모델은 더 유연성이 높아 다양한 종류의 머신러닝 모델과 최적화 알고리즘을 표현하기에 좋습니다. 텐서플로우에서는 파라메타를 업데이트하는 연산을 표현하기 위해서 그래프에 노드를 추가하면 되지만 디스트빌리프나 아담, 파라메타 서버 시스템은 파라메타 관리를 위한 별도의 서브 시스템을 두고 있습니다.

헬라이드(Halide) 시스템은 텐서플로우 데이터플로우 그래프와 유사한 표현 방식을 사용하여 이미지 프로세싱 파이프라인을 표현합니다. 텐서플로우와는 달리 헬라이드는 각 연산에 대해 매우 잘 설계가 되어 있어 여러 연산을 조합할 때 최적화된 코드를 생성해 냅니다만 단일 머신에서만 작동합니다.

텐서플로우처럼 데이터플로우 그래프를 분산환경에서 실행시킬 수 있는 시스템들도 있습니다. 드라이어드(Dryad)나 풀룸(Flume)은 워크 플로우를 데이터플로우 그래프로 표현하고 있고 시엘(CIEL)과 나이아드(Naiad)는 데이터에 상관없이 일반적인 흐름 제어 기능을 지원합니다. 스파크(Spark)는 RDD(Resilient Distributed Datasets) 데이터셋을 반복적으로 계산하는데 최적화되어 있으며 댄딜라이언(Dandelion)은 GPU를 비롯해 여러 종류의 디바이스에서 데이터플로우 그래프를 실행시킬 수 있습니다. 텐서플로우는 이런 시스템들의 장점을 모은 하이브리드 데이터플로우 그래프 모델입니다. 다음에 실행시킬 노드를 선택해주는 데이터플로우 스케줄러의 기본 알고리즘은 드라이어드, 풀룸, 시엘, 스파크와 같고 분산 구조는 나이아드와 유사합니다.

텐서플로우는 스파크와 나이아드처럼 연산에 필요한 데이터들을 유지할 수 있는 충분한 메모리가 클러스터 서버에 있을 경우 최대 성능을 발휘할 수 있습니다. 텐서플로우에서 병렬 처리는 하이브리드 방식을 사용하고 있어 그래프를 여러개 복제하여 변수를 공유하면서 동시에 실행할 수 있습니다. 복제된 그래프는 비동기적으로 데이터를 처리하거나 큐를 사용하여 동기적으로 처리할 수 있고 시엘과 나이아드의 중간 형태로 그래프내의 반복 기능을 지원합니다.

결론

텐서플로우는 단일 서버나 분산 환경에서 구동할 수 있는 유연한 데이터플로우 프로그래밍 모델입니다. 이 시스템은 구글 내부의 많은 연구와 경험을 토대로 탄생되었습니다. 이 시스템을 오픈소스로 공개함으로써 구글 밖에서도 널리 사용되길 기대합니다.


comments powered by Disqus