Edited at

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

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



47. 機能動詞構文のマイニング


動詞のヲ格にサ変接続名詞が入っている場合のみに着目したい.46のプログラムを以下の仕様を満たすように改変せよ.


  • 「サ変接続名詞+を(助詞)」で構成される文節が動詞に係る場合のみを対象とする

  • 述語は「サ変接続名詞+を+動詞の基本形」とし,文節中に複数の動詞があるときは,最左の動詞を用いる

  • 述語に係る助詞(文節)が複数あるときは,すべての助詞をスペース区切りで辞書順に並べる

  • 述語に係る文節が複数ある場合は,すべての項をスペース区切りで並べる(助詞の並び順と揃えよ)

例えば「別段くるにも及ばんさと、主人は手紙に返事をする。」という文から,以下の出力が得られるはずである.

返事をする      と に は        及ばんさと 手紙に 主人は

このプログラムの出力をファイルに保存し,以下の事項をUNIXコマンドを用いて確認せよ.


  • コーパス中で頻出する述語(サ変接続名詞+を+動詞)

  • コーパス中で頻出する述語と助詞パターン



出来上がったコード:


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]) # 項
))



実行結果:

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


result.txt(先頭部分)

決心をする と あるこうと

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

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


UNIXコマンド確認用のシェルスクリプト


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で切り出してuniqsortで集計、ソートしています。いずれのコマンドも問題19などで出てきたものです。


結果の確認:

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


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 いたずらをする

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


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にアップしています。


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

Chunkクラスにget_sahen_wo()を追加しました。文節の中に「サ変接続名詞+を(助詞)」がある場合に、その表層形を返します。それ以外のクラスや関数は前問のままです。

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


機能動詞とは

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

 

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


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