言語処理100本ノック 2015「第5章: 係り受け解析」の45本目「動詞の格パターンの抽出」記録です。
if
の条件分岐も増え、少しずつ複雑になっています。アルゴリズム考えるのがやや面倒です。
参考リンク
リンク | 備考 |
---|---|
045.動詞の格パターンの抽出.ipynb | 回答プログラムのGitHubリンク |
素人の言語処理100本ノック:45 | 多くのソース部分のコピペ元 |
CaboCha公式 | 最初に見ておくCaboChaのページ |
環境
CRF++とCaboChaはインストールしたのが昔すぎてインストール方法忘れました。全然更新されていないパッケージなので、環境再構築もしていません。CaboChaをWindowsで使おうと思い、挫折した記憶だけはあります。確か64bitのWindowsで使えなかった気がします(記憶が曖昧だし私の技術力の問題も多分にあるかも)。
種類 | バージョン | 内容 |
---|---|---|
OS | Ubuntu18.04.01 LTS | 仮想で動かしています |
pyenv | 1.2.16 | 複数Python環境を使うことがあるのでpyenv使っています |
Python | 3.8.1 | pyenv上でpython3.8.1を使っています パッケージはvenvを使って管理しています |
Mecab | 0.996-5 | apt-getでインストール |
CRF++ | 0.58 | 昔すぎてインストール方法忘れました(多分make install ) |
CaboCha | 0.69 | 昔すぎてインストール方法忘れました(多分make install ) |
第5章: 係り受け解析
学習内容
『吾輩は猫である』に係り受け解析器CaboChaを適用し,係り受け木の操作と統語的な分析を体験します.
ノック内容
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.
45. 動詞の格パターンの抽出
今回用いている文章をコーパスと見なし,日本語の述語が取りうる格を調査したい.動詞を述語,動詞に係っている文節の助詞を格と考え,述語と格をタブ区切り形式で出力せよ.ただし,出力は以下の仕様を満たすようにせよ.
- 動詞を含む文節において,最左の動詞の基本形を述語とする
- 述語に係る助詞を格とする
- 述語に係る助詞(文節)が複数あるときは,すべての助詞をスペース区切りで辞書順に並べる
「吾輩はここで始めて人間というものを見た」という例文(neko.txt.cabochaの8文目)を考える.この文は「始める」と「見る」の2つの動詞を含み,「始める」に係る文節は「ここで」,「見る」に係る文節は「吾輩は」と「ものを」と解析された場合は,次のような出力になるはずである.
始める で 見る は を
このプログラムの出力をファイルに保存し,以下の事項をUNIXコマンドを用いて確認せよ.
- コーパス中で頻出する述語と格パターンの組み合わせ
- 「する」「見る」「与える」という動詞の格パターン(コーパス中で出現頻度の高い順に並べよ)
課題補足(「格」について)
プログラムを完成させる目的では特に意識をしませんが、日本語の「格」というのは奥が深そうです。興味が出たらWikipedit「格」を見てみましょう。私はチラ見程度です。
昔、オーストラリアでランゲージ・エクスチェンジをしていたときに「は」と「が」の何が違うのかを聞かれたことを思い出しました。
回答
回答プログラム 045.動詞の格パターンの抽出.ipynb
import re
# 区切り文字
separator = re.compile('\t|,')
# 係り受け
dependancy = re.compile(r'''(?:\*\s\d+\s) # キャプチャ対象外
(-?\d+) # 数字(係り先)
''', re.VERBOSE)
def __init__(self, line):
#タブとカンマで分割
cols = separator.split(line)
self.surface = cols[0] # 表層形(surface)
self.base = cols[7] # 基本形(base)
self.pos = cols[1] # 品詞(pos)
self.pos1 = cols[2] # 品詞細分類1(pos1)
class Chunk:
def __init__(self, morphs, dst):
self.morphs = morphs
self.srcs = [] # 係り元文節インデックス番号のリスト
self.dst = dst # 係り先文節インデックス番号
self.verb = ''
self.joshi = ''
for morph in morphs:
if morph.pos != '記号':
self.joshi = '' # 記号を除いた最終行の助詞を取得するため、記号以外の場合はブランク
if morph.pos == '動詞':
self.verb = morph.base
if morph.pos == '助詞':
self.joshi = morph.base
# 係り元を代入し、Chunkリストを文のリストを追加
def append_sentence(chunks, sentences):
# 係り元を代入
for i, chunk in enumerate(chunks):
if chunk.dst != -1:
chunks[chunk.dst].srcs.append(i)
sentences.append(chunks)
return sentences, []
morphs = []
chunks = []
sentences = []
with open('./neko.txt.cabocha') as f:
for line in f:
dependancies = dependancy.match(line)
# EOSまたは係り受け解析結果でない場合
if not (line == 'EOS\n' or dependancies):
morphs.append(Morph(line))
# EOSまたは係り受け解析結果で、形態素解析結果がある場合
elif len(morphs) > 0:
chunks.append(Chunk(morphs, dst))
morphs = []
# 係り受け結果の場合
if dependancies:
dst = int(dependancies.group(1))
# EOSで係り受け結果がある場合
if line == 'EOS\n' and len(chunks) > 0:
sentences, chunks = append_sentence(chunks, sentences)
with open('./045.result_python.txt', 'w') as out_file:
for sentence in sentences:
for chunk in sentence:
if chunk.verb != '' and len(chunk.srcs) > 0:
# 係り元助詞のリストを作成
sources = [sentence[source].joshi for source in chunk.srcs if sentence[source].joshi != '']
if len(sources) > 0:
sources.sort()
out_file.write(('{}\t{}\n'.format(chunk.verb, ' '.join(sources))))
以下はUNIXコマンド部分です。grep
コマンドを初めて使いましたが便利なのですね。
# ソート、重複除去とカウント、降順ソート
sort 045.result_python.txt | uniq --count | sort --numeric-sort --reverse > "045.result_1_すべて.txt"
# 「(行頭)する(空白)」を抽出、ソート、重複除去とカウント、降順ソート
grep "^する\s" 045.result_python.txt | sort | uniq --count | sort --numeric-sort --reverse > "045.result_2_する.txt"
# 「(行頭)見る(空白)」を抽出、ソート、重複除去とカウント、降順ソート
grep "^見る\s" 045.result_python.txt | sort | uniq --count | sort --numeric-sort --reverse > "045.result_3_見る.txt"
# 「(行頭)与える(空白)」を抽出、ソート、重複除去とカウント、降順ソート
grep "^与える\s" 045.result_python.txt | sort | uniq --count | sort --numeric-sort --reverse > "045.result_4_与える.txt"
回答解説
Chunkクラス
Chunkクラスで動詞と助詞の原型を格納しています。1文節に複数の動詞があった場合は、後勝ちにしています。格となる助詞は文節の最後に出てくるはずなのですが、記号を考慮した条件分岐を入れています。
class Chunk:
def __init__(self, morphs, dst):
self.morphs = morphs
self.srcs = [] # 係り元文節インデックス番号のリスト
self.dst = dst # 係り先文節インデックス番号
self.verb = ''
self.joshi = ''
for morph in morphs:
if morph.pos != '記号':
self.joshi = '' # 記号を除いた最終行の助詞を取得するため、記号以外の場合はブランク
if morph.pos == '動詞':
self.verb = morph.base
if morph.pos == '助詞':
self.joshi = morph.base
出力部分
係り元の助詞はリスト内包表記でリスト化して、「辞書順に並べる」を満たすためにソートしています。そして、最後にjoin
関数を使ってスペース区切りで出力しています。ネストが深くて、書いていて気持ち悪いです。
with open('./045.result_python.txt', 'w') as out_file:
for sentence in sentences:
for chunk in sentence:
if chunk.verb != '' and len(chunk.srcs) > 0:
# 係り元助詞のリストを作成
sources = [sentence[source].joshi for source in chunk.srcs if sentence[source].joshi != '']
if len(sources) > 0:
sources.sort()
out_file.write(('{}\t{}\n'.format(chunk.verb, ' '.join(sources))))
出力結果(実行結果)
プログラム実行すると以下の結果が出力されます。多いので10行だけここに表示します。
Pythonの出力結果
生れる で
つく が と
泣く で
いる て は
始める で
見る は を
聞く で
捕える を
煮る て
食う て
UNIXコマンドの出力結果
多いので10行だけここに表示します。
3176 ある が
1997 つく が と
800 云う は
721 ある が と に
464 られる に
330 られる て と
309 思う と
305 見る の
301 かく たり を
262 ある まで
1099 する が
651 する が と
221 する で に は
109 する でも に
86 する まで
59 する と は は は
41 する たり は へ
27 する たり と は を
24 する て まで
18 する として
305 見る の
99 見る は を
31 見る て て は
24 見る から て
19 見る から
11 見る から て て
7 見る が ので
5 見る て て て は
2 見る ながら に を
2 見る で ばかり も
「与える」は出現頻度少なかく、これで全部です。
7 与える に を
4 与える で に を
3 与える て と は を
1 与える けれども は を
1 与える か として
1 与える が て と に は を