Mahout in action 中文翻译
【译】mahout in action 2.2 运行首个推荐引擎
博客分类:
Mahout包含一个推荐引擎的几种类型,事实上包含传统的基于用户(user-based),基于项目(item-based)推荐算法,也包括基于“slope-one”技术的实现(这一个新的有效的方法)。
你将根据实验,基于单机版的(SVD)初步实现。在下面的章节里,我们将会在Mahout的背景下和一些现实生活中的例子,来回顾上面的观察结果。我们将会考虑如何代表数据,如何进行有效的推荐算法,如何评估推荐器的效果,如何为一个特殊的问题调研和定制推荐器,最后考虑如何分布计算。
2.2.1 创建输入
我们将会以一个简单的例子,开始对Mahout中的推荐器认识。首先,我们需要给出推荐器的输入数据。在Mahout中称为“偏好(preferences)”的表单。因为最流行的推荐引擎,把需要推荐的项目推荐给用户,从用户到上面提到的关联items,作为一个关联最方便选择讨论。这些用户和项目可以成为任何东西。包含一个userID和一个itemID,通常用一个数字来表达用户和选项的喜好强度(preference)。在Mahout中的ID通常是数字,事实上总是int。这一项的值可以代表任意值,preference值越大,代表关联度越强。例如,这个值可能是从1到5的范围内的等级。在这里,用户分配“1”,则表示他不能忍受这个项目,分配“5”表示他很喜欢这个项目。
创建一个文本文件,包含相关用户,简单的命名“1”到“5”。为四本书的偏好,我们将会称它们为“101”到“104”。在现实生活中,这些可能是用户ID和产品ID列表,它们来自于一个公司的数据库;Mahout仅仅需要数字命名的用户和项目!我们将会简单的用逗号分割数值的格式,来记他们。把下面的复制到一个文件里,并把它作为intro.csv保留下来:
列表2.1 推荐器的输入文件intro.csv
通过上面的学习,我们可能已经注意到。用户1和5看起来有相似的品味。他们都喜欢book101,其次喜欢book102,然后都喜欢book103。同样的问题存在于用户1和4,因为它们看起来都喜欢101和103(虽然没有说用户4喜欢102).换句话说,通过运行计数,发现用户1和用户2喜欢的看起来相似:1喜欢101,而用户2不喜欢,用户1喜欢103,而用户2刚好相反。用户1和用户3没有很多重叠。双方同时喜欢书101的就很难。看图2.1,可能会想象用户和项目之间的关系,既有肯定的又有否定的。
图2.1 用户1到用户5,与项目101到项目107的关系。
虚划线代表否定的关联,用户看起来很不喜欢这个项目,但也表达了与这个项目的关系。
2.2.2创建一个推荐器
之所以选择这本书,我们推荐给用户1?而不是101,102,也不是103。是因为他明显的已经知道了这些书,推荐器是着力与发现新事物。感觉建议,因为用户4和用户5与用户1看起来相似,我们应该推荐一些用户4或用户5喜欢的东西。把104,105和106当作可能的推荐器。总体来说,根据为项目104的喜好值4.5和4.0的判断来看,104看起来可能性最大。现在,运行下面的代码:
列表2.2 Mahout上简单的user-based推荐算法程序
Java代码
package mia.recommender.ch02; import org.apache.mahout.cf.taste.impl.model.file.*; import org.apache.mahout.cf.taste.impl.neighborhood.*; import org.apache.mahout.cf.taste.impl.recommender.*; import org.apache.mahout.cf.taste.impl.similarity.*; import org.apache.mahout.cf.taste.model.*; import org.apache.mahout.cf.taste.neighborhood.*; import org.apache.mahout.cf.taste.recommender.*; import org.apache.mahout.cf.taste.similarity.*; import java.io.*; import java.util.*; class RecommenderIntro { public static void main(String[] args) throws Exception { DataModel model = new FileDataModel(new File("intro.csv")); //A UserSimilarity similarity = new PearsonCorrelationSimilarity(model); UserNeighborhood neighborhood = new NearestNUserNeighborhood(2, similarity, model); Recommender recommender = new GenericUserBasedRecommender( model, neighborhood, similarity); //B List<RecommendedItem> recommendations = recommender.recommend(1, 1); //C for (RecommendedItem recommendation : recommendations) { System.out.println(recommendation); } } }
A 加载数据文件
B 创建数据引擎
C 为用户1推荐1个项目
为了更清楚,可以通过下面章节更多的例子。这里我们将会缺省导入,类声明和类函数声明,而不只是重复程序声明本身。为了帮助认识,基础组件之间的关系,看图2.2。并不是所有的基于Mahout的推荐器,看起来都像这个样子。某些将会采用不同的组件,有不同的关系。但这可以给我们一个初步的认识,在我们这个例子中表现出来的组件。
图2.2 简单的说明在Mahout中基于用户(user-based)推荐器各个组件的相互关系
因为在下面的两节里,我们将会更详细的讨论这个内容中的每一项,并总结每一个组件的作用。一个DataModel的实现是计算中需要的首个因素,提供用户和项目数据储存的入口。一个UserSimiliarity的实现提供了两个用户如何表示相似的概念;这将会以所有可能的度量或计算中的一个为依据。一个UserSimiliarity的实施定义了一组用户的概念,这些用户与一个已知的用户很相似。最后,一个Recommender的实现把所有的内容拉在一起,把项目推荐给用户,和相关的功能。
2.2.3 分析输出数据
通过你喜欢的IDE,编译和运行。在终端或IDE上运行的程序,输出数据应该是:
RecommendedItem[item:104, value:4.257081]
我们曾经希望寻找并使用一个优秀的推荐器。这个推荐引擎把book104推荐给了user1.进一步说,推荐引擎也是这么做的,因为它估计到user1对book104的分值大约是4.3,而且它是所有对推荐器筛选的项目中分值最高的。
那不算太坏。我们没有得到107,它也是值得推荐的,但只与不同品味的user有关联。我们从106中挑选出104,当你注意到104是所有book中的选择比例非常高的,这是有意义的。进一步的说,对于user1有多喜欢项目104,我们已经得到了一个合理的估计:user4和user5所表达的介于4.0和4.5之间的比值。
从数据上来看,这个正确答案并不明显。但是推荐引擎对它做了一些适当的调整,并得到了一个更有说服力的答案。通过看到这个简单的程序,这个程序就是从一小堆数据中得到一个有用的并且不明显的答案,如果你从中得到了一次令人愉快的兴奋,那么这个机器学习的世界正是为你而存在的。
简单的说,像上面的构建在小型数据集合上推荐器并不重要。在现实生活中,数据量是巨大的,并且它们是很杂乱的。例如,想象一下,把一个新闻文章推荐给读者的一个畅销的新闻点,分值从文章的点击得到的。但是,许多分值可能是假的,也可能一个读者点击了他并不喜欢的一篇文章,或者说点击了一个错误的故事。可能许多点击已经出现了,但并没有生效,所以与user没有关联。在试想一下这样规模的数据量:可能在一个月内就有数以亿计的点击量。
从这些数据中快速产生一个正确的推荐数据,这是相当重要的。稍后,我们将会介绍一个Mahout工具,通过案例的方法研究,我们可以用Mahout工具攻克这一系列问题。它们将会显示出标准的方法,如何生产差劲的推荐数据或者消耗得大量的内存和CPU时间,并且如何安装和配置Mahout来改进。
博客分类:
这是一个推荐引擎的工作,用来解释下面的问题:“对用户来说,怎么的推荐数据才是最好的”。在搞清楚这个的答案之前,我们首先应该解决这个问题。一个好的推荐数据精度指的是什么?我们需要知道产出一个怎么样的推荐器来产生他们?本节的下面部分将会探讨对一个推荐器的考核。因为它是一个工具,当我们开始思考特殊的推荐系统时,它将会是很有用的。
最优秀的推荐器,能够感知你的心灵。它以某种方式知道你可能很喜欢某个的项目,而这些项目你还没有看过或表达过任何喜好。一个优秀的推荐器可以预测所有的分值,并且根据未来的分值和已经打的分值进行排序,从而推荐出最有可以的项目
确实,大部分的推荐引擎也试图通过一些或所有其他的项目估计比率来工作。所以,考核一个推荐器的推荐能力的一种方法,就是考核它估计分值的质量:考核估计分值与实际分值的相似程度。
2.3.1 训练数据和打分
这些实际的分值虽然并不存在。当然没有人知道你在未来会如何喜欢一些新的项目(包括你自己)。通过把一小部分的真实数据集合置于一旁,把它们当作试验数据,这可以模仿一个推荐引擎。这些试验分值不存在于训练数据中,训练数据是为考核过的推荐引擎提供参考:它是除了试验数据的所有数据。相反,这个推荐器是为漏掉的试验数据估计分值,考核就是让它与真实值对比。
在这里,为推荐器生成一个“分值”是相当简单的。例如,在估计分值和实际分值中可以计算出平均差异。这种类型的得分,低一点的分值是比较好的,因为那意味着估算与真实值的差异是相当小的。0.0是完全匹配值,即在估计值和实际值之间没有一点不同。
有时差异的均方根是非常有用的: 在实际值和估计值之间差异值的平均值的平方根。这个值依然是越低越好。
项目1 |
项目2 |
项目3 |
|
真实值 |
3.0 |
5.0 |
4.0 |
估计值 |
3.5 |
2.0 |
5.0 |
差值 |
0.5 |
3.0 |
1.0 |
差值的平均值 |
= (0.5 + 3.0 + 1.0) / 3 = 1.5 |
||
均方根 |
=√((0.52 + 3.02 + 1.02) / 3) = 1.8484 |
表2.1 平均差异和均平方根算法的一个演示
上面的表格显示了一组真实值和估计值之间的差异,以及它们如何形成的得分。最重要的是均方根可以除去那些尚有很大差距的估计值,如这里的项目2,这个结果还是很令人满意的。例如,被2点分开的估计值可能比两倍还多,这和被1点分开的估计值是一样的糟糕。因为这个简单的平均差异可能理解起来更直观,更容易,我们将会在下面的例子中使用它。
2.3.2运行RecommenderEvaluator
让我们再来看一下例子中的代码,评估一个简单的推荐器,这个推荐器是我们在较少的数据集合上创造的:
列表2.3配置运行一个推荐器的一个评估
Java代码
RandomUtils.useTestSeed(); //A DataModel model = new FileDataModel(new File("intro.csv")); RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator(); RecommenderBuilder builder = new RecommenderBuilder() { //B @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); } }; double score = evaluator.evaluate( builder, null, model, 0.7, 1.0); //C System.out.println(score);
A 只用在可重复结果的例子中
B 建立下面的推荐器
C 用百分之七十的数据来训练;余下的百分之三十来测试
大部分的action发生在evaluate()。在RecommenderEvaluator内部把数据分成一个训练和测试集合,建立一个新的训练模型(DataModel)和推荐器来试验,把它的估计值和实际的试验数据对比。
注意我们不会通过这种方法否定一个推荐器。这是因为,这个方法内部将需要围绕一个最新创造的训练模型(DataModel)来建立一个推荐器。所以我们必须提供一个对象,这个对象可以建立一个来自与数据模型(DataModel)的推荐器,既一个推荐器的构造者(recommenderbuilder).在这里我们进行了一个如同我们在第一节里努力进行的实施。
2.3.3 评估结果
程序执行后会在输出一个结果:分数,它可以表示这个推荐器是效果。这样你就可以简单的看结果: 1.0。虽然存在随机性,在evaluator内部选择使用的测试数据,但是这个结果应该是一致性。因为它都是调用RandomUtils.useTestSeed(),每一次生成的数据的随机性是一致的。这仅仅是在这种例子和单元测试中使用,来保证可重复的结果。所以请不要在你实际的代码中使用。
这里,这个值意味着什么,依赖于我们使用的实现AverageAbsoluteDifferenceRecommenderEvaluator。通常,结果1.0又意外着,推荐器估计一个值,这个值偏离实际值为1.0。
在1到5的范围内,1.0这个值不算大,但一些小数据从这里开始。你的结果可能与数据集合随机分解的不同,因此training和test的集合在每次运算中都可能不同。
这个技术可以适用于任何的Recommender和DataModel。用均方根分值,代替有RMSRecommenderEvaluator的实施的AverageAbsoluteDifferenceRecommenderEvaluator
对evaluate()的0参数也可以代替为DataModelBuilder的一个实例,这可以被用于控制training DataModel是如何从training数据创建起来的。通常这个默认值就是很好的;如果在部署上你正在使用DataModel的一个专门的实施,那这是不可能的。一个DataModelBuilder是你如何把它加入评估过程。
1.0参数在最终控制了在所有的输入数据中有多少被使用。这里是百分之百。这可以被用来生产一个quicker,如果少一点精确,只用一个潜在的巨大的数据集合的一小部分来评估。例如,0.1将意味着百分之十的数据被使用,而百分之九十的数据别忽视。当对一个recommender快速地试验小部分的改变,这是很有用的。
【译】mahout in action 2.4 评估的精确和调用
博客分类:
我们也可以得到一个关于recommender问题的更宽广的看法:对生产recommendations我们不用必须估计首选项值。没必要总是对用户提供估计的首选项值。在很多情况下,我们所想要的是一个recommendations的从最好到最差的排序列表。事实上,在有些情况下,我们不是很关心列表的精确排序:一组有点好的recommendations是好的。
用更一般的看法,我们也可以把经典的信息检索测量应用到评估recommenders:精度和调用。这些项目代表性的应用于像搜索引擎这样的程序,这为许多可能结果的查询返回一些最好的结果。
【译】mahout in action 2.5 评估GroupLens数据集合
博客分类:
用这些在进行中的工具,我们将可能不仅讨论速度,也讨论我们创造和修改的推荐引擎的质量。虽然大量真实数据的例子仍然要过几章才能讲到,我们将花一些时间在一个小型数据集合上去快速评估性能。
2.5.1 提取推荐器输入数据
GroupLens (http://grouplens.org/)是一个研究项目,它提供几个不同型号的数据集合,每一个都来自于真实的用户对电影的评分。这是几个有效的大型的真实世界的数据库中之一,在这本书中我们将会探究更多这种数据集合。从grouplens.org查找并下载“100K data set”,当前在
http://www.grouplens.org/node/73上可以得到。反归档(你下载的文件),在它内部,这个文件被称为ua.base。这是一个具有user IDs,item IDs,评级(首选项值)和一些额为信息的用tab键分隔的文件。
这个文件将会起作用吗?Tabs不是用逗号分开它的域,而且它也在终点包含一个额外的信息域。是的,这个FileDataModel的文件会起作用的。返回到listing 2.3中的以前的代码,在listing 2.3中我们建立了一个RecommenderEvaluator,努力进入ua.base的位置,用它来代替我们构造的这个小的数据文件。再一次运行它。这时,评估应该要花几分钟时间,因为它现在是基于100000的首选项值,而不是少数的首选项值。
最后,你应该得到0.9左右的数字。那并不算坏,虽然这个数字由于某种原因远离1到5这个规模上的一个整点,这个整点也不算好。难道我们尝试的这个独特的Recommender对这类数据来说并不是最好的?
让我们在这个数据库上test-drive一个“slope-one” recommender,在即将到来的这节里,我们将会在recommender
algorithms自身上讨论一个简单的算法。这像替换RecommenderBuilder一样容易。这个RecommenderBuilder用org.apache.mahout.cf.taste.impl.recommender.slopeone.SlopeOneRecommeder,像这样:
Listing 2.6 改变评估项目去运行一个SlopeOneRecommender
Java代码
RecommenderBuilder recommenderBuilder = new RecommenderBuilder() { @Override public Recommender buildRecommender(DataModel model) throws TasteException { returnnew SlopeOneRecommender(model); } };
博客分类:
在这章里,我们介绍了推荐引擎的概念。通过一个少量输入,创建一个简单的Mahout recommender,通过一个简单的计算来运行并解释了这个结果。
博客分类:
这章主要讲述:
。Mahout如何展现recommender data
。DataModel的实现及其用法
。没有评分数据
Recommendations的质量主要是由数据的数量和质量决定的。“无用输出,无用输入” 在这里是最真实的。同样,推荐器算法都是集中数据,运行的性能主要受数据的数量和展现的影响。这一章
介绍Mahout的一些关键class,和访问推荐器相关的数据。
【译】mahout in action 3.1 Preference对象
博客分类:
一个推荐引擎的输入数据是评分数据:它喜欢什么以及多少。所以,Mahout
recommenders的输入数据是一组简单的“userID”,“itemID”,和“评分数据”元组,当然,这是一个大的集合。评分数据有时候会被省略。
3.1.1 Preference对象
Preference是一个最基础的概念,它表现一个单一的userID,itemID和一个评分数据。这个对象表现为一个用户对一个项目的打分。Preference是一个接口,通常使用的实现类为GenericPreference。例如:创建一条记录,user(123),对item(456)的打分是3.0: new
GenericPreference(123, 456, 3.0f)。
一组Preferences如何表现?如果你给出了一个合理的答案,如Collection<Preference>
或者 Preference[],大部分情况下,在Mahout APIs中不是这样实现的。Collections和arrays对处理海量Preference对象是无效的。如果你在Java中从未研究过上面的对象,你可能会感到困惑!
单个的GenericPreference包含20个字节的有用数据:一个8字节的user ID(Java long),8字节的item ID(long),4字节的分值(float)。这个对象的存在使GenericPreference包含的字节有惊人的增长:28个字节!这个变化依赖的是JVM的实现;这个数字是从苹果Mac OS X 10.6的64位Java 6 VM 得到的。由于上面的对象和其他线性问题,对这个对象来说,28个字节中包括8字节的参考值,另外20个字节的空格,在对象自身的表现内。由于上面的现象,因此一个GenericPreference对象已经比它需要多消耗了140%的存储。
为什么这么做?在recommender算法中,都需要所有评分数据的集合,这些评分数据是与一个用户或一个项目联系在一起的。在这样一个集合里,user ID或者item ID与所有好像多余的Preference对象将会是配套的。
3.1.2
PreferenceArray和实现
进入PreferenceArray,这个接口的实现表现为一个具有类似与数组的API的分值的集合。例如,GenericUserPreferenceArray表现为一个用户的所有打分.它在内部包括一个单一的user ID,一系列的item IDs,一系列的评分值。在一个用户的所有打分中,需要占用12个字节的内存(一个8字节的item ID和一个4字节的评分值)。把它与需要一个完整的Preference项目的大约48个字节相比较。这个4字节内存,包括对齐这个特殊的实现,
但它也提供了小的性能提升,更小的对象必须被垃圾回收器分配和检查。比较图3.1 and 3.2去理解这些保存是如何完成的。
图3.1效率较低的评分值的表现,利用一系列的Preference对象。灰色的区域代表上面的对象。白色的区域是数据,它包括引用对象。
图3.2利用GenericUserPreferenceArray更有效的表现
下面的代码表现一个PreferenceArray的典型的构造和使用
列表3.1在一个PreferenceArray中设置评分值
Java代码
PreferenceArray user1Prefs = new GenericUserPreferenceArray(2); user1Prefs.setUserID(0, 1L); //A user1Prefs.setItemID(0, 101L); user1Prefs.setValue(0, 2.0f); //B user1Prefs.setItemID(1, 102L); user1Prefs.setValue(1, 3.0f); //C Preference pref = user1Prefs.get(1); //D
A 为所有打分设置user ID
B User 1当前为item 101的打分2.0
C User 1为item 102的打分3.0
D Item 102的一个Preference实现
同样这里存在一个称为GenericItemPreferenceArray的实现,它内部的所有分值,与item关联而不是与user关联。它的目的和用法都是完全类似的。
博客分类:
非常高兴的是,Mahout已经重新创造了“java数组对象”。这只是万里长征的第一步。我们提及到规模是重要的吗?可能,你已经被说服,我们将会面对处理巨大数量的数据,和不寻常响应。
这个reduced的内存需求,由PreferenceArray和它的实现,带来的复杂性是值得的。削减内存需求的百分之七十五不只是节约一对M字节。在一个合理的规模上,它节约了10分之一G内存。这可能是在你现存的硬盘上是否装配之间的不同。这是是否必须投资大量的RAM和可能的一个新的64-bit系统之间的不同。那是一个小的,但真正节能的技术,非常重要。
3.2.1 FastByIDMap 和 FastIDSet
当你听到Mahout recommenders大量的使用如map和set的典型的数据结构时将不会感到奇怪,但是不要使用如TreeSet和HashMap的普通的Java实现。相反,遍历这个实现和API你将会找到FastByIDMap和FastIDSet。它们是像Map和set一样的程序,但是是被明确的详细说明,并只提供Mahout
recommenders需要的程序。它们减少内存占用而不是在性能上显著的增加。
这里没有一个像java中的Collections。但是,它们在一个大范围的环境内,为有效的目的而精心设计。它们不能对未来的使用做出更多的假设。Mahout的需要对可得到的用法有更加特殊,更强的假设。主要不同是:
。如同HashMap,FastByIDMap 是 hash-based。它使用线性探索而不是分离链接来处理hash collisions。这避免了一个额外的Map.Entry对象的每个入口的需要;如我们所讨论的,Objects占用了令人惊奇的内存数量。
。Keys和members在Mahout recommenders中总是长的基元,而在objects中则不是。使用长的keys节约内存并提高性能。
。Set的实现不是使用下面的一个Map来实现的。
。FastByIDMap可以像一个cache一样起作用,因为它有一个“maximum size”的概念;超过这个尺寸,当增加了新的entries时,infrequently-used entries将会被删除。
存储的不同是有意义的:与HashSet 的84个字节相比,FastIDSet平均每个member需要大约14个字节。与HashMap 的每个入口的84个字节再次比较,FastByIDMap每个入口占用28个字节。这显示当一个人对用法做了更强的假设时,有意义的改善是可能的:主要在内存需求上。考虑到为recommender系统提供的讨论中的数据量,这些习惯的实现不仅仅证明了它自己。所以,这些类用在哪里?
【译】mahout in action 3.3 内存中的DataModel
博客分类:
这是个抽象概念,在Mahout中,recommender的输入数据是DataModel。DataModel的实现为各种推荐器算法需要的数据提供了有效的使用。例如,一个DataModel可以在输入数据中,提供一个包括所有user IDs的列表,或提供与一个item相关联的所有分值,或者提供一个为一系列item IDs打分的所有用户的列表。我们将会集中研究一些highlights;一个关于DataModel的API的更详细的描述,可以通过在线文档中找到。
3.3.1 GenericDataModel
这个我们先来看一下,最简单的实现(在内存中实现),GenericDataModel。当你想用编程的方法,而不是基于一个现存的外部数据资源。例如一个文件或相关数据库在内存中创建你的数据表现时,这是非常合适的。它只是以这种形式把分值当作输入数据,这个形式就是一个FastByIDMap映射user IDs到有这些用户的数据的PreferenceArrays上。
列表3.2 基于GenericDataModel,定义输入数据
Java代码
FastByIDMap<PreferenceArray> preferences = new FastByIDMap<PreferenceArray>(); PreferenceArray prefsForUser1 = new GenericUserPreferenceArray(10); //A prefsForUser1.setUserID(0, 1L); prefsForUser1.setItemID(0, 101L); //B prefsForUser1.setValue(0, 3.0f); //B prefsForUser1.setItemID(1, 102L); prefsForUser1.setValue(1, 4.5f); … (8 more) 10. preferences.put(1L, prefsForUser1);// C 11. DataModel model = new GenericDataModel(preferences); //D
A为user 1建立PreferenceArray
B添加第一个preference,在刚刚创建的10中
C把user 1的preference添加到输入数据上
D创建DataModel
一个GenericDataModel使用多少内存?储存的分值的数目占内存占用的绝对优势。通过一些经验揭示,每一preference占用28个字节的Java heap space 。它包括所有的数据和其他次要数据结构--如指数。如果你喜欢你也可以尝试一下;下载一个GenericDataModel,调用 System.gc() ,几次后,比较Runtime.totalMemory()和Runtime.freeMemory()的结构。这是未加工过的,但应该可以给出一个合理的估计,这个估计就是数据占有多少内存。
3.3.2 基于文件的数据
通常我们不会直接地使用GenericDataModel,而是可能使用FileDataModel:
FileDataModel从一个文件中读取数据,并可以在内存中储存作为结果的分值数据,从而转化为GenericDataModel。
几乎任何一个合理的文件都将会这么做。我们在第一节里已经看到了一个这样文件的例子,在这节里,我们创造了一个简单的用逗号分割数据的文件,在这个文件里,每一行都包含一个数据:user ID,item ID,分值。使用Tab分割的文件也同样这么做。如果它们的名字各自以“.zip” 或“.gz”为后缀,使用对应的zip和gzip解压。在压缩格式储存这一数据是一个好想法,因为它是巨大的,并且被压缩好的。