【问题标题】:How to optimize my PageRank calculation?如何优化我的 PageRank 计算?
【发布时间】:2010-03-20 19:41:13
【问题描述】:

Programming Collective Intelligence一书中,我找到了以下计算PageRank的函数:

def calculatepagerank(self,iterations=20):
    # clear out the current PageRank tables
    self.con.execute("drop table if exists pagerank")
    self.con.execute("create table pagerank(urlid primary key,score)")
    self.con.execute("create index prankidx on pagerank(urlid)")

    # initialize every url with a PageRank of 1.0
    self.con.execute("insert into pagerank select rowid,1.0 from urllist")
    self.dbcommit()

    for i in range(iterations):
        print "Iteration %d" % i
        for (urlid,) in self.con.execute("select rowid from urllist"):
            pr=0.15

            # Loop through all the pages that link to this one
            for (linker,) in self.con.execute("select distinct fromid from link where toid=%d" % urlid):
                # Get the PageRank of the linker
                linkingpr=self.con.execute("select score from pagerank where urlid=%d" % linker).fetchone()[0]

                # Get the total number of links from the linker
                linkingcount=self.con.execute("select count(*) from link where fromid=%d" % linker).fetchone()[0]

                pr+=0.85*(linkingpr/linkingcount)

            self.con.execute("update pagerank set score=%f where urlid=%d" % (pr,urlid))
        self.dbcommit()

但是,这个函数很慢,因为每次迭代都要进行所有的 SQL 查询

>>> import cProfile
>>> cProfile.run("crawler.calculatepagerank()")
         2262510 function calls in 136.006 CPU seconds

   Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000  136.006  136.006 <string>:1(<module>)
     1   20.826   20.826  136.006  136.006 searchengine.py:179(calculatepagerank)
    21    0.000    0.000    0.528    0.025 searchengine.py:27(dbcommit)
    21    0.528    0.025    0.528    0.025 {method 'commit' of 'sqlite3.Connecti
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler
1339864  112.602    0.000  112.602    0.000 {method 'execute' of 'sqlite3.Connec 
922600    2.050    0.000    2.050    0.000 {method 'fetchone' of 'sqlite3.Cursor' 
     1    0.000    0.000    0.000    0.000 {range}

于是我优化了函数,想出了这个:

def calculatepagerank2(self,iterations=20):
    # clear out the current PageRank tables
    self.con.execute("drop table if exists pagerank")
    self.con.execute("create table pagerank(urlid primary key,score)")
    self.con.execute("create index prankidx on pagerank(urlid)")

    # initialize every url with a PageRank of 1.0
    self.con.execute("insert into pagerank select rowid,1.0 from urllist")
    self.dbcommit()

    inlinks={}
    numoutlinks={}
    pagerank={}

    for (urlid,) in self.con.execute("select rowid from urllist"):
        inlinks[urlid]=[]
        numoutlinks[urlid]=0
        # Initialize pagerank vector with 1.0
        pagerank[urlid]=1.0
        # Loop through all the pages that link to this one
        for (inlink,) in self.con.execute("select distinct fromid from link where toid=%d" % urlid):
            inlinks[urlid].append(inlink)
            # get number of outgoing links from a page        
            numoutlinks[urlid]=self.con.execute("select count(*) from link where fromid=%d" % urlid).fetchone()[0]            

    for i in range(iterations):
        print "Iteration %d" % i

        for urlid in pagerank:
            pr=0.15
            for link in inlinks[urlid]:
                linkpr=pagerank[link]
                linkcount=numoutlinks[link]
                pr+=0.85*(linkpr/linkcount)
            pagerank[urlid]=pr
    for urlid in pagerank:
        self.con.execute("update pagerank set score=%f where urlid=%d" % (pagerank[urlid],urlid))
    self.dbcommit()

这个函数要快很多倍(但为所有临时字典使用更多内存),因为它避免了每次迭代中不必要的 SQL 查询:

>>> cProfile.run("crawler.calculatepagerank2()")
     90070 function calls in 3.527 CPU seconds
Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.004    0.004    3.527    3.527 <string>:1(<module>)
     1    1.154    1.154    3.523    3.523 searchengine.py:207(calculatepagerank2
     2    0.000    0.000    0.058    0.029 searchengine.py:27(dbcommit)
 23065    0.013    0.000    0.013    0.000 {method 'append' of 'list' objects}
     2    0.058    0.029    0.058    0.029 {method 'commit' of 'sqlite3.Connectio
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler
 43932    2.261    0.000    2.261    0.000 {method 'execute' of 'sqlite3.Connecti
 23065    0.037    0.000    0.037    0.000 {method 'fetchone' of 'sqlite3.Cursor'
     1    0.000    0.000    0.000    0.000 {range}

但是是否有可能进一步减少 SQL 查询的数量以进一步加快功能? 更新:修复了 calculatepagerank2() 中的缩进。

【问题讨论】:

    标签: python sql optimization pagerank


    【解决方案1】:

    如果您有一个非常大的数据库(例如 # 记录 ~ # WWW 中的页面),以类似于书中建议的方式使用数据库是有意义的,因为您将无法保留所有这些内存中的数据。

    如果您的数据集足够小,您可以(可能)通过不执行太多查询来改进您的第二个版本。尝试用这样的方式替换你的第一个循环:

    for urlid, in self.con.execute('select rowid from urllist'):
        inlinks[urlid] = []
        numoutlinks[urlid] = 0
        pagerank[urlid] = 1.0
    
    for src, dest in self.con.execute('select fromid, toid from link'):
        inlinks[dest].append(src)
        numoutlinks[src] += 1
    

    这个版本只执行 2 次查询,而不是 O(n^2) 次查询。

    【讨论】:

      【解决方案2】:

      我相信大部分时间都花在了这些 SQL 查询上:

      for (urlid,) in self.con.execute("select rowid from urllist"):
          ...
          for (inlink,) in self.con.execute("select distinct fromid from link where toid=%d" % urlid):
              ...
              numoutlinks[urlid]=self.con.execute("select count(*) from link where fromid=%d" % urlid).fetchone()[0]            
      

      假设您有足够的内存,您可以将其减少到只有两个查询:

      1. SELECT fromid,toid FROM link WHERE toid IN (SELECT rowid FROM urllist)
      2. SELECT fromid,count(*) FROM link WHERE fromid IN (SELECT rowid FROM urllist) GROUP BY fromid

      然后您可以遍历结果并构建inlinksnumoutlinkspagerank

      您也可以从使用collections.defaultdict 中受益:

      import collections
      import itertools
      def constant_factory(value):
          return itertools.repeat(value).next
      

      然后,以下内容使 inlinks 成为集合的字典。集合是合适的,因为 你只想要不同的网址

      inlinks=collections.defaultdict(set)
      

      这使得pagerank 成为一个默认值为 1.0 的字典:

      pagerank=collections.defaultdict(constant_factory(1.0))
      

      使用 collections.defaultdict 的好处是你 不需要预先初始化字典。

      所以,综合起来,我的建议看起来像这样:

      import collections
      def constant_factory(value):
          return itertools.repeat(value).next
      def calculatepagerank2(self,iterations=20):
          # clear out the current PageRank tables
          self.con.execute("DROP TABLE IF EXISTS pagerank")
          self.con.execute("CREATE TABLE pagerank(urlid primary key,score)")
          self.con.execute("CREATE INDEX prankidx ON pagerank(urlid)")
      
          # initialize every url with a PageRank of 1.0
          self.con.execute("INSERT INTO pagerank SELECT rowid,1.0 FROM urllist")
          self.dbcommit()
      
          inlinks=collections.defaultdict(set)
      
          sql='''SELECT fromid,toid FROM link WHERE toid IN (SELECT rowid FROM urllist)'''
          for f,t in self.con.execute(sql):
              inlinks[t].add(f)
      
          numoutlinks={}
          sql='''SELECT fromid,count(*) FROM link WHERE fromid IN (SELECT rowid FROM urllist) GROUP BY fromid'''
          for f,c in self.con.execute(sql):
              numoutlinks[f]=c
      
          pagerank=collections.defaultdict(constant_factory(1.0))
          for i in range(iterations):
              print "Iteration %d" % i
              for urlid in inlinks:
                  pr=0.15
                  for link in inlinks[urlid]:
                      linkpr=pagerank[link]
                      linkcount=numoutlinks[link]
                      pr+=0.85*(linkpr/linkcount)
                  pagerank[urlid]=pr
          sql="UPDATE pagerank SET score=? WHERE urlid=?"
          args=((pagerank[urlid],urlid) for urlid in pagerank)
          self.con.executemany(sql, args)
          self.dbcommit()
      

      【讨论】:

        【解决方案3】:

        您是否有足够的 RAM 以某种形式保存稀疏矩阵 (fromid, toid)?这将允许进行大的优化(具有大的算法更改)。至少,在内存中缓存(fromid, numlinks),你现在在最里面的循环中使用select count(*) 应该会有所帮助(我想那个缓存,如果你是空间中的O(N)处理N URL,更可能适合内存)。

        【讨论】:

          【解决方案4】:

          我正在回答我自己的问题,因为最后发现所有答案的混合最适合我:

              def calculatepagerank4(self,iterations=20):
              # clear out the current PageRank tables
              self.con.execute("drop table if exists pagerank")
              self.con.execute("create table pagerank(urlid primary key,score)")
              self.con.execute("create index prankidx on pagerank(urlid)")
          
              # initialize every url with a PageRank of 1.0
              self.con.execute("insert into pagerank select rowid,1.0 from urllist")
              self.dbcommit()
          
              inlinks={}
              numoutlinks={}
              pagerank={}
          
              for (urlid,) in self.con.execute("select rowid from urllist"):
                  inlinks[urlid]=[]
                  numoutlinks[urlid]=0
                  # Initialize pagerank vector with 1.0
                  pagerank[urlid]=1.0
          
              for src,dest in self.con.execute("select distinct fromid, toid from link"):
                  inlinks[dest].append(src)
                  numoutlinks[src]+=1          
          
              for i in range(iterations):
                  print "Iteration %d" % i
          
                  for urlid in pagerank:
                      pr=0.15
                      for link in inlinks[urlid]:
                          linkpr=pagerank[link]
                          linkcount=numoutlinks[link]
                          pr+=0.85*(linkpr/linkcount)
                      pagerank[urlid]=pr
          
              args=((pagerank[urlid],urlid) for urlid in pagerank)
              self.con.executemany("update pagerank set score=? where urlid=?" , args)
              self.dbcommit() 
          

          所以我按照allyourcode 的建议替换了前两个循环,但另外还使用了˜unutbu 的解决方案中的executemany()。但与˜unutbu 不同的是,我对 args 使用生成器表达式,以不浪费太多内存,尽管使用列表推导式要快一点。最后这个程序比书中建议的程序快了 100 倍:

          >>> cProfile.run("crawler.calculatepagerank4()")
               33512 function calls in 1.377 CPU seconds
          Ordered by: standard name
          
          ncalls  tottime  percall  cumtime  percall filename:lineno(function)
               1    0.004    0.004    1.377    1.377 <string>:1(<module>)
               2    0.000    0.000    0.073    0.036 searchengine.py:27(dbcommit)
               1    0.693    0.693    1.373    1.373 searchengine.py:286(calculatepagerank4
           10432    0.011    0.000    0.011    0.000 searchengine.py:321(<genexpr>)
           23065    0.009    0.000    0.009    0.000 {method 'append' of 'list' objects}
               2    0.073    0.036    0.073    0.036 {method 'commit' of 'sqlite3.Connectio
               1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler
               6    0.379    0.063    0.379    0.063 {method 'execute' of 'sqlite3.Connecti
               1    0.209    0.209    0.220    0.220 {method 'executemany' of 'sqlite3.Conn
               1    0.000    0.000    0.000    0.000 {range}
          

          还应注意以下问题:

          1. 如果您使用带有%f 的字符串格式而不是使用占位符? 来构造SQL 语句,您将失去精度(例如,我使用? 得到2.9796095721920315,但使用%f 得到2.9796100000000001。
          2. 在默认 PageRank 算法中,从一个页面到另一个页面的重复链接仅被视为一个链接。然而,书中的解决方案并未考虑到这一点。
          3. 书中的整个算法有缺陷:原因是,在每次迭代中,pagerank 分数没有存储在第二个表中。但这意味着迭代的结果取决于循环通过的页面的顺序,这可能会在几次迭代后极大地改变结果。要解决这个问题,要么必须使用额外的表/字典来存储下一次迭代的 pagerank,要么使用完全不同的算法,如Power Iteration

          【讨论】:

            猜你喜欢
            • 2010-11-30
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多