기계학습/밑바닥딥러닝3 오독오독 씹기

Chapter 3. 고차 미분 계산(step 25~26)/ 계산 그래프 시각화

H_erb Salt 2021. 1. 26. 08:25
ch3)step_25-26

25. 계산그래프 시각화(1)

  • 계산 그래프를 눈으로 확인하기
    • 계산 그래프를 시각화하면 문제가 발생했을 때 원인이 되는 부분을 파악하기 쉬워짐. 또한 더 나은 계산 방법을 발견할 수도 있고, 신경망의 구조를 제3자에게 시각적으로 전달하는 용도로도 활용할 수 있음
  • 시각화 도구로는 Graphviz를 활용하고, 사용법을 알아봄

26. 계산그래프 시각화(2)

  • dezero/utils.py 파일 작성
In [1]:
import numpy as np
from dezero import Variable
In [2]:
def _dot_var(v, verbose=False):
    dot_var = '{} [label="{}", color=orange, style=filled]\n'

    name = '' if v.name is None else v.name
    if verbose and v.data is not None:
        if v.name is not None:
            name += ': '
        name += str(v.shape) + ' ' + str(v.dtype)

    return dot_var.format(id(v), name)
  • _dot_var라는 보조 함수부터 코드 작성. 이름 밑에 밑줄이 붙은 이유는 이 함수를 로컬에서만, 즉 get_dot_graph 함수 전용으로 사용할 것이기 때문.
  • _dot_var 함수에 Variable 인스턴스를 건네면 인스턴스의 내용을 DOT 언어로 작성된 문자열로 바꿔서 변환함.
  • 변수 노드에 고유한 ID를 부여하기 위해 파이썬 내장함수인 id를 사용함.
  • id 함수는 주어진 객체의 ID를 반환하는데, 객체 ID는 다른 객체와 중복되지 않기 때문에 노드의 ID로 사용하기에 적합함
  • 또한 마지막 반한 직전에 format 메서드를 이용함. format 메서드는 문자열에 등장하는 "{}" 부분을 메서드 인수로 건넨 객체(문자열이나 정수 등)로 차례로 바꿔줌.
  • 가령 앞의 코드에서는 dot_var 문자열의 첫 번째 {} 자리에는 id(v)와 같이, 두 번째 {} 자리에는 name의 값이 채워짐
In [3]:
x = Variable(np.random.randn(2, 3))
x.name = 'x'
print(_dot_var(x))
print(_dot_var(x, verbose=True))
2981036815880 [label="x", color=orange, style=filled]

2981036815880 [label="x: (2, 3) float64", color=orange, style=filled]

  • _dot_func는 DeZero 함수를 DOT 언어로 반환하는 편의 함수.
  • 또한, 함수와 입력 변수의 관계, 함수와 출력 변수의 관계도 DOT 언어로 기술함.
  • 복습하자면 DeZero 함수는 Function 클래스를 상속하고 inputs과 outputs라는 인스턴스 변수를 가지고 있음.
In [4]:
def _dot_func(f):
    # for function
    dot_func = '{} [label="{}", color=lightblue, style=filled, shape=box]\n'
    ret = dot_func.format(id(f), f.__class__.__name__)

    # for edge
    dot_edge = '{} -> {}\n'
    for x in f.inputs:
        ret += dot_edge.format(id(x), id(f))
    for y in f.outputs:  # y is weakref
        ret += dot_edge.format(id(f), id(y()))
    return ret
In [5]:
x0 = Variable(np.array(1.0))
x1 = Variable(np.array(1.0))
y = x0 + x1

txt = _dot_func(y.creator)
print(txt)
2981031941640 [label="Add", color=lightblue, style=filled, shape=box]
2981031907208 -> 2981031941640
2981036694216 -> 2981031941640
2981031941640 -> 2981031943752

  • 준비가 끝났으면 본격적으로 get_dot_graph 함수를 구현함.
  • 함수 코드의 로직은 Variable 클래스의 backward 메서드와 거의 같음. backward 메서드는 미분값을 전파했지만, 여기에서는 미분 대신 DOT 언어로 기술한 문자열을 txt에 추가함.
  • 또한, 실제 역전파에서는 노드를 따라가는 순서가 중요함. 그래서 함수의 generation(세대)이라는 정숫값을 부여하고 그 값이 큰 순서대로 꺼냄. 하지만 get_dot_graph 함수에서는 노드를 추적하는 순서는 문제가 되지 않으므로 generation 값으로 정렬하는 코드를 주석으로 처리함.
    • 계산 그래프를 DOT 언어로 반환할 때는 '어떤 노드가 존재하는 가'와 '어떤 노드끼리 연결되는가'가 문제임. 즉, 노드의 추적 '순서'는 문제가 되지 않으므로 generation을 사용하여 순서대로 꺼내는 구조는 사용하지 않아도 됨
In [6]:
def get_dot_graph(output, verbose=True):
    txt = ''
    funcs = []
    seen_set = set()
    
    def add_func(f):
        if f not in seen_set:
            funcs.append(f)
#             funcs.sort(key=lambda x: x.generation)
            seen_set.add(f)
    
    add_func(output.creator)
    txt += _dot_var(output, verbose)
    
    while funcs:
        func = funcs.pop()
        txt += _dot_func(func)
        for x in func.inputs:
            txt += _dot_var(x, verbose)

            if x.creator is not None:
                add_func(x.creator)

    return 'digraph g {\n' + txt + '}'

26.3 이미지 변환까지 한 번에

  • get_dot_graph 함수는 계산 그래프를 DOT 언어로 변환함. 그런데 DOT 언어를 이미지로 변환하려면 dot 명령을 수동으로 실행해야 하므로 매번 하기에는 번거로움. 그래서 dot 명령 실행까지 한 번에 해주는 함수를 제공함
In [7]:
def plot_dot_graph(output, verbose=True, to_file='graph.png'):
    dot_graph = get_dot_graph(output, verbose)
    
    # 1. dot 데이터를 파일에 저장
    tmp_dir = os.path.join(os.path.expanduser('~'), '.dezero')
    if not os.path.exists(tmp_dir): # ~./dezero 디렉터리가 없다면 새로 생성
        os.mkdir(tmp_dir)
    graph_path = os.path.join(tmp_dir, 'tmp_graph.dot')
    
    with open(graph_path, 'w') as f:
        f.write(dot_graph)
        
    # 2. dot 명령 호출
    extension = os.path.splitext(to_file)[1][1:] # 확장자(png, pdf 등)
    cmd = 'dot {} -T {} -o {}'.format(graph_path, extension, to_file)
    subprocess.run(cmd, shell=True)
    
    # Return the image as a Jupyter Image object, to be displayed in-line.
    try:
        from IPython import display
        return display.Image(filename=to_file)
    except:
        pass
  • 우선, 1 에서는 방금 구현한 get_dot_graph 함수를 호출하여 계산 그래프를 DOT 언어(텍스트)로 반환하고, 파일에 저장함. 대상 디렉터리는 ~./dezero 이고 파일 이름은 tmp_graph.dot으로 함(일시적으로 사용할 파일이므로 tmp라는 이름을 씀)
  • os.path.expanduser('~') 라는 문장은 사용자의 홈 디렉터리를 뜻하는 '~'를 절대 경로로 풀어줌
  • 2 에서는 앞에서 저장한 파일 이름을 지정하여 dot 명령을 호출함. 이 때 plot_dot_graph함수의 인수인 to_file에 저장할 이미지 파일의 이름을 지정함. 참고로 파이썬에서 외부 프로그램을 호출하기 위해 subprogress.run 함수를 사용함
  • 여기에서 구현한 함수는 앞으로 다양한 장소에서 사용되기 때문에 dezero/utils.py 에 추가함. 그럼 from dezero.utils import plot_dot_graph로 임포트 가능
In [8]:
import numpy as np
from dezero import Variable
from dezero.utils import plot_dot_graph

def goldstein(x, y):
    z = (1 + (x + y + 1)**2 * (19 - 14*x + 3*x**2 - 14*y + 6*x*y + 3*y**2)) * \
        (30 + (2*x - 3*y)**2 * (18 - 32*x + 12*x**2 + 48*y - 36*x*y + 27*y**2))
    return z

x = Variable(np.array(1.0))
y = Variable(np.array(1.0))
z = goldstein(x, y)
z.backward()

x.name = 'x'
y.name = 'y'
z.name = 'z'
plot_dot_graph(z, verbose=False, to_file='ch3)goldstein.png')
Out[8]:
In [ ]: