[케라스 창시자에게 배우는 딥러닝] 참고
📢 목차
1. 원소별 연산 (element-wise operation)
3. 텐서 점곱 ( Tensor dot operation)
4. 텐서 크기 변환 (Tensor reshaping), 전치(transposition)
컴퓨터 프로그램의 이진수 입력을 몇 개의 이항 연산(AND, OR, NOR 등)으로 표현할 수 있는 것 처럼, 심층 신경망이 학습한 모든 변환을 텐서 연산(tensor operation)으로 나타낼 수 있다.
ex) 텐서 덧셈, 텐서 곱셈
<Dense>
keras.layers.Dense(512, activation='relu')
Dense 클래스의 객체가 모델(ex.Sequential클래스)의 add() 메서드에 추가될 때 Dense 객체의 build() 메서드가 호출되면서 가중치(커널) W와 편향 b가 생성되고, 각각 Dense 객체의 kernel과 bias 인스턴스 변수에 저장된다.
output = relu(dot(W, input) + b)
이 층은 2D 텐서를 입력으로 받고 입력 텐서의 새로운 표현인 또 다른 2D 텐서를 반환하는 함수처럼 해석할 수 있다.
(W = 2D텐서, b=벡터) 둘 모두 층의 속성이다.
현재 3개의 텐서 연산이 있다. 입력 텐서와 텐서 W 사이의 접곰(dot), 점곱의 결과인 2D 텐서와 벡터b 사이의 덧셈(+), 마지막으로 relu(렐루) 연산이다. ( relu(x)= max(x, 0) , 0보다 크면 그대로 입력값 반환. 0보다 작으면 0 반환.)
1. 원소별 연산(element-wise operation)
원소별 연산은 텐서에 있는 각 원소에 독립적으로 적용된다. 이 말은 고도의 병렬 구현(1970-1990년대 슈퍼컴퓨터의 구조인 벡터프로세서(vector processor)에서 온 용어인 벡터화된 구현을 말한다)이 가능한 연산이라는 의미이다.
파이썬으로 단순한 원소별 연산을 구현한다면 relu 연산이나 덧셈,뺄셈 등의 연산 구현처럼 for 반복문을 사용할 것이다.
# relu 연산 구현
def naive_relu(x):
assert len(x.shape) == 2 #x는 2D 넘파이 배열이다.
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
# 덧셈의 연산 구현
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는 고도로 병렬화되고 효율적인 저수준의 텐서 조작 루틴이며, 전형적으로 포트란(Fotran)이나 C언어로 구현되어 있다. 대표적인 BLAS 구현으로는 OpenBLAS, 인텔 MKL, ATLAS 등이 있다. 아나콘다 파이썬 배포판은 기본적으로 MKL 라이브러리를 사용한다.
# numpy
import numpy as np
z = x + y #원소별 덧셈
z = np.maximum(z, 0.) #원소별 relu함수
2. 브로드캐스팅 (broadcasting)
앞에서 구현한 naive_add 덧셈 함수는 동일한 크기의 2D 텐서만 지원한다. 하지만 Dense 층에는 2D 텐서와 벡터를 더했다. 크기가 다른 두 텐서가 더해질 때 무슨 일이 일어날까?
실행 가능하다면 작은 텐서가 큰 텐서의 크기에 맞추어 broadcasting 된다.
<브로드캐스팅의 2 STEP>
STEP 1. 큰 텐서의 ndim에 맞도록 작은 텐서에 (브로드캐스팅)축이 추가된다.
ex. 만약 X의 크기가 (32, 10)이고 y의 크기는 (10,)이라고 가정한다면, 먼저 y에 비어 있는 첫 번째 축을 추가해 크기를 (1,10)으로 만든다.
STEP 2. 작은 텐서가 새 축을 따라서 큰 텐서의 크기에 맞도록 반복된다.
ex. y를 이 축에 32번 반복하면 텐서 Y의 크기는 (32, 10)이 된다. 여기에서 Y[i, :] == y for i in range(0,32)이다. 이제 X와 Y의 크기가 같으므로 더할 수 있다.
사실 구현 입장에서는 새로운 텐서가 만들어지면 매우 비효율적이므로 실제론 어떤 2D 텐서도 만들어지지 않는다. 반복된 연산은 가상으로 설명한 것이며, 사실 이 과정은 메모리 과정이 아니라 알고리즘 과정에서 일어난다.
하지만 새로운 축을 따라 벡터가 32번 반복된다고 생각하는 것이 이해하기 쉽다.
# 크기가 다른 두 텐서 덧셈하기(broadcasting)
def naive_add_matrix_and_vector(x,y):
assert len(x.shape) == 2 # x=2D 넘파이 배열
assert len(y.shape) == 1 # y=1D 넘파이 벡터
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
# 크기가 다른 두 텐서에 브로드캐스팅으로 원소별 maximum 연산을 적용. numpy
import numpy as np
x = np.random.random((64, 3, 32, 10)) # x는 (64, 3, 32, 10)크기의 랜덤 텐서이다.
y = np.random.random((32, 10)) # y는 (32, 10)크기의 랜덤 텐서이다.
z = np.maximum(x,y) # 출력z 크기는 x와 동일하게 (64, 3, 32, 10)이다.
3. 텐서 점곱 (tensor dot operation)
텐서 곱셈(tensor product) 라고도 부르는 점곱 연산은 가장 널리 사용되고 유용한 텐서 연산이다.
원소별 연산과 반대로 입력 텐서의 원소들을 결합시킨다.
넘파이, 케라스, 씨아노, 텐서플로에서 원소별 곱셈은 * 연산자를 사용한다.
텐서플로에서는 matmul 연산자 또는 @를 사용하고 넘파이와 케라스는 보편적으로 dot 연산자를 사용한다.
- tensorflow : tf.matmul(x,y) or x@y(파이썬 3.5이상)
- keras : from keras import backend as K; K.dot(x,y)
1) 벡터와 벡터의 점곱
두 벡터를 점곱하면 스칼라가 되므로 원소 개수가 같은 벡터끼리 점곱이 가능하다.
# 두 벡터의 점곱 연산
def naive_vector_dot(x,y):
assert len(x.shape) == 1 # x=1D 넘파이 벡터
assert len(y.shape) == 1 # y=1D 넘파이 벡터
assert x.shape[0] == y.shape[0]
z = 0
for i in range(x.shape[0]):
z += x[i] * y[i]
return z
# numpy 텐서 점곱
import numpy as np
z = np.dot(x,y)
2) 행렬과 벡터의 점곱
행렬 x와 벡터 y 사이에도 점곱이 가능하다. y와 x의 행 사이에서 점곱이 일어나므로 벡터가 반환된다.
두 텐서 간 교환 법칙 성립하지 않음 ( dot(x,y) != dot(y,x) )
# 행렬x와 벡터y의 점곱 연산
def naive_matrix_vector_dot(x,y):
assert len(x.shape) == 2 # x=2D 넘파이 배열
assert len(y.shape) == 1 # y=1D 넘파이 벡터
assert x.shape[1] == y.shape[0] # x의 두번째 차원과 y의 첫 번째 차원이 같아야 함.
z = np.zeros(x.shape[0]) # x의 행과 같은 크기의 0이 채워진 벡터 만들기
for i in range(x.shape[0]):
for j in range(x.shape[1]):
z[i] += x[i,j] * y[j]
return z
2.5) 행렬-벡터 점곱 + 벡터-벡터 점곱 함수
- 행렬-벡터 점곱은 교환 법칙이 성립하지 않음. [ dot(x,y) != dot(y,x) ]
def naive_matrix_vector_dot(x, y):
z = np.zeros(x.shape[0])
for i in range(x.shape[0]):
z[i] = naive_vector_dot(x[i, :], y)
return z
3) 행렬과 행렬의 점곱
행렬x의 열과 행렬y의 행이 같은 차원이어야 연산이 가능하다.
def naive_matrix_dot(x,y):
assert len(x.shape) == 2 # x=2D 넘파이 배열
assert len(y.shape) == 2 # y=2D 넘파이 배열
assert x.shape[1] == y.shape[0] # x의 열과 y의 행이 같은 차원이어야 연산이 가능
z = np.zeros((x.shape[0], y.shape[1])) # 0이 채워진 특정 크기의 벡터
for i in range(x.shape[0]): # x의 행을 반복
for j in range(y.shape[1]): # y의 열을 반복
row_x = x[i, :]
column_y = y[:, j]
z[i,j] = naive_vector_dot(row_x, column_y)
return z
4. 텐서 크기 변환 (tensor reshaping), 전치(transposition)
tenor를 reshape한다는 것은 특정 크기에 맞게 열과 행을 재배열한다는 뜻이다.
신경망에 주입할 숫자 데이터를 전처리할 때 사용할 수 있다.
#예)
train_images = train_images.reshape((60000, 28*28))
당연히 크기가 변환된 텐서와 원래 텐서는 원소 개수가 동일하다.
자주 사용하는 특별한 크기 변환은 전치(trainsposition)이다.
행렬의 전치는 행과 열을 바꾸는 것을 의미한다. (즉 x[i, :]의 전치는 x[:, i]가 된다)
5. 텐서 연산의 기하학적 해석
텐서 연산이 조작하는 텐서의 내용은 어떤 기하학적 공간에 있는 좌표 포인트로 해석 될 수 있기 때문에 모든 텐서 연산은 기하학적 해석이 가능하다.
A와 B를 기하학적으로 벡터 화살표를 연결해 계산할 수 있고, 최종 위치는 두 벡터의 덧셈을 나타내는 벡터가 된다.
일반적으로 아핀 변환(affine transformation), 회전, 스케일링(scaling) 등 기본적인 기하학적 연산은 텐서 연산으로 표현될 수 있다.
*affine transformation : 점, 직선, 평면을 보존하는 아핀 공간으로의 변환. 이 변환은 거리의 비율과 직선의 평행을 유지하는 이동, 스케일링, 회전 등이 포함된다.
예를 들어 theta 각도로 2D 벡터를 회전하는 것은 2X2 행렬 R=[u,v]를 점곱해 구현할 수 있다. u와 v는 동일 평면상의 벡터이다.
- u = [cos(theta), sin(theta)]
- v = [-sin(theta), cos(theta)]
6. 딥러닝의 기하학적 해석
신경망은 전체적으로 텐서 연산의 연결로 구성된 것이고, 모든 텐서 연산은 입력 데이터의 기하학적 변환임을 공부했다. 단순한 단계들이 길게 이어져 구현된 신경망을 고차원 공간에서 매우 복잡한 기하학적 변환을 하는 것으로 해석할 수 있다.
<3D데이터와 신경망의 예시>
하나는 빨간색이고 다른 하나는 파란색인 2개의 색종이 두개를 뭉쳐 작은 공으로 만들었다고 상상해보자.
이 종이 공은 입력 데이터이고 색종이는 분류 문제의 데이터 클래스가 된다.
신경망(또는 다른 머신러닝 알고리즘)이 해야 할 일은 종이 공을 펼쳐서 두 클래스가 다시 깔끔하게 분리되는 변환을 찾는 것이다. 손가락으로 종이 공을 조금씩 펼치는 것처럼 딥러닝을 사용해 3D 공간에서 간단한 변환들을 연결해 이를 구현한다.
예시 | 신경망 |
2개 색종이를 뭉친 종이 공 |
입력 데이터 |
빨간 색종이 파란 색종이 |
분류 문제의 데이터 클래스 |
손으로 종이를 펴서 색종이들을 분리한다. | 신경망이 두 종이가 분리되는 변환을 찾아 분리한다. |
위 예시의 종이 공을 펼치는 것이 머신 러닝이 하는 일이다.
(즉, 복잡하고 심하게 꼬여 있는 데이터의 매니폴드에 대한 깔끔한 표현을 찾는 일.)
딥러닝의 심층 네트워크의 각 층은 데이터를 조금씩 풀어 주는 변환을 적용하므로, 이런 층을 깊게 쌓으면 아주 복잡한 분해 과정을 처리할 수 있다.
'딥러닝 (Deep Learning) > 딥러닝 기초' 카테고리의 다른 글
신경망에서 미분이 필요한 이유 1 : 미분, 순간 변화율, 기울기 (0) | 2021.04.25 |
---|---|
신경망 - 가중치(weight), 랜덤 초기화(random initialization), 훈련 루프(training loop) (0) | 2021.04.24 |
신경망의 데이터 표현 3 - tensor & data 종류와 예시 (0) | 2021.04.24 |
신경망의 데이터 표현 2 - data 다루기 (0) | 2021.04.24 |
신경망의 데이터 표현 1 - tensor (0) | 2021.04.23 |