言語処理100本ノック-48:名詞から根へのパスの抽出

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第5章: 係り受け解析」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch5)の[48本目「名詞から根へのパスの抽出」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec48)記録です。
前回ノックと比べて少しシンプルになりました。あまり条件がなく係り先を続けて出力しているだけだからです。

# 参考リンク

|リンク|備考|
|:--|:--|
|[048.名詞から根へのパスの抽出.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/048.%E5%90%8D%E8%A9%9E%E3%81%8B%E3%82%89%E6%A0%B9%E3%81%B8%E3%81%AE%E3%83%91%E3%82%B9%E3%81%AE%E6%8A%BD%E5%87%BA.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:48](https://qiita.com/segavvy/items/ed5cc48dc02afd8855a1)|多くのソース部分のコピペ元|
|[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というファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

### [48. 名詞から根へのパスの抽出](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec48)
>文中のすべての名詞を含む文節に対し，その文節から構文木の根に至るパスを抽出せよ． ただし，構文木上のパスは以下の仕様を満たすものとする．
>
> - 各文節は（表層形の）形態素列で表現する
> - パスの開始文節から終了文節に至るまで，各文節の表現を"`->`"で連結する
>
>「吾輩はここで始めて人間というものを見た」という文（neko.txt.cabochaの8文目）から，次のような出力が得られるはずである．
>
>```
> 吾輩は -> 見た
> ここで -> 始めて -> 人間という -> ものを -> 見た
> 人間という -> ものを -> 見た
> ものを -> 見た
>```

# 回答
## 回答プログラム [048.名詞から根へのパスの抽出.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/048.%E5%90%8D%E8%A9%9E%E3%81%8B%E3%82%89%E6%A0%B9%E3%81%B8%E3%81%AE%E3%83%91%E3%82%B9%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)

class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.dst  = dst  # 係り先文節インデックス番号
        
        self.phrase = ''
        self.noun = False
        
        for morph in morphs:
            if morph.pos != '記号':
                self.phrase += morph.surface # 記号以外の場合文節作成
            if morph.pos == '名詞':
                self.noun = True

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.append(chunks)
            chunks = []

for i, sentence in enumerate(sentences):
    for chunk in sentence:
        if chunk.noun and chunk.dst != -1:
            line = chunk.phrase
            current_chunk = chunk
            while current_chunk.dst != -1:
                line = line + ' -> ' + sentence[current_chunk.dst].phrase
                current_chunk = sentence[current_chunk.dst]
            print(i, '\t',line)
    # 多いので制限
    if i > 10:
        break
```

## 回答解説
### Chunkクラス
Chunkクラスは前回と比べてスッキリしました。名詞を含んでいるかをフラグとして持たせています。

```python:
class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.dst  = dst  # 係り先文節インデックス番号
        
        self.phrase = ''
        self.noun = False
        
        for morph in morphs:
            if morph.pos != '記号':
                self.phrase += morph.surface # 記号以外の場合文節作成
            if morph.pos == '名詞':
                self.noun = True
```

### 出力部
前回から色々とシンプルになりましたが、代わりに少し複雑にしているのが以下の出力部分です。
名詞句を含み係り先があった場合に、`while`句で係り先の終端まで分節をつなげています。

```python:
for i, sentence in enumerate(sentences):
    for chunk in sentence:
        if chunk.noun and chunk.dst != -1:
            line = chunk.phrase
            current_chunk = chunk
            while current_chunk.dst != -1:
                line = line + ' -> ' + sentence[current_chunk.dst].phrase
                current_chunk = sentence[current_chunk.dst]
            print(i, '\t',line)
    # 多いので制限
    if i > 10:
        break
```

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

```bash:出力結果
2 	 名前は -> 無い
3 	 どこで -> 生れた -> かとんと -> つかぬ
3 	 かとんと -> つかぬ
3 	 見当が -> つかぬ
4 	 何でも -> 薄暗い -> 泣いて -> 記憶している
4 	 した所で -> 泣いて -> 記憶している
4 	 いた事だけは -> 記憶している
5 	 吾輩は -> 見た
5 	 ここで -> 始めて -> 人間という -> ものを -> 見た
5 	 人間という -> ものを -> 見た
5 	 ものを -> 見た
6 	 あとで -> 聞くと -> そうだ
6 	 それは -> そうだ
6 	 書生という -> 人間中で -> 種族であった -> そうだ
6 	 人間中で -> 種族であった -> そうだ
6 	 一番 -> 獰悪な -> 種族であった -> そうだ
6 	 獰悪な -> 種族であった -> そうだ
6 	 種族であった -> そうだ
7 	 書生というのは -> 話である
7 	 我々を -> 捕えて -> 煮て -> 食うという -> 話である
8 	 当時は -> なかったから -> 思わなかった
8 	 何という -> 考も -> なかったから -> 思わなかった
8 	 考も -> なかったから -> 思わなかった
9 	 彼の -> 掌に -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
9 	 掌に -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
9 	 スーと -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
9 	 時 -> フワフワした -> 感じが -> あったばかりである
9 	 感じが -> あったばかりである
10 	 掌の -> 上で -> 落ちついて -> 見たのが -> 人間という -> ものの -> 見始であろう
10 	 上で -> 落ちついて -> 見たのが -> 人間という -> ものの -> 見始であろう
10 	 書生の -> 顔を -> 見たのが -> 人間という -> ものの -> 見始であろう
10 	 顔を -> 見たのが -> 人間という -> ものの -> 見始であろう
10 	 見たのが -> 人間という -> ものの -> 見始であろう
10 	 人間という -> ものの -> 見始であろう
10 	 ものの -> 見始であろう
11 	 時 -> ものだと -> 思った -> 感じが -> 残っている
11 	 妙な -> ものだと -> 思った -> 感じが -> 残っている
11 	 ものだと -> 思った -> 感じが -> 残っている
11 	 感じが -> 残っている
11 	 今でも -> 残っている
```