集合知プログラミング 2章 アイテムを推薦する

評価の類似度という尺度ができれば、それを元に評者を相対的にランキングしたり、自分が未評価なものを推薦してもらうこともできる。
ランキングはただ全員との類似度を調べてソートするだけなので省略。
推薦は、それぞれの評者の「アイテムの評価」×「その評者の類似度」で重み付けして合計し、そのアイテムを評価した評者の類似度の総計でそれを割ることにより正規化する。
これは未評価のアイテムに対する自分の評価の予想ともなる、と書いてある。


ScoredThing.java

package chapter2;

public class ScoredThing implements Comparable<ScoredThing> {
    private String name;
    private double score;

    /**
     * @param name
     * @param score
     */
    public ScoredThing(String name, double score) {
        super();
        this.name = name;
        this.score = score;
    }

    /* (non-Javadoc)
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    @Override
    public int compareTo(ScoredThing arg) {
        if (arg.score == this.score) {
            return 0;
        }
        
        if (arg.score > this.score) {
            return 1;
        } else {
            return -1;
        }
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @return the score
     */
    public double getScore() {
        return score;
    }
    
}


Main.java

package chapter2;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class Main {
    
    /**
     * @param targetName
     * @param simcal
     * @return simcalを使って計算した結果で、targetNameが未評価のもので推薦度が高い順にソートされたリスト
     */
    public List<ScoredThing> getRecommendations(String targetName, SimilarityCalculator simcal) {
        Critics critics = new Critics();
        List<ScoredThing> listRet = new LinkedList<ScoredThing>();
        // targetの評価
        Map<String, Double> mapTarget = critics.getMap(targetName);
        // 一時的な合計を保存する
        Map<String, Double> mapTotals = new HashMap<String, Double>();
        Map<String, Double> mapSimSum = new HashMap<String, Double>();
        
        Iterator<String> iterName = critics.dataKeySet().iterator();
        while (iterName.hasNext()) {
            String name = iterName.next();
            // 自分自身とは比較しない
            if (name.equals(targetName)) {
                continue;
            }
            // 0以下のスコアは無視する
            double similarity = simcal.getSimilarity(name, targetName);
            if (similarity < 0.0) {
                continue;
            }
            // まだ評価していないアイテムを探す
            Map<String, Double> mapCritique = critics.getMap(name);
            Iterator<String> iterKey = mapCritique.keySet().iterator();
            while (iterKey.hasNext()) {
                String item = iterKey.next();
                // 既に評価済みのアイテムは無視
                if (mapTarget.containsKey(item)) {
                    continue;
                }
                double rating = mapCritique.get(item).doubleValue();
                double currentTotals, currentSimSum;
                if (!mapTotals.containsKey(item)) {
                    currentTotals = 0.0;
                    currentSimSum = 0.0;
                } else {
                    currentTotals = mapTotals.get(item).doubleValue();
                    currentSimSum = mapSimSum.get(item).doubleValue();
                }                    
                // 類似度×スコアを合計していく
                mapTotals.put(item, Double.valueOf(rating * similarity + currentTotals));
                // 類似度を合計していく
                mapSimSum.put(item, Double.valueOf(similarity + currentSimSum));
            }
        }
        
        // 正規化したリストを作る
        Iterator<String> iterItems = mapTotals.keySet().iterator();
        while (iterItems.hasNext()) {
            String item = iterItems.next();
            if (mapSimSum.containsKey(item)) {
                double score = mapTotals.get(item).doubleValue() / mapSimSum.get(item).doubleValue();
                listRet.add(new ScoredThing(item, score));
            }
        }
        // スコア順にソート
        Collections.sort(listRet);
        
        return listRet;
    }
    
    /**
     * 
     */
    public Main() {
        List<ScoredThing> recommend = getRecommendations(Common.Toby, new PearsonCalculator());
        Iterator<ScoredThing> iterSim = recommend.iterator();
        while (iterSim.hasNext()) {
            ScoredThing st = iterSim.next();
            System.out.println(st.getName() + ":" + st.getScore());
        }
    }
    
    /**
     * @param args
     */
    public static void main(String[] args) {
        new Main();
        
    }
}

ピアソン相関を使うと、類似度が負の数になることも有り得る。
サンプルコードでは0以下のスコアは無視されているけど、他の評者の類似度が全員、負になってたりすることも有り得るのでは・・・?
そのときはそのときで負の数で重み付けすればそれなりの値はでそうだけど、類似度の総計が0に近づいたりするとおかしくなるのか。
この方法を使う場合は類似度は0から1の間の値になるようにしないといけない?
となると類似度も1を加えて2で割ったり・・・する必要はないのかな。。


しかし、このペースでやってて読み終わるのかな・・・