言語処理100本ノック-46:動詞の格フレーム情報の抽出

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第5章: 係り受け解析」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch5)の[46本目「動詞の格フレーム情報の抽出」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec46)記録です。
前回は格として助詞だけを出力しましたが、今度は文節(各フレーム)も併せて出力します。当然ながらさらに面倒に・・・

# 参考リンク

|リンク|備考|
|:--|:--|
|[046.動詞の格フレーム情報の抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/05.%E4%BF%82%E3%82%8A%E5%8F%97%E3%81%91%E8%A7%A3%E6%9E%90/046.%E5%8B%95%E8%A9%9E%E3%81%AE%E6%A0%BC%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E6%83%85%E5%A0%B1%E3%81%AE%E6%8A%BD%E5%87%BA.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:46](http://qiita.com/segavvy/items/25511a18ad7928e3982f)|多くのソース部分のコピペ元|
|[CaboCha公式](https://taku910.github.io/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を使っています<br>パッケージはvenvを使って管理しています|
|Mecab|0.996-5|apt-getでインストール|
|CRF++|0.58|昔すぎてインストール方法忘れました(多分`make install`)|
|CaboCha|0.69|昔すぎてインストール方法忘れました(多分`make install`)|

# [第5章: 係り受け解析](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch5)
## 学習内容
> 『吾輩は猫である』に係り受け解析器CaboChaを適用し，係り受け木の操作と統語的な分析を体験します．
>> クラス, 係り受け解析, [CaboCha](https://code.google.com/p/cabocha/), 文節, 係り受け, 格, 機能動詞構文, 係り受けパス, [Graphviz](http://www.graphviz.org/)

## ノック内容
> 夏目漱石の小説『吾輩は猫である』の文章（[neko.txt](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/neko.txt)）をCaboChaを使って係り受け解析し，その結果をneko.txt.cabochaというファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

### [46. 動詞の格フレーム情報の抽出](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec46)
>45のプログラムを改変し，述語と格パターンに続けて項（述語に係っている文節そのもの）をタブ区切り形式で出力せよ．45の仕様に加えて，以下の仕様を満たすようにせよ．
>
> - 項は述語に係っている文節の単語列とする（末尾の助詞を取り除く必要はない）
> - 述語に係る文節が複数あるときは，助詞と同一の基準・順序でスペース区切りで並べる
>
>「吾輩はここで始めて人間というものを見た」という例文（neko.txt.cabochaの8文目）を考える． この文は「始める」と「見る」の２つの動詞を含み，「始める」に係る文節は「ここで」，「見る」に係る文節は「吾輩は」と「ものを」と解析された場合は，次のような出力になるはずである．
>
>```
>始める  で      ここで
>見る    は を   吾輩は ものを
>```


### 課題補足(「各フレーム(格文法)」について)
興味ある方は[Wikipedia「格文法」](https://ja.wikipedia.org/wiki/%E6%A0%BC%E6%96%87%E6%B3%95)を見てください。見なくても解けます。私はチラ見程度で理解していません。

# 回答
## 回答プログラム [046.動詞の格フレーム情報の抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/05.%E4%BF%82%E3%82%8A%E5%8F%97%E3%81%91%E8%A7%A3%E6%9E%90/046.%E5%8B%95%E8%A9%9E%E3%81%AE%E6%A0%BC%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E6%83%85%E5%A0%B1%E3%81%AE%E6%8A%BD%E5%87%BA.ipynb)

```python
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)

    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.srcs = []   # 係り元文節インデックス番号のリスト
        self.dst  = dst  # 係り先文節インデックス番号
        
        self.phrase = ''
        self.verb = ''
        self.joshi = ''
        
        for morph in morphs:            
            if morph.pos != '記号':
                self.phrase += morph.surface # 記号以外の場合文節作成
                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)

def output_file(out_file, 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(chunk.verb, joshi, phrase)))

with open('./046.result_python.txt', 'w') as out_file:
    for sentence in sentences:
        for chunk in sentence:
            if chunk.verb != '' and len(chunk.srcs) > 0:
                output_file(out_file, sentence, chunk)
```

## 回答解説
### Chunkクラス
やはり、第5章の生命線となるChunkクラスを前回から変更しました。インスタンス変数の`phrase`を作り文節テキストを入れています。それ以外は同じです。

```python:
class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.srcs = []   # 係り元文節インデックス番号のリスト
        self.dst  = dst  # 係り先文節インデックス番号
        
        self.phrase = ''
        self.verb = ''
        self.joshi = ''
        
        for morph in morphs:
            if morph.pos != '記号':
                self.phrase += morph.surface # 記号以外の場合文節作成
                self.joshi = ''  # 記号を除いた最終行の助詞を取得するため、記号以外の場合はブランク
            if morph.pos == '動詞':
                self.verb = morph.base
            if morph.pos == '助詞':
                self.joshi = morph.base
```

### ファイル出力関数
そこそこ複雑になったのでファイル出力部分を関数化して切り出しています。最初のリスト内包表記で助詞と文節のリストを作成して、ソート後に`join`関数で出力しています。

```python:
def output_file(out_file, 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(chunk.verb, joshi, phrase)))
```

### 出力結果(実行結果)
プログラム実行すると以下の結果が出力されます(冒頭10件のみ)。

```bash:046.result_python.txt
生れる	で	どこで
つく	が と	見当が かとんと
泣く	で	した所で
いる	て は	泣いて いた事だけは
始める	で	ここで
見る	は を	吾輩は ものを
聞く	で	あとで
捕える	を	我々を
煮る	て	捕えて
食う	て	煮て
```