言語処理100本ノック-40:係り受け解析結果の読み込み(形態素)

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第5章: 係り受け解析」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch5)の[40本目「係り受け解析結果の読み込み（形態素）」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec40)記録です。
これから始まる第5章は、全体的にアルゴリズムを組むのが面倒で時間がかかり、言語処理100本ノックの最初の鬼門のような気がします。
今回は準備運動のようなものでたいして難しくないです。100本ノックでクラスを初めて使うのがせいぜい真新しい内容でしょうか。

# 参考リンク

|リンク|備考|
|:--|:--|
|[040.係り受け解析結果の読み込み（形態素）.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/040.%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(%E5%BD%A2%E6%85%8B%E7%B4%A0).ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:40](https://qiita.com/segavvy/items/2f686cfb0065c8cbe698)|多くのソース部分のコピペ元|
|[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というファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

### [40. 係り受け解析結果の読み込み（形態素）](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec40)
> 形態素を表すクラスMorphを実装せよ．このクラスは表層形（surface），基本形（base），品詞（pos），品詞細分類1（pos1）をメンバ変数に持つこととする．さらに，CaboChaの解析結果（neko.txt.cabocha）を読み込み，各文をMorphオブジェクトのリストとして表現し，3文目の形態素列を表示せよ．

### 課題補足(「係り受け」について)
「係り受け」は文節間の関係です。以前記事[「【お遊び】シンカリオンのトンデモメールを構文解析」](https://qiita.com/FukuharaYohei/items/a781b0573d8fdf78b6b6)で少しやったのですが、こんな文書でも
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/e5cfb529-e3be-c601-b5aa-8f1456a94e1d.png)

関係性を明確にすることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/f840740a-56eb-44ee-5f8b-6280d6de7735.png)

# 回答
## 回答前提
まずは、`CaboCha`で係り受け解析をします。

```bash
cabocha -f1 ../04.形態素解析/neko.txt -o neko.txt.cabocha
```

以下のような実行結果です。MeCabの結果に係り受け情報が付加されています。1行目の`* 0 -1D 0/0 0.000000`の部分が係り受け情報で3文字目の`0`が分節番号で、その後の`-1`が係り先を示しています。今回は`-1`と係り先なしなので、例が悪いですね。

```text:neko.txt.cabochaの一部抜粋
* 0 -1D 0/0 0.000000
一	名詞,数,*,*,*,*,一,イチ,イチ
EOS
EOS
* 0 -1D 1/1 0.000000
　	記号,空白,*,*,*,*,　,　,　
吾輩は猫である	名詞,固有名詞,一般,*,*,*,吾輩は猫である,ワガハイハネコデアル,ワガハイワネコデアル
。	記号,句点,*,*,*,*,。,。,。
EOS
* 0 2D 0/1 -1.911675
名前	名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
* 1 2D 0/0 -1.911675
まだ	副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ
* 2 -1D 0/0 0.000000
無い	形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ
。	記号,句点,*,*,*,*,。,。,。
EOS
EOS
* 0 1D 1/2 1.504358
　	記号,空白,*,*,*,*,　,　,　
どこ	名詞,代名詞,一般,*,*,*,どこ,ドコ,ドコ
で	助詞,格助詞,一般,*,*,*,で,デ,デ
* 1 2D 0/1 1.076607
生れ	動詞,自立,*,*,一段,連用形,生れる,ウマレ,ウマレ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
* 2 4D 0/1 -0.197109
かとん	名詞,一般,*,*,*,*,火遁,カトン,カトン
と	助詞,格助詞,一般,*,*,*,と,ト,ト
* 3 4D 0/1 -0.197109
見当	名詞,サ変接続,*,*,*,*,見当,ケントウ,ケントー
が	助詞,格助詞,一般,*,*,*,が,ガ,ガ
* 4 -1D 0/1 0.000000
つか	動詞,自立,*,*,五段・カ行イ音便,未然形,つく,ツカ,ツカ
ぬ	助動詞,*,*,*,特殊・ヌ,基本形,ぬ,ヌ,ヌ
。	記号,句点,*,*,*,*,。,。,。
EOS
```

## 回答プログラム [040.係り受け解析結果の読み込み（形態素）.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/040.%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(%E5%BD%A2%E6%85%8B%E7%B4%A0).ipynb)
でもって、本題のPythonプログラムです。

```python
import re

morphs = []
sentences = []

# 区切り文字
separator = re.compile('\t|,')

# 除外行
exclude = re.compile(r'''EOS\n      # EOS, 改行コード
                         |          # OR
                         \*\s\d+\s  # '*, 空白, 数字１つ以上, 空白
                       ''', 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)

with open('./neko.txt.cabocha') as f:
    
    for line in f:
        if not exclude.match(line):
            morphs.append(Morph(line))
        if   line == 'EOS\n' \
         and len(morphs) > 0:
            sentences.append(morphs)
            morphs = []

for sentence in sentences[2]:
    print(sentence.__dict__)
```

## 回答解説
### 正規表現
第2章で習った正規表現を練習と思い使っています。`separator`は形態素解析結果に対する区切り文字、`exclude`はEOSと係り受け結果を除外するための正規表現です。正規表現に関しては記事[「ゼロから覚えるPython正規表現の基本とTips」](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7)を参照ください。

```python:
# 区切り文字
separator = re.compile('\t|,')

# 除外行
exclude = re.compile(r'''EOS\n      # EOS, 改行コード
                         |          # OR
                         \*\s\d+\s  # '*, 空白, 数字１つ以上, 空白
                       ''', re.VERBOSE)
```

### クラス
100本ノックで始めてでてくるクラスです。`__init__`は初回時に呼ばれるコンストラクタです。形態素解析結果の行全体を受け取り、タブ/カンマで区切ってクラス変数に格納しています。

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

### クラス変数の出力
`__dict__`とすることでクラス変数を辞書型で出力してくれます。知らなかったのですが便利ですね。

```python:
for sentence in sentences[2]:
    print(sentence.__dict__)
```

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

```bash:出力結果
{'surface': '名前', 'base': '名前', 'pos': '名詞', 'pos1': '一般'}
{'surface': 'は', 'base': 'は', 'pos': '助詞', 'pos1': '係助詞'}
{'surface': 'まだ', 'base': 'まだ', 'pos': '副詞', 'pos1': '助詞類接続'}
{'surface': '無い', 'base': '無い', 'pos': '形容詞', 'pos1': '自立'}
{'surface': '。', 'base': '。', 'pos': '記号', 'pos1': '句点'}
```