Andrew Ng 교수님의 Machine Learning Specialization 강의를 정리한 내용입니다.
Coursera가 아닌 네이버 부스트코스에서 제공하는 버전을 수강하고 있습니다.
벡터화(Vectorization)은 for loop를 사용하지 않게 해주는 도구입니다. 딥러닝 알고리즘 구현의 결실을 거두게 하는 것이 바로 빅데이터인데요. 빅데이터는 연산이 오래걸릴 수 밖에 없고, 연산 시간이 오래걸리는 for loop 사용을 자제하는 것이 좋기 때문에 벡터화는 중요합니다.
Vectorization 예시
다음 그림과 같이 익숙한 식인 $z = wx+b$에서 w,b는 $n_x$ 차원을 가진 두 벡터인 경우를 봅시다.
벡터화를 하지 않았을 때에는 대략적으로 다음 코드랑 비슷하게 for loop를 사용해서 코드를 작성할 수 있겠습니다.
for i in range(n):
z += w[i]*x[i]
z += b
하지만, 벡터화된 코드로 표현한다면 numpy 모듈의 dot 함수를 이용해서 for loop를 없애버릴 수 있습니다.
z = np.dot(w,x) + b
실제로 Jupyter Notebook으로 가서 실행되는 Python 코드를 작성해서 속도를 계산해보겠습니다.
import time
a = np.random.rand(1000000)
b = np.random.rand(1000000)
tic = time.time()
c = np.dot(a,b)
toc = time.time()
print('계산 결과', c)
print('벡터화 버전은: ', str(1000*(toc-tic)), 'ms')
c = 0
tic = time.time()
for i in range(1000000):
c += a[i]*b[i]
toc = time.time()
print('계산 결과', c)
print('for loop 버전은: ', str(1000*(toc-tic)), 'ms')
동일한 계산을 수행하는 데 for loop과 벡터화 버전의 코드 속도 차이는 약 600배 나왔네요. 벡터화를 통해 또는 Numpy를 이용해 빠른 연산이 가능한 이유는 Numpy는 SIMD를 기반으로 연산을 하기 때문입니다. SIMD(Single Instruction Multiple Data)는 병렬 프로세서이며, 하나의 명령어로 여러 개의 값을 동시에 계산할 수 있는 방식을 말합니다. 이를 통해서 벡터화 연산이 가능한 것입니다. 특히 GPU는 SIMD 연산을 잘한다고 합니다.
Vectorization 예시2
벡터화를 통해 연산을 빠르게 단순하게 만드는 신경망 수식 예시를 더 보려고 합니다.
<예시1>
$u = A\cdot v$
$u_i = \displaystyle\sum_{j} A_ij*V_j$ 이를 for loop를 활용해서 코드를 대강 작성해본다고 하면 아래와 같이 작성할 수 있습니다.
u = np.zeros((0,1))
for i ...
for j ...
u[i] += A[i][j]*v[j]
만약 numpy를 활용해 벡터화를 한다면 한줄의 코드로 더 빠른 속도로 연산 가능합니다. for loop을 두개나 없애버렸네요.
u = np.dot(A,v)
<예시2>
벡터 전체 값을 지수로 사용하는 것도 for loop 를 먼저 떠올를 수 있지만, 벡터 연산으로 for loop를 완전히 없앨 수 있습니다.
# for loop를 사용하고, numpy를 통해 벡터화하지 않은 코드
u = np.zeros((n,1))
for i in range(n):
u[i] = math.exp(v[i])
# numpy를 통해 벡터화한 코드
import numpy as np
u = np.exp(v)
이런 exp() 처럼 Andrew Ng 교수님이 자주쓰는 numpy 함수도 알려주었습니다.
np.log(v)
np.abs(v)
np.maximum(v,0)
v**2
1/v
zeros((n,1))
지난주 강의(링크)에서 logistic regression 경사하강법을 손코딩으로 구현해봤을 때, for loop를 사용했었습니다. 그리고 feature 갯수를
2개로 제한하는 가정을 했는데, 만약 2개 이상이면 또 for loop가 추가됩니다. 아래 이미지는 지난주 강의 정리 때 사용했던 이미지입니다.
분홍색 형광펜 친 곳은 $dw = np.zeros((n_x,1))$, 노란색 형광펜 친 곳을 $dw += x^{(i)}dz^{(i)}$ 벡터 연산으로 대체 가능합니다. 마지막 하늘색 부분은 dw /= m으로 간단하게 작성할 수 있습니다. 이렇게 벡터화로 for loop를 단 몇 줄로 줄였습니다.
Vectorizing Logistic Regression
Logistic Regression 문제를 앞에서 공부했었는데요. Logistic Regression의 Gradient 를 계산하는 데까지 for loop이 하나도 없는 코드가 작성 가능합니다. 신경망의 forward propagation 부터 for loop 없이 벡터화로 간단하게 표현하는 방법을 보겠습니다.
Forward propagation은 조금 익숙해지실 값을 예측하는 z function과 $\sigma$ activation function으로 이뤄져있습니다. m개의 데이터셋이 있다면, m개의 값을 예측하기 위해서 아래의 식을 m번 반복하게 됩니다.
m번 반복 대신 모든 연산을 한꺼번에 처리하기 위해 열벡터인 x의 값 m개를 행렬로 모아 $(n_x, m)$ 차원으로 변경합니다. 그러면 연산 결과 z값이 행벡터로 나오게 되는데, 그로 인해 파라미터 w는 열벡터를 행벡터로 바꾸는 transpose 과정을 거쳐줍니다.
벡터화를 통해서 z값도 모두 (1,m)차원의 행벡터로 벡터화했습니다. 그러면 아래와 같이 파이썬 코드로 작성 가능합니다.
# Z = 소문자 z 변수를 가로로 쌓아서 만든 행렬
Z = np.dot(w.T, x) + b
b 는 (1, 1) 차원의 단일 실수 값으로 (1, m) 차원의 앞에 항과 자연스레 더해졌습니다. 행렬의 덧셈은 차원을 맞춰줘야 가능한데 말이지요. 파이써는 차원이 맞지 않아도 자동으로 값을 복사해서 연산이 가능하도록 하는 broadcasting 개념이 있기 때문입니다. 이번 주차에서 차차 알아가봅시다.
Vectorizing Logistic Regression's Gradient Computation
Forward propagation에서 for loop를 없애는 방법을 확인했다면, Back propagation에서 Gradient 즉, 미분값을 계산하는 데 벡터화를 통해 for loop를 없애보겠습니다. 앞선 강의(링크)에서 공부 했듯이 예측값 a가 있었고, 예측값들의 집합인 A 벡터를 준비를 합니다. 파이썬에서 미분값을 의미하는 $dz$ 값은 다음과 같이 반복 계산됩니다.
dZ는 $dz$값들의 집합인 (1, m) 차원의 열벡터입니다. 벡터화함으로써 한번에 계산이 가능해집니다.
dz가 계산이 되었다면, dw와 db도 계산이 되어야 합니다. dw와 db는 간단하게 말해서 m개의 모든 값을 더해주고 m으로 나눈 값이 됩니다. db의 일일히 계산과 요약 버전을 보여드리겠습니다.
$$ db = \frac{1}{m}\displaystyle\sum_{i=1}^{m} dz^{(i)}$$
요약된 버전을 코드로 작성할 수 있습니다.
db = 1/m*np.sum(dz)
dw 업데이트는 기존에 m번 업데이트 해준 방식은 아래 필기와 같습니다. Andrew Ng 교수님과 맞짱 뜨기 가능한 필기 실력입니다.
위에서 dw 계산했을 때 $X^{i}dz^{i}$의 합이 였으므로 (m, 1) 차원의 벡터로 보여질 수 있습니다.
$$ dw = \frac{1}{m}X dZ^{T}$$
이로써 아래 노란 형광펜 부분이 feature가 여러개라서 for loop가 들어간다고 가정하는 for loop 외에도 전체 train 데이터에 대해 1부터 m까지 돌리는 for loop까지 없앨 수 있습니다. 벡터화 참 위대하죠잉?
경사하강법에서 for loop 건덕지를 완전히 없애 버린 코드를 작성하기 귀찮아서 강의 필기노트를 아래 붙입니다.
for loop를 없앤 경사하강법을 천번, 만번 반복하고 싶을 경우에 사용하게 될 for loop까지 없애는 방법을 찾지 못했다고 합니다.
Broadcasting
행렬을 연산할 때 차원을 맞추는 것은 매우 중요합니다. Python에서는 꼭 차원을 맞추지 않고도 연산이 되는 경우가 있는데요, 아래 코드 예시로 하나의 값이 행렬 모든 값에 동시 연산이 되는 것을 확인할 수 있습니다. 그러나 차원은 연산하는 데 있어 여전히 중요하기 때문에 아래 코드에서 .reshape()으로 (1, 4) 차원을 확정, 명시해줬습니다. reshape() 함수는 연산 시 자원을 많이 잡아먹는 것이 아니고 단순 상수계산이니 적극 사용하는 것을 추천합니다.
아래 코드 예시는 100g의 음식 4개에 대해 탄단지 3요소 칼로리 값으로 각 요소의 백분율(%)를 행렬을 통해서 빠르고 간단하게 계산하는 것입니다. 탄단지 칼로리 값을 더하면 전체 칼로리가 나올 것입니다. sum 함수에서는 axis = 0을 통해 세로로 더하라고 했으니 4 가지 음식별 총 칼로리를 구할 수 있습니다. 그리고 각 음식별 총 칼로리로 나누고, 100을 곱해주면 값을 구할 수 있습니다.
# numpy 불러오기
import numpy as np
# A 행렬에 각 음식의 탄단지별 열량 값 저장하기
A = np.array([[56.0, 0.0, 4.4, 68.0],
[1.2, 104.0, 52.0, 8.0],
[1.8, 135.0, 99.0, 0.9]])
# 열량값을 모두 더해 총 칼로리 계산
cal = A.sum(axis=0)
# 탄단지별 열량 값을 각각 모두 총 칼로리로 나누고 100을 곱해 % 구하기
percentage = 100*A/cal.reshape(1,4)
파이썬에서 자동으로 값을 복사해서 계산이 가능하고, 가로 세로 상관없이 복사가 됩니다. 이 개념이 broadcasting입니다. 열벡터를 어떤 실수와 더한다면, 열벡터의 각 요소들이 모두 특정 실수와 각각 element-wise하게 더해집니다. 열벡터는 세로로 값을 복사했다면, 행벡터도 가로로 복사 가능합니다.
Broadcasting의 개념을 잘 설명하고 많은 블로그에서 인용되는 이미지를 덧붙입니다. 없던 차원을 복사로 늘려 연산해준다는 개념을 잘 표현하고 있습니다.
앞서 .reshape() 사용을 추천한다고 했는데, 행과 열의 값을 정확히 기입해서 사용해야 합니다. Python에서 행렬을 (n, )로 크기를 맞추기도 가능하나 이렇게 차원을 정의를 하고 연산하게 되면, 예상치 못한 결과가 도출될 수 있습니다. 이 외에도 예상하지 못하는 결과를 방지하기 위한 몇 가지 팁이 있습니다.
- .shape 으로 차원 확인하기
- (n, )또는 ( ,n) 차원인 rank1 배열 사용하지 않기
- 차라리 (n, 1) 또는 (1, n)로 행벡터, 열벡터 만들어 사용하기
import numpy as np
a = np.random.randn(5)
a.shape
- 차원을 모른다면 assert 함수 사용하기
assert(a.shape == (5,1))
지금 예제를 사실 Jupyter/Python Notebooks 을 이용해서 코드를 작성하고 실행결과를 그때그때 확인하면서 진행하는 것이 유리하다고 합니다. 저는 개인적으로 Jupyter Notebook 사용하기에는 Visual Studio Code 가 원탑이라고 생각합니다. 물론 패키지 설치에 어려움이 있다면 Colab이 더 나은 선택입니다.
Cost function of Logistic Regression
전혀 자연스럽지 않은 흐름으로 Logistic Regression의 Cost function이 왜 그렇게 생겼는지에 대해 설명해주십니다. 특히 저는 까마귀 고기를 먹어서 logistic regression을 강의노트(링크)를 다시 들여다 보면, binary classification을 위해 0과 1로 분류하는 하고 출력값 y가 0과 1 사이 값을 가지는 함수는 sigmoid 함수입니다.
$$\hat{y} = \sigma(w^Tx+b)$$
$$z=w^Tx+b$$
z값이 크다면, 1로 수렴하겠고, z값이 작다면 0으로 수렴하는 형태를 보이기 때문에 logistic regression을 표현하는 데에 사용하게 되었습니다.
- y값이 1이 될 확률: $P(y=1|x) = \hat{y}$
- y값이 0이 될 확률: $P(y=0 | x) = 1- \hat{y}$
두 식을 합친다면, y값에 따라 각각 두 식에서 정의한 대로 식이 나오도록 아래 식으로 요약할 수 있습니다.
$$P(y | x) = \hat{y}^y(1-y)^{(1-y)}$$
지수분이 너무 복잡해서 내려오게 하고 싶은데 이럴 때 단조적인 성격을 띄는 log 함수를 씌워주면 해결할 수 있습니다. 즉, $P(y | x)$ 증가시키려는 노력이 $\log P(y | x)$ 를 증가시키고 싶은 것이랑 같은 결과를 낼 수 있습니다.
$$\log P(y | x) = (y\log \hat{y} + (1-y)\log (1-\hat{y}))$$
해당 수식은 Loss function의 negative 버전입니다. 보통 학습 알고리즘을 훈련할 때에는 목적이 $\log P(y | x)$확률을 최대화 시키는 것이고, 손실함수는 최소화 시키려는 것이 목적이기 때문에 negative 관계를 가지게 됩니다. -1이 곱해진 값입니다.
Cost function은 m개의 훈련 데이터에 대해 각 샘플이 주어졌을 때의 확률값 $P(x^{(i)})$의 곱으로 구할 수 있습니다.
$$ P(\text{labels in training set}) = \prod_{i=1}^{m} P(y^{(i)} | x^{(i)}) $$
마찬가지로 양변에 log 취하고, Loss function을 치환된 값으로 넣어줍니다.
$$ \log P(\text{labels in training set}) = \log \prod_{i=1}^{m} P(y^{(i)} | x^{(i)}) = - \sum_{i=1}^{m} L(\hat{y}^{(i)}, y^{(i)}) $$
Cost function은 Loss function의 값들의 평균을 최소화하는 것이라고 정의를 한다고 합니다. Loss function의 평균값까지 아래와 같이 표현할 수 있습니다.
$$ J(w, b) = - \log P(\text{labels in training set}) = \frac{1}{m} \sum_{i=1}^{m} L(\hat{y}^{(i)}, y^{(i)}) $$
I'm super excited of this technique and when we talk about neural network without even a single exclusive for loop. -Andrew Ng 교수님의 한마디-
[출처]
- 강의: https://youtu.be/qsIrQi0fzbY?si=NHMHMk_SgV4McWJS
- astroML figure: https://www.astroml.org/book_figures/appendix/fig_broadcast_visual.html