본문 바로가기

딥러닝

미분 자동 계산 (4)

1. 구현 간소화

미분 자동 계산 (3) 에서 어떤 특정 연산을 수행하는 함수를 정의한 파이썬 클래스(Square, Exp 클래스)를 이용하여 순전파를 수행하기 위해서는, 다음과 같이 먼저 클래스의 인스턴스를 생성한 다음, 생성한 인스턴스를 호출해야 한다.

class Square(Function):
    def forward(self, x):
        y = x ** 2
        return y

    def backward(self, gy):
        x = self.input.data
        gx = 2 * x * gy 
        return gx
        
x = Variable(np.array(2.0))
f = Square() # 클래스 인스턴스 생성
y = f(x) # 호출

■ 이 과정은 파이썬의 사용자 정의 함수(def)를 이용하여 클래스의 인스턴스 생성과 호출을 한 번에 처리하도록 만들 수 있다.

■ 그리고 역전파 수행 시, \( \frac{dy}{dy] = 1 \)을 전달하기 위해 y.grad = np.array(1.0)을 정의하여 사용하였다.

이 부분은 Variable 클래스의 backward 메서드에 역전파 초기에 전달할 초깃값을 1로 설정하게끔 수정하면 된다.


1.1 특정 연산을 수행하는 과정 간소화

1.1.1 파이썬의 클래스(class) 대신, 사용자 정의 함수(def)로 구현

■ 다음과 같이 클래스로 정의된 것을, 다음과 같이 파이썬의 사용자 정의 함수(def)로 감싸서 구현하면, 함수 호출 시 클래스의 인스턴스 생성과 호출을 한 번에 처리할 수 있다.

def square(x):
    f = Square() # 클래스 인스턴스 생성
    return f(x) # 인스턴스 호출

def exp(x):
    f = Exp() # 클래스 인스턴스 생성
    return f(x) # 인스턴스 호출
    
또는 다음과 같이 한 줄로 작성할 수 있다.
def square(x):
    return Square()(x)

■ 이렇게 구현하면, 다음과 같이 단순히 함수를 한 번 호출하는 것만으로도 연산을 수행하는 클래스의 인스턴스 생성과 호출을 한 번에 처리할 수 있다.

x = Variable(np.array(2.0))
u = square(x) # 클래스 인스턴스 생성과 호출을 한 번에 처리
v = exp(u) # 클래스 인스턴스 생성과 호출을 한 번에 처리
y = square(v)

■ 이렇게 클래스의 인스턴스 생성과 호출을 한 번에 처리하기 때문에, 다음과 같이 함수를 연속으로 적용할 수 있다. 즉, 합성 함수의 형태로 나타낼 수 있다.

x = Variable(np.array(2.0))
y = square(exp(square(x))) # 연속 적용 # 합성 함수 형태

1.2 backward 메서드 간소화

다음과 같이 Variable 클래스의 backward 메서드를 수정하면, y.grad = np.array(1.0)이라는 코드를 별도로 작성하지 않아도 된다.

class Variable:
    def __init__(self, data):
        self.data = data
        self.grad = None
        self.creator = None

    def set_creator(self, func):
        self.creator = func

    def backward(self):
        if self.grad is None: 
            self.grad = np.ones_like(self.data)
            
        funcs = [self.creator]
        while funcs:
            f = funcs.pop() 
            x, y = f.input, f.output 
            x.grad = f.backward(y.grad) 

            if x.creator is not None: funcs.append(x.creator)

- 변수의 grad 값이 없다면(self.grad is None), 자동으로 미분값을 생성한다. 이때, self.grad는 np.ones_like(self.data)를 통해 만들어진다.

- 넘파이의 ones_like() 함수는 모든 요소가 1로 채워졌으며 지정한 배열과 동일한 크기와 데이터 타입을 가지는 배열을 생성해준다. 그러므로 self.data와 같은 형상 및 데이터 타입이 같은  self.grad가 생성된다.

x = Variable(np.array(2.0))
y = square(exp(square(x))) 
y.backward()

print(x.grad)
```#결과#```
23847.663896333823
````````````

■ 하지만, 다음과 같이 변수의 data 속성에 저장된 값은 numpy.ndarray인데 역전파로 계산된 변수의 grad 속성에는 numpy.float64로 달라지는 것을 볼 수 있다.

print(x.data.__class__)
print(x.grad.__class__)
```#결과#```
<class 'numpy.ndarray'>
<class 'numpy.float64'>
````````````

■ 이런 현상이 발생하는 이유는 입력으로 사용한 np.array(2.0)이 0차원의 ndarray이기 때문이다. 

x = np.array(2.0)
print(type(x)); print(x.ndim)
```#결과#```
<class 'numpy.ndarray'>
0
````````````

■ 다음과 같이 입력으로 1차원 ndarray를 사용하면, self.data와 self.grad가 동일한 형상과 데이터 타입을 가진다.

x = np.array([2.0])
print(type(x)); print(x.ndim)
```#결과#```
<class 'numpy.ndarray'>
1
````````````
x = Variable(np.array([2.0]))
y = square(exp(square(x))) 
y.backward()

print(x.data.__class__)
print(x.grad.__class__)
```#결과#```
<class 'numpy.ndarray'>
<class 'numpy.ndarray'>
````````````

print(x.data.shape == x.grad.shape)
```#결과#```
True
````````````

■ 즉, 0차원 ndarray를 사용하여 계산하면 결과의 데이터 타입이 numpy.float64나 numpy.float32 등으로 달라질 수 있다. 그러므로, 항상 self.data가 numpy.ndarray를 다루도록 강제하는 것이 안전한 방법이다.

 



2. Variable 클래스 데이터 타입


2.1 Variable 클래스에서 데이터 타입으로 ndarray 인스턴스만 허용

■ Variable 클래스를 ndarray 인스턴스만을 담는 '상자'로 만들려면, 먼저 다음과 같이 float이나 int 등의 다른 데이터 타입이 들어오지 않도록 TypeError를 설정할 수 있다. 

class Variable:
    def __init__(self, data):
        if data is not None:
            if not isinstance(data, np.ndarray): 
                raise TypeError(f'{type(data)} data must be a ndarray')
                
        self.data = data
        self.grad = None
        self.creator = None

    def set_creator(self, func):
        self.creator = func

    def backward(self):
        if self.grad is None: 
            self.grad = np.ones_like(self.data)
            
        funcs = [self.creator]
        while funcs:
            f = funcs.pop() 
            x, y = f.input, f.output 
            x.grad = f.backward(y.grad) 

            if x.creator is not None: funcs.append(x.creator)

- 인수로 주어진 data가 None이 아닐 때, 

- if not isinstance(data, np.ndarray):는 data가 numpy.ndarray 타입인지 확인한다. 

-- 인스턴스가 어떤 클래스 또는 데이터 타입인지 확인하는 함수가 isinstance이다. isinstance(인스턴스, 클래스/데이터 타입)으로 인스턴의 클래스 또는 데이터 타입이 일치하면 True, 아니면 False를 반환한다.

- data가 numpy.ndarray 타입이 아니라면 if문이 True가 되어 if 블록인 TypeError를 발생시킨다.

-- isinstance 함수 앞에 not을 붙였으므로, 클래스 또는 데이터 타입이 일치하지 않으면 not Flase = True가 반환되어 if 블록문을 실행하고, 일치하면 False이므로 if 블록문을 실행하지 않는다.

x = Variable(np.array(2.0))
print(x.data)
```#결과#```
2.0
````````````

x = Variable(None)
print(x.data)
```#결과#```
None
````````````

x = Variable(2.0)
```#결과#```
TypeError                                 Traceback (most recent call last)
Cell In[516], line 1
----> 1 x = Variable(2.0)

Cell In[506], line 5, in Variable.__init__(self, data)
      3 if data is not None:
      4     if not isinstance(data, np.ndarray): 
----> 5         raise TypeError(f'{type(data)} data must be a ndarray')
      7 self.data = data
      8 self.grad = None

TypeError: <class 'float'> data must be a ndarray
````````````

- 데이터 타입이 ndarray나 None이면 TypeError가 발생하지 않지만, 다른 데이터 타입을 입력하면 예외가 발생하는 것을 확인할 수 있다.

■ 그다음, 1.2에서 확인한 문제점으로 0차원 ndarray 인스턴스를 사용하면 계산 결과는 다른 데이터 타입을 가지는 문제가 있었다. 현재 Variable의 데이터는 항상 ndarray 인스턴스라고 가정하고 있지만, 다음과 같이 y = square(exp(square(x)))를 계산할 경우, Variable 클래스의 인스턴스인 y의 데이터 타입은 numpy.float64로 반환되는 것을 볼 수 있다.

x = Variable(np.array(2.0))
y = square(exp(square(x))) 
y.backward()
```#결과#```
TypeError                                 Traceback (most recent call last)
Cell In[533], line 2
      1 x = Variable(np.array(2.0))
----> 2 y = square(exp(square(x))) 
      3 y.backward()

Cell In[531], line 3, in square(x)
      1 def square(x):
      2     f = Square() # 클래스 인스턴스 생성
----> 3     return f(x)

Cell In[99], line 5, in Function.__call__(self, input)
      3 x = input.data
      4 y = self.forward(x)
----> 5 output = Variable(y)
      6 output.set_creator(self) 
      7 self.input = input

Cell In[506], line 5, in Variable.__init__(self, data)
      3 if data is not None:
      4     if not isinstance(data, np.ndarray): 
----> 5         raise TypeError(f'{type(data)} data must be a ndarray')
      7 self.data = data
      8 self.grad = None

TypeError: <class 'numpy.float64'> data must be a ndarray
````````````

- 에러를 확인하면, y = square(exp(square(x)))에서 문제가 발생했으며, 이는 Function 클래스를 상속받은 Square클래스의 output = Variable(y)가 TypeError의 원인인 것을 확인할 수 있다.

- 즉, output = Variable(y)을 수정해야 된다.

■ 이 문제는, 현재 Variable의 데이터는 항상 ndarray 인스턴스라고 가정하고 있으므로, 다음과 같은 함수를 정의하여 문제가 되는 output = Variable(y)에 적용하면 된다.

def as_array(x): # 함수의 입력은 x
    if np.isscalar(x): # 입력 x가 스칼라 타입인지 확인 
        return np.array(x) # x가 스칼라 타입이면 ndarray 인스턴스로 변환
    return x # 입력 x가 스칼라 타입이 아니면 그대로 반환

■ as_array( ) 함수는 입력 데이터 x가 numpy.float64같은 스칼라 타입인지 확인해서, 스칼라 타입이 아니면 입력 데이터를 그대로 반환하고, 스칼라 타입이면 입력 데이터를 ndarray 인스턴스로 변환하여 반환해주는 함수이다. 

■ 이 함수를 다음과 같이 Function 클래스의 output = Variable(y)에 적용해 주면 된다.

그리고 Variable 클래스에서 x = Variable(np.array(2.0))처럼, 0차원 ndarray를 입력으로 한 경우, x.grad를 계산할 때 f.backward(y.grad)의 계산 결과로numpy.ndarray가 아닌 numpy.float64 등의 다른 데이터 타입이 할당될 수 있다. 

■ x.grad = f.backward(y.grad) 라인에서 ndarray 타입으로의 변호나이 강제되지 않기 때문이다.

그러므로 다음과 같이 x.grad를 계산하는 부분에도 as_array( ) 함수를 적용해야 한다.

class Variable:
    def __init__(self, data):
        if data is not None:
            if not isinstance(data, np.ndarray): 
                raise TypeError(f'{type(data)} data must be a ndarray')
                
        self.data = data
        self.grad = None
        self.creator = None

    def set_creator(self, func):
        self.creator = func

    def backward(self):
        if self.grad is None: 
            self.grad = np.ones_like(self.data)
            
        funcs = [self.creator]
        while funcs:
            f = funcs.pop() 
            x, y = f.input, f.output 
            x.grad = as_array(f.backward(y.grad)) 
            if x.creator is not None: funcs.append(x.creator)
class Function:
    def __call__(self, input):
        x = input.data
        y = self.forward(x)
        output = Variable(as_array(y))
        output.set_creator(self)
        self.input = input
        self.output = output
        return output

    def forward(self, x):
        raise NotImplementedError()

    def backward(self, gy):
        raise NotImplementedError()
x = Variable(np.array(2.0)) # 0차원 ndarray를 입력 변수의 데이터로 사용
y = square(exp(square(x))) 
y.backward()

print(x.data.__class__)
print(x.grad.__class__)
```#결과#```
<class 'numpy.ndarray'>
<class 'numpy.ndarray'>
````````````

print(x.data.shape == x.grad.shape)
```#결과#```
True
````````````

 

'딥러닝' 카테고리의 다른 글

가변 길이 인수  (0) 2025.03.26
미분 자동 계산 (3)  (0) 2025.03.13
미분 자동 계산 (2)  (0) 2025.03.11
미분 자동 계산 (1)  (0) 2025.03.07
트랜스포머 (Transformer) (3)  (0) 2025.01.10