自然言語処理100本ノック 第5章 係り受け解析(後半)


第5章の後半の問題を解いた記録。
対象とするファイルはwebページにもある通り、neko.txtとする。
> 夏目漱石の小説『吾輩は猫である』の文章（neko.txt）をCaboChaを使って係り受け解析し，その結果をneko.txt.cabochaというファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．


## <i class="fa fa-leaf"></i> 45. 動詞の格パターンの抽出
> 今回用いている文章をコーパスと見なし，日本語の述語が取りうる格を調査したい． 動詞を述語，動詞に係っている文節の助詞を格と考え，述語と格をタブ区切り形式で出力せよ． ただし，出力は以下の仕様を満たすようにせよ．
動詞を含む文節において，最左の動詞の基本形を述語とする
- 述語に係る助詞を格とする
- 述語に係る助詞（文節）が複数あるときは，すべての助詞をスペース区切りで辞書順に並べる
このプログラムの出力をファイルに保存し，以下の事項をUNIXコマンドを用いて確認せよ．
コーパス中で頻出する述語と格パターンの組み合わせ
「する」「見る」「与える」という動詞の格パターン（コーパス中で出現頻度の高い順に並べよ）

```python
# -*- coding: utf-8 -*-
__author__ = 'todoroki'

import problem41


def extractVerbPatern(sentence):
    lst = []
    for chunk in sentence:
        if chunk.include_pos('動詞'):
            src_chunks = [sentence[src] for src in chunk.srcs]
            src_chunks_case = list(filter(lambda src_chunks: src_chunks.morphs_of_pos1('格助詞'), src_chunks))
            if src_chunks_case:
                lst.append((chunk, src_chunks_case))
    return lst


if __name__ == "__main__":
    f = open("neko.txt.cabocha", "r")
    sentences = problem41.read_chunk(f)
    verbPatterns = []
    for sentence in sentences:
        verbPatterns.append(extractVerbPatern(sentence))

    for verbPattern in verbPatterns:
        for verb, src_chunks in verbPattern:
            v = verb.morphs_of_pos('動詞')[-1].base
            ps = [src_chunk.morphs_of_pos1('格助詞')[-1].base for src_chunk in src_chunks]
            p = " ".join(sorted(ps))
            print "%s\t%s" % (v, p)
    f.close()
```

上記プログラムの結果を出現頻度順にソートして出力するのが以下のコマンド。
上から、全動詞、「する」、「見る」、「与える」に対する処理。

```bash
python problem45.py | sort | uniq -c | sort -nr
python problem45.py | sort | awk '$1=="する"{print $0}' | uniq -c | sort -nr
python problem45.py | sort | awk '$1=="見る"{print $0}' | uniq -c | sort -nr
python problem45.py | sort | awk '$1=="与える"{print $0}' | uniq -c | sort -nr
```


## <i class="fa fa-leaf"></i> 46. 動詞の格フレーム情報の抽出
> 45のプログラムを改変し，述語と格パターンに続けて項（述語に係っている文節そのもの）をタブ区切り形式で出力せよ．45の仕様に加えて，以下の仕様を満たすようにせよ．
- 項は述語に係っている文節の単語列とする（末尾の助詞を取り除く必要はない）
- 述語に係る文節が複数あるときは，助詞と同一の基準・順序でスペース区切りで並べる

```python
# -*- coding: utf-8 -*-
__author__ = 'todoroki'

import problem41
import problem45

if __name__ == "__main__":
    f = open("neko.txt.cabocha", "r")
    sentences = problem41.read_chunk(f)
    f.close()
    verbPatterns = []
    for sentence in sentences:
        verbPatterns.append(problem45.extractVerbPatern(sentence))

    for verbPattern in verbPatterns:
        for verb, src_chunks in verbPattern:
            col1 = verb.morphs_of_pos('動詞')[-1].base
            tmp = [(src_chunk.morphs_of_pos1('格助詞')[-1].base, str(src_chunk)) for src_chunk in src_chunks]
            tmp = sorted(tmp, key=lambda x:x[0])
            col2 = " ".join([col[0] for col in tmp])
            col3 = " ".join([col[1] for col in tmp])
            print "%s\t%s\t%s" % (col1, col2, col3)
```


## <i class="fa fa-leaf"></i> 47. 機能動詞構文のマイニング
> 動詞のヲ格にサ変接続名詞が入っている場合のみに着目したい．46のプログラムを以下の仕様を満たすように改変せよ．
- 「サ変接続名詞+を（助詞）」で構成される文節が動詞に係る場合のみを対象とする
- 述語は「サ変接続名詞+を+動詞の基本形」とし，文節中に複数の動詞があるときは，最左の動詞を用いる
- 述語に係る助詞（文節）が複数あるときは，すべての助詞をスペース区切りで辞書順に並べる
- 述語に係る文節が複数ある場合は，すべての項をスペース区切りで並べる（助詞の並び順と揃えよ）

> このプログラムの出力をファイルに保存し，以下の事項をUNIXコマンドを用いて確認せよ．
- コーパス中で頻出する述語（サ変接続名詞+を+動詞）
- コーパス中で頻出する述語と助詞パターン

```python
# -*- coding: utf-8 -*-
__author__ = 'todoroki'

import problem41
import problem45

def extractSahen(src_chunks):
    for i, src_chunk in enumerate(src_chunks):
        morphs = src_chunk.morphs
        if len(morphs) > 1:
            if morphs[-2].pos1 == "サ変接続" and morphs[-1].pos == "助詞" and morphs[-1].base == "を":
                src_chunks.pop(i)
                return src_chunk, src_chunks
    return None

if __name__ == "__main__":
    f = open("neko.txt.cabocha", "r")
    sentences = problem41.read_chunk(f)
    f.close()
    verbPatterns = []
    for sentence in sentences:
        verbPatterns.append(problem45.extractVerbPatern(sentence))

    for verbPattern in verbPatterns:
        for verb, src_chunks in verbPattern:
            sahen_chunks_set = extractSahen(src_chunks)
            if sahen_chunks_set:
                sahen_chunk, other_chunks = sahen_chunks_set
                col1 = str(sahen_chunk) + verb.morphs_of_pos('動詞')[-1].base
                tmp = [(other_chunk.morphs_of_pos1('格助詞')[-1].base, str(other_chunk)) for other_chunk in other_chunks]
                tmp = sorted(tmp, key=lambda x: x[0])
                col2 = " ".join([col[0] for col in tmp])
                col3 = " ".join([col[1] for col in tmp])
                print "%s\t%s\t%s" % (col1, col2, col3)
```

コーパス中で頻出する述語（サ変接続名詞+を+動詞）を出力するコマンド。

```bash
python problem47.py | cut -f 1 | sort | uniq -c | sort -nr
```

コーパス中で頻出する述語と助詞パターンを出力するコマンド。

```bash
python problem47.py | cut -f 1,2 | sort | uniq -c | sort -nr
```

## <i class="fa fa-leaf"></i> 48. 名詞から根へのパスの抽出
> 文中のすべての名詞を含む文節に対し，その文節から構文木の根に至るパスを抽出せよ． ただし，構文木上のパスは以下の仕様を満たすものとする．
- 各文節は（表層形の）形態素列で表現する
- パスの開始文節から終了文節に至るまで，各文節の表現を"->"で連結する

```python
# -*- coding: utf-8 -*-
__author__ = 'todoroki'

import problem41

def extractPath(chunk, sentence):
    path = [chunk]
    dst = chunk.dst
    while dst != -1:
        path.append(sentence[dst])
        dst = sentence[dst].dst
    return path

if __name__ == "__main__":
    f = open("neko.txt.cabocha", "r")
    sentences = problem41.read_chunk(f)
    f.close()
    paths = []
    for sentence in sentences:
        for chunk in sentence:
            if chunk.include_pos('名詞') and chunk.dst != -1:
                paths.append(extractPath(chunk, sentence))

    for path in paths:
        print " -> ".join([str(chunk) for chunk in path])
```

## <i class="fa fa-leaf"></i> 49. 名詞間の係り受けパスの抽出
> 文中のすべての名詞句のペアを結ぶ最短係り受けパスを抽出せよ．ただし，名詞句ペアの文節番号がiとj（i<j）のとき，係り受けパスは以下の仕様を満たすものとする．
- 問題48と同様に，パスは開始文節から終了文節に至るまでの各文節の表現（表層形の形態素列）を"->"で連結して表現する
- 文節iとjに含まれる名詞句はそれぞれ，XとYに置換する

> また，係り受けパスの形状は，以下の2通りが考えられる．
- 文節iから構文木の根に至る経路上に文節jが存在する場合: 文節iから文節jのパスを表示
- 上記以外で，文節iと文節jから構文木の根に至る経路上で共通の文節kで交わる場合: 文節iから文節kに至る直前のパスと文節jから文節kに至る直前までのパス，文節kの内容を"|"で連結して表示

```python
# -*- coding: utf-8 -*-
__author__ = 'todoroki'

from collections import namedtuple
from itertools import combinations
import problem41

def extractPathIndex(i_chunk, sentence):
    i, chunk = i_chunk
    path_index = [i]
    dst = chunk.dst
    while dst != -1:
        path_index.append(dst)
        dst = sentence[dst].dst
    return path_index

def posReplace(chunks, pos, repl, k=1):
    replaced_str = ""
    for morph in chunks[0].morphs:
        if morph.pos == pos and k > 0:
            replaced_str += repl
            k -= 1
        else:
            if morph.pos != '記号':
                replaced_str += morph.surface
    return [replaced_str] + [str(chunk) for chunk in chunks[1:]]


if __name__ == "__main__":
    f = open("neko.txt.cabocha", "r")
    sentences = problem41.read_chunk(f)
    f.close()
    paths = []
    N2Npath = namedtuple('N2Npath', ['X', 'Y', 'is_linear'])
    for sentence in sentences:
        noun_chunks = [(i, chunk) for i, chunk in enumerate(sentence) if chunk.include_pos('名詞')]
        if len(noun_chunks) > 1:
            for former, latter in combinations(noun_chunks, 2):
                f_index = extractPathIndex(former, sentence)
                l_index = extractPathIndex(latter, sentence)
                f_i, l_i = list(zip(reversed(f_index), reversed(l_index)))[-1]
                linear_flag = (f_i == l_i)
                if linear_flag:
                    f_index2 = f_index[:f_index.index(f_i)+1]
                    l_index2 = l_index[:l_index.index(l_i)+1]
                else:
                    f_index2 = f_index[:f_index.index(f_i)+2]
                    l_index2 = l_index[:l_index.index(l_i)+2]
                X = [sentence[k] for k in f_index2]
                Y = [sentence[k] for k in l_index2]
                paths.append(N2Npath(X=X, Y=Y, is_linear=linear_flag))

    for path in paths:
        x = posReplace(path.X, "名詞", "X")
        y = posReplace(path.Y, "名詞", "Y")
        if path.is_linear:
            x[-1] = "Y"
            print " -> ".join(x)
        else:
            print "%s | %s | %s" % (" -> ".join(x[:-1]), " -> ".join(y[:-1]), path.X[-1])
```

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">