Edited at

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

More than 1 year has passed since last update.

言語処理100本ノック 2015の挑戦記録です。環境はUbuntu 16.04 LTS + Python 3.5.2 :: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧はこちらからどうぞ。


第5章: 係り受け解析


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



46. 動詞の格フレーム情報の抽出


45のプログラムを改変し,述語と格パターンに続けて項(述語に係っている文節そのもの)をタブ区切り形式で出力せよ.45の仕様に加えて,以下の仕様を満たすようにせよ.


  • 項は述語に係っている文節の単語列とする(末尾の助詞を取り除く必要はない)

  • 述語に係る文節が複数あるときは,助詞と同一の基準・順序でスペース区切りで並べる

「吾輩はここで始めて人間というものを見た」という例文(neko.txt.cabochaの8文目)を考える. この文は「始める」と「見る」の2つの動詞を含み,「始める」に係る文節は「ここで」,「見る」に係る文節は「吾輩は」と「ものを」と解析された場合は,次のような出力になるはずである.

始める  で      ここで

見る は を 吾輩は ものを


出来上がったコード:


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

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

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



実行結果:

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


result.txt(先頭部分)

生れる   で どこで

つく か が 生れたか 見当が
泣く で 所で
する は 事だけは
始める で ここで
見る は を 吾輩は ものを
捕える を 我々を
煮る て 捕えて
食う て 煮て
思う から なかったから
載せる に 掌に
持ち上げる て と 載せられて スーと
ある が 感じが
落ちつく で 上で
見る て を 落ちついて 顔を
いう と 人間と
見る ものの ものの
思う と ものだと
残る が でも 感じが 今でも

結果全体はGitHubにアップしています。


項(文節そのもの)の列挙

Chunkクラスにget_kaku_prt()を追加しました。文節の中の助詞を取得するもので、複数ある場合は格助詞を優先し、それでも複数ある場合は一番最後の助詞を返します。

それ以外のクラスや関数は前問のままです。

今回は文節そのものを列挙する必要があるため、該当するchunkのリストを作り、それを先にソートしています。


格フレームとは

調べてみたのですが、軽くググっただけでは深くは理解できませんでした^^;

どうやら、文を分析するための「格文法」という文法理論において、動詞と、それに関わる文節とのルールのようなものを格フレームと呼ぶようです。

詳しくはウィキペディア「格文法」などを参照してください。

 

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


実行結果には、100本ノックで用いるコーパス・データで配布されているデータの一部が含まれます。この第5章で用いているデータは青空文庫で公開されている夏目漱石の長編小説『吾輩は猫である』が元になっています。