言語処理100本ノック 2015「第5章: 係り受け解析」の47本目「機能動詞構文のマイニング」記録です。
前回ノックに加え、抽出対象がさらに複雑な条件になります。問題文を理解するだけで少し時間がかかりますし、当然解くにも時間がかかります。
参考リンク
リンク | 備考 |
---|---|
047.機能動詞構文のマイニング.ipynb | 回答プログラムのGitHubリンク |
素人の言語処理100本ノック:47 | 多くのソース部分のコピペ元 |
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というファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.
47. 機能動詞構文のマイニング
動詞のヲ格にサ変接続名詞が入っている場合のみに着目したい.46のプログラムを以下の仕様を満たすように改変せよ.
- 「サ変接続名詞+を(助詞)」で構成される文節が動詞に係る場合のみを対象とする
- 述語は「サ変接続名詞+を+動詞の基本形」とし,文節中に複数の動詞があるときは,最左の動詞を用いる
- 述語に係る助詞(文節)が複数あるときは,すべての助詞をスペース区切りで辞書順に並べる
- 述語に係る文節が複数ある場合は,すべての項をスペース区切りで並べる(助詞の並び順と揃えよ)
例えば「別段くるにも及ばんさと、主人は手紙に返事をする。」という文から,以下の出力が得られるはずである.
返事をする と に は 及ばんさと 手紙に 主人は
このプログラムの出力をファイルに保存し,以下の事項をUNIXコマンドを用いて確認せよ.
- コーパス中で頻出する述語(サ変接続名詞+を+動詞)
- コーパス中で頻出する述語と助詞パターン
課題補足(「機能動詞」について)
機能動詞とは、「機能動詞・熟語動詞」によると以下を指すようです。つまり「する」みたいに名詞とくっついて「食事する」のようにしないと意味ないやつです。
動詞がその本来の意味を失い,動作名詞と結びついて動詞の意味を表す動詞を機能動詞
回答
回答プログラム 047.機能動詞構文のマイニング.ipynb
import re
# 区切り文字
separator = re.compile('\t|,')
# 係り受け
dependancy = re.compile(r'''(?:\*\s\d+\s) # キャプチャ対象外
(-?\d+) # 数字(係り先)
''', re.VERBOSE)
class Morph:
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.phrase = ''
self.verb = ''
self.joshi = ''
self.sahen = '' # サ変+を+動詞のパターン対象か否か
for i, morph in enumerate(morphs):
if morph.pos != '記号':
self.phrase += morph.surface # 記号以外の場合文節作成
self.joshi = '' # 記号を除いた最終行の助詞を取得するため、記号以外の場合はブランク
if morph.pos == '動詞' and self.verb == '':
self.verb = morph.base
if morphs[-1].pos == '助詞':
self.joshi = morphs[-1].base
try:
if morph.pos1 == 'サ変接続' and \
morphs[i+1].surface == 'を':
self.sahen = morph.surface + morphs[i+1].surface
except IndexError:
pass
# 係り元を代入し、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)
def output_file(out_file, sahen, sentence, chunk):
# 係り元助詞のリストを作成
sources = [[sentence[source].joshi, sentence[source].phrase] \
for source in chunk.srcs if sentence[source].joshi != '']
if len(sources) > 0:
sources.sort()
joshi = ' '.join([row[0] for row in sources])
phrase = ' '.join([row[1] for row in sources])
out_file.write(('{}\t{}\t{}\n'.format(sahen, joshi, phrase)))
with open('./047.result_python.txt', 'w') as out_file:
for sentence in sentences:
for chunk in sentence:
if chunk.sahen != '' and \
chunk.dst != -1 and \
sentence[chunk.dst].verb != '':
output_file(out_file, chunk.sahen+sentence[chunk.dst].verb,
sentence, sentence[chunk.dst])
# 述語でソートして重複除去し、その件数でソート
cut --fields=1 047.result_python.txt | sort | uniq --count \
| sort --numeric-sort --reverse > 047.result_unix1.txt
# 述語と助詞でソートして重複除去し、その件数でソート
cut --fields=1,2 047.result_python.txt | sort | uniq --count \
| sort --numeric-sort --reverse > 047.result_unix2.txt
回答解説
Chunkクラス
いつもどおり、生命線のChunkクラスを変更します。品詞細分類pos1
の値が「サ変接続」の場合に次エントリを見て「を」だったらインスタンス変数sahen
に両者を結合した文字列を入れています(例:返事+を)。
class Chunk:
def __init__(self, morphs, dst):
self.morphs = morphs
self.srcs = [] # 係り元文節インデックス番号のリスト
self.dst = dst # 係り先文節インデックス番号
self.phrase = ''
self.verb = ''
self.joshi = ''
self.sahen = '' # サ変+を+動詞のパターン対象か否か
for i, morph in enumerate(morphs):
if morph.pos != '記号':
self.phrase += morph.surface # 記号以外の場合文節作成
self.joshi = '' # 記号を除いた最終行の助詞を取得するため、記号以外の場合はブランク
if morph.pos == '動詞' and self.verb == '':
self.verb = morph.base
if morphs[-1].pos == '助詞':
self.joshi = morphs[-1].base
try:
if morph.pos1 == 'サ変接続' and \
morphs[i+1].surface == 'を':
self.sahen = morph.surface + morphs[i+1].surface
except IndexError:
pass
出力部
出力部の条件分岐を変えています。
with open('./047.result_python.txt', 'w') as out_file:
for sentence in sentences:
for chunk in sentence:
if chunk.sahen != '' and \
chunk.dst != -1 and \
sentence[chunk.dst].verb != '':
output_file(out_file, chunk.sahen+sentence[chunk.dst].verb,
sentence, sentence[chunk.dst])
出力結果(実行結果)
Python実行結果
Python Scriptを実行すると以下の結果が出力されます。
決心をする と を こうと 決心を
返報をする を んで 返報を 偸んで
昼寝をする を 昼寝を
昼寝をする が を 彼が 昼寝を
迫害を加える て を 追い廻して 迫害を
生活をする て を して 家族的生活を
話をする を 話を
投書をする て へ を やって ほととぎすへ 投書を
話をする に を 時に 話を
写生をする を 写生を
UNIXコマンド実行結果
UNIXコマンドを実行し、「コーパス中で頻出する述語(サ変接続名詞+を+動詞)」を出力
29 返事をする
21 挨拶をする
16 話をする
15 真似をする
13 喧嘩をする
9 運動をする
9 質問をする
6 注意をする
6 昼寝をする
6 問答をする
UNIXコマンドを実行し、「コーパス中で頻出する述語と助詞パターン」を出力
14 返事をする と を
9 運動をする を
9 真似をやる を
8 返事をする と は を
7 喧嘩をする を
6 話をする に を
6 挨拶をする と を
5 話をする を
5 挨拶をする で に を
4 質問をする て を