본문 바로가기

카테고리 없음

[AI] Back-propagation 구현

PyTorch를 이용한 forward propagation, backward propagation의 과정과 PyTorch autograd의 여러가지 기능을 간단하게 구현한다.

  • 실제 PyTorch source code는 매우 방대하고 복잡하기 때문에, 그 중에서 핵심적인 기능만을 고려한 toy library MyTorch의 MyTensor (텐서)를 구현한다.
  • PyTorch와 Numpy는 거의 동일하지만 PyTorch는 Backward를 할 수 있도록 만들어졌다는 차이점이 있다.

기존 딥러닝 프레임워크를 사용하지 않고 Linear regression 모델을 구현해서 Back-propagation까지 한다. Linear regression 모델은 원래 Back-propagation을 하지 않고 수식적으로 풀어서 한 번에 업데이트할 수 있는 매우 간단한 모델이지만, Back-propagation을 이해하기 위해 간단한 Linear regression을 구현한다.

 

01. Forward Propagation

  • 딥러닝 모델은 computational graph로 표현할 수 있으며, computational graph의 node는 텐서 혹은 텐서들의 operation을 나타낸다.
  • Forward propagation (forward pass, 순전파) 은 입력값에 대해 정의된 computational graph에 따라 연산하여 최종 output을 도출하는 과정을 의미한다.
  • 딥러닝 모델(computational graph)은 일반적으로 여러 층(layer)이 있고, 각각의 layer에서 계산된 중간 값들이 다음 layer의 입력값으로 사용되는 순차적인 구조를 갖기 때문에 Forward Propagation(전파) 라고 부른다.
  • 일반적으로 graph에서 children node가 없는 말단의 node를 leaf node라고 부른다. 마찬가지로, PyTorch에서도 이러한 node를 leaf tensor라고 부른다.
    • 위 그래프를 예시로 들면, a, b, c, f가 leaf node에 해당한다.
    • 반면 e, d, L은 다른 연산의 결과로서 생성되는 값을 data로 가지기 때문에(=chilren node가 있음) leaf node가 아니다.
    • 딥러닝 모델에서 leaf node는 입력 데이터 혹은 모델의 weight parameter에 해당한다.
    • 그 중에서 우리의 목적은 weight parameter를 조절해서 graph의 최종 output인 loss를 최대한 낮은 값으로 만드는 것이다.
    • 이를 위해서 backpropagation 및 gradient descent가 사용된다.

 

02. Backpropagation

  • Backpropagation은 chain rule을 활용해 output node $L$ (=loss) 의 leaf node $x$ 에 대한 gradient $\dfrac{\partial L}{\partial x}$를 계산하는 방식이다.
  • Output node에서 leaf node까지 역방향으로 local gradient를 반복적으로 곱하는 방식이기 때문에 backpropagation으로 불린다.
  • Backpropagation은 미분을 통해 한다. 미분은 수치적(Numerical)인 방법 혹은 수동적(Manual)인 방법으로 할 수 있다.
    • 수치적인 방법은 미분의 정의를 그대로 사용하는 방법이다.
      • 정확하지 않고 속도가 느려 실제로 거의 사용하지는 않는다.
    • 수동적인 방법은 미리 도함수를 구하는 규칙을 구현하는 방법이다.

02-1. Numerical gradient

  • Numerical gradient는 정확한 gradient 값이 아닌 근사값을 구하며, 미분하기 어려운 함수에도 사용할 수 있다. 다만, 모델이 거대해지면 속도가 느려지고 매우 비효율적인 방법이 된다.
  • 함수 $f$에 대한 $a$지점에서의 numerical gradient는 매우 작은 값 $h$를 사용해서 다음과 같이 구할 수 있다.
  • $\lim_{h \to 0+} \dfrac{f(a+h)-f(a)}{h}$ , $\lim_{h \to 0-} \dfrac{f(a)-f(a-h)}{h}$
    • 이를 손으로 계산하는 것은 매우 힘든 일이지만 컴퓨터는 계산할 수 있다.
    • h값은 보통 $10^{-6}$~ $10^{-5}$정도로 사용한다. $10^{-6}$보다 작은 값을 사용하면 float point error가 발생함으로 $10^{-6}$보다 작은 값은 사용하지 않는다.
    • h값을 양수로 하면 우극한을 의미하고 음수로 하면 좌극한을 의미한다. 우극한이나 좌극한 둘 중 하나만 사용할 수도 있지만 둘의 평균으로 우극한과 좌극한을 모두 고려한 값을 구할 수 있다.
    • $\lim_{h \to 0} \dfrac{f(a+h)-f(a-h)}{2h}$
  • 이 방식을 이용해 gradient를 구하기 위해서는 $f(a+h), f(a)$에 대한 forward pass 연산을 각각 수행해야 한다.
  • 또한, 매우 작은 $h$값을 사용해 값을 근사하기 때문에 정확도가 떨어질 수 있어 일반적으로 사용되지는 않는다.
  • 반면 구현이 쉽다는 장점이 있기 때문에 프로그래밍 과정에서 gradient가 제대로 계산이 되었는지 확인하는 디버깅 용도로 사용할 수 있다.
import math
import numpy as np
import matplotlib.pyplot as plt
def f(x):
    return 3*x**2 - 4*x + 5
h = 0.0001 # 10^-5
x = 3.0
grad = (f(x+h)-f(x))/h # numerical gradient
print(grad)
	# 14.000300000063248
  • gradient의 결과값은 14로 출력된다. 실제로 $3x^2 - 4x + 5$를 미분하면 $6x-4$이므로, $x=3$인 지점에서의 gradient 값이 14가 나오는 것을 확인할 수 있다.
  • 해당 수식의 의미를 살펴보면 입력값 $x=3$이 미세하게 커질 때 출력값이 얼마나 달라지는지를 나타낸다는 것을 알 수 있다.

02-2. Manual backpropagation with analytic gradient

  • 간단한 함수의 경우(computation graph)의 경우 미분을 이용해 numerical gradient를 쉽게 구할 수 있지만, 매우 복잡한 함수로 이루어진 딥러닝 모델의 경우 미분 연산에 매우 많은 비용이 소모된다.
  • 따라서, 현대의 딥러닝 모델의 학습은 chain rule을 이용해서 output에서 leaf node까지 gradient를 역순으로 구해나가는 analytic gradient를 이용한 backpropagation을 수행하고 있다.
  • 사칙연산, 지수함수, log, sin, cos 등 기본 연산의 도함수를 컴퓨터에게 알려주면 나머지는 기본 연산들의 조합임으로 computation graph를 구축했을 때 거의 대부분의 함수는 미분이 가능해진다.
  • 그러나 한 묶음으로 자주 쓰이는 함수(sigmoid, softmax 등)는 기본 연산 단위로 쪼개서 computation graph를 구축하면 비효율적이기 때문에 직접 손으로 미분한 결과를 모듈로 만들어서 사용한다.
    • 그렇기 때문에 모델을 만들 때 직접 함수를 구현하기 보다 pytorch에서 제공하는 “torch.nn.함수”를 사용하는 것이 좋다. 이는 클래스 형태의 함수로 backward가 모듈화되어 있어 효율적인 학습이 가능하다.
    • pytorch가 2.0버전으로 업그레이드 되면서 nn.함수가 아니더라도 pytorch 내에 있는 함수는 backward가 모듈화되어 있는 것 같다.
    • pytorch에 없는 함수라도 도함수를 구할 수 있다면 직접 모듈로 만들어서 사용할 수도 있다.

 

03. MyTorch Autograd 구현

  • PyTorch의 autograd는 backpropagation을 자동으로 수행한다.
  • Mytensor로 정의한 데이터는 backward를 할 때 gradient를 계산하고 저장할 수 있게 된다.
  • Mytensor 클래스에는 각 연산의 도함수 구하는 방법을 미리 정의해 둔다.
import random
import math
import numpy as np
import plotly.express as px
import plotly.graph_objects as go 

 

03-1. pytorch 구현

1. pytorch 기본 핵심 구현

class Mytensor: #이름은 텐서지만 스칼라만 계산
    def __init__(self, data, _children=(), _op='', name=''): # _op와 name은 시각화하는 용도
        self.data = data # 숫자 자체
        self.grad = 0.0 # 노드의 gradient값 (dL/d현재노드)
	        # pytorch는 초기값을 None으로 하는 듯 하다.
        self._prev = set(_children) # 어떤 값끼리 연산을 했는지 기록 
	        # 같은 클래스는 중복으로 저장 안함
        self._backward = lambda: None # 빈 함수 생성 
	        # out클래스가 어떤 연산으로 나왔는지에 따라 달라짐 
	        # chilren의 grad를 계산
	      self._op = _op # 어떤 연산을 했는지 시각화할 때 표기하는 용도
        self.name = name # 텐서의 이름을 시각화하는 용도
    
    def __repr__(self): # Mytensor를 print했을 때 출력되는 값 정의
        return f"MyTensor({self.name}, data={self.data})"

# backward하고자 하는 함수들을 정의
    def __add__(self,other): # a+b == a.__add__(b)
	    # def __함수__는 파이썬 기본 함수를 수정하는 것으로 __add__같은 경우 "+" 연산자를 
	    # 사용해도 내가 정의한 __add__함수와 동일하게 동작한다.
        out = Mytensor(self.data + other.data, (self, other), '+') 
	        # 두 텐서를 연산한 값을 텐서클래스로 만듬	
        def _backward(): 
	        # add gate의 도함수를 구하는 방법을 정의
	        # add gate에서 local gradient는 1이다.
            self.grad += 1.0 * out.grad # = 대신 += 을 통해 gradient 축적 
	            # 같은 노드가 여러번 연산했을 때 =로 하면 문제 발생
	            # out.grad는 upstream gradient
            other.grad += 1.0 * out.grad 
            out.grad = 0.0 
	            # out 클래스의 gradient 초기화
	            # children노드의 gradient를 계산하고 나면 앞단의 gradient는 필요가 없다.
	            # out.grad를 초기화하지 않으면 업데이트를 반복할 때 gradient가 게속 쌓임
        out._backward = _backward 
	        # out클래스의 backward 함수로 저장 
	        # 함수를 실행하는 것이 아닌 일단 저장만 함 #feed forward가 끝나고 실행됨
	        # 현재 함수는 out클래스에 저장되지만, 함수의 self와 other은 children 노드를 지칭한다.
	        # out클래스의 backward함수는 children의 gradient를 계산하는 역할을 한다.
        return out
    
    def __mul__(self, other):
        out = Mytensor(self.data * other.data, (self, other), '*')

        def _backward():
            self.grad += other.data * out.grad 
            other.grad += self.data * out.grad 
            out.grad = 0.0
        out._backward = _backward
        return out
    
    def mse(self, y):
        loss = (y.data-self.data)**2
        out = Mytensor(loss, (self, y), 'mse')

        def _backward():
            self.grad += (-2*(y.data-self.data)) * out.grad
            # 정답 데이터는 업데이트하지 않음으로 생략
            out.grad = 0.0
        out._backward = _backward
        return out
    
    # 실습에서 나온 함수(덤) 
		def tanh(self): 
        x = self.data
        t = (math.exp(2*x)-1)/(math.exp(2*x)+1)
        out = MyTensor(t, (self,), 'tanh')

        def _backward():
            self.grad += (1 - t**2) * out.grad # = 대신 += 을 통해 gradient accumulation
        out._backward = _backward
        return out
   
# feed forward가 끝나고 맨 마지막 노드(Loss)에서부터 backward 수행
    def backward(self): 
        topology = [] # backward할 순서 -> 역순으로 봐야함
        visited = set()

        def build_topo(v): # backward할 순서 구축 함수 # 아래 간단한 예시 참고
            if v not in visited:
                visited.add(v)
                for child in v._prev:
                    build_topo(child)
                topology.append(v)

        build_topo(self)

        self.grad = 1.0 # 모든 연산을 거친 마지막 합성함수(Loss)의 gradient는 자기 자신과 미분해서 1
        for node in reversed(topology): # 마지막 합성함수에서 차례대로 leaf 노드까지 backward를 수행
            node._backward()
  • tanh 함수는 $(tanh(x) = \dfrac{e^x - e^{-x}}{e^x + e^{-x}})$ non linear activation function으로 input 값을 -1 ~ 1 사이의 값으로 변환시킨다. (실습에서 사용한 예제로 아래 코드에서는 사용하지 않는다) 

 

 

topology 알고리즘 간단 예시

# topology 알고리즘 간단 예시
# 줄기를 타고 내려가며 children이 없는 leaf 노드부터 계층 순서대로 topology 리스트에 담는다.
# topology를 역순으로 보면 computational graph의 backward할 순서가 된다.
class Node:
    def __init__(self, data, children=()):
        self.data = data
        self.children = children

a = Node(1)
b = Node(2)
c = Node(3)
d = Node(4, (a,b))
e = Node(5, (c,d))

topology = []
visited = []
def build(v):
    visited.append(v.data)
    # if v not in visited:는 생략
    for child in v.children: 
        build(child) # 재귀함수 # child 노드로 가서 다시 build 수행
        # child가 없는 노드가 나올 때까지 재귀. 
    # child가 없는 노드면 다음 코드 실행
    topology.append(v.data) # child노드가 없는 노드를 topology 리스트에 append
build(e)

print("topology:",topology,"\\n", "visited:", visited)
	# topology: [3, 1, 2, 4, 5] 
	#  visited: [5, 3, 4, 1, 2]

 

2. gradient를 계산할 때 self.grad += 1.0 * out.grad 처럼 +=로 하는 이유

1. +=가 아닌 =로 했을 때 a+a 경우 예시,

a = MyTensor(3.0, name='a')
b = a + a; b.name = 'b'
b.backward()
draw_dot(b)

  • $b=a+a$ 수식에서 $\dfrac{\partial b}{\partial a}=2$, 즉 grad는 2를 가져야 하지만, 1값이 나온다.
  • MyTensor *add* method의 _backward 함수를 보면 같은 node 두개가 서로 더해지는 경우, _backward 함수 안에서 self와 other은 같은 object에 해당된다. 이 경우 self.grad 값에 1.0out.grad 값이 assign된 후, 아래줄에서 다시 grad 값을 1.0out.grad 값으로 덮어씌운다.
  • def __add__(self, other): out = MyTensor(self.data + other.data, (self, other), '+') def _backward(): self.grad = 1.0 * out.grad other.grad = 1.0 * out.grad # self와 other이 같을 경우 값을 덮어 씌움 out._backward = _backward return out

2. +=가 아닌 =로 했을 때 노드 여러개가 서로 연결되어 있는 경우 예시,

a = MyTensor(-2.0, name='a')
b = MyTensor(3.0, name='b')
d = a * b; d.name='d'
e = a + b; e.name='e'
f = d * e; f.name='f'

f.backward()
draw_dot(f)

  • 해당 computational graph에서 a와 b의 gradient가 잘못된 것을 확인할 수 있다.
  • 계산을 해보면 a.grad = -3, b.grad = -8이란 것을 알 수 있다.

결론:

  • =로 되어있을 때 gradient 값을 한번 이상 업데이트할 경우 문제가 발생한다. gradient 값이 덮어씌워지기 때문이다. 이를 방지하기 위해서, gradient값은 assignment (=) 대신 **accumulation (+=)**이 수행되어야 한다.
  • 그런데 accumulation (+=)을 하게 되면 업데이트를 반복하는 과정에서 gradient가 누적되게 된다. 그렇기 때문에 필히 gradient를 초기화해 주어야 한다.
    • PyTorch 역시 .backward() 함수를 호출할 때 마다 gradient가 누적되고, 따라서 Optimizer.zero_grad()를 통해 매 학습 iteration마다 각 노드(텐서)에 저장된 gradient값을 0으로 초기화해야 한다.

3. computational graph 시각화

  • 굳이 알 필요 없음
# !conda install graphviz -> pip가 아닌 conda로 해야한다. pip로 설치시 오류
# 시각화 코드. 중요하지 않으며 알 필요 없음.
from graphviz import Digraph

# helper function
def trace(root):
    nodes, edges = set(), set()
    def build(v):
        if v not in nodes:
            nodes.add(v)
            for child in v._prev:
                edges.add((child, v))
                build(child)
    build(root)
    return nodes, edges

# helper function
def draw_dot(root, format='svg', rankdir='LR'):
    nodes, edges = trace(root)
    dot = Digraph(format=format, graph_attr={'rankdir': rankdir}) #, node_attr={'rankdir': 'TB'})

    for n in nodes:
        dot.node(name=str(id(n)), label = "{ %s | data %.4f | grad %.4f }" % (n.name, n.data, n.grad), shape='record')
        if n._op:
            dot.node(name=str(id(n)) + n._op, label=n._op)
            dot.edge(str(id(n)) + n._op, str(id(n)))

    for n1, n2 in edges:
        dot.edge(str(id(n1)), str(id(n2)) + n2._op)

    return dot

 

Linear regression과 MSE loss의 computational graph 시각화

w1 = Mytensor(1, name='w1') 
x1 = Mytensor(-2, name='x1')
w2 = Mytensor(3, name='w2')
x2 = Mytensor(2, name='x2')
b = Mytensor(3, name='b')
y = Mytensor(5, name='y')

wx1 = w1 * x1; wx1.name = 'wx1' # name은 시각화만을 위한 인자다.
	# ;은 코드의 종결을 의미하고 ;으로 한 줄에 여러개의 코드를 작성할 수 있다.
	# ;를 통해 함수의 인자를 넣어줄 수 있다.
wx2 = w2 * x2; wx2.name = 'wx2'
wx1_wx2 = wx1 + wx2; wx1_wx2.name = 'wx1+wx2'
wxb = wx1_wx2 + b; wxb.name = 'wx1+wx2+b'
L = wxb.mse(y)

L.backward()
draw_dot(L) # # draw_dot() 힘수로 시각화 가능

 

03-2. 학습 구현

1. 학습을 위한 데이터 생성

# 데이터 생성
# 2변수 linear regression에 맞는 데이터 생성
def data_create(w1, w2, b): 
    x1 = np.random.normal(0,20,80) # 정규분포에서 랜덤값 추출 (평균,분산,벡터크기)
    x2 = np.random.normal(0,20,80)
    y = (w1*x1) + (w2*x2) + b #+ np.random.normal(0,5,80)
    return y, x1, x2

y, x1, x2 = data_create(3,-2,10) # weight1, weight2, bias 
# 파라미터를 w1=3, w2=-2, w3=10 으로 설정

생성한 데이터 시각화

fig = px.scatter_3d(x=x1, y=x2, z=y) 

fig.update_layout(width=400,height=400)
fig.show()

 

2. Gredient Descent 함수 구현

# Gredient Descent 함수 정의
class GD:
    def __init__(self, w1, w2, b): # 업데이트하고자하는 파라미터를 입력으로 받음
        self.w1 = w1
        self.w2 = w2
        self.b = b
        self.w1_grads = [] # 평균을 구하기 위해 batch만큼 gradient를 축적
        self.w2_grads = []
        self.b_grads = []
    def collect_grad(self): # 그래디언트의 평균을 구하기 위해 그래디언트를 축적하는 함수
        self.w1_grads.append(self.w1.grad)
        self.w2_grads.append(self.w2.grad)
        self.b_grads.append(self.b.grad)
    def initialize(self): # 그래디언트 초기화
        self.w1.grad = 0.0
        self.w2.grad = 0.0
        self.b.grad = 0.0
    def optimize(self, alpha): # Gredient Descent 함수
        self.w1.data = self.w1.data - (alpha * np.mean(self.w1_grads))
        self.w2.data = self.w2.data - (alpha * np.mean(self.w2_grads))
        self.b.data = self.b.data - (alpha * np.mean(self.b_grads))
        
        # 축적된 그래디언트 초기화
        self.w1_grads = []
        self.w2_grads = []
        self.b_grads = []

 

3. 학습

  • weight가 2개, biass가 1개인 Linear regression과 MSE loss function 사용
w1 = Mytensor(random.random(), name='w1') 
w2 = Mytensor(random.random(), name='w2')
b = Mytensor(random.random(), name='b')

opt = GD(w1,w2,b) # 업데이트할 파라미터 입력 # 이 클래스에서 파라미터를 꺼내써야 한다.
learning_rate = 0.001 # 작아야 업데이트가 잘됨 -> 그래디언트 값이 생각보다 매우 큼, 0.005만 해도 수렴에서 멀리 벗어남
Total_loss = []
for epoch in range(1000): # epoch == 업데이트 횟수(full batch) # 초기값과 많이 떨어져 있을수록 많은 epoch이 요구됨
    _loss = []
    for i, j, g in zip(x1, x2, y): # 데이터가 차례대로 들어옴 # MSE를 하기 위한 for문
        opt.initialize() # 그래디언트 초기화
        # 각 데이터를 내가 정의한 클래스 형태로 만들어준다
        x1_t = Mytensor(i, name='x1') 
        x2_t = Mytensor(j, name='x2')
        y_t = Mytensor(g, name='y')

        # linear regression Feed forward
        wx1 = opt.w1 * x1_t; wx1.name = 'wx1' 
        wx2 = opt.w2 * x2_t; wx2.name = 'wx2'
        wx1_wx2 = wx1 + wx2; wx1_wx2.name = 'wx1+wx2'
        wxb = wx1_wx2 + opt.b; wxb.name = 'wx1+wx2+b'
        L = wxb.mse(y_t) # SSE loss function # mse로 계산하고자 하지만 모든 데이터의 loss를 평균내는 것은 그래디언트를 평균내는 것과 같다.
        
        # backward
        L.backward()
        _loss.append(L.data)
        opt.collect_grad() # 그래디언트의 평균 계산을 위해 그래디언트를 축적
    
    opt.optimize(learning_rate) # learning rate인자로 입력
    print("Loss:", np.sqrt(np.mean(_loss))) # RMSE loss
    Total_loss.append( np.sqrt(np.mean(_loss))) # 1epoch마다 RMSE loss값 축적
    _loss = [] # 축적된 loss 초기화

 

loss 시각화

# loss 시각화
fig = px.line(x=np.arange(len(Total_loss)), y=Total_loss, title='Loss')
fig.update_layout(width=600,height=400)
fig.show()

 

# 학습된 파라미터 값 출력
print(opt.w1.data, opt.w2.data, opt.b.data)
	# 2.9966518635894435 -1.9946348661990307 8.677659135065339
	# 데이터를 생성할 때 설정했던 w1=3, w2=-2, w3=10 과 근사
  • 실험 결과 파라미터의 초기값과 목표로 하는 파라미터 값 간의 차이가 크면 그만큼 업데이트도 오래 걸린다.
    • 업데이트를 오래하면 loss가 아주 조금씩이지만 꾸준히 줄어든다.
  • learning rate를 작게 하는 것이 매우 중요하다. 조금만 크게 해도 학습되지 않으며 loss가 기하급수적으로 커진다.
    • Loss가 큰 경우 접선의 기울기가 무한에 가깝게 클 수 있다. 이때 learning rate를 매우 작게 해주지 않으면 파라미터가 매우 큰 거리를 한번에 이동하기 때문에 발산해 버릴 수 있다.

 

4. 학습 결과 시각화

# 학습 결과 시각화

# weight가 2개인 모델은 2차원 평면의 형태임으로 사각형을 표현하기 위해 최소 4개의 꼭짓점이 필요
x1_space = np.array([-50, -50, 50, 50])  
x2_space = np.array([-50, 50, -50, 50])
y_space = (x1_space*opt.w1.data) + (x2_space*opt.w2.data) + opt.b.data 

fig = go.Figure()
fig.add_trace(go.Mesh3d(x=x1_space, y=x2_space, z=y_space, opacity=0.50)) # 모델 시각화
fig.add_trace(go.Scatter3d(x=x1, y=x2, z=y, mode='markers')) # 데이터 시각화

fig.update_layout(width=500,height=500)
fig.show()

04. PyTorch Autograd 비교

  • 공부를 위해 하는 비교일 뿐 실제 활용도는 없으니 몰라도 된다.
  • PyTorch는 위에서 학습한 모든 기능들을 가지고 있고, 이를 매우 편리하게 사용할 수 있도록 한다.
  • Tensor.requires_grad=True를 통해 PyTorch Tensor가 gradient를 저장하도록 할 수 있다. (참고: torch.tensor)
    • 메모리를 차지하고 속도가 느려지기 때문에 실제로 쓸 일은 거의 없다.
    • 일반적으로 network input에 대해서는 gradient를 계산할 필요가 없기 때문에 기본값은 False로 되어있지만, 이를 True로 바꾼 후에 autograd를 활용할 수 있다.
  • Tensor.grad를 통해 해당 텐서의 gradient를 반환할 수 있다. 기본적으로 None으로 초기화 되고, backward function을 실행할 경우 해당 값을 얻을 수 있다.
  • 아래 셀의 결과 MyTorch 결과와 PyTorch 결과가 동일함으로 확인할 수 있다.
# MyTorch 결과 (recap)

x1 = MyTensor(2.0, name='x1')
x2 = MyTensor(0.0, name='x2') # inputs x1, x2

w1 = MyTensor(-3.0, name='w1')
w2 = MyTensor(1.0, name='w2') # weights w1, w2

b = MyTensor(6.881373, name='b') # bias

x1w1 = x1*w1; x1w1.name = 'x1*w1'
x2w2 = x2*w2; x2w2.name = 'x2*w2'
x1w1x2w2 = x1w1 + x2w2; x1w1x2w2.name = 'x1*w1 + x2*w2'

# x1w1 + x2w2 + b
n = x1w1x2w2 + b; n.name = 'n'

o = n.tanh(); o.name = 'o'

o.backward() # output node에 대해 backward 함수 실행

draw_dot(o)
# 이미지 출력 생략
import torch

# x1 = torch.Tensor([2.0]).double(); x1.requires_grad = True
x1 = torch.Tensor([2.0], dtype=torch.double, requires_grad = True) 
    # 위와 같이 정의하는 것이 동일한 연산인데 더 빠르다. 
    # 예시에서 처럼 ;로 인자를 넣게 되면 연산을 여러번 해야헤서 느려진다.
x2 = torch.Tensor([0.0]).double(); x2.requires_grad = True
w1 = torch.Tensor([-3.0]).double(); w1.requires_grad = True
w2 = torch.Tensor([1.0]).double(); w2.requires_grad = True
b = torch.Tensor([6.881373]).double(); b.requires_grad = True
n = x1*w1 + x2*w2 + b
o = torch.tanh(n)

print(o)
print()
print('x1.grad: ', x1.grad)
print('w1.grad: ', w1.grad)
print('x2.grad: ', x2.grad)
print('w2.grad: ', w2.grad)
print('b.grad: ', b.grad) # gradient가 none으로 초기화
print()

# backpropagation
o.backward()
print('x1.grad: ', x1.grad)
print('w1.grad: ', w1.grad)
print('x2.grad: ', x2.grad)
print('w2.grad: ', w2.grad)
print('b.grad: ', b.grad)

'''
tensor([0.7071], dtype=torch.float64, grad_fn=<TanhBackward0>)

x1.grad:  None
w1.grad:  None
x2.grad:  None
w2.grad:  None
b.grad:  None

x1.grad:  tensor([-1.5000], dtype=torch.float64)
w1.grad:  tensor([1.0000], dtype=torch.float64)
x2.grad:  tensor([0.5000], dtype=torch.float64)
w2.grad:  tensor([0.], dtype=torch.float64)
b.grad:  tensor([0.5000], dtype=torch.float64)
'''

텐서를 print할 경우, 해당 텐서의 data item과 type 외에 operation(grad_fn)을 확인할 수 있다. 혹은 Tensor.grad_fn으로 직접 출력할 수 있다.

print(n)
print(n.grad_fn)
	#tensor([0.8814], dtype=torch.float64, grad_fn=<AddBackward0>) 
	#<AddBackward0 object at 0x7a52be3c5150>
  • grad_fn=<AddBackward0> → 해당 텐서가 computational graph에서 연산되는 함수

MyTorch 구현과 동일하게, PyTorch에서도 leaf node(tensor)의 operation은 None인 것을 확인할 수 있다.

print(x1)
print(x1.grad_fn)
	# tensor([2.], dtype=torch.float64, requires_grad=True)
	# None

PyTorch에서도 retain_graph=True를 이용하면 gradient accumulation을 할 수 있다.

  • 기본 두 번 이상 backward를 하면 자동으로 오류를 출력하도록 만들었다. 그러나 retain_graph=True 를 통해 강제로 gradient를 누적되도록 만들 수 있다.

retain_graph=True를 사용할 일은 거의 없지만 gradient accumulation은 large model들을 학습시킬 때 자주 사용되므로 grad를 초기화 해주기 전까지 gradient accumulation이 된다는 사실은 알고 있으면 좋다.

import torch

x1 = torch.Tensor([2.0]).double(); x1.requires_grad = True
x2 = torch.Tensor([0.0]).double(); x2.requires_grad = True
w1 = torch.Tensor([-3.0]).double(); w1.requires_grad = True
w2 = torch.Tensor([1.0]).double(); w2.requires_grad = True
b = torch.Tensor([6.881373]).double(); b.requires_grad = True
n = x1*w1 + x2*w2 + b
o = torch.tanh(n)

# backpropagation
o.backward(retain_graph=True)
print('x1.grad: ', x1.grad)
print('w1.grad: ', w1.grad)
print('x2.grad: ', x2.grad)
print('w2.grad: ', w2.grad)
print('b.grad: ', b.grad)
print()

o.backward(retain_graph=True)
print('One more...')
print('x1.grad: ', x1.grad)
print('w1.grad: ', w1.grad)
print('x2.grad: ', x2.grad)
print('w2.grad: ', w2.grad)
print('b.grad: ', b.grad)
print()

o.backward(retain_graph=True)
print('One more...')
print('x1.grad: ', x1.grad)
print('w1.grad: ', w1.grad)
print('x2.grad: ', x2.grad)
print('w2.grad: ', w2.grad)
print('b.grad: ', b.grad)
print()

'''
x1.grad:  tensor([-1.5000], dtype=torch.float64)
w1.grad:  tensor([1.0000], dtype=torch.float64)
x2.grad:  tensor([0.5000], dtype=torch.float64)
w2.grad:  tensor([0.], dtype=torch.float64)
b.grad:  tensor([0.5000], dtype=torch.float64)

One more...
x1.grad:  tensor([-3.0000], dtype=torch.float64)
w1.grad:  tensor([2.0000], dtype=torch.float64)
x2.grad:  tensor([1.0000], dtype=torch.float64)
w2.grad:  tensor([0.], dtype=torch.float64)
b.grad:  tensor([1.0000], dtype=torch.float64)

One more...
x1.grad:  tensor([-4.5000], dtype=torch.float64)
w1.grad:  tensor([3.0000], dtype=torch.float64)
x2.grad:  tensor([1.5000], dtype=torch.float64)
w2.grad:  tensor([0.], dtype=torch.float64)
b.grad:  tensor([1.5000], dtype=torch.float64)
'''