Mahout 0.8でクラスタリング

こないだ「Mahout 0.8でレコメンデーションエンジンを作る」という、Mahoutの簡単なエントリーを書いたけど、今回はクラスタリングの話。

今回も、Mahoutの基本知識は知っていることを前提として書く。Mahout in Actionが入門向けで良いかと(ページ末尾参照)。

「こんなものも買っています」を出したい

用途は、ユーザーがあるアイテムを閲覧した時に、「このアイテムを買った人はこんなのも買ってます」みたいなのを出すこと。

ItemSimilarity?

前回のレコメンデーションエンジンの場合は、大雑把に以下の手順でオススメを作った。

  1. user – item – preferenceのデータをDataModelに読み込む
  2. ItemSimilarityかUserSimilarityを計算
  3. 類似度を元に、Recommenderがオススメを計算

今回の「こんなものも買ってます」を実装するにあたって、最初は、レコメンデーションエンジンのステップ2で作るItemSimilarityをそのまま使おうと思った。ステップ2で計算されたSimilarityの値が高いものから順に表示していけばそれで終わりかなと。

結果から言うと、自分達が持っている元データから計算したSimilarityだと使えそうなデータにはならなかった。一番問題だったのは、人気のアイテムの場合、別の人気のアイテムが表示されてしまったこと。そんなデータを使う位だったら単純に「売り上げランキング」とか「閲覧数ランキング」とかを見れば事が足りるので、わざわざ新たに「こんなものも買っています」機能を作る必要がないなと。

クラスタリング

持っている生データ

次に、クラスタリングを試してみることにした。(実際の本番データとは違うけど)以下の様なデータを持っていると仮定する。

  1. ユーザーのそのアイテムに対する評価(レコメンデーションで使われるpreferences)
  2. ユーザーがアイテムに対して自由にタグ付け出来る(「もののけ姫」だったら「アニメ」「ジブリ」「宮﨑駿」とか)
  3. アイテムの各種属性情報(ジャンル、値段、etc)

クラスタリングの流れ

各アイテムに対して、2と3の情報でベクトルを作成することにした。以下、話を簡略化のために、(運営側が作成する)ジャンルデータと(ユーザーが自由に作れる)タグデータを使う、という設定にする。

例えば、システム内部でジャンルが3個(アニメ、ホラー、その他)に分かれていて、ユーザーが今までに5種類のタグ(アニメ、宮﨑駿、スプラッター、デート向き、名作)を作成したとすると、3 + 5 = 8次元のベクトルが出来る。「もののけ姫」の場合、以下のようなデータがあると仮定する。

  • ジャンルが「アニメ」
  • 「アニメ」、「宮﨑駿」、「名作」のタグを付けた人がそれぞれ5,4,3人いたとする

この場合、もののけ姫の8次元空間での位置は(1, 0, 0, 5, 4, 0, 0, 3)となる。

これと同じ方法で各映画のベクトルを生成し、後はクラスタリングのアルゴリズムにかければOK。

ジャンルの1とタグの1は意味が違うけどいいの?って思う人がいるかもしれない(前者はbooleanのtrueという意味で1で、後者は人数、つまり数量)。それは正しい指摘なんだけど、その辺りもMahout in Actionに書いてあるので、本を読んで下さい。

プログラム

理屈だと簡単なんだけど、Mahout in Actionのクラスタリングの部分のサンプルプログラムが分かりにくくて結構悩んだ。

ベクトルの作成

ベクトルデータの実装は以下の3つがある。

  • DenseVector
  • RandomAccessSparseVector
  • SequentialAccessSparseVector

まぁ名前から分かると思うけど、最初のやつはデータが密なベクトル。全て(あるいはほぼ全て)の次元に値が入っている場合はこれを使う。残りの2つが疎なベクトル。今回は後者(疎なベクトル)。疎なベクトルにも2つあって、random accessかsequential accessかを選ぶんだけど、今回はsequential access でOK。

ということで、今回はSequentialAccessSparseVectorを使って以下のようにベクトルを作る。以下、コードサンプルはScalaだけど、Javaしか知らない人でも何となく分かるはず。

val features = new SequentialAccessSparseVector(8)

で、そのベクトルに値をセットしていく。

features.set(0, 1.0) // 「アニメ」ジャンル
features.set(3, 5.0) // 「アニメ」タグ
features.set(4, 4.0) // 「宮﨑駿」タグ
features.set(7, 3.0) // 「名作」タグ

ここまでが「もののけ姫」のデータ。これをNamedVectorというものに格納する。

val namedVector = new NamedVector(features, "もののけ姫")

これを各アイテムごとに実行し、それをjava.util.List等に格納しておく。

val allItems = new ListBuffer[NamedVector]() //全てのアイテムのベクトルを格納する入れ物
allItems += namedVector // もののけ姫のデータを格納

ファイルに書き出し

Mahout 0.6の頃だと、インメモリでクラスタリングを行うKmeansClustererを使うという選択肢もあったんだけど、Mahout 0.7でそのクラスが削除されたため、MapReduceを使わなければいけない。とは言え、Hadoopクラスターを作らなくても、pseudo modeでもOK。

MapReduceを使うので、ファイルに書き出す必要がある。

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.FileSystem
import org.apache.hadoop.fs.Path
import org.apache.hadoop.io.SequenceFile
import org.apache.mahout.math.VectorWritable

// 前準備
val conf = new Configuration
val fs = FileSystem.get(conf)
val outputDir = "outdir" // 出力先ディレクトリ
val vectorsFolder = new Path(outputDir, "vectors") // ベクトルの出力先
val centroidsFolder = new Path(outputDir, "centroids") // 
val clustersFolder = new Path(outputDir, "clusters") // 計算結果のクラスターの出力先

val writer = new SequenceFile.Writer(fs, conf, vectorsFolder, classOf[Text], classOf[VectorWritable])
val vectorWritable = new VectorWritable()

// 書き出し
for (vector <- allItems) {
  vectorWritable.set(vector)
  writer.append(new Text(vector.getName()), vectorWritable)
}

クラスタ計算の実行

あとは、中心点をランダムに選択して、K-means clusteringを実行する

val numOfIterations = 50 // 適当に
val k = 10 // クラスターの数
RandomSeedGenerator.buildRandom(conf, vectorsFolder, centroidsFolder, k,
    new CosineDistanceMeasure()) // 色々な距離関数を試すと良いかも

// クラスタリングの実行。結果はファイルに書き込まれる。
// 引数はjavadocを参照
KMeansDriver.run(vectorsFolder, centroidsFolder, clustersFolder,
    new CosineDistanceMeasure(), 0.01, numOfIterations, true, 0, true)

結果の読み込み

実行結果はファイルに出力されるので、それを読み込む。

// 手抜きしてファイル名決め打ちにしてる・・・
val reader = new SequenceFile.Reader(fs,
    new Path(clustersFolder, Cluster.CLUSTERED_POINTS_DIR + "/part-m-0"), conf)
val key = new Text()
val value = new WeightedVectorWritable()

while (reader.next(key, value)) {
  val namedVector: NamedVector = value.getVector().asInstanceOf[NamedVector]
  println(key + "\t" + namedVector.getDelegate());
}
reader.close();

まとめ

K-means クラスタリングは、理屈は簡単なんだけど、じゃMahoutでどう実装するかとなると、意外に迷ってしまう。

ベクトルデータの実装は3種類あるので適切なものを使用する。

Mahout 0.7以降ではin memoryのクラスタリングが出来なくなったので、Hadoopの擬似分散モードを使用する。具体的には、一度ファイルに書き出してKMeansDriverを実行。

クラスタリングに関しても、レコメンデーションと同様に、結果をみつつパラメーターをチューニングしていく必要がある。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です