31. 고차 미분(준비 편)
32. 고차 미분(이론 편)
33. 고차 미분(구현 편)
33. 뉴턴 방법으로 푸는 최적화(자동 계산)
34. sin 함수 고차 미분
35. 고차 미분 계산 그래프
36. 고차 미분 이외의 용도
31. 고차 미분(준비 편)
DeZero 구현의 요점
- 계산의 '연결'은 Function 클래스의 __call__ 메서드에서 만들어짐
- 구체적인 순전파와 역전파 계산은 Function 클래스를 상속한 클래스의 forward 메서드와 backward 메서드로 처리
주목 : 계산 그래프의 '연결'이 만들어지는 시점 = 순전파를 계산할 때!
역전파를 계산할 때는 만들어지지 않음 → 문제의 핵심
31.1 역전파 계산
역전파를 계산할 때도 '연결'이 만들어진다면, 고차 미분을 자동으로 계산할 수 있게 됨
- y = sin(x)의 계산 그래프
- y.backward()를 호출하면 y의 x에 대한 미분이 구해짐
- 'sin 함수의 미분'을 구하기 위한 계산 그래프
- cf) gx = gy * np.cos(x))를 계산 그래프로 나타낸 것
- 이러한 계산 그래프가 있으면, gx.backward()를 호출하여 gx의 x에 대한 미분 계산할 수 있음
- y = sin(x)의 미분인 gx에 gx.backward()를 호출하여 x에 대한 미분 한 번 더 = 2차 미분
목표 : '미분 계산(그림 31-2)'으례산 그래프로 만드는 것
31.2 역전파로 게산 그래프 만들기
함수의 backward 메서드에서도 ndarray 인스턴스가 아닌 Variable 인스턴스를 사용하면 계산의 '연결' 만들어짐
- 준비 : 미분값(기울기)를 Variable 인스턴스 형태로 유지
- 왼쪽 : 지금까지 Variable 클래스의 grad는 ndarray 인스턴스를 참조
- 오른쪽 : 이를 Variable 인스턴스를 참조하도록 변경
Sin 클래스의 순전파와 역전파를 수행한 후의 계산 그래프
- 중요 : 역전파 계산에 대한 계산 그래프도 만들어짐
- 미분값을 나타내는 gy가 Variable 인스턴스가 된 덕분에 gy를 사용한 계산에도 '연결'이 만들어짐
* CAUTION
- y = sin(x) 계산에서 y.backward()를 수행하면 x와 같은 '말단 변수'만 미분값 유지
- y는 함수가 만들어내는 변수이므로 미분값을 유지하지 않기 때문
- [그림 31-4]에서도 y.grad에서 gy 로의 참조는 없음
생략한 역전파 계산이 만드는 계산 그래프
- [그림 31-5]는 y.backward()를 호출함으로써 '새로' 만들어지는 계산 그래프
= '역전파에 의해 새롭게 계산 그래프가 만들어졌다' - 위와 같은 계산 그래프, gx.backward()를 호출함으로써 y의 x에 대한 2차 미분 이루어짐
32. 고차 미분(구현 편)
고차 미분을 해낼 수 있도록 DeZero를 변경
32.1 새로운 DeZero로!
DeZero로 가는 가장 중요한 변화는 Variable 클래스의 인스턴스 변수인 grad
→ 이전가지 ndarray 인스턴스 참조 => 다른 Vaariable 인스턴스 참조하도록 변경
def backward(self, retain_grad=False, create_graph=False):
if self.grad is None:
xp = dezero.cuda.get_array_module(self.data)
# self.grad = np.ones_like(self.data)
self.grad = Variable(xp.ones_like(self.data))
self.grad가 Variable 인스턴스를 담게 됨
32.2 함수 클래스의 역전파
backward 메서드 수정 (Mul, Sub, Div,m Pow 클래스)
- 전) Variable 인스턴스 안에 있는 데이터를 꺼내야 했음
- 후) Variable 인스턴스를 그대로 사용
[그림 32-1]
32.3 역전파를 더 효율적으로(모드 추가)
18단계에서 역전파 활성/비활성 모드 도입
=> 역전파가 필요 없는 경우 '역전파 비활성 모드'
def backward(self, retain_grad=False, create_graph=False):
...
while funcs:
f = funcs.pop()
gys = [output().grad for output in f.outputs] # output is weakref
with using_config('enable_backprop', create_graph): *
gxs = f.backward(*gys) * # 메인 backward
if not isinstance(gxs, tuple):
gxs = (gxs,)
for x, gx in zip(f.inputs, gxs):
if x.grad is None:
x.grad = gx
else:
x.grad = x.grad + gx * # 이 계산도 대상
if x.creator is not None:
add_func(x.creator)
- 실제 역전파의 처리를 with using_config(...)에서 수행
- 2차 이상의 미분이 필요하다면 create_graph를 True로 설정
→ 역전파를 계산할 때도 계산 그래프 만들어지므로 역전파를 반복 가능
32.4 __init__.py 변경
33. 뉴턴 방법으로 푸는 최적화(자동 계산)
2차 미분도 자동으로 계산
이어서 뉴턴 방법을 사용해 최적화
33.1 2차 미분 계산하기
- 기존 코드는 1차 미분 결과에 2차 미분 결과가 '더해진' 값이 출력됨
- 문제 해결 => 새로운 계산을 하기 전에 Variable의 미분값을 '재설정'
if '__file__' in globals():
import os, sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import numpy as np
from dezero import Variable
def f(x):
y = x ** 4 - 2 * x ** 2
return y
x = Variable(np.array(2.0))
iters = 10
for i in range(iters):
print(i, x)
y = f(x)
x.cleargrad()
y.backward(create_graph=True)
gx = x.grad
x.cleargrad() # 미분값 재설정
gx.backward()
gx2 = x.grad
x.data -= gx.data / gx2.data
- 한 줄이 x의 미분값을 재설정하여 역전파가 올바르게 이루어지게 함
33.2 뉴턴 방법을 활용한 최적화
- 1차 미분과 2차 미분을 사용하여 x를 갱신
if '__file__' in globals():
import os, sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import numpy as np
from dezero import Variable
def f(x):
y = x ** 4 - 2 * x ** 2
return y
x = Variable(np.array(2.0))
iters = 10
for i in range(iters):
print(i, x)
# 1차 미분
y = f(x)
x.cleargrad()
y.backward(create_graph=True)
# 2차 미분
gx = x.grad
x.cleargrad()
gx.backward()
gx2 = x.grad
x.data -= gx.data / gx2.data
- backward 메서드를 두 번 실행하여 자동으로 계산하여 수정
34. sin 함수 고차 미분
새로운 DeZero 함수 몇 개를 추가
34.1 sin 함수 구현
고차 미분에 대응하는 새로운 Sin 클래스 구현 : y = sin(x), y' = cos(x)
class Sin(Function):
def forward(self, x):
xp = cuda.get_array_module(x)
y = xp.sin(x)
return y
def backward(self, gy):
x, = self.inputs
gx = gy * cos(x) * 1
return gx
def sin(x):
return Sin()(x)
중요 : backward 메서드 안의 모든 변수가 Variable 인스턴스
따라서 1의 cos(x)는 DeZero의 cos 함수 => Sin 클래스를 구현하려면 Cos 클래스와 cos 함수 필요
backward 메서드 구현 시 모든 계산은 반드시 DeZero 함수 사용 or 새롭게 구현(오버로드)
34.2 cos 함수 구현
y = cos(x), y' = -sin(x)
class Cos(Function):
def forward(self, x):
xp = cuda.get_array_module(x)
y = xp.cos(x)
return y
def backward(self, gy):
x, = self.inputs
gx = gy * -sin(x)
return gx
def cos(x):
return Cos()(x)
34.3 sin 함수 고차 미분
import numpy as np
from dezero import Variable
import dezero.functions as F
x = Variable(np.array(1.0))
y = F.sin(x)
y.backward(create_graph=True)
for i in range(3):
gx = x.grad
x.cleargrad()
gx.backward(create_graph=True)
print(x.grad) # n차 미분
- for 문을 사용하여 역전파 반복
- 역전파 전 x.cleargrad()를 호출하여 미분값 재설정
- 이를 반복하여 n차 미분 계산
sin(x) cos(x) -sin(x) -cos(s) 식으로 진행
35. 고차 미분 계산 그래프
DeZero 함수 추가 : tanh(쌍곡탄젠트 or 하이퍼볼릭 탄젠트)
- 입력을 -1 ~ 1 사이의 값으로 변환
35.1 tanh 함수 미분
tanh 함수의 미분은 다음의 미분 공식 이용
35.2 tanh 함수 구현
class Tanh(Function):
def forward(self, x):
xp = cuda.get_array_module(x)
y = xp.tanh(x)
return y
def backward(self, gy):
y = self.outputs[0]() # weakref
gx = gy * (1 - y * y)
return gx
def tanh(x):
return Tanh()(x)
35.3 고차 미분 계산 그래프 시각화
차수가 늘어날수록 계산 그래프도 복잡해짐
36. 고차 미분 이외의 용도
지금까지 DeZero로 고차 미분 계산
= 역전파 시 수행되는 계산에 대해서도 '연결'을 만들도록 함 (DeZero의 새로운 능력)
활용 방법 알아보자!
36.1 double backprop의 용도
문제 : 다음의 두 식이 주어졌을 때 x = 2.0에서 x에 대한 z의 미분z'을 구하라
- 이 문제도 double backprop으로 계산할 수 있음
- y'을 계산하면 24x**2 + 2x = 2.0
- 이 식을 z에 대입하면 100.0이라는 답
코드
if '__file__' in globals():
import os, sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import numpy as np
from dezero import Variable
x = Variable(np.array(2.0))
y = x ** 2
y.backward(create_graph=True) *
gx = x.grad
x.cleargrad()
z = gx ** 3 + y
z.backward()
print(x.grad)
# 결과
variable(100.)
중요 : y.backward(create_graph=True)
- 새로운 계산 그래프 생성
- 역전파가 만들어낸 계산 그래프를 사용하여 새로운 계산, 다시 역전파
* NOTE
- 코드의 gx = x.grad는 단순한 변수(값)가 아니라 계산 그래프(식)
- 따라서 x.grad의 계산 그래프에 대해 추가로 역전파할 수 있음
36.2 딥러닝 연구에서의 사용 예
double backprop 사용하는 연구 여러가지
ex) WGAN-GP 논문
최적화하는 식에 기울기(텐서의 각 원소에 대한 미분 결과)가 들어있다는 것 (* 기울기 - 첫 번째 역전파에서 구할 수 있음)
이 기울기를 사용하여 함수 L을 계산
함수 L을 최적화하기 위해 두 번째 역전파
칼럼 : 뉴턴 방법과 double backprop 보충 방법
입력이 벡터인 경우의 뉴턴 방법
뉴턴 방법을 대체할 수 있는 또 다른 방법
double backprop의 실용적인 쓰임 예
다변수 함수의 뉴턴 방법
우리가 한 일 = '입력 변수가 하나(스칼라)인 경우의 뉴턴 방법을 구현'
입력이 다차원 배열일 경우 뉴턴 방법
입력 변수를 벡터 x로 바꾸고 함수 y = f(x)에 대해 생각
(* x는 벡터, x = (x1, x2, ..., xn) 형태로 n개의 원소 가짐
▽f(x)는 기울기, 기울기는 x의 각 원소에 대한 미분, 원소까지 표기하면 [식 C.2]
헤세 행렬은 x의 두 원소에 대한 미분
두 원소의 조합이 이루어지기 때문에 행렬 형태로 정의
[식 C.1]에서 기울기와 헤세 행렬을 사용하여 x를 갱신
x를 기울기 방향으로 갱신, 그 진행 거리를 헤세 행렬의 역행렬을 사용하여 조정
헤세 행렬이라는 2차 미분 정보를 이용함으로써 더 공격적으로 진행할 수 있어서 목적지에 더 빠르게 도달
뉴턴 방법의 문제와 대안
단점
- 매개변수가 많아지면 뉴턴 방법의 헤세 행렬(의 역행렬) 계산에 자원이 너무 많이 소모됨
- 매개변수가 n개면 n**2만큼의 메모리 사용, n x n의 역행렬 계산에는 n**3 만큼 사용
대안 : 준 뉴턴 방법(QNM)
- 뉴턴 방법 중 '헤세 행렬의 역행렬'을 근사해 사용하는 방법의 총칭
QNM의 예 : L-BFGS
- 기울기만으로 헤세 행렬 근사
- (정확한 값이 아닌) 근삿값을 구하여 계산 비용과 메모리 공간을 절약하는 전략
- 파이토치에 구현되어 있음
- But, 지금 주류는 기울기만을 사용한 최적화(SGD, Momentum, Adam 등)
double backprop의 용도 : 헤세 행렬과 벡터의 곱
- 원소 수가 늘어나면 헤세 행렬을 계산하는 비용이 매우 커짐
- 그러나 헤세 행렬과 벡터의 곱의 '결과'만 필요하다면 double backprop을 사용하여 효율적으로 구함
'AI > 딥러닝 프레임워크 기초' 카테고리의 다른 글
[밑시딥③] 제4고지. 신경망 만들기(후편) (1) | 2024.03.05 |
---|---|
[밑시딥③] 제4고지. 신경망 만들기(전편) (1) | 2024.03.04 |
[밑시딥③] 제3고지. 고차 미분 계산(전편) (1) | 2024.02.26 |
[프레임워크 기초] 제2고지. 자연스러운 코드(후편) (2) | 2024.02.08 |
[프레임워크 기초] 제2고지. 자연스러운 코드(전편) (1) | 2024.02.06 |