はじめに
近年、多くのWebサービスやアプリでは、ユーザーごとにパーソナライズされた情報が当たり前のように表示されるようになりました。
推薦システムは膨大なユーザー行動データやコンテンツ情報をもとに、それぞれのユーザーに最適なアイテムを提示する技術です。
本記事では、その中でも「協調フィルタリング (Collaborative Filtering)」という定番の手法の一つである Implicit Matrix Factorization (IMF) に焦点を当て、実際にPythonで簡単なモデルを構築してみます。
さらに題材として、自分の趣味でもある登山のデータを使って、ユーザーごとの登山履歴から「次におすすめの山」を推薦する仕組みを作ってみました。
「登山×データ分析」という少しユニークな事例を通して、推薦アルゴリズムの仕組みや面白さを感じてもらえればと思います。
結果だけ見たい方はこちらへ → 確認してみる
推薦システムとは?
代表的な手法
皆さんは普段Webサービスやアプリを通じて、 あなたにおすすめの商品はこちら
や この作品を見た人はこんな作品も見ています
のような表示は見たことがあるでしょうか?
これらは 推薦システム と呼ばれるものが裏側で動いており、知らず知らずのうちにあなたの行動が学習され、行動に応じて表示が制御されています。
推薦システムで、実際に表示するものを選定する仕組みを 推薦アルゴリズム といいます。
推薦アルゴリズムにはさまざまな手法がありますが、大きく分けると以下のようなアプローチが代表的です。
- 内容ベースフィルタリング (Content-Based Filtering)
- 協調フィルタリング (Collaborative Filtering)
- モデルベース法
- メモリベース法
内容ベースフィルタリングは直感的な推薦手法で、アイテムの情報(ex. ジャンル、年代、出版社)とユーザーの情報(ex. ジャンル:SFが好き)という内容を基にして、ユーザーにアイテムをオススメします。
協調フィルタリングは、自分と似た特性を持つ別のユーザーの情報(行動)から傾向を把握し、それを利用することで推薦する手法です。
感覚的には、自分と趣向が似ている友人(=ユーザー)から作品(=アイテム)をオススメされるようなイメージです。(自分と友人が協調している)
お気づきになられた方もいらっしゃるかもしれませんが、協調フィルタリングと内容ベースフィルタリングの大きな違いはユーザー/アイテムの情報に依存するかしないかという点です。
協調フィルタリングは趣向が似ているユーザー群を何かしらの類似度によって割り出すため、アイテムの個別の事情を考慮しなくても良いというメリットがあります。
さらに協調フィルタリングには メモリベース法 と モデルベース法 という2つの手法に大別することができます。
メモリベース法はアイテムを推薦するまで、推薦ロジックを実行することはなくユーザーの情報をメモリに保存するだけに留めます。(故にメモリベース法と言われます)
モデルベース法は蓄積されたユーザーの行動データを基にデータの規則性を学習したモデルを事前に作成します。推薦時にはそのモデルと推薦対象のユーザーの情報を利用して選定します。(モデルを事前に構築することからモデルベース法と言われます)
協調フィルタリングのモデルベース法
本記事では、推薦アルゴリズムの中でも 協調フィルタリングのモデルベース法 について、取り上げます。また、実際に簡単な推薦システムを構築し、推薦される様子をお見せしたいと思います。
モデルベース法で利用されるモデルにはさまざまなものがあります。クラスタリングによる分類、非線形問題に帰着させて解く方法、トピックモデルを利用する方法、行列分解を用いる方法など多岐に渡ります。
今回はこれらの中から 行列分解 に着目します。(別記事でその他の手法についても解説する予定です!)
行列分解は、ユーザーとアイテムの行動履歴を低次元の潜在ベクトルで表現し、その内積で好みの度合いを予測するシンプルかつ強力な手法です。特に、星評価のような明示的なスコアがなくても、クリックや購入・視聴履歴といった行動ログを用いて学習する Implicit Matrix Factorization (IMF) という手法は、実サービスのレコメンドエンジンで広く使われています。今回はこのIMFを中心に取り上げ、仕組みと実装例を紹介します。
Implicit Matrix Factorization (IMF)
推薦システムで扱うデータには大きく分けて 明示的評価値 (Explicit Feedback) と 暗黙的評価値(Implicit Feedback) があります。
明示的評価値はユーザーが自ら付けたスコアやレビュー (★5評価など) のように、好みが数値で直接表現されたデータです。一方、暗黙的評価値は購入・クリック・視聴・滞在時間といった行動ログから間接的に好みを推定するデータを指します。実サービスではユーザーがわざわざ星評価を付けることは少なく、ほとんど暗黙的な行動ログであることが多いです。
IMFはこの暗黙的評価値に対して強い(良い精度が出やすい)推薦アルゴリズムです。(理由は後述)
では、具体的なその手法について見ていきましょう。
まず、ユーザーの行動データ $r_{ui}$をユーザー$u$がアイテム$i$に対して行動した回数とします。
そしてユーザーの 好意 を次のような2値変数として表現します。
\bar{r}_{ui} =
\begin{cases}
1 & r_{ui} \gt 0 \\
0 & r_{ui} = 0
\end{cases}
例えば、閲覧を行動として扱う場合、ユーザーaがアイテム3を2回閲覧した時 $r_{a3} = 2$となり、その好意は$\bar{r}_{a3}=1$となります。
さらにこの好意に対する 信頼度 を次で定めます。
c_{ui}=1+\alpha r_{ui}
ここで、$\alpha$は confidence scaling factor と呼ばれる値で、行動回数に応じてどの程度信頼して良いか(どの程度重く見るか)を表しています。
商品の購入を行動として扱えばこの$\alpha$は高く、閲覧であれば低くなるようなイメージです。
(実際はシステムに合わせてチューニングします)
ここまででモデルの構築に必要な定義は終わりです。
ここまでで登場する変数やパラメータの定義がそろったので、いよいよ IMFの最適化問題 を数式で表します。
IMFでは以下の目的関数を最小にするようなユーザー因子行列 $P\in\mathbb{R}^{|U|\times k}$ と アイテム因子行列 $Q\in\mathbb{R}^{|I|\times k}$ を学習します。
\min_{P, Q}
\sum_{u,i} c_{ui} \left(\bar{r}_{ui} - \mathbf{p}_u^\top \mathbf{q}_i \right)^2
+ \lambda \left(
\sum_u \|\mathbf{p}_u\|^2 +
\sum_i \|\mathbf{q}_i\|^2
\right)
この目的関数を最小化するのは解析的には難しく Alternating Least Squares (ALS) や確率的勾配降下法 Stochastic Gradient Descent のような数値最適化手法によってパラメータを学習します。(今回はALSのライブラリを用います、解法について明るくないので今後勉強しようと思います)
推薦するまでの流れ
具体的なケースで値を書き出してみると以下のようになります。
$r_{ui}$ (閲覧有無)
ユーザー\商品 | 商品A | 商品B | 商品C |
---|---|---|---|
ユーザー1 | 1 | 1 | 0 |
ユーザー2 | 0 | 1 | 1 |
ユーザー3 | 1 | 0 | 0 |
信頼度行列 $c_{ui}$ の例
閲覧行動に対して信頼度スケーリング係数 $\alpha=3$ とした場合
ユーザー\商品 | 商品A | 商品B | 商品C |
---|---|---|---|
ユーザー1 | 4 | 4 | 1 |
ユーザー2 | 1 | 4 | 4 |
ユーザー3 | 4 | 1 | 1 |
- $c_{ui}=1+\alpha r_{ui}$
潜在ベクトルの例 (潜在次元 $k=2$)
ユーザー因子行列 $P$
ユーザー | $p_{u,1}$ | $p_{u,2}$ |
---|---|---|
ユーザー1 | 0.9 | 0.2 |
ユーザー2 | 0.3 | 0.8 |
ユーザー3 | 0.7 | 0.1 |
アイテム因子行列 $Q$
商品 | $q_{i,1}$ | $q_{i,2}$ |
---|---|---|
商品A | 0.8 | 0.3 |
商品B | 0.4 | 0.7 |
商品C | 0.2 | 0.9 |
例えば、ユーザー1 × 商品C のスコアは
\begin{align}
\hat{r}_{1C}
&= \mathbf{p}_1^\top \mathbf{q}_C \\
&= p_{1,1}q_{C,1} + p_{1,2}q_{C,2} \\
&= 0.9 \times 0.2 + 0.2 \times 0.9 \\
&= 0.36
\end{align}
となります。
同様にしてユーザー1に対する各商品のスコアを計算し、スコアが高い順に推薦します。
このように、学習済みモデルでは $\mathbf{p}_u$と$\mathbf{q}_i$の内積計算だけでユーザーに合った商品を推薦することができます。
実装
ここからは、実際に登山データを使って推薦モデルを構築してみます。
まずは ヤマレコAPI を使って登山記録データを収集し、そのデータを基に IMF (Implicit Matrix Factorization) を学習して推薦モデルを作成します。
「APIからのデータ収集」から「行列分解モデルの学習・推論」まで、一連の流れを簡単なコードで実装していきます。
ヤマレコAPIから登山データを取得する
まずは、ヤマレコの公開APIを利用して登山記録データを収集します。
ヤマレコ Web API 山行記録
ヤマレコは日本最大級の登山記録共有サービスで、ユーザーが投稿した山行記録をAPI経由で取得できます。
APIの /getReclist/{page}
エンドポイントでは、日付順に並んだ登山記録 (ユーザーID、山名、エリア情報など) をページング形式で取得できます。
今回はこのAPIをありがたく利用させていただいて、
- APIから全ページの登山記録を取得
- 各記録からユーザーID・山名・エリア・日付など必要な情報を抽出 (不要な項目は削除)
- 山名を正規化し、ユーザーごとに「登った山の集合」を作成
- 結果を
user_climbed_mountains_dataset.json
に保存
を実行するpythonスクリプトを作成しました
(※ 取得スクリプトに関しては掲載しないのでご了承ください)
このスクリプトを実行すると、
{
"12345": ["富士山", "八ヶ岳"],
"67890": ["槍ヶ岳", "泉ヶ岳"],
"99999": ["高尾山"],
}
のようなユーザーと登山した山の名前の一覧データのjsonが取得できます。 (※ 上記のデータは例です)
IMFによる推薦ロジックを実装する
ここからは、先ほど取得した登山データを用いてIMFによる推薦ロジックを構築します。
今回はPythonライブラリのimplicitを利用して、ユーザー × 登山した山の行動データからALS (Alternating Least Squares) による行列分解モデルを学習します。
データの読み込み
まずは、先ほど作成した user_climbed_mountains_dataset.json
を読み込み、
ユーザーごとに登った山の一覧を取得します。
ここでは「登山記録がないユーザー」は除外しています。
with open("assets/user_climbed_mountains_dataset.json", encoding="utf-8") as f:
raw_data = json.load(f)
# 登山歴のないユーザーを除いておく
data = {u: v for u, v in raw_data.items() if len(v) > 0}
users = list(data.keys())
# => ex. users: ['704585', '70563', '427261', '161523', '466286']
mountains = sorted(set(m for v in data.values() for m in v))
# => ex. mountains: ['佐倉山', '佐幌岳', '依遅ヶ根山', '信貴山', '信越トレイル袴岳']
user_to_idx = {u: idx for idx, u in enumerate(users)}
mountain_to_idx = {m: idx for idx, m in enumerate(mountains)}
- JSONのキー(keys)がユーザーID、値(values)が登った山のリストです
- ユーザーと山のリストを作り、user_to_idx と mountain_to_idx でインデックスに変換します
この変換を行うことで、後の行列化が簡単になります。
ユーザー × 山 行列の作成
次に、ユーザー × 山の 疎行列(sparse matrix) を作成します。
(ほとんどの人は全ての山に対してほんの一部しか登らないためとても疎な行列になります)
この行列の1行は「ユーザー」、1列は「山」を表し、登ったことがあれば1を立てます。
rows, cols, vals = [], [], []
for user, climbs in tqdm(data.items(), desc="行列作成"): # tqdmはループの進捗可視化用
for mountain in climbs:
rows.append(user_to_idx[user])
cols.append(mountain_to_idx[mountain])
vals.append(1)
user_item_matrix = csr_matrix((vals, (rows, cols)), shape=(len(users), len(mountains)))
item_user_matrix = user_item_matrix.T.tocsr() # ALS学習用
-
csr_matrix
はSciPyの疎行列形式。メモリ効率良く保持することが可能みたいです -
vals
はすべて1で、「その山に登った」という事実を表します -
item_user_matrix
はALS学習のために転置した行列です
イメージは↓みたいな感じです (実際にはもっと行と列のサイズが増えて0の羅列に見える)
ユーザー\山 | 佐倉山 | 札幌岳 | 富士山 | ・・・ |
---|---|---|---|---|
ユーザー1 | 1 | 0 | 0 | ・・・ |
ユーザー2 | 0 | 0 | 1 | ・・・ |
ユーザー3 | 0 | 0 | 1 | ・・・ |
・・・ | ・・・ | ・・・ | ・・・ | ・・・ |
※ ユーザーと山はid
モデル学習
行列ができたら、ALSモデルを構築します。
ここでは以下のようなパラメータを設定しました。
- 潜在因子の次元$k=20$
- データサイズに応じて一般的に10〜100程度で調整。特に理由はないですが20次元にしてみました
- regularization=0.1
- 過学習防止のための正則化係数
- iterations=10
- ALSの反復回数
- confidence scaling factor $\alpha=40$
- Collaborative Filtering for Implicit Feedback Datasets (Hu et al., 2008)によると、40ぐらいが経験的にいいそうです
model = AlternatingLeastSquares(factors=20, regularization=0.1, iterations=10, random_state=42)
alpha = 40
confidence_matrix = (user_item_matrix * alpha).astype("double")
model.fit(confidence_matrix, show_progress=True)
推薦関数の実装
特定のユーザーに対して、まだ登っていない山を推薦する関数を作成します。
def recommend_for_user(user_id, topn=5):
# 推薦対象ユーザーの登山履歴を取得
user_history = user_item_matrix[user_idx]
# 推薦計算
recommended_ids, recommended_scores = model.recommend(
user_idx, user_items=user_history, N=topn
)
print(f"\nユーザー {user_id} へのおすすめ:")
for idx, score in zip(recommended_ids, recommended_scores, strict=False):
print(f" - {mountains[idx]}: {score:.3f}")
確認してみる
「高尾山」と「幌尻岳」に登ったことがあるユーザーを探して、その人に推薦を行ってみます。
# 特定の山に登ったユーザーを探す関数
def recommend_user_by_climbed_mountain(mountain="高尾山"):
target_users = [u for u, climbs in data.items() if mountain in climbs]
if target_users:
sample_user = target_users[0] # 最初のユーザーを選ぶ
print(f"\n ユーザー {sample_user} の登山歴")
print(data[sample_user])
recommend_for_user(sample_user)
else:
print(f"{mountain} に登ったユーザーが見つかりませんでした")
recommend_user_by_climbed_mountain("高尾山")
recommend_user_by_climbed_mountain("幌尻岳")
❯ uv run python src/recommend/imf_als.py
ユーザー 691208 の登山歴
['高尾山']
ユーザー 691208 へのおすすめ:
- 小仏城山: 0.542
- 陣馬山: 0.454
- 景信山: 0.426
- 硫黄岳: 0.316
- 鳳凰三山: 0.280
ユーザー 916375 の登山歴
['幌尻岳']
ユーザー 916375 へのおすすめ:
- 槍ヶ岳: 0.639
- 白馬岳: 0.432
- 小仏城山: 0.394
- 水晶岳: 0.389
- トムラウシ山: 0.385
高尾山の登山経験があるユーザーには、小仏城山や陣馬山といった都内からアクセスが良く、比較的体力的にも余裕のある低山が推薦されていることがわかりますね。
一方で日本でも屈指の難易度を誇る幌尻岳を登ったユーザーには、槍ヶ岳や白馬岳といった難易度の高い山が推薦されています。
次に登る山を決める
折角なので自分の登山歴から次に登る山を決めてみたいと思います。
data
に自分のデータを追加するだけでできます。
# 自分の登山歴を追加
my_user_id = "me"
my_climbs = ["高尾山", "筑波山", "大山", "茶臼岳", "金時山", "白山", "雲取山", "木曽駒ヶ岳"]
data[my_user_id] = my_climbs
# ~~
recommend_for_user(my_user_id, topn=5)
結果は....
▼
▼
▼
▼
▼
❯ uv run python src/recommend/imf_als.py
ユーザー me へのおすすめ:
- 富士山: 1.161
- 那須岳: 0.897
- 朝日岳: 0.816
- 小仏城山: 0.758
- 日光白根山: 0.721
終わりに
今回の記事では、登山データを例に Implicit Matrix Factorization (IMF) を用いた推薦システムの実装を紹介しました。
登山記録のような「明示的な評価がない」データでも、行動ログを元に好みや傾向を推定し、ユーザーごとに最適なアイテムを推薦できることが分かりました。
実際にモデルを作ってみると、高尾山のような初心者向けの山を登ったユーザーには近郊の低山が推薦され、
幌尻岳のような難関峰を登ったユーザーには槍ヶ岳や白馬岳などの高難度山が推薦され、アルゴリズムの仕組みが直感的にも納得できる結果になったのではないかと思います。
今回の実装はあくまで入門的なサンプルで、
- 行動履歴の重み付けや $\alpha$ のチューニング
- モデル評価指標(MAP@KやNDCGなど)
- ハイブリッド型の推薦(内容ベースとの組み合わせ)
などを導入することで、より実践的なシステムに発展させられると思います。
「行列分解」というシンプルな仕組みを理解しやすい題材で触れてみると、
サービスやアプリの裏で使われている技術を身近に感じられて楽しいですね。
この記事が、推薦システムやデータ分析に興味を持つきっかけになれば嬉しいです。
おまけ
これは去年の11月頃に行った高尾山
こっちは8月の白山です
毎度天気には恵まれている気がします
参考
- 推薦システム実践入門
- 初学者がCollaborative Filtering for Implicit Feedback Datasetsを読んでスクラッチ実装してみる
- GMO ユーザーの暗黙的フィードバック(Implicit Feedback)からオススメアイテムを推奨したい
- Yifan Hu, Yehuda Koren, Chris Volinsky.
Collaborative Filtering for Implicit Feedback Datasets.
Proceedings of the 2008 Eighth IEEE International Conference on Data Mining (ICDM), 2008.
https://doi.org/10.1109/ICDM.2008.22
謝辞
本記事では ヤマレコAPI を利用して登山データを取得・可視化しています。
登山記録の著作権は投稿者に帰属します。掲載しているデータはヤマレコのサービスを通じて取得したもので、出典元リンクを明記のうえ引用しています。