Netflixのレーティングデータを扱う(1)

Grand Prizeが達成されたNetflix Prizeですが、データの公開が停止されたりすると困るので登録してデータを確保した。

Netflixのデータフォーマットは展開先のフォルダの下にtraining_setというフォルダができ、その中にmv_0000001.txt ...という形式で17770個の映画のレーティングデータが入っている。

フォーマットは

(映画のID):
(ユーザのID),(レーティング),(レーティングをつけた日(YYYY-MM-DDの形式))
...
(ユーザのID),(レーティング),(レーティングをつけた日(YYYY-MM-DDの形式))

となっている。

ここでレーティングの数は約1億個でたとえば一つのレーティングを

public class Rating {
  int user;
  int item;
  int rate;
  Rating(int u , int i , int r){
    user = u;
    item = i;
    rate = r;
  }
}

のようなクラスに格納すると必要なメモリ量は12バイト*1億=1.2GBであり、最近のPCでは全データをメモリに乗せることはできなくはないがレーティングの日付などを入れようとしたり、データが増えていったときにメモリが足りなくなる。

ただ、実際の協調フィルタリングの学習において、SVDのような手法を用いる場合、

for(Rating r : 全レートデータ){
  再急方向のベクトルを更新
}
再急方向に移動

のようなロジックが用いられることが多く、全レートデータをシーケンシャルに読むことができればよい。Javaでそういうことをしたい場合はOracle Technology Network for Java Developers | Oracle Technology Network | Oracleを継承したクラスを作成する、以下に参考のコードを示す

import java.io.*;
import java.util.*;

public class NetflixDataIterator implements Iterator<Rating>{
  static final String trainDirName = 
    "E:/netflix/download/training_set";
  File trainFiles[];
  private int currentIndex;
  private int currentMovieID;
  private Scanner currentInputStream;
  public NetflixDataIterator() {
    File dir = new File(trainDirName);
    trainFiles = dir.listFiles();
    currentIndex = 0;
    try{
      currentInputStream = new Scanner(trainFiles[currentIndex]);      
      setMovieID();
    }catch (Exception e) {
      throw new RuntimeException(e.getMessage());
    }
  }  

  @Override
  public boolean hasNext() {
    if(currentIndex == trainFiles.length){
      return false;
    }
    if(currentInputStream.hasNext()){
      return true;
    }else{
      return currentIndex < trainFiles.length - 1;
    }
  }
  
  private void setMovieID(){
    String firstLine = currentInputStream.nextLine();
    currentMovieID = Integer.parseInt(firstLine.substring(0, firstLine.length() - 1));    
  }

  @Override
  public Rating next(){
    if(!hasNext()){
      throw new NoSuchElementException();
    }
    if(!currentInputStream.hasNext()){
      try {
        currentInputStream = new Scanner(trainFiles[++currentIndex]);
        setMovieID();
      } catch (FileNotFoundException e) {
        throw new RuntimeException(e.getMessage());
      }      
    }
    String rating = currentInputStream.nextLine();
    String sep[] = rating.split(",");
    int user = Integer.parseInt(sep[0]);
    int rate = Integer.parseInt(sep[1]);
    Rating res = new Rating(user , currentMovieID , rate);
    return res;
  }
  
  @Override
  public void remove() {
    throw new UnsupportedOperationException();
  }
}

クライアントから実行する際には

  public static void main(String[] args) {
    Iterator<Rating> it = new NetflixDataIterator();
    long time = System.currentTimeMillis();
    int cnt = 0;
    int totalRate = 0;
    while(it.hasNext()){
      Rating r = it.next();
      totalRate += r.rate;
      ++cnt;
    }     
    long t = System.currentTimeMillis() - time;
    System.out.println(cnt);
    System.out.println("ave rate:"+(totalRate * 1.0 / cnt)+" "+t+"ms");
  }

のようなコードを書く。

実行結果

100480507
ave rate:3.604289964420661 638641ms

なお余談ではあるがこのJavaのコードは現在の状態をクラスのメンバ変数を用いて管理する必要がある。別の言語、例えばrubyではブロック付きメソッドといって

def readData
   while データをすべて読み終わる
      yield(次のレーティングデータ)
   end
end

totalRate = 0.0
readData(){|rating|
  totalRate += rating.rate
}

のように書くことができる。

追記:(07/26)

あと全部読むのに636秒かかっていますがこの部分はファイルを一つにまとめて、ScannerからBufferedReaderに変えて、レーティング情報をbit列でファイルに保存しておけば30秒ぐらいには縮まります。