言語処理100本ノック2020年版を解いてみた【第4章：形態素解析 30〜34】

東北大乾・岡崎研（現乾・鈴木研）で作成された、新人研修の一つであるプログラミング基礎勉強会の教材『[言語処理100本ノック 2020年版](https://nlp100.github.io/ja/)』をPython（3.7）で解いた記事の第7弾です。

第4章は昔研究で使っていた形態素解析です。
慣れてはいるものの、当時も独学だったので相変わらず間違いや非効率な部分があると思います。

改善点を見つけた際はご指摘いただけると幸いです。

ソースコードは[GitHub](https://github.com/Soh1121/NLP100-2020)にも公開しています。

## 第4章: 形態素解析

> 夏目漱石の小説『吾輩は猫である』の文章（[neko.txt](https://nlp100.github.io/data/neko.txt)）をMeCabを使って形態素解析し，その結果をneko.txt.mecabというファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

> なお，問題37, 38, 39は[matplotlib](http://matplotlib.org/)もしくは[Gnuplot](http://www.gnuplot.info/)を用いるとよい．

### MeCabを使った形態素解析

> 夏目漱石の小説『吾輩は猫である』の文章（[neko.txt](https://nlp100.github.io/data/neko.txt)）をMeCabを使って形態素解析し，その結果をneko.txt.mecabというファイルに保存せよ．

冒頭の形態素解析の実施を行います。
MeCabのインストールなどは[公式ページ](https://taku910.github.io/mecab/)や別な方が書かれた記事を参照してください。

```sh:mecab.sh
#!/bin/sh

mecab < ./input/neko.txt > ./output/neko.txt.mecab
```

`input`ディレクトリにある`neko.txt`を`<`を使って`mecab`コマンドに渡して形態素解析を実施します。
結果は`>`によって`output`ディレクトリに`neko.txt.mecab`というファイル名で出力します。

### 30. 形態素解析結果の読み込み

> 形態素解析結果（neko.txt.mecab）を読み込むプログラムを実装せよ．ただし，各形態素は表層形（surface），基本形（base），品詞（pos），品詞細分類1（pos1）をキーとするマッピング型に格納し，1文を形態素（マッピング型）のリストとして表現せよ．第4章の残りの問題では，ここで作ったプログラムを活用せよ．

```python:30.py
def parse(sentence):
    morphemes = []
    words = sentence.split("\n")
    words = [i for i in words if i != ""]
    for word in words:
        result = {}
        morpheme = word.split("\t")
        info = morpheme[1].split(",")
        result = {
            "surface": morpheme[0],
            "base": info[6],
            "pos": info[0],
            "pos1": info[1]
        }
        morphemes.append(result)
    return morphemes


file_name = "./output/neko.txt.mecab"
with open(file_name) as rf:
    sentences = rf.read().split("EOS\n")

sentences = [parse(s) for s in sentences if len(parse(s)) != 0]
print(result)
```

事前準備で作成した形態素解析データを読み込み、`EOS\n`ごとにリスト化します。
これをすることで、一文ごとに処理をできるようになります。

つづいて1文ごとにループを回して文を形態素単位に分割します。
分割した際に長さが0とならなければ分割結果をリストに追加することとしました。

形態素単位への分割は`parse()`関数で行っています。
`parse()`関数では与えられた文を`\n`で区切って形態素ごとに分割し、ループ内で取り出すよう言われている部分を抽出します。

抽出結果は後の処理のこともあり、一文ごとにリストとなって保存しています。

### 31. 動詞

> 動詞の表層形をすべて抽出せよ．

```python:31.py
def parse(sentence):
    # 「30. 形態素解析結果の読み込み」を参照
    ...

def verb_surfaces(sentence):
    verbs = list(filter(lambda x: x["pos"] == "動詞", sentence))
    return [verb["surface"] for verb in verbs]


file_name = "./output/neko.txt.mecab"
with open(file_name) as rf:
    sentences = rf.read().split("EOS\n")

sentences = [parse(s) for s in sentences if len(parse(s)) != 0]

# 以下、追加部分
verb_surface_list = set()
for sentence in sentences:
    verb_surface_list |= set(verb_surfaces(sentence))
print("\n".join(verb_surface_list))
```

テキストファイルを読み込んで形態素のリストを作る処理は前の問題で行ったので解説を省略します。
動詞の表層形を抽出ということで、抽出結果を入れるリストを作成し、形態素解析結果から動詞抽出する`verb_surfaces()`関数に一文ずつ処理を行わせます。

`verb_surfaces()`関数では`filter()`関数を用いて動詞を抽出しています。
`filter()`関数は

```python
filter(関数, リスト) # 関数の返り値はTrueまたはFalseになるもの
```

というふうに記述します。今回はlambda式を用いて`filter()`内の関数を作成しています。

```python
lambda x: x["動詞"] == "動詞"
```

で、リスト内の動詞の抽出です。

`return`の部分で抽出した動詞を表層形のみにして値を返却して回答となります。

### 32. 動詞の原形

> 動詞の原形をすべて抽出せよ．

```python:32.py
def parse(sentence):
    # 「30. 形態素解析結果の読み込み」を参照
    ...


def verb_bases(sentence):
    verbs = list(filter(lambda x: x["pos"] == "動詞", sentence))
    return [verb["base"] for verb in verbs]


file_name = "./output/neko.txt.mecab"
with open(file_name) as rf:
    sentences = rf.read().split("EOS\n")

sentences = [parse(s) for s in sentences if len(parse(s)) != 0]

# 以下、追加部分
verb_base_list = []
for sentence in sentences:
    verb_base_list += verb_bases(sentence)
print("\n".join(verb_base_list))
```

今度は動詞の原形を抽出ということで、31.同様抽出結果を入れるリストを作成し、形態素解析結果から動詞抽出する`verb_bases()`関数に一文ずつ処理を行わせます。

31.とほぼやっていることは同じで、filter()関数を用いて同士を抽出し、そこから更に原形のみにしたリストを返却して回答となります。

### 33. 「AのB」

> 2つの名詞が「の」で連結されている名詞句を抽出せよ．

```python:33.py
def parse(sentence):
    # 「30. 形態素解析結果の読み込み」を参照
    ...


def num_phrases(sentence):
    answer = []
    for i in range(len(sentence) - 2):
        words = sentence[i: i + 3]
        if words[1]["pos1"] == "連体化":
            answer.append("".join([j["surface"] for j in words]))
    return answer


file_name = "./output/neko.txt.mecab"
with open(file_name) as rf:
    sentences = rf.read().split("EOS\n")

sentences = [parse(s) for s in sentences if len(parse(s)) != 0]

# 以下、追加部分
num_phrase_list = []
for sentence in sentences:
    num_phrase_list += num_phrases(sentence)
print("\n".join(num_phrase_list))
```

33.は「の」でつながる名詞句の抽出です。  
31.・32.同様抽出結果を入れるリストを作成し、形態素解析結果から名詞句を抽出する`num_phrases()`関数に一文ずつ処理を行わせます。

`num_phrases()`関数では、1文内の連続する3つの単語を切り出し、真ん中の語が連体化となっていれば、3語を連結して回答群となるリスト化して値を返します。

### 34. 名詞の連接

> 名詞の連接（連続して出現する名詞）を最長一致で抽出せよ．

```python:34.py
def parse(sentence):
    # 「30. 形態素解析結果の読み込み」を参照
    ...


def num_connections(sentence):
    answer = []
    nouns = []
    for word in sentence:
        if word["pos"] == "名詞":
            nouns.append(word["surface"])
        else:
            if 1 < len(nouns):
                answer.append("".join(nouns))
            nouns = []
    if 1 < len(nouns):
        answer.append("".join(nouns))
    return answer


file_name = "./output/neko.txt.mecab"
with open(file_name) as rf:
    sentences = rf.read().split("EOS\n")

sentences = [parse(s) for s in sentences if len(parse(s)) != 0]

# 以下、追加部分
num_phrase_list = []
for sentence in sentences:
    num_phrase_list += num_connections(sentence)
print("\n".join(num_phrase_list))
```

34.は名詞が連なっている部分を最長一致で抽出です。  

これも1文ずつ`num_connections()`関数で処理を行っていきます。

 `num_connections()`関数では文中の単語を1語ずつ取り出し、名詞があった場合は`nouns`リストに追加します。

1度追加したあとは、名詞以外の語が来るまで`nouns`リストに`append()`メソッドで追加。

名詞以外の語が来た場合は、`nouns`リストが2語以上であれば結合して値を返却するリストに追加します。

文中の全語を調べ終わって`nouns`リストに語がある場合は文が名詞で終わっているので、2語以上で連なっている場合これも返却するリストに追加して回答を返しています。

## まとめ

この記事では言語処理100本ノック2020年版 第4章: 形態素解析の問題番号30〜34までを解いてみました。

形態素解析結果を元に条件を指定して抽出というタスクが多かったですね。

恐らく形態素解析に慣れるのが目的でしょう…！

自分はというと形態素解析は学生時代から使っていたので慣れているといえば慣れているのですが、未熟ゆえ泥臭い処理も多くなっていると思います。

徐々に洗練したコードにしていきたいと思っておりますので、ご指摘ありましたらどしどしコメントいただければ幸いです。

よろしくお願いいたします！

## 前回まで

- [言語処理100本ノック2020年版を解いてみた【第1章：準備運動 00〜04】](https://qiita.com/Soh1121/items/bd970195f93a23540d4c)
- [言語処理100本ノック2020年版を解いてみた【第1章：準備運動 05〜09】](https://qiita.com/Soh1121/items/edd976bc58909dd79062)
- [言語処理100本ノック2020年版を解いてみた【第2章：UNIXコマンド 10〜14】](https://qiita.com/Soh1121/items/5d35e6718d3f17a9976e)
- [言語処理100本ノック2020年版を解いてみた【第2章：UNIXコマンド 15〜19】](https://qiita.com/Soh1121/items/c372616f2445e98e368d)
- [言語処理100本ノック2020年版を解いてみた【第3章：正規表現 20〜24】](https://qiita.com/Soh1121/items/19405032949c4657fc8c)
- [言語処理100本ノック2020年版を解いてみた【第3章：正規表現 25〜29】](https://qiita.com/Soh1121/items/6449cd67a7b77d8bd244)