[PyTorch] Autograd-03 : Practice01 이어서
Graph가 무엇이냐?
DCG(Dynamic Computation Graph)
뭐 자세히는 모르겠고, 동적으로 즉, 계산 진행과 동시에 아래와 같은 graph를 생성하는 그런거 같다.
[!] backward()
함수는 Graph를 만든 후에 한 번만 호출하는 것을 가정하고 있다.
backward()
를 한 번 이상 호출하면 오류가 나온다.
출처 : https://teamdable.github.io/techblog/PyTorch-Autograd
x = torch.tensor(5.0, requires_grad=True)
y = x**3
z = torch.log(y)
z.backward(retain_graph=True) # graph를 유지하라!
print('x after backward', get_tensor_info(x))
print('y after backward', get_tensor_info(y))
print('z after backward', get_tensor_info(z))
z.backward()
print('x after 2backward', get_tensor_info(x))
print('y after 2backward', get_tensor_info(y))
print('z after 2backward', get_tensor_info(z))
>>>
x after backward requires_grad(True) is_leaf(True) retains_grad(False) grad_fn(None) grad(0.6000000238418579) tensor(tensor(5., requires_grad=True))
y after backward requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<PowBackward0 object at 0x7f86f95fc0a0>) grad(None) tensor(tensor(125., grad_fn=<PowBackward0>))
z after backward requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<LogBackward0 object at 0x7f86fcd5a7f0>) grad(None) tensor(tensor(4.8283, grad_fn=<LogBackward0>))
x after 2backward requires_grad(True) is_leaf(True) retains_grad(False) grad_fn(None) grad(1.2000000476837158) tensor(tensor(5., requires_grad=True))
y after 2backward requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<PowBackward0 object at 0x7f86f95e4f10>) grad(None) tensor(tensor(125., grad_fn=<PowBackward0>))
z after 2backward requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<LogBackward0 object at 0x7f86f95e4f40>) grad(None) tensor(tensor(4.8283, grad_fn=<LogBackward0>))
backward()
"(거꾸로 올라가면서 얘의)Gradient 계산해"backward(retain_graph=True)
"Gradient 계산하긴 하는데! 만든 graph는 날리지말고 유지시켜"
# 위 코드의 실행결과에서 x.grad의 값이 어떻게 되는지만 확인해보자
x after backward grad(0.6000000238418579)
x after 2backward grad(1.2000000476837158)
z.backward(retain_graph=True)와 같이 graph를 유지시키면
즉, backward() 호출에 필요한...
즉, Gradient를 계산하기 위해 필요한...
자원들을 해제하지 않음.
따라서 backward() 호출을 한번 더 할 수 있으나
새로 값을 덮어씌우지 않고 더해버림!
[?] 어떤 값에 대한 최종 output의 Gradient를 중첩시킬 필요가 있을까?
따라서
backward()
를 한 번 이상 호출할 때는
Gradient가 저장되는 곳, 즉x.grad
를 초기화 시켜준다.
x.grad.zero_()
'파생변수 추가' 라는 것이 맞는 표현인지는 모르겠음
지금까지 작성했던 코드에서는... 아래와 같은 파생 연산이 이루어졌었다.
Leaf - Stem - Root
여기서 Stem을 하나 더 추가해보자.
x = torch.tensor(5.0, requires_grad=True)
y = x ** 3
w = x ** 2
z = torch.log(y) + torch.sqrt(w)
print('x', get_tensor_info(x))
print('y', get_tensor_info(y))
print('w', get_tensor_info(w))
print('z', get_tensor_info(z))
>>>
x requires_grad(True) is_leaf(True) retains_grad(False) grad_fn(None) grad(None) tensor(tensor(5., requires_grad=True))
y requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<PowBackward0 object at 0x7f86fcd41fd0>) grad(None) tensor(tensor(125., grad_fn=<PowBackward0>))
w requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<PowBackward0 object at 0x7f86f6621b50>) grad(None) tensor(tensor(25., grad_fn=<PowBackward0>))
z requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<LogBackward0 object at 0x7f86f6621520>) grad(None) tensor(tensor(4.8283, grad_fn=<LogBackward0>))
requires_grad
는 말단의 leaf라고 볼 수 있는x
에 켜주다보니x
로부터 파생된 변수y
,w
,z
모두 requires_grad=True이다.is_leaf
는x
만True
이고 나머지는False
이다.grad_fn
에서는y
와w
모두x
로부터 파생연산(제곱연산)된 것 이다보니 PowBackward Function이 할당되었다.이제
z.backward()
를 호출시킨 다음 어떻게 gradient가 계산되는지 보자!
# gradient 연산이 어떻게 이루어지는지 확인하기 위하여y
,w
,z
의 gradient를 보존하자.
y.retain_grad()
w.retain_grad()
z.retain_grad()
z.backward()
print('x after backward', get_tensor_info(x))
print('y after backward', get_tensor_info(y))
print('w after backward', get_tensor_info(w))
print('z after backward', get_tensor_info(z))
>>>
x after backward requires_grad(True) is_leaf(True) retains_grad(False) grad_fn(None) grad(1.600000023841858) tensor(tensor(5., requires_grad=True))
y after backward requires_grad(True) is_leaf(False) retains_grad(True) grad_fn(<PowBackward0 object at 0x7f86fcd41fa0>) grad(0.00800000037997961) tensor(tensor(125., grad_fn=<PowBackward0>))
w after backward requires_grad(True) is_leaf(False) retains_grad(True) grad_fn(<PowBackward0 object at 0x7f86fcd5a9d0>) grad(0.10000000149011612) tensor(tensor(25., grad_fn=<PowBackward0>))
z after backward requires_grad(True) is_leaf(False) retains_grad(True) grad_fn(<AddBackward0 object at 0x7f86fcd5a2b0>) grad(1.0) tensor(tensor(9.8283, grad_fn=<AddBackward0>))
먼저 의 관계를 수식으로 적어보자.
거슬러 올라가면서 gradient는 어떻게 구할까?
- 를 구한다.
- 를 구한다.
- 와 를 구한다.
- 아래와 같이 Chain Rule을 사용한다.
이와 같이 일때
에 대한 의 Gradient는
가 를 통해 에 준 영향과
가 를 통해 에 준 영향을 더해서 계산한다.backward()에서 grad에 Gradient를 저장할 때 기존의 grad의 Gradient를 더하기 때문에
06.retain_graph 참고이런 계산이 자연스럽게 이루어짐.
Convolutional Neural Network의 Convolution Filter처럼 한 Weight가 여러 계산에 Share되면서 계산되는 경우에, 이런 식으로 Gradient가 합산되면서 grad에 저장됩니다.그렇다고 합니다...
여기서 중요한게 우리는
.backward()
를 어떤 Tensor에 호출시켜줌으로서 해당 Tensor의 Gradient를 구하는 것 이다.
따라서 우리가y
와w
에 backward()를 호출시키지 않는 한y
와w
의 Gradient는 볼 수 없다.계산이 되더라도...
- 컴퓨터가 보여주는 것
- 컴퓨터가 보여주지 않는 것
q = torch.tensor(3.0, requires_grad=True)
x = torch.tensor(5.0, requires_grad=True)
y = x ** q
z = torch.log(y)
print('q', get_tensor_info(q))
print('x', get_tensor_info(x))
print('y', get_tensor_info(y))
print('z', get_tensor_info(z))
>>>
q requires_grad(True) is_leaf(True) retains_grad(False) grad_fn(None) grad(None) tensor(tensor(3., requires_grad=True))
x requires_grad(True) is_leaf(True) retains_grad(False) grad_fn(None) grad(None) tensor(tensor(5., requires_grad=True))
y requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<PowBackward1 object at 0x000001C2AFE2A160>) grad(None) tensor(tensor(125., grad_fn=<PowBackward1>))
z requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<LogBackward0 object at 0x000001C2AFE2AEB0>) grad(None) tensor(tensor(4.8283, grad_fn=<LogBackward0>))
q
와x
로 부터y
가 파생되고, 그y
로부터z
가 파생되었다.
따라서q
와x
의 is_leaf값이True
임을 확인할 수 있고...
y
의 grad_fn값이 PowBackward1임을 볼 수 있다. [?] PowBackward '1'?
q,x,y,z
의 관계를 수식으로 살펴보면 다음과 같다.
backward()
를 호출하여 즉, 거슬러 올라가면서 Gradient는 어떻게 구할까?
q = torch.tensor(3.0, requires_grad=True)
x = torch.tensor(5.0, requires_grad=True)
y = x ** q
z = torch.log(y)
z.backward()
print('q_after_backward', get_tensor_info(q))
print('x_after_backward', get_tensor_info(x))
print('y_after_backward', get_tensor_info(y))
print('z_after_backward', get_tensor_info(z))
>>>
q_after_backward requires_grad(True) is_leaf(True) retains_grad(False) grad_fn(None) grad(1.6094380617141724) tensor(tensor(3., requires_grad=True))
x_after_backward requires_grad(True) is_leaf(True) retains_grad(False) grad_fn(None) grad(0.6000000238418579) tensor(tensor(5., requires_grad=True))
y_after_backward requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<PowBackward1 object at 0x000001C2AFE0D610>) grad(None) tensor(tensor(125., grad_fn=<PowBackward1>))
z_after_backward requires_grad(True) is_leaf(False) retains_grad(False) grad_fn(<LogBackward0 object at 0x000001C2AFE0DA90>) grad(None) tensor(tensor(4.8283, grad_fn=<LogBackward0>))
07. Stem 추가(파생변수 추가) 와는 다르게 여기서는 Leaf가 추가되었다.
따라서 두 개 의 Gradient 즉,
1.q
에 대한z
의 Gradient
2.x
에 대한z
의 Gradient
를 구하게 된다.
[+] 지수함수의 미분법
q.grad
와x.grad
를 출력해볼 필요도 없이
q
와x
의grad
에 각각 1.609...와 0.6이 할당되어 있는 것을 볼 수 있다.
# 노파심
print("z gradient w.r.t q:", q.grad)
print("z gradient w.r.t x:", x.grad)
>>>
z gradient w.r.t q: tensor(1.6094)
z gradient w.r.t x: tensor(0.6000)
아래의 그림을 꼭 살펴보자 (출처 : https://teamdable.github.io/techblog/PyTorch-Autograd)
q와 x의 정방향 흐름(파란색)부터 보고, 역방향 흐름(빨간색)을 보셈
참고 : https://hongl.tistory.com/206
PyTorch에서는 어떠한 기능을 하는 함수에 대해
forward()
, backward()
를 torch.autograd
모듈로 새롭게 정의할 수 있습니다.
torch.autograd.Function
클래스를 상속하여
@staticmethod
를 이용하여
입력에 대한 함수의 동작을 forward() 함수에
함수 출력에 대한 기울기를 받아 / 입력에 대한 기울기를 계산하는 backward() 함수를
새롭게 정의합니다.
무슨 소릴까...천천히 하나하나씩 정리해보자
니가 작성한 것 : [Python] Instance, Class, and Static Methods
08. Leaf 추가 에서 q
와 x
라는 leaf 2개를 통해 y
가 파생되었고
그 y
는 grad_fn
에 PowBackward1이라는 함수를 저장하고 있었다.
우리는 이 PowBackward1이 어떻게 작동하는지 관심이 있어서
직접 Pow 함수를 만들어서 살펴보려 한다.
class MyPow(torch.autograd.Function):
@staticmethod
def forward(ctx, input_1, input_2):
ctx.save_for_backward(input_1, input_2)
result = input_1 ** input_2
return result
@staticmethod
def backward(ctx, grad_output):
input_1, input_2 = ctx.saved_tensors
grad_input_1 = grad_output * input_2 * input_1 ** (input_2 - 1)
grad_input_2 = grad_output * input_1 ** input_2 * torch.log(input_1)
print('input_1', input_1)
print('input_2', input_2)
print('grad_output', grad_output)
print('grad_input_1', grad_input_1)
print('grad_input_2', grad_input_2)
return grad_input_1, grad_input_2
PyTorch에서 '연산자' 즉, Operation을 정의할 때 torch.autograd.Function
를 상속하여 forward()
와 backward()
를 구현합니다.
forward()
에는 ctx
와 연산자에 전달되는 argument이 차례대로 전달되고 이것을 이용해서 연산자가 계산해야 될 계산을 한 후에 계산결과를 return합니다. 여기서 추가로 처리해줘야 할 것이 있는데, backward()
에서 Gradient를 계산하기 위해서는 forward()
의 연산 당시의 상태를 알고 있어야 하기 때문에, backward()
에서 필요한 상태정보를 forward()
에서 ctx.save_for_backward()
를 호출하여 저장해줘야 합니다.
예를 들어서... 08. Leaf 추가에서의 코드처럼 (아래) 있을때
backward()
가 이라는 사실은 혼자서도 구할 수 있지만, Gradient를 구체적인 숫자로 계산하기 위해서는 forward()
의 연산 당시의 구체적인 와 의 값을 알아야
요렇게 구할 수 있습니다. 따라서 MyPow Class의 forward()
와 backward()
(static) method를 살펴보면...
class MyPow(torch.autograd.Function):
@staticmethod
def forward(ctx, input_1, input_2):
ctx.save_for_backward(input_1, input_2)
result = input_1 ** input_2
return result
@staticmethod
def backward(ctx, grad_output):
input_1, input_2 = ctx.saved_tensors
grad_input_1 = grad_output * input_2 * input_1 ** (input_2 - 1)
grad_input_2 = grad_output * input_1 ** input_2 * torch.log(input_1)
[+] context, ctx : n. 맥락, 배경상황
forward method
ctx
: forward에서는 input1, input2를 backward용으로 저장해두는 곳 (메모장 느낌?)
input_1
,input_2
: 이 MyPow 클래스는 지금 아래의 연산을 위해 존재하는 것이므로 각각 , 이다.
forward method :
backward method :
result
:
backward method
ctx
: 앞서 forward에서 input1, input2 즉 와 를 저장해 둔 곳grad_output
: 아래 그림에서와 같이 MyPow Class의 backward method
즉, 아래 그림의 PowBackward는 를 input으로 받아 연산을 수행하고 에게는 를 주고, 에게는 를 전달해준다. 따라서 input으로 받은 값임.
grad_input_1
: operation code를 보면 이므로... 이겠구나!grad_input_2
: operation cdoe를 보면 이므로... 이겠구나!
[!]여기서 Keypoint는 forward()
연산 진행 시 ctx.save_for_backward(input_1, input_2)
을 통해서 값과 값을 저장하고 backward()
연산에 ctx.saved_tensors
를 통해서 넘겨준다는 것이다.
[+] Code 내 변수들에게 저장된 값은 아래와 같다.
input_1 tensor(5., requires_grad=True)
input_2 tensor(3., requires_grad=True)
grad_output tensor(0.0080)
grad_input_1 tensor(0.6000)
grad_input_2 tensor(1.6094)
input_1
:
input_2
:
grad_output
:
grad_input_1
:
grad_input_2
:
해당 파트인 09. Operation 맨 앞에서 아래의 글이 잘 이해가 되지 않았었지?
...
PyTorch에서는 어떠한 기능을 하는 함수에 대해
forward()
,backward()
를torch.autograd
모듈로 새롭게 정의할 수 있습니다.
torch.autograd.Function
클래스를 상속하여
@staticmethod
를 이용하여
입력에 대한 함수의 동작을 forward() 함수에
함수 출력에 대한 기울기를 받아 / 입력에 대한 기울기를 계산하는 backward() 함수를
새롭게 정의합니다.
...
[?] 아직 torch.autograd.Function
을 상속한다는 것은 무슨말인지 모르겠음
[!] forward 메소드에는 입력에 대한 forward 방향 연산을 정의!
backward 메소드에는 해당 클래스함수가 출력한 것에 대한 기울기를 받아서 입력에 대한 기울기를 계산 즉, 위의 예시에서 MyPow 클래스는 무엇을 output 했고 무엇을 input 받았니?
출력한 것에 대한 기울기 :
입력한 것에 대한 기울기 :
자 09. Operation에서 보았듯이
연산함수 즉 Operation function (MyPow 클래스...)에는 forward와 backward 메소드가 있고 backward 메소드 호출을 대비하여 ctx.save_for_backward()
, grad_fn
등을 준비하므로 상당한 양의 메모리를 사용하게 된다.
[+] 선행조건 : 연산되는 Tensor의 requires_grad=True
근데 Training 과정에서만 backward가 필요하지 Inference, 즉 추론(= Test, Validation)과정에서는 backward가 필요하지 않다.
따라서
with torch.no_grad()
q = torch.tensor(3.0, requires_grad=True)
x = torch.tensor(5.0, requires_grad=True)
...
이와 같이 torch.no_grad()
context를 사용하여 해당 context 내에서 생성된 Tensor들의 requires_grad가 False
로 설정되도록 할 수 있다. 이는 메모리 사용량을 크게 줄일 수 있다.