代码之家  ›  专栏  ›  技术社区  ›  asmaier

如何优化我的PageRank计算?

  •  3
  • asmaier  · 技术社区  · 16 年前

    在书中 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()
    

    >>> 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()中的缩进。

    4 回复  |  直到 16 年前
        1
  •  2
  •   allyourcode    16 年前

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

    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
  •  1
  •   unutbu    16 年前

    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

    然后你就可以循环浏览结果并构建 inlinks , numoutlinks pagerank

    你也可以从使用 collections.defaultdict

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

    下面是 链接 一套格言。因为 您只需要不同的URL

    inlinks=collections.defaultdict(set)
    

    页面排名

    pagerank=collections.defaultdict(constant_factory(1.0))
    

    使用collections.defaultdict的优点是 不需要预先初始化dicts。

    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
  •  0
  •   Alex Martelli    16 年前

    你有足够的内存来容纳稀疏矩阵吗 (fromid, toid) 以某种形式?这将允许大的优化(大的算法变化)。至少,在内存中缓存 (fromid, numlinks) select count(*) 那个 O(N) 在太空中,如果你在处理 N

        4
  •  0
  •   asmaier    16 年前

    我在回答我自己的问题,因为最终发现,所有答案的混合对我最有效:

        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 联合国大学 我为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语句,您将失去精度(例如,我使用 ? 但29796100000001使用 .
    2. 在默认PageRank算法中,从一个页面到另一个页面的重复链接仅被视为一个链接。然而,书中的解决方案没有考虑到这一点。
    3. 书中的整个算法都有缺陷:原因是,在每次迭代中,pagerank得分都没有存储在第二个表中。但这意味着一次迭代的结果取决于循环页面的顺序,这可能会在几次迭代后极大地改变结果。要解决这个问题,要么使用一个额外的表/字典来存储pagerank以便下一次迭代,要么使用一个完全不同的算法,比如 Power Iteration .