レシピのタイトルから材料を予測する🚀

研究開発部のサウラブ(bira)です。

本稿ではユーザがレシピの作成にかける労力を減らすために取り入れた、機械学習を利用した機能の一つについて 解説します。この機能を利用すると、ユーザがレシピのタイトルを入力することで、利用されるであろう材料が予測できます。

要約

  • レシピのタイトルから材料を予測できるモデルを作りました。
  • 投稿開発部と協力してレシピエディタに材料提案機能を追加しました。

App Storeで入手可能な最新のCookpadアプリ(v19.6.0.0)でこの機能を使用できます。

モデルはどうなっているか

1. Embed

f:id:bira:20190220104854p:plain

  • 学習(Training): Word EmbeddingとSentence Embeddingを学習してS3にアップロードします。(次のセクションで説明
  • 前処理(Preprocessing): 特殊文字を削除します。 多くのCookpadユーザーはテキストに特殊文字を使用しています。 例:"✧おいしい♡タンドリーチキン♡^-^✧"に特殊文字が含まれています: , ,^-^。特殊文字には材料に関する情報が含まれていないので、それらを削除します。特殊文字を削除するには、次のpython Functionを作成しました:

コードを表示する

  import re
  def remove_special_characters(text):
      non_CJK_patterns = re.compile("[^"
                                    u"\U00003040-\U0000309F"  # Hiragana
                                    u"\U000030A0-\U000030FF"  # Katakana
                                    u"\U0000FF65-\U0000FF9F"  # Half width Katakana
                                    u"\U0000FF10-\U0000FF19"  # Full width digits
                                    u"\U0000FF21-\U0000FF3A"  # Full width Upper case  English Alphabets
                                    u"\U0000FF41-\U0000FF5A"  # Full width Lower case English Alphabets
                                    u"\U00000030-\U00000039"  # Half width digits
                                    u"\U00000041-\U0000005A"  # Half width  Upper case English Alphabets
                                    u"\U00000061-\U0000007A"  # Half width Lower case English Alphabets
                                    u"\U00003190-\U0000319F"  # Kanbun
                                    u"\U00004E00-\U00009FFF"  # CJK unified ideographs. kanjis
                                    "]+",  flags=re.UNICODE)
      return non_CJK_patterns.sub(r"", text)

  • トークン化する(Tokenize): MeCabを使ってテキストをトークン化します。
  • Embedding: Word EmbeddingとSentence Embedding モデルを使用して、Cookpadデータベース内の各レシピのタイトルをベクトルに変換します。
  • 索引付け(Indexing): Faissを使用してベクトルにインデックスを付け(method = IndexFlatIP=Exact Search for Inner Product)、インデックスをS3にアップロードします。Faiss(Facebook AI Similarity Search)は、ベクトルの効率的な類似検索のためにFacebook AIによって開発されたライブラリです。 Faissは10億スケールのベクトルセットで最近傍検索をサポートします。

    2. Search&Suggest (API Server)

    f:id:bira:20190220104850p:plain

  • S3からWord EmbeddingモデルとSentence EmbeddingモデルとFaiss Indexをダウンロードします。
  • Word EmbeddingモデルとSentence EmbeddingモデルとFaiss Indexをメモリにロードします。
  • Embeddingモデルを使用して、入力されたタイトルをベクトルに変換します。
  • Faissを使用してk個の類似するレシピを検索します。
  • 類似するレシピの中で最も一般的な材料を提案します。

Embeddingsを学習する:

レシピのタイトルデータでWord Embeddingモデル(Fasttext)を学習します。

gensimでFasttextを使っていました。gensimはとても使いやすいです。

コードを表示する

from gensim.models import FastText
# recipe_titles : [.....,牛乳で簡単!本格まろやか坦々麺,...]
# tokenize recipe titles using MeCab and then train fasttext model
# recipe_title_list(tokenized) : [...,['牛乳','で','簡単','!','','本格','まろやか','坦々','麺'],....]
ft_model = FastText(size=100,min_count=5,window=5,iter=100, sg=1)
ft_model.build_vocab(recipe_title_list)
ft_model.train(recipe_title_list, total_examples=ft_model.corpus_count, epochs=ft_model.iter)

なぜFasttextを選んだのですか?

Fasttext(これは本質的にword2vecモデルの拡張です)は、各単語を文字n-gramで構成されているものとして考えます。 そのため、単語ベクトルは、これらの文字数n-gramの合計で構成されます。例:”中華丼”の単語ベクトルはn-gram”<中”、”中”、”<中華”、”華”、”中華”、”中華丼>”、”華丼>”のベクトルの合計です。Fasttextはサブワード情報で単語ベクトルを充実させます。それゆえ: - 稀な単語に対してもより良いWord Embeddingsを生成します。たとえ言葉が稀であっても、それらの文字n-gramはまだ他の単語中に出現しています。そのため、その Embedding は使用可能です。例:”中華風”は”中華丼”や”中華サラダ”のような一般的な単語と文字n-gramを共有することは稀であるため、Fasttextを使用して適切な単語のEmbeddingを学習できます。 - 語彙外の単語 - 学習用コーパスに単語が出現していなくても、文字のn-gram数から単語ベクトルを作成できます。

Sentence Embeddingモデルを学習します。

二つの Sentence Embedding モデルを試してみました:

  • Average of Word Embeddings:文は本質的に単語で構成されているので、単に単語ベクトルの合計または平均を取れば文のベクトルになると言えるかもしれません。 このアプローチは、Bag-of-words表現に似ています。これは単語の順序と文の意味を完全に無視します(この問題で順序は重要でしょうか?🤔)。

コードを表示する

  import MeCab
  VECTOR_DIMENSION=200
  mecab_tokenizer_pos = MeCab.Tagger("-Ochasen")
  def sentence_embedding_avg(title, model=ft_model):
      relavant_words = [ws.split('\t') for ws in mecab_tokenizer_pos.parse(title).split('\n')[:-2]]
      relavant_words = [w[0] for w in relavant_words if w[3].split('-')[0] in ['名詞', '動詞', '形容詞']]
      sentence_embedding = np.zeros(VECTOR_DIMENSION)
      cnt = 0
      for word in relavant_words:
          if word in model.wv
              word_embedding = model.wv[word]
              sentence_embedding += word_embedding
              cnt += 1
      if cnt > 0:
          sentence_embedding /= cnt
      return sentence_embedding

  • トークン化する(Tokenize): MeCabを使用して文を形態素解析します。
  • フィルタ(filter) :名詞、形容詞、動詞だけを残して、他の単語を除外します。
  • 平均(Average): フィルタ処理した単語のWord Embeddingを取得し、それらを平均してタイトルベクトルを取得します。

  • Bi-LSTM Sentence Embeddings: Cookpadのレシピデータを使って教師あり学習によってSentence Embeddingを学習します。ラベルは2つのレシピ間のJaccard Similarityから導き出します。レシピを材料のセットと見なすと、2つのレシピ間のJaccard Similarityは次のように計算されます。 f:id:bira:20190220115601p:plain

    アイデアは、それらの間の高いJaccard Similarityを持つレシピのレシピタイトルベクトルをSentence Embeddingスペース内で互いに近くに配置することです。

    • データセットを作成します: 2つのレシピのタイトルと、これら2つのレシピの類似度を表すJaccardインデックスを含む各サンプル行を持つデータセットを作成します。{title_1, title_2, Jaccard_index}
    • 下のネットワークを学習します: f:id:bira:20190220104951p:plain 上記のネットワークは2つの設定で学習することができます:
      • Regression: g(-) : sigmoid と y = Jaccard Index
      • Classification: g(-): dense+dense(softmax) と y = Jaccardインデックスから派生したクラスラベル 5クラスの分類設定で上記のネットワークを学習することによって学習されたF( - )は、最もよく機能するようです。ネットワークにとって、回帰問題よりも分類問題の方が解きやすい場合があります。

      Kerasでネットワークを実装する:

コードを表示する

    from keras import backend as K
    from keras import optimizers
    from keras.models import Model
    from keras.layers import Embedding, LSTM, Input, Reshape, Lambda, Dense
    from keras.layers import Bidirectional
    import numpy as np
    def cosine_distance(vects):
        x, y = vects
        x = K.l2_normalize(x, axis=-1)
        y = K.l2_normalize(y, axis=-1)
        return K.sum(x * y, axis=-1, keepdims=True)

    title_1 = Input(shape=(MAX_SEQUENCE_LENGTH,))
    title_2 = Input(shape=(MAX_SEQUENCE_LENGTH,))
    word_vec_sequence_1 = embedding_layer(title_1)  # Word embedding layer(fasttext)
    word_vec_sequence_2 = embedding_layer(title_2)  # Word embedding layer(fasttext)
    F = Bidirectional(LSTM(100))
    sentence_embedding_1 = F(word_vec_sequence_1)
    sentence_embedding_2 = F(word_vec_sequence_2)

    similarity = Lambda(cosine_distance)([sentence_embedding_1, sentence_embedding_2])
    similarity = Dense(5)(similarity)
    y_dash = Dense(5, activation='softmax')(similarity)
    model = Model(inputs=[title_1, title_2],  output=y_dash)

    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    model.fit([train_title_1, train_title_2], y)  # [train_title_1, train_title_2], y are respectively input titles and class label
    np.save('bilstm_weights.npy', F.get_weights())

  • 前のステップで学習したF(-)を文のEmbeddingとして使用します:

コードを表示する

    from keras.models import Model
    from keras.layers import Embedding, LSTM, Input, Reshape, Lambda, Dense
    from keras.layers import Bidirectional
    import numpy as np

    title = Input(shape=(MAX_SEQUENCE_LENGTH,))
    word_embedding = embedding_layer(title)
    F = Bidirectional(LSTM(100))
    sentence_embeddding = F(word_embedding)
    sentence_embedding_model = Model(input=title, output=sentence_embedding)

    sentence_embedding_model.layers[2].trainable = False
    sentence_embedding_model.layers[2].set_weights(np.load('bilstm_weights.npy'))
    def sentence_embedding_bilstm_5c(text):
        txt_to_seq = keras_tokenizer.texts_to_sequences([mecab_tokenizer.parse(text)])
        padded_sequence =  sequence.pad_sequences(txt_to_seq,maxlen=MAX_SEQUENCE_LENGTH)
        return K.get_value(sentence_embedding_model(K.cast(padded_sequence,float32)))[0]

結果

以下はサービスにおける利用率です。例えば、3 out of 5 suggested ingredients matches actual は 5 個 suggest したうち 3 個が利用された割合です。

3 out of 5 suggested ingredients matches actual(%) 2 out of 5 suggested ingredients matches actual(%)
Average of word embeddings 53% 80%
Bi-LSTM Sentence Embeddings 50% 76%

Average of word embeddings(これはBag-of-Wordsに似ています)はBi-LSTM Sentence Embeddingよりもこの問題に適しています。これは、レシピのタイトルは短いテキストであるために、単語順序の情報は材料を予測するのにはあまり役に立たないからだと思われます。

まとめ

  • レシピのタイトルから材料を予測できるモデルを作りました。
  • 投稿開発部と協力してレシピエディタに材料提案機能を追加しました。

いかがでしたでしょうか。 Cookpadでは、機械学習を用いて新たなサービスを創り出していける方を募集しています。 興味のある方はぜひ話を聞きに遊びに来て下さい。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/