2013/08/16更新

[レコメンド] 類似ユーザーベース協調フィルタリングを用いたレコメンドシステムを実装してみる

このエントリーをはてなブックマークに追加      

こんにちは、@yoheiMuneです。
先日の記事で紹介させて頂いた類似度の計測(ユークリッド距離を用いた類似性の測定ピアソン相関を用いた類似度の計測)を用いて、簡単なレコメンドシステムを実装してみたいと思います。

画像



今回利用するデータ

以前の記事でも利用している、ユーザーの映画に対する評価データを用いたいと思います。

■ユーザー

林、 伊藤、 大原、 柳沢、 久保、 大内、 斉藤

■映画

① パイレーツinブリリアン
② 滝の中で
③ スターウエアーズ
④ サンバでGO
⑤ 世界の端でアイっと叫ぶ
⑥ここはどこ

■ユーザーの映画に対する評価(例)


1 8 4 3
伊藤 2 3 5 7 10
大原 5 4 5 1
柳沢 10 9 9 3
久保 10 4 3 8
大内 7 7 2 3
斉藤 10 10 1 8

※空欄は、評価していないことを示します。

※上記データは例で、後述で上記データを今回はランダムで生成します。




今回作ってみる簡易的なレコメンドシステムの内容

今回実装するレコメンドシステムは、類似ユーザーベース協調フィルタリングを用いたレコメンドシステムです。 内容としては、評価の似ているユーザー(=類似度ユーザー)の評価を参考にして、レコメンド対象者がまだ評価していない映画のうち、興味ありそうな映画を評価します。

今回のレコメンド対象者は、「久保さん」としたいと思います。 久保さんは、「②滝の中で」と「⑥ここはどこ」の2つの映画が未評価ですが、どちらがより久保さんに推薦すべき結果になるでしょうか。



レコメンド対象者とその他の人の類似度を計測する

まずは、レコメンド対象者とその他の人の類似度を計測します。 今回は、以前の記事で紹介させて頂いたユークリッド距離を類似度計測に用いたいと思います。
以前の記事で定義した「simDistance」メソッドを用いて、類似度を計測します。出てくる変数のうち、以下コードで定義していないものは、以前の記事で定義したものとさせてください。
// Map<ユーザー, Map<映画名, 評価値>>
Map<String, Map<String, Integer>>prefs = /*ユーザーの映画評価が入ったMap*/;
String user = "久保";

// 対象者を1件ずつ処理する
for (String other : prefs.keySet()) {

  // 自分同士はスキップ
  if (other.equals(user)) continue;

  // 自分との類似度を計算する
  double sim = this.simDistance(prefs, user, other, algoType);
  System.out.println(user + "," + other + ": " + sim);
}
この結果、以下のような類似度であることが分かります。
久保,伊藤: 0.1715728752538099
久保,柳沢: 0.2360679774997897
久保,林: 0.125
久保,大原: 0.2360679774997897
久保,大内: 0.2679491924311227
久保,斉藤: 0.20560464675956824
この中では、久保さん&大内さんペアが一番類似度が高いようです。
通常であれば類似度計算してTop5人とかTop10人とかのデータのみを使うかと思いますが、 今回はデータ数が少ないので、後続の処理では上記全員分のデータを利用します。



類似度を考慮した映画の評価を決定する

上記の類似度の一番高い人の映画をレコメンドすることも可能ですが、その人の思考が偏っていた場合(アクション映画がすごい好きとか)には、 久保さんの心に刺さるレコメンドが出来ない可能性があります。

そのため以下のように各人の評価と類似度を掛け合わせた結果を、レコメンドするか否かの判断に使うことで、評価者の好みによるぶれを小さくします。

評価者 類似度 Sim × ② Sim × ⑥
0.13 3 0.39
伊藤 0.17 3 0.51 7 1.19
大原 0.24 5 1.20
柳沢 0.24 10 2.40 9 2.16
大内 0.27 7 1.89
斉藤 0.21 10 2.1
合計 6.90 4.94
類似度合計 0.89 0.78
合計/類似度合計 7.75 6.33

今回の結果では、「②滝の中で」の映画の方が、より久保さんにお勧めという結果となりました。 ただし、今回のレコメンド結果が本当に正しいレコメンド内容かは、検証して調整していく必要があります。



上記のテーブルの内容を実装にすると

実装に落とすと、以下のようなコードなります。
protected List<Score> getRecommendations(Map<String, Map<String, Double>> prefs, String user, SimAlgorithm algoType) {
  System.out.println("レコメンド開始!");
  
  // 類似度で重み付けした映画評価の合計
  Map<String, Double> totals = new HashMap<String, Double>();

  // 類似度の合計
  Map<String, Double> simSums = new HashMap<String, Double>();
  
  // 評価者を1人ずつ
  for (String other : prefs.keySet()) {
    
    // 自分同士はスキップ
    if (other.equals(user)) continue;
    
    // 自分との類似度を計算する
    // simDistanceは以前のブログで紹介した類似度計測を利用します
    double sim = this.simDistance(prefs, user, other, algoType);
    
    // まだ見ていないアイテムのみ得点を加算する
    Map<String, Double> myMovieScores = prefs.get(user);
    Map<String, Double> otherMovieScores = prefs.get(other);
    for (String otherMovie : otherMovieScores.keySet()) {
      if (myMovieScores.keySet().contains(otherMovie) == false) {

        double total = 0.0, simSum = 0.0;
        if (totals.containsKey(otherMovie) == true) {
          total += totals.get(otherMovie);
          simSum += simSums.get(otherMovie);
        }
        total += otherMovieScores.get(otherMovie) * sim;
        simSum += sim;
        
        totals.put(otherMovie, total);
        simSums.put(otherMovie, simSum);
      }
    }
  }
  
  // debug
  System.out.println("totals: " + totals);
  System.out.println("simSums: " + simSums);
  
  // 正規化したリストを作る
  List<Score> scoreList = new ArrayList<Recommend.Score>();
  for (String movie : totals.keySet()) {
    double ranking = totals.get(movie) / simSums.get(movie);
    scoreList.add(new Score(movie, ranking));
  }
  
  // スコア良い順にソート
  Collections.sort(scoreList, new Comparator<Score>() {
    @Override public int compare(Score o1, Score o2) {
      return (int)(o1.score - o2.score) * -1;
    }
  });

  // スコアリストを返す
  return scoreList;
}
上記で利用しているScoreクラスは以下の内容です。
public class Score {
  public String itemName;
  public double score;
  
  public Score() {
    super();
  }

  public Score(String itemName, double score) {
    super();
    this.itemName = itemName;
    this.score = score;
  }

  public String toString() {
    return itemName + ": " + score;
  }
}

そして上記のレコメンドメソッドを使うと、久保さんへの映画のレコメンドを行うことが出来ます。
// ユーザーベース強調フィルタリング(ユークリッド距離)を用いてレコメンドを行う
List scoreList = this.getRecommendations(rawDataMap, "久保", SimAlgorithm.Euclidean);
System.out.println("レコメンド結果: " + scoreList);
実行結果は、以下のような感じとなります。
レコメンド結果: [滝の中で: 7.724841374558819, ここはどこ: 4.794333368781029]

これでユーザーの評価を元にしたレコメンド機能の実装が完了です。



参考資料

この記事では、以下の書籍・Webの内容を参考にしております。良き情報に感謝です。

- Amazon:集合知プログラミング
- 協調フィルタリングについてまとめてみた。 - Analize IT.


最後に

簡単ですがレコメンド機能の実装紹介をブログに書きました。
実際に使う場合には、類似度の計測には何を使うか、○○状況の人は省くべきか、データが疎な場合にどうするか、など色々な調整が必要になるかと思います。 色々と試しつつ、有益なレコメンド機能が作れるようになりたいと思う今日この頃でした。

最後までご覧頂きましてありがとうございました。






こんな記事もいかがですか?

RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。