このエントリーをはてなブックマークに追加
前回記事 CMSとレコメンドとApache Mahout の続きです。
writeWiredにレコメンド機能を実装していますが、協調フィルタリングのレコメンドエンジンとしてApache Mahoutを採用しました。実装の過程でわかった、他の方にも参考になりそうな方法をサンプルをご紹介します。

 

まずはMahoutの基本をおさらい

まずはMahoutの基本をおさらいします。まずはレコメンドする元にデータで以下のようなフォーマットです。

最初の1列目がユーザーID、次がアイテムID,最後がアイテムへの評価になります。
1行目なら「ユーザーIDが1の人が101の商品について5.0の評価をした」というものです。
サンプルデータ
1,101,5.0
1,102,3.0
1,103,2.5

2,101,2.0
2,102,2.5
2,103,5.0
2,104,2.0

3,101,2.5
3,104,4.0
3,105,4.5
3,107,5.0

4,101,5.0
4,103,3.0
4,104,4.5
4,106,4.0

5,101,4.0
5,102,3.0
5,103,2.0
5,104,4.0
5,105,3.5
5,106,4.0
Mahoutのレコメンドにはユーザーベース、アイテムベースの二種類があります。基本的なコードは以下になりとても簡単です。

ユーザーベース(あなたに似た人が見てるコンテンツをレコメンド)

// データを読み込み
DataModel model = new FileDataModel(new File("C:/mahout/recommend_sample1.csv"));
// ユーザー間の類似性指標の決定
UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
// 近傍ユーザーの指定
UserNeighborhood neighborhood = new NearestNUserNeighborhood(2, similarity, model);
// レコメンド
Recommender recommender = new GenericUserBasedRecommender(model, neighborhood, similarity);
// IDが1の人に1つレコメンド。
List<RecommendedItem> recommendations = recommender.recommend(1, 1);
for (RecommendedItem recommendation : recommendations) {
     System.out.println(recommendation);
}
結果
RecommendedItem[item:104, value:4.257081]

アイテムベース(今見ているアイテムに似ているアイテムをレコメンド)

// データを読み込み
DataModel model = new FileDataModel(new File("C:/mahout/recommend_sample1.csv"));
// アイテム間の類似性指標の決定
ItemSimilarity similarity = new PearsonCorrelationSimilarity(model);
// レコメンド
GenericItemBasedRecommender recommender = new GenericItemBasedRecommender(model, similarity);
// アイテムIDが101に近いアイテム
List<RecommendedItem> recommendations = recommender.mostSimilarItems(101, 1);
for (RecommendedItem recommendation : recommendations) {
     System.out.println(recommendation);
}
結果
RecommendedItem[item:102, value:0.9449112]

アイテムベース(あなたにアイテムをレコメンド)

// データを読み込み
DataModel model = new FileDataModel(new File("C:/mahout/recommend_sample1.csv"));
// アイテム間の類似性指標の決定
ItemSimilarity similarity = new PearsonCorrelationSimilarity(model);
// レコメンド
GenericItemBasedRecommender recommender = new GenericItemBasedRecommender(model, similarity);
// IDが1の人に1つレコメンド。
List<RecommendedItem> recommendations = recommender.recommend(1, 1);
for (RecommendedItem recommendation : recommendations) {
     System.out.println(recommendation);
}    
結果
RecommendedItem[item:104, value:4.257081]
※このレコメンドは動くのですが、引数は人のID(サンプルでは1)しか渡してないので、どんなロジックなんだろうと少し不思議です。
ユーザー、アイテムベースともに類似性指標には以下のようなクラスが用意されているので、それを切り替えるだけ動きます。
  • PearsonCorrelationSimilarity
  • EuclideanDistanceSimilarity
  • LogLikelihoodSimilarity
  • EuclideanDistanceSimilarity

評価が無いときの処理

Mahoutの世界ではサンプルデータで言うところの評価をプリファレンスと呼びます。このプリファレンスが無い時、例えばサイトのアクセスログの場合は評価がありません。このようなデータを使う場合はGenericBooleanPrefDataModelというクラスを使ってロードします。この方法を使うとMahoutの内部ではデータ構造がシンプルになり性能が向上するそうです。

// データを読み込み
DataModel model = new GenericBooleanPrefDataModel(
        GenericBooleanPrefDataModel.toDataMap(new FileDataModel(new File("C:/mahout/recommend_sample1.csv"))
);
 

学習に使えるデータ

org.apache.mahout.cf.taste.impl.model.file.FileDataModel

上記のサンプルソースで使っていたcsv形式のファイルを学習用データとして使うことができます。
 

org.apache.mahout.cf.taste.model.JDBCDataModel

JDBC経由で学習用データをロードします。MahoutのデフォルトではMySQL用があるようですが、自分で拡張することも出来ます。

さて、ここまで書きましたが、MahoutをHadoop上で動かす時はコマンドラインからHadoopジョブとして実行します。
その際には学習データはHDFSに配置、レコメンドの結果もHDFSに出力されるので、それをまた自分のシステムに戻して利用できる形にする必要があります。

今回はHadoopは使わずにJDBCDataModelを独自に実装します。

独自のアクセスログを学習用データとして使う、JDBCDataModelを実装する

まず実装にあたり、以下のように整理しました。
 
  1. Mahoutに十分なリソースを確保できるよう、別のサーバーに配置する。
  2. MahoutはユーザーID,アイテムID、プリファレンスの形式なので、アクセスログにあるユーザー情報(Cookie)などをMahout用にユニークな番号に振りなおす。
  3. writeWiredのアクセスログは複数サイトのログが含まれるので学習するときはそのサイトだけのデータに限定したい。
MahoutでのJDBCDataModelを使う場合の標準のテーブルレイアウトでMySQL用などはこのレイアウト前提で実装されています。
 

Mahoutデフォルトの学習用データのテーブルレイアウト

CREATE TABLE taste_preferences (
    user_id BIGINT NOT NULL,
    item_id BIGINT NOT NULL,
    preference REAL NOT NULL,
    PRIMARY KEY (user_id, item_id)
)

デフォルトのテーブルではなく、独自のwriteWiredのアクセスログから必要な情報だけを持つテーブルを用意して、Mahoutのデータ学習にはこのテーブルを読み込ませまてアクセスログの情報を使って抽出します。モデル構築時にレコメンド情報を識別するレコメンドID、というものを渡してあげてレコメンドIDに関連するサイトのアクセスログだけを学習に使う、というように制御します。
 

独自の学習用データのテーブルレイアウト

create table TWWACCESSLOG(
    SITECD TEXT,                    -- writeWiredのサイト識別
    COOKIE TEXT,                    -- writeWiredのユーザー情報
    CONTID DEC(11,0),                -- writeWiredのコンテンツ情報
    REQ_TIME TIMESTAMP,                -- writeWiredのアクセス日時
    USERID DEC(11,0),                -- Mahout用の取り込み時に付与されたユーザーID
    ITEMID DEC(11,0),                -- Mahout用の取り込み時に付与されたアイテムID
    PREF FLOAT                        -- Mahout用のプリファレンス(未使用)
);

イメージ

イメージはこんな感じです。
複数のサイトのログが入った一つのテーブルを元にレコメンド別によって学習時に使うデータを分けます。

MyMahoutJDBCDataModel.java
AbstractJDBCDataModelを親クラスにして内部で呼ばれるSQLを上書きするだけのクラスです。一つにまとめてもよかったのですが、わかりやすくするためにロジックと分けました。

import javax.sql.DataSource;

import org.apache.mahout.cf.taste.impl.model.jdbc.AbstractJDBCDataModel;
import org.apache.mahout.cf.taste.model.JDBCDataModel;

public class MyMahoutJDBCDataModel extends AbstractJDBCDataModel implements JDBCDataModel {
    private static final long serialVersionUID = 1L;
    
    private boolean usePreferenceValues = false;

    protected MyMahoutJDBCDataModel(DataSource dataSource,
            String preferenceTable,
            String userIDColumn,
            String itemIDColumn,
            String preferenceColumn,
            String getPreferenceSQL,
            String getPreferenceTimeSQL,
            String getUserSQL,
            String getAllUsersSQL,
            String getNumItemsSQL,
            String getNumUsersSQL,
            String setPreferenceSQL,
            String removePreferenceSQL,
            String getUsersSQL,
            String getItemsSQL,
            String getPrefsForItemSQL,
            String getNumPreferenceForItemSQL,
            String getNumPreferenceForItemsSQL,
            String getMaxPreferenceSQL,
            String getMinPreferenceSQL,boolean usePreferenceValues) {
        super(dataSource,preferenceTable,userIDColumn,itemIDColumn,preferenceColumn,
                getPreferenceSQL,getPreferenceTimeSQL,getUserSQL,getAllUsersSQL,getNumItemsSQL,
                getNumUsersSQL,setPreferenceSQL,removePreferenceSQL,getUsersSQL,getItemsSQL,getPrefsForItemSQL,
                getNumPreferenceForItemSQL,getNumPreferenceForItemsSQL,getMaxPreferenceSQL,getMinPreferenceSQL);
        this.usePreferenceValues = usePreferenceValues;
    }
    
    @Override
    public boolean hasPreferenceValues() {
        // Mahoutがモデルを生成する場合にプリファレンス有り無しを判別するのに使用されます。
        // true:GenericDataModel(delegate.exportWithPrefs())
        // false:GenericBooleanPrefDataModel(delegate.exportWithIDsOnly());
        return usePreferenceValues;
    }
}

MyMahoutJDBCDataModelFactory.java

Mahoutがモデルを生成する際にプリファレンスを読み込むロジックを上書きします。

import javax.sql.DataSource;

import org.apache.mahout.cf.taste.model.JDBCDataModel;

public class MyMahoutJDBCDataModelFactory {
    
    protected DataSource dataSource = null;
    protected String sitecd = "";
    protected Long recommid = 0L;
    
    private String where = "";
    private String andWhere = "";
    
    //    使うテーブルとカラムIDを指定
    private String preferenceTable = "TWWACCESSLOG";
    private String userIDColumn = "USERID";
    private String itemIDColumn = "ITEMID";
    private String preferenceColumn = "PREF";
    private String timestampColumn = "REQ_TIME";
    
    // Mahoutがデータをロードする時に使用するSQL
    private String getPreferenceSQL = "";
    private String getPreferenceTimeSQL = "";
    private String getUserSQL = "";
    private String getAllUsersSQL = "";
    private String getNumItemsSQL = "";
    private String getNumUsersSQL = "";
    private String setPreferenceSQL = "";
    private String removePreferenceSQL = "";
    private String getUsersSQL = "";
    private String getItemsSQL = "";
    private String getPrefsForItemSQL = "";
    private String getNumPreferenceForItemsSQL = "";
    private String getNumPreferenceForItemSQL = "";
    private String getMaxPreferenceSQL = "";
    private String getMinPreferenceSQL = "";
    
    /**
     * コンストラクタ
     * @param dataSource
     * @param sitecd
     * @param recommid
     */
    public MyMahoutJDBCDataModelFactory(DataSource dataSource,String sitecd, long recommid) {
        this.sitecd = sitecd;
        this.recommid = recommid;
        this.dataSource = dataSource;
        init();
    }
    
    /**
     * 初期処理 
     */
    private void init() {
        
        // 読み込むログを限定する処理を実装。
        // このサンプルではロードを開始する前に学習対象ユーザーを別テーブルに書き出しているので対象を
        // 絞りこんでいます。
        String exists = 
                " and exists(select ITEMID from TRECOMMENDLEARNINGUSERID "
                + "where TRECOMMENDLEARNINGUSERID.RECOMMID="+recommid+" and TWWACCESSLOG.USERID = TRECOMMENDLEARNINGUSERID.USERID)";
        // サイトを限定する
        where = " where SITECD = '" + sitecd + "'";
        andWhere = " and SITECD = '" + sitecd + "'";
        
        // exists,where,andWhereを読み込むSQLに追加する
        getPreferenceSQL = 
            "select " + preferenceColumn + " from " + preferenceTable + " where " + userIDColumn + "=? and " + itemIDColumn + "=?" + andWhere;
        
        getPreferenceTimeSQL = 
            "select " + timestampColumn + " from " + preferenceTable + " where " + userIDColumn + "=? and "
                + itemIDColumn + "=?" + andWhere;

        getUserSQL = 
            "select distinct " + userIDColumn + ", " + itemIDColumn + ", " + preferenceColumn + " from " + preferenceTable
                + " where " + userIDColumn + "=? " + andWhere + "order by " + itemIDColumn;
        
        getAllUsersSQL =
            "select distinct " + userIDColumn + ", " + itemIDColumn + ", " + preferenceColumn + " from " + preferenceTable
                + where + exists
                + " order by " + userIDColumn + ", " + itemIDColumn;
        
        getNumItemsSQL = 
            "select count(distinct " + itemIDColumn + ") from " + preferenceTable + where + exists;
        
        getNumUsersSQL = 
            "select count(distinct " + userIDColumn + ") from " + preferenceTable + where + exists;
        
        setPreferenceSQL = 
            "INSERT INTO " + preferenceTable + '(' + userIDColumn + ',' + itemIDColumn + ',' + preferenceColumn
                + ") VALUES (?,?,?)";
        
        removePreferenceSQL = 
            "DELETE from " + preferenceTable + " where " + userIDColumn + "=? and " + itemIDColumn + "=?";
        
        getUsersSQL = 
            "select distinct " + userIDColumn + " from " + preferenceTable  + where + exists + " order by " + userIDColumn;
        
        getItemsSQL = 
            "select distinct " + itemIDColumn + " from " + preferenceTable  + where + exists + " order by " + itemIDColumn;
        
        getPrefsForItemSQL = 
            "select distinct " + userIDColumn + ", " + itemIDColumn + ", " + preferenceColumn + " from " + preferenceTable
                + " where " + itemIDColumn + "=? " + andWhere + exists  + " order by " + userIDColumn;
        
        getNumPreferenceForItemSQL = 
            "select count(1) from " + preferenceTable + " where " + itemIDColumn + "=?" + andWhere;
        
        getNumPreferenceForItemsSQL = 
            "select count(1) from " + preferenceTable + " tp1 JOIN " + preferenceTable + " tp2 " + "USING ("
                + userIDColumn + ") where tp1." + itemIDColumn + "=? and tp2." + itemIDColumn + "=?" + andWhere;
        
        getMaxPreferenceSQL = 
            "select max(" + preferenceColumn + ") from " + preferenceTable + where + exists ; 
        
        getMinPreferenceSQL = 
            "select min(" + preferenceColumn + ") from " + preferenceTable +  where + exists  ;

        // updateは使わない
//        updatePreferenceSQL = 
//            "UPDATE " + preferenceTable + " SET " + preferenceColumn + "=? WHERE " + userIDColumn
//                + "=? AND " + itemIDColumn + "=?";    
    }

    /**
     * プリファレンスを使用するモデルを生成
     * @return
     */
    public JDBCDataModel create() {
        return new MyMahoutJDBCDataModel(dataSource,preferenceTable,userIDColumn,itemIDColumn,preferenceColumn,
                getPreferenceSQL,getPreferenceTimeSQL,getUserSQL,getAllUsersSQL,getNumItemsSQL,
                getNumUsersSQL,setPreferenceSQL,removePreferenceSQL,getUsersSQL,getItemsSQL,getPrefsForItemSQL,
                getNumPreferenceForItemSQL,getNumPreferenceForItemsSQL,getMaxPreferenceSQL,getMinPreferenceSQL,true);
        
    }
    /**
     * プリファレンスを使用しないモデルを生成
     * @return
     */
    public JDBCDataModel createBooleanPref( ) {
        return new MyMahoutJDBCDataModel(dataSource,preferenceTable,userIDColumn,itemIDColumn,preferenceColumn,
                getPreferenceSQL,getPreferenceTimeSQL,getUserSQL,getAllUsersSQL,getNumItemsSQL,
                getNumUsersSQL,setPreferenceSQL,removePreferenceSQL,getUsersSQL,getItemsSQL,getPrefsForItemSQL,
                getNumPreferenceForItemSQL,getNumPreferenceForItemsSQL,getMaxPreferenceSQL,getMinPreferenceSQL,false);
        
    }
}

使用するサンプル

モデルを生成するところ以外はこれまでのコードと変わりません。

        // データソースはjavax.sql.DataSource。このサンプルではPGSimpleDataSourceを使用
        PGSimpleDataSource dataSource = new PGSimpleDataSource();
        dataSource.setServerName("hostname");

        long recommid = 3;    // アプリで使うレコメンドの識別子
        
        // 自分で作ったDataModelを生成する
        MyMahoutJDBCDataModelFactory factory = 
                    new MyMahoutJDBCDataModelFactory(dataSource, "sample", recommid);
        
        DataModel model = null;
        
        // プリファレンスを使用しない場合はGenericBooleanPrefDataModel経由でモデル生成
        model = new GenericBooleanPrefDataModel(
                    GenericBooleanPrefDataModel.toDataMap(
                                new ReloadFromJDBCDataModel(factory.createBooleanPref())
                                )
                );
        // プリファレンスを使う場合はReloadFromJDBCDataModel経由でモデル生成
        model = new ReloadFromJDBCDataModel(factory.create());
        
        
        // 後はこれまでと同じです。
        
        // ユーザー間の類似性指標の決定
        UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
        // 近傍ユーザーの指定
        UserNeighborhood neighborhood = new NearestNUserNeighborhood(2, similarity, model);
        // レコメンド
        Recommender recommender = new GenericUserBasedRecommender(model, neighborhood, similarity);
        // IDが1の人に1つレコメンド。
        List<RecommendedItem> recommendations = recommender.recommend(1, 1);
        for (RecommendedItem recommendation : recommendations) {
             System.out.println(recommendation);
        }        

Mahoutは資料が少ない。

Mahoutを拡張するための資料が日本語はもちろん英語も少ないので、こうしてコードを読んでみるとあっさり出来ましたが、実際はMahoutのソースを解析したりして、割と苦戦しました。書籍だと有名なMahoutイン・アクションがありますが、こういうことが出来るよ、という紹介にとどまっているのと、Mahout自体のバージョンアップの頻度が高いので最新のもので参考にするとたまに動かなかったりクラスそのものが無かったりします。ご本家のサイトでもう少しサンプルを拡充してもらえれば・・・。 オライリー・ジャパン Mahoutイン・アクション

次回は

一旦ここまでで、Mahoutを実際に活用する場合の「学習用のデータを都度作るのは面倒くさい」というのは解決できました。
次回は「このコンテンツはレコメンドさせたくない」という方法をご紹介します。

このコンテンツは参考になりましたか?

送信する

デジタルマーケティングに対する
実体調査アンケート実施中

是非、アンケートにぜひご協力ください。

デジタルマーケティングに対する
実体調査アンケート実施中

是非、アンケートにぜひご協力ください。

 アンケートに回答する  ≫

無料eBookのダウンロード

【今からでも遅くない!】
初心者に贈る「インバウンドマーケティング」の始め方

 無料ダウンロードはこちら  ≫

人気の記事

タグ別