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 |