COTOHA API と絵文字で桃太郎をサマリーする

# はじめに

[COTOHA API](https://api.ce-cotoha.com/contents/index.html) を使った 記事 が面白いので、自分でも何かやってみたいと思い、「絵文字」と組み合わせて 文章の サマリー をやってみました。(WordCloudでの可視化 追記 2/25)

「桃太郎」[^1] はこんな風にサマライズされました。

![cotoha-api-momo4-taro1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118309/3c9927d2-d153-9be7-09ac-abd7d35ef3aa.png)

文章と絵文字の「自然言語処理的」なマッピングは私には無理でした。COTOHA の 構文解析 で取得した単語を WordNet データベースの類義語 (英語)と照合して 絵文字に置き換えてます。

[^1]:  「桃太郎」の文章は、[こちら](http://hukumusume.com/douwa/pc/jap/08/01.htm) のものを使わせてもらいました。



# 環境

- Windows10
- Python 3.6.5
- Jupyter notebook

今回使用した Notebook は [Gist](https://gist.github.com/kodamap/df630cab0b7e98e541b31465ea370df2) に上げました。

# 処理概要

COTOHA API の他は、類似語検索に [WordNet](http://compling.hss.ntu.edu.sg/wnja/) 、固有表現の可視化に [WordCloud](https://amueller.github.io/word_cloud/auto_examples/single_word.html)を使いました。

1. 前処理 : **[言い淀み除去 API]** 
入力する文章をきれいにします。

2. 感情分析サマリ: **[感情分析 API]** 
出力されるラベルと絵文字をマッピングして、スコアに応じて絵文字の数を増やして表示します。

3. 絵文字サマリ: **[構文解析 API] + [WordNet]:**
構文解析で 「名詞」と「動詞」を抽出して、WordNetから英語の類義語を照合します。取得した類似語を python の emoji モジュールから探します。英語の類義語を照合しているだけなので、結果の絵文字は微妙。 
**※** [構文解析 API] + [WordNet] の連携アイデアは、[「募ってはいるが、募集はしていない」 人たちへ](https://qiita.com/omiita/items/0f811f15e569bf2539b8)を参考にしました。

4. 固有表現サマリ: **[固有表現抽出 API]** + **[WordCloud]**
固有表現で抽出した単語を WordCloud で 可視化します。「ドンブラコ」は固有表現として抽出されるのですね。
**※** WordCloudでの可視化は[COTOHAを利用して、物語の舞台を抽出・図示してみた](https://qiita.com/tanaka-kazuki/items/a11a3277c500ffddff73)を真似しました。

## COTOHA APIのコード

**※**コードは[「自然言語処理を簡単に扱えると噂のCOTOHA APIをPythonで使ってみた」](https://qiita.com/gossy5454/items/83072418fb0c5f3e269f)を参考にしました。


<details><summary>コードを開く</summary><div>

```python
import urllib.request
import json
from pprint import pprint
import traceback

class CotohaApi:
    def __init__(self, client_id, client_secret, developer_api_base_url, access_token_publish_url):
        self.client_id = client_id
        self.client_secret = client_secret
        self.developer_api_base_url = developer_api_base_url
        self.access_token_publish_url = access_token_publish_url
        self.request_count = 0
        self.get_access_token()
        
    def request(self, url, data):
        headers={
            "Authorization": "Bearer " + self.access_token,
            "Content-Type": "application/json;charset=UTF-8",
        }
        req = urllib.request.Request(url, json.dumps(data).encode(), headers)  
        try:
            with urllib.request.urlopen(req) as res:
                res_body = json.loads(res.read())
        except urllib.error.HTTPError as e:
            if e.code == 401:
                print(e, ": retrieving an access token....\n")
                self.get_access_token()
                headers={
                    "Authorization": "Bearer " + self.access_token,
                    "Content-Type": "application/json;charset=UTF-8",
                }
                req = urllib.request.Request(url, json.dumps(data).encode(), headers) 
                with urllib.request.urlopen(req) as res:
                    res_body = json.loads(res.read())
            else:
                print(e)
                traceback.print_exc()
                return
        self.request_count+=1
        return res_body, res.status, res.reason      
        
    def get_access_token(self):
        url = self.access_token_publish_url
        headers={
            "Content-Type": "application/json;charset=UTF-8",
        }
        data = {
            "grantType": "client_credentials",
            "clientId": self.client_id,
            "clientSecret": self.client_secret
        }
        req = urllib.request.Request(url, json.dumps(data).encode(), headers)
        with urllib.request.urlopen(req) as res:
                res_body = json.loads(res.read())
        self.request_count+=1
        self.access_token = res_body["access_token"]

    # 構文解析
    def parse(self, sentence):
        url = self.developer_api_base_url + "v1/parse"
        data = {
            "sentence": sentence
        }
        res_body, status, reason = self.request(url, data)
        return res_body

    # 固有表現抽出
    def named_entry(self, sentence):
        url = self.developer_api_base_url + "v1/ne"
        data = {
            "sentence": sentence
        }
        res_body, status, reason = self.request(url, data)
        return res_body

    # 照応解析
    def coreference(self, document):
        url = self.developer_api_base_url + "v1/coreference"
        data = {
            "document": document,
            "type": "default",
            "do_segment":True,
        }
        res_body, status, reason = self.request(url, data)
        return res_body

    # キーワード抽出
    def keyword(self, document):
        url = self.developer_api_base_url + "v1/keyword"
        data = {
            "document": document,
            "type": "default",
        }
        res_body, status, reason = self.request(url, data)
        return res_body

    # 類似度算出
    def similarity(self, s1, s2):
        url = self.developer_api_base_url + "v1/similarity"
        data = {
            "s1": s1,
            "s2": s2
        }
        res_body, status, reason = self.request(url, data)
        return res_body

    # 文タイプ判定
    def sentence_type(self, sentence):
        url = self.developer_api_base_url + "v1/sentence_type"
        data = {
            "sentence": sentence
        }
        res_body, status, reason = self.request(url, data)
        return res_body

    # ユーザ属性推定
    def user_attribute(self, document):
        url = self.developer_api_base_url + "beta/user_attribute"
        data = {
            "document": document
        }
        res_body, status, reason = self.request(url, data)
        return res_body

    # 感情分析
    def sentiment(self, sentence):
        url = self.developer_api_base_url + "v1/sentiment"
        data = {
            "sentence": sentence
        }
        res_body, status, reason = self.request(url, data)
        return res_body

    # 言い淀み除去(β)
    def remove_filter(self, text):
        url = self.developer_api_base_url + "beta/remove_filler"
        data = {
            "text": text,
            "do_segment":True,
        }
        res_body, status, reason = self.request(url, data)
        return res_body
```

</div></details>



## WordNetから類似語を取得する


WordNetからデータベース `wnjpn.db` を[ダウンロード](http://compling.hss.ntu.edu.sg/wnja/)してカレントディレクトリに置いておきます。

<details><summary>類似語をもってくるクエリ</summary><div>

```py
query = """
SELECT
    word.wordid,
    COUNT(word.wordid) AS COUNT,
    word.lemma,
    word.lang,
    sense.synset
FROM
    sense
    JOIN word ON word.wordid = sense.wordid
WHERE
    1 = 1
    AND sense.synset IN(
        SELECT 
            synset
        FROM
            sense
        WHERE
            wordid IN (
                SELECT
                    wordid
                FROM 
                    word
                WHERE 
                    1 = 1
                    AND lemma = ?
            )
    )
    AND word.lang = ?
GROUP BY word.wordid
ORDER BY COUNT DESC
"""
```

</div></details>


<details><summary>類似語をもってくるコード</summary><div>

```py
# ref:
# WordNet Ja: http://compling.hss.ntu.edu.sg/wnja/
import sys, sqlite3
from pprint import pprint

def get_synonyms(word, lang='eng'):
    """
    search synonyms of the input word from wordnet japnese database
    http://compling.hss.ntu.edu.sg/wnja/
    """
    synonyms = []
    conn = sqlite3.connect("./wnjpn.db")
    c = conn.cursor()
    rows = c.execute(query, (word ,lang))       
    for row in rows:
        synonyms.append(row[2])
    c.close()
       
    return synonyms

def get_emoji(words, count):
    """
    words: list of words you want to emojize
    count: count of the emoji to display depending on the sentiment api score
    """
    assert(type(words) is list)
    emojis = []   
    for word in words:
        if re.search(':.*:', word):
            emoji = emj.emojize("{}".format(word)*count, use_aliases=True)
        else:
            emoji = emj.emojize(":{}:".format(word)*count, use_aliases=True)
        if not re.search(':.*:', emoji):
            emojis.append("{}([{})".format(emoji,word))
        #else:
            #print(":{}: could not be emojized".format(word))
    return emojis
```

</div></details>

こんな感じで類義語と絵文字を探します。

```py
word="桃"
synonyms = get_synonyms(word)
pprint(synonyms)
get_emoji(synonyms, 1)

# Output
['pink', 'peach']
['🍑([peach)']
```


## サマリーを表示するコード


**Input**

```py
sentence1= """
むかしむかし、あるところに、おじいさんとおばあさんが住んでいました。
おじいさんは山へしばかりに、おばあさんは川へせんたくに行きました。
おばあさんが川でせんたくをしていると、ドンブラコ、ドンブラコと、大きな桃が流れてきました。
「おや、これは良いおみやげになるわ」
おばあさんは大きな桃をひろいあげて、家に持ち帰りました。
そして、おじいさんとおばあさんが桃を食べようと桃を切ってみると、なんと中から元気の良い男の赤ちゃんが飛び出してきました。
「これはきっと、神さまがくださったにちがいない」
子どものいなかったおじいさんとおばあさんは、大喜びです。
桃から生まれた男の子を、おじいさんとおばあさんは桃太郎と名付けました。
"""
```

**サマリー表示**

環境変数に `CLIENT_SECRET`, `CLIENT_ID` を指定して、Pythonから読み込みます。Jupyter notebook の場合も、環境変数を指定して 起動すれば同じように使えます。
WordCloudで表示する際に必要な日本語フォント`ipagp.ttf` は[IPA フォント ダウンロードページ](https://ipafont.ipa.go.jp/old/ipafont/download.html)からダウンロードしてカレントディレクトリに置きます。


<details><summary>サマリー表示用のFunction</summary><div>

```py
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import numpy as np
from pprint import pprint

emotional_label = {'Positive': ':thumbsup:',
                   'Negative': ':thumbsdown:',
                   'Neutral': ':hand:'}
emotion_label = {'喜ぶ': ':smile:',
                 '怒る': ':rage:',
                 '悲しい': ':cry:',
                 '不安': ':worried:',
                 '恥ずかしい': ':flushed:',
                 '好ましい': ':expressionless:',
                 '嫌'': ': ':stuck_out_tongue_closed_eyes:',
                 '興奮': ':laughing:',
                 '安心': ':relieved:',
                 '驚く': ':astonished:',
                 '切ない': ':disappointed:',
                 '願望': ':wink:',
                 'P': ':smile:',
                 'N': ':pensive:',
                 'PN': ':neutral_face:'}


def preprocess(sentence):
    result = cotoha_api.remove_filter(sentence)
    fixed_sentences_list = []
    for i, fixed_sentence in enumerate(result['result']):
        fixed_sentences_list.append(result['result'][i]['fixed_sentence'])
    return fixed_sentences_list


def get_words_by_class(parsed_result, word_classes):
    words = []
    for id in range(len(parsed_result['result'])):
        for word_class in word_classes:
            if parsed_result['result'][id]['tokens'][0]['pos'] == word_class:
                word = parsed_result['result'][id]['tokens'][0]['lemma']
                words.append(word)
    return words


def emotion_score(sentence, verbose=False):
    # initialize emotion lable: 'PN' means Neutral(Positive-Negative)????
    emotion = 'PN'
    emotions = {}
    result = cotoha_api.sentiment(sentence)
    if verbose:
        pprint(result)

    sentiment = result['result']['sentiment']
    score = result['result']['score']
    emotional_phrase = result['result']['emotional_phrase']

    # emotinal score: 5 levels (low -> high : 1 to 5)
    # score: 0 < 0.2 < 0.4 < 0.6 < 0.8 < 1.0 -> level 1, 2, 3, 4, 5
    emotional = get_emoji([emotional_label[sentiment]], int(score//0.2 + 1))

    if emotional_phrase:
        for i in range(len(result['result']['emotional_phrase'])):
            emotion = result['result']['emotional_phrase'][i]['emotion']
            emotions[str(emotion)] = get_emoji([emotion_label[emotion]], 1)

    return emotional[0], sentiment, score, emotions


def show_emoji_summary(words):
    synonyms = {}
    emojis_summary = {}

    for word in words:
        synonyms[str(word)] = get_synonyms(word, lang='eng')

    for key, word_list in synonyms.items():
        if len(word_list) > 0:
            emoji_list = get_emoji(word_list, 1)
            if len(emoji_list) > 0:
                emojis_summary[str(key)] = emoji_list
    return emojis_summary


def show_named_entry(words):
    named_entry = []
    for word in words:
        result = cotoha_api.named_entry(word)
        if len(result['result']) > 0:
            named_entry.append(result['result'][0])
    return named_entry


def get_wordcrowd_mask(text):
    """ref:
    https://amueller.github.io/word_cloud/auto_examples/single_word.html
    """
    font_path = './ipagp.ttf'
    x, y = np.ogrid[:300, :300]
    mask = (x - 150) ** 2 + (y - 150) ** 2 > 130 ** 2
    mask = 255 * mask.astype(int)
    wc = WordCloud(font_path=font_path, random_state=1, 
                   mask=mask, background_color="white").generate(text)
    return wc
```

</div></details>


<details><summary>サマリー表示メイン部分</summary><div>


```py
%matplotlib inline
import os

CLIENT_ID = os.getenv('CLIENT_ID')
CLIENT_SECRET = os.getenv('CLIENT_SECRET')
ACCESS_TOKEN_PUBLISH_URL="https://api.ce-cotoha.com/v1/oauth/accesstokens"
DEVELOPER_API_BASE_URL = "https://api.ce-cotoha.com/api/dev/nlp/"

cotoha_api = CotohaApi(CLIENT_ID, CLIENT_SECRET, 
                       DEVELOPER_API_BASE_URL, ACCESS_TOKEN_PUBLISH_URL)

word_classes=["名詞", "動詞語幹"]

def get_summary(sentence):
    
    # preprocess: remove filter
    fixed_sentences_list = preprocess(sentence)
 
    # cancatenate fixed sentences produced by remove filter api.
    concat_sentence=""
    for fixed_sentence in fixed_sentences_list:
            concat_sentence+=fixed_sentence
        
    # get synomnyms of the words produced by the parse api.
    parsed_result = cotoha_api.parse(concat_sentence)
    words = get_words_by_class(parsed_result, word_classes)

    # Display summary
    print("\nInput:\n","-"*40,"\n",concat_sentence)

    # show emotion summmary
    print("\nOutput:\n","-"*40)
    emotional, sentiment, score, emotions = emotion_score(concat_sentence, verbose=False)
    print("*** emotion summary ***")
    print("{}:{} score:{:.2f}".format(emotional, sentiment, score))
    for key, values in emotions.items():
        print("  {}:{}".format(values, key))
              
    # show emoji summary of sentence 
    emojis_summary = show_emoji_summary(words)
    if len(emojis_summary) > 0:
        print("\n*** emoji summary ***")
        for key, value in emojis_summary.items():
            values = ""
            for v in value:
                values+=v
            print("{:5}{}".format(key, values))
    
    # show named entry summary of sentence 
    named_entries = cotoha_api.named_entry(concat_sentence)
    if len(named_entries['result']) > 0:
        print("\n*** named entry summary ***")
        named_entry_summary = ""
        for named_entry in named_entries['result']:
            named_entry_summary+=named_entry['form'] + " "
        #print("words:", named_entry_summary)
        wc = get_wordcrowd_mask(named_entry_summary)
        plt.imshow(wc, interpolation="bilinear")
        plt.axis("off")

# main
get_summary(sentence1)
```

</div></details>

# いざ鬼退治へ

鬼 :japanese_ogre: ,サル:monkey:, 犬:dog: が出たのはうれしいです（キジはいない・・・）。絵文字では鬼退治が表現できていないのが残念ですが、WordCloud はいい感じです。

![cotoha-api-momo4-taro2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118309/eaa87cdb-54f0-fc35-aed3-fa3e524e762d.png)

# まとめ

COTOHA API を はじめて知って使ってみましたがすごく使いやすいです。内容が充実しているしAPIのレスポンスが速いです。登録してすぐに使えて「日本語」に強いってところが素晴らしいと思いました。


そもそもは会社のレポートを感情分析して時系列に俯瞰したいと思ったのがモチベーションでした。敷居が高いと思っていた感情分析が COTOHA API でできるので、これを絵文字で表現すべく頑張りましたが、気の利いた絵文字へのマッピングは私には無理でした。

やりたかったのは 例えば**「仕事帰りに飲みに行った。」** ⇒ :beer: のような「文脈」からの変換です。`仕事` `終わり` `飲む` という特徴から :beer: を推測するには、コーパスに絵文字をマッピングするのかな、、わかりません。