제 1고지 미분 자동계산

1단계 상자로서의 변수

1.1 변수란

  • 데이터를 저장하는 상자와 같다

1.2 Variable 클래스 구현

  • DeZero에서 사용하는 변수라는 개념을 Variable이라는 이름의 클래스로 구현
  • 클래스 이름의 첫글자는 보통 대문자
  • 파이썬이 권장하는 코딩 규칙은 PEP8
    • PEP8 : 파이썬 개선 제안서, 파이썬 코드를 어떻게 구상할 지 알려주는 스타일 가이드
1
2
3
4
# steps/step01.py
class Variable:
def __init__(self, data):
self.data = data
  • __init__에 주어진 인수를 인스턴스 변수 data에 대입
  • Variable의 data에 보관
1
2
3
4
5
6
# steps/step01.py
import numpy as np

data = np.array(1.0)
x = Variable(data)
print(x.data)
1.0
  • 이 예에서 상자에 넣는 데이터로 ‘넘파이 다차원 배열’을 사용

  • x는 Variable 인스턴스 이며, 실제 데이터는 x안에 담겨 있음

  • x는 데이터 자체가 아니라 데이터의 담는 상자 즉, 변수

  • 머신러닝 시스템은 기본 데이터 구조로 ‘다차원 배열’을 사용

  • DeZero의 Variable 클래스는 넘파이의 다차원 배열만 취급

  • 넘파이 배열은 np.array 함수로 생성 가능

  • numpy.ndarray 인스턴스를 ndarray 인스턴스로 부름

1
2
3
# steps/step01.py
x.data = np.array(2.0) # x에 새로운 데이터 대입
print(x.data)
2.0

1.3 넘파이 다차원 배열

  • 다차원 배열은 숫자 등의 원소가 일정하게 모여 있는 데이터 구조
  • 다차원 배열에서 원소의 순서에는 방향이 있고, 이 방향을 차원(dimension) 혹은 축(axis)이라고 함
  • 0차원, 1차원, 2차원 배일이 있는데, 차례대로 스칼라(scalar), 벡터(vector), 행렬(matrix)이라고 함
    • 스칼라는 단순히 하나의 수
    • 벡터는 하나의 축을 따라 숫자가 늘어서 있음
    • 행렬은 축이 두 개
  • 다차원 배열을 0차원 텐서(tensor), 1차원 텐서, 2차원 텐서라고도 함
1
2
3
4
# ndim 은 'number of dimensions'의 약자로, 다차원 배열의 '차원 수'를 뜻함
import numpy as np
x = np.array(1)
x.ndim
0
1
2
x = np.array([1, 2, 3])
x.ndim
1
1
2
3
x = np.array([[1, 2, 3],
[4, 5, 6]])
x.ndim
2

2단계 변수를 낳는 함수

2.1 함수란

  • 어떤 변수로부터 다른 변수로의 대응 관계를 정한 것

2.2 Function 클래스 구현

  • Function 클래스는 Variable 인스턴스를 입력받아 Variable 인스턴스를 출력
  • Variable 인스턴스의 실제 데이터는 인스턴스 변수인 data에 있음
1
2
3
4
5
6
class Function:
def __call__(self, input):
x = input.data # 데이터를 꺼냄
y = x ** 2 # 실제 계산
output = Variable(y) # Variable 형태로 되돌림
return output
  • __call__ 메서드는 파이썬의 특수 메서드
  • f = Function() 형태로 함수의 인스턴스를 변수 f에 대입해 둠
  • f(…)형태로 __call__ 메서드를 호출할 수 있음

2.3 Function 클래스 이용

1
2
3
4
5
x = Variable(np.array(10))
f = Function()
y = f(x)
print(type(y))
print(y.data)
<class '__main__.Variable'>
100
  • Function 클래스는 기반 클래스로서, 모든 함수에 공통되는 기능을 구현
  • 구체적인 함수는 Function 클래스를 상속한 클래스에서 구현
1
2
3
4
5
6
7
8
9
# steps/step02.py
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x) # 구체적인 계산은 forward 메서드에서 한다.
output = Variable(y)
return output
def forward(self, x):
raise NotImplementedError()
  • NotImplementedError()는 ‘이 메서드는 상속하여 구현해야 한다’는 사실을 알려주는 예외처리
1
2
3
4
5
# steps/step02.py
# 입력값을 제곱하는 클래스 구현
class Square(Function):
def forward(self, x):
return x ** 2
1
2
3
4
5
x = Variable(np.array(10))
f = Square()
y = f(x)
print(type(y))
print(y.data)
<class '__main__.Variable'>
100

3단계 함수 연결

3.1 Exp 함수 구현

  • 오일러의 수, 네이피어 상수 구현
1
2
3
4
# steps/step03.py
class Exp(Function):
def forward(self, x):
return np.exp(x)

3.2 함수 연결

1
2
3
4
5
6
7
8
9
10
# steps/step03.py
A = Square()
B = Exp()
C = Square()

x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)
print(y.data)
1.648721270700128
  • 여러 함수로 구성된 함수를 ‘합성 함수’라고 함

4단계 수치 미분

4.1 미분이란

  • 미분은 변화율
  • 극한으로 짧은 시간(순간)에서의 변화량
  • 도함수 : 함수 f(x)가 주어졌을 때 함수의 정의역에 속하는 각각의 x의 값에 미분계수가 하나씩 대응되는 함수

4.2 수치 미분 구현

  • 컴퓨터는 극한을 취급할 수 없음
  • h = 0.0001(=1e-4)과 같은 매우 작은 값으로 대체
  • 미세한 차이를 이용하여 함수의 변화량을 구하는 방법을 ‘수치 미분’이라 함
  • 수치 미분은 작은 값을 사용하여 ‘진정한 미분’을 근사
  • 어쩔수 없이 오차가 포함
  • 근사 오차를 줄이는 방법으로 ‘중앙차분’을 씀
    • 중앙차분은 f(x)와 f(x+h)의 차이를 구한는 대신에 f(x-h)와 f(x+h)의 차이를 구함
  • 전진차분보다 중앙차분이 진정한 미분값에 가깝다는 사실은 테일러 급수를 이용해 증명가능

  • 중앙차분을 이용하여 수치 비분을 계산하는 함수 numerical_diff(f, x, eps=1e-4)을 구현
  • f는 Function의 인스턴스, x는 미분을 계산하는 변수로 Variable 인스턴스, eps은 작은 값
1
2
3
4
5
6
7
# steps/step04.py
def numerical_diff(f, x, eps=1e-4):
x0 = Variable(x.data - eps)
x1 = Variable(x.data + eps)
y0 = f(x0)
y1 = f(x1)
return (y1.data - y0.data) / (2 * eps)
1
2
3
4
5
6
# steps/step04.py
# Square 클래스를 대상으로 미분
f = Square()
x = Variable(np.array(2.0))
dy = numerical_diff(f, x)
print(dy)
4.000000000004

4.3 합성 함수의 미분

  • 합성 함수를 미분해보자
1
2
3
4
5
6
7
8
9
10
11
# steps/step04.py
def f(x):
A = Square()
B = Exp()
C = Square()
return C(B(A(x)))


x = Variable(np.array(0.5))
dy = numerical_diff(f, x)
print(dy)
3.2974426293330694

4.4 수치 미분의 문제점

  • 수치 미분의 결과에는 오차가 포함
  • 대부분의 경우 오차는 매우 작지만 어떤 계산이냐에 따라 커질 수 있음
  • 수치 미분의 결과에 오차가 포함되기 쉬운 이유는 주로 ‘자릿수 누락’ 때문
  • 계산량이 많다는 점도 심각한 문제
  • 그래서 등장한 것이 ‘역전파’
  • 역전파는 복잡한 알고리즘이라서 구현하면서 버그가 섞여 들어가기 쉬움
  • 역전파를 정확하게 구현했는지 확인하기 위해 수치 미분의 결과를 이용하는 방식을 ‘기울기 확인’이라함
  • 기울기 확인 : 단순히 수치 미분 결과와 역전파의 결과를 비교

5단계 역전파 이론

  • 역전파을 이용하면 미분을 효율적으로 계산할 수 있고 결과값의 오차도 적음

5.1 연쇄 법칙

  • 역전파를 이해하는 열쇠는 ‘연쇄 법칙(chain rule)’
  • 연쇄 법칙에 따르면 합성 함수의 미분은 구성 함수 각각을 미분한 후 곱한 것과 같음

5.2 역전파 원리 도출

  • 머신러닝은 주로 대량의 매개변수를 입력받아서 마지막에 ‘손실 함수(loss function)’를 거쳐 출력을 내는 형태
  • 손실 함수의 출력은 단일한 스칼라값이며, 이 값이 ‘중요 요소’
  • 머신러닝은 주로 대량의 매개변수를 입력받아서 마지막에 ‘손실 함수(loss function)’를 거쳐 출력을 내는 형태
  • 미분값을 출력에서 입력 방향으로 전파하면 한 번의 전파만으로 모든 매개변수에 대한 미분을 계산할 수 있음

5.3 계산 그래프로 살펴보기

  • 변수는 ‘통상값’과 ‘미분값’이 존재
  • 함수는 ‘통상 계산(순전파)’과 ‘미분값을 구하기 위한 계산(역전파)’이 존재
  • 역전파 시에는 순전파시에 이용한 데이터가 필요, 따라서 역전파를 구현하려면 먼저 순전파를 하고, 이때 각 함수가 입력 변수의 값을 기억해둬야함

그림 5-5

6단계 수동 역전파

  • 역전파의 구동 원리를 설명
  • Variable과 Function 클래스를 확장하여 역전파를 이용한 미분 구현

6.1 Variable 클래스 추가 구현

  • 통산값(data)과 더불어 그에 대응하는 미분값(grad)도 저장하도록 확장
1
2
3
4
5
# steps/step06.py
class Variable:
def __init__(self, data):
self.data = data
self.grad = None

6.2 Function 클래스 추가 구현

  • 미분을 계산하는 역전파(backward 메서드)
  • forward 메서드 호출 시 건네받은 Variable 인스턴스 유지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# steps/step06.py
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x)
output = Variable(y)
self.input = input # 입력 변수를 기억(보관)함
return output

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

def backward(self, gy):
raise NotImplementedError()
  • __call__ 메서드에서 입력된 input을 인스턴스 변수인 self.input에 저장
  • backward 메서드에서 함수에 입력한 변수가 필요할 때 self.input에서 가져와 사용

6.3 Square 와 Exp 클래스 추가 구현

1
2
3
4
5
6
7
8
9
10
# steps/step06.py
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
1
2
3
4
5
6
7
8
9
10
# steps/step06.py
class Exp(Function):
def forward(self, x):
y = np.exp(x)
return y

def backward(self, gy):
x = self.input.data
gx = np.exp(x) * gy
return gx
  • 순전파 코드
1
2
3
4
5
6
7
8
9
# steps/step06.py
A = Square()
B = Exp()
C = Square()

x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)
  • 역전파 코드
1
2
3
4
5
6
# steps/step06.py
y.grad = np.array(1.0)
b.grad = C.backward(y.grad)
a.grad = B.backward(b.grad)
x.grad = A.backward(a.grad)
print(x.grad)
3.297442541400256

7단계 역전파 자동화

  • 순전파를 한 번만 해주면 어떤 계산이라도 상관없이 역전파가 자동으로 이루어지는 구조 만들기
  • Define-by-Run이란 딥러닝에서 수행하는 계산들을 계산 시점에 ‘연결’하는 방식으로, ‘동적 계산 그래프’라고 함

7.1 역전파 자동화의 시작

  • 역전파 자동화로 가는 길은 변수와 함수의 ‘관계’를 이해하는 데서 출발

  • 함수 관점에서 변수는 ‘입력’과 ‘출력’에 쓰임

  • 변수 과점에서 함수는 ‘창조자’ 혹은 ‘부모’

  • 일반적인 순전파가 이루어지는 시점에 ‘관계’를 맺어줌

1
2
3
4
5
6
7
8
9
# steps/step07.py
class Variable:
def __init__(self, data):
self.data = data
self.grad = None
self.creator = None # 인스턴스 변수 추가

def set_creator(self, func): # creator 설정
self.creator = func
  • creator라는 인스턴스 변수 추가, creator 설정을 위한 set_creator 메서드 추가
1
2
3
4
5
6
7
8
9
10
# steps/step07.py
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x)
output = Variable(y)
output.set_creator(self) # Set parent(function)
self.input = input
self.output = output # Set output
return output
  • 순전파를 계산하면 그 결과로 output이라는 Variable 인스턴스가 생성
  • oupput이 creator를 기억
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A = Square()
B = Exp()
C = Square()

x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)

assert y.creator == C
assert y.creator.input == b
assert y.creator.input.creator == B
assert y.creator.input.creator.input == a
assert y.creator.input.creator.input.creator == A
assert y.creator.input.creator.input.creator.input == x
  • assert문은 조건을 충족하는지 여부를 확인하는 데 사용

7.2 역전파 도전!

    1. 함수를 가져온다.
    1. 함수의 입력을 가져온다.
    1. 함수의 backward 메서드를 호출한다.
1
2
3
4
5
y.grad = np.array(1.0)

C = y.creator # 1. 함수를 가져온다.
b = C.input # 2. 함수의 입력을 가져온다.
b.grad = C.backward(y.grad) # 3. 함수의 backward 메서드를 호출한다.
1
2
3
B = b.creator # 1. 함수를 가져온다.
a = B.input # 2. 함수의 입력을 가져온다.
a.grad = B.backward(b.grad) # 3. 함수의 backward 메서드를 호출한다.
1
2
3
4
A = a.creator # 1. 함수를 가져온다.
x = A.input # 2. 함수의 입력을 가져온다.
x.grad = A.backward(a.grad) # 3. 함수의 backward 메서드를 호출한다.
print(x.grad)
3.297442541400256

7.3 backward 메서드 추가

  • 위 반복작업을 자동화할 수 있도록 Variable 클래스에 backward 메서드 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# steps/step07.py
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):
f = self.creator # 1. Get a function
if f is not None:
x = f.input # 2. Get the function's input
x.grad = f.backward(self.grad) # 3. Call the function's backward
x.backward()
  • backward 메서드가 재귀적으로 호출면서 자동화
1
2
3
4
5
6
7
8
9
10
11
12
13
A = Square()
B = Exp()
C = Square()

x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)

# backward
y.grad = np.array(1.0)
y.backward()
print(x.grad)
3.297442541400256

8단계 재귀에서 반복문으로

  • 처리 효율을 개선하고 확장을 대비해 backward 메서드의 구현 방식을 변경

8.2 반복문을 이용한 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# steps/step08.py
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):
funcs = [self.creator]
while funcs:
f = funcs.pop() # 1. Get a function
x, y = f.input, f.output # 2. Get the function's input/output
x.grad = f.backward(y.grad) # 3. Call the function's backward

if x.creator is not None:
funcs.append(x.creator)
  • 처리해야 할 함수들을 funcs라는 리스트에 차례로 집어넣음
  • while 블록 안에서 funcs.pop()을 호출하여 처리할 함수 f를 꺼냄
  • f의 backward 메서드를 호출
  • f.input과 f.output에서 함수 f의 입력과 출력 변수를 얻음
  • f.backward()의 인수와 반환값을 올바르게 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# steps/step08.py
A = Square()
B = Exp()
C = Square()

x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)

# backward
y.grad = np.array(1.0)
y.backward()
print(x.grad)
3.297442541400256
  • 재귀는 함수를 재귀적으로 호출할 때마다 중간 결과를 메모리에 유지하면서 처리
  • 일반적으로 반복문 방식의 효율이 더 좋음

9단계 함수를 더 편리하게

9.1 파이썬 함수로 이용하기

1
2
def square(x):
return Square()(x)
1
2
def exp(x):
return Exp()(x)
1
2
3
4
5
6
7
8
x = Variable(np.array(0.5))
a = square(x)
b = exp(a)
y = square(b)

y.grad = np.array(1.0)
y.backward()
print(x.grad)
3.297442541400256
1
2
3
4
5
6
# 함수를 연속하여 적용
x = Variable(np.array(0.5))
y = square(exp(square(x)))
y.grad = np.array(1.0)
y.backward()
print(x.grad)
3.297442541400256

9.2 backward 메서드 간소화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# y.grad = np.array(1.0) 생략 위해 Variable에 backward 수정
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: # grad가 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)
1
2
3
4
x = Variable(np.array(0.5))
y = square(exp(square(x)))
y.backward()
print(x.grad)
3.297442541400256

9.3 ndarray 만 취급하기

  • Variable에 ndarray인스턴스 외의 데이터를 넣을 경우 즉시 오류를 일으킴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# steps/step09.py
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{} is not supported'.format(type(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: # grad가 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이 아니고 ndarray 인스턴스도 아니면 TypeError 예외 발생
1
2
3
4
# steps/step09.py
x = Variable(np.array(1.0)) # OK
x = Variable(None) # OK
x = Variable(1.0) # NG
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-53-33bce631aee3> in <module>
      2 x = Variable(np.array(1.0))  # OK
      3 x = Variable(None)  # OK
----> 4 x = Variable(1.0)  # NG


<ipython-input-50-830a6874675c> in __init__(self, data)
      3         if data is not None:
      4             if not isinstance(data, np.ndarray):
----> 5                 raise TypeError('{} is not supported'.format(type(data)))
      6 
      7         self.data = data


TypeError: <class 'float'> is not supported
  • ndarray나 None이면 아무 문제 없지만, 다른 데이터 타입을 입력하면 예외 발생
  • 잘못된 데이터 타입을 사용했음을 즉시 알 수 있음
1
2
3
4
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
  • y의 데이터 타입도 ndarray
1
2
3
4
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가 되어버림
  • Variable은 데이터가 항상 ndarray 인스턴스라고 가정하기 때문에 대처를 해줘야 함
1
2
3
4
5
# steps/step09.py
def as_array(x):
if np.isscalar(x):
return np.array(x)
return x
  • np.isscalar는 입력 데이터가 numpy.float64 같은 스칼라 타입인지 확인하는 함수
1
2
import numpy as np
np.isscalar(np.float64(1.0))
True
1
np.isscalar(2.0)
True
1
np.isscalar(np.array(1.0))
False
1
np.isscalar(np.array([1, 2, 3]))
False
  • 이처럼 x가 스칼라 타입인지 쉽게 확인 가능
  • as_array함수는 입력이 스칼라인 경우 ndarray 인스턴스로 변환
  • as_array라는 편의 함수가 준비되었으니 Function 클래스에 코드 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# steps/step09.py
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()
  • 순전파의 결과인 y를 Variable로 감쌀 때 as_array()를 이용
  • 출력 결과인 output은 항상 ndarray 인스턴스가 되도록 보장
  • 이제 0차원 ndarray 인스턴스를 사용한 계산도 모든 데이터가 ndarray 인스턴스

10단계 테스트

  • 간단한 테스트를 해보자

10.1 파이썬 단위 테스트

  • 파이썬으로 테스트할 때 표준 라이브러리에 포함된 nuittest를 사용하면 편함
1
2
3
4
5
6
7
8
9
10
# 이전 단계에서 구현한 square 함수 테스트
# steps/step10.py
import unittest

class SquareTest(unittest.TestCase):
def test_forward(self):
x = Variable(np.array(2.0))
y = square(x)
expected = np.array(4.0)
self.assertEqual(y.data, expected)
  • unittest를 임포트하고 nuittest.TestCase를 상속한 SquareTest 클래스를 구현
  • square 함수의 출력이 기댓값과 같은지 확인
  • self.assertEqual 메서드는 주어진 두 객체가 동일한지 여부를 판정
1
$ python -m unittest steps/step10.py
  • python 명령을 실행할 때 -m unittest 인수를 제공하면 파이썬 파일을 테스트 모드로 실행
  • 파일 끝에 다음 코드를 추가하면 ‘python steps/step10.py’만 입력해도 테스트 수행
    1
    unittest.main()

    10.2 square 함수의 역전파 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# steps/step10.py
class SquareTest(unittest.TestCase):
def test_forward(self):
x = Variable(np.array(2.0))
y = square(x)
expected = np.array(4.0)
self.assertEqual(y.data, expected)

def test_backward(self):
x = Variable(np.array(3.0))
y = square(x)
y.backward()
expected = np.array(6.0)
self.assertEqual(x.grad, expected)
  • test_backward 메서드 추가
  • y.backward()로 미분값을 구하고, 그 값이 기댓값과 일치하는지 확인

10.3 기울기 확인을 이용한 자동 테스트

  • 기울기 확인이란 수치 미분으로 구한 결과와 역전파로 구한 결과를 비교, 그 차이가 크면 역전파 구현에 문제가 있다고 판단하는 검증 기법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# steps/step10.py
def numerical_diff(f, x, eps=1e-4):
x0 = Variable(x.data - eps)
x1 = Variable(x.data + eps)
y0 = f(x0)
y1 = f(x1)
return (y1.data - y0.data) / (2 * eps)


class SquareTest(unittest.TestCase):
def test_forward(self):
x = Variable(np.array(2.0))
y = square(x)
expected = np.array(4.0)
self.assertEqual(y.data, expected)

def test_backward(self):
x = Variable(np.array(3.0))
y = square(x)
y.backward()
expected = np.array(6.0)
self.assertEqual(x.grad, expected)

def test_gradient_check(self):
x = Variable(np.random.rand(1))
y = square(x)
y.backward()
num_grad = numerical_diff(square, x)
flg = np.allclose(x.grad, num_grad)
self.assertTrue(flg)
  • 기울기 확인을 할 test_gradient_check 메서드 안에서 무작위 입력값을 하나 생성
  • 역전파로 미분값을 구하고, numerical_diff 함수를 사용해 수치 미분으로도 계산
  • 두 메서드로 각각 구한 값들이 거의 일치하는지 확인
  • np.allclose(a, b)는 ndarray 인스턴스인 a와 b의 값이 가까운지 판정
  • 얼마나 가까워야 가깐운 것인지는 np.allclose(a, b, rtol=1e-05, atol=1e-08)과 같이 인수 rtol과 atol로 지정 가능
  • a와 b의 모든 요소가 다음 조건을 만족하면 True 반환
    1
    |a - b| <= (atol + rtol * |b|)