밑시딥3_역전파_구현_솔루션

oogie·2023년 1월 31일
0

밑바닥부터 시작하는 딥러닝3는 현대 딥러닝 프레임워크와 유사한 DeZero 프레임워크를 파이썬으로 직접 구현하는 책이다.

책에서 단계별로 필요한 기능을 구현하는데, 아래 코드는 10단계의 마지막 코드로 실제 공부를 하시는 분들은 1단계부터 직접 고민하고 짜보는걸 추천한다.

첫 번째 파트는 Variable 클래스를 만드는 파트이다.

Variable은 우리가 만드는 DeZero 프레임워크의 데이터 타입이라고 볼 수 있다.

pytorch의 tensor 같은 역할이라고 볼 수 있다.

기본적으로 DeZero는 numpy ndarray로 연산을 수행한다.
따라서 Variable 내에서 ndarray가 아닌 data type에 대해 예외처리한다.

  1. as_array()함수는 numpy scalar를 ndarray로 변환하는 함수인데,
    이는 numpy 연산의 결과로 scalar값이 나올 때 ndarray를 scalar로 변환하기 때문에 필요하다.
    e.g.2(ndarray)**3 = 8(np.float64) -> 8를 ndarray로 변환해야함
  1. self.data : 클래스의 ndarray로 저장하는 값에 해당하는 부분이다. 가령 input이 2라고 하면 이 input또한 Variable 객체인데 이 2라는 값이 self.data에 해당한다.
    self.grad : data의 loss에 대한 gradient를 저장한다.
    오차 역전파 과정 상 각 데이터에 대해서 gradient를 추적하고 저장하고 있어야함을 알 수 있는데 이를 클래스 변수로 저장한다.
    (오차역전파 정리글)
    self.creator : 해당 변수를 낳는 함수라는 의미에서 creator라고 지칭한다.
    계산그래프로 봤을 때, 변수와 함수의 creator 관계는 다음과 같다.
    self.creator를 씀으로써 해당 변수를 낳은 이전 함수를 추적할 수 있다.
    set_creator로 creator를 지정하는데 이때 인자 func은 이후 설명할 Function 클래스 객체이다.
  2. backward() : 역전파를 자동화하는 메소드다. 기본적인 생각은 creator를 리스트로 집어넣어서 빈 리스트가 될 때까지(더 이상 이전의 연산이 없을 때까지) chain rule로 grad를 구하고 저장한다.
    이를 구현하기 위해 우선 self.grad가 None일때는 1로 지정한다.
    이는 첫 번째 gradient(dL/dL에 해당)를 자동으로 1로 설정하기 위함이다. ones_like를 사용하는 이유는 data와 같은 type,shape를 가져가기 위해서이다. 그리고 함수 리스트 funcs를 만드는데 첫 번째 원소는 self.creator로 지정한다. 그리고 funcs가 빈리스트가 아닌 동안 funcs.pop()으로 가장 최근의 Function 객체를 f로 지정한다.
    해당 객체의 input,output 변수를 x,y로 지정하고 y의 grad를 이용해 x의 grad를 구한다. (이에 대한 구현은 Function 클래스 참조)
    그리고 creator가 존재한다면(이전 연산이 존재한다면) 그 연산을 funcs리스트에 추가함으로서 구현한다.
import numpy as np


def as_array(x): #제곱등의 넘파이 연산시 ndarray에서 np float 64등으로 바뀌게 되는데 이를 방지하기 위해서 as_array선언
                 #Function 클래스의 Forward결과를 Variable 객체로 상속할때 사용한다. Forward에 사용되는 넘파이 연산이 스칼라값을 반환하기 때문
    if np.isscalar(x):
        return np.array(x)
    else: 
        return x

class Variable():
    def __init__(self,data):
        self.data = data
        if data is not None: 
            if not isinstance(data,np.ndarray): #ndarray타입만 지원하기 위한 예외처리 
                raise TypeError(f"{type(data)}는 지원하지 않습니다.")
        self.grad = None
        self.creator = None #계산 그래프상 이전 함수를 follow up 하기 위한 클래스 변수
    def set_creator(self,func):
        self.creator = func #해당 변수를 만든 함수 객체를 지정해주는 부분
    def  backward(self):
        if self.grad is None: #dy/dy는 무조건 1인데 이를 연산시에 지정하고 싶지 않으므로 이렇게 지정해둔다
            self.grad = np.ones_like(self.data) #ones_like를 쓰는 이유는 데이터 타입까지 따라가기 위해서
        #위 코드의 구체적 설명 : 현재 코드는 y라는 아웃풋을 가지고 함수의 backward()를 이용해 x라는 인풋의 grad를 구하면서 역전파를 수행
        #근데 가장 먼저 알아야하는 grad인 dy/dy의 경우 사실 무조건 1이다. 이를 따로 지정하지 않으면 y라는 객체를 처음 생성했을 때 grad가 None이 되는데 (Variable의 init 참조)
        #이 값이 무조건 1이 되어야하므로 None인 경우에 grad를 1로 지정한다. 이 dy/dy에 해당하는 grad를 제외하고는 chain rule에 의해 None이 아닌 가지게 되므로 원하는 역할을 수행한다.

        funcs = [self.creator] #함수를 리스트로 저장해서 역전파 구현
        while funcs: #이전에 연산한 함수가 있느 동안 계속 실행
            f = funcs.pop() #존재하는 가장 최근의 연산을 pop해서 지정
            print(f)
            x = f.input #해당 함수로의 인풋값
            y = f.output #해당 함수에서의 결과값 지정
            x.grad = f.backward(y.grad) #Function 객체에서의 backward
            if x.creator is not None: #만약 이전 함수가 있다면 즉 추가로 역전파할 함수가 있다면
                funcs.append(x.creator) #해당 함수를 funcs리스트에 추가

다음은 Function 클래스에 대한 설명이다.

Function 클래스를 함수의 연산과 미분을 구현한다.

이때 기본적인 큰 틀인 Function이라는 함수 백본을 만들고, 이를 상속받아서 구체적인 연산과 미분을 담는 식으로 구현한다

  1. 백본 : 클래스를 불렀을 때 작동할 수 있도록 call 메소드를 사용하며 인자로 input을 받는데, 이는 Variable 객체이다.
    따라서 연산을 수행할 때는 input.data를 x에 할당하고 이를 forward에 통과시킨다.
    이 forward가 상속받아서 구현하는 함수의 구체적 내용이다.
    통과시킨 결과를 Variable 객체로 만들고 이 객체의 creator는 self를 준다.
    output이라는 객체를 만드는데 현재 Function 객체를 사용한 것이므로 self를 creator로 지정한다.
    또한 함수 상 input과 output도 클래스 변수로 저장해준다.
    이를 통해 Function 객체만을 가지고 chain rule을 적용하는 Variable 클래스의 backward 메소드를 구현할 수 있다.
    forward,backward : 백본상에서는 forward와 backward에 대한 내용을 작성하지 않는다.
  2. Square,Exp : 이 두개의 클래스는 단순하게 제곱,exp를 수행하는 클래스인데 중요한 것은 이러한 연산을 forward에 구현하고 backward에서는 이에 대한 미분 계산을 구현한다.
    즉 forward에서 x^2를 구현했다면 backward에서는 이것의 미분인 2*x를 구현한다.
    backward를 수치 미분으로 구현하면 컴퓨터상 발생하는 수치적 오차가 발생하므로 직접 forward의 내용에 맞춰 구현한다.
  3. square(),exp() : Function 객체의 선언과 사용을 간단하게 하기 위해서 만든 함수이다.
class Function: #순전파와 역전파를 구현하는 클래스
    def __call__(self,input): #Variable 객체를 인자로 받는다
        x = input.data
        y = self.forward(x) #forward의 결과를 y로 저장
        output = Variable(as_array(y)) #이를 ndarray로 바꾸고 Variable객체 output 생성
        output.set_creator(self) #이 생성된 output의 creator(함수)는 자기자신이므로 self를 지정
        self.input = input #input
        self.output = output #output을 클래스 변수로 지정해두는데 이는 역전파를 할때 용이하게 하기 위함
        return output

    def forward(self,x): #forward와 backward의 구체적 내용은 상속을 통해 지정한다
        raise NotImplementedError()

    def backward(self,gy):
        raise NotImplementedError()


#Function 클래스를 상속받는다. 구현하고 싶은 함수의 내용을 forward, 그 forward의 미분 공식을 backward에 구현한다.
class Square(Function): 
    def forward(self,x):
        return x**2
    def backward(self,gy):
        return gy*(self.input.data)*2
    

class Exp(Function):
    def forward(self,x):
        return np.exp(x)
    def backward(self,gy):
        return gy*np.exp(self.input.data)

def square(x): #함수로 간소화
    return Square()(x) #function 객체를 반환함은 여전하다

def exp(x):
    return Exp()(x)

여기까지가 1고지의 내용이다.

아직 0차원 ndarray계산이고, 행렬곱등 딥러닝에서 필요한 연산들이 나오지 않았지만 오차역전파 자동화라는 중요한 과정을 구현했다.

이후 내용에서 나머지 단계들을 어떻게 구현할지 궁금해진다.

0개의 댓글