[Tensorflow2 강의자료] 3. Tensor Operations
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들은 사용방법이 동일하다.