기본적으로 텐서플로는 텐서(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)
'[독파하기] 핸즈온 머신러닝 > 12장 - 텐서플로를 사용한 사용자 정의 모델과 훈련' 카테고리의 다른 글
12.3 사용자 정의 모델과 훈련 알고리즘 - (1) (0) | 2022.03.26 |
---|---|
12.1 텐서플로 훑어보기 (0) | 2022.03.19 |
댓글