Youtube Link
01 벡터화 https://www.youtube.com/watch?v=qsIrQi0fzbY
02 더 많은 벡터화 예제 https://www.youtube.com/watch?v=pYWASRauTzs
03 로지스틱 회귀의 벡터화 https://www.youtube.com/watch?v=okpqeEUdEkY
04 로지스틱 회귀의 경사 계산을 벡터화 하기 https://www.youtube.com/watch?v=2BkqApHKwn0
05 파이썬의 브로드캐스팅 https://www.youtube.com/watch?v=tKcLaGdvabM
06 파이썬과 넘파이 벡터 https://www.youtube.com/watch?v=V2QlTmh6P2Y
07 Jupyter/iPython Notebooks 가이드
08 로지스틱 회귀의 비용함수 설명 https://www.youtube.com/watch?v=k_S5fnKjO-4
한국어 자막
01 벡터화
다시 오신 것을 환영합니다. 벡터화는 간단히 말하자면 코드에서 for문을 없애는 일종의 예술과 같습니다. 딥러닝 시대에서 실제 딥러닝을 할 때 딥러닝 알고리즘이 빛을 발하게 해주는 큰 데이터 세트를 학습시킬 때가 많습니다. 따라서 코드가 빠르게 실행되는게 중요하죠. 그렇지 않다면 큰 데이터 세트를 학습시킬 때 코드 실행시간이 길어지고 결과를 내기까지 오래 기다려야 합니다. 딥러닝 시대에서 벡터화할 수 있는 능력은 중요한 기술이 되었다고 생각합니다.
예를 들어 시작하죠. 벡터화가 뭘까요?
로지스틱 회귀에선 z = w^T x+b 를 계산해야 했습니다. w는 열 벡터이고 x도 마찬가지 입니다. w는 열 벡터이고 x도 마찬가지 입니다. 특성이 많다면 굉장히 큰 벡터가 됩니다. w와 x는 모두 R^(n_x)의 차원을 가진 벡터입니다. 벡터화 되지 않은 구현일 때엔 w^T x 를 계산하기 위해 z = 0와 i가 1부터 n_x까지일 때 z += w[i] * x[i]를 하고 마지막에 z += b를 할 수 있습니다. 이것은 벡터화되지 않은 구현이고 느립니다. 그에 반해 벡터화된 구현은 w^T x를 직접 계산합니다. 파이썬이나 NumPy에서 명령어는 np.dot(w,x)입니다. 이게 w^T x를 계산하죠. 그리고 b를 더해주면 됩니다. 이 방법이 훨씬 빠르다는 걸 발견할 것입니다.
예를 들어 설명해 보죠. 이건 제가 파이썬 코드를 작성할 Jupyter Notebook입니다. 먼저 NumPy 라이브러리를 np로 불러오죠. 예를 들어 a를 다음과 같은 하나의 배열로 지정합시다. 그 후엔 a를 출력하도록 하죠. 코드를 작성한 뒤에 shift+enter를 누르면 코드가 실행됩니다. 배열 a를 만들고 출력했습니다.
벡터화 예시를 해봅시다. time 모듈을 불러와서 다른 연산들이 얼마나 걸리는지 재겠습니다. 배열 a를 np.random.rand를 사용해 만들겠습니다. 이 코드는 난수로 이루어진 백만 차원의 배열을 만들어줍니다. b도 마찬가지로 백만 차원의 배열입니다. tic을 현재 시간으로 지정합니다. c는 np.dot(a,b)이고 toc을 time.time()으로 지정해줍니다. 출력을 합시다. 이건 벡터화된 버전이니 그렇게 써주고요. toc에서 tic을 빼주고 1000을 곱하여 밀리초 단위로 표현해줍니다. shift+enter를 누르겠습니다. 이 코드는 약 1.5밀리초 정도 걸렸네요. 1.5밀리초에서 3.5밀리초 정도 걸리네요. 실행할 때마다 조금씩 달라지지만 평균적으로 1.5밀리초가 걸립니다. 2밀리초 정도 걸릴 수도 있습니다.
여기에 벡터화되지 않은 구현을 더해봅시다. C를 0으로 설정해주고, tic 을 time.time()으로 해줍니다. 이제 공식을 구현해 보죠. i가 1부터 백만까지일 때 0의 개수가 맞나 봅시다. C += a[i] * b[i]를 쓰고, toc을 time.time()으로 지정합니다. for문을 썼을 때의 시간인 1000*(toc-tic)을 출력해줍니다. 뒤에 ms를 추가해 밀리초라는 걸 표기해줍니다. 추가로 c를 출력해 두 경우 모두 같은 값이라는 걸 확인합시다. shift+enter를 눌러 실행하겠습니다.
벡터화 버전과 아닌 버전 모두 같은 값을 계산했습니다. 벡터화된 버전은 1.5밀리초가 걸렸지만, for문을 쓴 벡터화되지 않은 버전은 500밀리초 가까이 걸렸습니다. 벡터화되지 않은 버전은 벡터화 버전보다 약 300배 오래 걸린거죠. 이 예에서 보시는 것처럼 코드를 벡터화하는 걸 기억한다면 코드가 300배 이상 빨라질 것입니다. 다시 한 번 실행해보죠. 벡터화 버전은 1.5밀리초가 걸렸고 for문을 쓴 버전은 481밀리초가 걸렸습니다. 다시 해도 300배 정도 느리다는 걸 알 수 있죠. 300배 느리다는 건 1분 걸릴 코드가 5시간 걸린다는 말입니다. 코드를 벡터화한다면 딥러닝 알고리즘을 구현 시 결과를 훨씬 빨리 얻을 수 있습니다.
몇몇 분들은 많은 확장형 딥러닝 구현이 GPU에서 계산된다고 들었을 텐데요. 방금 Jupyter Notebook에서 실행한 예제들은 모두 CPU에서 계산됐습니다. GPU와 CPU 모두에게 가끔 SIMD라고 불리는 병렬 명령어가 있습니다. Single Instruction Multiple Data의 줄임말이죠. 이 말은 np.dot을 사용하거나 for문이 필요 없는 다른 함수를 사용할 때 파이썬 NumPy가 병렬화의 장점을 통해 계산을 훨씬 빠르게 할 수 있게 해줍니다. CPU와 GPU상의 계산에서 모두 적용되는 이야깁니다. GPU는 SIMD 계산을 엄청나게 잘 하지만 CPU도 그렇게 나쁘지는 않습니다. GPU보다는 부족하지만요.
여러분은 벡터화가 코드를 얼마나 빠르게 해주는지 보았습니다. 여기서 기억해야 할 점은 될 수 있는 한 for문을 쓰지 않는 겁니다. 다음 동영상에서 벡터화의 예를 더 보고 로지스틱 회귀를 벡터화해보겠습니다.
02 더 많은 벡터화 예제
지난 비디오에선 내장 함수를 쓰고 for 문을 피하는 방법으로 벡터화가 어떻게 코드의 속도를 빠르게 해주는지 몇 가지 예를 봤습니다. 몇 가지 예를 더 살펴보죠.
신경망이나 로지스틱 회귀를 프로그래밍할 때 기억해야 할 것은 가능한 한 for 문을 쓰지 않는 것입니다. for 문을 쓰지 않는 게 항상 가능한 건 아니지만, 필요한 값을 계산할 때 내장 함수나 다른 방법을 쓸 수 있다면 for 문을 쓰는 것보다 대부분 빠를 것입니다.
다른 예시를 한번 보죠. 행렬 A와 벡터 v의 곱인 벡터 u를 계산하고 싶을 때, 행렬 곱셈의 정의는 u_i는 A_ij ⋅v_j의 합입니다. u_i의 정의가 그렇습니다. 벡터화되지 않은 구현은 먼저 u를 np.zeros(n,1)으로 정의하고, 그 후에 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 문이 있습니다. 하지만 파이썬 NumPy에는 이 벡터들을 하나의 호출로 계산해주는 내장 함수가 많습니다. 제가 구현할 방법은 먼저 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 문을 제거할 수 있는지 보죠. 로지스틱 회귀의 도함수를 구하는 코드를 봅시다. 여기엔 두 개의 for문이 있습니다. 하나는 여기있고, 두 번째는 여기죠. 예제에서는 n_x가 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 문이 하나 남아있죠.
이 영상이 벡터화에 대한 감을 주었으면 합니다. for 문을 하나 제거하는 것만으로도 코드는 빠르게 실행됩니다. 하지만 더 좋은 방법이 있습니다. 다음 영상에서는 로지스틱 회귀를 더 벡터화하는 방법을 보겠습니다. 훈련 샘플에서 조차 for 문을 하나도 쓰지 않고, 훈련 세트 전체를 동시에 처리할 수 있다는 놀라운 결과를 보겠습니다. 다음 영상에서 뵙겠습니다.
03 로지스틱 회귀의 벡터화
벡터화가 코드 속도를 어떻게 높여주는지 얘기해봤습니다. 이 영상에선 로지스틱 회귀를 벡터화하여 전체 훈련 세트에 대한 경사 하강법의 한 반복문에서 for 문을 하나도 쓰지 않고 구현하는지 알아보겠습니다. for 문이 하나도 없는 신경망을 소개하려니 매우 신나네요. 시작하죠.
일단 정방향 전파부터 살펴보죠. m개의 훈련 샘플이 있을 때 처음 샘플을 예측하려면 이것을 계산해야 합니다. 익숙한 이 공식으로 z를 계산하고, 활성값, y의 예측값도 계산해야 합니다. 두 번째 훈련 샘플을 예측하려면 이 값을 계산해야 하죠. 세 번째를 예측하기 위해서는 이 값을 계산해야 합니다. m개의 훈련 샘플이 있다면 이것을 m번 해야 합니다. 정방향 전파 단계를 실행하기 위해선, 그러니까 m개의 훈련 샘플에 대해 이 예측값을 계산하는데 for 문 없이 계산하는 방법이 있습니다.
어떻게 하는지 살펴봅시다. X는 훈련 입력을 열로 쌓은 행렬이란 걸 기억합시다. 이렇게 행렬이 있죠. n_x행 m열 행렬이죠. 파이썬 NumPy 방식으로 썼습니다. X가 (n_x, m) 차원 행렬이라는 것이죠. 먼저 보여주고 싶은 것은 z^(1), z^(2), z^(3)등을 한 단계 혹은 한 줄의 코드로 계산하는 법입니다. 행 벡터이기도 한 (1, m) 행렬을 먼저 만들죠. 여기에 z^(1), z^(2)에서 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를 더하게 됩니다. 결국엔 (1, m) 벡터를 얻게 되죠. 첫 요소와 두 번째 요소부터 m번째 요소까지 적혀있습니다. 위의 정의를 살펴보면 첫 번째 요소는 z^(1)의 정의와 일치하고, 두 번째 요소는 z^(2)의 정의와 일치한다는 걸 알 수 있습니다.
X가 훈련 샘플을 가로로 쌓은 결과인 것처럼 대문자 Z를 소문자 z들을 가로로 쌓은 것이라고 정의하겠습니다. 훈련 샘플인 소문자 x를 가로로 쌓았을 때 대문자 X 변수를 얻은 것처럼, 소문자 z 변수를 가로로 쌓으면 대문자 Z를 얻습니다. 이 값을 계산하는 NumPy 명령어는 Z = np.dot(w.T, X) + b 입니다. 파이썬에서 미묘한 점은 b는 하나의 수입니다. (1, 1) 행렬이라고도 할 수 있는 실수이죠. 이 벡터와 이 실수를 더한다면 파이썬은 실수 b를 자동으로 (1, m) 행 벡터로 바꿔줍니다. 이 이상한 연산은 파이썬에서 브로드캐스팅이라고 불립니다. 지금은 걱정하시지 않아도 되고 다음 영상에서 더 자세히 다루겠습니다.
여기서 얻어갈 점은 이 한 줄의 코드로 Z를 계산할 수 있다는 것입니다. 대문자 Z는 소문자 z^(1)부터 z^(m)까지를 포함하는 (1, m) 행렬이 됩니다. Z는 저렇게 계산했지만 a는 어떻게 할까요? 다음에 찾고 싶은 건 a^(1)부터 a^(m)을 동시에 계산하는 방법입니다. 소문자 x와 z를 가로로 쌓아서 대문자 X와 Z를 얻은 것처럼, 소문자 a를 가로로 쌓아 대문자 A라는 새로운 변수를 정의하겠습니다.
프로그래밍 과제에서 벡터 값을 가지는 시그모이드 함수를 구현하여, 함수가 이 대문자 Z를 입력으로 받고 효율적으로 대문자 A를 반환하는 걸 볼 것입니다. 자세한 내용은 프로그래밍 과제에서 보게 될 것입니다.
복습하자면 이 슬라이드에서 볼 수 있듯이, 소문자 z와 a를 하나씩 계산하기 위해 m개의 훈련 샘플을 순환하는 대신, 이 한 줄의 코드로 모든 z를 동시에 계산하고, 적절한 σ의 구현으로 한 줄의 코드로 모든 a를 동시에 계산할 수 있습니다. 이렇게 모든 m개의 훈련 샘플을 동시에 정방향 전파하는 벡터화된 구현을 할 수 있습니다.
요약하자면 벡터화를 통해 소문자 a로 표현되는 활성값을 동시에 효율적으로 계산하는 방법을 보았습니다. 벡터화를 통해 역방향 전파의 도함수도 효율적으로 구할 수 있습니다. 다음 영상에서 더 살펴보죠.
04 로지스틱 회귀의 경사 계산을 벡터화 하기
지난 영상에선 벡터화를 사용해 예측값인 a를 모든 훈련 세트에 대해 동시에 계산하는 방법을 보았습니다. 이 영상에서는 벡터화를 통해 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^(1)부터 a^(m)까지인 A를 계산하는 법을 알고 있습니다. Y는 y^(1)부터 y^(m)이라고 정의했습니다. 마찬가지로 가로로 쌓여있죠. 이 정의를 따른다면, dZ가 A - Y로 계산될 수 있다는 걸 알아차릴 수 있을 겁니다. 첫 요소는 a^(1) - y^(1)이고, 두 번째 요소는 a^(2) - y^(2)이고 이렇게 계속되기 때문이죠. 첫 요소인 a^(1) - y^(1)은 dz^(1)의 정의와 정확히 일치하고, 나머지 요소도 정확히 일치합니다. 따라서 한 줄의 코드로 이 모든 것을 동시에 계산할 수 있습니다.
지난 구현에서 이미 하나의 for문을 제거했지만, 훈련 샘플을 계산하는 두 번째 for 문은 여전히 남아있었습니다. dw를 영벡터로 초기화해준 뒤 여전히 훈련 샘플을 순환해줘야 했습니다. 첫 샘플에 대해 dw += x^(1) dz^(1)을 해주고, 두 번째 샘플과 나머지 샘플에 대해 m번 반복해야 했습니다. 마지막엔 m으로 나눠줬죠. db도 비슷하게 0으로 초기화 해주고, db += dz^(1), db += dz^(2), 이 작업을 dz^(m)까지 반복한 뒤 db를 m으로 나눠줬습니다. 지난 구현에서는 이렇게 했습니다. 이미 하나의 for 문을 제거해서 dw는 벡터이고, dw^(1), dw^(2) 등을 따로 갱신하지 않았습니다. 이미 제거했죠.
하지만 m개의 훈련 샘플에 대한 for 문이 남아있습니다. 이 연산을 벡터화해보죠. db에 대한 벡터화 구현을 살펴보면, 모든 dz를 더하고 m으로 나눠주는 것입니다. db는 i가 1부터 m까지일 때 dz(i)의 합을 m으로 나눈 값입니다. 여기서 모든 dz는 열 벡터입니다. 파이썬에서는 1/m에 np.sum(dZ)라고 쓰죠. 단순히 dZ를 가지고 이 함수를 호출하면 db를 반환합니다. dw는 어떨까요? 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를 계산하고, 이 줄로 dw를 계산합니다. for 문 없이 변수의 갱신값을 계산할 수 있게 됩니다.
모든 것을 모아 로지스틱 회귀를 어떻게 구현하는지 살펴봅시다. 이건 기존의 벡터화되지 않은 비효율적인 구현입니다. 지난 영상에서 처음 한 것은 이 for문을 없앤 것입니다. dw1, dw2를 순환하는 대신 벡터값으로 바꿔서, 모든게 벡터값인 dw += x^(i) dz^(i)로 대체했습니다. 이제는 아래 for 문 뿐만이 아니라 위의 for 문도 제거할 수 있다는 걸 보겠습니다. 한번 해보죠. 전 슬라이드의 내용을 가져오면, Z = w^T X + b라고 할 수 있습니다. 이에 상응하는 코드는 Z = np.dot(w^T, X) + b가 됩니다. A는 σ(Z)가 되겠죠. 모든 i에 대해 이 값들을 계산한 겁니다. 지난 슬라이드에서 본 것처럼, dZ는 A - Y이고 모든 i에 대해 이 값을 계산했습니다. 마지막으로, dw는 (1/m) X dZ^T이고, db는 (1/m) np.sum(dZ)가 됩니다. 정방향 전파와 역방향 전파를 했고, m개의 모든 훈련 샘플에 대해 예측값과 도함수도 계산했습니다. for 문은 쓰지 않았죠. 경사 하강법 갱신은 w = w - (학습률α) dw가 됩니다. dw는 위에서 계산했습니다. b는 b - (학습률α) db로 갱신됩니다. 콜론을 써서 할당이라는 걸 명시해줍니다. 이 표기법에 대해 일관적이진 못했군요.
이렇게 로지스틱 회귀 경사 하강법의 한 반복을 구현했습니다. 가능한 한 for 문을 제거해야 한다고 말했지만, 경사 하강법을 여러 번 반복하고 싶다면 반복 횟수에 따라 for 문이 필요합니다. 만약 경사 하강법을 천 번 반복하고 싶다면 반복 횟수만큼 for 문이 필요하겠죠. 가장 바깥쪽의 for 문이 되고 저걸 없앨 방법은 없다고 생각합니다. 경사 하강법의 한 반복을 for 문 없이 구현하는 것은 여전히 멋진 일이죠.
여기까지 로지스틱 회귀에 대한 꽤 많이 벡터화되어 있고 효율적인 구현을 살펴봤습니다. 다음 영상에서 다루고 싶은 한 가지 세부사항이 있는데, 이번 설명에서 브로드캐스팅이라는 기법을 암시했습니다. 브로드캐스팅은 파이썬과 NumPy에서 코드를 더욱 효율적으로 만들어주는 기법입니다. 다음 영상에서 브로드캐스팅에 대해 더 자세히 알아봅시다.
05 파이썬의 브로드캐스팅
지난 영상에서 브로드캐스팅은 파이썬 코드 실행 시간을 줄일 수 있는 또 다른 기법이라고 말씀드렸습니다. 이 영상에서 파이썬 브로드캐스팅이 어떻게 작동하는지 알아봅시다.
예를 들어 한번 보죠. 이 행렬은 네 가지 다른 음식 100g당 탄수화물, 단백질, 지방이 가지는 칼로리를 보여줍니다. 예를 들면 사과 100g에 들어있는 탄수화물은 56 칼로리를 주고, 단백질과 지방은 훨씬 적습니다. 소고기 100g에 들어있는 단백질은 104 칼로리를 주고, 지방은 135 칼로리를 줍니다. 우리의 목적은 각 네 가지 음식의 탄수화물, 단백질, 지방이 주는 칼로리의 백분율을 구하는 것이라고 합시다. 예를 들어 이 열의 모든 수를 다 더하면, 100g의 사과는 59 칼로리를 가지고 있다는 걸 알 수 있습니다. 백분율을 계산한다면, 사과에서 탄수화물이 주는 칼로리의 백분율은 56/59, 약 94.9%가 됩니다. 사과 칼로리의 대부분은 탄수화물에서 오네요. 소고기를 보면 상반되게 칼로리의 대부분은 단백질과 지방에서 옵니다.
여기서 하고 싶은 계산은 행렬의 네 열 안의 수의 합을 구하고, 100g의 사과, 소고기, 달걀, 감자 안의 총 칼로리가 되겠네요. 그 후엔 행렬 전체를 나눠서 네 가지 음식 안의 탄수화물, 단백질, 지방이 주는 칼로리의 백분율을 구합니다. 이걸 for 문을 쓰지 않고 할 수 있을까요? 어떻게 하는지 살펴봅시다. 제가 보여드릴 것은 A라고 부를 이 (3, 4) 행렬을 가지고 한 줄의 파이썬 코드만으로 각 열의 합을 구할 겁니다. 네 가지 다른 음식에 들어있는 총 칼로리인 네 숫자를 얻겠네요. 각 음식 100g이죠. 파이썬 두 번째 줄에서는 각 네 열을 각 열의 합으로 나눌 겁니다.
설명이 명쾌하지 않았다면, 파이썬 코드를 보며 이해가 되길 바랍니다. Jupyter Notebook인데요, 방금 보신 행렬 A를 만드는 코드를 미리 작성했습니다. shift + enter를 눌러 실행하면 행렬 A가 출력되죠. 파이썬 코드 두 줄을 작성해 보겠습니다. 첫 줄은 cal = A.sum(axis=0)입니다. 열을 더하겠다는 뜻이죠. 잠시 뒤에 더 설명하겠습니다. cal을 출력해보죠. 열을 더하면 방금 말했듯이 사과의 총 칼로리는 59이고, 소고기에는 239칼로리가 있고, 달걀과 감자의 칼로리도 나와있습니다. 백분율을 계산하려면, percentage = 100 * A/cal.reshape(1,4)라고 써주시면 됩니다. 백분율이니 100을 곱해줘야 합니다. percentage를 출력하고 실행해봅시다. 방금 명령어에선 행렬 A를 (1, 4) 행렬로 나눴습니다. 백분율 행렬을 주죠. 전에 손으로 계산한 것처럼 사과는 94.9%의 칼로리가 탄수화물에서 옵니다.
슬라이드로 돌아가죠. Jupyter Notebook에 작성했던 코드 두 줄을 다시 한번 봅시다. 조금 더 설명을 덧붙이면, axis = 0라는 매개 변수는 파이썬에게 세로로 더하라고 알려줍니다. 가로축은 axis 1입니다. 가로로 더하게 되겠죠. 이 명령어는 파이썬 브로드캐스팅의 한 예입니다. (3, 4) 행렬인 A를 (1, 4) 행렬로 나누는 거죠. 사실 코드 첫 줄이 실행된 이후에 변수 cal은 이미 (1, 4) 행렬입니다. reshape 함수를 사용할 필요는 없죠. 저는 파이썬 코드를 쓸 때 행렬의 차원이 확실하지 않다면, reshape 함수를 사용하여 필요한 차원의 행렬로 확실히 만들어줍니다. reshape 함수는 상수 시간이 걸리므로 호출이 굉장히 저렴합니다. reshape 함수를 써서 행렬의 차원을 확실하게 하는 걸 주저하지 마세요. 이 연산이 어떻게 작동하는지 더 자세히 설명하겠습니다. 여기서 (3, 4) 행렬을 (1, 4) 행렬로 나눴습니다. 어떻게 (3, 4) 행렬을 (1, 4) 행렬 혹은 벡터로 나눌 수 있을까요?
브로드캐스팅의 몇 가지 예를 더 봅시다. (4, 1) 벡터에 상수를 더한다면, 파이썬은 이 수를 자동으로 (4, 1) 벡터로 만들어줍니다. 저 두 벡터를 더하면 오른쪽에 있는 벡터가 됩니다. 모든 요소에 100을 더한 거죠. 전 영상에서 이미 상수 b에 브로드캐스팅을 한번 했었습니다. 이 브로드캐스팅은 행 벡터와 열 벡터 모두에게 작동합니다. 로지스틱 회귀에서 벡터에 b를 더할 때 비슷한 형태를 봤습니다.
다른 예시를 보죠. (2, 3) 행렬이 있다고 가정합시다. 여기에 (1, n) 행렬을 더하는 겁니다. 일반적으론 (m, n) 행렬이 있고, 그 행렬에 (1, n) 행렬을 더하는 거죠. 파이썬은 이 행렬을 m번 복사해서 (m, n) 행렬로 만들어줍니다. 여기서는 (1, 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) 행렬 혹은 열 벡터가 있을 때 실수와 덧셈, 뺄셈, 곱셈 혹은 나눗셈을 한다면 (1, 1) 행렬이라고도 할 수 있죠. 그럴 땐 이 실수를 m번 복사하여 (m, 1) 행렬을 만들어주고, 이 예처럼 요소별 덧셈을 해줍니다. 행 벡터에서도 비슷하게 작동합니다.
더 일반적인 브로드캐스팅은 더 많은 걸 할 수 있습니다. 관심 있으시다면 NumPy 브로드캐스팅 문서를 읽어보시면, 브로드캐스팅의 좀 더 일반적인 정의가 있습니다. 이 슬라이드는 신경망을 구현하기 위해 가장 중요한 브로드캐스팅을 적었습니다.
끝내기 전에 간단히 말씀드리면, MATLAB이나 Octave에 익숙하신 분들 중에 신경망 프로그래밍의 bsxfun 함수를 사용해 보셨다면, bsxfun은 비슷하지만 다른 일을 합니다. 파이썬 브로드캐스팅과 비슷한 목적으로 쓰이긴 합니다. MATLAB과 Octave 숙련자에게 하는 말이니 이해하지 못하셔도 괜찮습니다. 파이썬에서 신경망을 구현할 때는 알 필요 없습니다.
파이썬 브로드캐스팅을 다뤄봤습니다. 프로그래밍 숙제를 할 때 브로드캐스팅이 코드를 빠르게 해주고, 코드를 짧게 해줬으면 좋겠네요. 프로그래밍 예제를 하기 전에 한가지 공유하고 싶은 게 있습니다. 파이썬 코드에서 오류를 줄일 수 있는 조언인데요. 도움이 될 수 있으면 좋겠고 다음 영상에서 다루겠습니다.
06 파이썬과 넘파이 벡터
파이썬의 브로드캐스팅 연산, 더 일반적으로 말해서 NumPy를 사용한 파이썬 프로그래밍의 유연성은 프로그래밍 언어의 장점과 단점 모두 될 수 있다고 생각합니다. 장점으로 언어의 넓은 표현성과 유연성은 한 줄의 코드로도 많은 걸 할 수 있게 해줍니다. 하지만 단점으로는 브로드캐스팅의 유연성은 브로드캐스팅의 자세한 내용과 작동 방법을 모른다면 가끔 이상하고 찾기 힘든 오류가 생깁니다.
예를 들어 행 벡터와 열 벡터를 더한다면 차원 오류나 형 오류가 생길 거라고 생각하겠지만, 두 벡터의 합인 행렬이 나올 수도 있습니다. 이 이상한 결과는 파이썬 내부 논리로 처리되지만, 파이썬에 익숙하지 않다면 이상하고 찾기 어려운 오류들이 생깁니다. 이 영상에서 공유하고 싶은 건 제 코드를 간단하게 하거나 이상한 오류들을 없애 줬던 조언들입니다. 이 조언들이 오류 없는 파이썬 NumPy 코드를 쉽게 쓸 수 있도록 도와주면 좋겠네요.
파이썬 NumPy에서 특히 벡터를 만드는 과정에서 많이 생기는 직관적이지 않은 결과를 보기 위해 간단히 예를 보여드리겠습니다. a를 np.random.randn(5)로 지정하면 가우시안 분포를 따르는 변숫값 5개를 배열 a에 저장합니다. a를 출력해 봅시다. 이때 a의 크기는 (5,)입니다. 파이썬에서는 랭크가 1인 배열이라고 부릅니다. 행 벡터도 아니고 열 벡터도 아니죠. 따라서 직관적이지 않은 결과를 도출합니다. 예를 들어 a의 전치를 출력해도 a와 똑같이 생겼고, a와 a의 전치의 내적을 구한다면 외적이 되고 행렬이 나와야 한다고 생각할 수 있지만 하나의 수만 나옵니다.
제가 권장하는 바는 신경망을 구현할 때 크기가 (5,) 혹은 (n,)인 랭크가 1인 배열을 아예 사용하지 않는 것입니다. 대신 a를 np.random.randn(5,1)로 설정한다면, a는 5×1 열 벡터가 됩니다. 전에는 a와 a의 전치가 똑같이 생겼다면, 지금 a의 전치를 출력해보면 행 벡터가 됩니다. 이 자료구조에서 조금 다른 점은 a의 전치에는 대괄호가 두 개 있다는 것입니다. 전에는 하나만 있었죠. 그게 다른 점입니다. 이건 사실 1×5 행렬인 거죠. 이전 것은 랭크가 1인 배열이었습니다. a와 a의 전치의 곱을 출력한다면 벡터의 외적이 나오게 됩니다. 벡터의 외적은 행렬이 됩니다.
방금 본 것을 조금 더 자세히 살펴봅시다. 처음 실행한 명령어는 이겁니다. a.shape이 (5,)가 되는 이상한 자료구조를 만들었죠. 이건 랭크1 배열이라고 부릅니다. 행 벡터나 열 벡터 둘 다 아닌 이상한 자료구조입니다. 결과가 직관적이지 않게 하죠. 제가 권장하는 바는 프로그래밍 예제나 신경망에서 로지스틱 회귀를 구현할 때 랭크1 배열을 아예 사용하지 않는 것입니다. 대신 항상 배열을 만들 때 이처럼 (5, 1) 열 벡터를 만들거나, 행 벡터를 만들어준다면 벡터의 동작을 더 쉽게 이해할 수 있을 것입니다. 이 경우에는 a.shape은 (5, 1)이 되고 열 벡터가 되고 열 벡터처럼 동작합니다. 따라서 이걸 (5, 1) 행렬이나 열 벡터라고 생각할 수 있습니다. 여기선 a.shape은 (1,5)가 되고 한결같이 행 벡터처럼 반응하죠. 벡터가 필요할 땐 이 둘 중 하나를 쓰고, 랭크1 배열을 쓰지 마세요.
저는 코드에서 벡터의 차원을 확실히 알지 못할 때, 이처럼 a가 (5, 1) 열 벡터라는 걸 확실히 하기 위해 assert 함수를 써줍니다. assert 함수는 굉장히 저렴하고 문서의 역할도 할 수 있으니, 필요할 때 언제나 넣어주시면 좋습니다. 만약 랭크1 배열을 얻게 된다면 reshape 함수를 써서 (5, 1) 배열이나 (1, 5) 배열로 바꾸어, 일관되게 열 벡터나 행 벡터로 동작하게 할 수 있습니다.
랭크1 배열 때문에 학생들이 찾기 어려운 오류를 만드는 걸 자주 봤는데요, 저는 랭크1 배열을 없애서 코드를 더 간단하게 만들었고, 랭크1 배열이 없어도 코드 작성에 불편함이 없었습니다. 여기서 배울 점은 랭크1 배열을 사용하지 않고 코드를 간단히 하는 것이고, 열 벡터인 (n, 1) 행렬 혹은 행 벡터인 (1, n) 행렬을 사용하는 것입니다. assert 함수를 많이 써서 행렬과 배열의 차원을 확인하세요. 행렬과 벡터를 필요한 차원으로 만들기 위해 reshape 함수를 자주 사용하세요.
이 제안들이 여러분의 파이썬 코드에서 오류를 없애는 데 도움이 되고, 프로그래밍 예제를 쉽게 마칠 수 있게 해줬으면 좋겠군요.
08 로지스틱 회귀의 비용함수 설명
지난 영상에서 로지스틱 회귀의 비용 함수를 적어봤습니다. 이번 영상에서는 로지스틱 회귀에 왜 그 비용 함수를 쓰는지에 대해 설명하겠습니다. 로지스틱 회귀에서 y의 예측값은 σ(w^T x + b)였습니다. σ는 익숙한 함수죠. y의 예측값은 x가 주어졌을 때 y가 1일 확률이라고 했습니다. 따라서 알고리즘은 x라는 특성이 주어졌을 때 y가 1일 확률인 ŷ을 반환해야 합니다. 다른 말로 하자면 y가 1이라면 x가 주어졌을 때 y가 1일 확률은 ŷ이고, y가 0이라면 x가 주어졌을 때 y가 0일 확률은 1 - ŷ입니다. ŷ는 y가 1일 확률인 거고, 1 - ŷ는 y가 0일 확률입니다.
마지막 두 등식을 다음 슬라이드로 옮겨가죠. 여기서 할 일은 y가 0이거나 1일 때 p(y|x)를 정의하는 두 등식을 가지고 한 개의 등식으로 합치려고 합니다. y가 0이나 1이여만 하는 이유는 이진 분류이기 때문에 y가 0이거나 1일 수밖에 없습니다. 이 두 등식을 하나로 합쳐보겠습니다. 먼저 식을 쓰고 나중에 왜 그런지 설명하죠. 이 등식이 위에 있는 두 등식을 하나로 합친 겁니다. 설명해드리겠습니다.
먼저 y가 1이라고 가정을 해봅시다. 그럼 이 항은 ŷ이 됩니다. ŷ의 1승이기 때문이죠. 이 항은 1 - y가 0이기 때문에 (1 - ŷ)^0이 됩니다. 어떤 수의 0승은 1이기 때문에 이 항은 사라집니다. 따라서 y가 1일 때 이 식은 p(y|x) = ŷ가 됩니다. 정확히 저희가 원했던 거죠.
y가 0일 때는 어떻게 될까요? y가 0이라면 위의 식은 p(y|x) = ŷ^0으로 시작하죠. 하지만 어떤 수의 0승은 1이기 때문에 1과 같습니다. 거기에 (1 - ŷ)^(1 - y)를 곱해줍니다. 1 - y는 1 - 0이기 때문에 1이 됩니다. 따라서 이 식은 1×(1 - ŷ), 즉 1 - ŷ이 됩니다. y가 0일 때 p(y|x)는 1 - ŷ가 됩니다. 위에서 원했던 것과 정확히 일치합니다. 방금 보인 것은 이 식이 p(y|x)의 정확한 정의라는 것입니다.
마지막으로 로그 함수는 강한 단조 증가 함수이기 때문에 log p(y|x)를 최대화하는 것은 p(y|x)를 최대화하는 것과 같은 결과를 줍니다. log p(y|x)를 계산해본다면 log ŷ^y (1 - ŷ)^(1 - y)가 되고, y log ŷ + (1 - y) log(1 - ŷ)로 간소화됩니다. 전에 정의한 손실 함수의 음수가 됩니다. 음수가 된 이유는 보통 학습 알고리즘을 훈련시킬 땐 확률을 높이려고 하지만, 로지스틱 회귀에서는 손실 함수를 최소화하고 싶기 때문입니다. 손실 함수를 최소화시키는 건 확률의 로그값을 최대화시키는 것과 같죠.
하나의 샘플의 손실 함수는 이렇게 생겼습니다. 전체 훈련 세트의 m개 샘플에 대한 비용 함수는 어떻게 될까요? 한번 살펴봅시다. 조금 약식으로 적겠지만 훈련 세트 안의 모든 레이블에 대한 확률은, 훈련 샘플들이 독립동일분포라고 가정했을 때 전체 샘플에 대한 확률은 각 확률의 곱입니다. i가 1부터 m까지일 때 p(y^(i) | x^(i))의 곱이죠. 최대 우도 추정을 한다면 훈련 세트의 타깃 확률을 최대화해주는 변수를 찾아야 합니다.
이 값을 최대화하는 것은 로그값을 최대화하는 것과 같으므로 양변에 로그를 씌우겠습니다. 곱에 로그를 씌우면 로그의 합이 되므로 훈련 세트 레이블의 로그 확률 값은 i가 1부터 m까지일 때 log p(y^(i) | x^(i))의 합입니다. 전 슬라이드에서 이 값이 -L(ŷ^(i), y^(i))라는 걸 봤죠.
통계학에선 최대 우도 추정의 원칙이란 게 있는데, 이 값을 최대화하는 매개 변수를 찾으라는 뜻입니다. 다른 말로는 -L(ŷ^(i), y^(i))의 합을 최대화하는 값이죠. 음수 부호를 합 밖으로 빼냈습니다. 이 값은 L(ŷ^(i), y^(i))인 J(w,b)가 로지스틱 회귀의 비용이라는 걸 보여줍니다.
이젠 비용을 최소화하고 가능도를 최대화하려고 하기 때문에 음수 부호를 없앴습니다. 스케일을 맞추기 위해 편의상 1/m이라는 비례 계수를 추가했습니다. 요약하자면 비용 함수인 J(w, b)를 최소화하므로 로지스틱 회귀 모델의 최대 우도 추정을 한 것입니다. 훈련 샘플이 독립동일분포라고 가정했을 때 말이죠.
필수 영상이 아님에도 봐주셔서 감사합니다. 이 영상을 통해 왜 로지스틱 회귀에 이런 비용 함수를 쓰는지 이해가 됐으면 좋겠습니다. 이번 주 연습 문제와 퀴즈 문제를 풀어봤으면 좋겠네요. 퀴즈와 프로그래밍 예제에 행운을 빕니다.
'IT Expertise > Deep Learning' 카테고리의 다른 글
[Demucs] HTDemucs 논문 분석 (0) | 2025.04.25 |
---|---|
[Demucs] 1 Demucs와 간단한 실습 (1) | 2025.03.26 |
[Coursera] 2 신경망과 로지스틱 회귀 | Andrew Ng (한국어 자막 스크립트) (2) | 2025.03.22 |
[Coursera] 1 딥러닝 소개 | Andrew Ng (한국어 자막 스크립트) (2) | 2025.03.21 |
댓글