言語処理100本ノック 2015の挑戦記録です。環境はUbuntu 16.04 LTS + Python 3.5.2 :: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧はこちらからどうぞ。
第5章: 係り受け解析
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.
41. 係り受け解析結果の読み込み(文節・係り受け)
40に加えて,文節を表すクラスChunkを実装せよ.このクラスは形態素(Morphオブジェクト)のリスト(morphs),係り先文節インデックス番号(dst),係り元文節インデックス番号のリスト(srcs)をメンバ変数に持つこととする.さらに,入力テキストのCaboChaの解析結果を読み込み,1文をChunkオブジェクトのリストとして表現し,8文目の文節の文字列と係り先を表示せよ.第5章の残りの問題では,ここで作ったプログラムを活用せよ.
出来上がったコード:
# coding: utf-8
import CaboCha
import re
fname = 'neko.txt'
fname_parsed = 'neko.txt.cabocha'
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 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
					)
				)
		raise StopIteration
# 係り受け解析
parse_neko()
# 1文ずつリスト作成
for i, chunks in enumerate(neco_lines(), 1):
	# 8文目を表示
	if i == 8:
		for j, chunk in enumerate(chunks):
			print('[{}]{}'.format(j, chunk))
		break
実行結果:
「係り先を表示せよ」という問題ですが、Chunkクラスの実装確認も兼ねて係り元も表示しています。
[0]吾輩は	srcs[]	dst[5]
[1]ここで	srcs[]	dst[2]
[2]始めて	srcs[1]	dst[3]
[3]人間という	srcs[2]	dst[4]
[4]ものを	srcs[3]	dst[5]
[5]見た。	srcs[0, 4]	dst[-1]
CaboChaの解析結果のフォーマット
CaboChaによる係り受け解析結果は、形態素解析結果に対して*で始まる行が挿入されて、そこに係り受けの解析結果が出力される形になっています。
* 3 5D 1/2 0.656580
この行は空白区切りで、次の内容になっています。
| カラム | 意味 | 
|---|---|
| 1 | 先頭カラムは*。係り受け解析結果であることを示す。 | 
| 2 | 文節番号(0から始まる整数) | 
| 3 | 係り先番号+D
 | 
| 4 | 主辞/機能語の位置と任意の個数の素性列 | 
| 5 | 係り関係のスコア。係りやすさの度合で、一般に大きな値ほど係りやすい。 | 
今回の問題で使っているのはカラムの2と3のみです。解析結果の詳細はオフィシャルサイトCaboCha/南瓜: Yet Another Japanese Dependency Structure Analyzerをご参照ください。
Chunkオブジェクトの作成順序
今回悩んだのは、Chunkオブジェクトの作成順序です。
とりあえずneko.txt.cabochaを1行ずつ読んでいって、Chunkオブジェクトに格納する情報が1つでも取得できた時点で該当Chunkオブジェクトを作成し、すでに作ってあった場合はそこに情報を追加する、という流れで実装してみました。Chunkオブジェクトの作成順序は出現順ではなく、さらに辞書も使っていて中身が順不同なので、最後に文節番号でソートして取り出しています。
作ってから思いましたが、まず係り受けの情報なしで文節番号順にChunkオブジェクトを作成し、後から係り受けの情報をセットしていった方が実行効率は良かったかも知れません。
 
42本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。
実行結果には、100本ノックで用いるコーパス・データで配布されているデータの一部が含まれます。この第5章で用いているデータは青空文庫で公開されている夏目漱石の長編小説『吾輩は猫である』が元になっています。