본문 바로가기
Study 공부할레나

[스터디/밑시딥3] 제 3고지 고차미분계산_26. 계산 그래프 시각화(2)

by LENA-cfg 2023. 9. 3.

이번 단계에서는 Dezero 계산 그래프를 DOT 언어로 변환하는 기능을 구현합니다.

 

26.1 시각화 코드 예

import numpy as np
from dezero import Variable
from dezero.utils import get_dot_graph

x0 = Variable(np.array(1.0))
x1 = Variable(np.array(1.0))

y = x0 + x1

x0.name = 'x0'
x1.name = 'x1'
y.name = 'y'

txt = get_dot_graph(y, verbose=False)
print(txt)

with open ('C:/Users/000/Desktop/sample.dot.txt', 'w') as o:
    o.write(txt)

get_dot_graph 함수에는 최종 출력인 변수 y를 인수로 제공합니다. 

그러면 출력 변수 y를 기점으로 한 계산 과정을 DOT언어로 전환한 문자열을 반환합니다. 

get_dot_graph 함수를 호출하기 전 Variable 인스턴스 속성에 name을 추가해줘서

계산 그래프를 시각화 할 때 변수 노드에 레이블(이름)을 달아줍니다.

 

위의 출력 결과를 sample.dot 파일에 저장 후 터미널 창에서 아래 명령어를 실행해줍니다.

26.2 계산 그래프에서 DOT 언어로 변환하기

Dezero폴더에 utils.py를 만들어주고 _dot_var라는 보조 함수부터 구현합니다.

함수 이름앞에 _를 붙인 이유는 로컬에서만 사용하기 때문입니다.

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 함수에 Variable 인스턴스를 건네면 DOT 언어로 작성된 문자열로 바꿔서 반환합니다.

파이썬 내장 함수 id를 사용해 고유한 ID를 부여했고

객체 ID는 다른 객체와 중복되지 않아서 노드 ID로 사용하기 적합합니다.

return에서 .format 메서드를 사용하여 dot_var의 문자열에 등장하는 {} 이부분을 메서드 인수로 건넨 객체로 순서대로 바꿔줍니다. 첫번째 {}는 id(v)값이 두번째 {}는 name이 채워집니다. 

저는 디버그로 찍고 Variable 인스턴스를 x에 담고 이름을 부여한 후 _dot_var가 어떻게 실행되는지 확인했습니다. 

 

다음은 Dezero 함수를 DOT 언어로 변환하는 편의함수 구현입니다.

이름은 _dot_func입니다.

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

_dot_func 함수는 Dezero 함수를 DOT 언어로 기술합니다. 
함수와 입력 변수의 관계 그리고 함수와 출력 변수의 관계도 DOT언어로 기술합니다. 

 

다음은 get_dot_graph 함수 구현입니다. 

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 + '}'

Variable 클래스의 backward 메서드와 같은 로직이고 위에서 구현했던 _dot_var, _dot_func를 사용하여 그래프를 만들어주는 코드입니다.

변경된 부분은 초록색 형광펜이고 옆에 이해를 돕기위한 설명도 추가했습니다.

26.3 이미지 변환까지 한 번에

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:]  # Extension(e.g. png, pdf)
    cmd = 'dot {} -T {} -o {}'.format(graph_path, extension, to_file)
    subprocess.run(cmd, shell=True)

1. dot 데이터를 파일에 저장

- get_dot_graph 함수를 호출하여 계산 그래프를 DOT 언어(텍스트)로 변환하고 파일에 저장합니다.

대상 디렉터리는 ~/.dezero이고 파일 이름은 tmp_graph.dot으로 했습니다.

os.path.expanduser('~') 문장은 사용자의 홈 디렉터리를 뜻하는 '~'를 절대 경로로 풀어줍니다.

 

2. dot 명령 호출

plot_dot_graph 함수의 인수인 to_file에 저장할 이미지 파일 이름을 지정하고

파이썬에서 외부 프로그램을 호출하기 위해 subprocess.run 함수를 사용했습니다.

 

26.4 동작 확인 

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)  # sphere(x, y) / matyas(x, y)
z.backward()
# print(x.grad, y.grad)

x.name = 'x'
y.name = 'y'
z.name = 'z'
plot_dot_graph(z, verbose=False, to_file='goldstein.png')