Mahout in Action 1

推荐系统简介

Mahout关注的三个核心问题: 聚类,分类,推荐(协同过滤).

聚类: 聚类技术试图将大量的事物组合为拥有类似属性的簇(cluster),借以在一些规模较大或难于理解的数据集上发现层次结构和顺序,以揭示一些有用的模式或让数据更易于理解.

分类: 分类技术决定了一个事物多大程度上从属于某些类型或类别,或者多大程度上具有或不具有某些属性.

第一个推荐引擎

public class RecommenderIntro {
    public static void main(String[] args) throws Exception {

        String basePath = "/Users/zhange/work/projects/TestMahout/src/main/java/";

        DataModel model = new FileDataModel(new File(basePath + "Chapter02/intro.csv"));            // 加载数据文件

        UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
        UserNeighborhood neighborhood = new NearestNUserNeighborhood(2, similarity, model);

        Recommender recommender = new GenericUserBasedRecommender(model, neighborhood, similarity); // 生成推荐引擎

        List<RecommendedItem> recommendations = recommender.recommend(1, 1);                        // 为第一个用户推荐一本书

        for (RecommendedItem recommendation : recommendations) {
            System.out.println(recommendation);
        }
    }
}

上面用到了多个Mahout提供的组件:

  1. DataModel: 实现存储并未计算提供其所需的所有偏好,用户,物品数据
  2. UserSimilarity: 实现给出连个用户之间的相似度,可以从多种可能度量或计算中选用一种来作为依据
  3. UserNeighborhood: 实现明确了与给定用户最相似的一组用户
  4. Recommender: 实现合并这些组件,为用户推荐物品

各个组件之间的关系:

Mahout-UserCF组件关系

上面例子中的输出结果为: RecommendedItem[item:104, value:4.257081]
表示的意思是,将物品104推荐给用户1,并且给出用户1对104物品的估计评分为4.257081.

评估一个推荐引擎

一般的评估方法是,将真实的偏好数据与估计的偏好数据进行对比,当没有真实的偏好数据可用时,从需要评估的数据中取出一小部分作为真实数据,剩余的部分用作推荐引擎的训练计算数据,然后将二者进行对比,得到推荐引擎的效果评估结果.

简单的评估方式:

  1. 平均差值: 计算出估计和实际偏好的平均差值,差值越低越好
  2. 均方根: 计算出实际偏好值和估计值之间的差值之后,先进性平方再求其平均值的平方根,值越低越好.

平均差值与均方根的计算说明

评估过程

public class EvaluatorIntro {

    private EvaluatorIntro() {
    }

    public static void main(String[] args) throws Exception {
        String basePath = "/Users/zhange/work/projects/TestMahout/src/main/java/";

        // 生成可重复的结果
        RandomUtils.useTestSeed();
        DataModel model = new FileDataModel(new File(basePath + "Chapter02/intro.csv"));

        // 使用平均差值进行评估,或者可以使用均方根进行评估: RMSRecommenderEvaluator()
        RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator();

        // 跟上个例子一样,根据现有的数据创建一个recommender,返回一个RecommenderBuilder:
        RecommenderBuilder recommenderBuilder = new RecommenderBuilder() {
            //@Override
            public Recommender buildRecommender(DataModel model) throws TasteException {
                UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
                UserNeighborhood neighborhood =
                        new NearestNUserNeighborhood(2, similarity, model);
                return new GenericUserBasedRecommender(model, neighborhood, similarity);
            }
        };
        // 使用70%的数据进行训练,30%的数据进行测试
        double score = evaluator.evaluate(recommenderBuilder,   // 上面创建的RecommenderBuilder
                null,                                           // DataModelBuilder的实例,用于控制如何从训练数据中生成DataModel,通常为null,除非需要控制如何从训练数据中生成DataModel
                model,                                          //
                0.7,
                1.0);                                           // 用于控制使用多少输入数据,1.0即当前数据的100%
        System.out.println(score);
    }
}

如上所示,大部分计算行为发生在evaluate()中,EvaluatorIntro会将数据按照参数中提供的比例,将model数据分为训练集合测试集,将训练集与传入的RecommenderBuilder重新构建一个Recommender,然后根据训练结果与测试数据进行比较,得到评估结果.

评估的查准率(精度)和查全率(召回率)

使用经典的信息检索度量标准来评估推荐程序:查准率和查全率,其目的是尽量避免在搜索结果中返回无关信息,而应竭力返回相关的信息.

在推荐系统中的解释:

查准率: 指top结果中相关结果的比例,”Precision at 10(推荐10个结果时德尔查准率)”,是指这个比例来自对前10个top结果的判定.
查全率: 是指所有相关结果包含在top结果中的比例.

查准率与查全率

用在推荐中的说法就是: 查准率是top推荐的10个结果中有几个是真正相关的,查全率是所有相关的项出现在top中的比例.

Mahout中提供的相关评估:

public class IREvaluatorIntro {

    private IREvaluatorIntro() {
    }

    public static void main(String[] args) throws Exception {
        String basePath = "/Users/zhange/work/projects/TestMahout/src/main/java/";

        RandomUtils.useTestSeed();
        DataModel model = new FileDataModel(new File(basePath + "Chapter02/intro.csv"));

        RecommenderIRStatsEvaluator evaluator = new GenericRecommenderIRStatsEvaluator();   // 新建一个RecommenderEvaluator

        // 跟上个例子一样,根据现有的数据创建一个recommender,返回一个RecommenderBuilder:
        RecommenderBuilder recommenderBuilder = new RecommenderBuilder() {
            //@Override
            public Recommender buildRecommender(DataModel model) throws TasteException {
                UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
                UserNeighborhood neighborhood = new NearestNUserNeighborhood(2, similarity, model);
                return new GenericUserBasedRecommender(model, neighborhood, similarity);
            }
        };

        // 推荐两个结果时的评估:
        IRStatistics stats = evaluator.evaluate(recommenderBuilder,
                null, model, null, 2,
                GenericRecommenderIRStatsEvaluator.CHOOSE_THRESHOLD,
                1.0);
        System.out.println(stats.getPrecision());
        System.out.println(stats.getRecall());
    }
}

运行结果解析:

0.75
1.0

0.75: 表示退浆两个结果时的查准率为0.75,即75%的推荐结果是真正相关的
1.0: 表示推荐两个结果时的查全率为1.0, 即所有真正相关的项都包含在推荐结果中

偏好数据的表示

推荐引擎的输入为偏好数据,即: 用户ID,物品ID,偏好值.而时候偏好值会被忽略.

Preference

Preference是一个最基本的抽象,表示用户ID,物品ID,偏好值.Perference是一个接口,最常用的实现是GeneriPreference.比如:

new Preference(123, 145, 3.0f)

一个Preference中共包含20字节数据,(long,long,float),整个对象占用28字节,因此资源消耗很大.

而PreferenceArray表示一个聚合的偏好,具有类似数组的API.比如,GeneriUserPreferenceArray,表示与某个用户关联的偏好集合,包含一个单一用户ID,一个物品ID数组,一个偏好值数组,存储因而更加高效.

设置PreferenceArray中的偏好值:

PreferenceArrayy user1Prefs = new GeneriUserPreferenceArray(2);

user1Prefs.setUserID(0, 1L);

user1Prefs.setItemID(0, 122L);
user1Prefs.setValue(0,2.0f);

user1Prefs.setItemID(1, 103L);
user1Prefs.setValue(1, 3.0f);

而对应于物品的GenericItemPreferenceArray与其原理实现一致.

FastByIDMap和FastIDSet

Mahout中使用了大量Map和Set数据结构,但并不是JDK中的TreeSet和HashMap,而是FastMap,FastByIDMap,FastIDSet,以降低内存占用.

数据结构对比:

  1. 与HashMao类似,FastByIDMap是基于散列的.但是它在处理列冲突时使用的是线性探测,而非分离链接,这样便不必为每个条目都增加一个额外的Map.Entry对象.如前所述,Object对内存的消耗是惊人的.
  2. 在Mahout推荐程序中,键(key)和成员(member)通常采用原始类型long,而非object,使用long型的键能够节省内存并提升性能.
  3. Set实现的内部没有使用Map.
  4. FastByIDMap可以作为高速缓存,因为他有一个最大空间的概念,超过这个大小时,若要新加入条目则会把不常用的移走.

这些实现不是单纯为了提升性能,而是为了减少内存占用.

内存级DataModel

GenericDataModel的定义

基于文件的数据

可刷新组件

Refresh接口,只公开了一个方法: refresh(Collection).该方法简单的请求组件在最新的输入数据上进行: reload,recompute,refresh,并事先让它的依赖也进行同样的动作.

数据刷新顺序

FileDataModel支持刷新文件,在读取主数据文件之后额外生成的数据文件,并可以覆盖任何以前度去过对的数据.需要更新的文件必须目录相同,前缀相同,foo.tx -> foo.1.txt .

JDBCDataModel

JDBCDataModel中数据库的设置:

  1. 用户ID和物品ID列应为非空,而且必须被索引
  2. 主键必须为用户ID和物品ID的组合主键
  3. 列的数据类型根据Java中对应long和float的类型来选择.在MySQ中,应该为BIGING和FLOAT
  4. 注意调节缓冲区和查询告诉缓存,见 MySQLJDBCDataModel的javadoc
  5. 当使用MySQL的Connector/J驱动时,将驱动的参数设为True

无偏好值的处理

Mahout中没有偏好值的关联称为布尔型偏好.在内存中的表示使用GerericBooleanPrefDataModel.