본문 바로가기
Project/DL 스터디&프로젝트

[Euron 중급 세션 2주차] 3. 파이썬과 벡터화

by 주원주 2023. 9. 18.

3. 파이썬과 벡터화

 

📌벡터화(C1W2L11)

핵심어: 벡터화(Vectorization)

 

벡터화란 코드에서 for문을 없애주는 기술.

실제 딥러닝에서 큰 데이터셋을 학습시킬 때가 많은데, 이때 코드가 빠르게 실행되는 것이 중요함. 따라서 벡터화할 수 있는 능력은 매우 중요한 기술임.

 

로지스틱 회귀에서는 z=w^T+b를 계산해야 했음. (w, x는 모두 R^(n_x)의 차원을 가진 열벡터)

벡터화 되지 않은 구현일 때에는 w^Tx를 구하기 위해 z=0과 i가 1부터 n_x까지일 때,

z+=w[i]*x[i]를 하고 마지막에 z+=b를 해줌(원래 로지스틱 회귀에서의 계산).

이는 벡터화되지 안고 구현이 느림.

 

반면 벡터화된 구현은 w^Tx를 직접 계산함. 파이썬이나 NumPy에서 명령어는 np.dot(w,x)인데, 이것이 w^Tx를 계산함.

그리고 b를 더해주면 되는데, 이 방법이 앞서 설명한 방법보다 훨씬 빠르게 동작함.

넘파이를 파이썬에서 직접 실행한 코드는 다음과 같음:

 

import numpy as np #Numpy 라이브러리를 np로 불러옴

a = np.array([1, 2, 3, 4]) #a를 하나의 배열로 지정
print a #a를 출력
# 벡터화 예시
import time #time 모듈을 불러와서 다른 연산들이 얼마나 걸리는지 계산

a = np.random.rand(1000000)
b = np.random.rand(1000000) # a, b 모두 백만 차원의 배열

tic = time.time() # 현재 시간
c = np.dot(a, b)
toc = time.time()

print(c)
print("Vectorized cersion:" + str(1000*(toc-tic)) + "ms")

위 코드 실행 결과 실행할 때마다 조금씩 달라지지만, 실행 시간은 평균적으로 1.5밀리초에서 2밀리초 정가 걸림.

여기에 다음과 같이 벡터화되지 않은 구현을 더해봄.

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") #벡터화하지 않았을 때의 실행시간

실행 결과 벡터화 버전과 아닌 버전 모두 같은 값을 계산했으나(print(c) 구문을 통해 확인), for문을 쓴 벡터화되지 않은 버전은 500밀리초 가까이 걸림. 벡터화되지 않은 버전은 벡터화 버전보다 약 300배 오래 걸린 셈.

 

300배 느리다는 것은 1분 밖에 걸리지 않을 코드가 5시간이나 걸린다는 말인데, 이는 코드를 벡터화한다면 딥러닝 알고리즘을 구현할 시 결과를 훨씬 빨리 얻을 수 있음을 뜻함.

 

대규모 딥러닝 구현이 GPU에서 계산된다고도 하는데, 방금 실행한 파이썬 코드들은 모두 CPU에서 계산됨. GPU와 CPU 모두에게 가끔 SIMD(Single Instruction Multiple Data)라고 불리는 병렬 명령어가 있는데, 이 말은 np.dot을 사용하거나 for문이 필요 없는 다른 함수를 사용할 때 파이썬 NumPy가 병렬화의 장점을 통해 계산을 훨씬 빠르게 할 수 있게 해준다는 것임. CPU와 GPU상의 계산에서 모두 적용되는 이야기이며, GPU는 SIMD 계산에 뛰어나지만 CPU도 나쁘지 않음.

 

🔎파이썬 넘파이 내적 함수: numpy.dot()

벡터 내적(1차원*1차원), 행렬곱(2차원*2차원), 스칼라배(n차원*스칼라), 다차원 내적(n차원*1차원), 다차원 행렬곱(n차원*m차원) 등 여러 가지 계산 가능. 위 예시에서는 다차원 행렬곱(백만*백만)에서 사용함.

기본적으로 2개의 input을 받으며, 3개 이상의 array에 대한 곱은 np.dot 함수를 여러 차례 겹쳐서 실행해야 함.

(출처: https://jimmy-ai.tistory.com/75)

 

 

📌더 많은 벡터화 예제(C1W2L12)

핵심어: 벡터화(Vectorization), for 문(for loop), 넘파이(numpy)

 

신경망이나 로지스틱 회귀를 프로그래밍할 때 기억해야 할 것은 가능한 한 for문을 사용하지 않는 것임. for문을 절대 사용하지 말아야 하는 것은 아니지만, 필요한 값을 계산할 때 내장 함수나 다른 방법을 쓸 수 있다면 for문을 쓰는 것보다 대부분 더 빠를 것.

 

예를 들어 행렬 A벡터 v의 곱인 벡터 u를 계산하고 싶을 때, 행렬곱의 정의는 u_i = A_ij * v_j의 합(시그마j)과 같음.

우선 벡터화하지 않은 구현을 살펴보면, 먼저 u를 np.zeros(n,1)으로 정의(n*1의 0으로만 채워진 array 생성)하고 그 후에 i와 j에 대한 for문을 작성함. 이후 u[i]는 A[i][j] * v[j]가 됨. 여기엔 i와 j에 대한 두 개의 for문이 존재하며 벡터화되지 않은 버전임.

한편 벡터화된 버전은 먼저 u를 np.dot(A,v)로 지정함. 오른쪽에 벡터화된 버전은 두 개의 for문을 없애므로 훨씬 빠르게 계산 가능함.

 

또 다른 예시를 살펴보자면, 메모리상에 벡터 v가 있고 이 벡터 v의 모든 원소에 지수 연산을 하고 싶다고 가정. 다른 말로는 원소가 e^(v_1)부터 e^(v_n)인 벡터 u를 계산하는 것. 

우선 벡터화되지 않은 구현부터 살펴보자면, 먼저 u를 0인 벡터로 초기화하고 그 뒤에는 원소를 하나씩 계산하는 for문이 존재함.

u = np.zeros((n,1))
for i in range(n):
	u[i] = math.exp(v[i])

하지만 파이썬 NumPy에는 이 벡터들을 하나의 호출로 계산해주는 내장 함수가 많은데, 그 중 하나가 Numpy를 np로 가져오고 간단히 u를 np.exp(v)로 지정하는 것임.

import numpy as np
u = np.exp(v)

여기서는 입력 벡터인 v를 사용해 출력 벡터인 u를 한 줄로 계산하고 for문을 제거함. 위 구현이 for문이 필요한 구현보다 훨씬 속도가 빠르다고 할 수 있음.

 

🔎자연상수 e에 대한 지수함수: numpy.exp()

np.exp()는 밑수가 자연 상수 'e'(약 2.71828)이고, 이에 대해 입력값 n제곱을 한 것을 의미함.

예를 들어 np.exp(10) == math.e**10. 또한 np.exp(x)는 입력값 x가 어떤 수가 들어오더라도 0보다 작아지지 않음.

(출처: https://woogong80.tistory.com/277)

 

NumPy 라이브러리에는 다른 벡터 함수들도 많은데, 그 중 몇 가지 살펴보자면

  • np.log: 원소의 로그값 구함
  • np.abs: 절대값 구함
  • np.max(v,0): v의 원소와 0 중에서 더 큰 값 반환
  • v**2: 모든 원소를 제곱한 벡터 반환
  •  1/v: 원소의 역수로 이루어진 벡터 반환

따라서 for문을 쓰고 싶을 때, 그 공식을 쓰지 않고 NumPy 내장 함수를 쓸 수 있는지 확인하는 것이 좋음.

 

여기서 배운 내용을 가지고 로지스틱 회귀와 경사하강법에 적용하면 두 개 중 하나의 for문을 제거할 수 있음.

 

위 로지스틱 회귀의 도함수를 구하는 코드를 보면, 두 개의 for문이 있음. 예제에서는 n_x가 2였으나 특성이 2개 이상이라면 dw_1, dw_2 등에 for문이 필요하게 될 것. 마치 j가 1부터 n_x까지일 때 dw_j를 갱신하는 for문이 있는 셈.

이 두 번째 for문을 없애는 것이 현재 목표.

 

dw_1, dw_2 등을 0으로 초기화하는 대신(첫 번째 줄), 이 부분을 지우고 dw를 벡터로 만들 것. dw를 np.zeros((n_x,1))로 지정하여 n_x 차원의 벡터로 만들어 줌. 그러면 두 번째 for문 대신, 벡터 연산인 dw += x^(i)*dz(i)로 바꿀 수 있음. 그리고 마지막 부분에 dw /= m을 사용하여 총 개수를 나눠줄 수 있음.

 

이렇게 두 개의 for문을 하나로 줄였으며, 아직 훈련 샘플을 순환하는 첫 번째 for문이 남아있음.

 

 

📌로지스틱 회귀의 벡터화(C1W2L13)

핵심어: 벡터화(Vectorization), 로지스틱 회귀(Logistic Regression)

 

이제 로지스틱 회귀를 벡터화하여 전체 훈련 세트에 대한 경사하강법 반복문에서 for문을 전혀 사용하지 않고 구현하는 방법을 배울 것.

 

정방향 전파부터 살펴보자면, m개의 훈련 샘플이 있을 때 처음 샘플을 계산하려면,

 z(1) = w^T*x(1) + b 공식으로 z를 계산하고, a(1) = σ(z(1)) 공식으로 활성값, y의 예측값도 계산해야 함. 

두 번째 샘플을 계산하기 위해서는 z(2) = w^T*x(2) + b, a(2) = σ(z(2)) 값을,

세 번째 샘플을 계산하기 위해서는 z(3) = w^T*x(3) + b, a(3) = σ(z(2)) 값을 계산해야 함.

m개의 훈련 샘플이 있다면 이 과정을 m번 시행해야 함.

정방향 전파 단계를 실행하기 위해, 즉 m개의 훈련 샘플에 대해 이 예측값을 계산하는 데 for문 없이 계산하는 방법이 존재함.

 

우선 X는 훈련 입력을 열로 쌓은 행렬이라는 것을 기억해야 하며, n_x행 m열 행렬임(NumPy 방식으로 쓰자면 X=(n_x,m)차원 행렬). 먼저 z(1), z(2), z(3) 등을 한 단계 혹은 한 줄의 코드로 계산할 것.

행 벡터이기도 한 (1,m)행렬을 만든 후, 여기에 z(1), z(20)에서 z(m)까지 동시에 계산할 것. 이는 w^T*X와 b로 이루어진 벡터의 합으로 표현될 수 있음. b로 이루어진 것은 (1,m) 벡터 혹은 (1,m) 행렬, 즉 m차원 행 벡터임.

행렬 곱셈에 익숙해진다면, w^T에 x(1), x(2)부터 x(m)으로 이루어진 행렬을 곱했을 때, w^T는 행 벡터라는 것을 알 수 있음.

첫 항은 w^T*x(1)이 되고, 두 번째 항은 w^T*x(2), 그리고 계속해서 w^T*x(m)까지 이어질 것.

b로 이루어진 행 벡터를 더하면 각 요소에 b를 더하게 됨, 결국 첫 요소와 두 번째 요소부터 m번째 요소까지 적혀있는 (1,m)벡터를 얻는 것.

 

위의 정의를 살펴보면 첫 번째 요소는 z(1)의 정의와, 두 번째 요소는 z(2)의 정의와 일치한다는 것을 알 수 있음.

X가 훈련 샘플을 가로로 쌓은 결과인 것처럼, 대문자 Z를 소문자 z들을 가로로 쌓은 것이라고 정의함.

훈련 샘플인 소문자 x를 가로로 쌓았을 때 대문자 X 변수를 얻은 것처럼,  소문자 z를 가로로 쌓았을 때 대문자 Z 변수를 얻음. 이 값을 계산하는 넘파이 명령어는 Z = np.dot(w.T, X) + b.

파이썬에서 미묘한 점은, b는 하나의 수인데 (1, 1) 행렬이라고도 할 수 있음. 벡터 np.dot(w.T,X)와 실수 b를 더한다면 파이썬은 실수 b를 자동으로 (1, m)행 벡터로 바꿔줌. 이 연산은 브로드캐스팅이라고도 불림.

여기서 중요한 포인트는, Z = np.dot(w.T, X) + b라는 한 줄의 코드로 Z를 계산할 수 있다는 것. 대문자 Z는 소문자 z(1)부터 z(m)까지를 포함하는 (1, m) 행렬이 됨.

 

여기까지 Z를 계산했는데, 이제는 a(1)부터 a(m)을 동시에 계산하는 방법을 알아야 함.

소문자 x와 z를 가로로 쌓아서 대문자 X와 Z를 얻었던 것처럼, 소문자 a를 가로로 쌓아 대문자 A라는 새로운 변수 정의.

 

이처럼 소문자 z와 a를 하나씩 계산하기 위해 m개의 훈련 샘플을 순환하는 대신, Z = np.dot(w.T, X) + b 한 줄의 코드로 모든 z를 동시에 계산할 수 있고, A=[a(1), a(2), ..., a(m)] = σ(z)의 적절한 구현으로 모든 a를 동시에 계산할 수 있음.

 

이렇게 모든 m개의 훈련 샘플을 동시에 정방향 전파하는 벡터화된 구현을 할 수 있었음.

 

📌로지스틱 회귀의 경사 계산을 벡터화하기(C1W2L14)

핵심어: 벡터화(Vectorization), 로지스틱 회귀(Logistic Regresstion), 경사 하강법(Gradient Descent)

 

이번에는 벡터화를 통해 m개의 전체 훈련 세트에 대한 경사 계산을 동시에 하는 법을 배울 것.

 

경사 계산을 위해 첫 샘플 dz(1) = a(1) - y(1)을 계산하고, dz(2) = a(2) - y(2)이며 총 m개의 훈련 샘플에 대해 이 과정을 계속 반복함. 여기서 정의할 것은 dZ인데, 이는 dz(1), dz(2)부터 dz(m)까지 모든 dz를 가로로 쌓은 것이며 (1, m)행렬 혹은 m차원 열 벡터가 됨. 

A = [a(1) ... a(m)]이며, Y = [y(1) .... y(m)]이라고 정의함. 마찬가지로 가로로 쌓여있음. 이 정의를 따르면, dZ가 A-Y로 계산할 수 있음을 알 수 있음. 첫 번째 요소는 a(1) - y(1), 두 번째 요소는 a(2) - a(3)이고 이것이 계속되기 때문. 첫 요소인 a(1)-y(1)은 dz(1)의 정의와 정확히 일치하고, 나머지 요소도 정확히 일치함. 따라서 한 줄의 코드로 이 모든 것을 동시에 계산할 수 있음.

앞서 이미 하나의 for문을 제거했지만, 아직 훈련 샘플을 계산하는 두 번째 for문은 여전히 남아있었으며 이로 인해 dw를 영벡터로 초기화해준 뒤 여전히 훈련 샘플을 순환해주어야 했음.

첫 샘플에 대해 dw += x(1)*dz(1)을 해주고 두 번째 샘플과 나머지 샘플에 대해 m번 반복한 후 마지막에 m으로 나누어줌.

db도 마찬가지로 0으로 초기화해주고 db+=dz(1), db+=dz(2), ..., db+=dz(m)까지 반복한 뒤 db를 m으로 나누어줌. 

이렇게 지난 구현에서 했으며, 이미 하나의 for문을 제거해서 dw는 벡터이고 dw(1), dw(2)등을 따로 갱신하지 않음(이미 제거함). 하지만 m개의 훈련 샘플에 대한 for문은 여전히 남아있음. 이 연산을 벡터화 시켜줘야 함.

 

db는 i가 1부터 m까지일 때 dz(i)의 합을 m으로 나눈 값인데, 여기서 모든 dz는 열벡터임. 파이썬에서는 1/m에 np.sum(dZ)라고 작성. 단순히 dZ를 가지고 이 함수를 호출하면 db를 반환함.

dw에 대해서는, 식을 쓴 다음 확인해볼 것.

dw는 1/m * X * dZ^T를 곱한 값이 됨. 그 이유는 1/m에 x(1)부터 x(m)이 가로로 쌓인 행렬 X를 곱하고 dZ^T는 dz^(1)부터 dz^(m)까지의 벡터가 되기 때문. 이 행렬과 벡터를 곱하면 1/m에 x(1)dz(1)부터 x(m)dz(m)을 모두 더한 값을 곱한 것이 됨. 이 벡터는 (m,1) 벡터이고 dw의 값이 됨(dw는 x(i)dz(i)의 합이기 때문). 이 행렬과 벡터의 곱이 같음.

이에 따라 한 줄의 코드로 dw를 계산할 수 있게됨.

벡터화된 도함수 계산은 db=1/m(np.sum(dZ)) 줄로 db를 계산하고, dw=1/mXdz^T 줄로 dw를 계산함.

for문 없이 변수의 갱신값을 계산할 수 있게된 것. 이 모든 것을 모아 로지스틱 회귀를 어떻게 구현하는지 살펴보면 다음과 같음.

 

위는 기존의 벡터화되지 않은 비효율적인 구현임.

우선 dw1, dw2를 순환하는 대신 이를 벡터값으로 바꿔서 모든 것이 벡터값인 dw+=x(i)dz(i)로 대체하여 두 번째 for문을 없애줌.

그 다음 위의 for문을 제거하기 위해, Z=w^TX+b로 놓고 이에 상응하는 코드 Z=np.dot(w^T, X)+b를 작성. A=σ(Z)가 됨.

모든 i에 대해 z(i)와 a(i)값들을 계산해준 것.

dZ = A - Y이고, 이는 모든 i에 대한 dz(i)값을 계산한 것.

마지막으로 dw = (1/m)XdZ^T이고 db = 1/m*np.sum(dZ)가 됨.

이렇게 for문 없이 정방향 전파와 역방향 전파를 했고 m개의 모든 훈련 샘플에 대해 예측값과 도함수도 계산함.

경사 하강법 갱신은 w = w-αdw가 되며(α는 학습률이며, dw는 앞서 계산) b = b-αdb로 갱신됨.

이렇게 로지스틱 회귀의 한 구간을 반복함.

만약 경사하강법을 천 번 반복하고 싶다면, 반복 횟수만큼 for문이 필요함. 이는 가장 바깥쪽의 for문이 되며 이를 없앨 방법은 존재하지 않음.

 

📌파이썬의 브로드캐스팅(C1W2L15)

핵심어: 브로드캐스팅(Broadcasting)

 

브로드캐스팅은 파이썬 코드 실행 시간을 줄일 수 있는 또 다른 기법임.

 

예를 들어 위 행렬은 네 가지 다른 음식 100g당 탄수화물, 단백질, 지방이 가지는 칼로리를 보여줌. 사과 100g에 들어있는 탄수화물 56kcal이며 다른 두 성분은 현저히 낮고, 소고기 100g에 들어있는 단백질은 104kcal를 주며 지방은 135kcal을 줌. 우리의 목적은 각 네 가지 음식의 탄수화물, 단백질, 지방이 주는 칼로리의 백분율을 구하는 것. 예를 들어 Apples 열의 모든 수를 다 더하면(56.0 + 1.2 + 1.8) 100g의 사과는 59칼로리를 가지고 있다는 사실을 알 수 있음. 백분율을 계산한다면 사과에서 탄수화물이 주는 칼로리의 백분율은 (56/59)*100해서 약 94.9%가 됨. 즉 사과의 칼로리는 대부분 탄수화물에서 옴. 반면 동일한 방법으로 계산했을 때, 소고기는 칼로리의 대부분이 단백질과 지방에서 온다는 사실을 알 수 있음. 여기서 하고 싶은 계산은 행렬의 네 열 안의 수의 합을 구하고(100g의 사과, 소고기, 달걀, 감자 안의 총 칼로리) 그 후엔 행렬 전체를 나눠서 네 가지 음식 안의 탄수화물, 단백질, 지방이 주는 칼로리의 백분율을 구함. 이를 for문 없이 수행하기 위해, 브로드캐스팅이 필요함.

전체 행렬을 3X4 행렬 A라고 부른다고 가정하고, 한 줄의 파이썬 코드만으로 각 열의 합(네 가지 다른 음식에 들어있는 총 칼로리인 네 숫자)을 구할 것. 파이썬 두 번째 줄에서는 각 네 열을 각 열의 합으로 나눌 것.

import numpy as np

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]])
              
print(A)

위 코드로 행렬 A를 작성한 후, 앞서 설명한 파이썬 코드 두 줄을 작성함.

cal = A.sum(axis=0) # 열을 더하겠다는 의미
print cal

위 코드의 실행 결과는 [59.0, 239, 155.4, 76.9]로, 첫째 줄에서 열을 더하면 사과의 총 칼로리는 59이며 그 외 소고기, 달걀,감자의 칼로리 또한 출력됨.

다음 두 번째 줄 코드에서 백분율을 계산하면,

percentage = 100*A/cal.reshape(1,4)
print percentage

라고 작성함. 위 코드의 실행 결과, 행렬 A를 1x4 행렬로 나누었으며 백분율 행렬을 반환함. 예를 들어 이전에 직접 계산했던 것과 동일하 사과는 94.9%의 칼로리가 탄수화물에서 온다는 사실을 확인 가능.

 

조금 더 설명을 붙이자면, axis=0이라는 매개 변수는 파이썬에게 세로로 더하라고 알려주는 것이며, 가로축은 axis1이 됨. 

100*A/(cal.reshape(1,4) 명령어는 파이썬 브로드캐스팅의 한 예시로, 3x4 행렬인 A를 1x4 행렬로 나누어주는 과정임.

사실 코드 첫 줄이 실행된 이후에 변수 cal은 이미 1x4 행렬이라 reshape 함수를 사용할 필요는 없지만, 파이썬 코드를 사용할 때 행렬의 차원이 확실하지 않다면 reshape 함수를 사용하여 필요한 차원의 행렬로 확실히 만들어주는 것이 좋음. 

 

여기서 3x4 행렬을 1x4 행렬 혹은 벡터로 나누는 과정 대한 구체적인 설명을 하자면, 브로드캐스팅에 대한 더욱 자세한 이해가 필요함.

 

브로드캐스팅의 몇 가지 예를 더 들자면, 위와 같이 4x1 벡터에 상수를 더한다면 파이썬은 이 수를자동으로 4x1 벡터로 만들어줌. 이 두 벡터를 더하면 맨 오른쪽에 있는 벡터가 결과값으로 나옴(모든 요소에 100을 더한 것).

이 브로드캐스팅은 행 벡터와 열 벡터 모두에게 작동하며, 이전 로지스틱 회귀에서 벡터에 b를 더할 때 비슷한 형태를 볼 수 있었음. 

다른 예시에서, 2x3 행렬이 있다고 가정하면 여기에 (1, n) 행렬을 더할 수 있음. 일반화하자 (m, n) 행렬이 있고, 그 행렬에 (1, n) 행렬을 더하는 것. 파이썬은 이 행렬을 m번 복사해서 (m, n) 행렬로 만들어주는 것인데, 여기서는 (1, 3) 행렬을 두 번 복사해서 (2, 3) 행렬로 만들어주는 것임. 이 두 행렬을 더하면 가장 오른쪽의 (2, 3) 행렬이 되는 것(첫 열에는 100을 더하고, 다른 두 열에는 200과 300을 더함).

전 슬라이드에서도 동일한 과정을 거쳤으나 단지 덧셈 대신 나눗셈을 한 것일 뿐.

 

마지막 예시를 보자면, (m, n) 행렬과 (m, 1) 벡터 혹은 행렬을 더하면 이 행렬을 n번 가로로 복사해서 (m, n) 행렬로 만들어주는 것. 즉 가로로 세 번 복사해서 더한다고 생각할 수 있음. 결과는 첫 행에 100을 더하고, 둘째 행에는 200을 더한 것.

 

파이썬 브로드캐스팅의 좀 더 일반적인 원리를 살펴보자면, (m, n) 행렬에 (1, n) 행렬을 더하거나 빼거나 곱하거나 나눌 때 이 행렬을 m번 복사하여 (m, n) 행렬을 만든 뒤 요소별 연산을 해줌. (m, n)행렬을 (m, 1) 행렬과 연산한다면 이 행렬을 n번 복사해서 (m, n) 행렬로 만든 뒤 요소별 연산을 해줌. 

브로드캐스팅의 다른 형태를 보면, (m, 1) 행렬 혹은 열 벡터가 있을 때 실수(1x1 행렬)와 덧셈, 뺄셈, 곱셈, 혹은 나눗셈을 하고자 하면 이 실수를 m번 복사하여 (m, 1) 행렬을 만들어주고 이 예시처럼 요소별 덧셈을 해줌. 행 벡터에서 또한 비슷하게 작동함.

 

📌파이썬과 넘파이 벡터(C1W2L16)

핵심어: 넘파이(numpy)

 

파이썬의 브로드캐스팅 연산, 더 일반적으로 말해 NumPy를 사용한 파이썬 프로그래밍의 유연성은 프로그래밍 언어의 장점과 단점 모두 될 수 있음.

우선 장점으로, 언어의 넓은 표현성과 유연성은 한 줄의 코드만으로도 많은 것을 할 수 있게 해줌.

하지만 동시에 단점으로, 브로드캐스팅의 유연성은 브로드캐스팅의 자세한 내용과 작동 방법을 모른다면 가끔 원인 불명의 찾기 힘든 오류가 생김. 예를 들어 행 벡터와 열 벡터를 더한다면, 차원 오류나 형 오류가 생걸 것이라 예상하지만 결과적으로 두 벡터의 합인 행렬이 나올 수도 있음. 이 이상한 결과는 파이썬 내부 논리로 처리되지만, 파이썬에 익숙하지 않다면 이상하고 찾기 어려운 오류들이 간혹 발생함.

여기서 파이썬 코드를 간단하게 하거나 이상한 오류를 없애는 과정들이 도움이 될 것.

 

파이썬 NumPy에서, 특히 벡터를 만드는 과정에서 자주 발생하는 직관적이지 않은 결과를 보기 위해 다음과 같이 코드를 짬.

import numpy as np

# 가우시안 분포를 따르는 변숫값 5개를 배열 a에 저장
a = np.random.randn(5)
print(a)
[0.50290632 -0.29691149 0.95429684 -0.82126861 -1.46269164]
print a.shape
(5, )

이때 a의 크기는 (5, )이며, 파이썬에서는 랭크가 1인 배열이라고 부르는데 이는 행 벡터도 열 벡터도 아님.

따라서 직관적이지 않은 결과를 도출함.

print(a.T)
[0.50290632 -0.29691149 0.95429684 -0.82126861 -1.46269164]

a의 전치를 출력해도 a와 똑같이 생겼고,

print(np.dot(a, a.T))
4.06570109321

a와 a의 전치를 곱했을 때 외적이 되고 행렬이 나와야 한다고 생각할 수 있지만 하나의 수만 나옴.

 

신경망을 구현할 때, 위와 같이 크기가 (5, ) 혹은 (n, )인 랭크가 1인 배열을 아예 사용하지 않는 것이 좋음.

 

대신 다음과 같이 a를 np.random.randn(5,1)로 설정한다면, a는 5x1 열 벡터가 됨.

a = np.random.randn(5,1)
print(a)
[[-0.0967311 ]
 [-2.38617377]
 [-0.3243588 ]
 [-0.96216349]
 [ 0.54410384]]

앞서 살펴본 코드에서는 a와 a의 전치가 동일했으나, 지금 a의 전치를 출력해보면 행 벡터가 됨.

print(a.T)
[[-0.0967311 -2.38617377 -0.3243588 -0.96216349 0.54410384]]

다음과 같은 행 벡터가 출력됨.

이 자료구조에서 조금 다른 점은, 이전 코드와 달 a의 전치에는 대괄호가 두 개 있다는 점임. 이는 현재가 1x5 행렬이며, 이전 것은 랭크가 1인 배열이라는 것임.

a와 a의 전치의 곱을 출력하면

print(np.dot(a, a.T))

벡터의 외적 행이 나오게 됨.

 

처음 실행한 명령어 a = np.random.randn(5)에서는 랭크1 배열이라 부르며, 행 벡터도 열 벡터도 아닌 이상한 자료구조이며 결코 직관적이지 않음. 대신 프로개래밍 예제나 신경망에서 로지스틱 회귀를 구현할 때, 랭크1 배열을 사용하지 않는다는 것임.

대신 항상 배열을 만들 때 이처럼 (5, 1) 열 벡터나 행 벡터를 만들어준다면 벡터의 동작을 더 쉽게 이해할 수 있을 것.

a = np.random.randn(5,1)의 경우 열 벡터가 되고, 열 벡터처럼 동작하며 따라서 이를 (5, 1) 행렬이나 열 벡터라고 생각할 수 있음.

a = np.random.randn(1, 5)의 경우 a.shape = (1, 5)가 되고 한결같이 행 벡터처럼 반응함. 벡터가 필요할 때 이 둘 중 하나를 쓰고 랭크1은 되도록 쓰지 않는 것이 좋음.

코드에서 벡터의 차원을 확실히 알지 못할 때,

assert(a.shape == (5, 1))과 같이 a가 (5, 1) 열 벡터라는 것을 확실히 하기 위해 assert 함수를 써줌.

만약 랭크1 배열을 얻게 된다면 reshape 함수를 써서 (5, 1) 배열이나 (1, 5) 배열로 바꾸어 일관되게 열 벡터나 행 벡터로 동작하게 할 수 있음. 랭크1 배열을 없애면 코드를 더 간단하게 만들 수 있고 불필요한 오류를 줄일 수 있음.

따라서 랭크1 배열 대신, 열 벡터인 (n,1) 행렬 혹은 행 벡터인 (1, n) 행렬을 사용하는 것이 권장되며 행렬과 벡터를 필요한 차원으로 만들기 위해 reshape 함수를 자주 사용할 수 있음.

 

📌Jupyter / iPython Notebooks 가이드(C1W2L17)

핵심어: Jupyter Notebook

 

주피터 노트북의 몇 가지 특징을 살펴보자면 다음과 같음

  • 셀이라고 불리는 코드 구간을 실행하려면 Cell을 누른 뒤 Run Cell 누르기. 대부분 컴퓨터에서 단축키는 shift+enter
  • 마크다운 언어: 마찬가지로 셀을 실행하면 깔끔한 글이 보여짐.
  • 셀의 코드를 실행하면 실제로는 커널에서 실행이 되며, 이는 서버에서 이루어지기 때문에 인터넷 연결이 불안정할 경우 실행되는 커널이 꺼질 수 있음.
  • 커널이 꺼진 경우 Restart을 누르면 해결 가능

IPython Notebook에는 코드 구간이 여러 개 있을 수 있으며, 만약 프로그래밍 예제가 아닌 코드라도 꼭 실행해야 함

(NumPy를 불러오고 아래 구간에서 필요한 변수들을 초기화해주는 역할을 하기 때문)

 

마지막으로 구현을 다 끝내면 오른쪽 위에 제출 버튼이 있는데, 이를 눌러 채점을 위해 제출하면 완료.

이처럼 IPython Notebook는 코드를 구현하고 결과를 보고 학습하는 데 굉장히 효과적이라고 할 수 있음.

 

📌로지스틱 회귀의 비용함수 설명(C1W2L18)

핵심어: 로지스틱 회귀(Logistic Regression), 손실함수(Loss Function), 비용함수(Cost Function)

 

왜 로지스틱 회귀에서 비용함수를 사용하는 것일까.

로지스틱 회귀에서 y의 예측값은 σ(w^Tx + b)였음.

y의 예측값은 x가 주어졌을 때 y가 1일 확률이었는데, 따라서 알고리즘은 x라는 특성이 주어졌을 때 y가 1일 확률인 y의 예측값을 반환해야 함.

즉 y가 1이라면 x가 주어졌을 때 y가 1일 확률은 yhat이며, y가 0이라면 x가 주어졌을 때 y가 0일 확률은 1-yhat이 됨. yhat은 y가 1이 될 확률, 1-yhat은 y가 0이 될 확률이라는 것.

 

여기서 할 일은 y가 0이거나 1일 때 py(y|x)를 정의하는 두 등식을 가지고 한 개의 등식으로 합치는 것.

y가 0이거나 1ㅣ여만 하는 이유는 이진 분류이기 때문.

이 두 등식을 하나로 합쳐보면 다음과 같음.

P(y|x) = (yhat^y)*(1-yhat)^(1-y)

먼저 y가 1이라고 가정을 해보면, 그럼 (yhat^y)는 yhat의 1승이기 때문에 yhat 그대로가 결과가 됨.

(1-yhat)^(1-y) 항은 1-y가 0이기 때문에 (1-yhat)^0이 되며 어떤 수의 0승은 1이기 때문에 이 항은 사라지게 됨.

따라서 y가 1일 때 이 식은 p(y|x) = yhat이 된다고 볼 수 있음.

 

한편 y가 0이라고 가정하면, 위 식은 p(y|x) = yhat^0으로 시작함. 하지만 어떤 수의 0승은 1이기 때문에 결괏값은 1과 같음. 여기에 (1-yhat)^(1-y)를 곱해주면 1-y는 1-0이 되기 때문에 이 식은 1*(1-yhat), 즉 1-yhat이 됨.

따라서 y가 0일 때 이 식은 p(y|x) = 1-yhat이 된다고 볼 수 있음.

 

이를 통해 식 P(y|x) = (yhat^y)*(1-yhat)^(1-y)이 P(x|y)의 정확한 정의라는 것을 알 수 있음.

마지막으로 로그 함수는 강한 단조 증가 함수이기 때문에, log p(y|x)를 최대화 하는 것은 p(y|x)를 최대화 하는 것과 동일한 결과를 보여줌.

logp(y|x) = log (yhat^y)*(1-yhat)^(1-y)이 되며, 이는 ylog(yhat) + (1-y)log(1-yhat)으로 간소화할 수 있음.

이는 전에 정의한 손실 함수의 음수가 됨. 음수가 된 이유는 보통 학습 알고리즘을 훈련시킬 때 확률을 높이려고 하지만, 로지스틱 회귀에서는 손실 함수를 최소화 하고 싶기 때문임. 손실 함수를 최소화 시키는 것은 확률의 로그값을 최대화 시키는 것과 같음.

하나의 샘플의 손실 함수는 이렇게 생겼는데, 반면 전체 훈련 세트의 m개 샘플에 대한 비용 함수는 어떻게 될까.

 

훈련 세트 안의 모든 레이블에 대한 확률은 훈련 샘플들이 독립동일분포(iid)라고 가정했을 때,전체 샘플에 대한 확률은 각 확률의 곱임. i가 1부터 m까지일 때 p(y^(i) * x^(i))의 곱인 것.

최대 우도 추정을 한다면 훈련 세트의 타깃 확률을 최대화해주는 변수를 찾아야 함.

 

🔎최대 우도 추정이란?

우도란 가능한 정도, 가능도라고도 하는데, 통계학에서 우도란 어떤 확률분포에 대해서 주어진 관측값이 나올 가능성을 의미함. 이는 확률분포가 주어진 상태에서 관측값이 관측될 정도를 나타내는 확률과 또 다른 의미임.

이산확률분포의 경우 "확률 = 우도"이지만, 연속확률분포의 경우 확률은 확률밀도함수에서의 면적, 우도는 확률밀도함수에의 y값을 의미함. 우도가 크다는 것은 그만큼 데이터를 잘 설명하는 것을 의미하는데, 이는 확률분포에서 관측값을 발견할 가능성이 높기 때문(데이터가 관측된 이유는 특별히 목격된 것이 아닌 '흔히 있는 일' 즉, '발생 확률이 높은 사건'이라는 전제가 있다는 것).

따라서 최대 우도 추정이란 여러 개의 데이터들을 관측했을 때, 해당 사건들의 발생확률을 최대로 높이는 분포를 찾는 것이 목표임. 다른 말로 데이터들을 가장 잘 설명할 수 있는 분포를 찾아내는 것. 이는 곧 총 우도(모든 우도의 곱)가 최대가 되는 분포를 찾는 것으로 생각할 수 있음.

(출처: https://blog.naver.com/wissnis/222066468615)

 

해당 강의에서 최대 우도 추정이란 log p(y^(i)|x^(i))의 값을 최대화하는 매개 변수를 찾으라는 의미이며, 다른 말로는 -L(yhat^(i), y^(i))의 합을 최대화하는 값.

 

p(y^(i) * x^(i))를 최대화 한다는 것은 로그값을 최대화하는 것과 같으므로, 양변에 로그를 씌워줌. 곱에 로그를 씌우면 로그의 합이 되므로 훈련 세트 레이블의 로그 확률 값은 i가 1부터 m까지일 때 log p(y^(i)|x^(i))의 합이 됨. 앞서 해당 값이 손실 함수의 음수, 즉 -L(yhat^(i), y^(i))라는 것을 확인했었는데, 최대 우도 추정을 통해 알아낸 매개변수들으 ㅣ값이 J(w, b)가 로지스틱 회귀의 비용이라는 것을 보여줌.

이제는 비용을 최소화하고 가능도를 최대화하려고 하기 때문에 음수를 없애고, 스케일을 맞추기 위해 편의상 1/m이라는 비례 계수를 추가함.

요약하자면 비용 함수인 J(w, b)를 최소화함으로써, (훈련 샘플이 독립동일분포라고 가정했을 때) 로지스틱 회귀 모델의 최대 우도 추정을 한 것.