系列ラベリングによる NPS コメントのポジティブ・ネガティブ部分の抽出

こんにちは。研究開発部の深澤(@fukkaa1225)と申します。

クックパッドでは、顧客のロイヤルティを測る指標であるNPS(ネットプロモータースコア)のアンケートを毎月実施しています。 このNPSアンケートで集まってきたユーザの声(フリーコメント)は、クックパッドにとって大変貴重なものです。しかし、毎月多くの声が届くこともあり、担当者だけで目を通して集計するというのは難しくなってきました。そこで昨年、予め定義したカテゴリにコメントを自動で分類するシステムを構築し、既に稼働させています。 NPSアンケートを自動分類した話 - クックパッド開発者ブログ

このシステムによって「いただいたコメントが何を話題にしているか」はある程度自動的に把握できるようになりました。次に課題となったのは、例えば「このコメントはレシピの多さに関するものである。でもその中にはポジティブな部分とネガティブな部分が混じっている。これを分離できないか?」というものでした。

これはもちろん、人間であればコメントを見て容易に把握し、抽出できるでしょう。では、それを自動で行えるようにしたいとき、みなさんはどのような手段でこれを実現させるでしょうか。ルールベースだけでこうした抽出問題を解くのは骨が折れそうです。ここは機械学習の力を借りることにします。

本稿では、「このNPSコメントのどの部分がポジティブな記述で、どの部分がネガティブな記述なのか」を抽出するシステムの、機械学習モデルの実験について紹介します。

まとめ

  • あるコメントからポジティブ・ネガティブ部分を抽出する今回のタスクを、系列ラベリングと捉えて学習に必要なデータを作成。
  • CRF++、Bidirectional-LSTM、BERTをベースとしたモデルで実験。
  • sudachiで分かち書きし、学習済みword2vecにchiVeを用いたBidirectional-LSTMのモデルが最も高いF1値を記録した。しかし、CRF++と大きな差は見られなかった。
  • 引き続きエラー分析を行って、NPSコメントを業務改善に活かしていけるようなシステムの開発に努めていきます。

学習データを作ろう

さて、機械学習で取り掛かるぞということで、さっそく学習データを作っていきます。どんなデータがあればよいのかを考えてみます。

今回実現したい機能は

クックパッドはたくさんレシピがあってありがたいが、ありすぎて選びきれない時もある というコメントに対して、

  • Positive: たくさんレシピがあってありがたい
  • Negative: ありすぎて選びきれない時もある

といったように、ポジティブ・ネガティブに紐づく表現を抜き出すことです。

このようなタスクは Sequence Labeling(系列ラベリング)Token Classification など色々な呼び方ができると思います。各形態素ごとに「この形態素は{ポジティブ・ネガティブ}な箇所の{始点・中間・終点}なのか」を分類する問題として捉えられるでしょう。

ということで、アノテーションは以下のようなデータを作ってもらうことにします。

クックパッドは#pたくさんレシピがあってありがたい#pが、#nありすぎて選びきれない時もある#n

ポジティブな箇所は #p 、 ネガティブな箇所は #n で囲んでもらうようにします。アノテーションの体制は、2人のアノテータの方にそれぞれタグ付けをしていただいた上で、別の1人のアノテータの方にそれらの結果を統合してもらいます。これで統一性を確保するようにします。

実際に学習する際はこのタグ付けしてもらったデータをパースして、形態素ごとにBIOESラベル(e.g., BはBegin、IはInside、EはEnd、SはSingle、OはOther)を付与していきます。

B-positive たくさん
I-Positive レシピ
I-Positive が
I-Positive あって
E-Positive ありがたい

このようなルールのもとで、データを作っていってもらいました。最初に4,000、その後しばらく解析を進めながらもう一度ガッとタグ付けしていただいて、最終的に得られたデータ数は10,000コメント程度となりました。

どんなモデルでやるか

では、こうして得られたデータを使って抽出モデルを作成していきます。 まずはじめに考えるのはCRFですね。言わずと知れた系列ラベリングが得意なモデルです。これはCRF++をつかって容易にモデリングできます。

次に考えるのが、やはりディープラーニングを使う手法です。計算コストは当然CRFよりも高くなりますが、今回のような自然言語を扱うタスクにおいては十分な精度を出すことが期待されます。今回のタスクにおいては以下のようなアーキテクチャのモデルをベースとします。このモデルは[Lample+, 2016]で提案されたもので、インターンの学生の方が実装してくださいました。 単語をベクトルに変換するEmbedding層と文字列をBidirectional-LSTMでencodeする層を用意して、それらの出力値をconcatし、Bidirectional-LSTMに通すような構造です。 単語ベースの方は学習済みword2vecの重みを使います。

f:id:fufufukakaka:20200515101407p:plain
今回ベースとしたモデル[Lample+, 2016]

このモデルをベースとして、

  • 文字列のencoderをCNNにする
  • 学習済みword2vecで以下のものを試す
    • wikipediaコーパスで学習したもの
    • クックパッド手順コーパス(クックパッドに掲載されているレシピの手順を抜き出したもの)で学習したもの
    • ワークスアプリケーションズ徳島人工知能NLP研究所が公開している国語研日本語ウェブコーパスで学習したもの(chiVe)
  • Bidirectional-LSTMをtransformerに置き換える
  • tokenizerをsudachiにし、形態素を正規化する

といったモデルを試していきます。

また、これに加えてやはり外せないだろうということでBERTも実験対象に加えます。 ベースのモデルはhuggingfaceに東北大の乾・鈴木研究室が提供している bert-base-japanese-whole-word-masking を利用します。

バリエーションとしては以下の2つです。

  • BERT論文にならって、BERTから得られるtokenごとの出力値をそのまま使いfine-tuningする
  • 最終層にCRFを入れてfine-tuningを行う(BERT論文ではCRFは入っていなかった)

これは個人的な経験なのですが、自分が担当したタスクでBERTを用いて勝てたことがなかなかなく、今回も祈るような気持ちでBERTにトライしました。

まとめると以下の3パターンのモデルで実験を行います。

  • CRF++
  • 文字列encode+単語encode{by 学習済みword2vec} → Bidirectional-LSTM
    • 学習済みword2vecやtokenizerで何を選ぶか、LSTM層をtransformerにするか否かなどのバリエーションあり
  • BERT
    • 最終層にCRFをつけるかどうか

実験の管理

さてここで少し本筋から外れますが、僕がどのようにこれらの実験を管理していたかについて述べたいと思います。

僕は実験のパラメータをyamlで管理するのが好きです。いつもだいたい以下のようなyamlを用意しています。

{実験名}:
  char_encode: LSTM
  transformer_encode: False
  char_lstm_layer: 1
  lstm_layer: 1
  char_embedding_dim: 50
  lstm_char_dim: 25
  word_embedding_dim: 300
  lstm_dim: 100
  crf_drop_out: 0.5
  lstm_drop_out: 0.0
  tokenizer: sudachi
  normalized_token: True
  embed_path: /work/cache/chive-1.1-mc5-20200318.txt

python src/run_experiment.py --exp_name={実験名}

そしてこれを実験名を引数に入れたらyaml内の変数を展開してくれるwrapperを経由して、実験コードを流すようにしていました。出力結果も{実験名}_{日時}.log のような名前にすることで、同じ実験名での結果だということがわかりやすくなるようにしています。

yamlで設定を記述することで、パラメータの調整をする際に実験コードそのものに手を入れる必要がなくなります。設定ファイルだけで済むのはとても気楽で、そういった理由からここ数年は個人的にこのスタイルでやっています。

見通しも、ひたすらargparseclickの引数に渡しつづけるよりも良くなっているような気がします。モデルのtokenizerなのか単なるパラメータなど含めて書こうと思えば階層的に書ける点も好きです。

最近だとfacebookが出しているHydraなどもあり(今回は使っていませんでした)、yamlでパラメータ管理するのがどんどん楽になっており、ありがたいですね。

実験結果と考察

以上のような過程を踏みつつ、実験を行いました。得られた結果の中から主要なものを以下に表で示したいと思います(いずれのF1値もmicro-average)。

Name all_f1 negative_f1 positive_f1
Bidirectional-LSTMによる文字列encode・単語embed:chiVe→Stacked-Bidirectional-LSTM(tokenizer: sudachi) 0.609 0.4495 0.6717
CRF++ 0.6024 0.4839 0.6438
CNNによる文字列encode・単語embed:クックパッド手順コーパス→Stacked-Bidirectional-LSTM(tokenizer: mecab) 0.5607 0.3977 0.6181
Bidirectional-LSTMによる文字列encode・単語embed:クックパッド手順コーパス→transformer(tokenizer: mecab) 0.5129 0.3439 0.5695
Bidirectional-LSTMによる文字列encode・単語embed:クックパッド手順コーパス→Stacked-Bidirectional-LSTM(tokenizer: mecab) 0.5066 0.3102 0.5751
Bidirectional-LSTMによる文字列encode・単語embed:wikipedia→Stacked-Bidirectional-LSTM(tokenizer: mecab) 0.4898 0.351 0.5308
BERT-with-CRF 0.419 0.248 0.5
BERT 0.3843 0.2074 0.4734

chiVeを用いて最終層をStacked Bidirectional-LSTMにしたモデルが最も高いF1値を記録しました。しかしCRF++が想定以上によい結果を出しており、両者の差はほとんどないという結果になっています。

両者にあまり大きな差がないことから、いくつかの可能性が考えられます。今回採用したニューラルネットのモデルがBidirectional-LSTMを多用する計算コストの高いものであることから、恐らくデータ数が十分でなかった可能性が高いと現在は考えています。

BERTに関しては、なにかミスがあったのかなというくらいに低い結果となってしまいました。前述したようにBERT単体では相性が悪いのかもしれません。BERTにCRF層を加えたものでF1値の増加は確認できるので、全く機能していないというわけではないと思われますが、なにか根本的な改善が求められているということに変わりはなさそうです。引き続きBERTの勝利を願ってエラー分析をしていきたいと思っております。

ポジティブなコメント、ネガティブなコメント、それぞれのF1値に目を向けてみるとポジティブなコメントの抽出精度はどの手法でもネガティブなコメント抽出の精度よりも高くなっています。これは学習データにおけるラベルの不均衡に要因があると考えています。データの中でポジティブとしてタグ付けがされたのが7,218箇所あったのに対し、ネガティブとしてタグ付けが行われたものは2,346箇所と大きく差が開いていました。データ数が十分でなくネガティブに関するモデルの学習がうまく進まなかったことが考えられます。

最後にCRF++とchiVeを用いたStacked Bidirectional-LSTMの二者に絞ってエラーだった予測結果をいくつか見てみたいと思います。 基本的に短い文章でポジティブかネガティブのどちらかだけ出現するときはよく正解します。対照的に、長い文章・ポジティブとネガティブの両方出現するときに間違っていることが散見されました。

f:id:fufufukakaka:20200515101701p:plain
前半のポジティブな記述を取れていない例

こちらの例ではどちらも前半の「いいと思う」が取れていませんが、後半は捉えられています。

f:id:fufufukakaka:20200515101750p:plain
Stacked Bidirectional-LSTMが前半の記述を取れていない例

こちらは、CRF++のみが前半の「料理初心者だったためとても重宝している」がとれています(ただし、短めにとっています)。

f:id:fufufukakaka:20200515101811p:plain
CRF++が範囲を短めに取っている例

上の例と同じミスとして、CRF++が短めに範囲を捉えているケースがいくつかありました。

出来上がったシステムの全体像

さて、こうして作成された抽出モデルによって、NPSを解析するシステム全体は現在以下のような状態になっています。

f:id:fufufukakaka:20200515101828p:plain
NPSを解析するシステムの簡略図

毎月のNPS実施に合わせてコメント抽出・カテゴリ分類バッチが起動します。それらコメントはカテゴリごとに関連するslackチャンネルに通知されます。また解析結果は、NPSに関する数値を統合的に取り扱うために開発されているダッシュボードに取り込まれ、視覚的に分かりやすい形で残るようになっています。

今後について

NPSに対する解析は、ユーザの方々からの貴重なご意見を業務に役立てていく上で非常に重要なことであると感じています。より正確に、そして迅速に意見を取り込んでいけるように、引き続き自動解析システムの発展に努めていく所存です。

インフラにかかるコストを正しく「説明」するための取り組み

技術部 SRE グループの mozamimy です。

クックパッドでは、 SRE が中心となって、サービスを動かす基盤の大部分である AWS のコスト最適化を組織的に取り組んでいます。

昨年夏に公開した記事である、インフラのコスト最適化の重要性と RI (リザーブドインスタンス) の維持管理におけるクックパッドでの取り組みでは、

  • なぜインフラのコスト最適化が必要なのか、具体的にどのような考え方に沿って進めてゆけばよいのか。
  • SRE が一括して管理する AWS のリソースプールそのもののコスト最適化を実践するための具体的な取り組みの一例として、RI のモニタリングや異常時の対応フローによる維持管理。

といった話題にフォーカスしました。

今回は、インフラにかかるコストを正しく「説明」するための取り組みということで、コスト最適化に貢献する社内アプリケーションである Costco (Cost Console の略です) と、その設計思想や目指すところについて解説します。多分に社内コンテキストを含むツールなので OSS とはしていませんが、読者の皆さんの組織で同様の仕組みを構成するときの役に立つことでしょう。

今回ご紹介するトピックは、ある程度のパブリッククラウド (特に AWS) の知識があれば前回の記事の予備知識がなくとも読める内容となっています。しかしながら、読者の皆さんの組織に応用することを考えると、その背景を知っておくと理解がより深まると思いますので、前回の記事の、特に前半部分を読んだ上で今回の記事を読むことをおすすめします。

以降、単にコストと表記した場合は金銭コストのことを指すこととします。

あなたのサービスのインフラコスト、妥当な金額ですか?

いきなりですが、真か偽で答えることのできる、一つの問について考えてみましょう。

「あなたが運用しているサービスにかかっているインフラのコストは妥当な金額ですか?」

この問について、それが合っているか否かはさておき、確信を持って答えられる人は少ないのではないでしょうか。もし自信を持って答えられるのであれば、あなたの組織は高いレベルでコストを管理することができているでしょう。

では、なぜこの問に対して自信を持って答えることができないのでしょうか。理由は簡単で、インフラにかかっているコストの状況を継続的に把握できていないからです。逆に言えば、かかっているコストの妥当性を評価する仕組みを用意し、定期的にふりかえる場を持つことで、この問に答えるための根拠となるのです。

続きを読む

Ruby3 さみっと online 開催報告

Ruby インタプリタの開発をしている技術部の笹田です。以前から自主的にリモートワーク状態だったので、あまり仕事環境は変わっていません。が、子供の保育園の登園を自粛しているため、色々大変です(主に育休中の妻が)。日常がはやく戻ってくれることを祈るばかりです。

さて、去る 4/17 (金) に、Ruby3 さみっと online というウェビナーイベント(オンラインイベント)を開催しました(Ruby3 さみっと online - connpass)。今年の12月にリリースされると言われている Ruby 3 に関するトピックに絞った発表会です。本稿では、このイベントについてご報告します。

RubyKaigi 2020 が、4月から9月に延期されたので、Ruby 3 開発のマイルストーンがちょっと宙ぶらりんになってしまいました。 そこで、一つお披露目する機会を作ろうと企画したのがこのイベントです。

イベントによって、Ruby 3 開発者に締め切り効果をもたらす、それから Ruby 3 に関する進捗を他の方にも聞いて貰い、ご意見を募る、というのを狙っています。 総じて Ruby 3 開発のためのイベントですね。もちろん、興味ある方が楽しんで下されば、それにこしたことはありません。

平日にもかかわらず、多くの方にご参加頂きまして、ありがとうございました。zoom のログによれば、250人以上の方にご参加頂いたようです。

なお、このイベントはクックパッドが開催した、というわけでもないのですが、企画運営がクックパッドの開発者であること、zoom アカウントの提供がクックパッドだったこと、それから他に適当な場所も知らないので、ここでご報告します。

発表

プログラムは次のような感じでした。

  • 09:00-09:30 Opening / Ruby 3 by Matz (zoom 練習時間)
  • 09:30-10:30 Fiber (Samuel)
  • 10:30-11:30 JIT (k0kubun)
  • 11:30-12:30 Guild → Ractor (ko1)
  • 12:30-13:30 Lunch break
  • 13:30-15:00 Ruby 3 type activities (mame, soutaro)
  • 15:00-15:15 Roadmap for RubyGems 4 and Bundler 3 (hsbt)
  • 15:15-15:30 Proposal of Proc#using (shugo)
  • 15:30-15:45 Real Terminal Testing Framework (aycabta)
  • 15:45-15:50 Windows and UTF-8 (usa)
  • 15:50- Ruby3 Q&A

だいたいオンタイムで進みました。資料は https://hackmd.io/@ko1/ruby3samitto に(あるものは)あります。

Ruby 3 のメインゴールは JIT compile、Concurrency それから静的解析です。それらの大きな話に1時間ずつ(静的解析は二人で1.5時間)と大雑把に割り当てました。質疑応答も十分行えたのではないかと思います。また、その他の話題として、4人の方に「こんなことします」みたいな話をして頂きました。最後に Q&A タイムは、雑多な話題をのんびりと続けて、いつになくグダグダな時間になりました。

内容の詳細は、発表資料を見て下さい。

ランチブレイク中なども、zoom での中継は続けており、発表権限がある人たちで雑談していました。

アンケート結果

開催中に Google form でアンケートを作って、最後に参加者の方に伺いました。58名の方から回答を頂きました。

f:id:koichi-sasada:20200428180034p:plain
「大雑把にどうでした?」という質問への結果

今回のイベントについて「大雑把にどうでした?」という質問については、ごらんの通り、好評だったことがわかります(もちろん、好評だった人しか回答していなかったという可能性はあります)。少なくとも、60人弱の人達が楽しんで頂けたのしたら良かったです。

良かった発表については、一番好評だったのが「Ruby 3 Q&A」という結果でした。好評なら良かったんですが、グダグダ過ぎなかったかな、あれ。

感想では、次のような意見を頂きました(一部抜粋)。

良かった点:

  • リアルイベントと違ってゆるく参加できるのは良かった
  • お金かからない。単一セッションなので全部見れるのが良かった。
  • 家にいながら参加できるの大変助かります(配信などいつもありがとうございます)
  • オンラインでしたが物理イベントよりもホスト側との距離が近いと勝手に感じました。
  • リアルタイムに参加者の方が質問したりして、ライブ感があってよかったです。
  • アンケートなど交えていた点
  • Slack上でコミュニケーションを取りながらや、アンケートをフィードバックしながら発表を聞けるのはとても良かったと感じました
  • Rubyコミッターが普段どのように議論しているのかが感じられて良かったです。
  • RubyKaigi な感じがとても良かった
  • ゆるい進め方が良かったです
  • こういった会がある事自体がいいですね。Zoomとかでみんなの顔や声があって繋がるの、ここ最近の閉塞感を和らげるのにとても良かったとおもいます。
  • 緊急事態宣言の状況に対して、家にいながら Ruby 3 について聞けて良かったです。
  • このイベントを開催してくださったこと自体がとてもよかったことですし、内容もすごくよかったです
  • Samuelのライブコーディング見れたのがとても良かった!

改善案:

  • 視聴者からのリアクションが見えるようになると良さそう
  • 身内ノリが多いのはRubyコミュニティならではかなあと思ったけどとにかく身内ノリは多かった
  • 休憩中なら分かりやすく「休憩中(Rubyistの雑談の時間)」って書いてあると嬉しい
  • 休憩時間をこまめにほしかった。
  • 仕事をしながらの参加だと理解が追いつかないので、録画があると嬉しいです
  • 朝早すぎて起きれなかった
  • 情報量の多い資料は、今どの部分について話しているのかがわかりづらかったのでマウスポインタなどを活用して欲しい。

省力開催のウェビナー

我々が設定した目的は Ruby 3 開発を促進することなので、凝ればいくらでも時間がつぎ込めるイベント運営は極力省力化を目指しました。その決意の表れとして、「さみっと」という気の抜けた名前にしています(サミットとか Summit だと、なんか真面目にやらないといけない感じがしません?)。

ウェビナーという形式は初めてだったので、ちょっと運営に関するメモを残しておきます。

開催準備

  • 我々にとって手慣れたツールである zoom のウェビナーを利用する
    • ウェビナーの利用は初めてだったので、前日にリハーサルをしました。
    • セキュリティの懸念点から、zoom だと参加できない人もいるという声も聞きましたが、それはしょうがないとしました。
  • 運営ミーティングは1回だけ(1時間くらい)
    • 遠藤さんと1時間くらいでさっと決めました。
  • スケジュールはてきとーに決める
    • Ruby3 に関する3目標に関する人達の予定を抑えてスケジュール決定。
    • matz が平日のほうが都合が良いってことだったので、平日で。
    • あとは可能な人・希望する人だけ発表してもらう。
  • タイムテーブルはゆるく作る
    • もちろんシングルセッション。
    • 時間に余裕を持たせて、あとから発表希望者をプログラムに追加。
  • 募集などは Connpass のページ(https://rhc.connpass.com/event/169873/)だけ(発表者募集は ruby-dev ML を利用)
  • 日本語を公式コンテンツとする
    • 情報発信は日本語のみで行いました。
    • 英語話者(Samuel)が居ましたが、日本語しかないことを了承して頂きました。
    • 資料作成がやっぱ日本語だけだと本当に楽ですね...。
  • コンテンツ管理が面倒なレコーディングはしない

運営については、労力と時間はほとんどかけずに済ますことができました。 もうちょっと宣伝やっても良かったかも?

期間中の運営

zoom のウェビナーは、一般視聴者と、発言ができるパネリストの2つに分かれています。発表者がパネリストになるのは問題ないのですが、その他に誰がパネリストになるかは検討する必要があります。当日は、なんかしゃべりたそうな人を見つけたら、片っ端からパネリストにする、という運用を行いました。

一応、zoom のチャットや slack や twitter などを見て、発表者にフィードバックすることがあれば、気づいたパネリストが発表者にフィードバックする、というような感じで行いました。

Zoom では、参加者にリアルタイムアンケートをとる機能があるのですが、ウェビナーを立ち上げたホスト(笹田)のみが作成できるというものだったらしく、私が思いついた質問を参加者の方に投げかけるということを何度か行いました。ただ、一人の人間だけでやっていたので、質問が広げられなかった感じはします。

zoom のログを見ると、最大で160人が同時接続し、250人ほどが期間中に接続したようです(名寄せをちゃんとやっていないので、同じ人が複数デバイスで接続している場合があります)。

f:id:koichi-sasada:20200429033500p:plain
接続数の推移

お昼の接続数が最大だったんですが、やっぱり昼休みは見やすかったんですかね。

f:id:koichi-sasada:20200429033547p:plain
ユーザごとの滞在時間を昇順にソート

ずっと見て下さっていた方もいれば、ちょっと覗いてみた、という方も居そうです。

最後の発表者4人のうち、3人が 4pm から用事があるということを結構直前に知ったので、ちょっと順番を入れ替えました。そのあたり、少し事前に聞いておいても良かったかも知れません(が、そういう忙しい人でも、一部だけでも参加してくれるのは、ウェビナーの良い点ですね)。

たくさんのご参加、ありがとうございました。

おわりに

Ruby3 さみっと online というウェビナーイベントの開催についてレポートしました。

おかげさまで、省力開催にもかかわらず、ウェビナー開催は初めての経験でしたが、大きな失敗もなく開催することができました。発表者の皆様、ご参加頂いた皆様、それから運営を手伝って下さった皆様に、改めて感謝いたします。

また、こういう機会を作って Ruby 3 マイルストーンを用意して、開発を促進していければと思います。完成するといいなぁ。