Mahout in ActionのChapter3についての自分の理解をメモ。
嗜好データの表現
Preferenceオブジェクト
- Preferenceオブジェクトは最も基本的な概念で、一つのユーザID、アイテムID、嗜好値で嗜好性を表す
- 一つのオブジェクトが一人のユーザの一つのアイテムに対する嗜好性を表している
- 一つのGenericPreferenceは20バイトの有用なデータを保持しており、8バイトのユーザID(Javaのlong型)、8バイトのアイテムID(long)、4バイトの嗜好値(float)から成っている
- オブジェクトが存在するためには上記以外に28バイトものオーバーヘッドがかかる
PreferenceArrayと実装
- PreferenceArrayはPreferenceのコレクションを配列のように扱うためのインタフェース
- 例としてGenericUserPreferenceArrayは一つのユーザIDにアイテムIDの配列と嗜好値の配列を持つ
- 必要なメモリはわずか12バイト(8バイトのアイテムID、4バイトの嗜好値の配列)
- Preferenceオブジェクトと比べてメモリは節約できるがパフォーマンスの改善効果は少ない
- これはPreferenceArrayでは要素が分散されて配置され、ガベージコレクタによって評価されるため
FastByIDMapとFastIDSet
- Mahoutはmapやsetのようなデータ構造を使用するが、JavaのコレクションであるTreeSetやHashMapではなく、FastMap、FastByIDMap、やFastDISetを使用する
- これらはMapやSetのようなものであるが、MahoutのRecommenderが必要なものだけのために特化されている
- パフォーマンスの劇的な改善というよりむしろメモリの使用量を削減する
Javaのコレクションとの違いは下記のような点
・FastByIDMapはHashMapと同じようにハッシュベースで、ハッシュの衝突の解決には分離連鎖法ではなく線形探査法を使う
・MahoutのRecommenderではキーとメンバーは常にlong型のプリミティブ型でオブジェクトではない。
・longのキーを使うことでメモリの使用量を抑えパフォーマンスを改善する。
・FastByIDMapは最大サイズという考え方があるためキャッシュの様に使用することができ、このサイズを越えた場合は、新しい要素を追加する際に使われていない要素が削除される- FasByIDMapはエントリごとに28バイト使用する
インメモリデータモデル
GenericDataModel
- 最もシンプルなインメモリのDataModel実装はGenericDataModel
- GenericDataModelは嗜好度の入力としてユーザIDとPreferenceArrayのFastByIDMapを受け取る
FastByIDMap<PreferenceArray> preferences = new FastByIDMap<PreferenceArray>(); PreferenceArray prefsForUser1 = new GenericUserPreferenceArray(10); prefsForUser1.setUserID(0, 1L); prefsForUser1.setItemID(0, 101L); prefsForUser1.setValue(0, 3.0f); prefsForUser1.setItemID(1, 102L); prefsForUser1.setValue(1, 4.5f); ... (8 more) preferences.put(1L, prefsForUser1); DataModel model = new GenericDataModel(preferences);
- メモリの使用量は格納される嗜好データの数によるが、いくつかのテストの結果から、嗜好データ1つごとに28バイトのJava heap spaceが使われる。これにはすべてのデータとインデックスなどのデータ構造を含む。
ファイルベースのデータ
- FileDataModelはファイルからデータを読み込み、GenericDataModelとしてメモリに格納する
- CSVの他にTSVも使用可能。ファイル名の拡張子が.zipや.gzになっていればzipやgzipの圧縮ファイルも使用可能
コンポーネントのリフレッシュ
- データをリフレッシュするためには refresh(Collection
) というメソッドを使用する。 - このメソッドは最新のデータを元にコンポーネントの再読み込み、再計算、状態のリフレッシュを行う
- FileDataModelはその時点のファイルを読み込むだけで、パフォーマンスの問題で自動的にデータをリロードするようなことはしないため、refresh()メソッドを使用する
ファイルの更新
- FileDataModelはファイルの更新をサポートしている。メインのファイルを読み込んだ後でさらにファイルを読み込むことで先に読み込まれたデータを上書きする
- 削除は空の嗜好値データを渡すことによって行われる
1,108,3.0 1,103,
- 上記のデータではユーザ1のアイテム108についての嗜好度データが作成または更新されて嗜好度3.0がセットされ、ユーザ1のアイテム103についての嗜好度データが削除される
- このためにはメインのデータファイルと同じディレクトリに格納し、最初のピリオドまでのファイル名を同じにしておく必要がある
DBベースのデータ
- MahoutではRDBから嗜好度データを読み込むことができる
- DBからデータを読み込んでRecommenderを動かすのはかなり遅い
- データの抽出、並び替え、シリアライズ、転送、結果セットのデシリアライズのオーバーヘッドは依然として最適化されたインメモリ構造からデータを読み込むよりかなり大きい
JDBCとMySQL
- 嗜好度データにはJDBCDataModelの実装を通してJDBCでアクセスする
- JDBCDataModelのプライマリサブクラスはMySQL5.x用のMySQLJDBCDataModel
- Mahoutの開発バージョンにはPostgreSQL用のJDBCDataModel実装がある
- デフォルトではすべての嗜好度データはtaste_preferencesというテーブルに、ユーザIDが格納されるuser_idカラム、アイテムIDが格納されるitem_idカラム、嗜好度データが格納されるpreferenceカラムとともに格納されているとみなされる
- このテーブルにはJavaのlong型と互換性のあるtimestampカラムを含むことができる
JNDIによる設定
- JDBCDataModelはJNDIにjdbc/tasteという名前で登録されているDataSourceでアクセスできるものとみなされる
プログラムでの設定
- JNDIを直接使わなくても、MySQLJDBCDataModelのコンストラクタにDataSourceを直接渡すことができる
MysqlDataSource dataSource = new MysqlDataSource(); dataSource.setServerName("my_database_host"); dataSource.setUser("my_user"); dataSource.setPassword("my_password"); dataSource.setDatabaseName("my_database_name"); JDBCDataModel dataModel = new MySQLJDBCDataModel( dataSource, "my_prefs_table", "my_user_column", "my_item_column", "my_pref_value_column");
- 上記がデータベース内のデータをレコメンドに使うためのすべて
- MySQLJDBCDataModelのドキュメントが明確にしているように、効率的にレコメンドを提供するには下記のようなデータベースやドライバに対するコンフィギュレーションが求められる
・ユーザIDとアイテムIDカラムはnullを許容せず、インデックスされていること
・プライマリキーはユーザIDとアイテムIDの複合値であること
・カラムのデータ型はJavaのlongとfloatに対応していること
・バッファやクエリキャッシュのチューニングのためにはMySQLJDBCDataModelのJavadocを参照
・MySQLのConnector/Jドライバを使う場合は、cache-PreparedStatementsパラメータをtrueに設定する
嗜好度データ無しの対処
- ユーザとアイテムの関連はあるがつながりの強さを表す値がない嗜好度データを扱うことがある
- Mahout-speakではこのようなデータはBoolean preferencesと呼ばれ、”存在する” または ”存在しない” のいずれかの値を持つ
- これは”yes”、”no”を表すのではなく、全ての有効なユーザとアイテムの関連において、”好き”、”好きではない”、”なし”の3つの状態を設定する
データを無視する場合
- 好む、好まないというのが相対的に似たような状態の場合、少なくとも関連が全くないものと比較するケースでは、嗜好度データを無視することは有益である
嗜好度なしのインメモリ表現
- 嗜好度を持たないことは嗜好データの表現を劇的に単純化し、パフォーマンスの改善と、メモリ使用量の大幅な削減を可能にする
- 嗜好度を持たないことで一つの嗜好度データごとに4バイト抑えられるはずであるが、実際にテストをした結果では4バイトから24バイト削減された
- GenericBooleanPrefDataModelはGenericDataModelとは別のDataModel実装であるが、嗜好度を内部に保持せず、FastIDSetsのように関連のみを保持する
- DataModelのgetItemIDsForUser()などのいくつかのメソッドは速くなる
- getPreferencesFromUser()などのいくつかのメソッドは遅くなる。
- getPreferenceValue()メソッドは全てのケースにおいて1.0を返す。
- GenericBooleanDataModelの便利なメソッドであるtoDataMap()を使ってPreferenceArraysを要素としてもつFastByIDMapを、FastIDSetsを要素としてもつFastByIDMapに変換して、GenericBooleanDataModelの入力として渡すことが可能
互換性のある実装の選択
- EuclideanDistanceSimilarityなどは嗜好度なしで動かしても役に立つ結果は得られないため、嗜好度無しのデータでは動作しない
- 二つのデータが同じ値である場合、これらのピアソン相関は定義されない
- LogLikelihoodSimilarityは実際の嗜好度データに基づかない実装
- FileDataModelは入力データが嗜好度を含まない場合、自動的にGenericBooleanPrefDataModelを使用する
- MySQLBooleanPrefDataModelは嗜好度カラムを持たないデータベーステーブルを使用する場合に適している