原创文章,转载请注明: 转载自慢慢的回味
本文链接地址: 3 推荐系统数据表示
本章包括
Mahout怎么表示推荐系统数据
DataModel实现和使用方法
处理没有参考值的数据
数据的质量很大的决定类推荐系统的质量。拥有高质量的数据是很好的,拥有大量的数据也好。推荐引擎需要从数据中获取大量信息,强烈受数据影响。因此运行性能大大地受数据质量和数据表示方式影响。聪明的选择数据结构可以影响性能的几个数量级,而且在大规模上,它影响更多。本章探索了Mahout表示和获取数据的几种关键类。你将知道为什么用Mahout中表示用户,项和参考值的方式将更加有效和可伸缩性。本章也详细的了解了Mahout的数据模型: DataModel。最终,我们将看看当数据没有评估值或参考值的情况,即布尔参考值。
3.1 参考数据表示
推荐系统的输入是参考数据——谁喜欢什么,喜欢多少。即输入数据由用户ID,项ID 和参考值构成的元组表示,有些时候,甚至没有参考值。
参考数据对象
一个参考数据对象由用户ID,项ID和参考值构成,表示用户ID对项ID的参考值是多少。Preference是接口,有多种实现。通常可以用GenericPreference。例如表示用户123对项456的参考值为3.0为new GenericPreference(123, 456, 3.0f)。可是参考数据集合怎么表示呢?使用Preference[]吗?如果你这么想,在Mahout里面就错了。通常GenericPreference由20字节的有用数据表示:8字节的用户ID(Java long),8字节的项ID(Java long)和4字节的参考值(float)。但每个对象有28字节的头信息:8字节的对象引用,另外的20字节用于描述对象自己,这样头信息就占了有用信息的140%。所以如果是Preference[]的化,就有很多冗余。
参考数据数组和实现
PreferenceArray就是这种数组的实现。例如 GenericUser-PreferenceArray表示一个用户所关联的所以项以及每项的参考值。这种参考值的表示只需要12字节(8字节项ID和4字节参考值),只是原始的1/4。对比如下左右图可以看到是怎么实现的。
代码实现:
PreferenceArray user1Prefs = new GenericUserPreferenceArray(2); user1Prefs.setUserID(0, 1L); user1Prefs.setItemID(0, 101L); user1Prefs.setValue(0, 2.0f); user1Prefs.setItemID(1, 102L); user1Prefs.setValue(1, 3.0f); Preference pref = user1Prefs.get(1); |
提高集合速度
FastByIDMap和FastIDSet
Mahout大量的使用Map和Set,但是不使用Java提供的如TreeSet和HashMap,而使用自己定义的FastMap,FastByIDMap和FastIDSet。它们减少了内存占用,而不是显著提高性能。它们显著的几点不同:
就如HashMap,FastByIDMap也是基于hash的,它使用线性探测而不是使用分别的链来探测hash冲突,这避免了对每一项还有一个额外的Map.Entry对象。键值都是long型的而不是对象,这能提高不少性能。
Set的实现不由Map实现。
FastByIDMap就如一个缓存,它具有最大容量限制。当新项加入时,不常用的项将被删除。
这些存储的区别是显著的:FastIDSet平均每项需要14字节,而HashSet需要84字节。FastByIDMap每项需要28字节,而HashMap需要84字节。
3.2 内存中的数据模型
封装推荐引擎输入数据的是DataModel。不同的推荐引擎算法有各自的实现。例如,一个DataModel可以提供输入数据中用户ID的数量或列表,或提供每项的所有参考值,或提供对某个项集合有多少用户有过推荐值。这节只关注几个关键的,更多的请参考 (https://builds.apache.org/job/Mahout-Quality/javadoc/)。
通用数据模型
GenericDataModel这是最简单的内存数据模型实现,当你要在内存中构建数据,而非使用文件或数据库时,它是合适的。它能简单的描述输入数据,通过如下代码的形式:
FastByIDMap preferences = new FastByIDMap(); 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); preferences.put(1L, prefsForUser1); DataModel model = new GenericDataModel(preferences); |
文件类型数据
你可能不常直接用GenericDataModel,相反你需要FileDataModel,它从文件中读取数据把结果用GenericDataModel的形式存在内存中。任何可能的文件都可以,比如逗号分割文件,制表符分割文件,压缩文件。
可刷新组件
当讨论加载数据时,很有必要说说重载数据,即刷新接口,Mahout的推荐系统相关类都有实现。它们通过一个刷新方法的暴露来完成数据的重载,重新计算。
需要注意的是,FileDataModel只有当你调用方法的时候才会去重载,而不算自动的。
你可能不止要FileDataModel刷新,而且依赖它的对象也要刷新,所以为什么refresh方法需要在Recommender上了:
DataModel dataModel = new FileDataModel(new File("input.csv"); Recommender recommender = new SlopeOneRecommender(dataModel); ... recommender.refresh(null); |
更新文件
数据库类型数据
有时数据太大以至于不能装配到内存中,这个时候就需要利用关系数据库来存取数据了。Mahout的几个推荐引擎就把计算结果存在类数据库中,但是请记住,利用数据库这种方式将减慢运行速度,即使合适的调优和索引,但是在过量的存取,序列化,反序列化时还是比内存方式慢好多。可有时没有办法,比如你的数据本来就存储在数据库中,需要集成时。
JDBC和MySQL
通过JDBC的实现为JDBCDataModel,如MySQL5.x: MySQLJDBCDataModel。
配置JNDI
这里请自己参考实现。
用程序配置
例如:
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"); |
3.3 复制没有参考值的参考数据
有时,参考数据是没有参考值的,只是证明用户和项由关系。比如新闻网站需要推荐文章给用户,但是要求用户去给文章打分好像不大现实,我们只知道用户看了这个文章而已。这种情况下就没有参考值。在Mahout中,这种参考数据就是布尔参考数据,即喜欢,不喜欢,或没关系。
什么时候忽略参考值
什么时候忽略参考值?可能和上下文相关,如只关注喜欢或不喜欢。
使用内存表示没有参考值的参考数据
Mahout中有专门用于存储没有参考值的数据模型GenericBooleanPrefDataModel。每条参考数据会少4字节。它仅仅存储用户和项的关联关系,使用FastIDSets。这种新的实现在某些方法调用上会快些,如getItemIDsForUser(),因为已经有实现;但有些方法会慢,如getPreferencesFromUser(),因为它没有PreferenceArrays,但必须实现这个方法。你会疑问getPreferenceValue()应该返回什么呢?它每次返回人为的值:1.0。
关于GenericBooleanPrefDataModel,这儿有个例子:
DataModel model = new GenericBooleanPrefDataModel(GenericBooleanPrefDataModel.toDataMap(new FileDataModel( new File("ua.base")))); RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator(); RecommenderBuilder recommenderBuilder = new RecommenderBuilder() { public Recommender buildRecommender(DataModel model) throws TasteException { UserSimilarity similarity = new PearsonCorrelationSimilarity(model); UserNeighborhood neighborhood = new NearestNUserNeighborhood(10, similarity, model); return new GenericUserBasedRecommender(model, neighborhood, similarity); } }; DataModelBuilder modelBuilder = new DataModelBuilder() { public DataModel buildDataModel(FastByIDMap trainingData) { return new GenericBooleanPrefDataModel(GenericBooleanPrefDataModel.toDataMap(trainingData)); } }; double score = evaluator.evaluate(recommenderBuilder, modelBuilder, model, 0.9, 1.0); System.out.println(score); |
选择兼容的实现方式
你将发现使用上面的代码会有IllegalArgument-Exception产生,因为构造器是PearsonCorrelationSimilarity。为什么呢?GenericBooleanPrefDataModel也是数据模型啊?在计算相似性时,如EuclideanDistanceSimilarity拒绝工作当没有参考值时,因为即使计算,结果也是无意义的。为了解这个问题,必须选择合适的相似性计算维度,Log-LikelihoodSimilarity就是一个,因为它不依赖参考值。用它替换运行上面的代码,产生结果为0.0,即它能够合适的计算。它是正确的结果吗?是的。估计值和真实值的差异都是1,所以结果为0.测试本身是无效的,因为怎么都是结果0。但是精度和重现率还算有效的,看下面的代码:
DataModel model = new GenericBooleanPrefDataModel(new FileDataModel(new File("ua.base"))); RecommenderIRStatsEvaluator evaluator = new GenericRecommenderIRStatsEvaluator(); RecommenderBuilder recommenderBuilder = new RecommenderBuilder() { @Override public Recommender buildRecommender(DataModel model) { UserSimilarity similarity = new LogLikelihoodSimilarity(model); UserNeighborhood neighborhood = new NearestNUserNeighborhood(10, similarity, model); return new GenericUserBasedRecommender(model, neighborhood, similarity); } }; DataModelBuilder modelBuilder = new DataModelBuilder() { @Override public DataModel buildDataModel(FastByIDMap trainingData) { return new GenericBooleanPrefDataModel(GenericBooleanPrefDataModel.toDataMap(trainingData)); } }; IRStatistics stats = evaluator.evaluate(recommenderBuilder, modelBuilder, model, null, 10, GenericRecommenderIRStatsEvaluator.CHOOSE_THRESHOLD, 1.0); System.out.println(stats.getPrecision()); System.out.println(stats.getRecall()); |
结果如下,精度和重现率都是24.7。这不好,因为返回的推荐结果中大约1/4是好的,大约1/4的好推荐返回了。
我们来看看这个问题,在GenericUserBasedRecommender中,推荐引擎还是通过估计参考值来排序,但是这些参考值都是1.0,所以结果是无意义的。我们可以采用GenericBooleanPrefUserBasedRecommender,它将产生有意义的结果。它通过项的关联关系来度量相似用户,越相似的权重越大。替换后重新运行,结果为22.9,结果差不多,那是因为我们没有使用特别有效的推荐系统。
3.4 总结
略本作品采用知识共享署名 4.0 国际许可协议进行许可。