2018年05月20日

言語処理100本ノックでPython入門 #44 - Gpaphvizとpydotで有向グラフを可視化

  

今日は、言語処理100本ノック 2015の問題44です。

■ 問題
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

44. 係り受け木の可視化
与えられた文の係り受け木を有向グラフとして可視化せよ.可視化には,係り受け木をDOT言語に変換し,Graphvizを用いるとよい.また,Pythonから有向グラフを直接的に可視化するには,pydotを使うとよい.

■ Graphvizのインストール

Graphviz を用いると良い、とのことなので、以下のコマンドでGraphvizをインストール.

brew install graphviz

すると、linkも必要だよといったメッセージが出てきたので、さらに以下のコマンドを投入。

brew link graphviz

■ pydot_ngのインストール

このGraphvizをPythonから使うには、pydotを使うようなので調べたら、Python3用には、互換性のある pydot_ngを使うのが良いみたい。

 で、
conda install pydot_ng
とやってみたのですが、パッケージが見つからないと出ます。
pydot_ngじゃなくて、pydot-ngらしいです。

conda install pydot-ng
でも、上のコマンドを実行したら、今度は、python2.7にダウングレードするようなそんなメッセージが出てきたので、キャンセル。

どうも、pydot-ngは、python3.4が必要のようです。僕の環境は、Python3.6です。しかたがないので、python3.4の環境を作成。
conda create --name py34 python=3.4

source activate py34

これで、pydot-ngを再度インストール。
 
conda install pydot-ng

その後、Visual Studio Code for Mac で Shift+Command+P で「Python: インタープリターを選択」を呼び出し、py34の環境を選択。

これで、Pythonのコードで、
import pydot_ng as pydot
すれば、pydotが使えるようになります。
 

■ pydotを使って可視化

問題文に「係り受け木をDOT言語に変換し」とあるので、最初、DOT言語(text)に変換する以下のようなコードを書きました。
def toDot(sentence):
    dot = 'digraph mygraph  {\n'
    for chunk in sentence:
        if chunk.done:    # chunkにはdoneメンバーを追加
            continue
        s = ''
        cur = chunk
        while cur.dst >= 0:
            s += cur.concatMorphs()
            s += ' -> '
            cur.done = True
            cur = sentence[cur.dst]
        if s != '':
            s = '    ' + s + cur.concatMorphs() + ';\n'
            dot += s
    dot += '}\n'
    return dot

これで、以下のような文字列が得られます。
digraph mygraph  {
    何でも -> 薄暗い -> 所で -> 泣いて -> 記憶している;
    じめじめした -> 所で -> 泣いて -> 記憶している;
    ニャーニャー -> 泣いて -> 記憶している;
    いた事だけは -> 記憶している;
}
DOT言語って初めて知ったので、これで本当に合っているのか分からないですが...

とりあえず、このDOT言語をpydotに食わせてみて、ダメだったら、toDot関数を直せばいいやと思ったのですが、どうやって、pydotにこのDOT言語を食わせたらよいかがわからないです。
googleで検索してみましたが、サンプルが見つかりません。 pydotにはそんな機能はもともと無いのかもしれません。

しかたがないので、Generating Graph Visualizations with pydot and Graphviz のコードを参考にして、Egdeオブジェクトを使って有向グラフを作成することにします。
def toDot(sentence):
    edges = []
    for chunk in sentence:
        if chunk.dst >= 0:
            edges.append((chunk.concatMorphs(), sentence[chunk.dst].concatMorphs()))
    return edges
まあ、DOT言語に変換する必要がないので、このほうがプログラム的にも簡単でした。 


■ Graphvizを呼び出す

pydotを使い、Graphvizを呼び出すには以下のようなコードを書きます。 これも先ほどのリンク先のページに出ていたコードを参考にしています。


    graph = pydot.Dot(graph_type='digraph')
    graph.set_node_defaults(fontname='Meiryo UI', fontsize='10')

    article = analyze()
    for s, t in toDot(article[4]):
        graph.add_edge(pydot.Edge(s, t))
    graph.write_png('result44.png')

2行目はフォントを変更するためのコードです。

■ Pythonのコード

できあがったPythonのコードです。Morphクラス、Chunkクラスは変更なし、analyze関数も変更なしでしす。
import re
import os
import functools
import pydot_ng as pydot

class Morph:
    def __init__(self, surface, base, pos, pos1):
        self.surface = surface
        self.base = base
        self.pos = pos
        self.pos1 = pos1

    def toList(self):
        return [self.surface, self.base, self.pos, self.pos1]

class Chunk:
    def __init__(self, number, dst):
        self.number = number
        self.morphs = []
        self.dst = dst
        self.srcs = []

    def print(self):
        print(self.number)
        print([x.toList() for x in self.morphs])
        print(self.dst, self.srcs)
        print()

    def concatMorphs(self):
        seq = filter(lambda x: x.pos != '記号', self.morphs)
        return functools.reduce(lambda x, y: x + y.surface, seq, '')



def analyze():
    article = []
    sentence = []
    chunk = None
    with open('neko.txt.cabocha', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'\t|,|\n| ', line)
            if line[0] == '*':
                num = int(words[1])
                destNo = int(words[2].rstrip('D'))
                chunk = Chunk(num, destNo)
                sentence.append(chunk)
            elif words[0] == 'EOS':
                if sentence:
                    for index, c in enumerate(sentence, 0):
                        sentence[c.dst].srcs.append(index)
                    article.append(sentence)
                sentence = []
            else:
                chunk.morphs.append(Morph(
                    words[0],
                    words[7],
                    words[1],
                    words[2],
                ))
    return article

def toDot(sentence):
    edges = []
    for chunk in sentence:
        if chunk.dst >= 0:
            edges.append((chunk.concatMorphs(), sentence[chunk.dst].concatMorphs()))
    return edges

def main():
    graph = pydot.Dot(graph_type='digraph')
    graph.set_node_defaults(fontname='Meiryo UI', fontsize='10')

    article = analyze()
    for s, t in toDot(article[4]):
        graph.add_edge(pydot.Edge(s, t))
    graph.write_png('result44.png')

if __name__ == '__main__':
    main()

ソースコードは、GitHubで公開しています。


■ 結果 

出力したpngファイルです。

result44