페이지

2022년 8월 6일 토요일

STEP3 함수 연결

 지금까지 DeZero의 '변수'와 '함수'를 만들어봤습니ㅏㄷ. 그리고 2단계에서는 Square라는 제곱계산용 함수 클래스를 구현했습니다. 이번 단계에서는 또 다른 함수를 구현하고 여러 함수를 조합해 계산할 수 있도록 하겠습니다.

2.3 Function 클래스 이용

 Function 클래스를 실제로 사용해보죠. Variable 인스턴스인 x를 Function인스턴스인 f 에 입력해보겠습니다.

x = Variable(np.array(10))
f = Function()
y = f(x)

print(type(y)) # type() 함수는 객체의 클래스를 알려준다.
print(y.data)

이와 같이 Variable과 Function을 연계할 수 있습니다. 실행 결과를 보면 y의 클래스는 Variable이며, 데이터는 y.data에 잘 저장되어 있음을 알 수 있습니다.

그런데 방금 구현한 Function 클래스는 용도가 '입력값의 제곱'으로 고정된 함수입니다. 따라서 Sequare라는 명확한 이름이 더 어울립니다. 앞으로 Sin, Exp 등 당야한 함수가 필요하다는 점을 고려하면 Function클래스는 기반 크래스로 두고 DeZero의 모든 함수가 공통적으로 게족하는 기능만 담아주는 것이 좋겠습니다. 그래서 앞으로 모든 DeZero함수는 다음의 두 사항을 만족하도록 구현하겠습니다.

1) Function 클래스는 기반 클래스로서, 모든 함수에 공통되는 기능을 구현합니다.

2) 구체적인 함수는 Function 클래스를 상속한 캘래스에서 구현합니다.

이를 위해 Function 클래스를 다음처럼 수정합니다.

class Function:
  def __call__(selfinput):
    x = input.data
    y = self.forward(x) #구체적인 계산은 forward 메서드에서 한다.
    output = Variable(y)
    return output
  
  def forward(selfx):
    raise NotImplementedError()


__call__살짝 수정하고 forward라는 메서드를 추가했습니다. __call__메서드는 'Variable에서 ㅁ데이터 찾기'와 '계산 결과를 Variable에 포장하기'라는 두 가지 일을 합니다. 그리고 그 사이의 구체적인 계산은 forward메서드를 호출하여 수행합니다. 마지막으로 forward 메서드의 구체적인 로직은 하위 클래스에서 구현합니다.


Function 클래스의  forward메서드는 예외를 발생시킵니다. 이렇게 해두면 Function클래스의 forward메서드를 직접 호출한 사람에게 '이 메서드는 상속하여 구현해야 한다'는 사실을 알려줄 수 있습니다.


이러서 Function 클래스를 상속하여 입력값을 제곱하는 클래스를 구현하겠습니다. 클래스이름은 Squarea 라고 짓고 다음과 같이 구현합니다.

class Square(Function):
  def forward(selfx):
      return x **2

Square클래스는 Function클래스를 상속하기 때문에 __call__메서드는 그대로 계승됩니다. 따라서 forward메서드에 구체적인 계산 로직을 작성해 넣는 것만으로 구현은 끝입니다. 실제로 잘 동작하는지 Square 클래스를 사용하여 Variable을 처리하는 모습을 보시죠.

x = Variable(np.array(10))
f = Square()
y = f(x)
print(type(y))
print(y.data)

<class '__main__.Variable'> 100

2-2 Function 클래스 구현

 그러면 [그림2-1]의 함수를 프로그래밍 관점에서 생각해봅시다. 구체적으로는, 앞서 구현한 Variable인스턴슬르 변수로 다룰 수 있는 함수를 Function 클래스로 구현합니다. 여기서 주의할 점은 다음 두 가지입니다.

1) Function 클래스는 Variable인스턴스를 입력받아 Variable인스턴스를 출력합니다.

2) Variable 인스턴스의 실제 데이터는 인스턴스 변수인 data에 있습니다.

이 두 가지에 유의하여 Function 클래스를 다음과 같이 구현 합니다.

class Function:
  def __call__(selfinput):
    x = input.data # 데이터를 꺼낸다.
    y = x ** 2 #실제 계산
    output = Variable(y) # Variable 형태로 되돌린다.
    return output

__call__ 메서드의 인수 input은 Variable 인스턴스라고 가정합니다. 따라서 실제 데이터는 input.data에 존재합니다. 데이터를 커낸 후 원하는 계산(여기서는 제곱)을 하고, 결과를  Variable이라는 '상자'에 담아 돌려 줍니다.

__call__메서드는 파이썬의 특수 메서드입니다. 이 메서드를 정의하면 f = Function()형태로 함수의 인스턴스를 변수 f에 대입해주고, 나중에 f(...)형태로 __call__ 메서드를 호출할 수 있습니다.

2.1 함수란

 함수란 무엇일까요? 조금 딱딱하게 표현하면 '어떤 변수로부터 다른 변수로의 대응 관곌르 정한 것'이라고 할 수 있씁니다. 구체적인 예가 있으면 좋겠군요. 제곱을 계산하는 함수 f(x) = x2이 있다고 해봅시다. 이때 y=f(x)라고 하면 변수 y와 x의 관계가 함수 f에 의해 결정됩니다. 즉, 함수 f에 의해 'y는 x의 제곱이다'라는 관계가 성립됩니다.

이와 같은 변수 사이의 대응 관곌르 정하는 역할을 함수가 맡게 되며, [그림 2-1]은 그 의미를 시각적으로 표현한 모습니다.

[그림 2-1]은 변수 x 와 y, 그리고 함수 f의 관계를 보여줍니다. 이처럼 원(o)과 사각형 모양의 노드을 화살표로 연결해 계산 과정을 표현한 그림은 계산 그래프(computational graph)라고 합니다. 이 책에서는 변수를 동그라미 으로, 함수를 사각형으로 표시하겠습니다.

STEP2 변수를 낳는 함수

 앞 단계에서 Variable클래스를 상자로 사용할 수 있게 했습니다. 하지만 지금 이대로는 그냥 상자일뿐입니다. 우리에겐 단순한 상자를 마법의 상자로 바꾸는 장치가 필요한데, 그 열쇠는 바로 '함수'입니다. 이번 단계에서는 함수에 대해 생각해보겠습니다.


1.3 [보충] 넘파이의 다차원 배열

 마지막으로 넘파이 다차원 배열에 관해 간단히 보충학셌습니다. 다차원 배열은 숫자 등의 원소가 일정하게 모여 있는 데이터 구조입니다. 다차원 배열에서 원소의 순서에는 방향이 있고, 이 방향을 차원(dimension) 혹은 축(axis)이라고 합니다. [그림 1-2]는 다차원 배령의 예입니다.

[그림 1-2]에는 외쪽부터 0차원 배열, 1차원 배열, 2차원 배열이 나오는데, 차례대로 스칼라(scalar), 벡터(vector), 행열(matrix)이라고 합니다. 스칼라는 단순히 하나의 수를 나타냅니다. 벡터는 하나의 축을 따라 숫자가 들어서 있고, 행렬은 축이 2개입니다.

다차원 배열을 텐서(tensor)라고도 합니다. [그림1-2]는 외쪽부터 0차원 텐서, 1차원 텐서, 2차원 텐서가 되겠죠.

넘파이의 ndarray 인스턴스에는 ndim이라는 인스턴스 변수가 있습니다. ndim은 'number of dimensions'의 약자로, 다차원 배열의 '차원수'를 뜻합니다

import numpy as np
x = np.array(1)
x.ndim
0

x = np.array([1,2,3])
x.ndim
1

x = np.array([[1,2,3],[4,5,6]])
x.ndim
2

벡터를 다룰 때는 '차원'이라는 말에 주의해야 합니다. 예를 들어 np.array([1,2,3])은 벡터인
데, 세개의 요소가 일렬로 늘어서 있기 때문에 '3차원 벡터'라고도 합니다. 이때 '벡터의 차원'
은 벡터의 원소수를 말합니다. 한편 '3차원 배열'은 (원소가 아닌) 축이 3개라는 뜻입니다.

이상과 같이 ndarray인스턴스를 사용하면 스칼라, 벡터, 행렬, 심지어 더 높은 차원의 텐서를
만들 수 있습니다.




1.2 Variable 클래스 구현

 변수는 영어로 variable입니다. 그래서 DeZero에서 사용하는 변수라는 개념을 Variable이라는 이름의 클래스로 구현하겠습니다. 덧붙여서, 파이썬에서는 클래스 이름의 첫 글자를 보통 대문자로 합니다. 파이썬이 권항하는 코딩 규칙은  PEP8을 참고하세요.

이제 Variable 클래스가 상자가 되도록 구현해보죠. 다음은 이 기능을 최소한의 코드로 작성해본 모습니다.

class Variable:
  def __init__(selfdata):
    self.data = data
보는 바와 같이 초기화 함수 __init__에 주어진 인수를 인스턴스 변수 data에 대입했습니다.
아주 간단한 코드지만, 이제 Variable클래스를 상자로 사용할 수 있습니다. 실제 데이터가
Variable의 data에 보관되기 때문이죠, 다음 예를 보면 더 명확해질 것입니다.
import numpy as np
data = np.array(1.0)
x = Variable(data)
print(x.data)

1.0
이 예에서 상자에 데이터는 '넘파이의 다차원 배열'을 사용했습니다. 이때 x는 Variable
인스턴스이며, 실제 데이터는 x안에 담겨 있습니다. 즉, x는 데이터 자체가 아니라 데이터를
담은 상자가 됩니다.

머신 러닝 시스템은 기본 데이터 구조로 '다차원 배열'을 사용합니다. 그래서 DeZero의
Variable 클래스는 넘파이의 다차원 배열만 취급합니다. 넘파이의 다차원 배열 클래스는
numpyu.ndarray이며 np.ndarray로 줄여 쓰곤 합니다. 넘파이 배열은 앞의 코드에서 볼 수
있듯 np.array함수를 이용해 생성할 수 있습니다. 이 책에서는 앞으로numpy.ndarray인스터스
를 단순히 ndarray 인스턴스라고 부르겠습니다.
이어서 앞 코드에 x에 새로운 데이터를 대입해 보겠습니다.

x.data = np.array(2.0)
print(x.data)
2.0

보는 바와 같이 x.data = ... 형태로 쓰면 새로운 데이터가 대입됩니다. 이제 Variable클래스
를 상자로 사용할 수 있게 되었습니다.