PythonからGraphvizを使う

前回までは、Graphvizの概要とDOT言語の仕様、dotコマンドでの画像生成を中心に解説してきました。 今回は、GraphvizのPython言語バインディングについて簡単に説明します。

対象は、Pythonを触ったことがある方を想定しています。
以下のサンプルでは、Pythonのバージョンは3.6.3を使用しています。

サンプル7 をPythonで書く

今回は、第3回で 使用したサンプル7の最終形(ann05.dot)をPythonで書いてみます。

(再掲)ann05.dot

// ann05.dot
digraph ann05 {
  graph [ rankdir=LR; splines=false ];
  graph [ dpi=250; pad=0; margin=0; fontsize=9.5 ];
  node [ label=“”; shape=circle; penwidth=0.35 ];
  edge [ arrowsize=0.5; penwidth=0.35 ];
  subgraph cluster_i {
    margin=0;
    label=“Input”;
    fontcolor=red;
    penwidth=0;
    node [ color=red ];
    i1 i2 i3;
  }
  subgraph cluster_h {
    margin=0;
    label=“Hidden”;
    fontcolor=blue;
    penwidth=0;
    node [ color=blue ];
    h1 h2 h3 h4;
  }
  subgraph cluster_o {
    margin=0;
    label=“Output”;
    fontcolor=darkgreen;
    penwidth=0;
    node [ color=darkgreen ];
    o1 o2;
  }
  {i1;i2;i3} -> {h1;h2;h3;h4};
  {h1;h2;h3;h4} -> {o1;o2};
}

まずは、graphvizパッケージをインストールします。
Graphviz本体が未インストールの方は、Graphvizの基本 - 01をご覧ください。

pip install graphviz

ann05.dot を Pythonで書くと以下のようになります(一例)。
コード内に最小限のコメントを入れてありますが、それ以外は上の ann05.dot と見比べていただければ何をやっているかがすぐ分かると思います。

# py_ann.py

from graphviz import Digraph # 有向グラフを生成

G = Digraph(format='png') # 直接PNGファイルに書き出す
G.attr('graph', rankdir='LR') 
G.attr('graph', splines='false') 
G.attr('graph', dpi='250') 
G.attr('graph', pad='0') 
G.attr('graph', margin='0') 
G.attr('graph', fontsize='9.5') 
G.attr('node', label='') 
G.attr('node', shape='circle') 
G.attr('node', penwidth='0.35') 
G.attr('edge', arrowsize='0.5') 
G.attr('edge', penwidth='0.35') 

# Input subgraph
I = Digraph(name='cluster_i') # 一旦普通の有向グラフ(I)オブジェクトを作成
I.attr('graph', label='Input')
I.attr('graph', margin='0')
I.attr('graph', fontcolor='red')
I.attr('graph', penwidth='0')
I.attr('node', color='red')
for i in range(1,4):
	I.node('i%d' % i)

# Hidden subgraph
H = Digraph(name='cluster_h')
H.attr('graph', label='Hidden')
H.attr('graph', margin='0')
H.attr('graph', fontcolor='blue')
H.attr('graph', penwidth='0')
H.attr('node', color='blue')
for i in range(1,5):
	H.node('h%d' % i)

# Output subgraph
O = Digraph(name='cluster_o')
O.attr('graph', label='Output')
O.attr('graph', margin='0')
O.attr('graph', fontcolor='darkgreen')
O.attr('graph', penwidth='0')
O.attr('node', color='darkgreen')
for i in range(1,3):
	O.node('o%d' % i)

G.subgraph(I) # 有向グラフ(I)を有向グラフ(G)のサブグラフにする
G.subgraph(H)
G.subgraph(O)

# Input から Hidden への edge
for i in range(1,4):
	for j in range(1,5):
		G.edge('i%d' % i, 'h%d' % j)

# Hidden から Output への edge
for i in range(1,5):
	for j in range(1,3):
		G.edge('h%d' % i, 'o%d' % j)

# 出力先ファイル名 拡張子(.png)は書かない
G.render('/path/to/py_ann') # /path/to/py_ann.png が生成される

出力結果がこちらです。サンプル7の最後の結果と一致しました。

py_ann

pydotの利用

graphvizパッケージの他に、pydotというものがあるのであわせて紹介しておきます。
pydotは数種類存在しますが、ここではPython2/3で利用できるpydot_ngを使用しています。

pip install pydot_ng

関数名や使用法が違いますが、基本はほとんど同じということが分かるかと思います。
もはや、何の説明もいらないでしょう。

# pydot_ann.py

import pydot_ng as pydot

G = pydot.Dot(graph_type='digraph',rankdir='LR',splines='false',dpi='250',pad='0',margin='0',fontsize='9.5')
G.set_node_defaults(label='',shape='circle',penwidth='0.35')
G.set_edge_defaults(arrowsize='0.5',penwidth='0.35')

I = pydot.Subgraph('cluster_i',graph_type='digraph',label='Input',margin='0',fontcolor='red',penwidth='0')
for i in range(1,4):
	I.add_node(pydot.Node("i%d" % i, color='red'))

H = pydot.Subgraph('cluster_h',graph_type='digraph',label='Hidden',margin='0',fontcolor='blue',penwidth='0')
for i in range(1,5):
	H.add_node(pydot.Node("h%d" % i, color='blue'))

O = pydot.Subgraph('cluster_o',graph_type='digraph',label='Output',margin='0',fontcolor='darkgreen',penwidth='0')
for i in range(1,3):
	O.add_node(pydot.Node("o%d" % i, color='darkgreen'))

G.add_subgraph(I)
G.add_subgraph(H)
G.add_subgraph(O)

for i in range(1,4):
  for j in range(1,5):
    G.add_edge(pydot.Edge('i%d' % i, 'h%d' % j))

for i in range(1,5):
  for j in range(1,3):
    G.add_edge(pydot.Edge('h%d' % i, 'o%d' % j))

G.write_png('/path/to/pydot_ann.png')

graphvizパッケージで作成したものと全く同じ結果が得られました。

pydot_ann

決定木の作成

ついでに、第1回の表紙画像に使用した決定木の作成方法を見ておきます。
Anacondaをご利用の方は不要ですが、一応準備作業を書いておきます。

pip install scikit-learn
pip install numpy
pip install scipy

コードは以下の通り、たったこれだけです。内容を理解するには機械学習の知識が必要となりますが、Graphvizの解説の範囲を超えるため解説は省略します。

# py_iris.py

from sklearn import datasets
from sklearn import tree

iris = datasets.load_iris()
X = iris.data
y = iris.target

clf = tree.DecisionTreeClassifier(max_depth=5)
clf.fit(X, y)

tree.export_graphviz(
        clf,
        out_file="py_iris.dot",
        feature_names=iris.feature_names,
        class_names=iris.target_names,
        rounded=True,
        filled=True
    )

この例ではsklearnから直接dotを生成しているので、いままで見てきたDOTファイルの作成方法の知識がまったく必要ありません。このようにGraphvizはサードパーティのツールから利用されるケースが多いので、その意味でのサンプルとして、こういう使い方もあるんだな、という程度でお読みいただければ十分かと思います。

上のスクリプトを実行すると py_iris.dot ファイルができるので、dotコマンドを使用してpngを生成します。

dot -Tpng py_iris.dot -o py_iris.png

第1回の表紙画像とだいたい同じものができました(パラメータが異なるので若干違いますが)。

py_iris

ちなみに、このサンプルは Python で Graphviz を使用して 決定木 を作成する例題として頻繁に利用されるので、どこかで似たような画像を目にすることがあるかもしれません。

まとめ

Pythonを少しでも触ったことがある方であれば、PythonからGraphvizを使用するのが非常に簡単であることがお分かりいただけたと思います。

もう少し詳しく知りたい方は、こちら をご覧ください。