3 推荐系统数据表示

原创文章,转载请注明: 转载自慢慢的回味

本文链接地址: 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 国际许可协议进行许可。

发表回复