TensorFlow2 A to Z

[Tensorflow2 강의자료] 3. Tensor Operations

Shin's Lab 2020. 9. 29. 19:53

Element-wise Operations in Tensorflow

Tensorflow의 tensor operation들은 NumPy의 영향을 크게 받았다. 그리고 NumPy에서 제공하는 ndarray 끼리의 사칙연산은 element-wise operation이다. 따라서 Tensorflow에서도 다음과 같은 연산결과를 얻을 수 있다.

t1 = tf.constant([10, 20, 30])
t2 = tf.Variable([1, 2, 3])

print(t1 + t2)
# tf.Tensor([11 22 33], shape=(3,), dtype=int32)

print(t1 - t2)
# tf.Tensor([ 9 18 27], shape=(3,), dtype=int32)

print(t1 * t2)
# tf.Tensor([10 40 90], shape=(3,), dtype=int32)

print(t1 / t2)
# tf.Tensor([10. 10. 10.], shape=(3,), dtype=float64)

print(t1 % t2)
# tf.Tensor([0 0 0], shape=(3,), dtype=int32)

print(t1 // t2)
# tf.Tensor([10 10 10], shape=(3,), dtype=int32)

 


Broadcasting Mechanism

Tensorflow의 tensor operation에 존재하는 broadcasting또한 NumPy의 broadcasting과 동일하게 작동한다.

위에서는 같은 수의 element를 가지는 vector들끼리의 operation이었다면 이번엔 다음과 같은 상황을 생각해보자.

t1 = tf.random.uniform(shape=(3, 4), minval=0, maxval=10)
t2 = tf.random.uniform(shape=(1, 4), minval=0, maxval=10)

print(t1.shape)
# (3, 4)

print(t2.shape)
# (1, 4)

 

수학적으로 이 두 matrices를 더하는 것은 정의되지 않는다. 하지만 우리는 scalar와 vector를 곱할 때 vector의 각 element마다 scalar를 곱해주는 것을 알고 있고, 이 개념을 이용한다면 t2라는 row vector를 t1에 row-wise로 더해주는 것이 자연스럽다고 느낄 수 있다.

실제로 많은 경우에 tensor operation을 할 때 shape을 맞춰주는 것은 어려운 것을 떠나 귀찮은 일이 된다. 이를 위해 Tensorflow는 broadcasting을 통해 이 자연스러운 연산을 진행한다.

t1 = tf.random.uniform(shape=(3, 4), minval=0, maxval=10)
t2 = tf.random.uniform(shape=(1, 4), minval=0, maxval=10)

t1 = tf.cast(t1, tf.int16)
t2 = tf.cast(t2, tf.int16)

print(t1.numpy())
'''
[[3 8 2 7]
 [5 6 3 9]
 [7 9 0 5]]
'''

print(t2.numpy())
'''
[[8 7 6 4]]
'''

print((t1 + t2).numpy())
'''
[[11 15  8 11]
 [13 13  9 13]
 [15 16  6  9]]
'''

위의 tf.cast는 data type을 바꿔주는 API이고 결과를 더 보기 쉽게 만들기 위해 사용했다.

결과에서 보이는 것처럼 t1은 (3, 4), t2는 (1, 4)였지만 t2를 (3, 4)로 broadcasting하여 연산을 진행했다. 이런 연산은 tabular data가 있고 각 data sample마다 일정한 연산을 해줄 때 매우 편리하다

 

broadcasting은 shape에 따라 기능이 달라지므로 다음의 연산에서는 위와 다른 결과가 나온다.

t1 = tf.random.uniform(shape=(3, 4), minval=0, maxval=10)
t2 = tf.random.uniform(shape=(3, 1), minval=0, maxval=10)

t1 = tf.cast(t1, tf.int16)
t2 = tf.cast(t2, tf.int16)

print(t1.numpy())
'''
[[3 2 8 3]
 [3 6 6 1]
 [0 9 2 8]]
'''

print(t2.numpy())
'''
[[6]
 [9]
 [7]]
'''

print((t1 + t2).numpy())
'''
[[ 9  8 14  9]
 [12 15 15 10]
 [ 7 16  9 15]]
'''

이번에는 t2가 (3, 1)이었으므로 coloumn-wise로 broadcasting되어 연산했다. 마지막으로 서로 row vector, column vector일 때의 연산은 다음과 같다.

t1 = tf.random.uniform(shape=(3, 1), minval=0, maxval=10)
t2 = tf.random.uniform(shape=(1, 4), minval=0, maxval=10)

t1 = tf.cast(t1, tf.int16)
t2 = tf.cast(t2, tf.int16)

print(t1.numpy())
'''
[[3]
 [6]
 [9]]
'''

print(t2.numpy())
'''
[[5 2 5 1]]
'''

print((t1 + t2).numpy())
'''
[[ 8  5  8  4]
 [11  8 11  7]
 [14 11 14 10]]
'''

위의 결과에서 알 수 있듯이 (3, 1)과 (1, 4)를 연산하기 위해서 모두 (3, 4)로 shape을 바꿔준 뒤 연산이 진행됐다. 이처럼 broadcasting을 잘 사용하면 for문을 이용할 필요없이 더 빠르고 쉽게 연산을 진행할 수 있다.

 

 

 


tf.reduce Family

tf에는 다양한 reduce API들이 있다. 먼저 나열해보면 다음과 같다.

  • tf.reduce_sum: element의 summation을 구함
  • tf.reduce_prod: element의 곱을 구함

 

  • tf.reduce_max: 최댓값 구함
  • tf.reduce_min: 최솟값 구함

 

  • tf.reduce_mean: 평균 구함
  • tf.reduce_std: 표준편차 구함
  • tf.reduce_variance: 분산 구함

 

  • tf.reduce_all: AND 연산
  • tf.reduce_any: OR 연산

위에 나열된 reduce family들은 기본적인 사용법이 동일하다. 따라서 대표로 tf.reduce_sum을 이용해 사용법을 살펴보자.

 

t1 = tf.random.uniform((3, 3), maxval=10, dtype=tf.int32)

print(t1.numpy())
'''
[[3 6 1]
 [1 8 4]
 [9 2 9]]
'''
print(tf.reduce_sum(t1).numpy())
'''
43
'''

위와 같이 tf.reduce_sum에 tensor만 넣어주면 모든 element의 합을 구해준다. 하지만 이런 경우보다는 axis를 설정해주는 경우가 많으므로 axis argument를 사용하는 방법에 대해 알아보자. 이 axis는 생각보다 헷갈려하는 사람이 많으므로 꼭 스스로 연습을 할 필요가 있다.

 

t1 = tf.random.uniform((2, 3), maxval=10, dtype=tf.int32)
print(t1.numpy())
'''
[[4 1 2]
 [0 4 3]]
'''

t2 = tf.reduce_sum(t1, axis=0)
print(t2.numpy())
'''
[4 5 5]
'''

위에서 보면 같은 column에 있는 값들을 모두 더한 것을 알 수 있다. 이를 다시 말하면 첫 번째 dimension인 2를 없앴다고 생각할 수 있다.  

우리의 data들은 보통 row에는 sample, column에는 attribute가 들어있다. 이때 모든 sample들에 들어있는 attribute값들을 더한게 바로 axis=0이라고 생각할 수 있다. 이때 알아둬야 할 점은 첫 번째 dimension을 기준으로 더했기 때문에 결과는 3개의 element를 가지는 vector가 된다.

 

반대로 axis=1로 설정하면 다음과 같다.

t1 = tf.random.uniform((2, 3), maxval=10, dtype=tf.int32)
print(t1.numpy())
'''
[[1 1 1]
 [7 2 1]]
'''

t2 = tf.reduce_sum(t1, axis=1)
print(t2.numpy())
'''
[ 3 10]
'''

이번엔 두 번째 axis를 기준으로 더했기 때문에 두 번째 dimension에 있는 값들을 모두 더한 것이다. 즉 data들이 각자 가지고 있는 attribute들의 합이 된다. 여기서도 마찬가지로 두 번째 dimension인 3을 기준으로 더했기 때문에 결과는 2개의 element를 가지는 vector가 된다.

 

이 개념을 확실히 다지기 위해 3차원 tensor를 이용해서 복습해보자.

t1 = tf.random.uniform((2, 3, 4), maxval=10, dtype=tf.int32)
print(t1.numpy())
'''
[[[4 3 5 8]
  [6 6 0 7]
  [3 2 5 8]]

 [[1 2 7 6]
  [4 8 1 3]
  [9 1 7 6]]]
'''

t2 = tf.reduce_sum(t1, axis=0)
print(t2.numpy())
'''
[[ 5  5 12 14]
 [10 14  1 10]
 [12  3 12 14]]
'''

t1이 (2, 3, 4)의 shape을 가지는 tensor일 때 axis=0으로 더해보면 결과는 (3, 4)가 된다. t1은 (3, 4)짜리 matrix를 두 개 곂쳐놨다고 생각할 수 있고, axis=0으로 더하면 이 두 행렬을 element-wise로 더했다고 생각할 수 있다. 여기서는 첫 번째 dimension 2를 기준으로 더했기 때문에 결과는 (3, 4)가 된다.

 

다음의 예제는 RGB channel을 가지는 128x128의 이미지를 다루는 예제이다.

t1 = tf.random.uniform((128, 128, 3), maxval=10, dtype=tf.int32)
print(t1.shape)
'''
(128, 128, 3)
'''

t2 = tf.reduce_sum(t1, axis=2)
print(t2.shape)
'''
(128, 128)
'''

위와 같이 t1이라는 이미지에 대해 channel에 있는 모든 값을 합하고 싶다면 axis=2로 설정해주면 된다. channel-wise로 더했기 때문에 결과는 128x128의 gray image가 되는 것이다. 물론 uint8을 넘어가겠지만 개념적으로는 그렇다.

 

위와 같이 axis에 대한 개념을 잘 잡았다면 나머지 tf.reduce family들은 사용방법이 동일하다.