言語処理100本ノック-47:機能動詞構文のマイニング

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第5章: 係り受け解析」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch5)の[47本目「機能動詞構文のマイニング」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec47)記録です。
前回ノックに加え、抽出対象がさらに複雑な条件になります。問題文を理解するだけで少し時間がかかりますし、当然解くにも時間がかかります。

# 参考リンク

|リンク|備考|
|:--|:--|
|[047.機能動詞構文のマイニング.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/047.%E6%A9%9F%E8%83%BD%E5%8B%95%E8%A9%9E%E6%A7%8B%E6%96%87%E3%81%AE%E3%83%9E%E3%82%A4%E3%83%8B%E3%83%B3%E3%82%B0.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:47](https://qiita.com/segavvy/items/c40bbafef33c65f6c6ad)|多くのソース部分のコピペ元|
|[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というファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

### [47. 機能動詞構文のマイニング](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec47)
>動詞のヲ格にサ変接続名詞が入っている場合のみに着目したい．46のプログラムを以下の仕様を満たすように改変せよ．
>
> - 「サ変接続名詞+を（助詞）」で構成される文節が動詞に係る場合のみを対象とする
> - 述語は「サ変接続名詞+を+動詞の基本形」とし，文節中に複数の動詞があるときは，最左の動詞を用いる
> - 述語に係る助詞（文節）が複数あるときは，すべての助詞をスペース区切りで辞書順に並べる
> - 述語に係る文節が複数ある場合は，すべての項をスペース区切りで並べる（助詞の並び順と揃えよ）
>
>例えば「別段くるにも及ばんさと、主人は手紙に返事をする。」という文から，以下の出力が得られるはずである．
>
>```
>返事をする      と に は        及ばんさと 手紙に 主人は
>```
>
>このプログラムの出力をファイルに保存し，以下の事項をUNIXコマンドを用いて確認せよ．
>
> - コーパス中で頻出する述語（サ変接続名詞+を+動詞）
> - コーパス中で頻出する述語と助詞パターン

### 課題補足(「機能動詞」について)
機能動詞とは、[「機能動詞・熟語動詞」](http://www.harada.law.kyoto-u.ac.jp/hdg/06_funktionsverb.html)によると以下を指すようです。つまり「する」みたいに名詞とくっついて「食事する」のようにしないと意味ないやつです。

> 動詞がその本来の意味を失い，動作名詞と結びついて動詞の意味を表す動詞を機能動詞

# 回答
## 回答プログラム [047.機能動詞構文のマイニング.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/047.%E6%A9%9F%E8%83%BD%E5%8B%95%E8%A9%9E%E6%A7%8B%E6%96%87%E3%81%AE%E3%83%9E%E3%82%A4%E3%83%8B%E3%83%B3%E3%82%B0.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 = ''
        self.verb = ''
        self.joshi = ''
        self.sahen = '' # サ変+を+動詞のパターン対象か否か
        
        for i, morph in enumerate(morphs):
            if morph.pos != '記号':
                self.phrase += morph.surface # 記号以外の場合文節作成
                self.joshi = ''  # 記号を除いた最終行の助詞を取得するため、記号以外の場合はブランク
            
            if morph.pos == '動詞' and self.verb == '':
                self.verb = morph.base
            
            if morphs[-1].pos == '助詞':
                self.joshi = morphs[-1].base
                
            try:
                if morph.pos1 == 'サ変接続' and \
                   morphs[i+1].surface == 'を':
                    self.sahen = morph.surface + morphs[i+1].surface
            except IndexError:
                pass

# 係り元を代入し、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, sahen, 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(sahen, joshi, phrase)))

with open('./047.result_python.txt', 'w') as out_file:
    for sentence in sentences:
        for chunk in sentence:
            
            if chunk.sahen != '' and \
               chunk.dst != -1 and \
               sentence[chunk.dst].verb != '':
                output_file(out_file, chunk.sahen+sentence[chunk.dst].verb, 
                            sentence, sentence[chunk.dst])
```

```bash
# 述語でソートして重複除去し、その件数でソート
cut --fields=1 047.result_python.txt | sort | uniq --count \
| sort --numeric-sort --reverse > 047.result_unix1.txt

# 述語と助詞でソートして重複除去し、その件数でソート
cut --fields=1,2 047.result_python.txt | sort | uniq --count \
| sort --numeric-sort --reverse > 047.result_unix2.txt
```

## 回答解説
### Chunkクラス
いつもどおり、生命線のChunkクラスを変更します。品詞細分類`pos1`の値が「サ変接続」の場合に次エントリを見て「を」だったらインスタンス変数`sahen`に両者を結合した文字列を入れています(例：返事+を)。

```python:
class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.srcs = []   # 係り元文節インデックス番号のリスト
        self.dst  = dst  # 係り先文節インデックス番号
        
        self.phrase = ''
        self.verb = ''
        self.joshi = ''
        self.sahen = '' # サ変+を+動詞のパターン対象か否か
        
        for i, morph in enumerate(morphs):
            if morph.pos != '記号':
                self.phrase += morph.surface # 記号以外の場合文節作成
                self.joshi = ''  # 記号を除いた最終行の助詞を取得するため、記号以外の場合はブランク
            
            if morph.pos == '動詞' and self.verb == '':
                self.verb = morph.base
            
            if morphs[-1].pos == '助詞':
                self.joshi = morphs[-1].base
                
            try:
                if morph.pos1 == 'サ変接続' and \
                   morphs[i+1].surface == 'を':
                    self.sahen = morph.surface + morphs[i+1].surface
            except IndexError:
                pass
```

### 出力部
出力部の条件分岐を変えています。

```python:
with open('./047.result_python.txt', 'w') as out_file:
    for sentence in sentences:
        for chunk in sentence:
            
            if chunk.sahen != '' and \
               chunk.dst != -1 and \
               sentence[chunk.dst].verb != '':
                output_file(out_file, chunk.sahen+sentence[chunk.dst].verb, 
                            sentence, sentence[chunk.dst])
```


### 出力結果(実行結果)
#### Python実行結果
Python Scriptを実行すると以下の結果が出力されます。

```text:047.result_python.txt(先頭10件のみ)
決心をする	と を	こうと 決心を
返報をする	を んで	返報を 偸んで
昼寝をする	を	昼寝を
昼寝をする	が を	彼が 昼寝を
迫害を加える	て を	追い廻して 迫害を
生活をする	て を	して 家族的生活を
話をする	を	話を
投書をする	て へ を	やって ほととぎすへ 投書を
話をする	に を	時に 話を
写生をする	を	写生を
```

#### UNIXコマンド実行結果
UNIXコマンドを実行し、「コーパス中で頻出する述語（サ変接続名詞+を+動詞）」を出力

```text:047.result_unix1.txt(先頭10件のみ)
     29 返事をする
     21 挨拶をする
     16 話をする
     15 真似をする
     13 喧嘩をする
      9 運動をする
      9 質問をする
      6 注意をする
      6 昼寝をする
      6 問答をする
```

UNIXコマンドを実行し、「コーパス中で頻出する述語と助詞パターン」を出力

```text:047.result_unix2.txt(先頭10件のみ)
     14 返事をする	と を
      9 運動をする	を
      9 真似をやる	を
      8 返事をする	と は を
      7 喧嘩をする	を
      6 話をする	に を
      6 挨拶をする	と を
      5 話をする	を
      5 挨拶をする	で に を
      4 質問をする	て を
```