(11주차) 11월18일, 11월23일
체인룰, backpropagation
- 강의영상
- import
- 지난시간 복습
- 오늘수업의 목표
- 체인룰 (어려운 하나의 미분을 손쉬운 여러개의 미분으로 나누는 기법)
- 예시: 중간고사 문제..
- 잠깐 생각해보자..
- some comments
-
(1/4) 체인룰을 이용한 미분계수 계산, 이론 (1)
-
(2/4) 체인룰을 이용한 미분계수 계산, 이론 (2)
-
(3/4) 체인룰을 이용한 미분계수 계산, 실습
-
(4/4) 과제설명
-
(5/8) 체인룰을 이용한 미분계수 계산, 실습 (2)
-
(6/8) 체인룰을 이용한 미분계수 계산, 실습 (3) / back propagation (1)
-
(7/8) back propagation (2)
-
(8/8) some comments
import torch
-
회귀분석에서 손실함수에 대한 미분은 아래와 같은 과정으로 계산할 수 있다.
- $loss = ({\bf y}-{\bf X}{\bf W})^\top ({\bf y}-{\bf X}{\bf W})={\bf y}^\top {\bf y} - {\bf y}^\top {\bf X}{\bf W} - {\bf W}^\top {\bf X}^\top {\bf y} + {\bf W}^\top {\bf X}^\top {\bf X} {\bf W}$
- $\frac{\partial }{\partial {\bf W}}loss = -2{\bf X}^\top {\bf y} +2 {\bf X}^\top {\bf X} {\bf W}$
-
체인룰을 이해하자.
-
손실함수가 사실 아래와 같은 변환을 거쳐서 계산되었다고 볼 수 있다.
- ${\bf X} \to {\bf X}{\bf W} \to {\bf y} -{\bf X}{\bf W} \to ({\bf y}-{\bf X}{\bf W})^\top ({\bf y}-{\bf X}{\bf W})$
-
위의 과정을 수식으로 정리해보면 아래와 같다.
-
${\bf u}={\bf X}{\bf W}$, $\quad {\bf u}: n \times 1$
-
${\bf v} = {\bf y}- {\bf u},$ $\quad {\bf v}: n \times 1$
-
$loss={\bf v}^\top {\bf v},$ $\quad loss: 1 \times 1 $
-
손실함수에 대한 미분은 아래와 같다.
(그런데 이걸 어떻게 계산함?)
-
계산할 수 있는것들의 모음..
- $\frac{\partial}{\partial {\bf v}} loss = 2{\bf v} $ $\quad \to$ (n,1) 벡터
- $\frac{\partial }{\partial {\bf u}} {\bf v}^\top = -{\bf I}$ $\quad \to $ (n,n) 매트릭스
- $\frac{\partial }{\partial \bf W}{\bf u}^\top = {\bf X}^\top $ $\quad \to$ (p,n) 매트릭스
-
혹시.. 아래와 같이 쓸 수 있을까?
- 가능할것 같다. 뭐 기호야 정의하기 나름이니까!
-
그렇다면 혹시 아래와 같이 쓸 수 있을까?
- 이건 선을 넘는 것임.
- 그런데 어떠한 공식에 의해서 가능함. 그 공식 이름이 체인룰이다.
-
결국 정리하면 아래의 꼴이 되었다.
-
그렇다면?
그런데, ${\bf v}={\bf y}-{\bf u}={\bf y} -{\bf X}{\bf W}$ 이므로
정리하면
-
미분계수를 계산하는 문제였음..
-
체인룰을 이용하여 미분계수를 계산하여 보자.
ones= torch.ones(5)
x = torch.tensor([11.0,12.0,13.0,14.0,15.0])
X = torch.vstack([ones,x]).T
y = torch.tensor([17.7,18.5,21.2,23.6,24.2])
W = torch.tensor([3.0,3.0])
u = X@W
v = y-u
loss = v.T @ v
loss
-
$\frac{\partial}{\partial\bf W}loss $ 의 계산
X.T @ -torch.eye(5) @ (2*v)
-
참고로 중간고사 답은
X.T @ -torch.eye(5)@ (2*v) / 5
입니다.
-
확인
_W = torch.tensor([3.0,3.0],requires_grad=True)
_loss = (y-X@_W).T @ (y-X@_W)
_loss.backward()
_W.grad.data
-
$\frac{\partial}{\partial \bf v} loss= 2{\bf v}$ 임을 확인하라.
v
_v= torch.tensor([-18.3000, -20.5000, -20.8000, -21.4000, -23.8000],requires_grad=True)
_loss = _v.T @ _v
_loss.backward()
_v.grad.data, v
-
$\frac{\partial }{\partial {\bf u}}{\bf v}^\top$ 의 계산
_u = torch.tensor([36., 39., 42., 45., 48.],requires_grad=True)
_u
_v = y - _u ### 이전의 _v와 또다른 임시 _v
(_v.T).backward()
- 사실 토치에서는 스칼라아웃풋에 대해서만 미분을 계산할 수 있음
그런데 $\frac{\partial}{\partial {\bf u}}{\bf v}^\top=\frac{\partial}{\partial {\bf u}}(v_1,v_2,v_3,v_4,v_5)=\big(\frac{\partial}{\partial {\bf u}}v_1,\frac{\partial}{\partial {\bf u}}v_2,\frac{\partial}{\partial {\bf u}}v_3,\frac{\partial}{\partial {\bf u}}v_4,\frac{\partial}{\partial {\bf u}}v_5\big)$ 이므로
조금 귀찮은 과정을 거친다면 아래와 같은 알고리즘으로 계산할 수 있다.
(0) $\frac{\partial }{\partial {\bf u}} {\bf v}^\top$의 결과를 저장할 매트릭스를 만든다. 적당히 A
라고 만들자.
(1) _u
하나를 임시로 만든다. 그리고 $v_1$을 _u
로 미분하고 그 결과를 A
의 첫번째 칼럼에 기록한다.
(2) _u
를 또하나 임시로 만들고 $v_2$를 _u
로 미분한뒤 그 결과를 A
의 두번째 칼럼에 기록한다.
(3) (1)-(2)와 같은 작업을 $v_5$까지 반복한다.
(0)을 수행
A = torch.zeros((5,5))
A
(1)을 수행
u,v
_u = torch.tensor([36., 39., 42., 45., 48.],requires_grad=True)
v1 = (y-_u)[0]
- 이때 $v_1=g(f({\bf u}))$와 같이 표현할 수 있다. 여기에서 $f((u_1,\dots,u_5)^\top)=(y_1-u_1,\dots,y_5-u_5)^\top$, 그리고 $g((v_1,\dots,v_n)^\top)=v_1$ 라고 생각한다. 즉 $f$는 벡터 뺄셈을 수행하는 함수이고, $g$는 프로젝션 함수이다. 즉 $f:\mathbb{R}^5 \to \mathbb{R}^5$인 함수이고, $g:\mathbb{R}^5 \to \mathbb{R}$인 함수이다.
v1.backward()
_u.grad.data
A[:,0]= _u.grad.data
A
(2)를 수행
_u = torch.tensor([36., 39., 42., 45., 48.],requires_grad=True)
v2 = (y-_u)[1]
v2.backward()
_u.grad.data
A[:,1]= _u.grad.data
A
(3)을 수행 // 그냥 (1)~(2)도 새로 수행하자.
for i in range(5):
_u = torch.tensor([36., 39., 42., 45., 48.],requires_grad=True)
_v = (y-_u)[i]
_v.backward()
A[:,i]= _u.grad.data
A
- 이론적인 결과인 $-{\bf I}$와 일치한다.
-
$\frac{\partial }{\partial {\bf W}}{\bf u}^\top$의 계산
$\frac{\partial }{\partial {\bf W}}{\bf u}^\top = \frac{\partial }{\partial {\bf W}}(u_1,\dots,u_5)=\big(\frac{\partial }{\partial {\bf W}}u_1,\dots,\frac{\partial }{\partial {\bf W}}u_5 \big) $
B = torch.zeros((2,5))
B
W
_W = torch.tensor([3., 3.],requires_grad=True)
_W
for i in range(5):
_W = torch.tensor([3., 3.],requires_grad=True)
_u = (X@_W)[i]
_u.backward()
B[:,i]= _W.grad.data
B # X의 트랜스포즈
X
- 이론적인 결과와 일치
-
결국 위의 예제에 한정하여 임의의 ${\bf \hat{W}}$에 대한 $\frac{\partial}{\partial {\bf \hat W}}loss$는 아래와 같이 계산할 수 있다.
- (단계1) $2{\bf v}$를 계산하고
- (단계2) (단계1)의 결과 앞에 $-{\bf I}$를 곱하고
- (단계3) (단계2)의 결과 앞에 ${\bf X}^\top$를 곱한다.
-
step1에서 ${\bf v}$는 어떻게 알지?
- X $\to$ u=X@W $\to$ v = y-u
- 그런데 이것은 우리가 loss를 구하기 위해서 이미 계산해야 하는것 아니었나?
- step1: yhat, step2: loss, step3: derivate, step4: update
-
(중요) step2에서 loss만 구해서 저장할 생각 하지말고 중간과정을 다 저장해라. (그중에 v와 같이 필요한것이 있을테니까) 그리고 그걸 적당한 방법을 통하여 이용하여 보자.
-
아래와 같이 함수의 변환을 아키텍처로 이해하자. (함수의입력=레이어의입력, 함수의출력=레이어의출력)
- ${\bf X} \overset{l1}{\to} {\bf X}{\bf W} \overset{l2}{\to} {\bf y} -{\bf X}{\bf W} \overset{l3}{\to} ({\bf y}-{\bf X}{\bf W})^\top ({\bf y}-{\bf X}{\bf W})$
-
그런데 위의 계산과정을 아래와 같이 요약할 수도 있다. (${\bf X} \to {\bf \hat y} \to loss$가 아니라 ${\bf W} \to loss({\bf W})$로 생각해보세요)
- ${\bf W} \overset{l1}{\to} {\bf u} \overset{l2}{\to} {\bf v} \overset{l3}{\to} loss$
-
그렇다면 아래와 같은 사실을 관찰할 수 있다.
- (단계1) $2{\bf v}$는 function of ${\bf v}$이고, ${\bf v}$는 l3의 입력 (혹은 l2의 출력)
- (단계2) $-{\bf I}$는 function of ${\bf u}$이고, ${\bf u}$는 l2의 입력 (혹은 l1의 출력)
- (단계3) 마찬가지의 논리로 ${\bf X}^\top$는 function of ${\bf W}$로 해석할 수 있다.
-
요약: $2{\bf v},-{\bf I}, {\bf X}^\top$와 같은 핵심적인 값들이 사실 각 층의 입/출력 값들의 함수꼴로 표현가능하다. $\to$ 각 층의 입/출력 값들을 모두 기록하면 미분계산을 유리하게 할 수 있다.
- 문득의문: 각 층의 입출력값 ${\bf v}, {\bf u}, {\bf W}$로 부터 $2{\bf v}, -{\bf I}, {\bf X}^\top$ 를 만들어내는 방법을 모른다면 헛수고 아닌가?
- 의문해결: 어차피 우리가 쓰는 층은 선형+(렐루, 시그모이드, ...) 정도가 전부임. 따라서 변환규칙은 미리 계산할 수 있음.
-
결국
(1) 순전파를 하면서 입출력값을 모두 저장하고
(2) 그에 대응하는 층별 미분계수값 $2{\bf v}, -{\bf I}, {\bf X}^\top$ 를 구하고
(3) 층별미분계수값을 다시 곱하면 (그러니까 ${\bf X}^\top (-{\bf I}) 2{\bf v}$ 를 계산) 된다.
(1) 순전파를 계산하고 각 층별 입출력 값을 기록
- yhat = net(X)
- loss = loss_fn(yhat,y)
(2) 역전파를 수행하여 손실함수의 미분값을 계산
- loss.backward()
-
참고로 (1)에서 층별 입출력값은 GPU의 메모리에 기록된다.. 무려 GPU 메모리..
-
작동원리를 GPU의 관점에서 요약 (슬기로운 GPU 활용)
gpu특징: 큰 차원의 매트릭스 곱셈 전문가 (원리? 어마어마한 코어숫자)
- 아키텍처 설정: 모형의 파라메터값을 GPU 메모리에 올림 //
net.to("cuda:0")
- 순전파 계산: 중간 계산결과를 모두 GPU메모리에 저장 (순전파 계산을 위해서라면 굳이 GPU에 있을 필요는 없으나 후에 역전파를 계산하기 위한 대비) //
net(X)
- 오차 및 손실함수 계산:
loss = loss_fn(yhat,y)
- 역전파 계산: 순전파단계에서 저장된 계산결과를 활용하여 손실함수의 미분값을 계산 //
loss.backward()
- 다음 순전파 계산: 이전값은 삭제하고 새로운 중간계산결과를 GPU메모리에 올림
- 반복.
-
역전파기법은 체인룰 + $\alpha$ 이다.
-
오차역전파기법이라는 용어를 쓰는 사람도 있다.
-
이미 훈련한 네트워크에 입력 $X$를 넣어 결과값만 확인하고 싶을 경우 순전파만 사용하면 되고, 이 상황에서는 좋은 GPU가 필요 없다.