言語処理100本ノック 2015の挑戦記録です。環境はUbuntu 16.04 LTS + Python 3.5.2 :: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧はこちらからどうぞ。
第5章: 係り受け解析
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.
###44. 係り受け木の可視化
与えられた文の係り受け木を有向グラフとして可視化せよ.可視化には,係り受け木をDOT言語に変換し,Graphvizを用いるとよい.また,Pythonから有向グラフを直接的に可視化するには,pydotを使うとよい.
####出来上がったコード:
# coding: utf-8
import CaboCha
import re
import pydot_ng as pydot
fname = 'neko.txt.tmp'
fname_parsed = 'neko.txt.cabocha.tmp'
def parse_neko():
'''「吾輩は猫である」を係り受け解析
「吾輩は猫である」(neko.txt)を係り受け解析してneko.txt.cabochaに保存する
'''
with open(fname) as data_file, \
open(fname_parsed, mode='w') as out_file:
cabocha = CaboCha.Parser()
for line in data_file:
out_file.write(
cabocha.parse(line).toString(CaboCha.FORMAT_LATTICE)
)
class Morph:
'''
形態素クラス
表層形(surface)、基本形(base)、品詞(pos)、品詞細分類1(pos1)を
メンバー変数に持つ
'''
def __init__(self, surface, base, pos, pos1):
'''初期化'''
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1
def __str__(self):
'''オブジェクトの文字列表現'''
return 'surface[{}]\tbase[{}]\tpos[{}]\tpos1[{}]'\
.format(self.surface, self.base, self.pos, self.pos1)
class Chunk:
'''
文節クラス
形態素(Morphオブジェクト)のリスト(morphs)、係り先文節インデックス番号(dst)、
係り元文節インデックス番号のリスト(srcs)をメンバー変数に持つ
'''
def __init__(self):
'''初期化'''
self.morphs = []
self.srcs = []
self.dst = -1
def __str__(self):
'''オブジェクトの文字列表現'''
surface = ''
for morph in self.morphs:
surface += morph.surface
return '{}\tsrcs{}\tdst[{}]'.format(surface, self.srcs, self.dst)
def normalized_surface(self):
'''句読点などの記号を除いた表層形'''
result = ''
for morph in self.morphs:
if morph.pos != '記号':
result += morph.surface
return result
def chk_pos(self, pos):
'''指定した品詞(pos)を含むかチェックする
戻り値:
品詞(pos)を含む場合はTrue
'''
for morph in self.morphs:
if morph.pos == pos:
return True
return False
def neco_lines():
'''「吾輩は猫である」の係り受け解析結果のジェネレータ
「吾輩は猫である」の係り受け解析結果を順次読み込んで、
1文ずつChunkクラスのリストを返す
戻り値:
1文のChunkクラスのリスト
'''
with open(fname_parsed) as file_parsed:
chunks = dict() # idxをkeyにChunkを格納
idx = -1
for line in file_parsed:
# 1文の終了判定
if line == 'EOS\n':
# Chunkのリストを返す
if len(chunks) > 0:
# chunksをkeyでソートし、valueのみ取り出し
sorted_tuple = sorted(chunks.items(), key=lambda x: x[0])
yield list(zip(*sorted_tuple))[1]
chunks.clear()
else:
yield []
# 先頭が*の行は係り受け解析結果なので、Chunkを作成
elif line[0] == '*':
# Chunkのインデックス番号と係り先のインデックス番号取得
cols = line.split(' ')
idx = int(cols[1])
dst = int(re.search(r'(.*?)D', cols[2]).group(1))
# Chunkを生成(なければ)し、係り先のインデックス番号セット
if idx not in chunks:
chunks[idx] = Chunk()
chunks[idx].dst = dst
# 係り先のChunkを生成(なければ)し、係り元インデックス番号追加
if dst != -1:
if dst not in chunks:
chunks[dst] = Chunk()
chunks[dst].srcs.append(idx)
# それ以外の行は形態素解析結果なので、Morphを作りChunkに追加
else:
# 表層形はtab区切り、それ以外は','区切りでバラす
cols = line.split('\t')
res_cols = cols[1].split(',')
# Morph作成、リストに追加
chunks[idx].morphs.append(
Morph(
cols[0], # surface
res_cols[6], # base
res_cols[0], # pos
res_cols[1] # pos1
)
)
def graph_from_edges_ex(edge_list, directed=False):
'''pydot_ng.graph_from_edges()のノード識別子への対応版
graph_from_edges()のedge_listで指定するタプルは
識別子とグラフ表示時のラベルが同一のため、
ラベルが同じだが実体が異なるノードを表現することができない。
例えば文の係り受けをグラフにする際、文の中に同じ単語が
複数出てくると、それらのノードが同一視されて接続されてしまう。
この関数ではedge_listとして次の書式のタプルを受け取り、
ラベルが同一でも識別子が異なるノードは別ものとして扱う。
edge_list = [((識別子1,ラベル1),(識別子2,ラベル2)), ...]
識別子はノードを識別するためのもので表示されない。
ラベルは表示用で、同じでも識別子が異なれば別のノードになる。
なお、オリジナルの関数にあるnode_prefixは未実装。
戻り値:
pydot.Dotオブジェクト
'''
if directed:
graph = pydot.Dot(graph_type='digraph')
else:
graph = pydot.Dot(graph_type='graph')
for edge in edge_list:
id1 = str(edge[0][0])
label1 = str(edge[0][1])
id2 = str(edge[1][0])
label2 = str(edge[1][1])
# ノード追加
graph.add_node(pydot.Node(id1, label=label1))
graph.add_node(pydot.Node(id2, label=label2))
# エッジ追加
graph.add_edge(pydot.Edge(id1, id2))
return graph
# 対象文字列を入力してもらい、そのままfnameに保存
with open(fname, mode='w') as out_file:
out_file.write(input('文字列を入力してください--> '))
# 係り受け解析
parse_neko()
# 1文ずつリスト作成
for chunks in neco_lines():
# 係り先があるものを列挙
edges = []
for i, chunk in enumerate(chunks):
if chunk.dst != -1:
# 記号を除いた表層形をチェック、空なら除外
src = chunk.normalized_surface()
dst = chunks[chunk.dst].normalized_surface()
if src != '' and dst != '':
edges.append(((i, src), (chunk.dst, dst)))
# 描画
if len(edges) > 0:
graph = graph_from_edges_ex(edges, directed=True)
graph.write_png('result.png')
####実行結果:
実行結果をいくつか載せておきます。
文字列を入力してください--> どこで生れたかとんと見当がつかぬ。
文字列を入力してください--> 何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
文字列を入力してください--> 吾輩はここで始めて人間というものを見た。
いい感じ!
###pydotのインストール
まず、問題に取り組む前に環境構築です。
問題で勧められているpydot
ですが、Python 3に対応したpydot-ng
というものがありましたので今回はこれを利用しました。
pydot-ng
にはpyparsing
とGraphViz
が必要なので、今回追加で必要なのは次の3つです。
必要なパッケージ | 概要 | オフィシャルサイト |
---|---|---|
pyparsing | DOT言語のファイルを解析する際に利用されるライブラリです。DOT言語は、データ構造を表すグラフを表現するためのデータ記述言語です。 | pyparsing |
GraphViz | DOT言語のグラフ情報から画像ファイルを生成するツールです。 | Graphviz - Graph Visualization Software |
pydot-ng | GraphVizをPythonで使うためのライブラリです。pydotと互換があります。 | pydot - Python interface to Graphviz's Dot language |
####pyparsingのインストール
condaで探してみたところ、すでに入っていました。*
が付いているのはその印です。どうやらAnacondaでインストールされていたようです。
segavvy@ubuntu:~$ conda search pyparsing
Fetching package metadata .......
pyparsing 1.5.6 py26_0 defaults
1.5.6 py27_0 defaults
1.5.6 py33_0 defaults
2.0.1 py26_0 defaults
2.0.1 py27_0 defaults
2.0.1 py33_0 defaults
2.0.1 py34_0 defaults
2.0.3 py26_0 defaults
2.0.3 py27_0 defaults
2.0.3 py33_0 defaults
2.0.3 py34_0 defaults
2.0.3 py35_0 defaults
2.1.1 py27_0 defaults
2.1.1 py34_0 defaults
2.1.1 py35_0 defaults
2.1.4 py27_0 defaults
2.1.4 py34_0 defaults
* 2.1.4 py35_0 defaults
####GraphVizのインストール
続いてGraphVizのインストールです。
condaで見つかったので簡単にインストールできました。ついでにいろいろ新しくしてくれた模様です。いいですね。
segavvy@ubuntu:~$ conda search graphviz
Fetching package metadata .......
graphviz 2.38.0 2 defaults
segavvy@ubuntu:~$ conda install graphviz
Fetching package metadata .......
Solving package specifications: ..........
Package plan for installation in environment /home/segavvy/anaconda3:
The following packages will be downloaded:
package | build
---------------------------|-----------------
conda-env-2.6.0 | 0 502 B
expat-2.1.0 | 0 365 KB
libtool-2.4.2 | 0 547 KB
pixman-0.32.6 | 0 2.4 MB
glib-2.43.0 | 1 5.4 MB
ruamel_yaml-0.11.14 | py35_0 377 KB
conda-4.2.12 | py35_0 387 KB
cairo-1.12.18 | 6 594 KB
harfbuzz-0.9.39 | 1 1.1 MB
pango-1.39.0 | 1 668 KB
graphviz-2.38.0 | 2 12.0 MB
------------------------------------------------------------
Total: 23.7 MB
The following NEW packages will be INSTALLED:
cairo: 1.12.18-6
expat: 2.1.0-0
glib: 2.43.0-1
graphviz: 2.38.0-2
harfbuzz: 0.9.39-1
libtool: 2.4.2-0
pango: 1.39.0-1
pixman: 0.32.6-0
The following packages will be UPDATED:
conda: 4.1.6-py35_0 --> 4.2.12-py35_0
conda-env: 2.5.1-py35_0 --> 2.6.0-0
ruamel_yaml: 0.11.7-py35_0 --> 0.11.14-py35_0
Proceed ([y]/n)? y
Fetching packages ...
conda-env-2.6. 100% |################################| Time: 0:00:00 533.05 kB/s
expat-2.1.0-0. 100% |################################| Time: 0:00:00 1.35 MB/s
libtool-2.4.2- 100% |################################| Time: 0:00:00 2.70 MB/s
pixman-0.32.6- 100% |################################| Time: 0:00:01 2.49 MB/s
glib-2.43.0-1. 100% |################################| Time: 0:00:01 2.99 MB/s
ruamel_yaml-0. 100% |################################| Time: 0:00:00 5.87 MB/s
conda-4.2.12-p 100% |################################| Time: 0:00:00 3.74 MB/s
cairo-1.12.18- 100% |################################| Time: 0:00:00 5.90 MB/s
harfbuzz-0.9.3 100% |################################| Time: 0:00:00 6.05 MB/s
pango-1.39.0-1 100% |################################| Time: 0:00:00 2.71 MB/s
graphviz-2.38. 100% |################################| Time: 0:00:01 7.16 MB/s
Extracting packages ...
[ COMPLETE ]|###################################################| 100%
Unlinking packages ...
[ COMPLETE ]|###################################################| 100%
Linking packages ...
[ COMPLETE ]|###################################################| 100%
GraphVizが正しく入るとdot
コマンドが使えるようになるので、確認しておきます。
segavvy@ubuntu:~$ dot -V
dot - graphviz version 2.38.0 (20140413.2041)
####pydot-ngのインストール
ここまでは順調でしたがpydot-ngのインストールでつまずきました。環境が合わないというエラーのようです。
segavvy@ubuntu:~$ conda install pydot-ng
Fetching package metadata .......
Solving package specifications: ....
UnsatisfiableError: The following specifications were found to be in conflict:
- pydot-ng
- python 3.5*
Use "conda info <package>" to see the dependencies for each package.
言われたとおりconda info
してみます。
segavvy@ubuntu:~$ conda info pydot-ng
Fetching package metadata .......
pydot-ng 1.0.0.15 py27_0
------------------------
file name : pydot-ng-1.0.0.15-py27_0.tar.bz2
name : pydot-ng
version : 1.0.0.15
build number: 0
build string: py27_0
channel : defaults
size : 45 KB
date : 2015-09-09
fn : pydot-ng-1.0.0.15-py27_0.tar.bz2
license : MIT
md5 : 8b81a344723e64ec3545b5f030caca47
priority : 0
schannel : defaults
url : https://repo.continuum.io/pkgs/free/linux-64/pydot-ng-1.0.0.15-py27_0.tar.bz2
dependencies:
pyparsing
python 2.7*
pydot-ng 1.0.0.15 py34_0
------------------------
file name : pydot-ng-1.0.0.15-py34_0.tar.bz2
name : pydot-ng
version : 1.0.0.15
build number: 0
build string: py34_0
channel : defaults
size : 46 KB
date : 2015-09-09
fn : pydot-ng-1.0.0.15-py34_0.tar.bz2
license : MIT
md5 : 13e3a10b45edfb38d91a51d6b3ccabc7
priority : 0
schannel : defaults
url : https://repo.continuum.io/pkgs/free/linux-64/pydot-ng-1.0.0.15-py34_0.tar.bz2
dependencies:
pyparsing
python 3.4*
どうやらPython 3.4用のパッケージはPython 3.5には入れられない、ということのようです。でも世の中Python 3.5で使っている方もいるので、0.1の差で弾かないで欲しい...
condaのサイトの説明(Resolution: Fix the conflicts in the installation request)も見てみましたが、無理やり入れる方法はわかりませんでした。
仕方ないのでcondaでのインストールは諦めて、pipで挑戦です。
segavvy@ubuntu:~$ pip search pydot-ng
pypayd-ng (0.0.6) - A small daemon for processing
bitcoin payments compatible with
modern HD wallets
maestro-ng (0.4.1) - Orchestrator for multi-host
Docker deployments
django-auth-ldap-ng (1.7.6) - Django LDAP authentication
backend
pydot (1.2.3) - Python interface to Graphviz's
Dot
telescope-ng (0.1) - Observe SPARQLing RDF
constellations through Python
objects.
django-dajax-ng (0.9.4) - Easy to use library to create
asynchronous presentation logic
with django and dajaxice-ng
django-dajaxice-ng (0.7.0.7) - Agnostic and easy to use ajax
library for django
django-macaddress-ng (1.1.1) - MAC address model and form fields
for Django apps.
django-rest-hooks-ng (1.1.1) - A powerful mechanism for sending
real time API notifications via a
new subscription model.
django-ninjapaginator-ng (0.1.6) - Django application with multiple
type of pagination integrated
cmsplugin-text-ng (0.6) - django-cms improved text plugin
ApplianceKit-NG (0.6.2) - Tools to programatically create
distribution images from any
distribution
pydot-ng (1.0.0) - Python interface to Graphviz's
Dot
suds-ng (0.4.1) - Lightweight SOAP client - fork of
suds
leancloud-sdk-ng (2.0.0) - LeanCloud Python SDK
django-extra-views-ng (0.3.3) - Extra class-based views for
Django
django-cas-ng (3.5.6) - CAS 1.0/2.0 client authentication
backend for Django (inherited
from django-cas)
booby-ng (0.8.3.post1) - Data modeling and validation
Python library
keyserver-ng (0.1.0) - PGP keyserver with email and key
validation.
pysensu-ng (0.7.0) - This is a client to interact with
the Sensu API
django-crispy-forms-ng (2.0.0) - Best way to have Django DRY forms
of the next generation.
caffeine-ng (3.3.8) - A status bar application able to
temporarily prevent the
django-oauth2-provider-ng (0.2.7.4) - Provide OAuth2 access to your app
Flask-CDN-NG (1.3.0) - Serve the static files in your
Flask app from a CDN.
tlslite-ng (0.7.0-alpha2) - Pure python implementation of SSL
and TLS.
python-kyototycoon-ng (0.7.3) - Python client library for the
Kyoto Tycoon key-value store
brutal-ng (0.3.11) - The new generation of brutal, a
multi-network asynchronouschat
bot framework using twisted
django-timezone-field-ng (2.0) - A Django app providing database
and form fields for pytz timezone
objects.
django-tagging-ng (0.3.3) - Enhanced tagging application for
Django, based on django-tagging
cplay-ng (2.1.2) - A curses front-end for various
audio players
arrow-ng (0.5.0) - Better dates and times for Python
ng-mini (0.1.1) - rrdtool based server monitor
tool,and a web interface include
django-filebased-email-backend-ng (2.0.2) - A better 'file' email backend for
Django
micromodels-ng (0.6.3) - Declarative dictionary-based
model classes for Python
iencode-ng (0.9.5) - iPhone video encoding tools.
django-polymorphic-ng (0.8.0) - Seamless Polymorphic Inheritance
for Django Models
django-mockups-ng (0.5.0) - Provides tools to auto generate
content.
pytest-datadir-ng (1.1.0) - Fixtures for pytest allowing test
functions/methods to easily
retrieve test resources from the
local filesystem.
django-js-utils-ng (0.5.futu) - Django URL Exposure to Javascript
crontab-ng (0.20.3) - Parse and use crontab schedules
in Python
purity-ng (0.1) - general purpose purity testing
software
django-tastypie-swagger-ng (0.1.3) - An adapter to use swagger-ui with
django-tastypie
django-multilingual-ng (0.1.21) - Multilingual extension for Django
- NG
wymypy-ng (2.1) - Simple web interface for
controlling MPD
sinaweibopy-ng (1.1.5) - Sina Weibo OAuth 2 API Python SDK
pyLisp-NG (2.0.0) - A very simple implementation of
Lisp in Python that is perfectly
suitable for Python projects
needing Lisp-like capabilities.
checkbox-ng (0.29) - Checkbox - Command Line Test
Runner
You are using pip version 8.1.2, however version 9.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
なんか検索ノイズだらけですが、10数行目のところにpydot-ngがありました。
最後の2行でpipそのもののアップグレードをお勧めされているので、先にpipをアップグレードします。
segavvy@ubuntu:~$ pip install --upgrade pip
Collecting pip
Downloading pip-9.0.1-py2.py3-none-any.whl (1.3MB)
100% |████████████████████████████████| 1.3MB 827kB/s
Installing collected packages: pip
Found existing installation: pip 8.1.2
Uninstalling pip-8.1.2:
Successfully uninstalled pip-8.1.2
Successfully installed pip-9.0.1
続いてpydot-ngのインストールです。
segavvy@ubuntu:~$ pip install pydot-ng
Collecting pydot-ng
Downloading pydot_ng-1.0.0.zip
Requirement already satisfied: pyparsing>=2.0.1 in ./anaconda3/lib/python3.5/site-packages (from pydot-ng)
Building wheels for collected packages: pydot-ng
Running setup.py bdist_wheel for pydot-ng ... done
Stored in directory: /home/segavvy/.cache/pip/wheels/4f/09/d5/f96fd2578831e1b9021c634f057ab5306a3e4287efa800de29
Successfully built pydot-ng
Installing collected packages: pydot-ng
Successfully installed pydot-ng-1.0.0
無事インストールできました。
Pythonでimportできることを確認しておきます。なお、Installation:のところにあるように、import時はpydot_ng
です。ngの前は-
(ハイフン)ではなく_
(アンダースコア)なのでご注意ください。
segavvy@ubuntu:~$ python
Python 3.5.2 |Anaconda 4.1.1 (64-bit)| (default, Jul 2 2016, 17:53:06)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pydot_ng
>>>
これでやっと問題が始められます。ふぅ。
###問題42のコードの再利用
今回の問題は「吾輩は猫である」(neko.txt
)とは関係なく「与えられた文」を処理するのですが、グラフを書く直前までにやることは問題42とまったく同じです。そこでコードを最大限再利用するために(=最大限手を抜くために^^;)、入力された内容で「吾輩は猫である」の代わりのファイルを作り、これまでの処理をそのまま流す形にしました。
クラスと関数は問題42のままですが、ファイル名はneko.txt.tmp
とneko.txt.cabocha.tmp
に変更しています。
###有向グラフとは
ウィキブックスのグラフ理論の解説が分かりやすいです。最初の方を読めば、今回の問題には十分そうでした。
###pydotの使い方
pydotはまとまった解説が見つけられず、ちょっと苦戦しました。いろんなサイトから寄せ集めてきた知識をベースに、最終的にシンプルなコードに落ち着きました。
まず最初に、ノード(今回の実行結果の図における丸で囲まれた節の部分)をペアにしたタプルを作って、リストに詰め込んでいきます。このノードのペアがエッジ(実行結果の図における線の部分)で結ばれる形になります。
詰め込み終わったら~~pydot.graph_from_edges()
~~graph_from_edges_ex()
で、pydot.Dot
オブジェクトに変換します。この時にdirected=True
を指定すると有向グラフとなり、エッジが矢印になります。この関数のおかげで、DOT言語を知らなくても簡単にグラフが書けます。問題文にある「Pythonから有向グラフを直接的に可視化するには,pydotを使うとよい.」の「直接的」とは、DOT言語を使わずに済むことを指しているようです。
最後にpydot.Dot.write()
でファイルに出力します。この関数はwrite_png()
やwrite_pdf()
やwrite_ps()
といった感じで、関数名によってフォーマットを指定できます。今回はpngにしてみました。
2016/12/25追記
コードを修正しました。当初はpydotに用意されているpydot.graph_from_edges()
を使ったシンプルなコードだったのですが、これだと1つの文の中で同じ表記の文節があった場合に、混ざってしまうことに気づきました。
例えば「今日の天気は晴れでしたが、明日の天気は悪くなるそうです。」という文の場合、修正前のコードだと次のようになってしまいます。
しかし、文中に出てくる2つの「天気」は別物で、次のように区別する必要があります。
そこで、stack overflowにあった pydot: is it possible to plot two different nodes with the same string in them? という書き込みとpydot.graph_from_edges()
のソースを参考に、graph_from_edges_ex()
を作りました。
###pydotの仕様の調べ方?
前述のようにpydotについてはまとまった解説が見つけられなかったため、仕様が分からない部分はhelp()
で確認しました。
Pythonのモジュールは、docstringという仕組みに従ってソースにコメントが入って入ればhelp()
で確認できます。例えばpydot.graph_from_edges()
の仕様は、次のような感じで確認できます。
segavvy@ubuntu:~/ドキュメント/言語処理100本ノック2015/44$ python
Python 3.5.2 |Anaconda 4.1.1 (64-bit)| (default, Jul 2 2016, 17:53:06)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pydot_ng as pydot
>>> help(pydot.graph_from_edges)
Help on function graph_from_edges in module pydot_ng:
graph_from_edges(edge_list, node_prefix='', directed=False)
Creates a basic graph out of an edge list.
The edge list has to be a list of tuples representing
the nodes connected by the edge.
The values can be anything: bool, int, float, str.
If the graph is undirected by default, it is only
calculated from one of the symmetric halves of the matrix.
ただ、これでもちょっと説明が足りないので、ソースも覗いてみました。ソースの場所は__file__
で確認できます。
>>> print(pydot.__file__)
/home/segavvy/anaconda3/lib/python3.5/site-packages/pydot_ng/__init__.py
このファイルを開いてdef graph_from_edges
で検索すると実装部分が確認できます。Pythonで作られているモジュールは、この流れでソースの確認できるみたいです。仕様が良く分からないものはソースを確認するのが良さそうですね。
45本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。
実行結果には、100本ノックで用いるコーパス・データで配布されているデータの一部が含まれます。この第5章で用いているデータは青空文庫で公開されている夏目漱石の長編小説『吾輩は猫である』が元になっています。