代码改变世界

全文检索、数据挖掘、推荐引擎系列7---条目相似度算法

2011-08-29 17:11  java ee spring  阅读(479)  评论(0编辑  收藏  举报

在实际的项目中,有许多场合需要进行条目相似度计算,比如在电商系统中,经常有喜欢这个商品的用户还喜欢,通常计算商品的相似度是实现这种功能的方法之一,这可以视为一种基于内容的推荐系统的应用。同时,计算相似度不仅可以用于推荐商品,利用同样的算法,我们还可以计算出用户的相似度,可以向用户推荐其感兴趣的其他用户。与文本分析不同,对相似度的计算一般基于与用户的交互数据,如用户对商品进行投票、打分、浏览、购买等行为,经过适当的流程,将这些交互数据进行数字化,如浏览、购买、投票与否用0/1表示,对打分用实际的分数计算。

这类算法与文本分析算法相比具有两个明显的优势:第一是文本分析算法需要处理英文和中文问题,并且不同语言的处理方法会相当不同,如中文分词就比英文分词复杂很多,但是采用相似度计算方法,就不存在不同语言处理问题;第二是相似度计算以用户与系统间交互数据为依据,这样可以更好的反映某些热点条目,从而使推荐结果更具有时效性。

当然,利用计算相似度来进行推荐,由于是基于数据挖掘技术,难免会受原始数据中噪音的影响,而且由于当前网络环境,水军、门户网站位置营销或SEO等技术的采用,可能使某些质量低劣的项目成为热点,因此而降低了推荐质量。因此在使用相似度推荐时,需要综合考虑各种因素,找到最适合的推荐算法。

下面我们以用户对商品进行打分为例,描述相似度的计算方法:

  product1 product2 product3
user1 3 4 2
user2 2 2 4
user3 1 3 5

表1

为了简单起见,我们假设三个用户对三个商品进行了打分,分数如表1所示。我们首先想通过该数据计算产品的相似度,因此我们需要将数据进行重新整理:

  user1 user2 user3
product1 3 2 1
product2 4 2 3
product3 2 4 5

表2

到此原始数据准备完毕,下面就是计算相似度算法了。

通常相似度需要按相似度大小进行排序,我们定义了ItemInfo类来保存相似度排序列表中所需要的信息。

public class ItemInfo implements Comparable<ItemInfo> {
 public String getKey() {
  return key;
 }
 public void setKey(String key) {
  this.key = key;
 }
 public double getVal() {
  return val;
 }
 public void setVal(double val) {
  this.val = val;
 }
 private String key = null;
 private double val = 0.0;
 @Override
 public int compareTo(ItemInfo o) {
  // TODO Auto-generated method stub
  ItemInfo info = (ItemInfo)o;
  if (val > info.getVal()) {
   return -1;
  } else {
   return 1;
  }
 }
}

我们实现了比较接口,这样便于对本类组成的List列表进行排序操作。

我们需要定义一个某个产品与其它相品相似度存储列表类:

public class SmlrList {
 public SmlrList() {
  rates = new Hashtable<String, Double>();
  sortedKeys = new Vector<String>();
  sortedVals = new Vector<ItemInfo>();
 }
 public List<ItemInfo> getSortedVals() {
  return sortedVals;
 }
 public void setSortedVals(List<ItemInfo> sortedVals) {
  this.sortedVals = sortedVals;
 }
 public List<String> getSortedKeys() {
  return sortedKeys;
 }
 public void setSortedKeys(List<String> sortedKeys) {
  this.sortedKeys = sortedKeys;
 }
 public Hashtable<String, Double> getRates() {
  return rates;
 }
 public void setRates(Hashtable<String, Double> rates) {
  this.rates = rates;
 }
 
 private List<ItemInfo> sortedVals = null;
 private List<String> sortedKeys = null; // 存储按照相似度排序的key值
 private Hashtable<String, Double> rates = null; // 与其他元素的相似度列表
}
由上面的代码可以看出,该产品与其他产品相似度存储在rates列表中,为调试方便,我们加入了sortedKeys,可以按产品序号顺序显示相似度,按相似度由大到小排序的列表是sortedVals。注意:在实际系统中,可以只取sortedVals即可,其它属性需是用于调试目的。

具体调用代码如下所示:

/**
  * 计算条目的相似度列表,可以是如产品、地点的相似度,也可以是用户的相似度
  * 原始数据如下所示:
  *
  */
 public void test() {
  Hashtable<String, SmlrList> itemsSmlrList = null;
  Vector<String> sortedItemId = null; // 经过排序的条目编号
  Hashtable<String, Hashtable<String, Double>> rateData = null; // 条目被每个用户评分的信息
  Hashtable<String, Double> itemRateData = null;
  
  // 初始化原始数据
  rateData = new Hashtable<String, Hashtable<String, Double>>();
  int rowId = 1;
  int colId = 1;
  // 加入第三行
  rowId = 3;
  itemRateData = new Hashtable<String, Double>();
  colId = 1;
  itemRateData.put("" + colId, 2.0);
  colId = 2;
  itemRateData.put("" + colId, 4.0);
  colId = 3;
  itemRateData.put("" + colId, 5.0);
  rateData.put("" + rowId, itemRateData);
  // 加入第一行
  rowId = 1;
  itemRateData = new Hashtable<String, Double>();
  colId = 1;
  itemRateData.put("" + colId, 3.0);
  colId = 2;
  itemRateData.put("" + colId, 2.0);
  colId = 3;
  itemRateData.put("" + colId, 1.0);
  rateData.put("" + rowId, itemRateData);
  // 加入第二行
  rowId = 2;
  itemRateData = new Hashtable<String, Double>();
  colId = 1;
  itemRateData.put("" + colId, 4.0);
  colId = 2;
  itemRateData.put("" + colId, 2.0);
  colId = 3;
  itemRateData.put("" + colId, 3.0);
  rateData.put("" + rowId, itemRateData);
  
  sortedItemId = new Vector<String>(rateData.keySet());
  Collections.sort(sortedItemId);
  // 对原始数据进行归一化并显示
  Hashtable<String, Double> normUserRateData = null;
  Vector<String> sortedUk = null;
  for (String rowKey : sortedItemId) {
   double sum = 0.0;
   for (Double dbl : rateData.get(rowKey).values()) {
    sum += dbl.doubleValue() * dbl.doubleValue();
   }
   sum = Math.sqrt(sum);
   normUserRateData = new Hashtable<String, Double>();
   itemRateData = rateData.get(rowKey);
   for (String colKey : itemRateData.keySet()) {
    normUserRateData.put(colKey, itemRateData.get(colKey).doubleValue() / sum);
   }
   rateData.remove(rowKey);
   rateData.put(rowKey, normUserRateData);
   // 打印
   sortedUk = new Vector<String>(rateData.get(rowKey).keySet());
   Collections.sort(sortedUk);
   for (String suk : sortedUk) {
    System.out.print(" " + suk + ":" + rateData.get(rowKey).get(suk).doubleValue());
   }
   System.out.print("\r\n");
  }
  
  
  // 计算条目之间的相似度
  itemsSmlrList = new Hashtable<String, SmlrList>();
  SmlrList smlrList = null;
  ItemInfo itemInfo = null;
  int i = 0;
  int j = 0;
  double smlrVal = 0.0;
  for (i=0; i<sortedItemId.size(); i++) {
   smlrList = new SmlrList();
   for (j=0; j<sortedItemId.size(); j++) {
    smlrVal = calDotProd(new Vector<Double>(rateData.get(sortedItemId.get(i)).values()),
      new Vector<Double>(rateData.get(sortedItemId.get(j)).values()));
    smlrList.getSmlrs().put(sortedItemId.get(j), smlrVal);
    smlrList.getSortedKeys().add(sortedItemId.get(j));
    itemInfo = new ItemInfo();
    itemInfo.setKey(sortedItemId.get(j));
    itemInfo.setVal(smlrVal);
    smlrList.getSortedVals().add(itemInfo);
   }
   Collections.sort(smlrList.getSortedKeys());
   Collections.sort(smlrList.getSortedVals());
   itemsSmlrList.put(sortedItemId.get(i), smlrList);
  }
  
  // 显示相似度结果
  SmlrList sl02 = null;
  for (String uk2 : sortedItemId) {
   sl02 = itemsSmlrList.get(uk2);
   System.out.print(uk2 + ":");
   for (String uk3 : sl02.getSortedKeys()) {
    System.out.print(" " + sl02.getSmlrs().get(uk3) + "[" + uk3 + "] ");
   }
   System.out.print("\r\n");
  }
  System.out.println("**************************************");
  for (String rowKey : sortedItemId) {
   smlrList = itemsSmlrList.get(rowKey);
   System.out.print(rowKey + ":");
   for (ItemInfo itemInfo1 : smlrList.getSortedVals()) {
    if (!itemInfo1.getKey().equals(rowKey)) {
     System.out.print(" " + itemInfo1.getVal() + "[" + itemInfo1.getKey() + "] ");
    }
   }
   System.out.print("\r\n");
  }
 }

程序运行结果为:

 1:0.8017837257372732 2:0.5345224838248488 3:0.2672612419124244
 1:0.7427813527082074 2:0.3713906763541037 3:0.5570860145311556
 1:0.29814239699997197 2:0.5962847939999439 3:0.7453559924999299
1: 1.0[1]  0.9429541672723838[2]  0.756978119245116[3]
2: 0.9429541672723838[1]  1.0[2]  0.8581366251553131[3]
3: 0.756978119245116[1]  0.8581366251553131[2]  1.0[3]
**************************************
1: 0.9429541672723838[2]  0.756978119245116[3]
2: 0.9429541672723838[1]  0.8581366251553131[3]
3: 0.8581366251553131[2]  0.756978119245116[1]

 

同理,如果我们需要计算用户的相似度,只需要将原始数据变为表1格式读入程序即可。

如上所述,有了上述类和方法,我们可以很容易求出任意两个条目的相似度,同时可以对某个条目按相似度从大到小的方式,显示该条目与其他条目相似度。

相似度度量这里采用向量的点积,点积值越大,表明对应条目的相似度越大,具体计算方法如下所示:


 public double calDotProd(List<Double> vec1, List<Double> vec2) {
  double dotProd = 0.0;
  for (int i=0; i<vec1.size(); i++) {
   dotProd += vec1.get(i).doubleValue() * vec2.get(i).doubleValue();
  }
  return dotProd;
 }