https://www.youtube.com/watch?v=pYWASRauTzs
지난 비디오에선 내장 함수를 쓰고 for 문을 피하는 방법으로 벡터화가 어떻게 코드의 속도를 빠르게 해주는지 몇 가지 예를 봤습니다. 몇 가지 예를 더 살펴보죠. 신경망이나 로지스틱 회귀를 프로그래밍할 때 기억해야 할 것은 가능한 한 for 문을 쓰지 않는 것입니다. for 문을 쓰지 않는 게 항상 가능한 건 아니지만 필요한 값을 계산할 때 내장 함수나 다른 방법을 쓸 수 있다면 for 문을 쓰는 것 보다 대부분 빠를 것 입니다.
다른 예시를 한번 보죠. 행렬 $A$와 벡터 $v$의 곱인 벡터 $u$를 계산하고 싶을 때 ($u = Av$), 행렬 곱셈의 정의는 $u_i = \sum_{j}A_{ij}v_j$입니다. $u_i$의 정의가 그렇습니다.
u = np.zeros((n,1))
for i
for j
u[i] += A[i][j] * v[j]
벡터화되지 않은 구현은 먼저 $u$를 u = np.zeros(n,1)으로 정의하고 그 후에 i와 j에 대한 for 문을 작성합니다. 그 후에 u[i] = A[i][j] × v[j]가 되겠죠. 여기엔 i와 j에 대한 두 개의 for 문이 있습니다. 이것이 벡터화되지 않은 버전입니다.
u = np.dot(A,v)
벡터화된 버전은 먼저 $u$를 np.dot(A,v)로 지정해줍니다. 오른쪽에 벡터화된 버전은 두 개의 for 문을 없애므로 훨씬 빠릅니다.
$v=\begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_n \end{bmatrix}$, $u=\begin{bmatrix} e^{v_1} \\ e^{v_1} \\ \vdots \\ e^{v_1} \end{bmatrix}$
예시를 하나 더 보죠. 메모리 상에 벡터 $v$가 있다고 가정하고 이 벡터 $v$의 모든 원소에 지수 연산을 하고 싶다고 합시다. 다른 말로는 원소가 $e^{v_1}$ 부터 $e^{v_n}$인 벡터 $u$를 계산하는 겁니다.
u = np.zeros((n,1))
for i in range (n):
u[i] = math.exp(v[i])
이것은 벡터화되지 않은 구현입니다. 먼저 u를 0인 벡터로 초기화하고 그 뒤에는 원소를 하나씩 계산하는 for 문이 있습니다.
import numpy as np
u = np.exp(v)
하지만 파이썬 NumPy에는 이 벡터들을 하나의 호출로 계산해주는 내장 함수가 많습니다. 제가 구현할 방법은 먼저 NumPy를 np로 임포트하고 제가 구현할 방법은 먼저 NumPy를 np로 가져오고 간단히 $u$를 np.exp(v)로 지정해주면 됩니다. 전에는 for 문이 있었지만 입력 벡터인 $v$를 사용해 출력 벡터인 $u$를 한 줄로 계산하고 for문을 제거했습니다. 오른쪽 구현이 for문이 필요한 구현보다 훨씬 빠르기도 하죠.
NumPy 라이브러리에는 다른 벡터 함수가 많습니다. np.log는 원소의 로그값을 구하고 np.abs는 절대값을 구합니다. np.max(v,0)는 $v$의 원소와 0 중에서 더 큰 값을 반환해 줍니다. v**2는 모든 원소를 제곱한 벡터를 반환해줍니다. 1/v는 원소의 역수로 이루어진 벡터를 반환하죠. for 문을 쓰고 싶을 때 그 공식을 쓰지 않고 NumPy 내장 함수를 쓸 수 있는지 확인해보세요.
여기서 배운 걸 가지고 로지스틱 회귀와 경사 하강법에 적용해봅시다. 두 개 중 하나의 for 문을 제거할 수 있는지 보죠.
J = 0, dw1 = 0, dw2 = 0, db = 0
for i = 1 to m :
z[i] = w.T * x[i] + b
a[i] = sigmoid(z[i])
J += -(y[i] * log(yhat[i]) + (1-y[i]) * log(1-yhat[i]))
for j = 1 to n_x :
dw[j] += x[j][i] * dz[i]
db += dz[i]
J = J/m, dw1 = dw1/m, dw2 = dw2/m, db = db/m
로지스틱 회귀의 도함수를 구하는 코드를 봅시다. 여기엔 두 개의 for문이 있습니다. 하나는 여기있고 두 번째는 여기죠. 예제에서는 $n_x$가 2였지만 특성이 두 개 이상이라면 $dw_1$, $dw_2$, 등에 for 문이 필요하게 됩니다. 마치 $j$가 1부터 $n_x$ 까지일 때 $dw_j$를 갱신하는 for 문이 있는 셈이죠. 저희는 이 두 번째 for 문을 없애려고 합니다. 이 슬라이드에서 하려는 거죠.
저희가 할 방법은 $dw_1$, $dw_2$ 등을 0으로 초기화하는 대신 이 부분(dw1 = 0, dw2 = 0)을 지우고 $dw$를 벡터로 만들 겁니다. dw = np.zeros((n_x,1))로 지정하여 $n_x$ 차원의 벡터로 만듭시다. 그러면 이 부분(for j = 1 to n_x : dw[j] += x[j][i] * dz[i])에 for문을 쓰는 대신 벡터 연산인 dw += x[i] * dz[i]로 바꿀 수 있습니다. 그리고 이 부분(dw1 = dw1/m, dw2 = dw2/m) 대신 dw = dw/m을 쓸 수 있습니다. 두 개의 for 문을 하나로 줄였습니다. 아직 훈련 샘플을 순환하는 for 문이 하나 남아있죠.
J = 0, dw = np.zeros((n_x, 1)), db = 0
for i = 1 to m :
z[i] = w.T * x[i] + b
a[i] = sigmoid(z[i])
J += -(y[i] * log(yhat[i]) + (1-y[i]) * log(1-yhat[i]))
dw += x[i] * dz[i]
db += dz[i]
J = J/m, dw = dw/m, db = db/m
이 영상이 벡터화에 대한 감을 주었으면 합니다. for 문을 하나 제거하는 것 만으로도 코드는 빠르게 실행됩니다. 하지만 더 좋은 방법이 있습니다. 다음 영상에서는 로지스틱 회귀를 더 벡터화하는 방법을 보겠습니다. 훈련 샘플에서 조차 for 문을 하나도 쓰지 않고 훈련 세트 전체를 동시에 처리할 수 있다는 놀라운 결과를 보겠습니다. 다음 영상에서 뵙겠습니다.

댓글