Encoder-Decoder でレシピの材料名を正規化する

研究開発部の原島です。部のマネージメントのかたわら、自然言語処理関連の開発に従事しています。本エントリでは、最近社内で開発した自然言語処理システムを紹介します。

■ 「しょうゆ」のバリエーションは 100 種類以上

クックパッドで以前から解決したかった課題の一つに材料の名前(以下、材料名)の正規化があります。

f:id:jharashima:20171030074621j:plain

クックパッドのレシピは複数の材料から構成され、各材料は名前と分量から構成されています。例えば、上のレシピの一つ目の材料は「豚薄切り肉」が名前で、「200g」が分量です。

さて、この材料名はこのレシピでは「豚薄切り肉」という表現でした。しかし、他のレシピでは「豚うす切り肉」という表現かもしれません。「豚うすぎり肉」や「ぶた薄切り肉」、「豚薄ぎり肉」等の表現もありえますね。

これは異表記同義(いわゆる表記揺れ)の問題ですが、同様の問題は他にも沢山あります。例えば、以下のようなものです。

  • 同義表現系の問題
    • 異表記同義(上述)
    • 異形同義(e.g.「じゃがいも」と「馬鈴薯」)
    • 略記(e.g.「ホットケーキミックス」と「HM」)
    • 綴り間違い(e.g.「アボカド」と「アボガド」)
  • 付属表現系の問題
    • 記号(e.g.「★味噌」)
    • 括弧表現(e.g.「油(炒め用)」)
    • 接頭辞(e.g.「お砂糖」)
    • その他(e.g.「お好みでローズマリー」)

面倒なことに、これらの問題は複合的に発生することも珍しくありません。その結果、例えば、クックパッドにおける「しょうゆ」には、分かっているだけで、100 種類以上のバリエーションが存在しています。

時に、これがサービス開発において問題となります。

例えば、あるレシピのカロリーを計算するとしましょう。そのためには、そのレシピ中の全ての材料名のカロリーが必要です。つまり、材料名ごとにカロリーを登録したデータベースが必要です。

しかし、クックパッドには数百万種類の材料名が存在します。これらの全てに人手でカロリーを登録するのは途方もない作業です。「しょうゆ」は大さじ 1 杯で約 13 kcal ですが、あらゆる「しょうゆ」のバリエーションにこの情報を登録するのは大変です。

こういった時に、「材料名を正規化したい」という考えに至ります。あらゆる「しょうゆ」のバリエーションを全て「しょうゆ」に正規化できれば、「しょうゆ」にだけカロリーを登録すれば良いわけです。大分コストを削減できそうですね。

このように、材料名の正規化は、以前から解決したいと思っていた課題の一つでした。

■ 正規化しよう!とは言え...

どうすれば材料名を正規化できるのでしょうか。

最初に検討すべきは正規表現です。例えば、材料名の冒頭の記号と末尾の括弧表現を除去するというのはどうでしょう。しかし、この方法だと、同義表現系の問題は全く解決できません。

クラウドソーシングはどうでしょう。人手で正規化すれば、全ての問題を解決できそうです。しかし、1 個の材料名を 10 円で正規化できたとしても、数千万円が必要です。仕様変更等でやり直しが発生すれば、さらに必要ですね。

機械学習の出番かもしれません。例えば、何らかの語彙を定義できれば、SVM や Random Forest 等で各材料名を語彙のいずれかに分類できそうです。しかし、そもそも語彙を定義するのが困難です。未知語とかどうしましょう。

材料名の各文字について CRF で「必要」もしくは「不要」というラベルを付与して、必要な文字だけを抽出するというのはどうでしょう。しかし、この方法でも、同義表現系の問題は解決できません。

いずれの手法にも何かしらの問題がありそうです。

■ Encoder-Decoder で正規化しよう!

そこで、クックパッドでは Encoder-Decoder を利用することにしました。

Encoder-Decoder は、おおまかには、「入力の情報を何らかのベクトルに集約し、そのベクトルから出力を生成するモデル」です。前半を処理する部分が Encoder で、後半を処理する部分が Decoder です。

このモデルの利用例としては機械翻訳が代表的です。日英翻訳であれば、日本語文が入力で、その英訳が出力です。入力と出力は一般的には単語列です。正解データ(この場合は日英対訳)さえあれば、翻訳モデルが構築できます。

材料名を正規化する場合、材料名が入力で、その正規化後の表現が出力です。イメージは下図の通りです。上述の通り、Encoder-Decoder の一般的な設定では入力も出力も単語列ですが、我々の設定では文字列になります。図の EOW は End-of-Word の略です。

f:id:jharashima:20171030074722p:plain

Encoder-Decoder は入力から出力を抽出するわけではなく、生成します。そのため、付属表現系の問題だけではなく、同義表現系の問題も解決できます。また、正規化のパターンが学習できれば、未知語にもある程度は対応できます。

なお、このように Encoder-Decoder を正規化に利用するのは我々が最初ではありません(業務に導入したのは我々が最初かもしれません)。例えば、以下の 2 本の論文で同様のアイデアが提案されています。

  • Japanese Text Normalization with Encoder-Decoder Model(Ikeda et al. 2016)
  • 疑似データの事前学習に基づくEncoder-decoder型日本語崩れ表記正規化(斉藤ら 2017)

■ 全体像はどうなってる?

正規化システムの全体像は大きく三つに分割できます。正解データ収集とモデル開発、バッチ実行です。以下でそれぞれについて概説します。

f:id:jharashima:20171030074818p:plain

正解データ収集

Encoder-Decoder を学習(と検証、テスト)するには正解データが必要です。そこで、スタッフが毎日利用する社内ツール(クックパッドと同様、Rails 製)に、正解データを収集するための仕組みを追加しました。普段の業務をこなすと、正解データが自然と MySQL に蓄積されるようにしました。

本エントリの執筆時点で、約 12,000 個の正解データが収集されていました。

モデル開発

開発環境には p2.8xlarge を、フレームワークには Chainer を利用しました。やや詳細な話になりますが、Encoder と Decoder には Stacked Uni-directional LSTM(3 層)を使用しました。なお、我々のタスクでは Bi-directional LSTM や Attention はあまり効果がありませんでした。

PR がマージされると Jenkins が起動して、プログラムの実行環境をまとめた Docker イメージが社内の Docker レポジトリに登録されます。このイメージは後述するバッチで利用されます。

バッチ実行

クックパッドは主に Rails で構築されているため、そのデータの多くは MySQL に蓄積されています。さらに、これらはログデータ等とともに Redshift に集約されています。正規化バッチはこれらのデータをもとに動作しています。

まず、正規化の対象とする材料名を Redshift から取得して、S3 に保存します。次に、Docker イメージをもとに Encoder-Decoder の実行環境をインスタンスに準備した後、材料名を正規化します。最後に、S3 を経由して、結果を MySQL に保存します。

■ 正答率はどれくらい?

最後に、簡単な実験結果を紹介します。実験では正解データの 10% をテストデータに、別の 10% を検証データに、残りの 80% を学習データに使用しました。

手法 正答率
Baseline A(変更なし) 20.8%
Baseline B(正規表現) 28.6%
Encoder-Decoder 71.2%

まず、ベースラインの結果を確認しましょう。Baseline A は、入力をそのまま出力した場合の結果です。正答率(正解との完全一致率)は 20.8% でした。これは、テストデータの約 80% に何らかの正規化が必要ということを意味しています。

Baseline B は正規表現です。冒頭の記号と末尾の括弧表現を入力から除去した場合の結果です。正答率は 28.6% でした。上でも議論したように、正規表現だけで正規化するのはやはり困難でした。

最後に、Encoder-Decoder の正答率は 71.2% でした。同義表現系の問題(e.g. 桜エビ → サクラエビ)や付属表現系の問題(e.g. ※牛乳 → 牛乳)、それらの複合的な問題(e.g. ☆ローリエ → 月桂樹)に幅広く対応できたので、ベースラインより圧倒的に高い正答率となりました。

補足として、この数字はあくまでもテストデータでの値です。我々のテストデータ(と言うより、正解データ)は、テストのため、低頻度の材料名を少なからず含んでいます。現実のデータにおける数字はもう少し高いです。

また、実際に正規化結果を利用する際はスタッフがざっと確認して、明らかな間違いは修正しています。なお、修正されたデータは新たな正解データとして利用できます。

■ Appendix

以下は少し発展的な話題です。Encoder-Decoder でも失敗するのはどういうケースでしょうか。そして、それらのケースはどうすれば改善できるのでしょうか。

探索範囲が狭い

Decoder が N 文字目を出力する際は、その時点での生成確率が一番高い文字を選択します。しかし、確率が二番目に高い文字は無視して良いのでしょうか。N+1 文字目以降を出力する際に後悔しないでしょうか。

そこで、ビームサーチです。N 文字目を出力する際、確率が高い文字を M 個キープしておきます。そして、最終的に生成確率が一番高い文字列を出力としましょう。追加実験では、M = 10 の時、正答率が 72.3% になりました(統計的に優位でした。以降の数字も同様)。

存在しえない文字列を出力

例えば、「こめ油」を「米油」と正規化すべきところ、「コー油」と正規化してしまうケースです。「コー油」という文字列は少なくともクックパッドには存在しません。機械翻訳でも、Encoder-Decoder がおかしな文(単語列)を出力してしまうのはよくある話です。

そこで、このような文字列を出力しないように、Decoder を制御してみました。具体的には、ビームサーチの途中でクックパッドに存在しない文字列が生成された場合、その文字列を正規化候補から除外してみました。トライ木を利用すれば簡単に実装できます。正答率は 72.8% になりました。

入力と無関係の文字列を出力

例えば、「トーモロコシ」を「とうもろこし」と正規化すべきところ、「とろろこんぶ」と正規化してしまうケースです。「とろろこんぶ」は世の中に存在する文字列ですが、もちろんこの正規化は間違いです。

そこで、ビームサーチで取得した正規化候補を入力との類似度(と Encoder-Decoder のスコア)でリランキングしてみました。類似度の計算には word2vec を、その学習データにはクックパッドの全レシピを利用しました。正答率は 73.3% になりました。

その他

さて、まだまだ改善の余地があります。しかし、調査してみたところ、これらの多くは学習データの不足に起因するものでした。ここから先はモデルを改善するよりも、学習データを追加する方が楽かもしれませんね。

■ おわりに

本エントリでは、Encoder-Decoder でレシピの材料名を正規化するシステムについて紹介しました。

このモデルには、レシピの材料名だけでなく、EC サイトの商品名や不動産サイトの物件名等も正規化できる可能性があります。本エントリが、正規化に頭を抱える誰かのお役に立てば幸いです。

最後に、クックパッドの研究開発部では自然言語処理や画像認識の専門家を募集しています。あなたの知識・スキルで世界中の毎日の料理を楽しみにしませんか。ご興味がある方は採用ページを是非ご覧ください。ご連絡をお待ちしております。

クックパッド株式会社 研究開発部 採用情報