reniew's blog
cs and deep learning

CS20(TensorFlow) Lecture Note (1),(2): Overview & TensorFlow Operation

|

스탠포드의 TensorFlow 강의인 cs20 강의의 lecture note를 정리한 글입니다. 강의는 오픈되지 않아서 Lecture note, slide 위주로 정리된 글임을 참고 해주시길 바랍니다. 강의의 자세한 Syllabus 및 자료들을 아래 링크를 참고해 주세요.

CS20: TensorFlow for Deep Learning Research


Post list

1. Introduction to TensofFlow

텐서플로우에 대한 소개는 텐서플로우 웹사이트에서 소개한 말을 인용한다.

TensorFlow is an open source software library for numerical computation using data flow graphs

소개에 대한 부분은 자세히 쓰지 않는다. 텐서플로우의 장점 및 자세한 내용은 텐서플로우 홈페이지를 참고하자.

2. TensorFlow Ops

TensorBoard

텐서플로우의 사용 방법 이전에 우선 텐서플로우를 사용하면서 좀 더 직관적인 이해와 시각화를 위한 소프트웨어인 텐서보드를 먼저 소개한다.

텐서보드는 텐서플로 설치시 포함되어있는 Graph visualization 소프트웨어로 Google의 TensorBoard에 대한 설명을 인용한다.

“The computations you’ll use TensorFlow for - like training a massive deep neural network - can be complex and confusing. To make it easier to understand, debug, and optimize TensorFlow programs, we’ve included a suite of visualization tools called TensorBoard”

텐서플로우로 실행시킨 내역에 대해서 텐서보드로 확인 할 수 있다. 예를 들어 아래의 텐서플로우 코드에 대해서 텐서보드로 확인한다고 하자.

import tensorflow as tf
a = tf.constant(2)
b = tf.constant(3)
x = tf.add(a, b)
with tf.Session() as sess:
      print(sess.run(x))

텐서보드로 실행시키기 위해서는 계산되는 값에 대해 log file을 만들어 줭야 한다. 따라서 아래의 명령어를 입력해야 한다.

writer = tf.summary.FileWriter([logdir], [graph])

logdir은 log file이 저장될 경로를 뜻하고, graph는 우리가 동작시키는 프로그램의 하나의 graph를 뜻한다.

로그파일을 저장했다면 명령 프롬프트에서 다음과 같이 입력하면 된다.

$ python [my_program.py]
$ tensorboard --logdir="logdir" --port 6006

이후 웹 브라우저에서 http://localhost:6006으로 들어가면 TensorBoard를 사용할 수 있다.

그리고 텐서보드를 사용할 때 로그파일을 저장하는데, log파일이 쌓여서 겹칠 경우에는 가장 최신의 파일을 인식한다. 그리고 경고가 뜨는데 이 경고를 없에기 위해서는 쌓여있는 log파일을 정리해줘야 한다.

Constant

constant를 만드는 가장 기본적인 방법은 다음과 같다.

tf.constant(value, dtype=None, shape=None, name='Const',verify_shape=Flase)

그 외에 다양한 종류의 constant를 만드는 방법은 다음과 같다.

tf.zeros(shape, dtype=tf.float32, name=None)
tf.zeros_like(input_tensor, dtype=None, name=None, optimize=True)
tf.ones(shape, dtype=tf.float32, name=None)
tf.ones_like(input_tensor, dtype=None, name=None, optimize=True)
tf.fill(dims, value, name=None)
tf.lin_space(start, stop, num, name=None)
tf.range([start], limit=None, delta=1, dtype=None, name='range')

여기서 주의해야 할 점은 아래의 tf.lin_space()tf.range()함수는 iterable하지 않기 때문에 for문에서 사용할 수 없다.

각 함수를 사용해서 직접 만드는 예시는 다음과 같다.

tf.zeros([2, 3], tf.int32) ==> [[0, 0, 0], [0, 0, 0]]
tf.zeros_like(input_tensor) ==> [[0, 0], [0, 0], [0, 0]]
tf.ones([2, 3], tf.int32) ==> [[1, 1, 1], [1, 1, 1]]
tf.ones_like(input_tensor) ==> [[1, 1], [1, 1], [1, 1]]
tf.fill([2, 3], 8) ==> [[8, 8, 8], [8, 8, 8]]
tf.lin_space(10.0, 13.0, 4, name="linspace") ==> [10.0 11.0 12.0 13.0]
# 'start' is 3, 'limit' is 18, 'delta' is 3
tf.range(start, limit, delta) ==> [3, 6, 9, 12, 15]
# 'limit' is 5
tf.range(limit) ==> [0, 1, 2, 3, 4]

위의 방법외에도 random한 값을 가지는 constant를 지정해줄 수 있는데 아래와 같은 다양한 함수를 통해 만들 수 있다.

tf.random_normal
tf.truncated_normal
tf.random_uniform
tf.random_shuffle
tf.random_crop
tf.multinomial
tf.random_gamma
tf.set_random_seed

이 함수들의 자세한 내용은 링크를 참고하자

Math Operation

텐서플로우는 많은 수학 연산을 제공하는데 전체 리스트는 링크를 참고하자.

  • Division operation

텐서플로우에는 다양한 나눗셈 연산들이 있다. tf.division은 일반적인 파이썬의 연산을 생각하면 된다. 그리고 tf.div는 텐서플로우 스타일의 나눗셈 연산으로 사용된다. 전체 나눗셈에 대한 자세한 내용은 documentation을 참고하자. 아래는 나눗셈을 사용하는 예시이다.

a = tf.constant([2, 2], name='a')
b = tf.constant([[0, 1], [2, 3]], name='b')
with tf.Session() as sess:
	print(sess.run(tf.div(b, a)))              [[0 0] [1 1]]
	print(sess.run(tf.divide(b, a)))           [[0. 0.5] [1. 1.5]]
	print(sess.run(tf.truediv(b, a)))          [[0. 0.5] [1. 1.5]]
	print(sess.run(tf.floordiv(b, a)))         [[0 0] [1 1]]
	print(sess.run(tf.realdiv(b, a)))          # Error: only works for real values
	print(sess.run(tf.truncatediv(b, a)))      [[0 0] [1 1]]
	print(sess.run(tf.floor_div(b, a)))        [[0 0] [1 1]]
  • tf.add_n

덧셈 연산을 한다. tf.add_n([a,b,b])a+b+b와 같다.

  • Dot product & matmul

tf.matmultf.tensordot을 구분해서 사용해야 한다. 전자는 rank가 2이상인 matrix에서 사용하는 것이다. 아래의 예시를 보자.

a = tf.constant([10, 20], name='a')
b = tf.constant([2, 3], name='b')
with tf.Session() as sess:
	print(sess.run(tf.multiply(a, b)))            [20 60] # element-wise multiplication
	print(sess.run(tf.tensordot(a, b, 1)))        80

Data Type

  • Python Native Type

파이썬의 기본 데이터 타입의 경우에는 텐서플로우에서 다음과 같이 취급 된다.

Data example in Tensor
Single value Bool,String,Numeric 0-d tensor(scalar)
List of value [‘a’,’b’,’c’], [2,3],[True,False] 1-d tensor(vector)
List of list [[1,2,], [2,3]] 2-d tensor(matrix)

파이썬 데이터 타입을 사용하면 예를들어 아래와 같이 적용된다.

t_0 = 19 # Treated as a 0-d tensor, or "scalar"
tf.zeros_like(t_0)                   # ==> 0

t_1 = [b"apple", b"peach", b"grape"] # treated as a 1-d tensor, or "vector"
tf.zeros_like(t_1)             

t_2 = [[True, False, False],
       [False, False, True],
       [False, True, False]]         # treated as a 2-d tensor, or "matrix"

tf.zeros_like(t_2)                   # ==> 3x3 tensor, all elements are False
  • TensorFlow Native Data

텐서플로우는 많은 데이터 타입을 가지고 있다. 전체 리스트는 다음과 같다.

  • tf.float16: 16-bit half-precision floating-point.
  • tf.float32: 32-bit single-precision floating-point.
  • tf.float64: 64-bit double-precision floating-point.
  • tf.bfloat16: 16-bit truncated floating-point.
  • tf.complex64: 64-bit single-precision complex.
  • tf.complex128: 128-bit double-precision complex.
  • tf.int8: 8-bit signed integer.
  • tf.uint8: 8-bit unsigned integer.
  • tf.uint16: 16-bit unsigned integer.
  • tf.uint32: 32-bit unsigned integer.
  • tf.uint64: 64-bit unsigned integer.
  • tf.int16: 16-bit signed integer.
  • tf.int32: 32-bit signed integer.
  • tf.int64: 64-bit signed integer.
  • tf.bool: Boolean.
  • tf.string: String.
  • tf.qint8: Quantized 8-bit signed integer.
  • tf.quint8: Quantized 8-bit unsigned integer.
  • tf.qint16: Quantized 16-bit signed integer.
  • tf.quint16: Quantized 16-bit unsigned integer.
  • tf.qint32: Quantized 32-bit signed integer.
  • tf.resource: Handle to a mutable resource.
  • tf.variant: Values of arbitrary types.

텐서플로우의 데이터 타입을 보면 Numpy의 데이터 타입과 비슷하게 생겼다는 것을 알 수 있다. 실제로도 두 데이터 타입은 같은 데이터 타입이다. 텐서플로우의 것이 넘파이의 데이터 타입을 기반으로 만들어졌기 때문이다. 따라서 tf.int32 == np.int32 와 같이 사용하면 True의 반환한다.

그리고 넘파이 데이터로 텐서플로우 데이터 생성시 run하기 전까지는 텐서플로우 데이터로 나오나 run이후에는 넘파이 데이터로 나온다.

sess = tf.Session()
a = tf.zeros([2, 3], np.int32)
print(type(a))  			# ⇒ <class 'tensorflow.python.framework.ops.Tensor'>
a = sess.run(a)
print(type(a))  			# ⇒ <class 'numpy.ndarray'>

Variables

Constant와 variable의 차이에 대해서 먼저 얘기해보자.

  1. constant는 constant이다. 즉 불변하다는 것이다. 학습과정에서 우리는 weight와 bias를 update해줘야 한다.
  2. constant 값은 graph에 저장된다. 그리고 graph가 로드될 때 마다 계속해서 복제된다. 그러나 variable은 각각 저장되어서 복제되지 않고 parameter서버에서 동시에 사용된다.

2번의 내용이 매우 중요하다. constant는 graph definition에 저장되는데 constant 자체가 매우 크면 계속해서 graph를 호출할 때 마다 복제되어서 시스템이 느려진다. 여기서 말하는 graph의 정의를 호출하는 방법은 다음과 같다.

import tensorflow as tf

my_const = tf.constant([1.0, 2.0], name="my_const")
print(tf.get_default_graph().as_graph_def())

결과는 다음과 같다.

node {
  name: "my_const"
  op: "Const"
  attr {
    key: "dtype"
    value {
      type: DT_FLOAT
    }
  }
  attr {
    key: "value"
    value {
      tensor {
        dtype: DT_FLOAT
        tensor_shape {
          dim {
            size: 2
          }
        }
        tensor_content: "\000\000\200?\000\000\000@"
      }
    }
  }
}
versions {
  producer: 24
}
  • Creating variables

변수를 선언하기 위해서는 우선 tf.Variable객체의 인스턴스를 만들어야한다.

tf.constant의 c는 소문자이고 tf.Variable의 V는 대문자이다. 그 이유는 constant는 연산이고 Variable은 연산을 포함하는 객체이기 때문이다.

변수를 만드는 고전적인 방법은 tf.Variable(<initial-value>, name=<optional-name>) 를 사용하는 것이다. 예시를 보자.

s = tf.Variable(2, name="scalar")
m = tf.Variable([[0, 1], [2, 3]], name="matrix")
W = tf.Variable(tf.zeros([784,10]))

하지만 이러한 고전적인 방법보다는 tf.get_variable을 사용하는 것을 추천한다. 이 방법을 통하면 변수들을 공유하는 것이 매우 쉬워진다. 사용하는 방법은 다음과 같다.

tf.get_variable(
    name,
    shape=None,
    dtype=None,
    initializer=None,
    regularizer=None,
    trainable=True,
    collections=None,
    caching_device=None,
    partitioner=None,
    validate_shape=True,
    use_resource=None,
    custom_getter=None,
    constraint=None
)

직접 tf.get_variable 사용해서 변수를 생성해보자.

s = tf.get_variable("scalar", initializer=tf.constant(2))
m = tf.get_variable("matrix", initializer=tf.constant([[0, 1], [2, 3]]))
W = tf.get_variable("big_matrix", shape=(784, 10), initializer=tf.zeros_initializer())
  • Initialize variables

변수를 선언만하고 값을 할당하지 않으면 FailedPreconditionError가 발생하면서 사용할 수 없다. 따라서 우리는 변수를 초기화 해야 하는데, 초기화에는 다양한 방법들이 있다. 우선은 방법들을 알아보기 전에 초기화되지 않은 변수들의 리스트를 확인하는 방법에 대해서 먼저 알아보자.

print(session.run(tf.report_uninitialized_variables()))

그리고 모든 변수를 한번에 초기화하는 방법은 다음과 같다.

with tf.Session() as sess:
	sess.run(tf.global_variables_initializer())

부분적으로 초기화 하거나 하나씩 초기화하는 방법은 다음과 같다.

#initialize subset of variables
with tf.Session() as sess:
	sess.run(tf.variables_initializer([a, b]))
#initialize each variables
with tf.Session() as sess:
  	sess.run(W.initializer)
  • Assign values to variables

변수에 값을 할당하는 방법은 tf.Varibale.assign()를 사용하는 것이다.

W = tf.Variable(10)
W.assign(100)
with tf.Session() as sess:
	sess.run(W.initializer)
	print(W.eval()) # >> 10

하지만 위와 같이 실행을 하면 W에 100이 할당되지 않은 것을 확인할 수 있다. 왜 그럴까? 이유는 assign을 선언만 해놓고 실제 실행을 하지 않았기 떄문이다.(assign도 하나의 연산이기 떄문에 run해줘야 적용된다.)

즉 아래와 같이 assign을 run을 해야 한다.

W = tf.Variable(10)

assign_op = W.assign(100)
with tf.Session() as sess:
	sess.run(assign_op)
	print(W.eval()) # >> 100

그리고 변수를 초기화할 떄 assign을 사용해서 초기화 할 수도 있다.

# in the source code
self._initializer_op = state_ops.assign(self._variable, self._initial_value,validate_shape=validate_shape).op

assign_add, assign_sub등 여러 할당 함수들이 있다.

W = tf.Variable(10)

with tf.Session() as sess:
	sess.run(W.initializer)
	print(sess.run(W.assign_add(10))) # >> 20
	print(sess.run(W.assign_sub(2)))  # >> 18
  • Interactive Session

Interacitve session을 사용하면 별다른 session을 호출할 필요없이 바로 run, eval을 할 수 있다. 간단한 방법이지만 만약 여러 session을 다루기에는 어려움이 있다.

sess = tf.InteractiveSession()
a = tf.constant(5.0)
b = tf.constant(6.0)
c = a * b
print(c.eval()) # we can use 'c.eval()' without explicitly stating a session
sess.close()
  • Importing Data

이제 텐서플로우 사용에서 가장 중요한 방법인 data를 불러오는 방법이다. 먼저 기존의 방식에 대해서 설명한다.

기존의 방식은 placeholders 와 feed_dict를 사용하는 방식이다.

paceholders란 데이터를 담아둘 곳이라고 생각하면 된다. 그리고 feed_dict를 통해 값을 할당하면 된다. 우선 placeholder를 정의하는 방법은 다음과 같다.

tf.placeholder(dtype, shape=None, name=None)

옵션을 보면 dtype은 데이터 타입을 뜻하고 name은 사용자가 지정할 이름을 뜻한다. 그리고 shape는 담을 데이터의 형태를 뜻하는데 만약 지정하지 않으면 어떤 데이터이든지 넣을 수 있다는 장점이 있지만 디버그과정에서는 문제가 되는 부분을 찾기가 매우 힘들어 진다. 따라서 웬만하면 shape를 지정하도록 하자.

이제 placeholder를 정의하고 feed_dict를 통해 값을 할당하는 방법에 대해 알아보자.

a = tf.placeholder(tf.float32, shape=[3]) # a is placeholder for a vector of 3 elements
b = tf.constant([5, 5, 5], tf.float32)
c = a + b # use the placeholder as you would any tensor

with tf.Session() as sess:
	# compute the value of c given the value of a is [1, 2, 3]
	print(sess.run(c, {a: [1, 2, 3]})) 		# [6. 7. 8.]

위와 같이 사용하면 된다. feed_dict는 iterative하게 사용가능하다.

with tf.Session() as sess:
	for a_value in list_of_a_values:
		print(sess.run(c, {a: a_value}))

그리고 feed_dict는 placeholder에만 사용되는 것이 아니고 일반 연산에서도 사용 가능하다.

a = tf.add(2, 5)
b = tf.multiply(a, 3)

with tf.Session() as sess:
	print(sess.run(b)) 						# >> 21
	# compute the value of b given the value of a is 15
	print(sess.run(b, feed_dict={a: 15})) 			# >> 45

위의 place_holder와 feed_dict는 데이터를 다루는 고전적인 방법이라고 배웠다. 그렇다면 최신의 방법은 무었일까 바로 tf.data를 사용하는 것이다. 이 방법에 대해서는 다음 강의에서 알아보도록 한다.

  • Lazy Loading

마지막으로 텐서플로우를 사용하면서 발생할 수 있는 문제에 대해서 소개한다. 우선 아래의 두 예시를 보자.

x = tf.Variable(10, name='x')
y = tf.Variable(20, name='y')
z = tf.add(x, y)

with tf.Session() as sess:
	sess.run(tf.global_variables_initializer())
	writer = tf.summary.FileWriter('graphs/normal_loading', sess.graph)
	for _ in range(10):
		sess.run(z)
	writer.close()
x = tf.Variable(10, name='x')
y = tf.Variable(20, name='y')

with tf.Session() as sess:
	sess.run(tf.global_variables_initializer())
	writer = tf.summary.FileWriter('graphs/lazy_loading', sess.graph)
	for _ in range(10):
		sess.run(tf.add(x, y))
	print(tf.get_default_graph().as_graph_def())
	writer.close()

예시를 보면 두 코드다 같이 반복적으로 add연산을 수행하고 있다. 하지만 실행해보면 아래의 코드가 속도가 더 느리다. 만약 반복횟수가 더 커지면 커질수록 느려질 것이다. 왜그럴까?

아래의 코드는 ‘add’ 노드를 연산 수행할 때마다 계속해서 정의하고 수행한다. 그에 반해 첫번째 코드는 연산을 먼저 정의한 후 정의한 연산을 반복 사용만하는 것이다. 따라서 아래의 코드에 비해 속도가 빠른것이다.

위와 같이 정의자체를 반복하도록하면 ‘add’ 노드 자체가 여러개 생겨서 속도가 느려질 것이다. 따라서 코딩을 할 때 연산의 경우 정의와 실행을 구분해서 사용하도록 하자.

Comments