AWS Startup ブログ

【寄稿】dely株式会社における機械学習の取り組み

この投稿はdely株式会社 データサイエンティスト 辻 隆太郎 氏に、クラシルでの Amazon SageMaker を中心とした機械学習の取り組みについて寄稿頂いたものです。

クラシルというサービスについて

dely株式会社の辻と申します。delyでは、70億人に1日3回の幸せを届けるというミッションを掲げ、レシピ動画サービスのクラシルを展開しています。

AWSはサービス開始当初から利用しており、様々なサービスを組み合わせながら柔軟なアーキテクチャを構築できることにとても魅力を感じています。また、AWSを利用する上で直面する課題についても、AWSの担当者さんからのフォローアップや、直接サービス開発チームの方へフィードバックできるなど、手厚いサポートにいつも助けられています。

図: AWSサービスの組み合わせによって不確実性に対処する 

現在、クラシルがユーザに届けられている価値は、単にレシピを見つけられるという点が最も大きいと言えますが、実際の調理プロセスには困難な課題がいくつも潜伏しており、レシピを見つけられるのはこのうちのほんの氷山の一角をサポートしているに過ぎません。
例えば、一口にレシピを見つけると言っても、今日の献立をどうするかを決めるためには、まず冷蔵庫の残り物を確認したり、家族の好き嫌いに配慮したり、お子さんの給食や家族のランチと被らないようになど、考慮すべき課題がたくさんあります。また、買い物に出かけた後にも、食材の価格や鮮度など最適な選択を行い、そして実際に作る際にも失敗しない様に、家族の帰宅に間に合うように効率よく、などなど不確実な課題が山積です。
我々delyでは、クラシルというアプリを通じて、こうした「料理に関する工程全体の課題」に対して可能な限り解決に導くサービスを提供していくことで、本当の意味でのクラシルの価値をユーザお届けできるようなサービスへと進化していきたいと考えています。本日はそこに至る取り組みについて、ほんの一部ですがご紹介させて頂ければと思っています。前半でレシピの素性抽出、後半ではユーザの素性抽出について具体的にご紹介します。

 

図: 調理は氷山の一角

主菜にあう副菜のレコメンド

クラシルでは献立機能というサービスを提供しています。こちらはその名の通り「献立をつくる」ための機能です。主菜、副菜、汁物の中から好きなレシピを選び献立を作ります。そして作った献立に含まれる食材たちの「買い物リスト」をつくることができます。さて、この機能の一部として、 主菜に合う副菜のレコメンドを行なっているのですが、その取り組みについて少しご紹介させて頂きます。

図: 献立機能

レシピの素性抽出

この主菜に合う副菜のレコメンドでは、最初はこれ自体を目的とした機能の開発を目指したわけではありませんでした。我々が最も重要と考えるのは、繰り返しになりますが本当の意味でクラシルの価値をユーザに届けることです。そしてそのためには、まずレシピの潜在的な素性を抽出することが重要な鍵となります。
このレシピ素性を抽出するというのは、そのレシピが持つ基本的な情報(タイトル、料理手順、食材、サムネイル、動画など)に加えて、レシピが潜在的に持っている情報を推定し付加していくという意味です。たとえば、調味料や食材といった情報から一人当たりのカロリーを推定したり、サムネイルを構成している主要な色素成分を抽出したりすることでレシピを構成している素性項目を増やし、レシピの特徴表現をより豊かにすることができます。その素性項目の一つとして主食、主菜、副菜、汁物、甘味といったカテゴリの分類があるのですが、その分類に至る工程の結果として、いわば副次的にできたのがこの副菜レコメンド機能といえます。それでは、このレシピの潜在的な素性を抽出するための具体的な3つの工程についてご紹介したいと思います。

  1. 人海戦術・ルールベースでアノテーション
  2. アノテーションした素性に基づいて他の素性を推論して得る
  3. 機能リリース後にユーザの行動からフィードバックを得る

1. 人海戦術・ルールベースでアノテーション

クラシルのようにコンテンツを自社で制作して提供するサービスの場合は、UGCのサービスとは異なり、コンテンツ自体がコントローラブルで非常に扱いやすいというメリットがあります。たとえば、現在クラシルには27,000以上の動画レシピコンテンツを有しています。既存コンテンツに対して後から新しい情報を付加したいといった場合、UGCサービスではまずは自然言語処理や画像処理を行うなど難しい課題も多くあると思いますが、クラシルではすでに多くのデータが数値化された状態で管理されているため、少人数短期間であっても比較的正確なアノテーションを効率よく行うことができます。具体的にこのカテゴリ分類に関しては、アノテーターとして有識者1名が担当し、付与したアノテーション結果に対して、社内のクラシルシェフと管理栄養士が定期的に協議を重ねながら、約1.5ヶ月ほどでアノテーション作業を終えることができました。

図: アノテーション画面 

2. アノテーションした素性に基づいて他の素性を推論して得る

次に、このアノテーションしたデータセットからカテゴリ分類モデルを作成しました。以下のようにAmazon SageMakerのビルトインアルゴリズムであるXGBoostとHPOを利用することで、ごく短期間で機能を実装することができます。

1. Amazon SageMakerビルトインアルゴリズムXGBoostでトレーニングする

from sagemaker.amazon.amazon_estimator import get_image_uri

smclient = boto3.Session().client('sagemaker')
sess = sagemaker.Session()
container = get_image_uri(region, 'xgboost', repo_version='latest')

xgb = sagemaker.estimator.Estimator(container,
                                        obj.sage_role, 
                                        train_instance_count=1,
                                        train_instance_type='ml.m4.xlarge',
                                        output_path=output_path,
                                        sagemaker_session=sess)

xgb.set_hyperparameters(eval_metric='auc',
                            objective='binary:logistic',
                            num_round=100,
                            rate_drop=0.3,
                            tweedie_variance_power=1.4)

2. Amazon SageMakerのHPOにXGBoostのEstimatorを渡す

hyperparameter_ranges = {'eta': ContinuousParameter(0, 1),
                             'min_child_weight': ContinuousParameter(1, 10),
                             'alpha': ContinuousParameter(0, 2),
                             'max_depth': IntegerParameter(1, 10)}

objective_metric_name = 'validation:auc'

tuner = HyperparameterTuner(xgb,
                                objective_metric_name,
                                hyperparameter_ranges,
                                max_jobs=12,
                                max_parallel_jobs=3)

tuner.fit({'train': train, 'validation': validation})

3. 継続処理で待ち合わせが必要な場合

while True:
    status = smclient.describe_hyper_parameter_tuning_job(HyperParameterTuningJobName=tuner.latest_tuning_job.job_name)['HyperParameterTuningJobStatus']
    if status in ['InProgress']: 
        print('wait...')
        sleep(20)
    elif status in ['Completed']:
        print(status)
        predictor = tuner.deploy(initial_instance_count=1,instance_type='ml.m4.xlarge')
        break
    else:
        try:
            raise ValueError("error!")
        except ValueError as e:
            print(e, status)
            break

カテゴリと同様に、和食、洋食、中華といったジャンル、春夏秋冬といったシーズナリティ、さらには、朝食、昼食、夕飯といったシーン毎のアノテーションなどについても次々と素性をレシピに追加していきました。このようにして、人海戦術によるアノテーションが可能であること、そしてAmazon SageMakerが提供しているトレーニングジョブやモデルに関するサービスを活用することによって、たとえ担当者が少ない場合であっても非常に簡単に機械学習の基盤を効率よく構築できると思います。

また、カロリーや価格の高低や、調理時間の長短など単純な2値分類については、既存のレシピ全体から集計した基礎統計量に基づきルールベースで分類を行い、その後でそれらの結果について定性的な妥当性の有無についてクラシルシェフと管理栄養士と協議しながらルールに調整を加えました。

ルールベースでクラスタリングしたレシピのカロリーの平均値の和に対して基礎統計量から算出した重みwを掛ける

# カロリーの平均値の和
# ルールベースでグループ化したものついてk-Meansでのクラスタリングを想定
# group_a_recipe_cluster1 : グループAのクラスタ1
# group_b_recipe_cluster2 : グループBのクラスタ2
# group_c_recipe_cluster3 : グループCのクラスタ3

sum_calorie_mean = group_a_recipe_cluster1.describe()['calorie_of_one']['mean'] + group_b_recipe_cluster2.describe()['calorie_of_one']['mean'] + group_c_recipe_cluster3.describe()['calorie_of_one']['mean']
sum_calorie_mean = sum_calorie_mean * w

このような流れを経て、ある程度レシピの素性データが揃った後、これらをベースにして、レシピのクラスタリングや他の素性を推論するためのモデル作成を行い、さらなるレシピ素性を充足させていきました。そして最終的には、これらを用いて献立を作成するべく、最適化問題におけるハードな制約およびソフトな制約を施して、献立組み合わせアルゴリズムを作成しました。この献立組み合わせアルゴリズムによって生成された献立についても、組み合わせの良し悪しや見栄えの良し悪しなどについて、クラシルシェフおよび管理栄養士に定期的に評価してもらいながら、組み合わせアルゴリズムの改善を行いつつ、およそ15,000通りほどの献立教師データを1ヶ月ほどで作成しました。

3. ユーザのフィードバックデータを反映する方法

しかしながら、実はこのようにして生成した献立教師データは、一時的にサービス提供した後、現在では提供を行なっていません。その理由は特段ネガティブな理由があったわけではなく、リリース後の分析によって「本当の意味でのクラシルの価値をユーザお届けできるようなサービス」か否かもう一度しっかり検討する必要があるという判断をしたためでした。
その経験を踏まえて、改めてリリースしたのが現在の献立機能で、ユーザ自身に組み合わせてもらう機能として提供してるので、ユーザの立場からみたニーズをより理解できると考えています。確かにクラシルシェフや管理栄養士はレシピのプロではありますが、ユーザの求めている献立が果たして我々が教科書的に正解とする観点と一致するだろうか?この疑問について、現在の献立機能で作成されている献立と、献立教師データとの構成を比較分析することで実態が徐々に明らかになってきています。そしてもし、教科書的には決して良いとはされない献立だが、ユーザから非常に求められている献立があるとするならば、その献立はもっと良い形で最適化された提案されていくべきだと考えています。

図: ユーザが立てた献立と教師データとに乖離があるか? 

 

図: 献立教師データのサムネイル

 

献立教師データに対して見た目の適切不適切を推定

with open(file_name1, 'rb') as f:
    payload = f.read()
    payload = bytearray(payload)
    
response = runtime.invoke_endpoint(EndpointName='image-classification-XXXXXXXXXXX', 
                                   ContentType='application/x-image', 
                                   Body=payload)

result = response['Body'].read()
result = json.loads(result)
index = np.argmax(result)
object_categories = ['適切!', '不適切!']
print("Result: label - " + object_categories[index] + ", probability - " + str(result[index]))

## => Result: label - 適切!, probability - 0.6134527325630188

ユーザにレシピのレコメンド

ここまではレシピ側のみの素性抽出にフォーカスしてきましたが、我々にとって本来の目的は、本当の意味でのクラシルの価値をユーザにお届けすることです。つまり、ユーザの嗜好性や家族構成、ライフスタイルなどが反映されたパーソナライズサービスを提供することにあります。その一つがレシピのレコメンドと考えられるのですが、このレコメンドを考える上で最初に検討すべき項目が大きく3点ほどあると思います。

  1. ユーザの利用頻度
  2. ユーザの利用期間
  3. ユーザのコンテキスト

1つ目がユーザの利用頻度です。利用頻度が高いユーザについては、ウォームスタートが可能なため、協調フィルタリングよるレシピのレーティングが可能です。しかし利用頻度が少ないユーザは、コールドスタート問題によりレコメンドのための十分な情報を得られません。したがって、この場合はアイテムベースレコメンドなど、接触に起因する類似性によるレコメンドを行うなど別の対策を取る必要があります。そのためにユーザの利用頻度の閾値がレコメンドを始める前提であり、それはユーザの行動ログに基づいて基礎統計量から算出して、閾値を上回る利用頻度のユーザのみをウォームスタートによるレコメンドの対象としています。

2つ目がユーザ利用期間です。料理の視聴は、季節や行事イベントに大きく左右されます。それは、どんなにそうめんが好きな人でも真冬にそうめんをレコメンドされてもあまり嬉しくないのが感覚的にも理解できるように、サンプル取得すべき定義域をどの範囲で取るのかという点は非常に重要です。
また、期間において考慮すべきなのはこの点だけでなく、直近で肯定的な反応を示したレシピに対して、後日同じレシピをレコメンドした場合に、「あのレシピが美味しかったのでまた作ろう」と再び肯定的に想起されるのか、あるいは逆に「このレシピは確かに美味しかったけれど、だからといって今週も同じものを作りたくない」と否定的に想起されるかはユーザによっても異なります。このように、利用期間については、利用頻度とは異なり単に1ユーザあたりの利用期間の閾値を取るだけでは事足りず、シーズナリティの分割、行事イベントの影響など全体的な時間軸と、各ユーザ毎の直近の時間軸など併せて考慮する必要があります。

そして3つ目がユーザのコンテキストです。殊にレシピのレコメンドは外的要因に大きく左右されます。たとえば、テレビであまり知られていない食材を扱った番組が放送されるとその食材を使ったレシピの検索数が多くなる傾向があるなどトレンドの影響は大きく受けます。また、日々の気温の変化などによってユーザの嗜好性は大きく変動します。
これらについては、ユーザの特性を詳細に分析して適切なレコメンドを行う必要がありますが、とはいえ完璧に提案することは非常に困難で、無理に複雑なモデルを作ろうとしてトレーニングに莫大な費用をかけたところで、費用対効果が見合うかどうかを事前に判断する必要があります。従ってこの場合は、いくつかのコンテキストごとのレコメンドを提案して、ユーザにそのなかから選んでもらうという手法が有用と考えて現在その方法での実現に向けて取り組んでいます。

図: パーソナライズ 

以上のようにユーザ行動ログ(イベントログ)は具に取得する必要があると同時に、非機能要件として不可欠な要件となります。そこで取得すべきイベントデータに漏れがないことをリリース前に確認するために、開発に着手する前にイベントを設計するイベントデザインファーストの取り組みを行なっています。

図: イベントデザインファースト

ユーザ素性の抽出

では、具体的にどのようにユーザ素性を抽出しているかをご紹介させて頂きたいと思います。こちらについても、大きく3ステップで行なっています。

  1. ユーザ行動に基づく素性抽出
  2. クラスタリング
  3. クラスタ毎の個別対応

1. ユーザ行動に基づく素性抽出

レシピ素性については、どのように抽出しているかここまでに前半で詳しくご説明してきました。ユーザ行動についてはかなり詳細な行動ログを取得していますので、この行動ログに基づいてユーザ素性を抽出しています。基本的には、ユーザがどのレシピをよく視聴したのか、どのくらいの時間視聴したのか、何件お気に入りをしたのかなど、ユーザとレシピの接触行動について行動ログの集計を行います。ここで先ほど言及しましたウォームスタート可能なユーザに絞るために利用頻度に基づいて半分足切りを行います。そして次に、各レシピの素性を軸とする集計を行なっていきます。たとえば、レシピの素性として時短という素性を持っているレシピに対する接触行動が多いのであれば、ユーザは時短レシピに対する関心が高いと考えられます。しかしながら、時短レシピに対する接触が直近に偏りが現れている場合には、時短レシピへの関心は一時的なものである可能性もあるので、安易にそのユーザの本質的な素性として時短レシピ好きとみなすのはいくぶん早計といえます。ところがある一定期間において、傾向が一様な分布を維持していると判断できたなら、そのユーザは時短レシピ好きとみなしても支障はないといえと考えます。しかし、そのように一様な分布傾向を観測するためには、継続性のかなり高いユーザが一定数以上必要となるため、サンプル絶対数が少ないことで信頼性が揺らぎます。そこでまず、ユーザは全体としてみなすのではなく、あるk個のクラスタリングに分類することで汎化した課題を局所化する必要があります。

2. クラスタリング

ユーザクラスタリングは、ルールベースとk-meansの両方で行います。以下のようにAmazon SageMakerのビルトインアルゴリズムとして提供されているK-meansを利用しています。

K-meansでトレーニングし推論エンドポイントをデプロイする

role = os.getenv("SAGE_ROLE", "arn:aws:iam::XXXXXXXXXXXXXXXXXXXXXXX")
bucket = os.getenv("SAGE_OUT_BUCKET", "XXXXXXXXXXXXXXX")
data_location = 's3://ZZZZZZZZZ'
output_location = 's3://YYYYYYYYYYY'
k = N

kmeans = KMeans(role=role,
                    train_instance_count=2,
                    train_instance_type='ml.c4.8xlarge',
                    output_path=output_location,
                    k=k,
                    epochs=20,
                    data_location=data_location)
    
kmeans.fit(kmeans.record_set(train.astype(np.float32).as_matrix()))

predictor = kmeans.deploy(initial_instance_count=1,instance_type='ml.m5.xlarge')

3. クラスタ毎の個別対応

各クラスタを分析していくことでクラスタ特性が見えてきます。

たとえば、とあるクラスタについては、レコメンド初日と2日目のCTRやお気に入りの減少勾配がとても急で、新奇性に左右される傾向が最も高いクラスタではないかという仮定を立てました。これについては、実際に他のクラスタと検定した場合にも有意差があると判定できましたし、また、このクラスタに含まれるユーザ特徴としては、週末の利用が多い、視聴時間が比較的少ないなどから、お気に入りは再考にのみ用いていて、調理時には動画を再生しないほど調理スキルが高いのではないかという仮説も立ちました。つまり、このクラスタに含まれるユーザへのレコメンド対応としては、通常の提案とは異なるレシピを提案を行うことで、初日は新奇性効果によってCTR、お気に入り数とともに伸ばし、もともと利用頻度が高く飽きやすいことから、一度見たレシピやお気に入りしたレシピのレーティングを下げる調整を行なったことで、このクラスタにおけるCTRがレコメンドした場合としなかった場合と比較して平均で約15%ほどの向上が見られました。

クラスタ毎のCTR遷移 f:id:long10:20190512181800p:plain

まとめ

いかがでしたでしょうか?
クラシルにおける取り組みについてご紹介させて頂きました。
delyでは、コンテンツを自社で制作しているという点の強みとして、人海戦術やルールベースによるアノテーションをコントローラブルに行うことができ、またユーザ行動からのフィードバックをコンテンツに素早く反映させることもできます。また、Amazon SageMakerを使うことでトレーニングからモデルのデプロイに至るまで、リソースコストをかけずに行えるのは非常にメリットだと考えています。

今後はさらに、ユーザからレビュー機能などによって、直近のレシピに対するより具体的な嗜好性のフィードバックを得ることや、献立作成時に家族構成や好きな食材、嫌いな食材(アレルギー体質)などの情報を得ることによって、さらにユーザに寄り添い食に関してユーザに価値を感じて頂けるようなサービスに成長していきたいと思っています。また、それと同時にレシピや動画の構成を計画的に分析することによって、良いクオリティの再現性を高めていくことで、質の高いコンテンツを提供し続けられるようにしていきたいと考えています。

著者について

dely株式会社
データサイエンティスト
辻 隆太郎 氏

dely株式会社で機械学習を担当。
データ分析基盤および機械学習を用いたサービスの設計・構築・運用と、データサイエンティスト業務を一人で兼任。
20年のエンジニアキャリアの中では、メインフレームでCOBOLを書いていた時期から、SIerで基幹系システムの受託開発や組み込み系開発、toC向けにWEB・アプリ開発など従事。
数学(主に数論)が趣味で、暇さえあれば何かしら数学のことを考えています。