機械学習
自然非言語処理
表情認識
マルチアームバンディット

自然非言語処理第14日目:表情を手がかりとしたお笑いの自律的学習(応用編)

slot.png

あなたは今カジノのコインをいくつか拾ったとします。
ふと目の前を見れば、いくつものスロットマシンがあります。
さて、あなたはどのスロットのアームに手をかけますか?

われわれの人生はギャンブルにあふれています。

どんな職業を選ぶのか、どんなパートナーを選ぶのか、どんな最後を選ぶのか。

コミュニケーションだって同じです。
よくわからない相手に対して顔色を見つつ手探りで、相手が喜びそうな話題を提供したり、プレゼントをしたりするでしょう。

人間はさておき、機械は人に手探りするようにコミュニケーションを取ることが可能なのでしょうか。

今回はマルチアームバンディット問題をコミュニケーションに応用し、実装してみましょう。

マルチアームバンディット問題

マルチアームバンディット問題とは、「複数のアームと呼ばれる候補から最も良いものを逐次的に探す問題」[1]です。
もう少し詳しく言うと、最初の小話で想像してもらえると説明がはかどりますが、
複数のスロットマシンがあり、それぞれのスロットマシンのあたり確率が偏っている場合において、
ギャンブラーが手持ちのコインをなるべく多くするにはどのスロットマシンをどのように選べばいいのでしょうかという
思考実験っぽい問題です。

マルチアームバンディット問題を試しにギャンブラーXさんに解かせてみましょう。
あたり確率の分からないスロットマシンAとスロットマシンBが2つあったとします。
手持ちのコインは10枚ぐらいとしましょう。また、スロットマシンを1回試すのにコイン1枚かかるとしましょう。
スロットマシンAを5回試したところ、1回あたりました。4枚ゲットしました。
スロットマシンBを5回試したところ、1回当たりました。4枚ゲットしました。
残り8枚です。
Xさんは「どっちもなんだかあたり確率は変わらなさそう」と思いました。
さて、次に
スロットマシンAを3回試したところ、1回あたりました。4枚ゲットしました。
Xさんは「スロットマシンAが得じゃね」と思いました。
なぜならば、
スロットマシンAのあたり確率:(1+1)/(5+3)=2/8=25%
スロットマシンBのあたり確率:1/5=20%
となるからです。
そこで、Xさんはあとのことは何も考えず、スロットマシンAに全部かけました。
結果は0枚ですっからかんです。
一体何がダメだったんでしょうか。
Xさんがスロットマシンのあたり確率を確定させるのには試行回数が不十分だったという点です。
ただ、同情できるところもあるでしょう。10枚という限られた枚数の中ではあまり試すことができないからです。

このように(1)スロットマシンを試さないとあたり確率がわからない、(2)しかし、試して予想したあたり確率を利用するほかない、という2点が問題のキーポイントとなってきます。これを探索と活用のトレードオフと言います。また、コインの枚数を多くするという目的に従って、ギャンブラーは自律的に行動しています。これもポイントです。

わからない人とコミュニケーションをとることも探索と活用のトレードオフが実は潜んでいます。つまり、コミュニケーションを取らないと好きな話題がわからない、好きな話題を与えたほうがコミュニケーションが捗るというところです。あまり興味のない話題を振っても相手はつまらないばかりになります。実は、もっと好きな話題が実はあったりするかもしれません。しかし、相手と話せる時間も話題を変えられる回数も限られています。

じゃあ、なんでそんなわからない人とコミュニケーション取るんだ、めんどくさいという話です。
相手に好かれたい、あるいは幸福にしたいという欲求が人にはあるからでしょう。相手が魅力的ならなおさらです。

そこで、「相手を幸福にしたい」ということを目的とした好きな話題、というとめんどくさいので、ツボにはまりそうなギャグを探す問題を機械に与えてみましょう。

実装: 面白いオヤジギャグを披露する機械

ということで、面白いオヤジギャグをドヤ顔で披露する機械を作ってみましょう。

問題設定の詳細

時間は2分。(Twitterの動画投稿の都合上)
機械の目的を「相手を笑わせること」にします。
目的を定量化すると「相手を笑わせた回数」になります。
アームの種類をオヤジギャグとします。

僕をにやりと笑わせた回数をなるべく多くすることを機械は目指します。
まさにイロモネアみたいですね、この問題設定。

ソースコード

今回話してくるオヤジギャグの種類は以下のとおりです。

gagList.txt
アルミ缶の上にあるみかん
トイレにいっといれ
引用してもいいんよう
校長絶好調
草刈ったら臭かった
こんばん脇毛
最高だぜ!さあ行こう!
象だぞう
ふとんがふっとんだ

次にオヤジギャグの種類を学習する部分は以下のとおりです。
なお、学習の方針としてε-greedy法を採用しています。
これは一定の低確率でランダムにオヤジギャグの種類を選びながら、ツボにはまりそうなギャグを探しつつ、その確率をひかなかったときはツボにはまったギャグを繰り返し行うという戦法(ポリシー)です。

MultiArm.py
# coding: UTF-8
__auhtor__="alfredplpl"

#See also:
#https://westus.dev.cognitive.microsoft.com/docs/services/5639d931ca73072154c1ce89/operations/56f23eb019845524ec61c4d7
#http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_gui/py_video_display/py_video_display.html

import httplib,cv2,json
import numpy as np
import winsound
import time
from datetime import datetime

## constants
# ユーザの状態を表現するための変数たち
HAPINESS_PATH="./img/hapiness_user.png"
LAUGH_PATH="./img/laugh_user.png"
NEUTRAL_PATH="./img/neutral_user.png"
HAPINESS_IMG=cv2.imread(HAPINESS_PATH)
LAUGH_IMG=cv2.imread(LAUGH_PATH)
NEUTRAL_IMG=cv2.imread(NEUTRAL_PATH)
############

#マルチバンディッドのアームを表すクラス
class Arm:
    def __init__(self,gag):
        self.gag=gag
        self.count=0
        self.score=0

    def getGag(self):
        self.count+=1
        return self.gag

    def setReward(self,reward):
        self.score+=reward

    def getExpectation(self):
        if(self.count==0):return 0
        return self.score/self.count

#人間っぽいもののクラス
class Agent:
    # to display the machine
    ROBOT_PATH = "./img/robot.png"
    ROBOT_IMG = cv2.imread(ROBOT_PATH)

    #MSのクラウドサービスを使うためのヘッダー類
    headersEmotion = {
        # Request headers
        'Content-Type': 'application/octet-stream',
        'Ocp-Apim-Subscription-Key': '[Your API Key]',
    }
    tokenTTS = {
        # Request headers
        'Ocp-Apim-Subscription-Key': '[Your API Key]',
    }
    headersTTS={
        'Content-Type': 'application/ssml+xml',
        "X-Microsoft-OutputFormat": "riff-16khz-16bit-mono-pcm",
    }
    SSML_TEMPLATE="<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='http://www.w3.org/2001/mstts' xml:lang='ja-JP'><voice xml:lang='ja-JP' name='Microsoft Server Speech Text to Speech Voice (ja-JP, Ichiro, Apollo)'>{}</voice></speak>"
    EPS=0.5
    SIZE = (640, 480)
    def __init__(self):
        self.arms=[]
        self.windowImg=np.zeros([720,1280,3],dtype=np.uint8)

        with open("gagList.txt") as f:
            for line in f:
                self.arms.append(Arm(line[:len(line)-1]))

        #音声合成開始用認証トークン取得。これを最初にやらないと音声合成できない
        try:
            conn = httplib.HTTPSConnection('api.cognitive.microsoft.com')
            conn.request("POST", "/sts/v1.0/issueToken","",
                         self.tokenTTS)
            response = conn.getresponse()
            token = response.read()
            print(token)
            conn.close()
        except Exception as e:
            print("[Errno {0}] {1}".format(e.errno, e.strerror))

        #音声合成用のヘッダの修正
        self.headersTTS["Authorization"]=token
        self.headersTTS["User-Agent"]="python"

        self.cap = cv2.VideoCapture(0)
        self.windowImg[:self.ROBOT_IMG.shape[0],:self.ROBOT_IMG.shape[1],:]=self.ROBOT_IMG
        self.windowImg[:NEUTRAL_IMG.shape[0],self.ROBOT_IMG.shape[1]:(NEUTRAL_IMG.shape[1]+self.ROBOT_IMG.shape[1]),:]=NEUTRAL_IMG

        cv2.imshow("OYAJI GAG ROBOT",self.windowImg)

        cv2.waitKey(32)
        text="おれはオヤジギャグロボットだ。まずはカメラやウィンドウの位置を整えろ。"
        #フィラーを入れることで最初が再生されない問題を解決
        self.textToSpeech("えーっ、"+text)

        #カメラやウィンドウの調整
        previous = datetime.now()
        while ((datetime.now() - previous).total_seconds() < 15):
            ret, frame = self.cap.read()
            resized=cv2.resize(frame,self.SIZE)
            cv2.imshow("Captured",resized)
            cv2.waitKey(32)

        text="それではこれからオヤジギャグ地獄をはじめるぞ。"
        self.textToSpeech("えーっ、"+text)

    def __del__(self):
        self.cap.release()
        cv2.destroyAllWindows()

    #実際に行動する部分。eps greedy法で学習
    def doAction(self):
        if(np.random.random()<self.EPS):
            selectedArm=np.random.choice(self.arms)
        else:
            tmp=np.argmax([arm.getExpectation() for arm in self.arms])
            selectedArm=self.arms[tmp]

        text=selectedArm.getGag()

        winsound.PlaySound("se/question.wav", winsound.SND_FILENAME)
        self.textToSpeech("えーっ、"+text)

        reward = self.checkReward()
        selectedArm.setReward(reward)

        if(reward==0):
            winsound.PlaySound("se/incorrect.wav", winsound.SND_FILENAME)
            self.textToSpeech("あーっ、面白くないのか。")
        else:
            winsound.PlaySound("se/correct.wav", winsound.SND_FILENAME)
            self.textToSpeech("あーっ、面白いのか?")

    #テキストを渡すと音声合成してくれる関数
    def textToSpeech(self,text):
        msg=self.SSML_TEMPLATE.format(text)
        try:
            conn = httplib.HTTPSConnection('speech.platform.bing.com')
            conn.request("POST", "/synthesize",msg,self.headersTTS)
            response = conn.getresponse()
            if(response.status==400):
                print(response.msg)
                raise Exception()
            soundBin = response.read()
            winsound.PlaySound(soundBin, winsound.SND_MEMORY)
            conn.close()
        except Exception as e:
            print("[Errno {0}] {1}".format(e.errno, e.strerror))

    #相手が笑っているかどうかで報酬を決める関数
    def checkReward(self):
        #flush buffer
        previous = datetime.now()
        while ((datetime.now() - previous).total_seconds() < 3):
            ret, frame = self.cap.read()
            resized=cv2.resize(frame,self.SIZE)
            cv2.imshow("Captured",resized)
            cv2.waitKey(32)

        cv2.imencode(".jpg",resized)
        result, encimg = cv2.imencode('.jpg', resized)

        try:
            conn = httplib.HTTPSConnection('westus.api.cognitive.microsoft.com')
            conn.request("POST", "/emotion/v1.0/recognize", encimg,
                         self.headersEmotion)
            response = conn.getresponse()
            data = response.read()
            print(data)
            conn.close()
        except Exception as e:
            print("[Errno {0}] {1}".format(e.errno, e.strerror))
            return 0

        result=json.loads(data)

        if(len(result)>0):
            hs=result[0]["scores"]["happiness"]

            if(hs>0.3):
                self.windowImg[:HAPINESS_IMG.shape[0],self.ROBOT_IMG.shape[1]:(HAPINESS_IMG.shape[1] + self.ROBOT_IMG.shape[1]), :] = HAPINESS_IMG
                cv2.imshow("OYAJI GAG ROBOT", self.windowImg)
                cv2.waitKey(32)
            elif(hs>0.5):
                self.windowImg[:LAUGH_IMG.shape[0],self.ROBOT_IMG.shape[1]:(LAUGH_IMG.shape[1] + self.ROBOT_IMG.shape[1]), :] = LAUGH_IMG
                cv2.imshow("OYAJI GAG ROBOT", self.windowImg)
                cv2.waitKey(32)

            if(hs>0.3):
                return 1

        self.windowImg[:NEUTRAL_IMG.shape[0], self.ROBOT_IMG.shape[1]:(NEUTRAL_IMG.shape[1] + self.ROBOT_IMG.shape[1]),:] = NEUTRAL_IMG
        cv2.imshow("OYAJI GAG ROBOT", self.windowImg)
        cv2.waitKey(32)
        return 0

#メイン関数
if __name__ == "__main__":
    agent=Agent()
    previous=datetime.now()
    while((datetime.now()-previous).total_seconds()<2*60):
        agent.doAction()

    agent.textToSpeech("あーっ、もうおしまいか。")
    agent.textToSpeech("あーっ、面白かったか?面白くなかったか?どうでもいいわい。")
    agent.textToSpeech("あーっ、それではな")

動作結果

実際に上のコードを動かした結果が下の動画になります。

感想

これ非言語じゃなくて思いっきり言語じゃん!!!
とか思った人するどいです。
そうです。
ネタに詰まった結果、若干言語も使わせてもらいました。
ただ、たとえば、この文章を自動生成することによって、人間が笑えるようなネタを機械自身が考えることができるようになります。今回は実装がめんどくさかったので、文章生成はやめましたが、本来はそのようにしてインタラクションに応用することに向いていると思われます。

ロボット大喜利とか言うぐらいだったら、これぐらいはやってほしいですよね。
某社大手町の方々。

応用例

マルチアームバンディッド問題は広告配信システムやWebデザインの調査によく現れてきます。
例えば、「どの広告を配信すれば、Webサイトに訪れる人が最大となるのか」という問題はマルチアームバンディッド問題の具体例と言えるでしょう。

また、学術的には、強化学習というジャンルの基礎として取り扱われることも多いです。AlphaGoの中核にはDeep Q-leaningという機構の他にモンテカルロ木探索がありますが、このモンテカルロ木探索の中にマルチアームバンディッド問題が含まれています。

まとめ

今回はコミュニケーションの学習方法として、マルチアームバンディッド問題の解法を応用しました。

人々を幸福にすることを目的とした人工知能が普及したら、世の中はどうなるのでしょうか。

個人的に気になっている研究テーマの1つです。

参考文献

[0] 本多 淳也、中村 篤祥、バンディット問題の理論とアルゴリズム (機械学習プロフェッショナルシリーズ)、2016
[1] https://www.ai-gakkai.or.jp/my-bookmark_vol31-no5/
[2] http://ibisml.org/archive/ibis2014/ibis2014_bandit.pdf
[3] https://www.jstage.jst.go.jp/article/abjaba/86/0/86_109/_pdf