1. 텐서플로 자료구조
■ 텐서는 데이터를 다차원 배열 형태로 표현한 것으로 딥러닝에서는 텐서가 데이터를 표현하는 기본 단위로 사용된다.
■ 텐서의 차원(축)은 차수가 1씩 증가함에 따라 데이터 구조가 확장된다. 스칼라 \( \rightarrow \) 벡터 \( \rightarrow \) 행렬 \( \rightarrow \) 3차원 텐서 \( \rightarrow \) 4차원 텐서.... 이렇게 구조가 확장되면서 점이 선으로, 선이 면으로, 면이 입체로 변화한다.
- 위의 그림에서 랭크는 텐서의 차수를 의미한다.
■ 여기서 차수(차원의 수)는 텐서를 구성하는 벡터의 개수이다.
- 0차원 텐서인 스칼라는 텐서를 구성하는 벡터의 개수가 0 개, 1차원 텐서인 벡터는 벡터의 개수가 1개, 2차원 텐서인 행렬은 벡터의 개수가 2개, 3차원 텐서는 텐서를 구성하는 벡터의 개수가 3개, ....
■ 벡터는 방향과 크기를 동시에 나타내는 값으로 어떤 축 방향으로 어떤 양, 크기가 존재하는 것을 표현한다. 따라서 각 차원은 각각 고유의 정보를 나타내는 축이라고 이해할 수 있다.
■ 예를 들어 2 x 2 행렬에서 1차원 벡터 2개는 각각 행 방향과 열 방향을 나타내는 축이며, 이미지 데이터를 생각해 보면 3차원 텐서에서 각 축은 첫 번째 축이 높이(세로)에 대한 정보, 두 번째 축이 너비(가로)에 대한 정보, 세 번째 축이 채널에 대한 정보를 담고 있는 것으로 볼 수 있다.
1.1 스칼라(랭크-0 텐서)
■ 스칼라(또는 스칼라 텐서, 랭크-0 텐서, 0D 텐서)는 하나의 숫자이므로 양을 나타내기는 하지만, 벡터처럼 방향성은 갖지 않는다.
■ 스칼라를 랭크-0 텐서라고 하는 이유는 스칼라 텐서의 축 개수가 0이기 때문이다. 즉 벡터가 없는 0 차원으로 표현되기 때문에 랭크-0 텐서라고 정의한다.
import numpy as np
a = np.array(10)
print(type(a), a)
```#결과#```
<class 'numpy.ndarray'> 10
````````````
a.ndim # 넘파이 배열 차원(축 개수)
```#결과#```
0
````````````
■ 텐서플로에서 스칼라 텐서는 'constant( )' 함수에 상수 값을 입력해서 만들 수 있다.
import tensorflow as tf
a = tf.constant(10)
b = tf.constant(20)
print(type(a), a); print(b)
```#결과#```
<class 'tensorflow.python.framework.ops.EagerTensor'> tf.Tensor(10, shape=(), dtype=int32)
tf.Tensor(20, shape=(), dtype=int32)
````````````
- 정수 10과 20의 타입은 텐서(tf.Tensor)로 변환된 것을 볼 수 있다.
- 출력 결과에서 shape은 배열의 크기를 나타내는 값으로 정수 10과 20은 배열이 존재하지 않는 즉, 0차원이기 때문에 'shape = ( )' 값으로 나타난 것이다.
■ shape(형상)의 의미는 각 축에 따른 차원(원소의 수)을 의미한다.
cf) dtype은 텐서에 저장된 값의 자료형으로, int32는 32비트 정수형이라는 뜻이다.
■ 만약, 텐서 객체의 랭크(차수)만 따로 확인하고 싶다면, rank( ) 함수를 사용하면 된다.
print(tf.rank(a))
```#결과#```
tf.Tensor(0, shape=(), dtype=int32)
````````````
■ 딥러닝 연산에서 숫자형 데이터는 float32를 기본 자료형으로 사용한다. 만약, 숫자 데이터의 자료형을 float32로 바꾸고 싶다면 cast( ) 함수를 사용하면 된다.
a = tf.cast(a, tf.float32) # 텐서 객체 a의 자료형을 float32로
b = tf.cast(b, tf.float32) # 텐서 객체 b의 자료형을 float32로
print(a); print(b)
```#결과#```
tf.Tensor(10.0, shape=(), dtype=float32)
tf.Tensor(20.0, shape=(), dtype=float32)
````````````
■ 스칼라 텐서 간의 덧셈, 뺄셈, 곱셈, 나눗셈, mod 등의 연산을 수행하고자 할 때, 다음과 같이 math 모듈을 사용하면 된다.
print(tf.math.add(a, b)) # 덧셈
print(tf.math.subtract(a, b)) # 뺄셈
print(tf.math.multiply(a, b)) # 곱셈
print(tf.math.divide(a, b)) # 나눗셈
print(tf.math.mod(a, b)) # 나눗셈(나머지)
print(tf.math.floordiv(a, b)) # 나눗셈(몫)
```#결과#```
tf.Tensor(30.0, shape=(), dtype=float32)
tf.Tensor(-10.0, shape=(), dtype=float32)
tf.Tensor(200.0, shape=(), dtype=float32)
tf.Tensor(0.5, shape=(), dtype=float32)
tf.Tensor(10.0, shape=(), dtype=float32)
tf.Tensor(0.0, shape=(), dtype=float32)
`````````````
1.2 벡터(랭크-1 텐서)
■ 벡터는 여러 개의 스칼라(숫자) 값을 원소로 갖는 1차원 배열로 표현되기 때문에, 스칼라 값 여러 개가 동일한 축 방향으로 나열되는 것으로 볼 수 있다.
- 예를 들어 1, 2, 3, 4, 5로 나타낼 경우 이는 단순한 숫자의 나열이지만, a = [1, 2, 3, 4, 5]로 나타낼 경우 스칼라 1, 2, 3, 4, 5는 [ ] 안에 들어가 동일한 축 방향으로 나열되며, 여러 개의 값들이 모여서 a라는 하나의 대표성을 갖는 값이 된다.
- 이렇게 벡터의 원소가 되는 스칼라 1, 2, 3, 4, 5가 모여서 하나의 축을 갖는 벡터가 된다.
- 벡터는 위의 그림처럼 하나의 축을 갖기 때문에 차수가 1이라서 '랭크-1'텐서, 1D 텐서라고도 한다.
■ 좌표 공간으로 벡터를 나타냈을 때 벡터는 어떤 방향으로 크기를 갖는데, 원소들이 나열되는 순서가 달라지면 다음과 같이 다른 방향을 가리키게 된다. 따라서 원소들이 나열되는 순서도 의미가 있다.
a = np.array([1, 2, 3, 4, 5])
a
```#결과#```
array([1, 2, 3, 4, 5])
````````````
```#결과#```
1
````````````
- 이 벡터는 5개의 원소를 가지므로 5D 벡터(5차원 벡터)로 하나의 동일한 축을 따라 5개의 차원을 가진 것이다.
cf) 5D 텐서는 5개의 '축'을 가진 것이다. 각 축을 따라 여러 개의 차원을 가진 벡터가 존재할 수 있다.
■ 텐서플로에서는 파이썬 리스트, 넘파이 배열로 만든 1차원 배열을 constant( ) 함수에 입력하면 1차원 텐서인 벡터로 변환된다.
# 1차원 배열 - 리스트와 넘파이 배열
vec_list = [1, 2, 3] # 리스트
vec_arr = np.array([10., 20., 30.]) # 넘파이 배열
# 랭크-1 텐서 변환
vec_1 = tf.constant(vec_list, dtype = tf.float32)
vec_2 = tf.constant(vec_arr, dtype = tf.float32)
print(vec_1);print(vec_2)
```
# 결과
tf.Tensor([1. 2. 3.], shape=(3,), dtype=float32)
tf.Tensor([10. 20. 30.], shape=(3,), dtype=float32)
```
- 벡터의 shape은 (원소 개수, ) 형태로 표시된다. 따라서 이 예에서 텐서의 shape 속성 (3, )은 1개의 축에 3개의 원소가 있다는 뜻. 즉, 이 벡터는 3차원(원소의 수가 3개)이다.
■ 텐서의 크기는 shape x data type(bit)으로 계산할 수 있다.
- 이 예에서 텐서의 차원은 3, 데이터 타입은 32 bit = 4byte이다. 따라서 이 텐서는 3 x 4 byte = 12 byte를 차지한다.
■ shape이 (3, )이므로 shape을 통해 축이 1개임을 알 수 있지만, 스칼라 텐서와 마찬가지로 rank( ) 함수를 통해 텐서의 차수를 확인할 수 있다.
print(tf.rank(vec_1));print(tf.rank(vec_2))
```#결과#```
tf.Tensor(1, shape=(), dtype=int32)
tf.Tensor(1, shape=(), dtype=int32)
````````````
■ 마찬가지로 math 모듈을 통해 덧셈, 뺄셈, 곱셈 등의 산술 연산을 수행할 수 있다. 이때 같은 위치에 있는 원소들끼리 짝을 이루어 계산되어 이 예에서는 계산 결과, 원소 3개를 갖는 벡터(랭크-1텐서) 형태가 그대로 유지되는 것을 볼 수 있다.
- 파이썬 내장 덧셈 연산자 '+'를 통해서도 계산이 가능하다.
add_1 = tf.math.add(vec_1, vec_2)
add_2 = vec_1 + vec_2
print('add_1 result:', add_1); print('add_1 rank:', tf.rank(add_1))
```#결과#```
add_1 result: tf.Tensor([11. 22. 33.], shape=(3,), dtype=float32)
add_1 rank: tf.Tensor(1, shape=(), dtype=int32)
````````````
print('add_2 result:', add_2); print('add_2 rank:', tf.rank(add_2))
```#결과#```
add_2 result: tf.Tensor([11. 22. 33.], shape=(3,), dtype=float32)
add_2 rank: tf.Tensor(1, shape=(), dtype=int32)
````````````
- 나머지 연산들도 math 모듈을 사용하거나 파이썬 내장 연산자를 이용해 산술 연산을 처리할 수 있으며, 같은 위치에 있는 원소들끼리 연산된다.
# 뺄셈
print(tf.math.subtract(vec_1, vec_2))
print(vec_1 - vec_2)
```#결과#```
tf.Tensor([ -9. -18. -27.], shape=(3,), dtype=float32)
tf.Tensor([ -9. -18. -27.], shape=(3,), dtype=float32)
````````````
# 곱셈
print(tf.math.multiply(vec_1, vec_2))
print(vec_1 * vec_2)
```#결과#```
tf.Tensor([10. 40. 90.], shape=(3,), dtype=float32)
tf.Tensor([10. 40. 90.], shape=(3,), dtype=float32)
````````````
# 나눗셈
print(tf.math.divide(vec_1, vec_2))
print(vec_1 / vec_2)
```#결과#```
tf.Tensor([0.1 0.1 0.1], shape=(3,), dtype=float32)
tf.Tensor([0.1 0.1 0.1], shape=(3,), dtype=float32)
````````````
# 나눗셈(나머지)
print(tf.math.mod(vec_1, vec_2))
print(vec_1 % vec_2)
```#결과#```
tf.Tensor([1. 2. 3.], shape=(3,), dtype=float32)
tf.Tensor([1. 2. 3.], shape=(3,), dtype=float32)
````````````
# 나눗셈(몫)
print(tf.math.floordiv(vec_1, vec_2))
print(vec_1 // vec_2)
```#결과#```
tf.Tensor([0. 0. 0.], shape=(3,), dtype=float32)
tf.Tensor([0. 0. 0.], shape=(3,), dtype=float32)
````````````
■ 이외에도 거듭제곱, 제곱근 연산을 처리할 수 있다.
# 거듭제곱
print(tf.math.square(vec_1))
print(vec_1**2)
```#결과```
tf.Tensor([1. 4. 9.], shape=(3,), dtype=float32)
tf.Tensor([1. 4. 9.], shape=(3,), dtype=float32)
````````````
# 제곱근
print(tf.math.sqrt(vec_2))
print(vec_2**0.5)
```#결과#```
tf.Tensor([3.1622777 4.472136 5.477226 ], shape=(3,), dtype=float32)
tf.Tensor([3.1622777 4.472136 5.477226 ], shape=(3,), dtype=float32)
````````````
■ reduce_sum 함수를 사용하면 벡터 원소들의 합계를 구할 수 있다.
print(tf.reduce_sum(vec_1)) # vec_1 = 1, 2, 3
print(tf.reduce_sum(vec_2)) # vec_2 = 10, 20, 30
```
# 결과
tf.Tensor(6.0, shape=(), dtype=float32)
tf.Tensor(60.0, shape=(), dtype=float32)
```
■ 또한, 텐서 배열의 경우 넘파이 배열의 브로드캐스팅 연산이 가능하다.
# 브로드캐스팅 연산
print (vec_1 + 10)
```
# 결과
tf.Tensor([11. 12. 13.], shape=(3,), dtype=float32)
```
1.3 행렬(랭크-2 텐서)
■ 행렬은 차수가 1인 벡터를 동일한 축 방향으로 나열한 것으로 볼 수 있다.
이렇게 행렬에는 2개의 축이 있어 '랭크-2 텐서', 2D 텐서라고도 한다.
a = np.array([ [1, 2],
[3, 4] ])
a
```#결과#```
array([[1, 2],
[3, 4]])
````````````
a.ndim
```#결과#```
2
````````````
■ 랭크-2 텐서를 만드는 방법도 위의 넘파이 배열처럼 리스트 원소를 갖는 리스트를 만든 다음, constant 함수에 입력하여 텐서로 변환하면 된다. 2차원 배열이 입력값으로 전달되었기 때문에 함수가 반환하는 텐서는 2차원이 된다.
lst = [[1, 2], [3, 4]] # constant 함수값으로 사용할 2차원 배열
mat_1 = tf.constant(lst) # constant 함수에 2차원 배열 넣기
print(mat_1);print(tf.rank(mat_1))
```#결과#```
tf.Tensor(
[[1 2]
[3 4]], shape=(2, 2), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
````````````
lst2 = [ [1, 2], [3, 4], [5, 6] ]
mat_2 = tf.constant(lst2)
print(mat_2)
```
# 결과
tf.Tensor(
[[1 2]
[3 4]
[5 6]], shape=(3, 2), dtype=int32)
```
- 행 방향과 열 방향, 2개의 축이 존재하므로 행렬의 차수는 2이며, rank 함수를 이용해 차수를 확인해 보면 랭크도 2임을 볼 수 있다.
- 행렬의 shape은 (행의 개수, 열의 개수)이며, 행의 개수는 2차원 배열을 구성하는 벡터의 개수, 열의 개수는 각 벡터를 구성하는 원소의 개수가 된다.
- 이 예에서는 2채원 배열을 구성하는 벡터가 [1, 2]와 [3, 4], [5, 6]으로 세 개, 각 벡터를 구성하는 원소의 개수는 2개이므로, 두 번째 행렬의 shape은 (3, 2)가 되는 것이다.
- 이 텐서의 dtype을 보면 int32이다. 즉, 각 원소당 int 32를 차지하므로 이 랭크 2-텐서는 3 x 2 x 4 byte = 24 byte의 크기를 갖는다.
cf) 반대로, numpy( ) 메서드를 텐서 객체에 적용하면 텐서를 넘파이 배열로 변환할 수 있다.
numpy_arr = mat_1.numpy()
print(type(numpy_arr)); print(numpy_arr)
```#결과#```
<class 'numpy.ndarray'>
[[1 2]
[3 4]]
````````````
■ stack 함수를 사용해서 랭크-2 텐서를 만들 수도 있다. 다음과 같이 constant( ) 함수로 만든 1차원 벡터 2개에 stack( ) 함수를 적용하여 2차원으로 결합하면 된다.
a_list = [10, 20] # 파이썬 리스트
b_numpy_arr = np.array([40, 50]) # 넘파이 배열
vec_1 = tf.constant(a_list, dtype = tf.int32)
vec_2 = tf.constant(b_numpy_arr, dtype = tf.int32)
vec_1, vec_2
```#결과#```
(<tf.Tensor: shape=(2,), dtype=int32, numpy=array([10, 20])>,
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([40, 50])>)
````````````
mat_2 = tf.stack([vec_1, vec_2])
print(mat_2);print(tf.rank(mat_2))
```#결과#```
tf.Tensor(
[[11 22]
[43 54]], shape=(2, 2), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
````````````
■ 행렬도 math 모듈의 수학 함수들을 사용할 수 있으며, 계산은 같은 위치에 있는 원소들끼리 짝을 지어 계산된다.
# 덧셈
add_mat_1 = tf.math.add(mat_1, mat_2)
print(add_mat_1);print(tf.rank(add_mat_1))
add_mat_2 = mat_1 + mat_2
print(add_mat_2);print(tf.rank(add_mat_2))
```#결과#```
tf.Tensor(
[[11 22]
[43 54]], shape=(2, 2), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
tf.Tensor(
[[11 22]
[43 54]], shape=(2, 2), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
````````````
# 뺄셈
sub_mat_1 = tf.math.subtract(mat_1, mat_2)
print(sub_mat_1);print(tf.rank(sub_mat_1))
sub_mat_2 = mat_1 - mat_2
print(sub_mat_2);print(tf.rank(sub_mat_2))
```#결과#```
tf.Tensor(
[[ -9 -18]
[-37 -46]], shape=(2, 2), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
tf.Tensor(
[[ -9 -18]
[-37 -46]], shape=(2, 2), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
````````````
# 곱셈
mul_mat_1 = tf.math.multiply(mat_1, mat_2)
print(mul_mat_1);print(tf.rank(mul_mat_1))
mul_mat_2 = mat_1 * mat_2
print(mul_mat_2);print(tf.rank(mul_mat_2))
```#결과#```
tf.Tensor(
[[ 10 40]
[120 200]], shape=(2, 2), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
tf.Tensor(
[[ 10 40]
[120 200]], shape=(2, 2), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
````````````
# 나눗셈
div_mat_1 = tf.math.divide(mat_1, mat_2)
print(div_mat_1);print(tf.rank(div_mat_1))
div_mat_2 = mat_1 / mat_2
print(div_mat_2);print(tf.rank(div_mat_2))
```#결과#```
tf.Tensor(
[[0.1 0.1 ]
[0.075 0.08 ]], shape=(2, 2), dtype=float64)
tf.Tensor(2, shape=(), dtype=int32)
tf.Tensor(
[[0.1 0.1 ]
[0.075 0.08 ]], shape=(2, 2), dtype=float64)
tf.Tensor(2, shape=(), dtype=int32)
````````````
- 행렬의 곱셈과 나눗셈도 위와 같이 같은 위치에 있는 원소들끼리 짝을 이루어 계산한다.
■ 선형대수에서 다루는 '스칼라곱' 연산과 '행렬곱' 연산도 브로드캐스팅 연산, matmul( ) 함수를 사용해서 처리할 수 있다.
# 스칼라곱
scalar_mat = tf.math.multiply(mat_1, 10)
print(scalar_mat);print(tf.rank(scalar_mat))
```#결과#```
tf.Tensor(
[[10 20]
[30 40]], shape=(2, 2), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
````````````
# 행렬곱
mat_mul = tf.matmul(mat_1, mat_2)
print(mat_mul);print(tf.rank(mat_mul))
```#결과#```
tf.Tensor(
[[ 90 120]
[190 260]], shape=(2, 2), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
````````````
1.4 고차원 텐서(랭크-3 텐서와 더 높은 랭크의 텐서)
■ 먼저 랭크-3 텐서는 여러 행렬들이 하나의 새로운 배열로 합쳐진 구조로 랭크-3 텐서, 3D 텐서라고 부른다.
cf) 3차원의 shape을 확인하는 방법은 예를 들어 다음과 같은 3차원 구조가 있다고 했을 때
[[[1 2 3]
[4 5 6]
[7 8 9]]
[[1 2 3]
[4 5 6]
[7 8 9]]]
안쪽부터 차원을 세면 된다.
- 가장 안쪽을 보면 하나의 리스트로 묶인 구조가 보이는데, 이 구조 안에 원소 3개가 있다. -> 3
- 그다음은 하나의 리스트로 묶인 [1 2 3]같은 원소가 [1 2 3], [4 5 6] [7 8 9]가 있는데, 이 3개의 원소는 중간 리스트에 묶여 있는 것을 볼 수 있다. 즉, 중간 리스트의 입장에서는 [1 2 3] 형태의 원소 3개를 가지는 것이다. -> 3
- 가장 바깥 쪽 리스트는 중간 리스트 [[ ]], [[ ]] 형태의 원소를 2개 가지는 것을 볼 수 있다. -> 2
- 따라서 이 예시의 차원은 (2, 3, 3)이다.
- dtype은 int32이므로 이 예시는 2 x 3 x 3 x 4 byte = 72 byte이다.
a = np.array([
[
[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]
],
[
[13, 14, 15, 16],
[17, 18, 19, 20],
[21, 22, 23, 24]
]
])
print(a.shape); print(a.ndim)
```#결과#```
(2, 3, 4)
3
````````````
■ 이렇게 랭크-1 텐서(벡터)를 동일한 축 방향으로 결합하면 랭크-2 텐서(행렬)가 되고, 랭크-2 텐서(행렬)를 동일한 축 방향으로 결합하면 랭크-3 텐서가 되고, 랭크-3 텐서도 동일한 축 방향으로 결합하면 랭크-4 텐서가 된다.
■ 다음과 같이 1차원(벡터)를 원소로 갖는 행렬(2차원)을 만드는 방식으로 랭크-3 텐서(1차원+2차원)를 만들 수 있다.
# 1차원 배열 정의
vec_1 = [1, 2, 3, 4]
vec_2 = [5, 6, 7, 8]
vec_3 = [9, 10, 11, 12]
vec_4 = [13, 14, 15, 16]
vec_5 = [17, 18, 19, 20]
vec_6 = [21, 22, 23, 24]
# 1차원 배열을 원소로 갖는 2차원 배열 정의
arr = [ [vec_1, vec_2],
[vec_3, vec_4],
[vec_5, vec_6] ]
# 텐서 변환
tensor_3 = tf.constant(arr)
print(tf.rank(tensor_3))
print(tensor_3)
```#결과#```
tf.Tensor(3, shape=(), dtype=int32)
tf.Tensor(
[[[ 1 2 3 4]
[ 5 6 7 8]]
[[ 9 10 11 12]
[13 14 15 16]]
[[17 18 19 20]
[21 22 23 24]]], shape=(3, 2, 4), dtype=int32)
``````````````
■ 또는 2차원 배열을 먼저 정의한 다음, constant 함수에 입력값으로 넣거나 stack 함수를 이용해 2차원 배열들을 결합하면 된다.
## 3차원 텐서
lst_mat_1 =[
[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]
]
lst_mat_2 =[
[13, 14, 15, 16],
[17, 18, 19, 20],
[21, 22, 23, 24]
]
tensor_1 = tf.constant([lst_mat_1, lst_mat_2])
print(tensor_1);print(tf.rank(tensor_1))
```#결과#```
tf.Tensor(
[[[ 1 2 3 4]
[ 5 6 7 8]
[ 9 10 11 12]]
[[13 14 15 16]
[17 18 19 20]
[21 22 23 24]]], shape=(2, 3, 4), dtype=int32)
tf.Tensor(3, shape=(), dtype=int32)
````````````
tensor_2 = tf.stack([lst_mat_1, lst_mat_2])
print(tensor_2);print(tf.rank(tensor_2))
```#결과#```
tf.Tensor(
[[[ 1 2 3 4]
[ 5 6 7 8]
[ 9 10 11 12]]
[[13 14 15 16]
[17 18 19 20]
[21 22 23 24]]], shape=(2, 3, 4), dtype=int32)
tf.Tensor(3, shape=(), dtype=int32)
````````````
■ 이 예에서 (2, 3, 4) 크기를 갖는 두 개의 3차원 배열을 stack 함수를 이용해 다시 하나의 배열로 묶으면 다음과 같이 4차원 텐서가 된다.
## 4차원 텐서
tensor_3 = tf.stack([tensor_1, tensor_2])
tensor_3
```#결과#```
<tf.Tensor: shape=(2, 2, 3, 4), dtype=int32, numpy=
array([[[[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]],
[[13, 14, 15, 16],
[17, 18, 19, 20],
[21, 22, 23, 24]]],
[[[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]],
[[13, 14, 15, 16],
[17, 18, 19, 20],
[21, 22, 23, 24]]]])>
````````````
- 안쪽부터 보면 (3, 4) 크기를 갖는 2차원 행렬이 두 개씩 모여 (2, 3, 4) 형태의 3차원 텐서가 된다. 그리고 이러한 (2, 3, 4) 크기의 3차원 텐서가 두 개 있으므로 전체 텐서는 (2, 2, 3, 4)의 4차원 텐서로 볼 수 있다.
■ 이렇게 텐서의 속성은 ① 축의 개수(랭크), ② 크기(shape), ③ 데이터 타입으로 볼 수 있다.
- ① 축의 개수(랭크)는 예를 들어 축이 2개면 랭크-2 텐서, 축이 3개면 랭크-3 텐서
- ② 크기(shape)는 텐서의 각 축을 따라 얼마나 많은 차원이 있는지를 나타낸다.
- ③ 데이터 타입은 텐서에 포함된 데이터의 타입이다. float16, float32, float64, unit8 등이 될 수 있다.
■ MNIST 데이터 셋을 예로 들면
from tensorflow.keras.datasets import mnist
(train_set, train_labels), (test_set, test_labels) = mnist.load_data()
print(train_set.ndim, test_set.ndim);print(train_labels.ndim, test_labels.ndim)
```#결과#```
3 3
1 1
````````````
- train_set과 test_set은 랭크-3 텐서, 정답(레이블)은 랭크-1 텐서이다.
print(train_set.shape)
print(train_set.shape[0]);print(train_set.shape[1:3])
```#결과#```
(60000, 28, 28)
60000
(28, 28)
````````````
train_set.dtype
```#결과#```
dtype('uint8')
````````````
train_set[0].shape
```#결과#```
(28, 28)
````````````
import matplotlib.pyplot as plt
plt.imshow(train_set[0], cmap = plt.cm.binary) # 흑백
plt.show()
- train_set은 8비트 정수형 랭크-3 텐서이며, 28 x 28 크기의 이미지 6만 개, 좀 더 정확하게는 이미지 하나가 28 x 28 크기의 정수 행렬이므로 28 x 28 크기의 정수 행렬 6만 개가 있는 배열로 볼 수 있으며, 각 행렬의 원소는 0에서 255 사이의 값을 갖는다.
cf) 배치 크기(= 데이터 개수)가 N이라면, 이미지 데이터의 shape은 텐서플로냐 파이토치냐에 따라 (N, height, width, channels) 또는 (N, channels, height, width)로 랭크-4 텐서로 나타낼 수 있다. shape을 해석하면 각 N 개 데이터의 픽셀은 height x width 2D이고 각 픽셀은 수치 값(channel)의 벡터이다. 컬러 이미지의 경우 red, blue, green 채널이 존재해 채널 수는 3이며, 흑백 이미지는 단일 컬러이므로 채널 수는 1이다.
2. 텐서 인덱싱(Indexing)
■ 파이썬 리스트, 넘파이 배열 인덱싱처럼 인덱스는 0부터 시작 & 마지막 인덱스는 -1이며, 인덱스로 원소를 추출할 수 있고, 위의 예에서 train_set의 첫 번째 데이터를 확인하기 위해 train_set[0]을 한 것처럼 배열에 있는 특정 원소들을 인덱싱할 수도 있고, [시작:끝]의 슬라이싱(slicing) 추출도 가능하다.
■ 1차원 구조 벡터의 인덱싱과 슬라이싱은 다음과 같다.
# 1차원 벡터 인덱싱
vec = tf.constant( [10, 20, 30, 40, 50] )
print(vec[0])
print(vec[-1])
```#결과#```
tf.Tensor(10, shape=(), dtype=int32)
tf.Tensor(50, shape=(), dtype=int32)
````````````
# 1차원 벡터 슬라이싱
print(vec[1:4])
print(vec[:3])
```#결과#```
tf.Tensor([20 30 40], shape=(3,), dtype=int32)
tf.Tensor([10 20 30], shape=(3,), dtype=int32)
````````````
■ 행 방향, 열 방향, 2개의 축을 갖는 2차원 구조 행렬 텐서는 행 방향의 인덱스와 열 방향의 인덱스를 지정해 원소를 추출한다. 예를 들어 [0, 1]이면 0행 1열에 위치한 원소를 추출한다.
(인덱스는 0부터 시작, 따라서 파이썬에서의 0행 1열은 선형대수에서 1행 2열)
■ 또한 [ 행 인덱스의 범위, 열 인덱스의 범위 ]로 인덱스 범위를 설정해서 슬라이싱도 가능하다.
예를 들어 [ 0, : ]이면 0행의 모든 열, 즉 1행의 모든 원소가 포함된 벡터가 추출되고 [ : ,1]은 모든 행의 2열 원소가 추출된다.
[ : , : ]은 모든 행과 모든 열이 되므로 원래 행렬이 추출된다.
# 행렬
mat = tf.constant( [ [10, 20, 30],
[40, 50, 60] ] )
# 인덱싱
print(mat[0, 0]);print(mat[0, 1]);print(mat[0, 2]);print(mat[1, 1])
```#결과#```
tf.Tensor(10, shape=(), dtype=int32)
tf.Tensor(20, shape=(), dtype=int32)
tf.Tensor(30, shape=(), dtype=int32)
tf.Tensor(50, shape=(), dtype=int32)
````````````
# 슬라이싱
print(mat[0, :]) 첫 번째 행 & 모든 열
print(mat[:, 1]) 모든 행 & 두 번째 열
print(mat[:, :]) # 모든 행 & 모든 열
```
```#결과#```
tf.Tensor([10 20 30], shape=(3,), dtype=int32)
tf.Tensor([20 50], shape=(2,), dtype=int32)
tf.Tensor(
[[10 20 30]
[40 50 60]], shape=(2, 3), dtype=int32)
`````````````
■ 랭크가 3 이상인 고차원 텐서의 인덱싱은, 예를 들어 3차원 텐서는 축이 3개 이므로 [ 축1, 축2, 축3 ] 형식으로 인덱스 또는 인덱스 범위를 지정하면 된다.
- 예를 들어 [ 0, :, : ]은 [(축1의) 인덱스 0, (축2의) 모든 인덱스, (축3의) 모든 인덱스]이므로, 3차원 텐서의 크기가 (2, 2, 3)이면,축1 방향으로 첫 번째 원소인 (2, 3)크기의 행렬의 모든 인덱스에 해당되는 원소가 추출된다.
■ 즉, 고차원 텐서의 인덱싱은 2차원 구조인 행렬 텐서의 개념이 확장되어 처리된다.
- 예를 들어 [ :, :2, :2 ]라면 축1 방향으로 모든 원소를 선택하므로 축1 방향을 구성하는 행렬이 모두 선택된다.
- 그리고 축2, 축3 방향으로 :2, :2 범위의 슬라이싱이 적용된다.
- 슬라이싱이 적용되는 대상은 크기가 (2, 3)인 행렬에서 행 인덱스 0 ~ 1, 열 인덱스 0 ~ 1 이므로 (2, 2) 크기의 행렬이 추출된다.
- 따라서 [ :, :2, :2 ]는 (2, 2) 크기의 행렬 2개를 원소로 갖는 3차원 텐서, 즉 (2, 2, 2) 크기의 3차원 텐서가 추출된다.
# 랭크-3 텐서
tensor = tf.constant(
[
[[10, 20, 30],
[40, 50, 60]],
[[-10, -20, -30],
[-40, -50, -60]],
]
)
print(tensor)
```#결과```
tf.Tensor(
[[[ 10 20 30]
[ 40 50 60]]
[[-10 -20 -30]
[-40 -50 -60]]], shape=(2, 2, 3), dtype=int32)
````````````
print(tensor[0, :, :])
```#결과#```
tf.Tensor(
[[10 20 30]
[40 50 60]], shape=(2, 3), dtype=int32)
````````````
print(tensor[:, :2, :2])
```#결과#```
tf.Tensor(
[[[ 10 20]
[ 40 50]]
[[-10 -20]
[-40 -50]]], shape=(2, 2, 2), dtype=int32)
`````````````
print(tensor[:, :2, :1]) # 축 1 방향으로 모든 원소 & 축 2 방향으로 행 인덱스 0~1, 열 인덱스 0
```#결과#```
tf.Tensor(
[[[ 10]
[ 40]]
[[-10]
[-40]]], shape=(2, 2, 1), dtype=int32)
````````````
■ 이렇게 슬라이싱을 이용하면 배치 데이터를 쉽게 설정할 수 있다.
- 예를 들어 배치 크기가 64이면, 다음과 같이 64씩 나눠지므로
train_set[:64].shape
```#결과#```
(64, 28, 28)
````````````
train_set[64:128].shape
```#결과#```
(64, 28, 28)
````````````
- n 번째 배치는 다음과 같이 나타낼 수 있다.
n = 10
train_set[64*n : 64*(n+1)].shape
```#결과#```
(64, 28, 28)
````````````
3. 텐서 형태(크기) 변환
■ 텐서의 형태를 reshape 하는 것은, 특정 크기에 맞게 축을 재배열하는 것이며 변환 전 텐서와 변환 후 텐서의 원소 개수는 동일해야 한다.
- reshape은 CNN에서 4차원 이미지 데이터를 2차원 행렬로 데이터를 전개하여 합성곱 연산을 수행할 때 유용하다.
- 예를 들어 순전파 과정에서는 im2col 방식을 사용해 4차원 이미지를 2차원 행렬로 변환하고, 역전파 과정에서는 col2um 방식을 통해 다시 원래 형태로 복원할 때 reshape을 사용할 수 있다.
■ 텐서의 reshape은 넘파이 reshape 함수와 사용법이 비슷하다.
■ 예를 들어 원소가 24개인 랭크-1 텐서의 shape은 (24, )가 된다. 이를 (3, 8) 형태의 행렬로 변환할 수 있으며, 변환 전과 변환 후의 원소 개수는 그대로 유지된다. (24 = 3 x 8)
## 넘파이
numpy_arr = np.array(range(0, 24))
print(numpy_arr);print(numpy_arr.shape)
```#결과#```
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
(24,)
````````````
## 텐서플로
# 랭크-1 텐서
tensor = tf.constant(range(0, 24))
print(tensor)
```#결과#```
tf.Tensor([ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23], shape=(24,), dtype=int32)
````````````
### (3, 8) 형태의 행렬(랭크-2 텐서)로 변환
## 넘파이
numpy_arr_1 = numpy_arr.reshape((3, 8))
print(numpy_arr_1);print(numpy_arr_1.shape)
```#결과#```
[[ 0 1 2 3 4 5 6 7]
[ 8 9 10 11 12 13 14 15]
[16 17 18 19 20 21 22 23]]
(3, 8)
`````````````
tensor_1 = tf.reshape(tensor, [3, 8])
print(tensor_1)
```#결과#```
tf.Tensor(
[[ 0 1 2 3 4 5 6 7]
[ 8 9 10 11 12 13 14 15]
[16 17 18 19 20 21 22 23]], shape=(3, 8), dtype=int32)
`````````````
■ 행 인덱스를 -1로 지정하면 음수 -1은 먼저 지정된 값에 따라 값을 결정한다.
- 예를 들어 크기가 (3, 8)인 행렬을 (6, 4)로 변환하는 과정은 [-1, 4]로 설정하면, 먼저 열의 크기인 4를 적용하고, 원소 개수와 배열의 형태에 따라 행의 크기 6를 결정하게 된다.
## 넘파이
numpy_arr_2 = numpy_arr_1.reshape((-1, 4))
print(numpy_arr_2);print(numpy_arr_2.shape)
```#결과#```
tf.Tensor(
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]
(6, 4)
`````````````
## 텐서플로
tensor_2 = tf.reshape(tensor_1, [-1, 4])
print(tensor_2)
```#결과#```
tf.Tensor(
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]
[16 17 18 19]
[20 21 22 23]], shape=(6, 4), dtype=int32)
`````````````
- 예시에서는 원소 개수 24를 열의 크기 4로 나눈 값인 6이 행의 크기가 되어 (3, 8) 형태의 행렬이 (6, 4) 형태의 행렬로 변환되는 것을 확인할 수 있다.
■ 그리고 '-1'을 이용하면 다차원 형태를 1차원인 벡터 형태로 변환할 수 있다.
- 다음과 같이 '-1'만 설정할 경우 원소 개수는 24개 이므로 tensor_3의 shape은 (24, )가 된다.
## 넘파이
numpy_arr_3 = numpy_arr_2.reshape((-1))
print(numpy_arr_3);print(numpy_arr_3.shape)
```#결과#```
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
(24,)
````````````
## 텐서플로
tensor_3 = tf.reshape(tensor_2, [-1])
print(tensor_3)
```#결과#```
tf.Tensor([ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23], shape=(24,), dtype=int32)
````````````
■ 랭크-1 텐서를 랭크-3 텐서로 변환할 수 있다. 다음은 (24, ) 형태를 (2, 3, 4) 형태로 변환하는 예시이다.
## 넘파이
numpy_arr_4 = numpy_arr_3.reshape((-1, 3, 4))
print(numpy_arr_4);print(numpy_arr_4.shape)
```#결과#```
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
(2, 3, 4)
`````````````
## 텐서플로
tensor_4 = tf.reshape(tensor_3, [-1, 3, 4])
print(tensor_4)
```#결과#```
tf.Tensor(
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]], shape=(2, 3, 4), dtype=int32)
`````````````
- (2, 3, 4) 즉, 크기가 (3, 4)인 행렬을 2개 만드는 것으로 생각하면, 음수 -1은 24 ÷ (3 × 4)로 계산되어, 축1에는 2가 설정된다.
■ 랭크-3 텐서의 형태를 또 다른 형태의 랭크-3 텐서로 변환할 수 있다. 다음은 (2, 3, 4) 형태를 (3, 2, 4)로 변환하는 예시이다.
## 넘파이
numpy_arr_5 = numpy_arr_4.reshape((3, 2, 4))
print(numpy_arr_5);print(numpy_arr_5.shape)
```#결과#```
[[[ 0 1 2 3]
[ 4 5 6 7]]
[[ 8 9 10 11]
[12 13 14 15]]
[[16 17 18 19]
[20 21 22 23]]]
(3, 2, 4)
`````````````
## 텐서플로
tensor_5 = tf.reshape(tensor_4, [3, 2, 4])
print(tensor_5)
```#결과#```
tf.Tensor(
[[[ 0 1 2 3]
[ 4 5 6 7]]
[[ 8 9 10 11]
[12 13 14 15]]
[[16 17 18 19]
[20 21 22 23]]], shape=(3, 2, 4), dtype=int32)
`````````````
■ 랭크-3 텐서를 랭크-4텐서로 변환할 수 있다. 다음은 (3, 2, 4) 형태를 (3, 1, 2, 4)로 변환하는 예시이다.
## 넘파이
numpy_arr_6 = numpy_arr_5.reshape((3, 1, 2, 4))
print(numpy_arr_6);print(numpy_arr_6.shape)
```#결과#```
[[[[ 0 1 2 3]
[ 4 5 6 7]]]
[[[ 8 9 10 11]
[12 13 14 15]]]
[[[16 17 18 19]
[20 21 22 23]]]]
(3, 1, 2, 4)
````````````
## 텐서플로
tensor_6 = tf.reshape(tensor_5, [3, 1, 2, 4])
print(tensor_6)
```#결과#```
tf.Tensor(
[[[[ 0 1 2 3]
[ 4 5 6 7]]]
[[[ 8 9 10 11]
[12 13 14 15]]]
[[[16 17 18 19]
[20 21 22 23]]]], shape=(3, 1, 2, 4), dtype=int32)
````````````
■ 만약, 이 랭크-4 텐서를 4차원 이미지 타입 (N, C, H, W)로 생각하면, (3, 1, 2, 4)라는 형상은 2 x 4 크기의 흑백 이미지 3 장으로 해석할 수 있다. 또한 다음과 같은 인덱싱을 통해 각 데이터에 접근하거나, 각 데이터의 채널 데이터에 접근할 수 있다.
## 넘파이
numpy_arr_6[0], numpy_arr_6[1] # 첫 번째, 두 번째 데이터에 접근
```#결과#```
(array([[[0, 1, 2, 3],
[4, 5, 6, 7]]]),
array([[[ 8, 9, 10, 11],
[12, 13, 14, 15]]]))
````````````
## 텐서플로
tensor_6[0], tensor_6[1] # 첫 번째, 두 번째 데이터에 접근
```#결과#```
(<tf.Tensor: shape=(1, 2, 4), dtype=int32, numpy=
array([[[0, 1, 2, 3],
[4, 5, 6, 7]]])>,
<tf.Tensor: shape=(1, 2, 4), dtype=int32, numpy=
array([[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])>)
````````````
## 넘파이
numpy_arr_6[0][0] # 첫 번째 데이터의 첫 채널의 공간 데이터, x[0, 0]도 같은 의미
```#결과#```
array([[0, 1, 2, 3],
[4, 5, 6, 7]])
````````````
numpy_arr_6[0][1] # 첫 번째 데이터의 두 번째 채널의 공간 데이터 -> 존재하지 않음
```#결과#```
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[374], line 1
----> 1 numpy_arr_6[0][1] # 첫 번째 데이터의 두 번째 채널의 공간 데이터 -> 존재하지 않음
IndexError: index 1 is out of bounds for axis 0 with size 1
````````````
## 텐서플로
tensor_6[0][0]
```#결과#```
<tf.Tensor: shape=(2, 4), dtype=int32, numpy=
array([[0, 1, 2, 3],
[4, 5, 6, 7]])>
````````````
tensor_6[0][1]
```#결과#```
InvalidArgumentError 발생
slice index 1 of dimension 0 out of bounds.
````````````
4. 변수
■ 텐서플로에서 변수는 다음 2가지 경우에 사용한다.
- 1) 텐서플로는 그래프 구조를 이용해 업데이트할 가중치 매개변수에 대해서 수많은 미분 연산을 반복 계산한다. 이때 변수를 사용하면 미분의 중간 연산 결과를 저장할 수 있다.
- 학습 중간 단계마다 모델의 가중치 행렬을 변수에 저장하며, 계산을 반복하면서 변수의 값을 업데이트한다.
- 2) 모델이 학습하는 속도인 학습률을 저장할 때 사용한다.
- 최적화 알고리즘을 적용하는 과정에서, 최적값을 찾기 위해 학습률을 조금씩 조정하는 경우가 있다. 이때 변수는 조정된 학습률 값을 저장하게 된다.
■ 텐서플로 변수를 생성하는 방법은 다음과 같이 텐서를 Variable( ) 함수에 입력하면 된다.
tensor_1 = tf.constant([[0.70592342, -1.07330552, 1.23598877],
[1.2146732, -0.5135574, -0.61834042]])
tensor_1
```#결과#```
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[ 0.70592344, -1.0733055 , 1.2359887 ],
[ 1.2146732 , -0.5135574 , -0.61834043]], dtype=float32)>
````````````
tensor_var_1 = tf.Variable(tensor_1)
tensor_var_1
```#결과#```
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[ 0.70592344, -1.0733055 , 1.2359887 ],
[ 1.2146732 , -0.5135574 , -0.61834043]], dtype=float32)>
````````````
- 입력한 텐서가 초기 가중치 매개변수 행렬이라면, 가중치가 업데이트되면 초깃값은 다른 값으로 변경된다.
■ 텐서플로 변수의 속성에는 '텐서플로 변수의 이름(name)', '크기(shape)', '자료형(dtype)', '배열'이 있다.
- 배열에 대한 정보는 numpy( ) 메소드를 적용시켜 확인할 수 있다.
print(f'name: {tensor_var_1.name}, shape: {tensor_var_1.shape}, dtype: {tensor_var_1.dtype},\narray: {tensor_var_1.numpy()}')
```#결과#```
name: Variable:0, shape: (2, 3), dtype: <dtype: 'float32'>,
array: [[ 0.70592344 -1.0733055 1.2359887 ]
[ 1.2146732 -0.5135574 -0.61834043]]
````````````
- 텐서플로 변수의 이름을 바꾸고 싶다면 name 인수에 사용하고자 하는 이름을 입력하면 된다.
tensor_var_2 = tf.Variable(tensor_1, name = 'tensor')
tensor_var_2
```#결과#```
<tf.Variable 'tensor:0' shape=(2, 3) dtype=float32, numpy=
array([[ 0.70592344, -1.0733055 , 1.2359887 ],
[ 1.2146732 , -0.5135574 , -0.61834043]], dtype=float32)>
````````````
■ 만약, 텐서플로 변수의 값을 다른 값으로 대체하고 싶으면 assign( ) 메소드를 적용시키면 된다. 단, 대체하려는 배열의 크기와 자료형이 원래 텐서플로 변수의 크기가 일치해야 한다.
replace_data = [[1., 2., 3.], [4., 5., 6.]] # 2 x 3 행렬
tensor_var_2.assign(replace_data) # 변경
tensor_var_2
```#결과#```
<tf.Variable 'tensor:0' shape=(2, 3) dtype=float32, numpy=
array([[1., 2., 3.],
[4., 5., 6.]], dtype=float32)>
````````````
replace_data2 = [[10, 20, 30], [40, 50, 60]] # 정수
tensor_var_2.assign(replace_data2) # 변경
```#결과#```
<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=
array([[10., 20., 30.],
[40., 50., 60.]], dtype=float32)>
````````````
replace_data3 = [[1., 2.], [4., 5.]] # 크기 불일치
tensor_var_2.assign(replace_data3) # 변경
```#결과#```
ValueError 발생
ValueError: Cannot assign value to variable ' tensor:0': Shape mismatch.The variable shape (2, 3),
and the assigned value shape (2, 2) are incompatible.
````````````
■ 텐서를 텐서플로 변수로 변환할 수 있듯이, convert_to_tensor( )를 적용하면, 텐서플로 변수를 텐서로 변환할 수 있다.
tensor_2 = tf.convert_to_tensor(tensor_var_2)
tensor_2
```#결과#```
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[10., 20., 30.],
[40., 50., 60.]], dtype=float32)>
````````````
■ 텐서플로 변수도 텐서 연산처럼 파이썬 내장 연산자 또는 함수를 사용해 산술 연산을 처리할 수 있다. 이때 연산되는 값은 텐서플로 변수가 저장하고 있는 텐서 값이 사용된다.
tf.math.add(tensor_var_2, tensor_2)
```#결과#```
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[ 20., 40., 60.],
[ 80., 100., 120.]], dtype=float32)>
````````````
tensor_var_2 + tensor_2
```#결과#```
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[ 20., 40., 60.],
[ 80., 100., 120.]], dtype=float32)>
````````````
5. 자동 미분
■ 텐서플로는 자동 미분(automatic differentiation)을 지원하는 프레임워크이며, 미분 가능한 텐서 연산이 조합된 형태라면 어떠한 경우에도 그래디언트(gradient)를 계산할 수 있다.
■ 텐서플로의 자동 미분 기능을 활용할 수 있는 API는 'GradientTape'이다.
■ GradientTape는 파이썬 with 문과 함께 사용되며, with 문 안의 모든 텐서 연산을 계산 그래프 형태로 기록한다. 이후, 이 그래프를 이용해 tf.Variable 클래스의 인스턴스 또는 변수 집합에 대한 어떤 출력의 그래디언트도 계산할 수 있다. 단, 훈련 가능한 텐서에 대해서만 미분을 계산할 수 있다.
a = tf.Variable([1, 2, 3, 4], dtype = tf.float32)
# a가 미분 가능한 객체인지, 즉 훈련 가능한 객체인지 확인
print(a.trainable)
```#결과#```
True
````````````
■ 만약 상수 텐서처럼 훈련 가능한 텐서가 아니라면, 다음과 같이 tape에 watch( )를 적용해야 한다.
cost = tf.constant(3., )
with tf.GradientTape( ) as tape:
tape.watch(cost)
result = tf.square(cost) # result = cost^2
grad = tape.gradient(result, cost) # result' = 2 * cost
cost, result, grad
```#결과#```
(<tf.Tensor: shape=(), dtype=float32, numpy=3.0>,
<tf.Tensor: shape=(), dtype=float32, numpy=9.0>,
<tf.Tensor: shape=(), dtype=float32, numpy=6.0>)
````````````
■ 다음과 같이 다차원 텐서와 함께 사용할 수 있다.
x = tf.Variable(tensor_2) # 초깃값 0으로 스칼라 변수 생성
with tf.GradientTape() as tape: # GradientTape 블록
y = 2*x + 3
grad = tape.gradient(y, x) # tape를 사용해 변수 x에 대한 출력 y의 그래디언트를 계산
- tf.GradientTape( )에서 계산 과정을 기록한 뒤 gradient( ) 메소드를 통해 (편)미분을 계산한다.
- tape.gradient(z, [x, y])이면 z를 x, y에 대한 편미분을 계산하는 것이다.
■ 딕셔너리를 이용해 그래디언트 결과를 확인할 수 있다.
g = tf.random.Generator.from_seed(2024)
W = tf.Variable(tf.random.uniform((4, 4)))
x = g.normal(shape=(4,))
X = tf.reshape(x, (1, 4)) # x의 shape을 (1, 4)으로 변경
with tf.GradientTape() as tape:
Y = tf.matmul(X, W)
grad = tape.gradient(Y, {'dW':W})
d_W = grad['dW']
d_W
```#결과#```
<tf.Tensor: shape=(4, 4), dtype=float32, numpy=
array([[ 0.9029707 , 0.9029707 , 0.9029707 , 0.9029707 ],
[ 0.08384313, 0.08384313, 0.08384313, 0.08384313],
[-0.43693087, -0.43693087, -0.43693087, -0.43693087],
[-0.28045925, -0.28045925, -0.28045925, -0.28045925]],
dtype=float32)>
````````````
- d_W는 텐서 Y를 변수 W에 대해 미분한 결과, 즉 Y의 W에 대한 그래디언트를 나타낸다.
■ 예를 들어 y = 2x + 3이라는 선형 함수에 대해, 주어진 데이터 x와 y의 관계를 학습하여 y = ax + b의 최적의 기울기 a와 절편 값 b를 실제 값과 예측 값 사이의 손실 함수 값을 최소화하는 방식으로 찾을 수 있다.
- 손실 함수를 MSE로 설정하여 손실 함수의 최솟값을 찾아보면
g = tf.random.Generator.from_seed(2024)
x = g.normal(shape=(10, ))
y = 2 * x + 3
# Loss 함수 정의
def MSE(x, y, a, b):
y_pred = a * x + b
mse = tf.reduce_mean((y_pred - y)**2)
return mse
a, b = tf.Variable(0.), tf.Variable(0.) # 계수와 상수항을 0.0으로 초기화
tf.debugging.set_log_device_placement(False)
for epoch in range(1, 101):
with tf.GradientTape() as tape: # tf.GradientTape로 자동미분 과정을 기록
mse = MSE(x, y, a, b) # MSE값을 mse 변수에 저장
grad = tape.gradient(mse, {'da':a, 'db':b}) # 저장된 미분값을 grad 변수에 저장
d_a, d_b = grad['da'], grad['db'] # 계수의 미분값과 상수항의 미분값을 저장
# 미분값에 학습률 0.1을 곱한 값을 기존의 계수와 상수항에서 차감
a.assign_sub(d_a * 0.1) # a = a - d_a*0.1
b.assign_sub(d_b * 0.1) # b = b - d_b*0.1
if epoch % 20 == 0: print(f'epoch {epoch} mse {mse:.5f} a: {a.numpy():.3f}, b: {b.numpy():.3f}')
```#결과#```
epoch 20 mse 0.21971 a: 1.448, b: 2.567
epoch 40 mse 0.00923 a: 1.887, b: 2.911
epoch 60 mse 0.00039 a: 1.977, b: 2.982
epoch 80 mse 0.00002 a: 1.995, b: 2.996
epoch 100 mse 0.00000 a: 1.999, b: 2.999
````````````
- assign_sub( )는 -= 연산과 동일하다. cf) assign_add( )는 +=
- 경사 하강법과 학습률 0.1을 적용하여 MSE를 낮추는 방향으로 a, b 값이 계속 업데이트된다. 이 과정이 tape 객체에 기록된다.
- 이 과정을 100번 가까이 반복할수록 손실 함수인 mse는 0에 가까워지고 a와 b의 값은 점차 실제 값인 2와 3에 근접하게 되는 것을 볼 수 있다.
'텐서플로' 카테고리의 다른 글
임베딩(Embedding) 순환신경망(Recurrent Neural Network, RNN) (0) | 2025.01.21 |
---|---|
텐서플로 합성곱 신경망(CNN) (2) (0) | 2024.11.22 |
텐서플로 합성곱 신경망(CNN) (1) (0) | 2024.11.15 |
케라스(Keras) (2) (2) | 2024.08.20 |
케라스(Keras) (1) (0) | 2024.08.18 |