페이지

2022년 8월 13일 토요일

9.3 ndarray만 취급하기

 DeZero의 Variable은 데이터로 ndarray 인스턴스만 취급하게끔 의도했습니다. 하지만 사용하는 사람이 모르고 float나 int같은 의도치 않은 데이터 타입을 사용하는 일도 충분히 일어날 수 있습니다. 예컨대 Variable(1.0)혹은 Variable(3)처럼 사용할 수도 있겠죠, 이런 사태를 막기 위해 Variable의 ndarray 인스턴스만을 담는 '상자'가 되도록 고민을 해봤습니다. 그래서 Variable에 ndarray인스턴스 외의 데이터를 넣을 경우 즉시 오류를 일으키리로 했습니다(미분값은 None으로 유지합니다). 이렇게 하면 문제를 조기에 발견할 수 있겠지요. 자, 우선 Variable클래스의 초기화 부분에 다음 코드를 추가합니다.

class Variable:
  def __init__(selfdata):
    if data is not None:
      if not isinstance(data, np.ndarray):
        raise TypeError('{}은(는) 지원하지 않습니다.' .format(type(data)))
    
    self.data = data
    self.grad = None
    self.creator = None

이와 같이 인수로 주어진 data가 None이 아니고 ndarray 인스턴스도 아니라면 TypeError라는 예외를 발생시킵니다. 이때 오류 메시지로 출력할 문장도 준비합니다. 이제 새로워진 Variable을 사용해봅시다.

x = Variable(np.array(1.0)) # OK
x = Variable(None)          # OK

x = Variable(1.0)           # NG: 오류 발생!

TypeError                                 Traceback (most recent call last)
<ipython-input-11-b6a0c1e73929> in <module>()
      2 x = Variable(None)          # OK
      3 
----> 4 x = Variable(1.0)           # NG: 오류 발생!

<ipython-input-10-4b32df2c7112> in __init__(self, data)
      3     if data is not None:
      4       if not isinstance(data, np.ndarray):
----> 5         raise TypeError('{}은(는) 지원하지 않습니다.' .format(type(data)))
      6 
      7     self.data = data

TypeError: <class 'float'>은(는) 지원하지 않습니다.

보는 바와 같이 ndarray나 None이면 아무 문제가 없지만, 다른 데이터 타입을 입력하면 (앞의 예에서는 float)예외가 발생합니다. 덕분에 잘못된 데이터 타입을 사용했음을 즉시 알수 있습니다.

그런데 이렇게 바꾸면 주의할 게 하나 생깁니다. 넘파이의 독특한 관례 때문인데요. 다음 코드를 보면서 설명하겠습니다.


x = np.array([1.0])
y = x ** 2
print(type(x), x.ndim)
print(type(y))


<class 'numpy.ndarray'> 1 <class 'numpy.ndarray'>

여기서 x는 1차원 ndarray입니다. 여기에 제곱 (x ** 2)을 하면 y의 데이터 타입도 ndarray가 됩니다. 예상대로의 결과죠. 문제가 되는 것은 다음 경우입니다.

x = np.array(1.0)   # 0차원 ndarray
y = x ** 2
print(type(x), x.ndim)
print(type(y))

<class 'numpy.ndarray'> 0 <class 'numpy.float64'>

여기서 x는 0차원의 ndarray인데, 제곱(x ** 2)을 하면 np.float64가 되어버립니다. 이상해보일지 모르지만 넘파이가 의도한 동작입니다. 즉, 0차원 ndarray인스턴스를 사용하여 계산하면 결과의 데이터 타입이 numpy.float64나 numpy.float32 등으로 달라집니다. 다시말해 DeZero 함수의 계산 결과(출력)도 numpy.float64나 numpy.float32가 되는 경우가 나옵니다. 그러나 우리 Variable은 데이터가 항상 ndarray 인스턴스라고 자정하고 있으니 대처를 해줘야 합니다. 이를 위해 우선 다음과 같은 편의 함수를 준비합니다.

def as_array(x):
  if np.isscalar(x):
    return np.array(x)
  return x


여기에 쓰인 np.isscalar는 입력 데이터가 numpy.float64 같은 스칼라 타입인지 확인해주는 함수입니다(파이썬의 int와 float 타입도 스칼라로 판단합니다). 다음은 np.isscalar함수를 사용하는 예입니다.

import numpy as np
np.isscalar(np.float64(1.0))
True
np.isscalar(2.0)
True
np.isscalar(np.array(1.0))
False
np.isscalar(np.array([123]))
False

이처럼 x가 스칼라 타입인지 쉽게 확인할 수 있으며, as_array함수는 이를 이용하여 입력이 스칼라인 경우 ndarray인스턴스로 변환해줍니다. 이제 as_array라는 편의 함수가 준비되었으니 Function 클래스에 다음의 음영 부분 코드를 추가합니다.

class Function:
  def __call__(selfinput):
    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(selfx):
    raise NotImplementedError()

이와 같이 순전파의 결과인 y를 Variable 로 감쌀때 as_array()를 이용합니다. 이렇게 하여 출력 결과인 output은 항상 ndarray 인스턴스가 되도록 보장하는 것이죠. 이제 0차원 ndarray인스턴스를 사용한 계산에서도 모든 데이터는 ndarray인스턴스이니 안심해도 좋습니다.

이상으로 이번 단계에서 할 일을 마쳤습니다. 다음 단계의 이야기 주제는 DeZero의 '테스트'입니다.


댓글 없음: