본 포스팅은 다음 과정을 정리 한 글입니다.
Custom and Distributed Training with TensorFlow
지난 시간 리뷰
현재 TensorFlow2를 사용 중이고, 이 과정을 듣고 계신다면 더 쉬운 개발 및 디버깅을 위한 High-Level API와 Keras, Eager mode (Eager Execution)를 사용하고 있을 것입니다.
우리는 때때로 모델에서 추가 성능이 필요할 때가 있습니다. 엔지니어링을 하다 보면 거의 매번 높은 성능을 요구할 때가 많이 있습니다ㅠ 이때 사용할 수 있는 한 가지 방법이 그래프 기반 모델을 사용하는 것입니다.
이번 강의에서는 그래프 기반 코드를 좀 더 쉽게 개발할 수 있게 해주는 AutoGraph 기술을 살펴보겠습니다.
이전에 Eager 모드를 사용하여 즉각적인 결과를 얻을 수 있는 코드를 작성하는 방법을 배웠습니다.
Tensorflow는 원래 그래프를 실행하기 전에 모든 작업을 그래프를 정의해야 하는 그래프 모드에서 수행되는 프로그래밍을 중심으로 설계되었습니다.
예를 들어, y의 ReLu는 Wx의 ReLu에 b를 더한 것과 같은 공식을 계산한다고 했을 때 위의 그림과 같은 그래프가 됩니다.
w와 x를 곱셈 연산에 load 되는 변수로 취급합니다. 그 결과는 변수 b와 함께 add 연산에 로드되고 그 결과는 다음과 같은 다른 연산에 load 됩니다. Relu를 지나면 최종 답인 y가 나옵니다.
이것들은 Python 개발자만큼 직관적이지 않은 것처럼 보일 수 있지만 실제로 빠르게 작동하며 그래프를 사용하면 훈련 및 추론 시간을 확실히 단축할 수 있습니다.
그러나 코딩하기 어렵고 곱하기, 더하기, ReLu와 같은 연산은 그래프가 완전히 설계될 때까지 실행 되지 않기 때문에 디버깅하기 어려울 수 있습니다.
Control Flow을 예로 들어보겠습니다.
위에 "if"문이 있는 함수가 있습니다. x가 0보다 크면 x의 제곱을 반환하고 그렇지 않으면 x를 반환합니다.
Python에서 Eager mode를 사용하면 매우 친숙하고, 간단한 pythonic 코드를 작성할 수 있습니다.
그러나 그래프는 "if" 조건을 지원하지 않으므로 tf.cond 조건을 사용하여 이와 같은 코드를 작성해야 합니다.
여기에서 x가 0보다 큰지는 tf.greater를 사용하여 비교할 수 있습니다.
다음 매개변수는 함수가 true이면 호출되는 함수이고 여기에 함수의 이름을 지정합니다. 그다음은 거짓이면 호출되는 함수입니다. 여기의 경우 if_true, if_false를 각각 설정하여 true이면 x의 제곱, false이면 x를 return 하는 함수가 작성되어 있습니다.
Eager 모드를 사용하면 표준 제어 흐름 구문으로 표준 Python 코드를 어느 정도 사용할 수 있습니다. 그러나 그래프가 제공하는 이점 중 일부를 잃게 됩니다.
AutoGraph에서 그래프에는 명시적 종속성이 있습니다. 즉, 그래프의 노드를 보면 그래프를 역방향으로 추적하여 실행에 의존하는 작업을 미리 알 수 있습니다.
각 작업에 대한 종속성을 알면 특정 작업이 수행될 때 효율적으로 수행할 수 있습니다. 예를 들어 일부 작업을 병렬로 실행하거나 다른 시스템에 배포할 수 있습니다.
Eager 모드와 그래프 모드는 서로 충돌하는 것처럼 보이지만 두 가지 접근 방식이 각각 다른 장점을 가집니다.
Eager 모드를 사용하여 새 모델을 개발하거나 디버깅하고 나서 더 많은 성능을 끌어내고자 하는 경우(프로덕션에 배포할 준비가 되었고 최상의 모델 성능을 원하는 경우)에 그래프 모드로 전환하면 됩니다.
쉽고 친숙한 Python 방식 대신 왜 이와 같은 코드를 작성하는지 궁금할 것입니다. 그래프에는 명시적 종속성이 있으므로 비교적 쉽게 계산을 병렬화하고 분산할 수 있습니다. 또한 GPU 및 XLA라는 TensorFlow의 무언가를 사용할 때 커널 융합과 같은 전체 프로그램 최적화를 허용합니다. 더 설명드리기에는 이 과정의 범위를 벗어나기 때문에 따로 찾아보시기 바랍니다.
아무튼 정리하면 두 가지 접근 방식을 모두 사용하면 이점을 얻을 수 있습니다. Eager 모드는 새 모델을 개발 및 디버깅할 때, 속도 최적화가 필요할 때 그래프 모드로 전환하면 됩니다.
AutoGraph가 실제로 도움이 될 수 있는 것은 위의 그림과 같이 Eager-style의 Python 코드를 자동으로 그래프로 변환하고 그 반대로 변환할 수 있는 기술입니다.
AutoGraph를 사용하려면 Eager 모드에서 일부 작업을 수행하는 함수를 구현하는 것으로 시작하면 됩니다.
예를 들어, 다음 두 개의 매개변수 a와 b를 합하여 이들의 합을 반환하는 간단한 함수입니다.
그런 다음 함수 정의 위의 줄에 @tf.function(decorator)을 추가하여 add함수를 장식합니다. 간단하죠?!
이 @tf.function는 사용자 정의 코드, a + b를 가져와 미리 빌드된 @tf.function 안에 래핑 하는 것으로 생각할 수 있습니다. 따라서 add 함수에는 이제 사용자 정의 코드와 결합된 @tf.function의 기능이 있습니다.
이제 decoratored 된 추가 기능에 볼 수 있는 그래프 코드가 있습니다.
그래프 코드가 어떻게 생겼는지 살펴보고 싶다면 tf.autograph.to_code 메서드를 사용하고 지금 구현한 add 함수나 tf.function을 사용하여 정의한 어떤 함수를 전달하여 살펴볼 수 있습니다.
여기에 위의 add 함수를 그래프 모드로 만드는 작업들이 있습니다. 간단하게는 fscope.mark_return_value(( a + b ))가 구현되어 있는 것을 확인할 수 있습니다.
다음은 방금 정의한 add 함수에서 gradient를 계산하는 예입니다.
tf.GradientTape 내부에서 값 1과 변수 v를 전달할 수 있습니다. 그런 다음 add function을 호출하여 변수 v와 1을 전달하고 결과를 저장합니다.
Gradient를 계산하기 위해 result와 v를 전달하는 tape.gradient를 호출할 수 있습니다. 이것은 v에 대한 result의 gradient를 계산합니다.
코드에서 여러 기능을 사용하는 경우 모든 기능에 주석을 달 필요가 없습니다. 주석이 달린 함수 내에서 호출된 모든 함수는 그래프 모드에서도 실행됩니다.
여기서 deep_net은 tf.function으로 decorate 되어 있지만 linear_layer는 그렇지 않습니다. 그러나 linear_layer는 deep_net에서 호출되기 때문에 linear_layer도 AutoGraph에 의해 그래프 모드로 변환됩니다.
파이썬의 함수는 polymorphic(다형성)을 가지고 있습니다. 즉, 함수가 취하는 매개변수가 정수, 부동 소수점 또는 문자열로 전달되어도 동작이 될 수 있습니다. 이 polymorphic은 그래프 스타일의 코드에도 적용이 됩니다.
다형성이란 무엇입니까?
다형성의 문자 그대로의 의미는 다양한 형태로 발생하는 조건입니다.
다형성은 프로그래밍에서 매우 중요한 개념입니다. 단일 유형 엔터티(메서드, 연산자 또는 개체)를 사용하여 다양한 시나리오에서 다양한 유형을 나타내는 것을 말합니다.
위에 보이는 것과 같이 모든 작업이 Python으로 코딩할 때와 같이 작동하는 것을 볼 수 있습니다. 여기에서 매개변수를 가져와 동일한 것을 더하여 return 하는 double이라는 데코레이팅 된 함수를 정의했습니다.
정수 1을 제공하면 숫자 2를 반환하는 함수입니다. 부동 소수점 1.1에 대해서도 2.2를 반환하고 문자열에서도 작동합니다. 문자 "a"를 제공하면 문자열 "aa"가 return 됩니다. 접두사 b는 'aa'가 byte literal이라는 것을 나타냅니다.
앞서 “byte literal”이라고 말했지만, 사실상 이 아이는 그냥 bytes일 뿐입니다. 볼 때, 아스키코드 상으로 변환될 수 있는 것들은 이미 변환되어, 문자열처럼 보이는 것뿐이죠. 그리고 앞에 bytes형태로 되어 있음을 명확하게 보여주는 b라는 글자가 붙습니다.
무슨 소리야? 싶을 수 있습니다. 그럼 다음을 보시죠. 0을 bytes형태로 저장하고 그것을 출력합니다. 이번에는 문자가 아니라, \x로 시작하는 16진법의 값들이 막 나왔습니다. 일단은 각 byte에 해당하는 문자를 출력해주지 못하여, 16진법의 형태로 그대로 출력해준 것이죠. 이 경우에도 마찬가지로, 앞에는 b가 붙어 있습니다.
a = bytes([0, 16, 160, 255]) print(a) b'\x00\x10\xa0\xff'
Reference : https://frhyme.github.io/python-basic/py_byte_string/
데이터가 byte literal로 저장되면 문자열 대신 숫자를 저장한다는 의미라고 이해하시면 됩니다.
이 과정의 앞부분에서 배웠던 것인데 기억하시는지 모르겠네요.
Keras 클래스의 하위 클래스를 정의하는 건데 하위 클래스에서도 그래프를 사용할 수도 있습니다.
여기서는 Keras 모델 클래스를 상속하는, 사용자 지정 모델이 클래스를 정의했습니다.
클래스 내에서 @tf.function으로 장식된 호출 메서드를 정의하여 그래프 모드로 변환시킵니다.
성능을 위해 AutoGraph를 사용하면 Ops를 많이 사용하는 코드에서 가장 큰 성능 향상을 얻을 수 있습니다.
코드는 본질적으로 복잡할 필요가 없으며 종종 매우 간단한 파이썬 코드 조각이 생각하는 것보다 훨씬 더 많은 연산을 사용하고 있습니다.
위에 간단한 게임인 FizzBuzz를 소개하고 있습니다. 7개의 숫자를 반복하는 간단한 알고리즘입니다.
3의 배수이면 Fizz를 출력하고 5의 배수이면 Buzz를 출력하고 3의 배수이고 5의 배수이면 FizzBuzz를 출력합니다.
그러나 조건문과 제어문은 말할 것도 없고 이를 수행하기 위해 얼마나 많은 Ops가 있는지 보시기 바랍니다.
이러한 유형의 코드는 그래프 모드에서 매우 복잡해 보일 수 있지만 훨씬 빠르게 실행할 수 있으며 그래프를 사용하기 위한 최상의 시나리오 유형입니다.
작은 연산을 많이 사용하는 코드가 최상의 성능 향상을 보이는 경향이 있음을 기억하시기 바랍니다. 그래프 모드에서 FizzBuzz를 보면 손으로 코딩하기가 매우 어려울 것입니다.
@tf.function을 사용하여 AutoGraph가 이 모든 것을 생성할 수 있다는 것만 기억하시면 됩니다!
생성된 코드를 보려면 tf.autograph.to_code를 호출하여 python_function property와 함께 함수 이름을 전달하면 됩니다.
자동으로 그래프로 변환되는 코드를 작성할 때 주의해야 할 점은 실행 순서가 의도한 것과 같은지 확인하는 것입니다.
이와 같은 간단한 그래프의 경우 먼저 W에 x를 곱한 다음 결과를 b에 추가하면 됩니다.
Python을 작성할 때 그래프 명령은 원래 Python 코드와 동일한 순서로 구현된다는 점을 기억하는 것이 중요합니다.
예제를 살펴보겠습니다.
@tf.function을 사용하여 일반 Python 코드에서 자동으로 그래프를 생성하면 복잡한 그래프를 스스로 설계하지 않아도 됩니다. 복잡한 그래프가 있는 코드의 예는 동일한 변수가 일부 계산의 입력으로 사용되지만 다른 계산의 결과를 저장하는 데 사용될 수 있습니다.
예를 들어, a 및 b에 대해 많은 읽기 및 쓰기를 수행하는 이 코드를 보면 f(1.0, 2.0)의 답은 10입니다.
어떻게 이 값을 얻었을까요? a는 1.0으로 b는 2.0으로 초기화하는 것부터 시작합니다.
여기서 가장 먼저 일어나는 연산은 a에 y * b를 할당하는 것입니다.
b는 2.0이고 y는 함수의 두 번째 매개 변수 이므로 2.0입니다. 따라서 a에는 4.0이 할당이 됩니다.
b는 이미 2.0이지만 assign_add를 호출하여 x에 a를 곱한 값을 본인에게 더합니다. x는 첫 번째 매개 변수 이므로 1.0입니다. a는 아까 계산된 대로 4.0이고 따라서 b는 6.0이 됩니다.
마지막으로 a + b를 반환하면 10이 됩니다.
이러한 과정을 모두 그래프로 직접 디자인하려면 정말 끔찍한데요. Autograph는 이러한 복잡한 일을 처리해 줍니다. 정말 대단하죠...
또한 autograph를 사용하여 조건부 제어 흐름에 대한 그래프를 생성할 수 있습니다.
예를 들어, x가 0보다 크면 양수인 문자열을 반환하고 그렇지 않으면 음수인 문자열을 반환하는 간단한 함수를 보겠습니다.
위에 코드는 왼쪽에 작성되어 있는 Python코드와 오른쪽에 autograph로 생성한 그래프 코드입니다.
x가 0보다 큰 경우의 조건문이 그래프 코드에서는 어떻게 구현되는지 먼저 살펴보겠습니다.
오른쪽에 있는 코도의 맨 아래 몇 줄을 살펴보겠습니다.
먼저 true/false로 평가되는 "x가 0보다 크다" 표현식은 cond라는 변수에 저장되고 그 아래 있는 ag__ 객체에는 여러 매개변수를 받는 if_stmt 함수가 있습니다.
우리는 처음 세 가지 매개변수를 살펴보겠습니다. 첫 번째 매개변수는 Boolean 조건, 두 번째 매개변수는 조건이 참일 때 호출할 함수, 세 번째 매개변수는 조건이 거짓일 때 호출할 함수입니다.
if_true 함수는 cond조건이 true일 때 호출이 되고 try, except 블록 내에서 수행되는 오류 검사를 진행합니다.
반대로 if_false는 cond가 false일 때 수행됩니다.
autograph는 conditional 함수도 그래프 코드로 변환을 성공적으로 하였습니다. 우리는 왼쪽의 코드처럼 구현하고 @tf.function으로 decorate만 해주면 됩니다.
이제 loop와 같은 제어 흐름의 예를 살펴봅시다.
위의 코드는 while 루프가 들어간 복잡한 예입니다.
루프 내에서 tf.print(x)에 저장된 값을 출력합니다.
다음 줄에서 x는 tf.tanh(x)를 호출하여 x값을 전달한 다음 그 결과를 x에 저장합니다. x의 reduce_sum이 1보다 크면 루프가 계속됩니다.
이 함수는 모든 요소의 합이 1보다 높은 한 tf.tanh(x) 연산을 계속 수행합니다.
이것도 동일하게 왼쪽에 Python코드가 있고 오른쪽은 autograph가 생성한 코드입니다.
이 코드는 직접 한번 분석해보시기 바랍니다~
autograph를 사용할 때 고려해야 할 또 다른 사항은 즉시 실행 코드에서 그래프 기반 코드로 이동하는 것이 for문에 영향을 미친다는 것입니다.
Tracing 문은 개발자가 코드가 실행되는 동안 실행된 함수와 변수 값을 추적하는데 도움이 되는 코드입니다.
위의 예는 f라는 함수를 만든 다음 매개 변수로 x를 받아 x로 Traced 되었다는 명령문을 출력하는 코드입니다.
여기에서 볼 수 있는 것처럼 루프 내에서 해당 함수를 호출합니다.
이 함수는 호출될 때마다 값을 출력합니다.
이제 함수 f를 @tf.function으로 decorate 하면 어떻게 되는지 알아봅시다.
즉, 그래프 모드로 작동하는 것입니다. 또한 이제 우리는 print문 뒤에 다른 코드를 추가하였는데 이것은 Tensorflow의 print문인 tf.print입니다.
이제 위의 함수와 어떻게 다른지 살펴봅시다.
위에서는 "Traced with 2"가 5번 출력되었지만 여기에서는 단 한 번만 출력이 되고 tf.print문인 "Executed with 2"가 5번 출력이 되었습니다.
이는 Python print문이 그래프 또는 그래프에 의해 실행되는 세션 내에서 동작되도록 설계되지 않았기 때문입니다.
그래서 Python print문은 단 한번만 호출된 것입니다. 반대로 그래프를 인식하는 TensorFlow의 tf.print는 올바르게 5번 실행된 것을 확인할 수 있습니다.
고려해야 할 또 다른 사항은 변수를 생성하는 위치입니다.
일반적으로 함수 범위 내에서 로컬 변수를 생성할 수 있다고 생각합니다. 그러나 변수와 Ops를 별도로 보관하고 처리해야 하는 그래프 모드에서 이 코드는 오류가 발생할 수 있습니다.
이와 같이 선언을 함수 외부로 이동하면 올바르게 변환이 됩니다.
이것으로 TensorFlow의 그래프 모드 변환, Autograph에 대해 모두 살펴봤습니다.
감사합니다.
댓글