2.2 신경망을 위한 데이터 표현

최근의 모든 머신 러닝 시스템은 텐서(tensor)를 기본 데이터 구조로 사용합니다. 텐서에 대해 살펴보겠습니다.1 (논외로 구글 텐서플로의 이름을 여기서 따왔습니다.)

텐서란 임의의 차원 개수를 가지는 데이터의 배열이라 볼 수 있습니다.
표로 정리하여 보겠습니다.

RANK TYPE EXAMPLE
0 0d-tensor(scalar) 1
1 1d-tensor(vector) [1,2]
2 2d-tensor(matrix) [[1,2],[3,4]]
3 3d-tensor [[[1,2],[3,4]],[[1,2],[3,4]],[[1,2],[3,4]]]
n nd-tensor

표 2-1. 텐서 정리

텐서에서는 차원(dimension)을 종종 축(axis)이라고 부릅니다. 또한, 텐서의 축 개수를 랭크(rank)2 라고 부릅니다.

2.2.1 스칼라(0D 텐서)

하나의 숫자만 담고 있는 텐서를 스칼라(scala) 라고 부릅니다. 넘파이에서는 float32float64 데이터 하나가 스칼라 텐서(또는 배열 스칼라(array scalar)3)입니다.

ndim속성에서 넘파이 배열의 축 개수를 확인할 수 있고, 스칼라의 랭크는 0입니다.

>>> import NumPy as np
>>> x = np.array(12)
>>> x
array(12)
>>> x.ndim
0

2.2.2 벡터(1D 텐서)

숫자의 배열을 벡터(vector) 또는 1D 텐서라고 부릅니다. 즉, 1개의 축을 가지는 랭크 1인 텐서입니다.

>>> x = np.array([12, 3, 6, 14, 7])
>>> x
array([12, 3, 6, 14, 7])
>>> x.ndim
1

5D 벡터와 5D 텐서를 혼동하지 마세요!
이 벡터는 5개의 원소를 가지는 5차원 벡터라고 부릅니다. 즉 한개의 축을 따라 5개의 차원을 가진 것이고 5D 텐서는 5개의 축을 가진 것입니다.

가끔 차원수는 5D벡터와 같이 원소의 개수를 나타내기도, 5D 텐서와 같이 축의 개수를 나타내기도 합니다. 후자의 경우 랭크 5인 텐서라고 말하는 것이 기술적으로 좀 더 정확합니다.

2.2.3 행렬(2D 텐서)

벡터의 배열을 행렬(matrix) 또는 2D 텐서라고 부릅니다. 행렬에는 2개의 축, 즉 행(row)과 열(column)이 있습니다.

>>> x = np.array([[5, 78, 2, 34, 0],
                  [6, 79, 3, 35, 1],
                  [7, 80, 4, 36, 2]])
>>> x.ndim
2

2.2.4 3D 텐서와 고차원 텐서

이런 행렬들을 하나의 새로운 배열로 합치면 직육면체 형태로 해석할 수 있는 3D 텐서가 만들어집니다.

>>> x = np.array([[[5, 78, 2, 34, 0],
                   [6, 79, 3, 35, 1],
                   [7, 80, 4, 36, 2]],
                  [[5, 78, 2, 34, 0],
                   [6, 79, 3, 35, 1],
                   [7, 80, 4, 36, 2]],
                  [[5, 78, 2, 34, 0],
                   [6, 79, 3, 35, 1],
                   [7, 80, 4, 36, 2]]])
>>> x.ndim
3

이어서 3D 텐서들을 하나의 배열로 합치면 4D 텐서를 만들 수 있습니다. 딥러닝에서는 보통 0D에서 4D까지의 텐서를 다룹니다. 동영상 데이터를 다룰 경우에는 5D 텐서까지 가기도 합니다.

2.2.5 핵심 속성

  • 축의 개수(rank): 3D 텐서에는 3개의 축, 2D 텐서(행렬)에는 2개의 축이 있습니다. 넘파이 라이브러리에서는 ndim 속성에서 확인 할 수 있습니다.
  • 크기(shape) : 텐서의 각 축을 따라 얼마나 많은 차원이 있는지를 나타낸 파이썬의 튜플입니다. 예로 2D는 (3,5)와 같이 나타낼 수 있습니다. 넘파이에서는 shape 속성에서 확인 할 수 있습니다.
  • 데이터 타입 : 텐서에 포함된 데이터 타입입니다. 넘파이에서는 dtype 속성에서 확인 할 수 있으며, 텐서의 경우 unit8, float32, float64 등이 될 수 있습니다. 사전에 할당되어 연속된 메모리에 저장되어야 하여 가변 길이 문자열을 지원하지 않습니다.

구체적으로 확인해 보기 위해 MNIST 예제에서 사용했던 데이터를 다시 살펴 보겠습니다.

from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
>>> print(train_images.ndim)
3
>>> print(train_images.shape)
(60000, 28, 28)
>>> print(train_images.dtype)
uinit8

train_images 배열의 ndim, shape, dtype 속성으로 랭크, 배열의 크기, 데이터 타입을 확인합니다.

8비트 3D 텐서로, 정확하게는 28x28 크기의 정수 행렬 6만 개가 있는 배열입니다. 각 행렬은 하나의 흑백 이미지이고, 각 원소는 [0 255] 범위의 값을 가집니다.

5번 째 샘플을 matplotlib를 사용해서 확인해 봅시다.4

코드 2-6. 다섯 번째 이미지 출력하기

import matplotlib.pyplot as plt

digit = train_images[4]
plt.imshow(digit, cmap=plt.cm.binary)
plt.show()

나타낼 수 없음
그림 2-2. 데이터셋에 있는 다섯 번째 샘플

2.2.6 넘파이로 텐서 조작하기

코드 2-6에서 train_images[4]와 같이 파이썬의 배열에서 특정 원소를 선택하는 것을 슬라이싱 이라고 합니다. 넘파이에서는 좀 특별한 슬라이싱이 가능합니다.

train_images는 (60000, 28, 28)의 shape을 가지고 있습니다. 여기서 11번째에서 101번째까지(101번째는 제외한) 숫자를 선택하여 (90, 28, 28) 크기의 배열을 만듭니다.

>>> my_slice = train_images[10:100]
>>> print(my_slice.shape)
(90, 28, 28)

동일하지만 다른 표현을 살펴 보겠습니다.

>>> my_slice = train_images[10:100, :, :] #이전 예와 동일합니다.
>>> my_slice.shape
(90, 28, 28)
>>> my_slice = train_images[10:100, 0:28, 0:28] #역시 이전과 동일합니다.
>>> my_slice.shape
(90, 28, 28)

이미지의 오른쪽 아래 14x14 크기의 픽셀을 선택하려면 다음과 같이 합니다.
(크기가 28x28이므로 (14, 14)로부터 나머지 선택을 하는 코드입니다.)

my_slice = train_images[:, 14:, 14:]

음수 인덱스는 상대적인 위치를 나타냅니다. 정중앙에 위치한 14×14픽셀 조각을 이미지에서 잘라 내려면 다음과 같이 합니다.

my_slice = train_images[:, 7:-7, 7:-7] #같은 표현 [:, 7:7+14, 7:7+14]

2.2.7 배치 데이터

모든 데이터 텐서의 첫번째 축(인덱스 0)은 샘플 축(sample axis) 입니다. 딥러닝 모델은 한 번에 전체 데이터셋을 처리하는 것이 아닌 단위로 나누어 순차적으로 처리합니다. 이때 나누는 단위를 배치(batch) 라고 합니다.

예시로 train_images에서 크기 128의 첫번째 배치는 다음과 같습니다.

batch = train_images[:128]

그 다음 배치는 다음과 같습니다.

batch = train_images[128:256]

n번째 배치는 다음과 같습니다.

batch = train_images[128*n:128*(n+1)]

이렇게 배치 데이터를 다룰 때는 첫 번쨰 축을 배치 축 또는 배치 차원 이라고 부릅니다.

2.2.8 텐서의 실제 사례

  • 벡터테이터: (samples, features) 크기의 2D 텐서
  • 시계열 데이터 또는 시퀀스 데이터: (sample, timesteps, features) 크기의 3D 텐서
  • 이미지: (samples, frames, width, channels) 또는 (samples, channels, height, width)의 크기의 4D 텐서
  • 동영상: (samples, frames, height, width, channels) 또는 (samples, frames, channels, height, width) 크기의 5D 텐서

2.2.9 벡터 데이터

대부분의 경우에 해당됩니다. 하나의 데이터 포인트가 벡터로 인코딩 될 수 있으므로 배치 데이터는 2D 텐서로 인코딩될 것입니다. 첫 번째 축은 샘플 축, 두 번째 축은 특성 축 입니다.

2개의 예를 살펴보겠습니다.

  • 사람의 나이, 우편 번호, 소득으로 구성된 인구 통계 데이터. 각 사람은 3개의 값을 가진 벡터로 구성되고 10만 명이 포함된 전체 데이터셋은 (100000, 3) 크기의 텐서에 저장될 수 있습니다.
  • (공통 단어 2만 개로 만든 사전에서) 각 단어가 등장한 횟수로 표현된 텍스트 문서 데이터셋. 각 문서는 2만 개의 원소(사전에 있는 단어마다 하나의 원소에 대응합니다)를 가진 벡터로 인코딩될 수 있습니다. 500개의 문서로 이루어진 전체 데이터셋은 (500, 20000) 크기의 텐서로 저장됩니다.

2.2.10 시계열 데이터 또는 시퀀스 데이터

위의 벡터 데이터에서 시간 축이 추가된 데이터셋으로 3D 텐서로 저장됩니다.

나타낼 수 없음
그림 2-3. 3D 시계열 데이터 텐서

관례적으로 시간 축은 항상 두 번째 축(인덱스 1)입니다.5
예를 들어 보겠습니다.

  • 주식 가격 데이터셋: 1분마다 현재 주식 가격, 지난 1분 동안에 최고 가격과 최소 가격을 저장합니다. 1분마다 데이터는 3D 벡터로 인코딩되고 하루 동안의 거래는 (390, 3) 크기의 2D 텐서로 인코딩됩니다(하루의 거래 시간은 390분입니다). 250일치의 데이터는 (250, 390, 3) 크기의 3D 텐서로 저장될 수 있습니다. 여기에서 1일치 데이터가 하나의 샘플이 됩니다.
  • 트윗 데이터셋: 각 트윗은 128개의 알파벳으로 구성된 280개의 문자 시퀀스입니다. 여기에서는 각 문자가 128개의 크기인 이진 벡터로 인코딩될 수 있습니다(해당 문자의 인덱스만 1이고 나머지는 모두 0인 벡터). 그러면 각 트윗은 (280, 128) 크기의 2D 텐서로 인코딩될 수 있습니다. 100만 개의 트윗으로 구성된 데이터셋은 (1000000, 280, 128) 크기의 텐서에 저장됩니다.

2.2.11 이미지 데이터

전형적으로 높이, 너비, 컬러채널의 3차원으로 흑백 이미지는 하나의 컬러 채널만을 가지고 있어 2D 텐서로 나타낼 수 있지만 관례상 컬러 채널을 포함한 3D 텐서로 저장됩니다. 또한 배치 데이터는 4D 텐서가 됩니다. 256x256 크기의 흑백 이미지 128개의 배치는 (128,256,256,1)로, 3채널 컬러 이미지는 (128,256,256,3)로 저장될 수 있습니다.

나타낼 수 없음6
그림 2-4. 4D 이미지 데이터 텐서(채널 우선 표기)

이미지의 경우에는 두 자기 방식으로 텐서의 크기를 지정합니다.

  • 채널 마지막 방식: (samples, height, width, color_depth), 텐서플로에서 사용
  • 채널 우선 방식: (samples, color_depth, height, width), 씨아노

케라스에서는 두 형식을 모두 지원합니다. 옵션 설정으로 가능합니다.7

2.2.12 비디오 데이터

하나의 비디오는 프레임의 연속이고 각 프레임은 컬러 이미지입니다. 따라서 하나의 비디오는 (frames, height, width, color_depth)의 4D 텐서로 여러 비디오 배치는 (samples, frames, height, width, color_depth)의 5D 텐서로 저장될 수 있습니다.

  1. 텐서플로를 비롯한 딥러닝 라이브러리들은 다차원 배열을 텐서(tensor)라 부릅니다. 이 책에서는 넘파이 배열도 텐서라 하지만 파이썬 커뮤니티에서는 텐서라 부르지 않습니다. 

  2. 여기서 랭크는 선형대수에서 행렬의 선형 독립 행이나 열을 나타내는 계수(rank)와는 다릅니다. 

  3. 넘파이의 배열 스칼라(array scalar)는 수정할 수 없는 0차원의 넘파이 배열이며 프로그래밍 언어의 스칼라 변수와는 다릅니다. 

  4. 배열의 인덱스는 0부터 시작하므로 다섯 번째 이미지의 인덱스가 4입니다. 

  5. 시간 축이 두 번째 축이면 그림 2-3에서 타임스텝이 세로 축에 놓여야 하지만 가로 축에 표기되어 있습니다. 관례적으로 시간의 흐름을 가로 방향으로 놓기 때문에 편의상 텐서 축의 순서와 맞지 않게 그려져 있습니다. 

  6. 일반적으로 이미지의 크기는 보통 너비 × 높이로 말하지만 행렬에서는 행이 먼저 나오므로 높이 × 너비로 표현됩니다. 

  7. 케라스 설정 파일 keras.json에서 “image_data_format” 옵션을 “channels_last” 또는 “channels_first”로 지정할 수 있습니다. 

댓글남기기