목차
👀 코드 보기 , 🤷♀️
이 두개의 아이콘을 누르시면 코드, 개념 부가 설명을 보실 수 있습니다:)
CS231N: Lecture 3 강의를 빠짐 없이 정리하였고, 어려운 개념에 대한 보충 설명까지 알기 쉽게 추가해 두었습니다.
Intro
지난시간에 이어서,
우리가 실제로 우리가 원하는것은 analytic gradient를 유도하고 사용하는 것이다
하지만 동시에 analytic gradient를 사용한 응용에 대해서 수학적 검증도 필요하다
➡ 임의의 복잡한 함수를 통해 어떻게 analytic gradient를 계산하는지에 대해 이야기할 것이다.
➡ computational graph(계산 그래프)라고 부르는 프레임워크를 사용할 것이다
computational graph
어떤 함수든지 computational graph(계산그래프)로 표현가능
노드: 연산 단계를 나타냄
[EX]
- 식: 예제는 우리가 지난시간에 배운 선형 classifier
- \(x\),\(W\): input
- 곱셈 노드: 행렬 곱셈
- 파라미터 W와 데이터 x의 곱셈은 score vector를 출력
- hinge loss 노드: 데이터 항 \(Lᵢ\)를 계산
- regularization 항을 계산
- 그 후, 우리의 최종 loss는 L은 regularization 항과 데이터 항의 합
[효과]
- computational graph를 사용해서 함수를 표현하게 됨으로써, backpropagation가 사용가능해짐
- backpropagation은 gradient를 얻기위해 computational graph 내부의 모든 변수에 대해 chain rule을 재귀적으로 사용함
- 이것은 우리가 매우 복잡한 함수를 이용하여 작업할때 아주 유용하게 사용됨
[EX: AlexNet(CNN)]
이 모델의 구성은
- 가장 윗층(top): 입력 이미지가 들어감
- 아래(bottom): loss가 있음
➡ 입력 이미지는 loss function으로 가기까지 많은 layer를 거쳐 변형을 겪게 됨
[EX: neural turing machine(딥러닝 모델)]
- 미치게 복잡해보이지만 어짜피 계산 그래프기 때문에 결국 시간이 지남에 따라 이것을 풀게 될 것이다.
Backpropagation
그럼 이제 계산그래프를 이용하여 역전파(Backpropagation)를 구해볼 것이다.
[기본 계산 원리]
그 전에 역전파의 원리에 대해서 간단하게 살펴보자.
책에서는 그냥 넘어가 있어서 꼭 한번씩 읽어보고 포스트를 이어 읽는것이 이해에 많은 도움이 된다.
계산
1️⃣ computational grap로 나타내기
- 첫 번째 단계는 항상 함수 f를 이용해서 computational graph로 나타내는 것
- \(x,y,z\)로 구성된 function \(f\)는 \((x+y)* z\)라는 함수인 것 파악 가능
- computational graph: 오른쪽 그림
- x,y에 대한 덧셈 노드 존재
- 다음 연산을 위한 곱셈 노드도 존재
- 이 네트워크에 우리가 가지고 있는 값을 전달할 것임
- \(x= -2\).
- \(y= 5\).
- \(z= -4\).
- 중간중간 값도 계산하여 computational graph에 값을 적어둠
- \(x+y = 3\).
- 마지막으로 최종 노드를 통과시켜 -12를 얻음
2️⃣ gradient 표시
- 중간 변수에 이름붙임
- \(q\): \(x+y\) 덧셈 노드
- \(f\): \(q* z\) 곱셈 노드
- 각각의 gradient를 표시
- 빨간색 네모
- \(x\)와 \(y\)에 각각에 대한 \(q\)의 gradient를 표시
- 단순 덧셈이기 때문에 1(원래 값 유지)
- 파란색 네모
- \(q\)와 \(z\)에 대한 \(f\)의 gradient
- 곱셈규칙에 의해 각각 z와 q(서로 값이 바뀜)
- 빨간색 네모
- 우리의 목표
- \(x,y,z\) 각각에 대한 f의 gradient 찾기
3️⃣ backpropagation
- backpropagation은 chain rule의 재귀적인 응용임
- chiain rule에 의해 뒤에서부터 gradient를 계산을 시작함
- 3-1) f 계산
- 우리는 출력의 \(f\)에 대한 gradient를 계산(가장 뒤)
- \(∂f/∂f\)=1
- \(f\) gradient= 1
- 3-2) z 계산
- \(q\): \(z\)에 대한 \(f\)의 미분(위의 파란색 네모)(곱셈 법칙에 의해)
- \(∂f/∂z\)= \(q\) = 3
- \(z\) gradient= 3
- 3-3) q 계산
- \(z\): \(q\)에 대한 \(f\)의 미분(위의 파란색 네모)(곱셈 법칙에 의해)
- \(∂f/∂q\)= \(z\) = -4
- \(q\) gradient= -4
- 3-4) y 계산
- y에 대한 f의 미분값을 알고 싶지만 여기에서 y는 f와 바로 연결되어 있지 않음
- 구하기 위해서 chain rule을 이용함
- y에 대한 f의 미분은 q에 대한 f의 미분과 y에 대한 q의 미분의 곱으로 나타낼 수 있음
- 즉, q* f의 연산에서 q의 영향을 구할때와 동등함
- 즉, y에 대한 q의 미분은 1이므로 q값 그대로 흘려보냄.
- 만약 우리가 y를 조금 변화시키면 q는 그것의 영향력 만큼 조금 변할것임.
- 이것은 내가 y를 조금 바꿨을때 그것이 q에 대해 1만큼의 영향력을 미치는 것을 의미함.
- \(∂f/∂y\)= \(z\) = -4
- \(y\) gradient= -4
- 3-5) x 계산
- 3-4)와 똑같음
- f에 대한 x의 gradient는 -4 * 1 이므로 -4임.
특징: local 계산
위의 backpropagation에서 우리는 computational graph안에 모든 노드를 포함하고 있었다.
하지만 각 노드는 오직 주변에 대해서만 관심이 있다.
➡ 우리가 가지고 있는건 각 노드와 각 노드의 local 입력일 뿐 한 노드가 전체를 보지 않는다.
입력은 그 노드와 연결되어있고 값은 노드로 흘러들어가 이 노드로부터 출력을 얻게 된다.
그림을 통해 위의 특징을 이해해 보자,
- 그래프 해석
- local 입력: x,y
- 출력: z
- 이 노드에서 우리는 local gradient를 구할 수 있음
- z에서 x에 대한 gradient를 구할 수 있으며 y에 대한 gradient도 마찬가지이다.
- 그것들은 쉽게 gradient를 구할 수 있고, 우리는 그것을 찾기 위해 복잡한 미적분을 할 필요가 없다.
- 간단한 연산이다.
- chain rule을 사용해서, z에 대한 L의 gradient와 그리고 y에 대한 z의 gradient를 서로 곱한다.
- chain rule에 의해 이 둘의 곱은 원하는 gradient를 준다.
- 그리고 나서 우리가 gradient를 구하면 이 노드와 연결된 직전 노드로 전달가능하다.
[local 계산]
각 노드는 우리가 backpropagation을 통해 계산한 local gradient를 가지고 있다.
이 값들은 상위 노드 방향으로 계속 전달되고,
우리는 이것을 받아 local gradient와 곱하기만 하면 된다.
➡ 우리는 노드와 연결된 노드 이외의 다른 어떤 값에 대하여 신경쓰지 않아도 된다. 이 특징을 local 계산이라고 한다.
복잡한 예제
이번엔 조금 더 복잡한 예제로 계산해 보겠다.
➡ backpropagation이 얼마나 유용한 방식인지를 알게 될 것이
1️⃣ computational grap로 나타내기
- 첫 번째 단계는 항상 함수 f를 이용해서 computational graph로 나타내는 것
- \(w,x\)에 대한 함수 f는 \(1 / e^{-(w_0x_0 + w_1x_1 + w_2)}\)인 것 파악 가능
- computational graph 분석
- \(w\)와 \(x\)에 대한 곱셈 노드 존재
- \(x_0\)과 \(x_0\), \(x_1\)과 \(x_1\) 그리고 \(w_2\)에 대한 덧셈 노드도 존재
- -1을 곱하고 exponential을 취하고, 1을 더함
- 마지막으로 모든 term을 역수로 뒤집음
- 우리에게 ws와 xs가 주어졌다고 했을때, 우리는 값을 이용해 모든 단계마다 계산을 통해서 앞으로(오른쪽 방향으로) 진행할 수 있음
2️⃣ backpropagation
- 다시한번 그래프의 뒤에서부터 시작
- 3-1) 최종 출력 f 계산
- \(1/x\) 이전의 input에 대한 gradient
- 우리는 출력의 \(f\)에 대한 gradient를 계산(가장 뒤)
- \(∂f/∂f\)=1
- \(f\) gradient= 1
- 3-2) \(1/x\)에 대한 local gradient 계산
- \(x\)에 대한 \(f\)의 미분은 \(-1/(x^2)\)$
- x = 1.37
- \(∂f/∂x\)= \(-1/(x^2)\)
- \(x\) gradient= \(1/(-1.37^{2}) * 1\) \(= -0.53\)
- 3-3) +1 에 대한 local gradient 계산
- upstream gradient는 -0.53
- (x+상수)에 대한 local gradient는 1(덧셈노드니까)
- \(∂f/∂x\)= 1
- \(x\) gradient= \(1 * -0.53\) = -0.53
- 3-4) exponential 에 대한 local gradient 계산
- upstream gradient는 -0.53
- local gradient는 chain rule에 의해 gradient가 \(-0.53* e^x\)임
- x = -1
- \(∂f/∂x\)= \(e^x\)
- \(x\) gradient= \(-0.53* e^x\) = -0.2
- 3-5) \(* -1\) 에 대한 local gradient 계산
- upstream gradient는 -0.2
- local gradient는 x에 대한 f의 미분이므로 \(1 * -1 = - 1\)이다.
- \(∂f/∂x\)= \(-1\)
- \(x\) gradient= = \(-1 * -0.2\) = 0.2
- 3-6) 덧셈노드에 대한 local gradient 계산
- upstream gradient는 0.2
- local gradient는 1이다(덧셈노드는 그래도 보내주면 된다)
- \(∂f/∂x\)= \(1\)
- \(x\) gradient= \(1 * 0.2\) = 0.2
- 3-7) \(w₀\)과 \(x₀\)에 대한 local gradient 계산
- upstream gradient는 0.2
- 곱셈 노드에서 input에 대한 gradient는 한 인풋에 대하여 다른 인풋의 값임
- \(∂f/∂w₀\)= \(x₀\), \(∂f/∂x₀\)= \(w₀\)
- \(w₀\) gradient= -1 x 0.2 = -0.2
- \(x₀\) gradient= 2 x 0.2 = 0.4
analytic gradient의 간편성
[왜 analytic gradient를 유도해 값을 계산하는 것보다 단순할까?]
- 전 챕터에서 나온 질문이다.
- 우리가 써야만 하는 local gradient에 대한 표현을 여기에서 볼 수 있다.
- 우린 곱셈으로 나타내기 위해 chain rule을 사용했고 뒤에서부터 시작하여 모든 변수에 대한 gradients를 구했다. * w0과 x0에 대한 gradient 또한 같은 방법을 사용해 구했다.
- 여기서 중요한건, 우리가 computational graph를 만들 때, computational 노드에 대해 우리가 원하는 세분화된 정의를 할 수 있다.
- 위의 예제들에서 덧셈과 곱셈, 절대적으로 작은 단위로 쪼갰다.
- 덧셈과 곱셈은 더 단순해질 수 없다.
- 필요하다면 이 노드들을 더 복잡한 그룹으로 묶는 것도 가능하다.
- 우리는 그 노드에 대한 local gradient를 적어두기만 하면 된다.
[sigmoid 함수: 노드화]
- 예를 들어 sigmoid 함수를 보자.
- 책에는 역전파 과정이 자시히 나와있지 않아 이 링크를 참고해서 역전파를 이해해보자
- 이는 \(1 / (1 + e^-x)\)와 같다.
➡ 그리고 우리는 이것에 대한 gradient를 계산가능
➡ 우리는 이 gradient를 사용 가능하며, 분석적으로 이를 살펴본다면 마지막에 좋은 표현(\(1 / (1 + e^-x)\))을 얻을 수 있음
➡ 따라서 이 예제에서 이것은 \(1- sigma(x)) * sigma(x)\)와 같음
- 효과
- 이 경우에 우리는 sigmoid를 만들기 위한 모든 계산을 그래프에 가지고 있음
- 그러므로 우리는 이것을 하나의 big 노드, sigmoid로 바꾸기 가능(gate의 local gradient, expression 그리고 x에 대한 sigmoid의 미분을알고 있기 때문)
- ✨ local gradient를 적을 수 있는한 더 복잡한 노드 그룹을 만들기 가능
- 이 모든것은 trade-off의 관계에 있음
➡ 간결하고 단순한 그래프를 얻기 위해 당신이 많은 수학을 알고 싶어하는 것과 각 노드에서 단순한 gradient를 얻고 싶어하는 것 사이에서 그 결과 당신은 원하는 만큼 복잡한 computational graph를 쓸 수 있음.
[sigmoid 함수: 그룹화]
- 아시다시피 우리는 위에서 노드들을 sigmoid로 그룹화하였다.
- 실제로 이것이 동등한지 확인해보겠다
- sigmoid를 한 그룹으로 연결하면,
- 입력: 1 (초록색으로 쓰여 있음)
- 출력: 0.73
- 이렇게 gradient를 얻기 원한다면 하나의 완전한 sigmoid 노드를 이용할 수 있다.
- 앞에서 유도한 local gradient를 sigmoid 그룹을 이용하여 풀기
- \((1-sigmoid(x)) * sigmoid(x)\)이 식을 sigmoid 그룹에 대입해보면,
- x는 0.73
- 이 x를 대입한 gradient 값은 0.2이다.
- 0.2를 upstream gradient인 1과 곱하면, 우리는 sigmoid gate로 바꾸기 이전의 작은 노드들로 계산된 값과 정확히 같은 값을 얻을 수 있습니다.
[computational graph의 평가]
- 편리함
- gradient를 구할때나 다른 무언가의 gradient를 찾아야만 할 때, sigmoid 처럼 설사사 그 식이 정말로 복잡해도, backpropagation과 chain rule을 통해 비교적 쉽게 유도가능함.
- 그리고 그 결과 gradient를 계산가능
- gradient를 찾기 어려울 때 언제든지 이 방식을 사용해서 알아낼 수 있음
- 모든 부분들을 쪼개고 chain rule을 적용할 수 있음
Patterns in backward flow
[add gate]
- gradient distributor
- 두 개의 브랜치와 연결되는 덧셈 게이트에서는 upstream gradient를 연결된 브랜치에 정확히 같은 값으로 나눠줌
[max 게이트]
- gradient router: 최대값을 취한 다음에 그것을 통과시킴
- 덧셈 노드에서는 같은 gradient를 들어오는 브랜치에게 전달해주기 때문에 max 게이트는 그것을 받고 하나의 브랜치를 지정
- 입력
- z = 2 ➡ 얘가 더 크니까 얘만 살림
- w = -1
- upstream gradient는 2
- max 노드의 local gradient
- z는 gradient 2
- w는 gradient 0
- 즉, 하나는 전체 값이, 다른 하나에는 0의 gradient가 향하게 됨
- forward pass를 통한 의미 분석
- 순전파에서 최대값만 통과한다.
- 그래서 결국 최대값만이 함수 계산에 실제로 영향을 주는 값은 유일한 값이므로 역전파에도 한 값에 대한 정보만 흘려보내는 것이다.
- scaling의 기능
- upstream gradient를 받아 다른 브랜치의 값으로 scaling 함
- upstream gradient를 받아 다른 브랜치의 값으로 scaling 함
[인풋값이 여러 노드 일 때]
- 다른 주목할만한 것은 우리가 여러 노드와 연결되어 있는 하나의 노드를 가지고 있을 때 gradient는 이 노드에서 합산됨.
- 다 변수(multivariate) chain rule을 사용하는 노드들에서 우리는 단지 각 노드들로부터 들어오는 upstream gradient 값을 취하고 그리고 이것들을 합함
- 만약 당신이 이 노드를 조금 변경시킨다면 그래프를 따라 forward pass를 할 때 연결된 노드들에게 영향을 미칠 것이다.
- 그리고 backpropagation을 할 때 이 두개의 gradient가 돌아오면 이 노드에 영향을 미치게 될 것이다.
- 따라서 이 값을 더해야 총 upstream gradient가 된다.
➕ 여기서 주목할 점 ➕
지금까지 우리는 실제 가중치에 대해 아무런 업데이트도 하지 않았음
➡ 변수에 대한 gradient만 찾았음
➡ 최적화 강의에서 배운 모든 것을 적용할 예정(다음 강의)
여기에서 우리가 한 것은 임의의 복잡한 함수에 대해 gradient를 계산하는 것을 배운 것임
➡ 나중에 신경망과 같은 복잡한 함수에 대해 이야기 할 때 유용할 수 있음
이제 우리는 스칼라 값에 대한 모든 예제를 완료했다.
벡터로 확장
위의 예제들은 스칼라 값에 관한 것들이였다.
이번엔 스칼라가 아닌 벡터일 때를 보겠다.
우리의 변수 x,y,z에 대해서 숫자 대신에 vector를 가지고 있다도 가정해보자.
아래 예제의 흐름은 전과 같다.
차이점은 기존의 gradient가 Jacobian 행렬이 된 것이다.
➡ 각 요소의 미분을 포함하는 행렬이 될 것이다.
백터화 예제 1
그럼, 예제로 적용을 해보자.
우리의 입력은 4096차원의 벡터(CNN에서 흔히 볼 수 있는 사이즈)이다.
그리고 출력 또한 4096 차원의 벡터이다.
➡ 이 노드는 요소별로(elementwise) 최대값을 취한다.
❔이 경우에 Jacobian 행렬의 사이즈는 얼마인가요?
📜 코드 설명 보기
답: 4096의 제곱
이전에 말했던 것을 기억해보자.
Jacobian 행렬의 각 행은 입력에 대한 출력의 편미분이 될 것이다.
그리고 4096*4096은 매우 크다.
그리고 실질적으로 이것은 더 커질 수 있다.
in practice we process an entire minibatch (e.g. 100) of examples at one time:
예로들면 100개의 인풋을 동시에 입력으로 같는 배치를 이용해 작업 가능하다.
그것은 우리의 노드를 더욱 효율적으로 만들지만 그것은 100배 커지게 만든다.
즉, 실제 Jacobian가 4096000 * 4096000이 되는 것이다.
이는 이것은 너무 거대해서 작업에 실용적이지 않다.
하지만 실제로는, 우리는 이 거대한 Jacobian을 계산할 필요가 없다.
그렇다면 Jacobian 행렬이 왜 만들어진 것일까?
다음 질문을 통해서 이 답을 더 구체화해보겠다.
❔그러면 이 Jacobian 행렬이 어떻게 생겨나게 되었을까요?
📜 코드 설명 보기
답: 오직 출력의 해당 요소에만 영향을 주어 한 행에 한 요소만 있게 생겨난다.
만약 우리가 요소별로 최대값을 같는 여기에서 어떤 일이 일어나는지 생각해 본다면,
우리는 각각의 편미분에 대해 생각해볼 수 있다.
- 먼저 이 질문부터 대답해보자.
- 입력의 어떤 차원이 출력의 어떤 차원에 영향을 줍니까?
- 즉, 이 Jacobian 행렬에서 어떤 종류의 구조를 볼 수 있습니까?
- ➡ 대각선이다.
- 이것은 요소별 이기 때문에, 입력의 각 요소, 첫번째 차원은 오직 출력의 해당 요소에만 영향을 준다.
그렇기 때문에 우리의 Jacobian 행렬은 대각행렬이 될 것이다.
(대각 행렬 예시)
실제로 이 전체 Jacobian 행렬을 작성하고 공식화 할 필요는 없다.
➡ 우리는 출력에 대한 x의 영향에 대해서 그리고 이 값을 사용하는 것에 대해서만 알면 되기 때문이다.
➡ 그리고 우리가 계산한 gradient의 값을 채워 넣으면 된다..
백터화 예제 2
이제 computational graph보다 구체적인 벡터화 된 예제를 보겠다.
-
x와 W에 대한 함수 f = x에 의해 곱해진 W의 L2($$ ^2$$)와 같음 - 이 경우 x는 n-차원
- W는 n* n
그렇다면 이제 computational graph를 확인해보겠다.
1️⃣ 순전파 구하기
우리는 x에 의해 곱해진 W를 가지고 있고 L2(\(|| ||^2\))가 뒤따른다.
먼저, 이에 대한 순전파부터 구해보자.
- 1-1) shape 파악
- W= 2x2 행렬
- x= 2차원의 벡터
- 1-2) 중간노드(\(W * x\)) 요소별 방식으로 재정의(q)
- \(W_1, 1 * x1 + W_1, 2 * x_2\)$$ 등등
- 그러면 우리는 이제 f를 q에 대한 표현으로 나타낼 수 있다.
- 1-3) L2 계산
-
f를 q에 대한 표현으로 나타내면, $$f(q) = q ^2 = (q_1)^2 + … + (q_n)^2$$이다. - 따라서 q의 두 번째 노드를 보면 이것은 q의 L2 norm과 같다.
- 이건 q1의 제곱과 q2의 제곱을 합친 것과 같다.
➡ q와 최종 출력을 구함.
-
2️⃣ backpropagation구하기
출력에 대한 gradient를 가지고 있다.
- 2-1) 첫 gradient
- 항상 1이다.
- 2-2) q에 대한 gradient
- L2 이전의 중간 변수인 q에 대한 gradient를 찾아보겠다.
- q는 2차원의 벡터
- 목표: q의 각각의 요소가 f의 최종 값에 어떤 영향을 미치는지 파악
- q1 = qi에 대한 f의 gradient = qi * 2 (도함수를 구하는 식에서 도출)
- 각각의 요소 qi 를 벡터로 표현했고(1️⃣에서), 이 벡터에 2를 곱한것과 같다.
- 우리는 이 벡터에서 gradient를 0.44와 0.52를 얻었다.
- ✨ 벡터의 gradient는 항상 원본 벡터의 사이즈와 같다.
- ✨ gradient의 각 요소 함수의 최종 출력에 얼마나 특별한 영향을 미치는지를 항상 의미한다.
- 2-3) W의 gradient
- 여기에서 우리는 다시 chain rule을 사용할 것이디.
- W에 대한 q의 local gradient를 계산하기 원한다면 요소별 연산을 다시 해야함.
- 최종 gradient의 유도 과정은 아래와 같다.
- 여기서 파란색 하이라이트는 Jacobian에 의해 유도된다.
- \(2q * x^{T}\)로 유도 되므로 아래와 같은 계산이 된다.
- 2-4) x의 gradient
- 여기에서 우리는 다시 chain rule을 사용할 것이디.
- x에 대한 q의 local gradient를 계산하기 원한다면 요소별 연산을 다시 해야함.
- 최종 gradient의 유도 과정은 아래와 같다.
- 여기서 파란색 하이라이트는 Jacobian에 의해 유도된다.
- \(2W^{T} * q\)로 유도 되므로 아래와 같은 계산이 된다.
forward / backward API
우리는 각 노드를 local하게 보았고 upstream gradient와 함께 chain rule을 이용해서 local gradient를 계산했다.
이것을 forward pass, 그리고 backward pass의 API로 생각할 수 있다.
- forward pass: 노드의 출력을 계산하는 함수를 구현
- backward pass: gradient를 계산
그래서 우리가 실제로 코드에서 이것을 구현한다면,
- forward, backward 함수를 구현할 때: backward 함수는 chain rule을 이용해 계산
- forward
- 그래프의 모든 노드를 반복함으로써 전체 그래프를 forward pass로 통과시킬 수 있음
- 여기에서는 게이트와 노드라는 단어를 사용해보자
- 모든 게이트를 반복하여 각 게이트를 호출한다.
- 우리는 이것을 위치적으로 정렬 된 순서로 수행하기를 원한다.
- 우리는 노드를 처리하기 전에 이 전에 노드로 들어오는 모든 입력을 처리한다.
- backward
- 역순서로 모든 게이트를 통과한 다음에 게이트 각각을 거꾸로 호출
- 역순서로 모든 게이트를 통과한 다음에 게이트 각각을 거꾸로 호출
👀 코드 보기
class ComputationalGraph(object):
#...
def forward(inputs):
# 1. [pass inputs to input gates...]
# 2. forward the computational graph:
for gate in self.graph.nodes_topologically_sorted():
gate.forward()
return loss # the final gate in the graph outputs the loss
def backward():
for gate in reversed(self.graph.nodes_topologically_sorted()):
gate.backward()
return inputs_gradients
[곱셈 게이트]
- forward pass
- x,y를 입력으로 받고 z를 리턴
- forward pass의 값을 저장(cache)해야 하는 것이 중요함
- 왜냐하면 cache는 이것(forward pass)이 끝나고 backward pass에서 더 많이 사용하기 때문이다.
- x와 y도 저장
- 이 값을 이용하여 backward pass에서 chain rule을 사용하여 upstream gradient 값을 이용해 다른 브랜치의 값과 곱함
- self.y 와 dz를 곱해 얻은 dx를 보관하게 될 것이다.
- dy에 대해서도 마찬가지
- backward
- 입력으로 upstream gradient인 dz를 받음
- 출력으로 입력인 x와 y에 gradient를 전달
- 즉 여기서 출력은 dx와 dy
- 그리고 이 예제의 경우 모든 것은 scalar(스칼라)(벡터가 아님)이다.
- 여기서는 스칼라니까 ‘전치’가 적용되기 않고 그냥 구현하면 된다.
- 벡터면 당연히 전치가 들어가게된다 ➡ 파이썬
numpy
에선.T
연산자로 간단하게 전치 가능하다.
👀 코드 보기
class MultiplyGate(object):
def forward(x,y):
z = x*y
self.x = x
self.y = y
return z
def backward(dz): # ∂L/∂z
# dx = self.y *dz # [dz/dx * dL/dz]
# dy = self.x *dz # [dz/dy * dL/dz]
return [dx, dy] # ∂L/∂x
관련 프레임워크
이건 그냥 이런게 있다 정도로만 넘어가면 된다.
이제 딥러닝 프레임워크 그리고 라이브러리를 보면
그들은 아래와 같은 종류의 모듈화를 따른다.
ex) Caffe: 유명한 딥러닝 프레임워크
- Caffe의 소스 코드를 볼 수 있음.
- layer라고 부르는 폴더를 구할 수 있음
- 레이어는 기본적으로 computational node
- 보통 레이어는 sigmoid 처럼 당신이 생각하는 것보다 좀 더 복잡함
[computational 노드의 전체 목록]
sigmoid, convolution, Argmax등 여러 레이어 존재.
➡ 이것들을 탐구하면 정확하게 forward pass, backward pass 등을 구현, 호출 가능.
Neural Networks
그럼 딥러닝이 어디에서 왔는지 간략하게 알아보자.
이제 마지막으로 신경망에 대해서 이야기 해보자.
사람들은 신경망과 뇌 사이에서 많은 유추와 여러 종류의 생물학적 영감을 이끌어낸다.
[Before: 선형 변환식]
지금까지 우리는 아시다시피 뇌 기능이 없는, 그저 함수 클래스로서 그것들을 보았었다.
- 즉, 우리가 지금까지 이야기한것은 우리는 다양한 선형 score 함수를 이용한 것이다.
- 우리는 이것을 최적화할 때의 실행 예제로 사용했었다.
[Now: 2층짜리 신경망의 레이어]
그러면 single 변환을 사용하는 대신에 아주 간단한 형태의 신경망을 사용해보자.
이것의 구성을 보자.
- W1과 x의 행렬 곱
- 그리고 우리는 이것의 중간값을 얻고, 우리가가진 max(0, W)의 비선형 함수를 이용해 선형 레이어 출력의 max를 얻음.
- 이 비선형 변환은 매우 중요하다 ➡ 나중에 더 설명하겠다고 나온다.
- 만약 당신이 선형 레이어들을 쌓는다면 그것들은 결국 선형 함수가 될 것이다.
- 그럼 이제 위의 식으로 층을 쌓아 2층짜리 신경망 레이어를 만들어보겠다.
- 1층: 비선형 레이어를 가지고 있습니다.
- 2층: 이것의 위에 선형 레이어를 추가할 것입니다.
- 여기에서 최종적으로 우리는 score 함수를 얻을 수 있습니다.
- 그래서 기본적으로 광범위하게 말하면 신경망은 함수들의 집합(class) 이다.
- 비선형의 복잡한 함수를 만들기 위해서 간단한 함수들을 계층적으로 여러개 쌓아올린 것이다
- 그리고 이 아이디어는 여러 단계의 계층적 계산으로 이루어져 있다.
- 행렬 곱, 중간에 비선형 함수와 함께 선형 레이어를 여러개 쌓아 계산하는 것이다.
- 다중 레이어의 기능
- 그리고 레이어들의 기능을 이해하기위해 선형 score 함수에 대해서 이야기 했던 것을 생각해보자.
- 가중치 행렬 W의 각 행이 각각의 템플릿과 같았는지를 논의했던 것을 기억해보자.
- 그것은 일종의 표현된 템플릿이었다.
- 구체적인 클래스에 대한 입력의 예
- 자동차 템플릿은 이런 종류의 빨간 차처럼 보인다.
- 입력에 대한 car 클래스 스코어를 계산한다.
- 그런데 여기서는 오직 하나의 빨간색 자동차 템플릿만 가지고 있다.
- 하지만 실제로 우리는 다른 여러 종류의 차를 찾기 원할 수도 있다.
- ➡ 다중 레이어 네트워크는 그것을 가능하게 한다.
- ➡ 중간 변수 h와 W1은 이러한 종류의 템플릿일 수 있다.
- ➡ 또한 h에서는 이 템플릿들에 대한 모든 점수를 가지고 있습니다. 그리고 우리는 이것을 결합하는 또 다른 레이어를 가질 수도 있는 것이다.
사용할 수 있는 여러 비선형들에 대해서 추후 강의에 이야기 할 것이다.
실제 뉴런과 차이점
우리는 sigmoid 및 여러 종류의 비선형과 관련해서 이야기 했습니다.
그리고 이것은 loose한 종류입니다.
우리는 비선형성으로 뉴런의 firing이나 spiking에 대해서 표현할 수 있다.
우리의 뉴런이 이산 spike 종류를 사용해서 신호를 전송한다.
- 그것들이 매우 빠르게 spiking 하는 경우 전달되는 강한 신호가 있다.
- 그래서 우리는 활성함수를 통과한 다음에 이 값을 생각할 수 있다.
➡ 실제로, 이것을 연구하고 있는 신경 과학자들은 실제 방식과 가장 유사한 비선형성의 종류가 ReLU라고 말한다.
(강의에서는 추후 활성화 함수에서 더 자세히 설명한다고 나와있다)
실제로 이것들은 뉴런들의 행동과 비슷하지만,
실제 생물학적 뉴런은 이보다 훨씬 복잡하기 때문에 이러한 종류의 신경을 만드는 것은 매우 신중해야 한다.
생물학적 뉴런에는 여러 종류가 있다.
수상돌기는 실제로 복잡한 비선형 계산을 수행할 수 있다.
우리의 시냅스, 이전에 비유적으로 그렸던 \(W_0\)은 우리가 가진 것처럼 단일 가중치가 아니며, 실제로 엄청 복잡한 비선형 시스템이다.
또한, 우리의 활성함수를 일종의 rate code나 fire rate로 해석하는 이러한 아이디어는 실제로는 실제 뉴런을 표현하기엔 불충분하다.
➡ 뉴런이 어떻게 하부의 뉴런과 통신하는지에 대해서아주 간단한 방법처럼 뉴런은 가변 fire rate로 발사할 것이고,이 가변성에 대해서 고려되어야 할 것이기 때문이다.
➡ 즉, 우리가 다루는 것보다 훨씬 복잡한 모든 것이 있다.
하지만 실제로 알다시피, 우리는 높은 수준에서 많은 뉴런을 닮은 것들을 볼 수 있다.
➡ 하지만 실제로 그것들은 더 복잡하다.