본문 바로가기
[독파하기] 핸즈온 머신러닝/12장 - 텐서플로를 사용한 사용자 정의 모델과 훈련

12.2 넘파이처럼 텐서플로 사용하기

by Bebsae 2022. 3. 21.

기본적으로 텐서플로는 텐서(tensor)를 사용한다. 텐서는 차원에 따라 스칼라 혹은 배열이 될 수 있다. 그리고, 텐서는 넘파이 배열(ndarray)과 유사하기 때문에 넘파이 패키지가 익숙하면 좋다.

 

텐서의 생성

텐서를 생성하기 위해 tf.constant() 함수에 텐서로 만들기 원하는 값을 인자로 전달하면 된다.

import tensorflow as tf

tf.constant([1, 2, 3])
>>>
<tf.Tensor: shape=(3,), dtype=int32, numpy=array([1, 2, 3])>

 # 다차원 배열
tf.constant([[1, 2, 3], [4, 5, 6]])
>>>
<tf.Tensor: shape=(2, 3), dtype=int32, numpy=
array([[1, 2, 3],
       [4, 5, 6]])>

# 데이터 타입 명시
tf.constant([1, 2, 3, 4, 5, 6], dtype=tf.float32)
>>>
<tf.Tensor: shape=(6,), dtype=float32, numpy=array([1., 2., 3., 4., 5., 6.], dtype=float32)>

 

텐서의 인덱싱

텐서는 넘파이 배열 혹은 파이썬의 리스트처럼 대괄호를 통해 인덱스 참조가 가능하다. 인덱싱을 통해 참조할 때에는 [start:end:step] 구조로 참조할 수 있다. (start부터 end까지 step간격으로)

t = tf.constant([[1, 2, 3], 
                 [4, 5, 6]])

# 모든 행(:)에 대해 1번째 열 이후(1:)
t[:, 1:]
>>>
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[2, 3],
       [5, 6]])>

# 모든 행(:)에 대해 2번째 열 간격(::2)
t[:, ::2]
>>>
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[1, 3],
       [4, 6]])>

# 마지막 차원에 대해 1번째 요소
t[..., 1]
>>>
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([2, 5])>

 

텐서의 연산

참고 : 텐서플로 이외에 keras.backend 내부에도 자체적인 저수준 API가 존재한다. 다른 케라스 구현에도 적용하려면 케라스 함수를 사용해야 한다. e.g Theano, CNTK 등 다른 딥러닝 프레임워크

t + 10
>>>
<tf.Tensor: shape=(2, 3), dtype=int32, numpy=
array([[11, 12, 13],
       [14, 15, 16]])>

# tf.square() : 제곱
tf.square(t)
>>>
<tf.Tensor: shape=(2, 3), dtype=int32, numpy=
array([[ 1,  4,  9],
       [16, 25, 36]])>

# tf.transpose() : 전치
# 주의 : 넘파이 배열처럼 전치행렬을 구할 때 (.T)를 사용할 수 없다.
t @ tf.transpose(t)
>>>
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[14, 32],
       [32, 77]])>
       
# tf.reduce_mean() : 평균
tf.reduce_mean(t)
>>>
<tf.Tensor: shape=(), dtype=int32, numpy=3>

# tf.reduce_prod() : 원소 곱
tf.reduce_prod(t)
>>>
<tf.Tensor: shape=(), dtype=int32, numpy=720>

tf.reduce_prod(t, axis=0)
>>>
<tf.Tensor: shape=(3,), dtype=int32, numpy=array([ 4, 10, 18])>

tf.reduce_prod(t, axis=1)
>>>
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([  6, 120])>

 

텐서와 넘파이 배열사이의 변환

# 텐서에서 넘파이 배열 변환
t.numpy()
>>>
array([[1, 2, 3],
       [4, 5, 6]])

# 넘파이 배열에서 텐서 변환
tf.constant(np.array([1, 2, 3]))
>>>
<tf.Tensor: shape=(3,), dtype=int32, numpy=array([1, 2, 3])>

np.square(t)
>>>
array([[ 1,  4,  9],
       [16, 25, 36]], dtype=int32)
       
tf.square(np.array([1, 2, 3]))
>>>
<tf.Tensor: shape=(3,), dtype=int32, numpy=array([1, 4, 9])>

 

타입 변환

텐서플로에서는 성능 이슈로 자동 타입 변환을 지원하지 않는다. 즉, 명시적으로 타입을 지정해주어야 한다. 주의해야 할 점은 같은 실수(float)이라도 float32와 float64는 연산을 지원하지 않는다.

# 타입 에러 (float과 integer의 연산)
tf.constant(2.) + tf.constant(40)

# 타입 명시적 선언 (float32와 float32)
tf.constant(2.) + tf.constant(40., dtype=tf.float32) 
tf.constant(2.) + tf.constant(40, dtype=tf.float32)
>>>
<tf.Tensor: shape=(), dtype=float32, numpy=42.0>

# tf.cast()를 통한 타입 캐스팅
t1 = tf.constant(30.) # tf.int32
t2 = tf.constant(40) # tf.float32
t1 + tf.cast(t2, dtype=tf.float32)
>>>
<tf.Tensor: shape=(), dtype=float32, numpy=70.0>

 

변수

지금까지 본 텐서는 상수(constant) 텐서이다. 말그대로 상수이기 때문에 값을 수정할 수 없다. 즉, 신경망에서 역전파로 인해 조정되어야 할 가중치 텐서가 상수로 구현되어 있으면 안된다. 고로, 값을 변경할 수 있는 변수(variable)을 알아야 한다.

v = tf.Variable([[1., 2., 3.], [4., 5., 6.]])
v
>>>
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[1., 2., 3.],
       [4., 5., 6.]], dtype=float32)>

assign() 함수를 통해 해당 텐서에 새로운 텐서를 할당할 수 있다. 이때 주의해야 할점은 mutable 함수이기 때문에 원본 데이터(텐서)에 바로 반영이 된다는 점이다.

# assign() : 해당 텐서에 새로운 텐서를 할당한다. (mutable)
v.assign(2 * v)
>>>
<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=
array([[ 2., 42.,  6.],
       [ 8., 10., 12.]], dtype=float32)>

# 0행 1열에 42 할당
v[0, 1].assign(42)
>>>
<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=
array([[ 2., 42.,  0.],
       [ 8., 10.,  1.]], dtype=float32)>

# 모든 행 2열에 0과 1 할당
v[:, 2].assign([0., 1.])
>>>
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[ 2., 42.,  0.],
       [ 8., 10.,  1.]], dtype=float32)>

# assign_add() : 인자로 주어진 값만큼 더한다.
v.assign_add(v)
>>>
<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=
array([[ 4., 84.,  0.],
       [16., 20.,  2.]], dtype=float32)>
       
# assign_sub() : 인자로 주어진 값만큼 뺀다.
v.assign_sub(v)
>>>
<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=
array([[0., 0., 0.],
       [0., 0., 0.]], dtype=float32)>

인덱싱을 통해 assign할 수도 있지만, scatter_nd_update 함수도 있다.

# scatter_nd_update() : indices에 해당하는 위치에 updates 값으로 업데이트 한다.
v.scatter_nd_update(indices=[[0, 0], [1, 2]],
                    updates=[100., 200.])
>>>
<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=
array([[100.,   0.,   0.],
       [  0.,   0., 200.]], dtype=float32)>

 

다른 데이터 구조

  • 문자열 텐서 (string tensor)
tf.constant(b"hello world")
>>>
<tf.Tensor: shape=(), dtype=string, numpy=b'hello world'>

# 바이트 텐서
tf.constant("café")
>>>
<tf.Tensor: shape=(), dtype=string, numpy=b'caf\xc3\xa9'>

# 바이트 텐서를 아스키 코드로 디코딩 (ord 사용)
u = tf.constant([ord(c) for c in "café"])
u
>>>
<tf.Tensor: shape=(4,), dtype=int32, numpy=array([ 99,  97, 102, 233])>

# 아스키 코드 텐서에서 바이트 텐서로 인코딩 (é = \xc3\xa9) 
b = tf.strings.unicode_encode(u, "UTF-8")
b
>>>
<tf.Tensor: shape=(), dtype=string, numpy=b'caf\xc3\xa9'>

# UTF-8 인코딩에서 문자의 갯수 출력 (문자 하나에 1바이트)
tf.strings.length(b, unit="UTF8_CHAR")
>>>
<tf.Tensor: shape=(), dtype=int32, numpy=4>

# 바이트 텐서를 아스키 코드로 디코딩 (tf.strings.unicode_decode 사용)
tf.strings.unicode_decode(b, input_encoding="UTF-8")
>>>
<tf.Tensor: shape=(4,), dtype=int32, numpy=array([ 99,  97, 102, 233])>
# 문자열 배열
p = tf.constant(["Café", "Coffee", "caffè", "咖啡"])
p
>>>
<tf.Tensor: shape=(4,), dtype=string, numpy=
array([b'Caf\xc3\xa9', b'Coffee', b'caff\xc3\xa8',
       b'\xe5\x92\x96\xe5\x95\xa1'], dtype=object)>
       
# 각 단어의 바이트 수 반환
tf.strings.length(p, unit="UTF8_CHAR")
>>>
<tf.Tensor: shape=(4,), dtype=int32, numpy=array([4, 6, 5, 2])>

# 각 단어(바이트 텐서)를 아스키 코드로 디코딩
r = f.strings.unicode_decode(p, "UTF8")
>>>
<tf.RaggedTensor [[67, 97, 102, 233], [67, 111, 102, 102, 101, 101], [99, 97, 102, 102, 232], [21654, 21857]]>

 

  • 레그드 텐서 (ragged tensor)

레그드 텐서는 리스트의 리스트를 나타낸다. 레그드 텐서를 위한 연산은 tf.ragged 패키지에서 제공된다.

r
>>>
<tf.RaggedTensor [[67, 97, 102, 233], 
                  [67, 111, 102, 102, 101, 101], 
                  [99, 97, 102, 102, 232], 
                  [21654, 21857]]>

# 새로운 레그드 상수 텐서 생성
r2 = tf.ragged.constant([[65, 66], [], [67]])
r2
>>>
<tf.RaggedTensor [[65, 66], 
                  [], 
                  [67]]>

# axis=0 : 행 방향으로 이어 붙임
tf.concat([r, r2], axis=0)
>>>
<tf.RaggedTensor [[67, 97, 102, 233], 
                  [67, 111, 102, 102, 101, 101], 
                  [99, 97, 102, 102, 232], 
                  [21654, 21857], 
                  [65, 66], 
                  [], 
                  [67]]>

# axis=1 : 열 방향으로 이어 붙임
r3 = tf.ragged.constant([[1], [2, 3], [4], []])
tf.concat([r, r3], axis=1)
>>>
<tf.RaggedTensor [[67, 97, 102, 233, 1], 
                  [67, 111, 102, 102, 101, 101, 2, 3], 
                  [99, 97, 102, 102, 232, 4], 
                  [21654, 21857]]>

# 레그드 텐서를 일반 텐서로 변환 (비어있는 공간은 0으로 padding)
r.to_tensor()
>>>
<tf.Tensor: shape=(4, 6), dtype=int32, numpy=
array([[   67,    97,   102,   233,     0,     0],
       [   67,   111,   102,   102,   101,   101],
       [   99,    97,   102,   102,   232,     0],
       [21654, 21857,     0,     0,     0,     0]])>

 

  • 희소 텐서 (sparse tensor)

대부분 0으로 채워진 텐서를 압축하여 효율적으로 나타낸다. 희소 텐서를 위한 연산은 tf.sparse 패키지에서 제공된다. 

# (3, 4) shape의 0으로 채워진 행렬을 생성 후
# [0, 1]위치에 1., [1, 0]위치에 2., [2, 3]위치에 3. 할당
s = tf.SparseTensor(indices=[[0, 1], [1, 0], [2, 3]],
                    values=[1., 2., 3.],
                    dense_shape=[3, 4])

print(s)
>>>
SparseTensor(indices=tf.Tensor(
[[0 1]
 [1 0]
 [2 3]], shape=(3, 2), dtype=int64), values=tf.Tensor([1. 2. 3.], shape=(3,), dtype=float32), dense_shape=tf.Tensor([3 4], shape=(2,), dtype=int64))

# 희소 텐서를 밀집 텐서로 변환 (압축 해제)
tf.sparse.to_dense(s)
>>>
tf.Tensor(
[[0. 1. 0. 0.]
 [2. 0. 0. 0.]
 [0. 0. 0. 3.]], shape=(3, 4), dtype=float32)

# values만 2배 커짐 (스케일링)
s2 = s * 2.0
print(s2)
>>>
SparseTensor(indices=tf.Tensor(
[[0 1]
 [1 0]
 [2 3]], shape=(3, 2), dtype=int64), values=tf.Tensor([2. 4. 6.], shape=(3,), dtype=float32), dense_shape=tf.Tensor([3 4], shape=(2,), dtype=int64))

# 덧셈, 뺄셈 연산을 지원하지 않음
try:
    s3 = s + 1.
except TypeError as e:
    print(e)

# 희소 텐서와 레그드 텐서 사이의 행렬곱
# 희소 텐서를 밀집텐서로 변환 후(3 x 4)
# 밀집 텐서 s4(4 x 2)와 행렬곱 수행
s4 = tf.constant([[10., 20.], [30., 40.], [50., 60.], [70., 80.]])
print(tf.sparse.sparse_dense_matmul(s, s4))
>>>
tf.Tensor(
[[ 30.  40.]
 [ 20.  40.]
 [210. 240.]], shape=(3, 2), dtype=float32)

# to_dense() 함수 호출시 indices는 정렬된 상태로 요구되기 때문에
# tf.sparse.reorder() 함수를 호출한 다음에 to_dense()로 변환해야 한다.
s5 = tf.SparseTensor(indices=[[0, 2], [0, 1]],
                     values=[1., 2.],
                     dense_shape=[3, 4])
print(s5)

try:
    tf.sparse.to_dense(s5)
except tf.errors.InvalidArgumentError as ex:
    print(ex)

# tf.sparse.reorder() 함수로 indices 재정렬
s6 = tf.sparse.reorder(s5)
tf.sparse.to_dense(s6)
>>>
<tf.Tensor: shape=(3, 4), dtype=float32, numpy=
array([[0., 2., 1., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]], dtype=float32)>

희소 텐서의 특징을 정리하자면

- 덧셈, 뺄셈 연산 불가

- indices가 정렬되어야 함

 

  • 집합 (set)

집합은 일반적인 텐서 혹은 희소 텐서로 표현된다. 예를 들면, tf.constant([[1, 2], [3, 4]])는 두 개의 집합 {1, 2}와 {3, 4}를 나타낸다. 일반적으로 각 집합은 텐서의 마지막 축에 있는 벡터에 의해 표현된다. 집합에 관련된 연산은  tf.sets 패키지에서 제공된다.

set1 = tf.constant([[2, 3, 5, 7], 
                   [7, 9, 0, 0]])
set2 = tf.constant([[4, 5, 6], 
                    [9, 10, 0]])

# 마지막 축의 벡터의 합집합(union)
u = tf.sets.union(set1, set2)
print(u)
>>>
SparseTensor(indices=tf.Tensor(
[[0 0]
 [0 1]
 [0 2]
 [0 3]
 [0 4]
 [0 5]
 [1 0]
 [1 1]
 [1 2]
 [1 3]], shape=(10, 2), dtype=int64), values=tf.Tensor([ 2  3  4  5  6  7  0  7  9 10], shape=(10,), dtype=int32), dense_shape=tf.Tensor([2 6], shape=(2,), dtype=int64))
 
# 합집합 희소 텐서를 밀집 텐서로 변환
# [1, 4], [1, 5] indices의 0 두 개는 희소 텐서를 밀집 텐서로 변환하는 과정에서 생성
print(tf.sparse.to_dense(u))
>>>
tf.Tensor(
[[ 2  3  4  5  6  7]
 [ 0  7  9 10  0  0]], shape=(2, 6), dtype=int32)
 
# 마지막 축의 벡터의 교집합(intersection)
i = tf.sets.intersection(set1, set2)
print(i)
>>>
SparseTensor(indices=tf.Tensor(
[[0 0]
 [1 0]
 [1 1]], shape=(3, 2), dtype=int64), values=tf.Tensor([5 0 9], shape=(3,), dtype=int32), dense_shape=tf.Tensor([2 2], shape=(2,), dtype=int64))

# 교집합 희소 텐서를 밀집 텐서로 변환
# [0, 1] indicies의 0은 희소 텐서를 밀집 텐서로 변환하는 과정에서 생성
print(tf.sparse.to_dense(i))
>>>
tf.Tensor(
[[5 0]
 [0 9]], shape=(2, 2), dtype=int32)
  • 큐 (queue)

 

댓글