～現役薬剤師がAIを触ってみた～　その相談はポジティブですか？、ネガティブですか？


# はじめに
筆者の本職は薬剤師ですがご縁があってCOTOHAの中の人をやっております。薬局薬剤師歴５年目（やっと中堅に差し掛かったくらいかな？）pythonを学び始めて約３カ月の初心者です。

薬局薬剤師という仕事柄、患者さんからの相談を受けることがありますが皆様もプライベート、業務に関係なく相談を受けたときこんなことを思ったことはありませんか？

それは・・・
<br>
<br>
<br>
<br>
### 「真面目にかえすか、それともスルーするか！」(●´･ω･)a ｪｯﾄ…
<br>
<br>
<br>
<br>
薬局ではせっかく相談してくれたことを蔑ろにはできません、が、あまり長く話し込んでしまいますと業務が滞って他の患者さんに迷惑をかけてしまいます。

（待ち時間に対してのお叱りは本当に多いです。大変申し訳なく思っておりますm(__)m）
<br>

それでも薬剤師なら時間がない中でもこんなことを思うはず。

・なるべく多くの相談にのってあげたい！！
・落ちた気持ちを少しでも前向きにさせてあげたい！！
・病気がよくなった。気持ちが楽になったと言ってほしい！！

そんな薬剤師のため患者さんの心のケアを助けるツールとしてCOTOHAの音声認識※と感情分析を組み合わせてみました。

※音声認識APIはFor Enterprise版の提供のみとなっています，3ヶ月のお試し期間があるためそちらを使ってお試しいただければと思います!!詳細は下記ポータルを参照ください
[COTOHA API ポータル](https://api.ce-cotoha.com/contents/index.html)

# 実行環境
Windows10

Python 3.7.5

Visual Studio Code

COTOHA API

# 実装
APIアカウント認証情報
<details><summary>Source Code | クリックしてください</summary><div>

``` json
{
"client_id":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"client_secret":"yyyyyyyyyyyyy",
"domain_id":"zzzzzzzz"
}
```
上記に認証情報を記載したjsonファイルを任意の名前で作成してください。今回は［your.json]と名付けます。
</div></details>
<br>

音声認識クリーン_レスポンス
<details><summary>Source Code | クリックしてください</summary><div>

``` python
def clean_response(text):  # 全角を半角に変換、スペースを除去、句点を付与
    OFFSET = ord('０')-ord('0')  # 全角コードと半角コードの差分を取得
    dic = {chr(ord('０')+i): chr(ord('０')+i-OFFSET) for i in range(10)}  # 数字
    dic.update({chr(ord('Ａ')+i): chr(ord('Ａ')+i-OFFSET) for i in range(26)})  # 英字大文字
    dic.update({chr(ord('ａ')+i): chr(ord('ａ')+i-OFFSET) for i in range(26)})  # 英字小文字
    text = text.translate(str.maketrans(dic))
    text = text.replace(" ", "")
    text += "。"
    return text
```
返ってきた音声認識の文を見やすくキレイに整えてくれるものです。［clean_response.py]として保存してください
</div></details>
<br>

メインコード
<details><summary>Source Code | クリックしてください</summary><div>

``` python
import json
import sys
import time

import pyaudio
import requests
import termcolor
import numpy as np
import matplotlib.pyplot as plt
import matplotlib

from pylab import *

from clean_response import clean_response

args = sys.argv
json_name = args[1]
oauth_url = 'https://api.ce-cotoha.com/v1/oauth/accesstokens'
model_id = 'ja-gen_tf-16'
hostname = 'https://api.ce-cotoha.com/api/'
url = hostname + 'asr/v1/speech_recognition/' + model_id
with open(json_name) as f:
    credential = json.load(f)
client_id = credential['client_id']
client_secret = credential['client_secret']
domain_id = credential['domain_id']


#円グラフの作成
def pie_chart(x,y):
    plt.close() 
    matplotlib.use("wxagg")
    plt.figure(figsize=(5,3))
    colors = ["blue", "red"]
    label = ["Positive", "Negative"]
    plt.title('Total') 
    plt.pie(np.array([x,y]),labels=label, counterclock=False, startangle=90,autopct="%1.1f%%")
    thismanager = get_current_fig_manager()
    thismanager.window.SetPosition((850, 300))#画面右下にグラフを表示
    plt.pause(1)


class StreamingRequester:
    def __init__(self):

        self.channels = 1
        self.format = pyaudio.paInt16
        self.rate = 44100
        self.Interval = 0.24
        self.nframes = int(self.rate * self.Interval)

        self.url = url
        self.client_id = client_id
        self.client_secret = client_secret
        self.param_json = {"param": {
            "baseParam.samplingRate": self.rate,
            "recognizeParameter.domainId": domain_id,
            "recognizeParameter.enableContinuous": 'true'
            }}
        self.x = 0
        self.y = 0

#アクセストークンの取得
    def get_token(self):
        headers = {"Content-Type": "application/json;charset=UTF-8"}
        obj = {'grantType': 'client_credentials', 'clientId': self.client_id,
               'clientSecret': self.client_secret}
        data_json = json.dumps(obj).encode("utf-8")
        response = requests.post(url=oauth_url, data=data_json,
                                 headers=headers)
        self.access_token = response.json()['access_token']

# エラー発生時の処理
    def check_response(self):
        if self.response.status_code not in [200, 204]:
            print("STATUS_CODE:", self.response.status_code)
            print(self.response.text)
            exit()

# 感情分析
    def sentiment(self):
        base_url = hostname + "nlp/"
        headers = {
            "Content-Type": "application/json",
            "charset": "UTF-8",
            "Authorization": "Bearer "+ self.access_token
            }
        data = {
            "sentence": self.sentence,
            "type": "default"
                }
        r = requests.post(base_url + "v1/sentiment",
                        headers=headers,
                        data=json.dumps(data))

        return r.json()

# レスポンスをパースし認識結果を標準出力
    def print_result(self):
        # statusが200のときのみresponseにjsonが含まれる
        if self.response.status_code == 200:
            for res in self.response.json():
                if res['msg']['msgname'] == 'recognized':
                    # type=2ではsentenceの中身が空の配列の場合がある
                    if res['result']['sentence'] != []:
                        self.sentence = clean_response(res['result']['sentence'][0]['surface'])
                        #感情分析を開始
                        e_phrase = self.sentiment()['result']['emotional_phrase']
                        
                        for e_select in e_phrase[0:]:
                            if e_select['emotion'] == "P" 
                            or e_select['emotion'] == "好ましい" 
                            or e_select['emotion'] == "喜ぶ" 
                            or e_select['emotion'] == "安心":
                                e_color = termcolor.colored(e_select['form'],'blue')
                                self.sentence = self.sentence.replace(e_select['form'],e_color)
                                self.x =self.x + 1
                            elif e_select['emotion'] == "N" 
                            or e_select['emotion'] == "怒る" 
                            or e_select['emotion'] == "不安"
                            or e_select['emotion'] == "恥ずかしい" 
                            or e_select['emotion'] == "嫌":
                                e_color = termcolor.colored(e_select['form'],'red')
                                self.sentence = self.sentence.replace(e_select['form'],e_color)
                                self.y =self.y + 1

                        print(self.sentence)
                        """ print(self.sentiment()) """#ONにすると感情分析の解析結果を参照できる
                        pie_chart(self.x,self.y)

# 開始要求
    def start(self):
        obj = self.param_json
        obj['msg'] = {'msgname': 'start'}
        data_json = json.dumps(obj).encode("utf-8")
        headers = {"Content-Type": "application/json;charset=UTF-8",
                   "Authorization": "Bearer " + self.access_token}
        self.requests = requests.Session()
        self.response = self.requests.post(url=self.url, data=data_json,
                                           headers=headers)
        self.check_response()
        # uniqueIdはデータ転送、停止要求に必要
        self.unique_id = self.response.json()[0]['msg']['uniqueId']

 # 停止要求
    def stop(self):
        headers = {"Unique-ID": self.unique_id,
                   "Content-Type": "application/json;charset=UTF-8  ",
                   "Authorization": "Bearer " + self.access_token}
        obj = {"msg": {"msgname": "stop"}}
        data_json = json.dumps(obj).encode("utf-8")
        self.response = self.requests.post(url=self.url, data=data_json,
                                           headers=headers)
        self.check_response()
        self.print_result()


    def _post(self):
        self.headers = {"Content-Type": "application/octet-stream",
                        "Unique-ID": self.unique_id,
                        "Authorization": "Bearer " + self.access_token}

        self.response = self.requests.post(url=self.url, data=self.data,
                                           headers=self.headers)

        self.check_response()
        self.print_result()


    def listen(self):

        p = pyaudio.PyAudio()

        stream = p.open(format=self.format,
                        channels=self.channels,
                        rate=self.rate,
                        input=True,
                        frames_per_buffer=self.nframes
                        )

        print("Now Recording")

        try:
            while True:
                self.data = stream.read(self.nframes, exception_on_overflow=False)
                self._post()
        #　"Ctrl+C" でプログラム終了
        except KeyboardInterrupt:
            stream.close()
            p.terminate()
            sys.exit("Exit Voice Recognition")

if __name__ == '__main__':
    requester = StreamingRequester()
    requester.get_token()
    requester.start()
    requester.listen()
    requester.stop()
```
音声認識と感情分析を行うコードです。今回は［Positive_Negative.py]で保存します
</div></details>
<br>

上記３種類のSource Codeを同じフォルダにいれて/srcディレクトリに移動しpython3 Positive_Negative.py your.json を実行してください。

# 検証
## 患者さんとのやり取り　①
私「今回は糖尿病のお薬が強くなっていますね、先生からお話は伺っていますか？」

患「はい、年末年始に忘年会や新年会に出席することが多くて暴飲暴食してしまいました。
　　血液検査の結果も悪くなっていて先生から怒られてしまいました。
　　気を付けてはいるんだけど営業職だから付き合いは断れなくて・・・」

私「なるほど、ですが今回は原因もはっきりしていますのでしっかりと改善していきましょう。
　　糖尿のお薬は強くなっているので低血糖のリスクが上がっています、しばらくは注意してくださいね。」

患「はい。あと、良くないと思っているのですが仕事で遅くなったときは疲れて食べてすぐに
　　寝るから薬を飲み忘れてしまいます。それで夕食後の薬だけ余って来てしまいました。」

私「もしかしたら飲み忘れも悪化の原因になっているかもしれません。
　　生活習慣も含めてDrとも用法について相談していった方が良いでしょう。
　　もしよろしければ私の方からお薬の日数の調節と用法についてDrと相談してみましょうか？」

患「やっぱりそうかな。飲み忘れのことは次回に自分から話してみます。
　　お薬の余りも10日分くらいだから予備として持っておくことにします。
　　ところでいつも食事に気をつけろって言われるけど何に気を付ければいいの？」


<details><summary>想定①　解析結果 | クリックしてください</summary><div>

<img width="700" alt="demo1_sentence.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517900/b3859b9e-3396-151a-db28-823437a4847b.png">

<font color="DodgerBlue">**ポジティブ**</font>ワードは<font color="DodgerBlue">**青文字**</font>

<font color="Tomato">**ネガティブ**</font>ワードは<font color="Tomato">**赤文字**</font>で表示されます。

<img width="500" alt="demo1_pie_chart.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517900/b9bbec01-f127-8056-4d20-4de2a6046d60.png">

患者さんの音声だけを認識にかけていると想像してください。

解析の結果から患者さんはとても<font color="Tomato">**Negative**</font>な状態にあることがわかります。
あまり気分が落ち込みすぎれば、生活に支障をきたすこともあるでしょう。なるべく相談にのって継続的にケアしていきたいところです。

</div></details>
<br>
<br>

## 患者さんとのやり取り　②
私「今回もいつもと同じお薬です、お変わりありませんね。」

患「最近近くにパン屋さんができたでしょ？そこで買った食パンがとってもおいしかったの。
　　それからお向かいのスーパーはお野菜が安いのよ。」

私「そうですか。」

患「先週は３歳になった孫が遊びにきてね、この前産まれたばかりだと思っていたのに、
　　すっかり大きくなっておしゃべりもできるようになったんですよ。」

私「なるほど」

患「今週の月９ドラマはみたかしら？続きがとても気になる終わり方でしたね。
　　薬剤師さんは今後の展開どうなると思います？」

<details><summary>想定②　解析結果 | クリックしてください</summary><div>

<img width="700" alt="demo2_sentence.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517900/5bb0cddb-d977-37ae-8ccf-58267ea1a1ee.png">

<img width="500" alt="demo2_pie_chart.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517900/75489433-26e2-02fa-955a-71991af37133.png">

解析結果は<font color="DodgerBlue">**Positive**</font>です。
<font color="DodgerBlue">**Positive**</font>な人だから相談にのってあげないというわけではありませんが、今回は内容に脈略がなく話が長い人の典型例です。相談内容に緊急性が全くないことがわかります。
時間があれば世間話に付き合ってあげるのもよいのですが、混雑時にはなるべく早めに切り上げたいところです。
</div></details>


# まとめ
いかがだったでしょうか？COTOHA APIで簡易的な感情分析ができました！

実際のお薬の受け渡しは対面で接していますので患者さんの声のトーンや表情、仕草から感情を推測し会話されていると思います。今回紹介したツールは投薬中の出番はないかもしれません。

ただこの解析結果を薬歴に残しておけば、次回別の薬剤師が対応したときも前回の記録をもとにより詳しく患者さんの状態を推測できます。

店舗に置いてやってみたいところですが、まわりが十分に静かな環境が必要であったり指向性の高いマイクを患者さんに向けなければなりませんのでまだ少しハードルが高いかなと思います。

もし今後、遠隔医療が進んでタブレット画面に向かいながら服薬指導なんてことも多くなれば、映像では受け取りにくい患者さんのリアルタイムな情報を補完してくれるツールになるかもしれません。

<img width="300" alt="遠隔診療.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/467283/f27cf998-cde1-37a4-fb69-9bd96ed3143d.png">



# 参考記事
[オレ プログラム ウゴカス オマエ ゲンシジン ナル](https://qiita.com/Harusugi/items/f499e8707b36d0f570c4)

[COTOHA音声認識を中の人が仕組みからサンプルまで軽く解説](https://qiita.com/cotocoto/items/3494a625bb339a6ccb4b)

# 参考図書
* 株式会社ビープラウド・監修／リブロ・ワークス・著　「Python ふりがなプログラミング」、インプレス出版
* 斎藤　康毅・著　「ゼロから作る　Deep Learning Pythonで学ぶディープラーニングの理論と実装」　発行：オライリー・ジャパン　発売：オーム社