TensorFlow를 처음 시작하기에는 허들이 꽤 높다. 그러한 허들을 극복하기 위해서는 독특한 개념을 이해할 필요가 있다. 그러한 개념을 간단히 살펴보고자 한다.
(그러나 후술할 고수준의 API를 사용하면 어느정도 가볍게 작성할 수 있다. 예를 들어 "심층학습을 간단히 시험해보고 싶다"는 목적의 경우 이러한 API를 사용하는 것을 추천한다.)
TensorFlow 최초의 허들
TensorFlow의 최초로 맞닥뜨리게 되는 장애물은 아래의 두 가지라고 생각된다.
- 그래프와 세션
- 변수를 다루는 방법
이것들은 독자적인 형태로 맨 처음 봤을 때 반드시 당황하게 되는 부분이다. 이번 포스팅에서는 이 두 가지를 중점적으로 살펴보고자한다. 이 두 가지를 파악해두면 "코딩하고 로그를 출력해 디버깅한다"라는 가장 기본적인 개발 사이클도 가능하게 된다.
고수준 API의 존재
TensorFlow를 사용한 코드를 작성할 때 그래프와 세션의 조작을 회피할 수 있는 방법이 있다. 그 방법은 고수준의 API(tf.estimator.Estimator이나 tf.keras등)를 사용하는 것이다.
포스팅의 앞에서 잠깐 언급했듯, 고수준의 API를 사용하면, 그래프나 세션의의 조작은 은폐되기 때문에 프로그래머는 이러한 문제를 직면할 필요가 없어진다. 그러나 세밀한 조작이 필요하게 됐을 때에 그래프나 세션의 지식이 중요하게 된다고 개인적으로 생각한다. 고수준 API의 존재를 알고 있으면서도 그래프나 세션과 같은 저수준의 API도 알아두고 싶다면 계속 포스팅을 읽어주길 바란다.
그래프와 세션
TensorFlow이라는 것은 Define-and-Run이다. 코드 내에 기술한 계산 순서에 따라 "계산 그래프"를 최초에 구축해 두고, 그 뒤에 계산 처리를 실행하는 구조이다(그러나 현재의 TensorFlow는 Eager Execution이라는 Define-and-Run이 아닌 Define-by-Run에 의한 API가 준비되어 있다).
TensorFlow의 첫 번째 발자국으로는 먼저 tf.Graph와 tf.Session 2개를 기억하는 것으로 시작될 것이라고 생각된다. 이것들이 바로 그래프와 세션이다.
tf.Graph는 계산 처리의 내용을 유지하는 것으로 이 자체로만으로는 계산을 실행하는 것은 아니다.
tf.Session이 계산 처리 실행을 담당하는 기구이다.
* TensorFlow에서는 계산 그래프는 dataflow graph라고 부른다.
그래프의 구조
뒤에서 실제 코드를 살펴보면서 설명하겠지만, tf.add(덧셈)이나 tf.matmul(행렬의 곱) 등의 tf.Operation를 다음의 tf.Operation의 인수로써 전달하는 순서를 반복하는 것으로 그래프가 구축된다.
tf.Operation이란 연산 그 자체이다. 이것에 대해서는 나중에 설명하도록 하겠다.
세션에 의한 계산 처리의 실행
tf.Session의 생성시에는 "graph"이라는 인수에 tf.Graph를 지정한다. 계산 실행할 때에는 tf.Session.run이라는 함수를 실행한다. 인수는 tf.Operation이 된다. 이로 인해 지정한 연산을 실행하게 된다.
연산을 실행하고 반환되는 값은 tf.Tensor로 값을 얻을 수 있다. 즉, tf.Session을 run하는 것으로 첫 계산 결과의 수치가 (Python쪽에서) 받을 수 있다는 것을 의미한다.
tf.Session.close를 호출하면 세션이 종료된다.
실전 : 3차원 공간에 있어서 거리의 계산
그럼 그래프와 세션을 조작을 실제로 해보자. 다음과 같은 흐름으로 해보려고 한다.
1. 3개의 수치를 각각 제곱한다.
2. 제곱해서 얻어진 3개의 값을 모두 더한다.
3. 더한 값의 루트(평방근)을 계산한다.
3차원 공간에 있어서, 점 (a, b, c)가 원점으로 부터 얼마나 떨어져 있는지를 계산한다.
import tensorflow as tf
# <Step 1> 먼저 그래프를 구축
g = tf.Graph()
with g.as_default():
a = tf.Variable(2, name="a", dtype=tf.int32)
b = tf.Variable(3, name="b", dtype=tf.int32)
c = tf.Variable(6, name="c", dtype=tf.int32)
sum = tf.add_n([tf.square(a), tf.square(b), tf.square(c)], name="squared_distance") #덧셈
output = tf.sqrt(tf.cast(sum, dtype=tf.float32), name="distance") # 루트계산
init_op = tf.global_variables_initializer()
# <Step 2> tf.Session.run를 호출해 계산 처리를 실행
sess = tf.Session(graph=g)
sess.run(init_op) # 계산을 실행하기 전에 변수(tf.Variable)를 초기화
# init_op.run(session=sess) # 참고로 초기화 방법으로 이 방법도 사용할 수 있다.
print('-- print(output) --')
print(output) # 그러나, print()해도 계산 결과 값을 얻을 수 없다
print('-- sess.run(output) --')
print(sess.run(output))
sess.close()
# summary_writer = tf.summary.FileWriter('logs', sess.graph) # 계산 그래프 파일의 저장
# summary_writer.close()
실제로 TensorFlow의 변수는 tf.Variable()로 생성만 될 뿐이니 별도의 초기화 처리가 필요하므로 주의하자. 그러한 초기화처리가 tf.global_variables_initializer이다.
위 프로그램에서는 맨 처음에 tf.global_variable_initializer를 sess.run(init_op)로 실행하여 모든 변수를 일괄적으로 초기화하고 있다.
다음의 것들을 주의해야한다.
- 초기화하지 않고 sess.run(output) 실행하면, Attempting to user uninitialized value이라는 에러가 출력된다.
- sess.close()를 실행한 후에 sess.run(output) 등을 호출하면 에러가 되어, Attempted to use a closed Session.이라는 구문이 출력된다.
- 위 프로그램에서는 루트 처리를 하는 tf.sqrt의 직전에 tf.cast 처리를 넣었는데, 이것은 tf.sqrt에 입력한 숫자의 형태가 tf.int32 그대로일 경우 에러가 되기 때문이다. 여기서 형태를 tf.float32로 변환 처리를 하고 있다.
또한, tf.Session를 생성하는 부분은 인수 없이 tf.Session()해도 괜ㅊ낳다. 또한, 위 프로그램에서는 변수의 초기화를 sess.run(init_op)로 했으나 대신에 init_op.run(session=sess)로 실행해 초기화하는 것도 가능하다.
실행 결과
위 프로그램의 실행결과는 다음과 같다.
-- print(output) --
Tensor("distance:0", shape=(), dtype=float32)
-- sess.run(output) --
7.0
print(output)라고 작성해도 계산 결과의 값을 얻을 수 없다는 것을 확인할 수 있다.
sess.run(output)이라고 쓰면 먼저 계산 처리가 실행되고 결과 값인 7.0을 얻어낼 수 있다.
여기서 출력되는 Tensor이란 tf.Tensor의 오브젝트이다. Tensor에 대해서는 나중에 설명하도록 하겠다.
Tensor 과 Operation
TensorFlow에 있어서 "그래프"라는 단어는 "그래프 이론"의 그래프라고 생각해두는 것이 좋을 것이다. 이 "그래프"는 노드와 엣지로 구성되어 있다. 하나의 이미지로써는 점에 선이 연결되어 있는 네트워크라고 할 수 있다.
TensorFlow의 개념에서는 노드가 연산(오퍼레이션), 엣지가 데이터(텐서)에 대응된다. 프로그램상에서는 각각 tf.Operation와 tf.Tensor가 대응된다.
tf.add나 tf.multiply와 같이 사칙연산은 tf.Operation에 소속되어 있다. 심층 학습에 있어서 활성화 함수 중 하나인 tf.nn.relu나 손실함의 최소화 처리도 tf.Operation이다.
아래의 그림을 살펴보면, 그래프와 노드의 이미지를 파악할 수 있을 거라고 생각한다. 이것은 "그래프와 세션"의 항목의 샘플코드(example_01.py) 계산 그래프를 TensorBoard로 가시화한 것이다(오른쪽 맨 위에 보이는 "init"이 변수의 초기화 처리 오퍼레이션이다).
변수와 정수, 플레이스 홀더(place holder)
TensorFlow에서는 변수(tf.Variable)를 다룰 때에 주의가 필요하다.
- 변수는 tf.Variable등으로 생성만 하면 사용할 수 없으므로, 별도의 초기화 처리가 필요하다.
- 변수에 새로운 값을 저장하고 싶은 경우에 소정의 API인 tf.assign를 사용할 필요가 있다.
첫 번째 주의점에 대해서는 예를 들어 기존에 봤던 tf.global_variable_initializer가 초기화의 처리이다. 변수의 생성 방법은 tf.Variable외에 tf.get_variable이 있다. 이것은 기존의 변수를 재사용할 때(공유변수) 필요한 함수이다. 뒤에서 설명하도록 하겠다.
정수와 플레이스 홀더(place holder)
TensorFlow로 변수를 준비할 때, 벌써 몇 번이나 소개한 tf.Variable 이외에 다음의 것도 사용할 수 있다.
- 정수 tf.constant
- 플레이스 홀더 tf.placeholder
정수는 따로 설명이 필요없을 것 같다. 보통의 프로그래밍과 동일한 정수 설정이다. 또한, tf.Variable과 같은 초기화는 필요 없다. 또한, tf.constant()는 tf.Tensor를 반환한다.
플레이스 홀더는 tf.Variable등과 달리 맨 처음의 시점에 입력 값을 결정하지 않아도 괜찮은 구조로 되어 있다. 입력 값은 tf.Session.run했을 때 인수로써 전달한다.
플레이스 홀더의 구조는 계산 그래프의 구축 시점에서는 데이터의 내용이나 갯수가 정해지지 않았을 때 유용하다. 특히, 심층학습에 있어서 트레이닝 때에 미니 배치를 입력하는 처리에서는 플레이스 홀더를 사용하는 것이 좋다고 생각한다.
tensor-like objects
계산 그래프를 짤 때, 각 노드에 대한 인수로써 입력할 수 있는 오브젝트는 Tensor이다. 그러나 반드시 tf.Tensor 그 자체여야 한다는 것이 아닌 Python 리스트도 허용된다.
TensorFlow 공식 문서(Graphs and Sessions)에서는 이러한 텐서로 다뤄지는 것들을 tensor-like objects라고 부른다. tensor-like objects 종류는 다음과 같다.
- tf.Tensor
- tf.Variable
- numpy.ndarray(Numpy의 배열)
- Python의 리스트
- Python의 스칼라 값(int, float 등)
텐서(Tensor)란 무엇이냐하면, 벡터나 배열을 포함하는 상위 개념으로 다차원 배열이라고 생각하면 좋을 것 같다. 벡터는 한 방향으로 숫자가, 행렬은 두 방향으로 숫자가 나열되어 있다. 행렬를 여러 개 겹치면 3층의 텐서가 된다. 또한 스칼라는 0층의 텐서이다.
이 포스팅의 테마에서 벗어나지만, 이미지 인식에서는 "채널"이라는 개념이 나오므로, 3층의 텐서가 자주 등장한다. 그러나 3층의 텐서는 행렬을 나열했을 뿐이다.
실전 : 행렬의 곱셈
플레이스 홀더에 Tensor-like objects중 하나인, Numpy 배열을 입력하는 샘플 프로그램을 살펴보자. 처리 내용은 단순히 행렬을 곱셈할 뿐이다.
import tensorflow as tf
import numpy as np
# 2행2열의 행렬의 2개 생성(ndarray)
np_a = np.arange(0, 4).reshape(2, 2)
np_b = np.arange(4, 8).reshape(2, 2)
g = tf.Graph()
with g.as_default():
with tf.name_scope("input"): # 이름공간
a = tf.placeholder(tf.int32, shape=(2, 2), name="a")
b = tf.placeholder(tf.int32, shape=(2, 2), name="b")
with tf.name_scope("output"): # 이름공간
output = tf.matmul(a, b, name="product") # 행렬의 곱셈
sess = tf.Session(graph=g)
tf.global_variables_initializer().run(session=sess)
print(a)
print(b)
print('--')
print(a.name) # tf.Tensor의 이름을 참고
print(b.name)
print(output.name)
print()
print('-- matrix a --')
print(a.eval(feed_dict={a: np_a}, session=sess))
print()
print('-- matrix b --')
print(b.eval(feed_dict={b: np_b}, session=sess))
print()
print('-- output --')
print(sess.run(output, feed_dict={a: np_a, b: np_b}))
sess.close()
tf.placeholder에 입력한 값은 feed_dict이라는 Python 사전에 저장되어 전달된다. 또한, tf.Tensor에 대해 "eval"이라는 메소드를 호출하고 있지만, 이것은 sess.run과 동일한 처리를 실행한다. sess.run를 사용할 것인가 eval를 사용할 것인가에 대해서는 사용하기 편리한 것을 사용하면 그만이라고 생각된다.
실행 결과
Tensor("input/a:0", shape=(2, 2), dtype=int32)
Tensor("input/b:0", shape=(2, 2), dtype=int32)
--
input/a:0
input/b:0
output/product:0
-- matrix a --
[[0 1]
[2 3]]
-- matrix b --
[[4 5]
[6 7]]
-- output --
[[ 6 7]
[26 31]]
행렬 a와 b의 곱이 출력되고 있다는 것을 알 수 있다. 또한 tf.Tensor는 "name"이라는 속성을 가지고 있다. 위 코드와 같이, a.name등과 같이 이름을 참조할 수 있다. "name"의 맨 앞에는 이름 공간의 정보가 붙어져 있다(위 예에서는 "input", "output").
이름공간(name space)
복잡한 계산 그래프를 만들 때는 정리를 위해 이름 공간(name space)을 사용하는 것이 좋다. 이름 공간을 사용하는 메리트는 다음과 같다.
- 소스 코드가 읽기 쉽게 된다.
- 계산 그래프의 각 노드에 붙어 있는 이름(name)의 충돌을 방지할 수 있다.
- TensorBoard로 계산 그래프를 가시화할 때, 노드가 이름 공간에 묶여 보기 쉬워진다.
이름 공간의 사용법으로는 두 가지 방법이 있다.
- tf.name_scope
- tf.variable_scope
tf.name_scope는 tf.get_variable를 사용하는 경우에는 무시되므로 주의할 필요가 있다. tf.get_variable를 사용할 때는 tf.variable_scope를 사용하면 된다.
tf.variable_scope는 이름에 "variable"이 포함되어 있으나, tf.Variable이외의 노드(tf.Tensor등)에 대해 유효하므로, 무시되거나 하지 않으므로 불안해할 필요가 없다.
변수의 재사용
실행하고 싶은 계산 처리의 내용에 따라, 동일한 변수(tf.Variable)를 몇 번이고 반복해서 사용하고 싶은 경우가 있다. 예를 들어, 재귀 뉴럴네트워크(베이직 RNN, LSTM, GRU)를 만들 경우, 은닉층을 구성하는 변수를 몇 번이고 사용하게 된다.
tf.get_variable와 tf.variable_scope를 합쳐서 사용해, 특정의 제어 방법을 사용하는 것으로, 변수를 재사용할 수 있다. tf.Variable()로 생성한 변수의 경우는 재사용할 수 없으므로 주의하자! 그 외에도 주의해야할 점들이 있다.
- 세션을 닫지 않고 유지되고 있다는 것을 전제로 한다.
- tf.get_variable로 변수를 재사용할 때에는 tf.variable_scope가 필요하다.
- tf.get_variable는 tf.variable_scope의 reuse옵션으로 재사용하지 않도록 되어 있는 경우, 기존 변수와 중복하는 변수를 지정하여 변수를 생성하고자 하면 에러가 발생한다.
- tf.Variable() (함수)를 호출하여 생성한 변수는 tf.get_variable로 재사용할 수 없다.
변수의 재사용은 tf.variable_scope의 "reuse"이라는 인수를 사용하여 제약할 수 있다. 혹은 tf.get_variable_scope().reuse_variables()를 사용하는 것도 가능하다.
# reuse=True로 재사용
with tf.variable_scope("test", reuse=True):
# 여기에 각 종 변수의 처리를 기술
# 혹은 아래와 같이 작성한다.
with tf.variable_scope("test"):
tf.get_variable_scope().reuse_variables()
실전 : 3항간 점화식
실수의 재사용을 실전 코드의 예는 다음과 같다.
import tensorflow as tf
def get_next_term(term_1, term_2):
# 다음의 항목 값을 계산
coef_1 = tf.get_variable("coef_1", shape=(), dtype=tf.int32)
coef_2 = tf.get_variable("coef_2", shape=(), dtype=tf.int32)
return coef_1 * term_1 + coef_2 * term_2
def main():
# 인접 3항목 점화식에 따라 10번째까지의 항목을 취득
# 초기값
init_val_1 = 1
init_val_2 = 1
g = tf.Graph()
with g.as_default():
tf_init_val_1 = tf.Variable(init_val_1) #dtype=tf.int32
tf_init_val_2 = tf.Variable(init_val_2) #dtype=tf.int32
sess = tf.Session(graph=g)
output_list = []
# 편의상, 2개의 초기값을 추가해둔다.
output_list.extend([tf_init_val_1, tf_init_val_2])
with tf.variable_scope("recurrence") as scope:
# 재사용(공유)하는 두 개의 변수. 점화식의 계수의 역할.
coef_1 = tf.get_variable(name="coef_1", shape=(), dtype=tf.int32 , initializer=tf.constant_initializer(2))
coef_2 = tf.get_variable(name="coef_2", shape=(), dtype=tf.int32 , initializer=tf.constant_initializer(1))
print(coef_1.name)
print(coef_2.name)
for i in range(8): # 2개의 초기 항목 + 8항목으로, 합계 10개의 항목을 취득.
scope.reuse_variables() # 변수의 재사용
output = get_next_term(output_list[i], output_list[i+1])
output_list.append(output)
init_op = tf.global_variables_initializer() # 초기화
sess = tf.Session()
init_op.run(session=sess)
print("-- output --")
print(sess.run(output_list))
sess.close()
if __name__=='__main__':
main()
이름 공간 "tf.variable_scope"는 get_next_term 함수 외측에서 문제없이 사용할 수 있다. 위 코드는 점화식의 계수 부분에는 정수 tf.constant를 사용하면 되면 좋을텐데 변수를 사용하고 있는 것이 부자연스럽지만, 어디까지나 tf.get_variable에 의한 변수 재사용 방법을 설명하기 위해 인위적으로 작성한 것이다.
또한, shape=()하는 것으로 0층의 텐서(=스칼라)를 지정하는 것이 가능하다 (shape=[1]으로 하면, 스칼라가 아니게 되므로 주의하자. shape=0이나 shape=None은 에러가 된다). 무엇보다, TensorFlow의 텐서 API는 Numpy를 따르고 있으므로, Numpy에 대해서 잘 아는 사람은 이 문제와 관련 없을 수도 있다. 스칼라 지정 방법이 shape=()인 것은 사실 Numpy와 같다.
또한 위 코드에서는 아무렇지 않게 sess.run(output_list)라고 쓰고 있지만, tf.Session.run은 여러 개의 오퍼레이션으로 전달하면 그 리스트의 오퍼레이션을 일괄로 실행시, 그 계산 결과를 리스트로 반환한다.
실행결과
위 프로그램의 실행 결과는 다음과 같다.
recurrence/coef_1:0
recurrence/coef_2:0
-- output --
[1, 1, 3, 5, 11, 21, 43, 85, 171, 341]
메리트
위의 tf.get_variable에 의해 변수를 재사용하는 방법에 대해, tf.Variable()를 변수를 계속 들고 다니면 되지 않을까 생각할 수 있을지도 모르겠지만, tf.get_variable을 이용해 재사용하는 편이 코드가 간략해진다.
기본적으로는 tf.Variable()로 생성한 변수를 계속 들고 다니는 방법으로도 충분히 구현할 수 있다. 그러나 TensorFlow의 계산 그래프의 구축 처리를 몇 개의 보조 함수에 분할하는 경우, tf.Variable을 전달한만큼 함수의 인수가 늘어버리게 된다. 또한, 동일한 보조함수로 변수으의 생성과 재사용을 겸할 수 없다.
이러한 점에서 tf.get_variable만으로 구현한 보조함수를 사용하면, tf.variable_scope의 "reuse"인수로 제어하는 것만으로도 생성과 재사용 모두 사용할 수 있다. 이로 인해, 코드가 보다 간략해지고 보기 쉽게 된다.
마무리
이번 포스팅은 TensorFlow 1.x를 상정한 내용이다. 앞서 말했듯, TensorFlow2.x에서는 기본적으로 Eager Execution이다. 그러나 꽤 많은 논문에서 TensorFlow 1.x 코드로 작성된 경우가 많아 이러한 내용을 알아두면 TensorFlow1.x 코드를 읽을 때 꽤 도움이 될 것이라고 생각한다.
참고자료
'IT > AI\ML' 카테고리의 다른 글
[논문] point cloud 와 딥러닝(PointNet 간단 해설) (2) | 2022.01.21 |
---|---|
[python/TensorFlow2.x] TensorFlow의 GradientTape에 대한 간단한 설명 (0) | 2022.01.15 |
[python/Tensorflow1.x] Tensorflow의 이름공간(NameSpace)와 공유 변수(Sharing Variable) (0) | 2022.01.12 |
[python] 결정계수 R2와 자유도 조정 결정 계수 R*2 (0) | 2021.11.28 |
[Tensorflow] Tensorflow Profiler 사용법 (0) | 2021.09.21 |