수학적 관점보다 데이터 엔지니어로서 딥러닝 모델 구축 때 도움이 될 만한 내용들이다.
신경망에서 사용할 수 있는 여러 데이터 종류와, 이것을 담는 텐서 계산을 수행하는 텐서 연산을 공부한다.
텐서(tensor) : 넘파이 배열이고 데이터를 위한 컨테이너이다. 텐서에 담아서 텐서 연산을 수행한다. 임의의 차원 수를 가지는 행렬의 일반화된 모습니다.
차원(dimension) : 축(axis)라고도 하고, 텐서에서의 차원의 수(dimensionality)는 "특정 축을 따라 놓인 원소의 개수"(5D vector) 혹은 "텐서의 축 개수"(5D tensor)를 의미한다.
#scalar
x = np.array(12)
print(x.ndim) ## 0
#vector
x = np.array([12, 3, 6, 14, 7])
print(x.ndim) ## 1
#matrix
x = np.array([[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]])
print(x.ndim) ## 2
#3D tenser
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]]])
print(x.ndim) ##4
위 3D 텐서들을 하나의 배열로 합치면 4D 텐서를 만드는 식으로 이어진다.
딥러닝은 보통 4D텐서 까지 다루고, 동영상 데이터를 다룰 경우에는 5D 텐서까지도 다룬다.
### dimensionality, array shape, data type
print(train_images.ndim, train_images.shape, train_images.dtype) #3, (60000, 28, 28), uint8
### slicing
my_slice = train_images[10:100]
print(my_slice.shape) ## 90, 28, 28
my_slice = train_images[10:100, :, :] #same
my_slice = train_images[10:100, 0:28, 0:28] #same
my_slice = train_images[:, 14:, 14:] #이미지 오른쪽 아래 14x14 픽셀 선택
my_slice = train_images[:, 7:-7, 7:-7] #음수 인덱스도 선택 가능, 정중앙에 14x14 픽셀 선택
":"은 전체 인덱스를 선택한다.
딥러닝에서는 데이터를 나눠서 이때 나눠지는 단위가 배치이다.
### batch
batch = train_images[:128]
batch = train_images[128:256]
batch = train_images[128 * n : 128 * (n+1)]
입력 데이터에 따라서 텐서의 축도 증감된다. (시계열 데이터는 3D 텐서, 이미지 데이터는 4D 텐서)
케라스 API 구현을 파이썬 구현 단계에서 살펴본다. 예를 들어
keras.layers.Dense(512, activation='relu')
output = relu(dot(W, input) + b) #relu(x) = max(x, 0) 이다.
위에 케라스로 구현한 2D 텐서 층은 입력 텐서의 또 다른 표현의 2D 텐서를 반환하는 함수로 해석할 수 있고, 파이썬으로 구현하면 밑에 연산과 같겠다.
(W는 2D 텐서, b는 벡터이다. (가중치와 벡터))
relu 함수와 덧셈은 원소별 연산(element-wise operation)이다. 이 연산은 텐서에 있는 각 원소에 독립적으로 적용된다.
### relu
def naive_relu(x):
assert len(x.shape) == 2
x = x.copy() # 입력 텐서 자체를 바꾸지 않도록 복사
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] = max(x[i, j], 0)
return x
### add
def naive_add(x, y):
assert len(x.shape) == 2 # x, y는 2D 넘파이 배열이다.
assert x.shape == y.shape
x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] += y[i, j]
return x
넘파이는 효율을 위해 시스템에 설치된 BLAS(Basic Linear Algebra subprogram) 구현에 위임한다.
BLAS : 고도로 병렬화되고 효율적인 저수준의 텐서 조작 루틴, 포트란, C로 구현되어 있다.
(아키텍처상 GPU는 CUDA, CPU는 BLAS)
넘파이를 활용해서 구현하면
### numpy를 활용해 구현
z = x + y # 원소별 덧셈
z = np.maximum(z, 0.) # 원소별 relu
크기가 다른 텐서 연산을 할 때 작은 텐서가 큰 테서의 크기의 맞추어 하는 연산이 브로드캐스트이다.
## 브로드캐스팅
def naive_add_matrix_and_vector(x, y):
assert len(x.shape) == 2
assert len(y.shpae) == 1
assert x.shape[1] == y.shape[0]
x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] += y[j]
return x
### numpy 활용 브로드캐스팅
x = np.random.random((64, 3, 32, 10))
y = np.random.random((32, 10))
z = np.maximum(x, y)
원소별 연산과 반대로 입력 텐서의 원소들을 결합시키는 연산이 텐서 곱셈(tensor product)이다.
주의할 점은 텐서의 차원(ndim)이 다르다면 dot 연산에 교환 법칙이 성립되지 않아 계산이 되지 않는다.
## 텐서 점곱
z = np.dot(x, y)
텐서 크기 변환(tensor reshaping)과 전치(transposition)도 자주 사용하는 연산이다.
텐서 크기 변환 : 텐서의 크기를 특정 크기에 맞게 열과 행을 재배열하는 연산
전치 : 행과 열을 바꾸는 연산
## 텐서 크기 변환, 전치
x = np.array([[0., 1.],
[2., 3.],
[4., 5.]])
x = x.reshape((6, 1))
x = np.transpose(x)