Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

内容ベースフィルタリング

概要

内容ベースフィルタリング(Content-Based Filtering) は、アイテムの属性(特徴量)とユーザーの過去の嗜好情報を用いて推薦を行う手法。

協調フィルタリングが「他のユーザーの行動」を利用するのに対し、内容ベースフィルタリングは アイテムそのものの特徴 に着目する。

  • 例:「ユーザーAがアクション映画を好んで観ている → 他のアクション映画を推薦する」

  • アイテムの属性(ジャンル、テキスト、カテゴリなど)からアイテムのプロファイルを構築し、ユーザーの嗜好プロファイルとの類似度でランキングする

基本的な枠組み

1. アイテムプロファイルの構築

各アイテム jj を特徴ベクトル xjRd\boldsymbol{x}_j \in \mathbb{R}^d で表現する。特徴量の例:

ドメイン特徴量の例
映画ジャンル、監督、出演俳優、あらすじのテキスト
ニュース記事カテゴリ、キーワード、TF-IDF
商品カテゴリ、価格帯、ブランド、商品説明文
音楽ジャンル、テンポ、アーティスト、音響特徴量

2. ユーザープロファイルの構築

ユーザー uu が過去に高評価したアイテム集合 Iu+I_u^+ から、ユーザープロファイル pu\boldsymbol{p}_u を構築する。

最もシンプルな方法は、高評価アイテムの特徴ベクトルの平均:

pu=1Iu+jIu+xj\boldsymbol{p}_u = \frac{1}{|I_u^+|} \sum_{j \in I_u^+} \boldsymbol{x}_j

3. 推薦スコアの計算

ユーザープロファイル pu\boldsymbol{p}_u と各アイテムの特徴ベクトル xj\boldsymbol{x}_j のコサイン類似度を計算し、スコアが高い順に推薦する:

score(u,j)=cos(pu,xj)=puxjpuxj\text{score}(u, j) = \cos(\boldsymbol{p}_u, \boldsymbol{x}_j) = \frac{\boldsymbol{p}_u \cdot \boldsymbol{x}_j}{\|\boldsymbol{p}_u\| \|\boldsymbol{x}_j\|}

テキスト特徴量の抽出

アイテムの説明文やタイトルなどのテキスト情報から特徴ベクトルを構築する代表的な手法。

TF-IDF

TF-IDF(Term Frequency - Inverse Document Frequency) は古典的だが内容ベースフィルタリングで広く使われている手法。

TF-IDF(t,d)=TF(t,d)×IDF(t)\text{TF-IDF}(t, d) = \text{TF}(t, d) \times \text{IDF}(t)
  • TF(t,d)\text{TF}(t, d): 文書 dd における単語 tt の出現頻度

  • IDF(t)=logN{d:td}\text{IDF}(t) = \log \frac{N}{|\{d : t \in d\}|}: 単語 tt のレア度(NN は全文書数)

Embedding

近年は事前学習済みの言語モデルによる埋め込みベクトルが主流になりつつある。

  • Word2Vec / fastText → 単語レベルの埋め込みを平均

  • Sentence-BERT → 文レベルの埋め込み

  • OpenAI Embeddings / その他のLLM埋め込み

TF-IDFと比較して、意味的な類似性 を捉えられるのが利点。

実装例:MovieLens + TF-IDF

import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

# MovieLens 100K のデータを取得
ratings = pd.read_csv(
    "http://files.grouplens.org/datasets/movielens/ml-100k/u.data",
    names=["user_id", "item_id", "rating", "timestamp"],
    sep="\t",
)

# 映画情報(ジャンルを特徴量として使う)
genre_cols = [
    "unknown", "Action", "Adventure", "Animation", "Children's", "Comedy",
    "Crime", "Documentary", "Drama", "Fantasy", "Film-Noir", "Horror",
    "Musical", "Mystery", "Romance", "Sci-Fi", "Thriller", "War", "Western",
]
movies = pd.read_csv(
    "http://files.grouplens.org/datasets/movielens/ml-100k/u.item",
    names=["item_id", "title", "release_date", "video_release_date", "imdb_url"] + genre_cols,
    sep="|",
    encoding="latin-1",
)
movies.head(3)
# ジャンル情報を文字列に変換してTF-IDFの入力にする
# 例: [0, 1, 0, 0, 0, 1, ...] → "Action Comedy"
movies["genre_text"] = movies[genre_cols].apply(
    lambda row: " ".join([g for g, v in zip(genre_cols, row) if v == 1]),
    axis=1,
)
movies[["title", "genre_text"]].head()
# TF-IDFでアイテムプロファイルを構築
tfidf = TfidfVectorizer()
item_profiles = tfidf.fit_transform(movies["genre_text"])

print(f"アイテムプロファイル行列: {item_profiles.shape}")
print(f"特徴量: {tfidf.get_feature_names_out()}")
def build_user_profile(user_id: int, ratings_df: pd.DataFrame, item_profiles, threshold: float = 4.0):
    """ユーザーが高評価(threshold以上)したアイテムの特徴ベクトルを平均してユーザープロファイルを構築"""
    user_ratings = ratings_df[ratings_df["user_id"] == user_id]
    liked_items = user_ratings[user_ratings["rating"] >= threshold]["item_id"].values

    # item_id は1始まりなのでインデックス調整
    liked_indices = [i - 1 for i in liked_items if i - 1 < item_profiles.shape[0]]

    if len(liked_indices) == 0:
        return None

    user_profile = item_profiles[liked_indices].mean(axis=0)
    return np.asarray(user_profile)


def recommend(user_id: int, ratings_df: pd.DataFrame, item_profiles, movies_df: pd.DataFrame, top_n: int = 10):
    """ユーザープロファイルとアイテムプロファイルのコサイン類似度で推薦"""
    user_profile = build_user_profile(user_id, ratings_df, item_profiles)
    if user_profile is None:
        return pd.DataFrame()

    scores = cosine_similarity(user_profile, item_profiles).flatten()

    # 既に評価済みのアイテムは除外
    rated_items = set(ratings_df[ratings_df["user_id"] == user_id]["item_id"].values)
    candidate_indices = [i for i in range(len(scores)) if (i + 1) not in rated_items]

    top_indices = sorted(candidate_indices, key=lambda i: scores[i], reverse=True)[:top_n]

    results = movies_df.iloc[top_indices][["item_id", "title", "genre_text"]].copy()
    results["score"] = [scores[i] for i in top_indices]
    return results.reset_index(drop=True)
# ユーザー1に対する推薦
user_id = 1

# ユーザーが高評価した映画を確認
user_liked = ratings[(ratings["user_id"] == user_id) & (ratings["rating"] >= 4)].merge(
    movies[["item_id", "title", "genre_text"]], on="item_id"
)
print(f"ユーザー{user_id}が高評価した映画({len(user_liked)}件、上位5件):")
print(user_liked[["title", "genre_text", "rating"]].head().to_string(index=False))
print()

# 推薦結果
recs = recommend(user_id, ratings, item_profiles, movies, top_n=10)
print(f"ユーザー{user_id}への推薦:")
recs

協調フィルタリングとの比較

観点内容ベースフィルタリング協調フィルタリング
必要なデータアイテムの属性情報ユーザーの行動履歴(評価、クリックなど)
コールドスタート(アイテム)属性さえあれば新規アイテムも推薦可能他のユーザーの評価がないと推薦できない
コールドスタート(ユーザー)ある程度の行動履歴が必要同様に行動履歴が必要
セレンディピティ低い(既知の嗜好に近いものばかり推薦される)高い(意外な発見がありうる)
スケーラビリティユーザー数に依存しないユーザー数×アイテム数に依存
ドメイン知識必要(特徴量設計)不要
フィルターバブル陥りやすい比較的陥りにくい

課題と発展

フィルターバブル問題

内容ベースフィルタリングは、ユーザーが過去に好んだアイテムと類似したものばかりを推薦するため、推薦の多様性が低下 しやすい。対策として:

  • 推薦結果に多様性制約を加える(MMR: Maximal Marginal Relevanceなど)

  • 協調フィルタリングとのハイブリッド化

ハイブリッド手法

実用的なシステムでは、内容ベースと協調フィルタリングを組み合わせた ハイブリッド手法 が一般的。

  • 重み付きハイブリッド: 両手法のスコアを線形結合する

  • スイッチング: 状況に応じて手法を切り替える(例:コールドスタート時は内容ベース、データが蓄積したら協調フィルタリング)

  • 特徴量結合: 内容ベースの特徴量を協調フィルタリングモデルの入力に加える

参考