【Databricks】公式デモ「Content-Based Recommenders」解説

# はじめに
以前の記事では、Databricks の動作デモであるdbdemosの紹介をしました。そちらは非公式に作られたガイドでしたが、Databricks 公式でも活用デモとしてノートブックを公開しており、こちらも Databricks を利用する際に参考になります。

今回は公式で公開されているもののうち、[Personalizing the Customer Experience with Recommendations](https://www.databricks.com/blog/2020/12/18/personalizing-the-customer-experience-with-recommendations.html) で公開されている、「Content-Based Recommenders」について解説します。

# ダウンロード、利用の方法
[公式のノートブック](https://www.databricks.com/notebooksrecommenders/cn_01_data_preparation.html) にアクセスし、「Download all notebooks」をクリックします。

するとフォームが出てくるので必要事項を埋めるとダウンロードのリンクが表示されるのでコピー。

その後、Databricks にアクセスしてインポートをクリックしてからURLを貼り付けるとノートブックが読み込まれます。
![2023-05-12_18h49_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/9c7adac2-2c11-4c3a-2ec5-d496bb8bbf22.png)

:::note
ダウンロードしたノートブックを直接アップロードするとうまくいかないようなのでURL経由でのインポートを推奨します。
:::

# ノートブック解説
ノートブックでは、Webショッピングにおいて、関連商品や過去の購入に基づいたレコメンドを表示するプログラムを作成する流れについて解説しています。

![reviews_recommendations.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/92308282-ef4d-7440-5187-079bbcca1fec.png)
![reviews_profile_recs2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/1c9445ce-b3fc-1e15-73fe-95c9f554145e.png)

ここでは、商品のタイトルなどから特徴量を抽出し、さらにユーザーレビューのデータを基に各ユーザーが好む商品の特徴量を抽出します。これらの特徴量の類似度を計算し、類似度が最も高い商品をレコメンドするのがこのシステムの概要です。

- 01では、データセットを読み込み、分析に必要なテーブルの作成を行います。
- 02a~cでは商品のタイトル、説明文、カテゴリを基に特徴量ベクトルを作成します、
- 03ではユーザーの商品レビューを基にして、ユーザーが好む商品の特徴量を分析します。
- 04ではシステムの構築例と、実際に類似度の比較を行った例を示しています。

# CN01: Data Preparation
## Step1: Download & Decompress Files
データセットは 2018 Amazon reviewes dataset を利用します。
データをダウンロードし、中身を確認します。

:::note warn
現在ノートブックに書かれているURLは古く、そのままではダウンロードに失敗します。

URLの`deepyeti.ucsd.edu/jianmo/amazon`をすべて`datarepo.eng.ucsd.edu/mcauley_group/data/amazon_v2`に置換してください。
:::

元のデータはjson形式になっていますが、Databricks ではこれを Delta Table という形式で保存した上で操作を行います。
このテーブルは Python のプログラム内で SQL クエリを使用した操作が行えるほか、必要に応じて Scala による関数定義などが行えるのが特徴です。

以降の行程では様々な分類、予測アルゴリズムが登場しますが、多くは Databricks 側が用意している spark などのライブラリから利用する事が可能です。

## Step2: Prep Metadata
最初に、テーブルを保存するための環境を作ります。

メタデータファイルから情報を展開しテーブル化します。
テーブルを作る際には、まずスキーマを宣言してからそこに json ファイルを読み込ませる、という順で行います。メタデータのスキーマは以下の通りです。

- 'asin' : 商品ID(ASIN)
- 'category' : カテゴリー
- 'description' : 説明
- 'title' : 商品名

ノートブック上で display 関数を実行することでテーブルをプレビューすることが可能です。

![preview1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/8290c8da-eef1-5f34-f9e0-61cf287d3aa4.png)
![preview2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/bd45a15a-2ccd-f5d1-f0a4-f7d850cf47b8.png)


その後、不要な属性や重複を削除、HTML タグの削除などクレンジングを行います。

## Step3: Prep Reviews
同様に、レビューファイルも読み込みテーブル化します。

- 'asin' 
- 'overall' : 評価
- 'reviewerID' : レビュワーのID
- 'unixReviewTime' : 投稿時刻

一人が同じ商品に複数投稿している場合は最新のものだけ残す処理を行っています。

# CN02a Determine Title Similarity
- 商品の特徴を抽出し、ベクトルとして保存する
- また、計算量が爆発しないように特徴量が近いものをクラスタリングし、その中だけで検索するようにする
- 類似度を計算して最も類似性の高い商品を表示する

02a,b,c では以上の流れをそれぞれ「商品名」「説明文」「カテゴリ」を基に特徴量抽出しながら行います。

::: note
ここでは利用するライブラリ (NLTK) の都合上、実行のためのクラスターの作成時に特定のスクリプト（Generate Cluster Init Script のセルに表示されている）を実行しないとエラーを吐きます。

作成済みのクラスターを使う場合は、02a の import セルの後に以下のコードを入れておくと動作することを確認しています。

```Python
nltk.download('wordnet')
nltk.download('omw-1.4')
nltk.download('averaged_perceptron_tagger')
```
:::

## 類似度の計算について

![cos](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/9d255e46-9072-5fd1-1e31-6f0b001d16df.png)

特徴量ベクトルは、類似性が高いものほど近い値を取るように配置されます。
これを原点と作る角度の小ささ（コサインの大きさ）で示したのがコサイン類似度であり、文書の類似度を計測する際によく使われる指標となっています。

この後の処理で特徴量を正規化するのは、コサイン=内積となり計算を簡略化できるためです。

## Step1: Prepare Title Data

タイトル間の類似度比較のためは TF-IDF 値を用います。
- TF: あるタイトル内に単語が出現する確率
- IDF: すべてのタイトルで単語が出現する確率の対数
  - 単語の持つ差別化情報であり、一般的に頻出する単語ほど重要性は下がるという考え方
- TF-IDF スコア: TF と IDF の積

最初にタイトルを一貫性のあるトークンに分割します(ここでは RegexTokenizer が利用されます)。
次にNLTKというライブラリを用いて、単数-複数、現在-過去などの活用を取り除いて語幹のみの形に統一します。これを利用するために、計算用のクラスターを構成するコード内で必要なモジュール（コーパスと品詞予測器）を入れる必要があります。

最後に、タイトルを記号や前置詞などが排除した上で単語の配列に変換します。

## Step2 Calculate TF-IDF Scores

ある単語が一つのタイトルに出現する確率と、すべてのタイトルで出現する確率を算出します。

あまりにもニッチな語を避けるためカウント対象は上位262144種（Spark のデフォルト設定）に限定します。
各語の登場回数は CountVectorize rを利用して数え上げます。

カウントした後にIDF関数を適用することでTF-IDF値が求められました。
- 'lemmata': 前処理した単語のIDの列
- 'tfidf': 各単語のtf_idf値の列

その後、ベクトルのノルム（長さ）が1になるように各次元をノルムで割る正規化を行います。これは前述通りコサイン計算のためです。

## Step3 Identify Products with Similar Titles
ここで商品同士の特徴量からコサイン類似度を求める事を考えると、約1000万存在する商品の全ての組み合わせを全て計算するのは非常にコストが嵩むため、ある程度類似している商品を事前にグループ化しておき、同じグループ内のみで検索するという方法を使います。

LSH(Local Sensitive Hashing) というアルゴリズムで、類似しているベクトルを同一のバケットに分類します。

ランダムに選んだ商品から、同じバケット内にあり類似度が上位である商品を一覧すると、ある程度似たタイトルが選ばれていることが分かります。

![result](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/b7a2e096-3f2a-e7e3-72f0-1f6a4cfe9a66.png)

ただし、「どれくらい似ているか」の尺度は主観的になりがちなため、性能評価については社内で基準を考える必要があります。

# CN02b: Determine Description Similarity
商品説明からの特徴抽出を行い、同じく類似度を測定する

## Step1: Prepare Description Data
説明欄はタイトルよりも長く情報量も多いが、同時に処理するデータ量が大きくなるため、ここでは次元数削減を行い、テキストをより狭い範囲のトピックに凝縮します。

まずはCN02aと同様に単語の前処理を行います。

## Step2a: Extract LDA Topic Features
LDAアルゴリズムは、一定数に分けられた「トピック」に対し文章がどれくらい語っているかを予測するアルゴリズムです。このトピックは何か特定のジャンルに分かれているということではなく、より近い事が書かれている文同士が纏められている抽象的なグループのようなものと捉えてください。

単語数のカウントは　HashTF　を用います。これは　CountVectorizer　よりも高速でより大規模なデータ向きですがハッシュの衝突可能性により値の正確性がやや下がります。

LDA（Latent Dirichlet Allocation）アルゴリズムを適用して、トピックを一定数(ここでは100)に分類し、各文章がそれぞれのトピックを含む割合を算出し、これを特徴量の一つとして用います。

## Step2b: Extract Word2Vec Concept Features
LDAに加えて、Word2Vec を利用して文書をベクトル化します。
Word2Vec ではある単語の意味を周囲の単語を基に予測するアルゴリズムで、LDAとは単語の位置を考慮する点が異なります。

## Step3: Calculate Descriptions Similarities
k-means アルゴリズムを利用して、文書の類似性に基づきクラスタリングを行います。こちらは LSH よりも高速での分類が可能です。

そして、02a で行ったときと同様に、類似商品の検索をクラスター内に限定します。

LDA や Word2Vec での特徴量生成は冒頭の数語に絞って行うことが推奨されています。
- Word2Vec は引数で設定可能、LDA の場合は手動でトークンを切ること

# CN02c: Determine Category Similarity
メタデータ内のカテゴリー階層データを利用して類似性を算出します。

## Step1: Resrive & Prepare Category Data
このデータセットにおけるカテゴリは、実際にはツリー構造になっていません。
- 親子の位置が逆転していたり子が親と飛び越えて共通の祖先につながってたりなどの矛盾が見られる

データ内ではカテゴリは配列として格納され、インデックスが階層を示しています。
エラー値（商品の説明などが混入している）を取り除くため、階層の最大値を10、ラベル名を100字に制限します。

ここではカテゴリを区別する為に、それぞれの階層の名前に親階層の名前を付け加える、という処理を行いました。例えば「ゲームソフト」でも「おもちゃ>TVゲーム>ゲームソフト」と「PC>PCソフト>ゲームソフト」のような違う構造の場合は別のカテゴリと認識されます。

## Step2: Calculate Category Similarities
各タグを one-hot 形式（カテゴリの番号に相当する次元のみ1の値を取るベクトル）に変換します。
ここではカテゴリの一番上の要素に基づいてバケットに分類します。

類似度は Jaccard 類似度を使って計測します。これは2つの商品に対し、それらの持つカテゴリのうち重複している要素の割合です。

カテゴリは種類が多いわけではなく一致 (similarity=1) する商品が多数ヒットするようです。これを単独で指標にすることは難しそうに見えます。

![2023-05-11_19h23_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/4b13d637-0133-c708-2088-22968badcbd9.png)

## Step3: Combine Similarity Scores to Build Recommendations

類似度が複数の観点から得られたが、これらをどのように組み合わせれば良いかを考える必要があります。

考えられる手法は
- 単純に乗算
- 重み付けをして平均
- 複数のレコメンダが出力したスコアを統合

などがあるが一つに絞るのではなく目的に応じて様々なアプローチをとるのが重要です。

# CN03: Construct User Profile Recommenders
ここでは、商品同士の関連性ではなくユーザー自体にアピールできる商品を提示するために、カスタマーレビューのデータからユーザーの好む商品を抽出し、それを基にユーザーの特徴量を作成します。

## Step1: Retrieve Product Profiles
初めに、CN02b で作成した Word2Vec の特徴量とクラスター/バケットの割り当てを取得し、レビューデータにもこれらによる変換を適用します。

## Step2: Assemble User Profiles
ユーザーがレビューで高得点をつけた商品ほどその商品との適合率が高いとみなして、各人がレビューした商品を基にデータセットを2種類（すべての商品対象/最近レビューした商品対象）作ります。

ここで、複数商品の特徴量を組み合わせるにあたって、星の数で重みづけして加重平均を出す、というシンプルな方法が考えられますが、レビューの持つ性質に注意する必要があります。
- 一般的にユーザーは非常に満足したか不満な時にしか投稿しない
- 評価は嗜好を表しているのか、それとも製品やサプライヤーへの評価なのか
- 「3」は基準になるのか？それ以下は平均値を下げることになる？

![newplot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/7acd9bb6-d1d5-a9c5-c034-bd77ba70c598.png)

このサンプルでは5を付けた人の方が圧倒的に多いため、こちらを重点的にみる方がよさそうです。そのため、高評価（４～５）のデータを「強い好み」として抽出し、それ以外を無視する方式を採用します。

加重平均を行うためには Summarizer を利用します。この場合、ベクトルの平均を出した後で正規化をする必要があります。

ユーザーの特徴量を算出したあとは、近い商品を探すスピードを早めるために商品のクラスターと一致するようにクラスタリングを行います（CN02 のクラスタリングモデルを再利用する）。

## Step3: Build & Evaluate Recommendations
商品の特徴量とユーザーの特徴量が算出出来たため、両方を比較することによってレコメンドを算出します。ここでユーザーを一人選び、特徴量の類似順に製品を表示した結果が以下となります。

![2023-05-16_16h31_21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/efbd5be7-1cc7-9606-4b90-93979191bb4c.png)

ユーザーを少数サンプリングして彼らに対するレコメンドを表示し、順位によって重みづけされた平均の類似度を計算して評価を行います。
![2023-05-16_16h31_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/f4112be9-d608-e697-1eb7-415b7e0af9cf.png)

あまり高いスコアとは言えなさそうです。考えられる原因としては、レコメンド対象の商品があまりにも幅広すぎることと、ユーザーも幅広い種類の製品をアマゾンで買っていることから、これらを平均した結果、ユーザーの特徴が特定のジャンルに寄らなくなったと考えられます。

現実的には、特定の製品カテゴリに絞って別々のプロファイルを作った方が理にかなっているかもしれないとノートブック内では締められています。

# CN04: Deploy Content-Base  Recommender
デモ用のアーキテクチャをパイプラインと呼ばれるデータ取得・処理を自動化するシステムを用いて構築します。

専用のテーブルを用意し、特徴量の生成を行った後に類似度を計算し、レコメンドを表示する流れについて見ていきます。

## Step1: Assemble Product Feature Pipelines
商品データ（タイトル・説明・カテゴリ）から得られる特徴のセットを得るための変換方法をパイプラインに保存しています。

タイトルから特徴量の変換手順を示し、特徴量セットを得るところまで行います。この手順をmlflowという機能を用いて保存することができます。

説明文やカテゴリについても同様の手順を行います。

## Step2: Generate Product Features
データセットからの特徴量を作成します。

データ取得と自動化の設定を行う

## Step3: Generate User Profiles
ユーザー特徴量の作成を行います。

## Step4a: Create Product-Based Recommendations
キャッシュを作成し、レコメンド表示を高速化する手段もあります。キャッシュを保存できDatabricksと接続できるDBとしては、以下のようなものがあります。
- Redis
- MongoDB
- CouchBase
- Azure CosmosDB
- AWS DocumentDB

ここでは単純にあるカテゴリ内の商品に対して関連性の高い商品を取得する方法を見ていきます。

まず、どの商品に対してレコメンドを行うかを決めます（ここではカテゴリを1つに限定する）。次に商品の特徴量を用いて類似度を計算します。

先ほども述べた通り、組み合わせが多くなりすぎないように類似度の比較はバケット内のみで比較する制限が設けられています。同じカテゴリの商品内で行う、あるいは関数を利用して比較する商品の数を制限するといった実装も考えられるでしょう。

## Step4b: Create Profile-Based Recommendations
ユーザーを選び、そのユーザーとの関連性が高い上位25品を表示します。

![2023-05-11_19h11_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327019/b024802d-350e-781e-f704-a7627b9a847d.png)

# まとめ
今回は Databricks 公式のノートブックの中から、レコメンド作成システムの内容について解説しました。

このノートブック内ではかなりの量のライブラリを使用していますが、いずれも Databricks で用意されているものであり、機械学習関連での Databricks の便利さが分かる内容になっています。

これを試した後は、別のアプローチ(協調フィルタリング)でレコメンドを作成するノートブックも同梱されていますし、 [Databricks 公式ブログ](https://www.databricks.com/blog)からサンプルのノートブックを探すのもよいでしょう。