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


[言語処理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というファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

###45. 動詞の格パターンの抽出
>今回用いている文章をコーパスと見なし，日本語の述語が取りうる格を調査したい．動詞を述語，動詞に係っている文節の助詞を格と考え，述語と格をタブ区切り形式で出力せよ．ただし，出力は以下の仕様を満たすようにせよ．
>
> - 動詞を含む文節において，最左の動詞の基本形を述語とする
> - 述語に係る助詞を格とする
> - 述語に係る助詞（文節）が複数あるときは，すべての助詞をスペース区切りで辞書順に並べる
>
>「吾輩はここで始めて人間というものを見た」という例文（neko.txt.cabochaの8文目）を考える．この文は「始める」と「見る」の２つの動詞を含み，「始める」に係る文節は「ここで」，「見る」に係る文節は「吾輩は」と「ものを」と解析された場合は，次のような出力になるはずである．
>
>```
>始める  で
>見る    は を
>```
>
>このプログラムの出力をファイルに保存し，以下の事項を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 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

			# 係り元の列挙
			prts = []
			for src in chunk.srcs:

				# 助詞を検索
				prts_in_chunk = chunks[src].get_morphs_by_pos('助詞')
				if len(prts_in_chunk) > 1:

					# Chunk内に2つ以上助詞がある場合は、格助詞を優先
					kaku_prts = chunks[src].get_morphs_by_pos('助詞', '格助詞')
					if len(kaku_prts) > 0:
						prts_in_chunk = kaku_prts

				if len(prts_in_chunk) > 0:
					prts.append(prts_in_chunk[-1])	# 抽出する助詞はChunk当たり最後の1つ

			if len(prts) < 1:
				continue

			# 出力
			out_file.write('{}\t{}\n'.format(
				verbs[0].base,		# 最左の動詞の基本系
				' '.join(sorted(prt.surface for prt in prts))	# 助詞は辞書順
			))
```

####実行結果：

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

```console:result.txt（先頭部分）
生れる	で
つく	か が
泣く	で
する	は
始める	で
見る	は を
捕える	を
煮る	て
食う	て
思う	から
載せる	に
持ち上げる	て と
ある	が
落ちつく	で
見る	て を
いう	と
見る	ものの
思う	と
残る	が でも
する	をもって
```

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

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

# ソートして重複除去して、その件数でソート
sort result.txt | uniq --count | sort --numeric-sort --reverse > "すべて.txt"

# 「する」のみ
grep "^する\s" result.txt | sort | uniq --count | sort --numeric-sort --reverse > "する.txt"

# 「見る」のみ
grep "^見る\s" result.txt | sort | uniq --count | sort --numeric-sort --reverse > "見る.txt"

# 「与える」のみ
grep "^与える\s" result.txt | sort | uniq --count | sort --numeric-sort --reverse > "与える.txt"
```

`grep`で検索し、`uniq`と`sort`で集計、ソートしています。`uniq`と`sort`については、[問題19](http://qiita.com/segavvy/items/58f11bce5f786e590d98)で出てきたものです。

####結果の確認：

長いものは先頭部分のみの抜粋です。

```console:すべて.txt（先頭部分）
    720 云う	と
    447 する	を
    344 思う	と
    218 なる	に
    202 する	に
    199 ある	が
    170 見る	て
    167 する	と
    121 する	が
    118 する	に を
     98 見える	と
     94 見る	を
     92 する	て を
     79 する	は
     66 ある	が に
     65 する	て
     60 する	が を
     59 する	と を
     54 云う	を
     54 する	で を
     54 ある	の
     53 行く	へ
     51 云う	が と
     50 する	から
```

```console:する.txt（先頭部分）
    447 する	を
    202 する	に
    167 する	と
    121 する	が
    118 する	に を
     92 する	て を
     79 する	は
     65 する	て
     60 する	が を
     59 する	と を
     54 する	で を
     50 する	から
     45 する	で
     44 する	も
     41 する	の
     39 する	と は
     37 する	から を
     36 する	は を
     34 する	が と
     33 する	が に
     25 する	に は
     23 する	と は を
     21 する	て に
```

```console:見る.txt（先頭部分）
    170 見る	て
     94 見る	を
     25 見る	て て
     20 見る	から
     17 見る	と
     16 見る	て を
     15 見る	で
     12 見る	から て
     11 見る	て は
      8 見る	に を
      8 見る	に
      8 見る	と を
      8 見る	が
      7 見る	が を
      6 見る	て と
      5 見る	て は を
      4 見る	も
      4 見る	で を
      4 見る	で は
      4 見る	て に
      4 見る	て で
      4 見る	て て は
      4 見る	から を
      4 見る	か て
```

```console:与える.txt
      4 与える	に を
      2 与える	て に を
      1 与える	も を
      1 与える	ば を
      1 与える	に に対して も
      1 与える	として を
      1 与える	で に を
      1 与える	て は を
      1 与える	て に は を
      1 与える	て に に は を
      1 与える	て と は を
      1 与える	けれども に を
      1 与える	が を
      1 与える	が て と に は は を
```

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

###文節から品詞指定で形態素を取り出し
今回の問題では、文節から動詞や助詞を取り出す必要があるため、[前問](http://qiita.com/segavvy/items/d1a9a8d87d8dc10a8f15)のChunkクラスに、指定した品詞の形態素をすべて取り出す`get_morphs_by_pos()`を追加しました。最左の動詞を使う場合は、この関数で取得した形態素の先頭を使っています。
それ以外の関数やクラスは[前問](http://qiita.com/segavvy/items/d1a9a8d87d8dc10a8f15)のままです。

###格とは
「格」とは、名詞にくっつけて主語や目的語であることを示したり、行為の対象や物体の所有者であることを示したりするためのマーク、ということのようです。詳細は[ウィキペディア「格」](https://ja.wikipedia.org/wiki/%E6%A0%BC)を参照してください。

なお、最初は問題文の指示どおりに助詞を格と見なして抽出していましたが、[ウィキペディア「助詞」](https://ja.wikipedia.org/wiki/%E5%8A%A9%E8%A9%9E)によると、助詞にもいろいろ種類があることがわかりました。どうやら助詞の中でも「格助詞」というのが格を示す助詞のようです。

また、文節内に複数の助詞が含まれることも分かりました。例えば「　別段くるにも及ばんさと、主人は手紙に返事をする。」のCaboChaの解析結果は次のようになります。文節2の「くるにも」や文節3の「及ばんさと、」には、それぞれ助詞が2つずつあります。

```console:「　別段くるにも及ばんさと、主人は手紙に返事をする。」のCaboCha解析結果
* 0 3D 0/0 0.455238
　	記号,空白,*,*,*,*,　,　,　,,
* 1 2D 0/0 0.948277
別段	副詞,一般,*,*,*,*,別段,ベツダン,ベツダン,,
* 2 3D 0/2 2.294919
くる	動詞,自立,*,*,カ変・クル,基本形,くる,クル,クル,くる/来る,
に	助詞,格助詞,一般,*,*,*,に,ニ,ニ,,
も	助詞,係助詞,*,*,*,*,も,モ,モ,,
* 3 7D 0/3 -1.816825
及ば	動詞,自立,*,*,五段・バ行,未然形,及ぶ,オヨバ,オヨバ,およば/及ば,
ん	助動詞,*,*,*,不変化型,基本形,ん,ン,ン,,
さ	助詞,終助詞,*,*,*,*,さ,サ,サ,,
と	助詞,格助詞,引用,*,*,*,と,ト,ト,,
、	記号,読点,*,*,*,*,、,、,、,,
* 4 7D 0/1 -1.816825
主人	名詞,一般,*,*,*,*,主人,シュジン,シュジン,,
は	助詞,係助詞,*,*,*,*,は,ハ,ワ,,
* 5 7D 0/1 -1.816825
手紙	名詞,一般,*,*,*,*,手紙,テガミ,テガミ,,
に	助詞,格助詞,一般,*,*,*,に,ニ,ニ,,
* 6 7D 0/1 -1.816825
返事	名詞,サ変接続,*,*,*,*,返事,ヘンジ,ヘンジ,,
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ,,
* 7 -1D 0/0 0.000000
する	動詞,自立,*,*,サ変・スル,基本形,する,スル,スル,,
。	記号,句点,*,*,*,*,。,。,。,,
EOS
```

問題45や46の問題文を読むと、抽出する助詞は1文節当たり1つという前提のようです。そこで、文節内に複数の助詞があった場合は、CaboCha(MeCab)の解析結果の品詞細分類1が格助詞のものを優先した上で、文節内の最後の1つだけを抽出するようにしてみました。

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

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