言語処理100本ノック-41:係り受け解析結果の読み込み(文節・係り受け)

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第5章: 係り受け解析」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch5)の[41本目「係り受け解析結果の読み込み(文節・係り受け)」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec41)記録です。
前回は準備運動の内容だったので、今回からが係り受けの本番です。第5章は全体的に、第4章「形態素解析」のようにパッケージを活用した短いコード、というわけにはいかず、アルゴリズムを作っていかないといけません。今回はそんなに複雑ではないですが、それでもある程度考えさせられます。

# 参考リンク

|リンク|備考|
|:--|:--|
|[041.係り受け解析結果の読み込み(文節・係り受け).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/041.%E4%BF%82%E3%82%8A%E5%8F%97%E3%81%91%E8%A7%A3%E6%9E%90%E7%B5%90%E6%9E%9C%E3%81%AE%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF(%E6%96%87%E7%AF%80%E3%83%BB%E4%BF%82%E3%82%8A%E5%8F%97%E3%81%91).ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:41](https://qiita.com/segavvy/items/003e8b7e0f132f4fde1b)|多くのソース部分のコピペ元|
|[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というファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

### [41. 係り受け解析結果の読み込み(文節・係り受け)](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec41)
> 40に加えて，文節を表すクラスChunkを実装せよ．このクラスは形態素（Morphオブジェクト）のリスト（morphs），係り先文節インデックス番号（dst），係り元文節インデックス番号のリスト（srcs）をメンバ変数に持つこととする．さらに，入力テキストのCaboChaの解析結果を読み込み，１文をChunkオブジェクトのリストとして表現し，8文目の文節の文字列と係り先を表示せよ．第5章の残りの問題では，ここで作ったプログラムを活用せよ．

# 回答
## 回答プログラム [041.係り受け解析結果の読み込み(文節・係り受け).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/041.%E4%BF%82%E3%82%8A%E5%8F%97%E3%81%91%E8%A7%A3%E6%9E%90%E7%B5%90%E6%9E%9C%E3%81%AE%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF(%E6%96%87%E7%AF%80%E3%83%BB%E4%BF%82%E3%82%8A%E5%8F%97%E3%81%91).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)

class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.srcs = []   # 係り元文節インデックス番号のリスト
        self.dst  = dst  # 係り先文節インデックス番号
        self.phrase = ''.join([morph.surface for morph in morphs]) # 文節

# 係り元を代入し、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)

for i, chunk in enumerate(sentences[7]):
    print('{}: {}, 係り先:{}, 係り元:{}'.format(i, chunk.phrase, chunk.dst, chunk.srcs))
```

## 回答解説
### 係り先取得の正規表現
係り先を取得できる正規表現を使っています。`(-?\d+)`が係り先の数字を取得する部分です。正規表現に関しては記事[「ゼロから覚えるPython正規表現の基本とTips」](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7)を参照ください。
正規表現を使わなくても取得できると思いますが、練習のために使っています。

```python:
# 係り受け
dependancy = re.compile(r'''(?:\*\s\d+\s) # キャプチャ対象外
                            (-?\d+)       # 数字(係り先)
                          ''', re.VERBOSE)
```

### Chunkクラス
ノックの指定にかかれていない`phrase`という変数も定義しています。後で出力するときに便利なので。`srcs`は定義だけしておき`__init__`では値の代入をしていません。

```python:
class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.srcs = []   # 係り元文節インデックス番号のリスト
        self.dst  = dst  # 係り先文節インデックス番号
        self.phrase = ''.join([morph.surface for morph in morphs]) # 文節
```

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

```bash:出力結果
0: この, 係り先:1, 係り元:[]
1: 書生というのは, 係り先:7, 係り元:[0]
2: 時々, 係り先:4, 係り元:[]
3: 我々を, 係り先:4, 係り元:[]
4: 捕えて, 係り先:5, 係り元:[2, 3]
5: 煮て, 係り先:6, 係り元:[4]
6: 食うという, 係り先:7, 係り元:[5]
7: 話である。, 係り先:-1, 係り元:[1, 6]
```