機械学習を用いたプロダクトはどうやって改善するか

 こんにちは。Wantedlyでインターンしてる滝川です。WantedlyBotのロジックをメインで実装しています。WantedlyBotでは 機械学習を使用しているので、今回は機械学習を使用したプロダクトをWantedlyではどうやって改善しているか?について話そうと思います。

WantedlyBotとは

 WantedlyBotについて簡単に話すと、FacebookMessengerとSyncをプラットフォームにした募集紹介Chatbotです。ユーザに自由入力された質問を解析し、ユーザに適した募集を返します。また、国内でのFacebookボットの中では最も早くにリリースできたのではないかと思っています。 ( 日本経済新聞の記事, TechCrunchの記事, ASCII.jpの記事 )

機械学習はまだまだ「過度な期待」に位置

 冒頭でも述べた通り、WantedlyBotでは機械学習を使用しています。みんな大好き機械学習。テクノロジの成熟度を表すガートナーのハイプサイクルでは、「過度な期待」に位置していて、現実よりも期待値のほうが高くなっているような技術領域と言われています。ちなみに自然言語による質疑応答システムもこのあたりに入っています


 見て分かる通り、機械学習も自然言語処理も生産性の安定期とはほどほど遠いもの。周りを見てもGoogleやFacebookはともかく、実際に使用できてるプロダクトもまだ少ないのかな?という印象です。

機械学習をプロダクトに入れることの難しさ

 技術的にはだいぶ前からあるのになぜプロダクトレベルとなるとなぜ難しいのか?その原因の一つが“保守/改善の難しさ”な気がしています。機械学習は普通のプログラムと異なりコードを見ても結果がわかりません。ちょっと実装を変えて、意図しない結果が出たとしても、その根本的な原因がわからないのです。リリースする前なら大きな問題はないかもしれません。ガンガン失敗してもいいものがリリースできれば問題はないでしょう。しかし、リリースした後に変更して壊れて戻らない、といった事態は絶対に避けなければなりません。その一方で、どんなに慎重になり、どんなにコードを見直しても結果はわからないのです。絶対な保証が取れない恐怖。Wantedlybotでも、リリース直後「これ…壊れると怖いからこの改善はリリースしないでおこう」と言われてた時がありました。

実際に起きた問題

1. chitchatの対応



まず、Wanteldybotは"募集を探すChatbot"です。そのため、「テニスがしたい」「おふろはいりたい」「カレーが食べたい」と言われても募集を探しに行きます。あらかじめ言っておきますがこれは仕様です。募集を探しに行くbotなのですから募集を探しに行くのは当然です。当然なのです。しかしながらさすがに「こんにちは」「ありがとう」「いいね」と言われても募集を探しに行くのはどうかと思うわけです。そのため"挨拶"、"お礼"、"褒められた"と判断された場合は、募集を探しに行きません。

 ここで問題なのは“どうやって判断するのか?”です。改善しようとするとき、一番ここが怖かったです。いわゆる副作用への恐怖です。「下手に変えることで昔できたことができなくなるのではないか?」。例えば、「いいね」を"褒められている"と判定させる機構を付け加えた後、”何かいい募集ありますか?“って言われたときに"褒められた"と判断してしまうのではないか?といった恐怖です(これは実際起こりかけた例です)

 多分どのプロダクトでも危惧するところだと思います。でも普通のプロダクトだと小規模のものはそれほど恐れることでもない場合も多々あると思います。しかし機械学習を用いているとそんなことはありません。小規模だろうと何であろうと“中で何やっているのかわからない”のですから。これがいざ改善しようとして足踏みしてしまった原因です。

2. 否定語の対応


もう一つくらい例を紹介しようと思います。Wantedlybotは、「東京でエンジニアじゃない仕事」というクエリがあった時「エンジニアを求めてはいない」ことを認識し、エンジニア以外の仕事を探しに行きます。この辺りは単純なルールベースで行っているのですが、日本語というものは「ない」って単語が来たら否定していることが多いわけです。そのため「ない」という単語が来たら、その「ない」がかかっていそうな単語を見つけ、その単語は“否定されている”ことを認識します。この場合は「エンジニア」が否定されてているためエンジニアの仕事は探しません。 しかし「ない」という単語が使われていたとしても否定していない場合も珍しくありません。その一つが「エンジニアでも構わない」。全く逆の意味でむしろ肯定しています。このような場合は否定してはいけません。ルールベースなので“こういう場合はこうする”を書き足していけばいいのですが、複雑になればなるほどわかりづらくなり、前できてたことが今どうなるのかよくわからなくなります。

問題解決のために、やったこと

 大切なのは確実に進歩していることを確認すること、つまり“昔できたことができなくならないこと” = “副作用が起きないこと”です。Wantedlybotでは“副作用が起きないように”するために、“起きにくくする”ことと“起きてないことを確認する”ことの二つを行っています。 “副作用が起きにくくする”ためにモジュール化し、“副作用が起きてないことを確認する”ためにテストを行っています。

モジュール化

Wantedlybotはこんな感じで処理しています。


 現在は、図のブロックごとにモジュール化し、内部の処理には他のモジュールからは参照できないようにしています。こうすることの利点としては、

1. 一つのモジュールを変更しても他のモジュールには影響がない。

2. 改善する人も自分が担当するモジュールの内部と入力と出力さえ理解できれば改善することが可能になる。

3. モジュールごとで役割が明確になるため、何をしているのかがわかりやすくなる

4. モジュール単位で改善することで、おかしくなった時に原因がわかりやすくなる。

 リリース直後は(少し急いでいたこともあって)モジュールがキレイに分かれていませんでした。それでもなんとなくは分かれていましたし、(まだ小規模なので)コードを追って行けばどこで何をやっているのかはわかりました。しかし、やはり意識して分けようって思わないと、例えば同じ変数をあっちでもこっちでも使ったり、どこで何が行われているのか理解するのに時間がかかったり、一つを変えると全部変えないといけなかったりしました。

 モジュールを明確にし、内部の影響を隔離することでこれらの問題は解決しました。入出力を構造化したもの(クラス)で固定しているのと、モジュール単位の役割は明確なので、どこで何がやっているかはわかりやすくなります。また見るべきコードも限定されるので追いかけるのが容易になります。変えようって思った時、どこをどう変えればいいかすぐにわかるようになるのです。

 さらに、人間面白いもので、一つの変更による影響の範囲が限定されると、途端にやる気が湧いてくるものなのです(経験談)。壊れてもここのモジュールそっくり戻せば大丈夫っていう妙な安心感が生まれた気がします。またコードが見やすくなったため仕事の分担も容易になります。いいことしかありません。

 余談ですが、このモジュール構造はIBMのWatsonを参考にしています。正確に言うと2010年に出てるWatsonの論文を参考にしています。当然のように英語だらけでしんどいですが興味ある方は読んでみてもいいと思います[1]

テスト

 Wantedlybotでは回帰テストを使用しています。回帰テストってご存知でしょうか。簡単に言うとプログラムを変更した時に、その変更によって予想外の影響が現れていないかどうか確認するテストの事を指します。

 具体的な使い方としては、「過去に正しく実行された時の結果」を保存しておき、変更した後の「現在実行した結果」と比較して、変わりがないかを確認する作業を行っています。このテストをプロダクト内のあちこちで挟むことで“退化していない”確認を行っているのです。

 この回帰テストを導入することで、“褒められた”かを判定する機構を付け加えた後、「何かいい募集ありますか?」というクエリのテストを使用することで、“無事募集を探しに行った”ことを確認することができます。安心して”褒められた“かを判定する機構を付け加えられるようになるのです。同様に、「エンジニアじゃない」「エンジニアでも構わない」と言ったテストケースを用意するだけで安心して次の改善に進めるようになります。

出力の回帰テスト

 基本はoutputのチェックです。inputとしてユーザに入力されるであろうクエリを与え、実際に出力される結果をチェックします。WantedlyBotは文言と募集を返すため、両方テストします。「過去に正しく実行された時の結果」をxx.expected.jsonという名前で保存しておき、テスト実行時の「現在実行した結果」をxx.actual.jsonという名前で保存します。こうすることで,もし差分があった時にGithub上のdiffで簡単に差異を見つけることが可能となります。

モジュールの回帰テスト

 WantedlyBotでは出力だけでなく、途中のモジュールの出力ごとにも回帰テストを行っています。改善するとき、モジュールの内部は変えてもモジュールの入力/出力は形式を変えないためテストが可能になります。モジュールごとにテストをすることで“意図しない出力”があったとき、どこのモジュールでおかしくなったのかすぐにわかります。

mustとshould

 回帰テストの中には必ずしも完全一致しなくてもいい場合もあります。例えば「東京でエンジニアの仕事を探しています」と言われた時。募集の過去の結果(expected)には東京のrailsの仕事の募集しか載ってないとします。しかし構文解析の方法を変えた結果、テストを実行した時(actual)には東京のPHP関連の仕事が紛れ込んでいたとします。この場合完全一致ではありません。が、結局東京のエンジニアの仕事なので特に問題はありません。だがしかし「東京のエンジニア」と言われ「神奈川県」の募集を出すとちょっと問題なのでテストは行わなければなりません。

 テストは行う。しかし結果は完全一致でなくてもいい。こういうテストケースはshouldとし、”仮に不一致でもエラーとはしない”とします。エラーは出なくてもexpectedとactualのdiffを取ることはできるため、どこがどう違ったのかは簡単にわかります。

 逆に絶対に完全一致でないといけないテストケースはmustとして、ちょっとでも不一致だった場合はエラーを吐くようにします。例えば「こんにちは」と言われて、募集を探しに行くようなケースはあってはなりません。このような場合はmustとのテストケースとし、「こんにちは」と言われたら「こんにちは」ときちんと返すか?を完全一致のテストを行います。

 このように、同じテストでも場合分けをすることで柔軟に対応できる機構を整えておくことで開発スピードを上げることが可能になっているのです。

まとめ

 今回はWantedlyにおける機械学習を使ったプロダクトの改善方法について述べさせていただきました。 本当に一行でまとめるのであれば、“どんなに難しいことをやっても基本は変わらない”ということです。難しいことをやってると何もかも難しくみえてきそうですが、分割して(モジュール化)結果の確認(テスト)さえしっかりすれば保守としては成り立つのかなと思っています。機械学習であっても、信頼性の高いソフトウェア開発の基本であるモジュール化とテストの体制を整えることで、しっかり改善をしていける土壌を築くことが出来ます。

 全ての入力を自由入力の自然言語としているWantedlybot。まだまだ改善していくので、暖かく見守っていただけると幸いです。

Wantedlyボット - http://m.me/wantedly

参考文献

[1] Ferrucci, D., Brown, E., Chu-Carroll, J., Fan, J., Gondek, D., Kalyanpur, A. A., . . . Welty, C. (2010). Building watson: An overview of the deepqa project. In Ai magazine fall. : Association for the Advancement of Artificial Intelligenc

一緒に開発をしてみたい方へ

Wantedly, Inc.'s job postings
18 Likes
18 Likes

Weekly ranking

Show other rankings