人工無脳の会話ボットとGoogle Cloud APIの接合

「pythonプログラミングパーフェクトマスター」のマルコフ連鎖の応答部分を、WikiPediaからの情報に基づいたものにするという、ほぼ、目的どうりの人工無脳会話ボットができた。
人工無脳は、ほぼお笑いのボケの一種となる。つっこめるのだ。
一方、お題についてのWikipedia情報は、人工知能的なもので、そのギャップが笑いになる。
これをGoogle Cloud APIのスピーチシステムに接合すれば目的のものが出来上がる。

辞書をMySqlに変更したdictionary.py (2017.02.14更新)

辞書をMySqlにしたばあいのdictionary.pyは次のようになった。ただし、テンプレート型までしかいれていない。python2用であり、python3でやると不都合が起きると思う。なお、テキストブックの方は、python3用になっていた。
####################################

# -*- coding: utf-8 -*-
import MySQLdb
from analyzer import *
import re
"""
様々な辞書を処理するクラス
金城俊哉著「Pythonプログラミングパーフェクトマスター」を参照した
2017年2月12日 作成
"""
class Dictionary:
    def __init__(self):
        print "Dictionary を初期化します"
        self.conn = MySQLdb.connect(
            user='washida',
            passwd='robotcomedian',
            host='localhost',
            db='dictionary',
            charset="utf8"
            )
        self.c = self.conn.cursor()
        # 辞書データを全て取得する
        # ランダムフレーズの取得
        sql = 'select * from random'
        self.c.execute(sql)
        self.random = [] #
        print 'ランダムフレーズの取得 >> ',
        count = 0
        for row in self.c.fetchall():
            #print 'id:', row[0], 'Phrase:', row[1], "\n"
            if (row[1] != ''):
                count += 1
                self.random.append(row[1])
        print "終了 総数 = ",str(count)
        # パターンフレーズの取得
        sql = 'select * from pattern'
        self.c.execute(sql)
        # 辞書型のインスタンスを用意
        # self.pattern = {}
        self.pattern = []
        print 'パターンフレーズの取得 >> ',
        count = 0
        for row in self.c.fetchall():
            # print 'id:', row[0], 'Pattern:', row[1], 'Phrase:', row[2], "\n"
            if row[1] != '' and row[2] != '':
                count += 1;
                #self.pattern.setdefault('pattern', []).append(row[1])
                #self.pattern.setdefault('phrases', []).append(row[2])
                self.pattern.append({'pattern':row[1],'phrases':row[2]})
        print "終了 総数 = ",str(count)
        # 終了時に、データの更新が必要なpatternオブジェクト
        self.update_pattern = []
        # 終了時に新規追加が必要なpatternオブジェクト
        self.newitem_pattern = []
        # テンプレートデータの取得
        print 'テンプレートフレーズの取得 >> ',
        self.template = {}
        sql = 'select max(nounnum) from template'
        self.c.execute(sql)
        #print "sql = ",sql
        row = self.c.fetchone()
        print '名詞最大数 = ', row[0],
        count = 0
        for i in range(int(row[0])):
            j = i+1
            sql = "select * from template where nounnum = '" + str(j) + "'"
            self.c.execute(sql)
            for row in self.c.fetchall():
                #print 'id:', row[0], 'Nounnum:', row[1], 'Template:', row[2], "\n"
                if row[1] != '' and row[2] != '':
                    if not row[1] in self.template:
                        self.template[row[1]] = []
                self.template[row[1]].append(row[2])
                count += 1;
        print "終了 総数 = ",str(count)
        # 終了時に、データの更新が必要なtemplateオブジェクト
        self.update_template = []
        # 終了時に新規追加が必要なtemplateオブジェクト
        self.newitem_template = []
        self.c.close()
        self.conn.close()
    def study(self, input, parts):
        """ study_random()とstudy_pattern()を呼ぶ
            @param input  ユーザーの発言
            @param parts  形態素解析結果
        """
        # インプット文字列末尾の改行は取り除いておく
        input = input.rstrip('\n')
        # インプット文字列と解析結果を引数に、パターン辞書の登録メソッドを呼ぶ
        self.study_pattern(input, parts)
        # テンプレート辞書の登録メソッドを呼ぶ
        self.study_template(parts)
    def study_pattern(self, input, parts):
        """ ユーザーの発言を学習する
            @param input  インプット文字列
            @param parts  形態素解析の結果(リスト)
        """
        print "学習を開始します"
        # 多重リストの要素を2つのパラメーターに取り出す
        for word, part in parts:
            # analyzerのkeyword_check()関数による名詞チェックが
            # Trueの場合
            if (keyword_check(part)):
                print "キーワードが存在しました!!"
                depend = False # パターンオブジェクトを保持する変数
                # patternリストのpattern辞書オブジェクトを反復処理
                for ptn_item in self.pattern:
                    # インプットされた名詞が既存のパターンとマッチしたら
                    # patternリストからマッチしたParseItemオブジェクトを取得
                    if(re.search(ptn_item['pattern'], word)):
                        depend = ptn_item
                        break   #マッチしたら止める
                # 既存パターンとマッチしたParseItemオブジェクトから
                # add_phraseを呼ぶ
                if depend:
                    print "キーワードが既存のパターンにマッチするものでした!"
                    #depend.add_phrase(input) # 引数はインプット文字列
                    if(re.search(depend['phrases'], input)):
                        # すでにフレーズも含まれている場合は、なにもしない
                        pass
                    else:
                        # 既存のパターンに合致するが、フレーズに含まれていないものについては、フレーズに加えるだけにする
                        depend['phrases'] += ('|'+input)
                        # 終了時に更新すべきオブジェクトに位置付ける
                        self.update_pattern.append(depend)
                else:
                    print "キーワードが既存のパターンにマッチしないものでした!"
                    # 既存パターンに存在しない名詞であれば
                    # 新規のParseItemオブジェクトを
                    # patternリストに追加
                    newitem = {'pattern':word, 'phrases':input}
                    self.pattern.append(newitem)
                    # 終了時に新規追加しなければならない
                    self.newitem_pattern.append(newitem)
    def study_template(self, parts):
        """ テンプレートを学習する
            @param parts  形態素解析の結果(リスト)
        """
        template = ''
        count = 0
        for word, part in parts:
            # 名詞であるかをチェック
            if (keyword_check(part)):
                word = '%noun%'
                count += 1
            template += word
        # self.templateのキーにcount(出現回数)が存在しなければ
        # countをキーにして空のリストを要素として追加
        if count > 0:
            count = str(count)
            if not count in self.template:
                self.template[count] = []
                self.newitem_template.append(count)
            # countキーのリストにテンプレート文字列を追加
            if not template in self.template[count]:
                self.template[count].append(template)
                self.update_template.append({'nounnum':count,'template':template})
    def closeDict(self):
        # データベースへの変更を保存、あれば
        self.conn = MySQLdb.connect(
            user='washida',
            passwd='robotcomedian',
            host='localhost',
            db='dictionary',
            charset="utf8"
            )
        self.c = self.conn.cursor()
        # 辞書データを更新する
        print "辞書データベースを更新します"
        for item in self.update_pattern:
            #
            sql = "update pattern set phrases = '" + item['phrases'] + "' where pattern = '" + item['pattern'] + "'"
            print "sql = ",sql
            self.c.execute(sql)
        for item in self.newitem_pattern:
            #
            sql = "insert into pattern (pattern, phrases) values ('" + item['pattern'] + "', '" + item['phrases'] + "')"
            print "sql = ",sql
            self.c.execute(sql)
        for item in self.update_template:
            # アップデートすべきtemplateだけがはいっているので、全部加える
            sql = "insert into template (nounnum, template) values ('" + item['nounnum'] + "', '" + item['template'] + "')"
            print "sql = ",sql
            self.c.execute(sql)
        for num in self.newitem_template:
            # 新規なので、全部加える 新規の番号の文字列だけが入っている
            for tmp in self.template[num]:
                sql = "insert into template (nounnum, template) values  ('" + num + "', '" + tmp + "')"
                print "sql = ",sql
                self.c.execute(sql)
        self.conn.commit()
        #データベースを閉じる
        self.c.close()
        self.conn.close()

あるPythonの本

「Python プログラミングパーフェクトマスター」という本を購入して、記載のプログラムを動かしている。辞書のところを、MySqlのデータベースに変換、プログラムを一部改定しながら動かし続け、形態素解析のところまできた。実に面白い本だ。結構たくさんのプログラミング関係の本を買ってきたが、こんなにドキドキする本には滅多に会わない。最後まで、きちんと追っていきたい。
ただ、だからと言ってこの本を勧めているわけではない。プログラミング本の場合、読者が求めているものと一致するかどうかが問題であって、合わないと、やたら問題だけが目につくというパターンがあるからだ。本を買う行為は常に自己責任だ。不安な本は買わなけれりゃいいし、買ってから本に八つ当たりするのは馬鹿げている。
次のような変更を加える。
(1)当面、感情の要素には興味ないので、全て削除する。感情はもっと違う入れ方があると思っている。
(2)dictionaryのpatternオブジェクトを、
self.pattern = {}
for row in c.fetchall():
if row[1] != '' and row[2] != '':
self.pattern.setdefault('pattern', []).append(row[1])
self.pattern.setdefault('phrases', []).append(row[2])
のような入れ方から
self.pattern = []
for row in c.fetchall():
if row[1] != '' and row[2] != '':
self.pattern.append({'pattern':row[1],'phrases':row[2]})
に変更する。
つまり、パターントフレーズのセットを辞書オブジェクトにして、pattern配列に次々に入れておくようにした。
これ以前は、patternは、それを入れた配列と、phraseを入れた配列を辞書オブジェクトにしていた。
いわば、縦にしていたものを横にしたという感じた。
(3)それにともなって、ParseItem系のクラスが全く必要なくなる。

久しぶりのJAVA

この間、でっかいTOPICファイルを二つばかり作り、そこにいろんなネタのデータを放り込んだんだけれど、大きなデータをTOPICファイルに入れるのはやや無理があると思い、それらをもう一度読み取って、MySqlのデータベースに入れようと思った。変換のためのJAVAプログラムを適当に作成しようと思って、簡単にできるはずなのだが、久しぶりにJAVAを使おうとすると、頭が戻りにくい。まあ、また、思い出し思い出しやるしかないな。

形態素解析と係り受け解析

どちらもGoogle Cloud APIでできるのだが、もうひとつ、いいものだという実感がないので、日本で開発されたMecabとCabochをインストールしてみた。以前、kuromojiも動かしたことがあるので(このサイトにも掲載してある)その辺りはある程度わかっているが、どれをどのように使うのかという迷いはある。kuromojiで形態素解析をして、その要素を使うだけで良いような気もするが、係り受け解析は必要か。
Google Cloud APIのいいところは、rootの単語を拾い出すことのような気がする。
しばらく迷う必要がある。ただ、あと2週間以内に、ロボットにちょっとしたことをさせたいと思っている。
以下は、CaboChaでの出力の画像である。

自然言語処理のGoogle Cloud APIを使ってみた

ロボットのキャッチした音声データをGoogle Cloud APIでテキスト化することの見通しがほぼたったので、それを自然言語処理のGoogle Cloud APIで、意味的理解の基礎付けをしようと思った。
https://github.com/GoogleCloudPlatform/python-docs-samples/tree/master/language/api
に記載されている内容をそのままなぞって、実現することができた。Speechの場合と違うことは、

$ pip install -r requirements.txt

を実行する上での内容の違いだけである。
また、unicode文字列のエスケープをデコードする必要があるが、
http://d.hatena.ne.jp/gepuro/20120317/1331991888
にある、pythonプログラムを使用すると簡単になる。
いよいよ、本丸に近づいてきた。

エコーロボットの枠組み

ロボットのマイクに話しかけ、その音声をPCを経由してGoogle Cloud APIに送り、帰ってきた言葉をリアルタイムでロボットに再生させるというエコーロボット、ほぼできた。Googleからの返送がストリーミングで、ほぼリアルタイムなので、違和感なくロボットはしゃべり返せる。さしあたって、ALTextToSpeechで喋らせている。
ただ、PC側の処理の中で、返された文字列を、ロボットに喋るぶぶだけに処理すると、返送されてくる言葉が壊れる状況が発生する。いまのところ、理由がわからない。これさえなければ、ほぼ、終わりなのだが。

Google Cloud Speech APIとNaoqiのストリーミング接合について

記憶から消える前に、どうやってつなげたかという基本的なメソッドだけを書いておく。pythonのソースをあげても良いのだが、著作権問題がよくわからないので、避けよう。
(1)Google APIの基本は以下のソースを利用する。これを(G)としよう。
https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/speech/grpc/transcribe_streaming.py
(2)ALAudioDevice側のストリーミング処理の基本ソースは、
http://www.baku-dreameater.net/archives/9331
を利用させていただいた。(R)とする
(3)基本は、(G)のpyaudioの部分をALAudioDeviceに置き換えることである。まず、main関数の冒頭に、(R)のロボットのIPとポート処理、qimessasingのオブジェクト生成部分を書き加える。ここは、それらを理解していれば簡単にできるだろう。それらに関連するライブラリをimportすることもおわすれなく。わすれたら、エラーになるのでその時直しても良い。その後、Google APIを利用する権限を持ったserviceオブジェクトの生成に入っている。
(4)record_audio関数の書き換え。
0、Queueからqueueオブジェクトを作る、ときに、chunkの制限を加えたが、これの必要性はチェックしていない。まあ、やったことはやったので、書いておく。次のように変更した。
buff = queue.Queue(chunk)
バッファが一杯になる値を加えたのだが。必要なのか?
1、audio_interfaceとaudio_streamのオブジェクト生成に関する部分は、pyaudioのものなので、削除する。したがってそのクローズ処理も不要である。
2、record_audioの引数の最後にappを加える。
3、削除した行の代わりに、次の3行を加える
player = EchoRobotHumanSpeech(app, buff, rate)
app.session.registerService("MyEchoRobotHumanSpeech", player)
alaudio = player.start("MyEchoRobotHumanSpeech")
EchoRobotHumanSpeechは、次に説明するALAudioDevice利用のためのクラスであり、MyEchoRobotHumanSpeechは、モジュール名でNaoqiがモジュールの識別子とするものである。
4、yield _audio_data_generator(buff)の直後に、
alaudio.robot_audio.unsubscribe("MyEchoRobotHumanSpeech")
を加える。モジュールの終了処理である。
(5)クラスEchoRobotHumanSpeechの定義。基本は、(R)のSoundDownloadPlayerクラスである。ただ、このクラスは、pyaudioでパソコンから出力するようになっているのだが、それは不要であるので削除する。代わりに、Google APIへのストリーミングアップロード処理を加える。
1、コンストラクタの引数に、buff, rateを加える。rateはグローバル変数のRATEを使っても良いが、プログラムの質的にはここに引数を与えた方が良い。
2、コンストラクタに、次の2行を加える。record_audioのなかでQueueから作られるbuffは全体として、決定的に重要な役割を果たしている。
self.buff = buff
self.rate = rate
3、startメソドには、次の2行だけを残して、全て削除する
self.robot_audio.setClientPreferences(serviceName, self.rate, 3, 0)
self.robot_audio.subscribe(serviceName)
最初のものはデバイスのプリファレンスで、3は、前方マイクから音を取得するもの。次のものは、ストリーミングの開始メソッド、その後、コールバックがprocessRemoteに設定され、音声データが次々にそのコルバックに送られる。
(6)コールバックの設定:ここで色々詰まった。一番わからなかったが、次のようにすれば良い。
1、オリジナルのコールバック、
def _fill_buffer(buff, in_data, frame_count, time_info, status_flags):
は不要なので削除する。
2、次の新しいコールバックを、クラスEchoRobotHumanSpeechの中に設定する。
def processRemote(self, nbOfChannels, nbOfSamplesByChannel, timeStamp, in_data):
self.buff.put(str(in_data))
#print("processRemote No." + str(self.count) + " / in_data = " + str(len(in_data))
#    + " サイズ = " + str(self.buff.qsize()))
return None
コメントを外して動かすと、コールバックがどのように呼び出されるか少しわかるだろう。
バッファにデータを文字列化して付け加えるというのも一つのポイントだが、これは、そうしないとエラーになるのですぐわかる。
(7)request_streamの日本語化などは、少し前の記事に詳細に書いたのでそれと同じである。
他は大きく変えなかったが、最終出力がラインフィードで同じ行を書き換える操作をしているので、何をしているかがわかりにくくなるから、常に改行してGoogleから送られてくるデータがどんなものかをわかるようにしたくらいだ。
できてしまえば簡単な感じなのだが、途中で分からなくて投げ出しそうになった。