素人の言語処理100本ノック:47


[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)の挑戦記録です。環境はUbuntu 16.04 LTS ＋ Python 3.5.2 \:\: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧は[こちら](http://qiita.com/segavvy/items/fb50ba8097d59475f760)からどうぞ。

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

###47. 機能動詞構文のマイニング
>動詞のヲ格にサ変接続名詞が入っている場合のみに着目したい．46のプログラムを以下の仕様を満たすように改変せよ．
>
> - 「サ変接続名詞+を（助詞）」で構成される文節が動詞に係る場合のみを対象とする
> - 述語は「サ変接続名詞+を+動詞の基本形」とし，文節中に複数の動詞があるときは，最左の動詞を用いる
> - 述語に係る助詞（文節）が複数あるときは，すべての助詞をスペース区切りで辞書順に並べる
> - 述語に係る文節が複数ある場合は，すべての項をスペース区切りで並べる（助詞の並び順と揃えよ）
>
>例えば「別段くるにも及ばんさと、主人は手紙に返事をする。」という文から，以下の出力が得られるはずである．
>
>```
>返事をする      と に は        及ばんさと 手紙に 主人は
>```
>
>このプログラムの出力をファイルに保存し，以下の事項をUNIXコマンドを用いて確認せよ．
>
> - コーパス中で頻出する述語（サ変接続名詞+を+動詞）
> - コーパス中で頻出する述語と助詞パターン

####出来上がったコード：

```python:main.py
# coding: utf-8
import CaboCha
import re

fname = 'neko.txt'
fname_parsed = 'neko.txt.cabocha'
fname_result = 'result.txt'


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 get_morphs_by_pos(self, pos, pos1=''):
		'''指定した品詞（pos）、品詞細分類1（pos1）の形態素のリストを返す
		pos1の指定がない場合はposのみで判定する

		戻り値：
		形態素（morph）のリスト、該当形態素がない場合は空のリスト
		'''
		if len(pos1) > 0:
			return [res for res in self.morphs
					if (res.pos == pos) and (res.pos1 == pos1)]
		else:
			return [res for res in self.morphs if res.pos == pos]

	def get_kaku_prt(self):
		'''助詞を1つ返す
		複数ある場合は格助詞を優先し、最後の助詞を返す。

		戻り値：
		助詞、ない場合は空文字列
		'''
		prts = self.get_morphs_by_pos('助詞')
		if len(prts) > 1:

			# 2つ以上助詞がある場合は、格助詞を優先
			kaku_prts = self.get_morphs_by_pos('助詞', '格助詞')
			if len(kaku_prts) > 0:
				prts = kaku_prts

		if len(prts) > 0:
			return prts[-1].surface		# 最後を返す
		else:
			return ''

	def get_sahen_wo(self):
		'''「サ変接続名詞＋を」を含無場合は、その部分の表層形を返す

		戻り値：
		「サ変接続名詞＋を」の文字列、なければ空文字列
		'''
		for i, morph in enumerate(self.morphs[0:-1]):

			if (morph.pos == '名詞') \
					and (morph.pos1 == 'サ変接続') \
					and (self.morphs[i + 1].pos == '助詞') \
					and (self.morphs[i + 1].surface == 'を'):
				return morph.surface + self.morphs[i + 1].surface

		return ''


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()

# 結果ファイル作成
with open(fname_result, mode='w') as out_file:

	# 1文ずつリスト作成
	for chunks in neco_lines():

		# chunkを列挙
		for chunk in chunks:

			# 動詞を含むかチェック
			verbs = chunk.get_morphs_by_pos('動詞')
			if len(verbs) < 1:
				continue

			# 係り元に助詞を含むchunkを列挙
			chunks_include_prt = []
			for src in chunk.srcs:
				if len(chunks[src].get_kaku_prt()) > 0:
					chunks_include_prt.append(chunks[src])
			if len(chunks_include_prt) < 1:
				continue

			# 係り元に「サ変接続名詞＋を（助詞）」があるかチェック
			sahen_wo = ''
			for chunk_src in chunks_include_prt:
				sahen_wo = chunk_src.get_sahen_wo()
				if len(sahen_wo) > 0:
					chunk_remove = chunk_src
					break
			if len(sahen_wo) < 1:
				continue

			# 「サ変接続名詞＋を（助詞）」は述語として動詞と一緒に出力するので係り元からは除外
			chunks_include_prt.remove(chunk_remove)

			# chunkを助詞の辞書順でソート
			chunks_include_prt.sort(key=lambda x: x.get_kaku_prt())

			# 出力
			out_file.write('{}\t{}\t{}\n'.format(
				sahen_wo + verbs[0].base,	# サ変接続名詞+を+最左の動詞の基本系
				' '.join([chunk.get_kaku_prt() \
						for chunk in chunks_include_prt]),		# 助詞
				' '.join([chunk.normalized_surface() \
						for chunk in chunks_include_prt])		# 項
			))
```

####実行結果：

以下、結果の先頭部分です。

```console:result.txt（先頭部分）
決心をする	と	あるこうと
返報をしてやる	んで	偸んで
昼寝をする		
昼寝をする	が	彼が
迫害を加える	て	追い廻して
生活をする	が て を	等猫族が 完くして 愛を
話をする		
投書をする	て へ	やって ほととぎすへ
話をする	に	時に
写生をする		
昼寝をする	て	出て
彩色を見る		
欠伸をする	から て て	なったから して 押し出して
報道をする	に	耳に
前後を忘れる	に	心に
挨拶をする		
御馳走を食う	と	見ると
問答をする		
雑談をする	ながら は	寝転びながら 黒は
自慢をする		
呼吸を飲み込む	から	なってから
思案を定める	と は	若くはないと 吾輩は
御馳走をあるく	って て	猟って なって
御馳走を食う		
放蕩をする		
放蕩をする	が	ものだからが
放蕩をする	より	云うよりも
放蕩をする		
写生を力む	に従って	忠告に従って
写生をする		
```

結果全体は[GitHub](https://github.com/segavvy/nlp100_Python/tree/master/47)にアップしています。

####UNIXコマンド確認用のシェルスクリプト
```bash:test.sh
#!/bin/sh

# 述語でソートして重複除去し、その件数でソート
cut --fields=1 result.txt | sort | uniq --count | sort --numeric-sort --reverse > "predicate.txt"

# 述語と助詞でソートして重複除去し、その件数でソート
cut --fields=1,2 result.txt | sort | uniq --count | sort --numeric-sort --reverse > "predicate_Particle.txt"
```

`cut`で切り出して`uniq`と`sort`で集計、ソートしています。いずれのコマンドも[問題19](http://qiita.com/segavvy/items/58f11bce5f786e590d98)などで出てきたものです。

####結果の確認：

コーパス中で頻出する述語（サ変接続名詞+を+動詞）の先頭部分です。

```console:predicate.txt（先頭部分）
     30 返事をする
     21 挨拶をする
     15 話をする
     14 真似をする
     12 喧嘩をする
      8 質問をする
      7 運動をする
      6 注意をする
      6 昼寝をする
      6 邪魔をする
      5 話を聞く
      5 問答をする
      5 病気をする
      5 相談をする
      5 質問をかける
      5 辞儀をする
      4 放蕩をする
      4 戦争をする
      4 散歩をする
      4 降参をする
      4 欠伸をする
      4 休養を要する
      4 演説をする
      4 いたずらをする
```

コーパス中で頻出する述語と助詞パターンの先頭部分です。

```console:predicate_particle.txt（先頭部分）
      8 真似をする	
      7 返事をする	と
      6 運動をする	
      5 喧嘩をする	
      4 話を聞く	
      4 話をする	
      4 返事をする	と は
      4 挨拶をする	と
      4 挨拶をする	から
      3 返事をする	
      3 質問をかける	と は
      3 喧嘩をする	と
      2 問答をする	
      2 放蕩をする	
      2 返事をする	から と
      2 平均を破る	
      2 病気をする	
      2 同情を表す	に
      2 注意を惹く	
      2 注意をする	
      2 深入りをする	
      2 写生をする	
      2 散歩をする	
      2 講義をする	
      2 行動をとる	
      2 工夫をする	
      2 交際をする	
      2 御無沙汰をする	
      2 休養を要する	は
      2 議論をする	て
      2 学問をする	
      2 覚悟をする	と
      2 運動をやる	
      2 安心を得る	が
      2 挨拶をする	と も
      2 挨拶をする	で
      2 挨拶をする	
      2 いたずらをする	
```

結果全体は[GitHub](https://github.com/segavvy/nlp100_Python/tree/master/47)にアップしています。

###述語に係る助詞が1つしかない場合の扱い

Chunkクラスに`get_sahen_wo()`を追加しました。文節の中に「サ変接続名詞+を（助詞）」がある場合に、その表層形を返します。それ以外のクラスや関数は[前問](http://qiita.com/segavvy/items/25511a18ad7928e3982f)のままです。

この問題で悩んだのは、「サ変接続名詞+を（助詞）」の文節以外に、動詞を含む文節に係る文節がなかった場合の扱いです。除去すべきなのか良くわからなかったので、「サ変接続名詞+を（助詞）」の文節しか動詞に係る文節がないものも残してあります。タブ区切りの2カラム目と3カラム目が空文字列になっているのはこのパターンです。


###機能動詞とは

動詞だけでは内容がわからず、名詞と組み合わせて内容がわかるようになる動詞だそうです。確かに「する」だけでは内容が分かりませんが「返事を」と組み合わせると内容が分かりますね。このような動詞のことです。

　
48本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。
<hr>

*実行結果には、[100本ノックで用いるコーパス・データ](http://www.cl.ecei.tohoku.ac.jp/nlp100/#data)で配布されているデータの一部が含まれます。この第5章で用いているデータは[青空文庫](http://www.aozora.gr.jp)で公開されている夏目漱石の長編小説『吾輩は猫である』が元になっています。*