기본 콘텐츠로 건너뛰기

벡터와 행렬에 관련된 그림들

자동미분(Autograd)

내용

자동미분(Autograd)

torch.autograd

신경망은 설정한 층들을 통과하면서 feature와 label의 관계를 적합하게 설명하는 모델을 구축합니다. 이 과정을 학습(train)이라고 하며 이 학습은 다음과 같이 두 단계로 구성됩니다.

$$X \rightarrow \left(\begin{aligned}\color{blue}{W\cdot x+b \rightarrow \hat{y}(\text{forward})}\\\uparrow \qquad\qquad \qquad \downarrow\\\color{red}{\frac{dw}{dx}, \frac{db}{dx}(\text{backward})}\end{aligned} \right)\rightarrow \hat{Y}$$

위 식에서 W, b는 각각 가중치와 편향입니다.

위의 역전파(backward) 단계서는 순전파(forward)에서 생성된 가중치와 편향에 의한 추정치 $\hat{y}$와 실제값인 label(Y)와의 오차(error)를 계산하고 그 값에 대한 가중치와 편향을 평가(변화도, 미분)하여 그 매개변수(W, b)를 조절합니다. 이 조절을 매개변수의 업데이트(update)라고 하며 그 차이가 최소 또한 변화가 없을 수준까지 이 과정은 반복합니다. 이 과정은 최적화(optimization)라고 하며 그 결과가 최종의 모델이 됩니다.

pytorch에서 torch.autograd는 역전파시 실행되는 미분과정을 담당하는 자동미분엔진입니다.

autograd는 데이터(텐서)의 실행된 모든 연산들(연산 결과가 새로운 텐서인 경우도 포함하여)의 기록을 Function 객체로 구성된 방향성 비순환 그래프(DAG; Directed Acyclic Graph)에 저장(keep)합니다.

그림 1과 같이 방향성 비순환 그래프(DAG)의 잎(leave)은 입력 텐서(feature, W0, b0)이고, 뿌리(root)는 결과 텐서(추정치, 최종 w, b)입니다. 이 그래프를 뿌리에서부터 잎까지 추적하면 연쇄 법칙(chain rule)에 따라 변화도를 자동으로 계산할 수 있습니다.

그림 1. 단순 신경망의 흐름과 노드와 잎.

순전파 단계에서, autograd는 다음 두 가지 작업을 동시에 수행합니다.

  • 요청된 연산을 수행하여 결과 텐서를 계산
  • 역전파 단계에서 계산된 변화도를 유지

역전파는 순전파 단계에서 계산된 loss에 .backward()가 호출될 때 시작됩니다. autograd에 의해 계산하고 기록합니다.

  • 각 변수에 적용한 연산의 종류는 .grad_fn로 확인
  • 각 텐서의 .grad 속성에 계산 결과를 누적(accumulate)
  • 연쇄 법칙을 사용하여, 모든 잎(leaf) 텐서들까지 전파(propagate)

requires_grad 설정

requires_grad=True로 설정된 변수에만 변화도가 계산되고 기록됩니다. 즉 역전파(.backward()) 동안 require_grad=True로 설정된 leaf tensor에만 .grad 필드가 누적됩니다. 그러므로 require_grad=True가 설정된 텐서(grad_fn이 존재하는 텐서)에 대해 계산됩니다. 그러므로 순전파에서 주어지거나 계산되는 nn.Module의 매개변수 등은 그래디언트(미분)의 대상이 되며 grad_fn이 존재합니다. pytorch의 nn.Module에서 자동으로 설정되는 W, b의 초기값에는 자동으로 requires_grad=True가 설정됩니다. 또한 nn.Module.requires_grad_()를 사용하여 모듈 수준에서 requires_grad를 설정할 수도 있습니다. 모듈에 적용하면 .requires_grad_()는 모든 모듈의 매개변수(기본적으로 require_grad=True가 있음)에 적용됩니다.

모델의 일부를 고정하기위해 업데이트하지 않으려는 매개변수에 .requires_grad_(False)를 적용하기만 하면 됩니다. 이 경우 .grad 필드가 업데이트되지 않습니다.

다음 코드에서 입력 x, 매개변수 wb , 그리고 일부 손실 함수가 있는 가장 간단한 단일 계층 신경망을 가정하겠습니다. PyTorch에서는 다음과 같이 정의할 수 있습니다

import torch, torchvision
torch.manual_seed(1)
x = torch.ones(5)  # input tensor
y = torch.zeros(3)  # expected output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b
loss = torch.nn.functional.mse_loss(z, y)
x, w
(tensor([1., 1., 1., 1., 1.]),
 tensor([[ 0.6614,  0.2669,  0.0617],
         [ 0.6213, -0.4519, -0.1661],
         [-1.5228,  0.3817, -1.0276],
         [-0.5631, -0.8923, -0.0583],
         [-0.1955, -0.9656,  0.4224]], requires_grad=True))
z
tensor([-0.7313, -2.0824, -1.2786], grad_fn=&tl;AddBackward0>)
loss
tensor(2.1687, grad_fn=&tl;MseLossBackward>)

이 신경망에서, wb는 최적화해야 하는 매개변수입니다. 따라서 이러한 변수들에 대한 손실 함수의 변화도를 계산할 수 있어야 합니다. 이를 위해서 해당 텐서에 requires_grad 속성을 설정합니다.

Note

requires_grad의 값은 텐서를 생성할 때 설정하거나, 나중에 x.requires_grad_(True) 메소드를 사용하여 나중에 설정할 수도 있습니다.

역방향 전파 함수에 대한 참조(reference)는 텐서의 grad_fn 속성에 저장됩니다. 이 속성은 연산그래프를 구성하기 위해 텐서에 적용하는 Function의 클래스 객체입니다.

print('Gradient function for z =', z.grad_fn)
print('Gradient function for loss =', loss.grad_fn)
Gradient function for z = <AddBackward0 object at 0x7f4d931503a0>
Gradient function for loss = <MseLossBackward object at 0x7f4dd41fe1f0>

변화도(Gradient) 계산하기

신경망에서 매개변수의 가중치를 최적화하려면 매개변수에 대한 손실함수의 도함수(derivative)를 계산해야 합니다. 즉 x에 대한 loss의 변화량으로 $\frac{\partial w}{\partial x}$와 $\frac{\partial b}{\partial x}$를 계산합니다.

이러한 도함수를 계산하기 위해, loss.backward()를 호출한 다음 w.gradb.grad에서 값을 가져옵니다.

loss.backward()
print(w.grad)
print(b.grad)
tensor([[-1.4688,  0.9328, -0.3732],
        [-1.4688,  0.9328, -0.3732],
        [-1.4688,  0.9328, -0.3732],
        [-1.4688,  0.9328, -0.3732],
        [-1.4688,  0.9328, -0.3732]])
tensor([-1.4688,  0.9328, -0.3732])
Note
  • 성능 상의 이유로, 주어진 그래프에서의 backward를 사용한 변화도 계산은 한 번만 수행할 수 있습니다. 즉, 한 번의 미분만 가능하다. 미분의 결과를 다시 미분하기 위해서는 backward(etain_graph=True)와 같이 이 함수에 retain_graph=True을 전달해야 합니다. 이러한 지정은 연속된 미분이 필요시 모든 경우에서 지정해야 합니다.
loss.backward()
---------------------------------------------------------------------------
RuntimeError: Trying to backward through the graph a second time 
(or directly access saved variables after they have already been freed). 
Saved intermediate values of the graph are freed when you call .backward() or autograd.grad(). 
Specify retain_graph=True if you need to backward through the graph a second time or 
if you need to access saved variables after calling backward.
loss1 = torch.nn.functional.mse_loss(z, y)
loss1.backward(retain_graph=True)
w.grad #첫번째 미분
tensor([[-0.4876, -1.3883, -0.8524],
        [-0.4876, -1.3883, -0.8524],
        [-0.4876, -1.3883, -0.8524],
        [-0.4876, -1.3883, -0.8524],
        [-0.4876, -1.3883, -0.8524]])
loss1.backward(retain_graph=True) #두번째미분
w.grad
tensor([[-0.9751, -2.7766, -1.7048],
        [-0.9751, -2.7766, -1.7048],
        [-0.9751, -2.7766, -1.7048],
        [-0.9751, -2.7766, -1.7048],
        [-0.9751, -2.7766, -1.7048]])
loss1.backward() #3번째 미분
w.grad
tensor([[-1.4627, -4.1648, -2.5572],
        [-1.4627, -4.1648, -2.5572],
        [-1.4627, -4.1648, -2.5572],
        [-1.4627, -4.1648, -2.5572],
        [-1.4627, -4.1648, -2.5572]])

변화도 추적 멈추기

기본적으로 requires_grad=True를 설정한 텐서는 변화도의 계산을 지원하고 기록이 추적됩니다. 그러나 학습단계에서 구축한 모델을 검정하는 단계에서는 순전파 단계만 필요합니다. 이와같이 추적이나 지원이 필요없을 경우에는 torch.no_grad() 블록으로 이 추적을 멈출수 있습니다.

신경망의 일부 매개변수를 고정해서 사용할 경우 즉, 학습된 매개변수의 미세한 조정이 필요한 경우 변화도 추적을 멈추어야 합니다. 이러한 과정은 연산속도가 향상됩니다.

z = torch.matmul(x, w)+b
print(z.requires_grad)
with torch.no_grad():
    z = torch.matmul(x, w)+b
print(z.requires_grad)
True
False

동일한 결과를 얻는 다른 방법은 텐서에 detach() 메소드를 사용하는 것입니다.

z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)
False
z_det
tensor([-0.7313, -2.0824, -1.2786])

Grad Modes

require_grad 설정 외에도 PyTorch의 계산이 autograd에 의해 내부적으로 처리되는 방식에 영향을 줄 수 있는 즉 그래디언트의 활성 또는 비활성을 위한 세 가지 모드가 있습니다.

기본 모드(grad 모드)
no_grad나 평가모드가 선언되지 않은 경우에 적용되는 암시적 모드입니다. 즉, no_grad나 평가모드를 진행하기 위해서는 그 모드가 선언되어야 것에 반해 이 모드는 단지 이 두 모드가 선언되지 않은 상태에서 진행됩니다.
기본모드는 required_grad=True가 적용되지만 다른 두 모드에서는 require_grad=False로 재정의됩니다.
no_grad 모드
이 모드하에서는 변화량이 계산되지 않습니다. 즉, 역전파 단계는 이루어지지 않습니다.
텐서에 requires_grad=False로 설정함으로서그래디언트를 비활성화하는 것과 같습니다.
Torch.nn.init의 구현은 초기화된 매개변수를 제자리에서 업데이트할 때 autograd 추적을 피하기 위해 매개변수를 초기화할 때 no_grad 모드에 의존합니다.
추론 모드
평가 모드(nn.Module.eval())
eval()을 사용하여 module로 하여금 inference mode로 바꾸어주게 되면, (gradient를 계산하지 않도록 함으로써) inference 속도 뿐만 아니라, dropout 또는 batch-normalization과 같은 training과 inference 시에 다른 forward() 동작을 하는 module들에 대해서 각기 때에 따라 올바른 동작을 하도록 합니다. 다만, inference가 끝나면 다시 train()을 선언 해 주어, 원래의 훈련모드로 돌아가게 해 주어야 합니다.
구축되는 모델에 따라 중지되는 부분이 차이가 보임
학습
model.eval()#평가모드 선언
y_hat = model(x_valid)
model.train()#학습단계로 확인

댓글

이 블로그의 인기 게시물

[Linear Algebra] 유사변환(Similarity transformation)

유사변환(Similarity transformation) n×n 차원의 정방 행렬 A, B 그리고 가역 행렬 P 사이에 식 1의 관계가 성립하면 행렬 A와 B는 유사행렬(similarity matrix)이 되며 행렬 A를 가역행렬 P와 B로 분해하는 것을 유사 변환(similarity transformation) 이라고 합니다. $$\tag{1} A = PBP^{-1} \Leftrightarrow P^{-1}AP = B $$ 식 2는 식 1의 양변에 B의 고유값을 고려한 것입니다. \begin{align}\tag{식 2} B - \lambda I &= P^{-1}AP – \lambda P^{-1}P\\ &= P^{-1}(AP – \lambda P)\\ &= P^{-1}(A - \lambda I)P \end{align} 식 2의 행렬식은 식 3과 같이 정리됩니다. \begin{align} &\begin{aligned}\textsf{det}(B - \lambda I ) & = \textsf{det}(P^{-1}(AP – \lambda P))\\ &= \textsf{det}(P^{-1}) \textsf{det}((A – \lambda I)) \textsf{det}(P)\\ &= \textsf{det}(P^{-1}) \textsf{det}(P) \textsf{det}((A – \lambda I))\\ &= \textsf{det}(A – \lambda I)\end{aligned}\\ &\begin{aligned}\because \; \textsf{det}(P^{-1}) \textsf{det}(P) &= \textsf{det}(P^{-1}P)\\ &= \textsf{det}(I)\end{aligned}\end{align} 유사행렬의 특성 유사행렬인 두 정방행렬 A와 B는 'A ~ B' 와 같...

[sympy] Sympy객체의 표현을 위한 함수들

Sympy객체의 표현을 위한 함수들 General simplify(x): 식 x(sympy 객체)를 간단히 정리 합니다. import numpy as np from sympy import * x=symbols("x") a=sin(x)**2+cos(x)**2 a $\sin^{2}{\left(x \right)} + \cos^{2}{\left(x \right)}$ simplify(a) 1 simplify(b) $\frac{x^{3} + x^{2} - x - 1}{x^{2} + 2 x + 1}$ simplify(b) x - 1 c=gamma(x)/gamma(x-2) c $\frac{\Gamma\left(x\right)}{\Gamma\left(x - 2\right)}$ simplify(c) $\displaystyle \left(x - 2\right) \left(x - 1\right)$ 위의 예들 중 객체 c의 감마함수(gamma(x))는 확률분포 등 여러 부분에서 사용되는 표현식으로 다음과 같이 정의 됩니다. 감마함수는 음이 아닌 정수를 제외한 모든 수에서 정의됩니다. 식 1과 같이 자연수에서 감마함수는 factorial(!), 부동소수(양의 실수)인 경우 적분을 적용하여 계산합니다. $$\tag{식 1}\Gamma(n) =\begin{cases}(n-1)!& n:\text{자연수}\\\int^\infty_0x^{n-1}e^{-x}\,dx& n:\text{부동소수}\end{cases}$$ x=symbols('x') gamma(x).subs(x,4) $\displaystyle 6$ factorial 계산은 math.factorial() 함수를 사용할 수 있습니다. import math math.factorial(3) 6 a=gamma(x).subs(x,4.5) a.evalf(3) 11.6 simpilfy() 함수의 알고리즘은 식에서 공통사항을 찾아 정리하...

sympy.solvers로 방정식해 구하기

sympy.solvers로 방정식해 구하기 대수 방정식을 해를 계산하기 위해 다음 함수를 사용합니다. sympy.solvers.solve(f, *symbols, **flags) f=0, 즉 동차방정식에 대해 지정한 변수의 해를 계산 f : 식 또는 함수 symbols: 식의 해를 계산하기 위한 변수, 변수가 하나인 경우는 생략가능(자동으로 인식) flags: 계산 또는 결과의 방식을 지정하기 위한 인수들 dict=True: {x:3, y:1}같이 사전형식, 기본값 = False set=True :{(x,3),(y,1)}같이 집합형식, 기본값 = False ratioal=True : 실수를 유리수로 반환, 기본값 = False positive=True: 해들 중에 양수만을 반환, 기본값 = False 예 $x^2=1$의 해를 결정합니다. solve() 함수에 적용하기 위해서는 다음과 같이 식의 한쪽이 0이 되는 형태인 동차식으로 구성되어야 합니다. $$x^2-1=0$$ import numpy as np from sympy import * x = symbols('x') solve(x**2-1, x) [-1, 1] 위 식은 계산 과정은 다음과 같습니다. $$\begin{aligned}x^2-1=0 \rightarrow (x+1)(x-1)=0 \\ x=1 \; \text{or}\; -1\end{aligned}$$ 예 $x^4=1$의 해를 결정합니다. solve() 함수의 인수 set=True를 지정하였으므로 결과는 집합(set)형으로 반환됩니다. eq=x**4-1 solve(eq, set=True) ([x], {(-1,), (-I,), (1,), (I,)}) 위의 경우 I는 복소수입니다.즉 위 결과의 과정은 다음과 같습니다. $$x^4-1=(x^2+1)(x+1)(x-1)=0 \rightarrow x=\pm \sqrt{-1}, \; \pm 1=\pm i,\; \pm1$$ 실수...