pKeicho CUIで作ったチャットボットをMattermostに接続したものにWeb UIをつけたい ごちゃごちゃしてて混乱するので書きながら進める
- 既存のリポジトリが色々なしがらみが絡まってごちゃごちゃになってるので、一旦新しいリポジトリに切り離そう
$ git init keicho-server
- Herokuで自然言語処理を見ながら作業
$ heroku create keicho
- Herokuに置くのはserverなの自明だし
- サーバ上で最小限のコードが動くようになった
- ここからが面倒
- 過去のコードの理解と整理をやる
- CUI時代には情報をオンメモリに持っていた
- MattermostとOutgoing Webhooksで繋ぐ
- そのためにサーバに持っていこうとした段階で情報をFirebaseに入れるようになった
- serverからは
chat.connect_slack(username, text)
を呼ぶ- 当時はパッケージにしてなかったが、今この機会にやるべきか
- environment.pyがEnvオブジェクトの定義とFirebaseでの永続化を一手に引き受けている
- あー、ここでアクセス権のためのクレデンシャルをリポジトリに入れると公開しにくくなるのだな…
- で、「秘密部分だけリポジトリわけたとして、どうやってHeroku内でくっつけるのか、そのくっつけるための情報がまた秘密情報になるのでは」と思って全体非公開にしたんだな
- デフォルト非公開のまま、公開できる部分だけを公開リポジトリに切り出せば良い
- ここまでのインポートはうまく行った
- Firebase接続のテスト用のURLを作って叩いてみる
- environment.pyから state.pyを呼んでいる
- INITIAL_STATEを参照
- あんまりここを密結合にしたくないでstate_constant.py / state_action_map.py に分けた
- 永続化周りは動くようになった
- Firebase接続のテスト用のURLを作って叩いてみる
- 次 python
def connect_slack(name, text):
"Top-level interface for chat server"
env = environment.load(name)
env.log.append([1, text])
if text[0] in "!(>#~@": # (1)
environment.save(name, env)
return ""
process_command(env, text) # (2)
res = make_response(env) # (3)
if res:
env.log.append([0, res])
environment.save(name, env)
return res
- (1)が何のために必要なのかわからない…
- process_command
- まず重要なのは「よくない質問」の時のフィードバック
- NGKWならキーワードの選択自体が悪い
- NGはキーワード自体は悪くないが質問との組み合わせが悪くて回答できない
- 人間がこの二つをちゃんと区別して使い分けれるかは微妙だと思うので後者でも2〜3回使ったらキーワードが悪いと判断した方が良いかも
- リセットコマンドが送られた時に環境のリセットを行なっている
- これはチャットの場合「セッション」などがないのでリセットのタイミングがないから
- 「話そう」などで通常フローが開始される
- これはHerokuがスリープしてるのを起こす目的もある
- Web UIの場合、ページにアクセスした時点で入力可能であるべきで、バックグラウンドで起こすべき
- 「まず聞いて」の相槌モードと「おしまい」による通常モードへの復帰
- 「別にない」みたいな発言でスコアを落とす実験的コード
- NGコマンドの処理などと一緒にすべき感
- いったんコメントアウト、後で検討
- 後で読むための読みやすい形でのログの出力をしているが、CUI時代のコードなのでローカルファイルに書いている
- Slack時代にログ表示機能をつけた気がするのでなくて良さそう
- 環境と回答長さのデータの保存
- これは「回答がたくさん帰ってくる質問が良い質問」という発想
- envの更新
- 後述
- 質問キーワード対のデータの更新
- 学習用のデータをローカルファイルに吐いていた、Slack時代にコメントアウトしてた
-
うーん、なるほど
- process_command が、その曖昧ななんでも入りそうな関数名のせいで肥大してて、さらにそれがupdate_envなんていうなんでも入りそうな関数を呼んでる
- 逆に make_responseは2行(しかも片方assert)
- これは分割境界が適切でないな、建て増しを繰り返して迷宮化してる
- いったんくっつけてから適切に分割しよう
- update_envの中でさらにstate_transitionを呼んでる
- ここは状態遷移を主にやってる
- 状態遷移をログに書き出してるけど、やっぱりローカルファイルシステムのみ
- そもそもオンメモリの情報をFirebaseで永続化する形に書き換えたタイミングで、ファイルに書き出してるログも別途Firebaseに置き場所を作るべきだったのでは?(後で検討)
- 整理
- process_command の、本当に文字列をコマンドとして処理する部分はreturnで脱出したいので関数に分かれているのが自然
- state_transitionも同様
- update_env_with_inputの中でキーフレーズ抽出をしてたの、奥まりすぎ
-
キーフレーズ抽出
- 今はいったん過去のコードのままにするけど、近々更新予定なので、切り出しておく
- キーフレーズ抽出は他のプロジェクトでも使うのでパッケージにしておこう
-
NGKWでのキーフレーズの削除
- フレーズ指定ありとなしの両方が実装されてるけど、指定して使うことはない
- ここ、単純に記憶から削除してるけど、それだと「削除された」という情報が残らない
- 再度追加される可能性がある
- セッション中はブラックリストに入れるなどして追加されないようにし、長期的にはキーフレーズ抽出の部分でブラックリストで弾くべき(後で検討)
-
Mattermostに繋いだことで使いやすくなった反面、ログを諸々無効化したことで改善がしにくくなったのだなぁ、反省
-
次はアクション周り
- ステートごとにアクションがあり、envを引数にして実行される、という設計
state_action_map[env.state](env)
- しかし実際のところ少数の例外を除いて「引数で受け取った質問のリストから選んで質問する」というアクション
- 選ぶところはランダムかと思ったが違った
- すべてのキーフレーズ×質問についてスコアを計算
- 既出のものや、NGリストに入ってるものは除外
- このNGリストはどこから来るんだっけ…
- envの永続化対象には入ってるが、NGリストのファイル書き出しを止めた際にリストへの追加も止まってるから常に空だな
- 基本2質問に下駄を履かせてる
- 直前にした質問に使ったキーワードに正の補正あり
- ここで自然な質問かどうかの調整が入ってるが、有効に機能してるかが未検証
- 未検証だと思うけど検証したんだっけ?という気持ち
- 一旦外して後で検討
- 既出のものや、NGリストに入ってるものは除外
- そしてスコア最大のものを選択
- 使用済みに入れる
- 対称形の質問に対してはキーワードを交換したものに関しても使用済みに入れる
- すべてのキーフレーズ×質問についてスコアを計算
- 選ぶところはランダムかと思ったが違った
- ステートごとにアクションがあり、envを引数にして実行される、という設計
-
質問
- 質問の種類ごとに「1つキーフレーズを選ぶ」「2個選ぶ」などが実装されている
- スコアに応じたサンプリングもある
- あれ?さっき全部のキーフレーズ質問対についてスコアを計算して最大のものを選んだんじゃなかったっけ?
- そうだ、これ呼ばれてない
- 例外であるChainQuestionは「直前のキーワードを使い続ける」なのだが…
- これちゃんとテストされてない気がするな
- たぶんたまたま「前回最高スコアだったキーワードは、次も最高である確率が高い」ってだけで、逆転しうる
- 「直前のキーワードを使い続ける質問」ってフラグを立てて、スコア計算の時に特別扱いするべき
- これちゃんとテストされてない気がするな
- 使われてないコードを取り除く
-
ローカルのサーバで動いた
- Herokuでも動いた
「後で検討」
- ランダムな扱いをしている部分、シードを固定する方法を用意して回帰テストできるようにしてはどうか
- そもそもランダム性は除去済み
- 細かい粒度のテストがないのは良くない
- A: ファイルに書き出してるログも別途Firebaseに置き場所を作るべき
- Yes
- どのようなログを取るべきか
- 現状「人間が読めるログ」自体もリセットされる領域に置かれてるがこれはおかしい
- 最低限、人間が読むためのログが保存されるべき
- 加えて、ログを見て「イマイチな返答」が起きてた時に、その時の内部データを後から確認できるといい
- これは気をつけないと容量が大きくなりそう…
- B: 長期的にはキーフレーズ抽出の部分でブラックリストで弾くべき
- これを記録しておく場所が今はない
- まずAする
- C: 「別にない」みたいな発言でスコアを落とす実験的コード
- NGコマンドの処理などと一緒にすべき感
- Bしてからまとめる