生成 N*N 矩阵的高效算法

1 投票
2 回答
683 浏览
提问于 2025-04-17 20:08

我正在做一个简单的协同过滤(CF),具体是物品对物品的协同过滤。举个例子,我有一个很大的字典,里面有N个商品,字典的键是商品的名字,值是购买这些商品的顾客列表:

d={
item1:[customer1,customer3,customer7], 
item2:[customer3, customer5],
...
itemN:[customerX...customerY]
}

我还有一个小函数,用来计算每个商品之间顾客的相似度,比如说商品1和商品2之间的相似度:

def littlefunction(...):

    #convert them to a set
    item1=set(d['item1'])
    item2=set(d['item2'])

    commonCustomer=item1.intersect(item2)
    totalCustomer=item1.union(item2)

    similarity=float(len(commonCustomer))/len(totalCustomer)

为了找到每个指定商品的最相似商品,我需要扫描并计算N次相似度,然后再进行排序。所以对于N个商品来说,复杂度是O(N*N)

现在每个商品的运行时间是2分钟(N大约是300万)。要生成一个完整的N*N相似度表,估计需要10万小时。有没有比这种暴力计算更好的算法?其实每个商品只需要最前面的几个结果。

2 个回答

1

这其实和@Jim Mischel的回答是一个意思,就是建立一个明确的反向索引,不过这里没有建立一个明确的NxN矩阵,而是实现了一个具体的方案,而不是只是一个系统的草图。

当顾客数量很大时(比如说100万),性能就不是很好(大约需要20秒来进行3次相似度检查),不过数据是随机的。可以推测,实际顾客的购买数据中会有更多的相关性,随机性会少一些。

首先,生成一些用于测试的合成数据:

import random
import operator

NUM_CUSTOMERS = 10000
NUM_ITEMS = 1000
MIN_PER_CUST = 2
MAX_PER_CUST = 10
NUM_INTEREST = 10

customers = ["customer_{}".format(num) for num in xrange(1, NUM_CUSTOMERS+1)]
items = ["item_{}".format(num) for num in xrange(1, NUM_ITEMS+1)]

customer_items = {
    customer: random.sample(items, random.randint(MIN_PER_CUST, MAX_PER_CUST))
    for customer in customers}

item_customers = {}
for customer, this_items in customer_items.iteritems():
    for item in this_items:
        item_customers.setdefault(item, [])
        item_customers[item].append(customer)

然后定义几个函数:

def similarity(item1, item2):
    item1_set = set(item_customers[item1])
    item2_set = set(item_customers[item2])

    num_common = len(item1_set.intersection(item2_set))
    num_total = len(item1_set.union(item2_set))

    return float(num_common) / num_total

def similarity_for(item):
    to_check = {
        itm for customer in item_customers[item]
            for itm in customer_items[customer] }
    to_check.discard(item)
    rankings = {
        item2: similarity(item, item2)
        for item2 in to_check }
    return sorted(rankings.iteritems(), key=operator.itemgetter(1), reverse=True)

最后,运行它:

for index, item in enumerate(sorted(random.sample(items, 3))):
    rankings = similarity_for(item)
    print "check {}; {} (of {}):".format(index, item, len(rankings))
    for item_other, ranking in rankings[:NUM_INTEREST]:
        print "  {}: {}".format(item_other, ranking)

得到的结果看起来像这样:

% python /tmp/similarity.py
check 0; item_121 (of 309):
  item_520: 0.0283018867925
  item_361: 0.027027027027
  item_536: 0.0265486725664
  item_637: 0.0238095238095
  item_515: 0.020202020202
  item_750: 0.019801980198
  item_960: 0.0192307692308
  item_25: 0.0190476190476
  item_548: 0.018691588785
  item_841: 0.018691588785
check 1; item_714 (of 298):
  item_162: 0.0285714285714
  item_491: 0.0272727272727
  item_617: 0.0265486725664
  item_949: 0.0260869565217
  item_788: 0.0192307692308
  item_446: 0.0190476190476
  item_558: 0.018691588785
  item_606: 0.0181818181818
  item_177: 0.0181818181818
  item_577: 0.018018018018
check 2; item_991 (of 352):
  item_758: 0.0298507462687
  item_204: 0.025
  item_85: 0.0247933884298
  item_769: 0.024
  item_501: 0.0232558139535
  item_860: 0.0227272727273
  item_408: 0.0225563909774
  item_480: 0.0223880597015
  item_73: 0.0220588235294
  item_593: 0.021897810219
4

创建一个倒排索引,内容包括:

customer1: [item1, item3, item8, ...]
customer2: [item7, item8, item74, ...]

这样你就可以:

  1. 查找某个商品,获取购买该商品的顾客列表
  2. 查找每个顾客,获取该顾客购买的商品列表

这样每个商品的查询时间可以从2分钟缩短到不到2秒。

虽然这个第二个索引需要更多的内存,但你并没有重复存储数据。如果内存不够用,你可以把它存储在一个简单的数据库里,这样依然会比你现在使用的N^2算法快很多。

更多细节

你想创建一个N*N的矩阵,用来显示任意两个商品之间的相似度。使用我的方法,你可以这样做:

Create an N*N matrix, and initialize it to 0.
for each item
  Get the list of customers who bought the item (from your item-to-customer index).
  Create an empty dictionary of related items
  for each customer in that list
    for each item that the customer bought
      update the dictionary (add new item, or increase count)
    end for
  end for
  You now have a dictionary that contains the related items,
  and how many customers bought each one. You can update the matrix row
  for the current item from that dictionary.
end for

撰写回答