集体智慧编程(三)搜索和排名

作者博客网站:http://andyheart.me

本文主要介绍了一种全文的搜索引擎,允许人们在大量的文档中搜索出来一系列单词,并根据文档与单词的相关度对结果进行排名。

本章主要介绍了以下几个方面的内容

  1. 爬虫爬取网页(获取文档)
  2. 搜索引擎的搜索部分
  3. 基于内容的排名
  4. 外部回指链接排名法和PageRank算法
  5. 从点击行为中学习(简单人工神经网络)

爬虫爬取网页(获取文档)

本节主要使用python 建立了一个简单的爬虫程序,来爬取一些网页上的内容,从而将链接和内容获取到,以及网页之间的链接关系。主要用的技术手段有

  • urllib2 ————— python自带的模块
  • BeautifulSoup ———— 可以解析HTML的python外部模块
  • SQLite ————- 一个简单数据库

在爬虫爬取到内容以后,还要做分词操作,英文分词用空格,中文分词需要找到相应的中文分词模块。下面贴了本部分的代码,可以自行研究。


import urllib2
from BeautifulSoup import *
from urlparse import urljoin
from pysqlite2 import dbapi2 as sqlite

# Create a list of words to ignore
ignorewords={'the':1,'of':1,'to':1,'and':1,'a':1,'in':1,'is':1,'it':1}
class crawler:
  # Initialize the crawler with the name of database
  def __init__(self,dbname):
    self.con=sqlite.connect(dbname)

  def __del__(self):
    self.con.close()

  def dbcommit(self):
    self.con.commit()

  # Auxilliary function for getting an entry id and adding 
  # it if it's not present
  def getentryid(self,table,field,value,createnew=True):
    cur=self.con.execute(
    "select rowid from %s where %s='%s'" % (table,field,value))
    res=cur.fetchone()
    if res==None:
      cur=self.con.execute(
      "insert into %s (%s) values ('%s')" % (table,field,value))
      return cur.lastrowid
    else:
      return res[0] 


  # Index an individual page
  def addtoindex(self,url,soup):
    if self.isindexed(url): return
    print 'Indexing '+url

    # Get the individual words
    text=self.gettextonly(soup)
    words=self.separatewords(text)

    # Get the URL id
    urlid=self.getentryid('urllist','url',url)

    # Link each word to this url
    for i in range(len(words)):
      word=words[i]
      if word in ignorewords: continue
      wordid=self.getentryid('wordlist','word',word)
      self.con.execute("insert into wordlocation(urlid,wordid,location) values (%d,%d,%d)" % (urlid,wordid,i))



  # Extract the text from an HTML page (no tags)
  def gettextonly(self,soup):
    v=soup.string
    if v==Null:   
      c=soup.contents
      resulttext=''
      for t in c:
        subtext=self.gettextonly(t)
        resulttext+=subtext+'\n'
      return resulttext
    else:
      return v.strip()

  # Seperate the words by any non-whitespace character
  def separatewords(self,text):
    splitter=re.compile('\\W*')
    return [s.lower() for s in splitter.split(text) if s!='']


  # Return true if this url is already indexed
  def isindexed(self,url):
    return False

  # Add a link between two pages
  def addlinkref(self,urlFrom,urlTo,linkText):
    words=self.separateWords(linkText)
    fromid=self.getentryid('urllist','url',urlFrom)
    toid=self.getentryid('urllist','url',urlTo)
    if fromid==toid: return
    cur=self.con.execute("insert into link(fromid,toid) values (%d,%d)" % (fromid,toid))
    linkid=cur.lastrowid
    for word in words:
      if word in ignorewords: continue
      wordid=self.getentryid('wordlist','word',word)
      self.con.execute("insert into linkwords(linkid,wordid) values (%d,%d)" % (linkid,wordid))

  # Starting with a list of pages, do a breadth
  # first search to the given depth, indexing pages
  # as we go
  def crawl(self,pages,depth=2):
    for i in range(depth):
      newpages={}
      for page in pages:
        try:
          c=urllib2.urlopen(page)
        except:
          print "Could not open %s" % page
          continue
        try:
          soup=BeautifulSoup(c.read())
          self.addtoindex(page,soup)

          links=soup('a')
          for link in links:
            if ('href' in dict(link.attrs)):
              url=urljoin(page,link['href'])
              if url.find("'")!=-1: continue
              url=url.split('#')[0]  # remove location portion
              if url[0:4]=='http' and not self.isindexed(url):
                newpages[url]=1
              linkText=self.gettextonly(link)
              self.addlinkref(page,url,linkText)

          self.dbcommit()
        except:
          print "Could not parse page %s" % page

      pages=newpages


  # Create the database tables
  def createindextables(self): 
    self.con.execute('create table urllist(url)')
    self.con.execute('create table wordlist(word)')
    self.con.execute('create table wordlocation(urlid,wordid,location)')
    self.con.execute('create table link(fromid integer,toid integer)')
    self.con.execute('create table linkwords(wordid,linkid)')
    self.con.execute('create index wordidx on wordlist(word)')
    self.con.execute('create index urlidx on urllist(url)')
    self.con.execute('create index wordurlidx on wordlocation(wordid)')
    self.con.execute('create index urltoidx on link(toid)')
    self.con.execute('create index urlfromidx on link(fromid)')
    self.dbcommit()

搜索引擎的搜索部分

上一个部分完成了文档的收集和索引,本节的主要内容是完成查询功能。上一节的createindextables方法中建立了五张表,每个表的具体内容可以查询书籍P59看到。

wordlocation中保存了单词在网页中的位置,可以方便的进行查询,而且我们要实现多词查询。因此searcher类中的代码如下所示。

class searcher:
  def __init__(self,dbname):
    self.con=sqlite.connect(dbname)

  def __del__(self):
    self.con.close()

  def getmatchrows(self,q):
    # Strings to build the query
    fieldlist='w0.urlid'
    tablelist=''  
    clauselist=''
    wordids=[]

    # Split the words by spaces
    words=q.split(' ')  
    tablenumber=0

    for word in words:
      # Get the word ID
      wordrow=self.con.execute(
      "select rowid from wordlist where word='%s'" % word).fetchone()
      if wordrow!=None:
        wordid=wordrow[0]
        wordids.append(wordid)
        if tablenumber>0:
          tablelist+=','
          clauselist+=' and '
          clauselist+='w%d.urlid=w%d.urlid and ' % (tablenumber-1,tablenumber)
        fieldlist+=',w%d.location' % tablenumber
        tablelist+='wordlocation w%d' % tablenumber      
        clauselist+='w%d.wordid=%d' % (tablenumber,wordid)
        tablenumber+=1

    # Create the query from the separate parts
    fullquery='select %s from %s where %s' % (fieldlist,tablelist,clauselist)
    print fullquery
    cur=self.con.execute(fullquery)
    rows=[row for row in cur]

    return rows,wordids

多词查询的时候,根据单词位置的不同组合,每个urlid可能返回多词,这样的搜索结果只是按照其被检索时的顺序来返回的,因此我们需要几种方法来改进搜索结果的排名顺序。

基于内容的排名

顾名思义,基于内容的排名就是根据单词在文档中的出现次数,出现位置等等相关的有意义的内容来进行文档的排名。本节主要介绍以下三种:

  • 单词频度(单词在文档中出现的次数)
  • 文档位置(文档的主题有可能出现在靠近文档的开始处)
  • 单词距离(如果查询条件中有多个单词,则它们在文档中应该很靠近)

下面的代码,接受查询请求,将获取到的行集放到字典中,并以格式化列表的形式显示输出:

  def getscoredlist(self,rows,wordids):
    totalscores=dict([(row[0],0) for row in rows])

    # 这里放置的是不同的排名方法函数
    weights=[(1.0,self.locationscore(rows)), 
             (1.0,self.frequencyscore(rows)),
             (1.0,self.pagerankscore(rows)),
             (1.0,self.linktextscore(rows,wordids)),
             (5.0,self.nnscore(rows,wordids))]
    for (weight,scores) in weights:
      for url in totalscores:
        totalscores[url]+=weight*scores[url]

    return totalscores

  def geturlname(self,id):
    return self.con.execute(
    "select url from urllist where rowid=%d" % id).fetchone()[0]

  def query(self,q):
    rows,wordids=self.getmatchrows(q)
    scores=self.getscoredlist(rows,wordids)
    rankedscores=[(score,url) for (url,score) in scores.items()]
    rankedscores.sort()
    rankedscores.reverse()
    for (score,urlid) in rankedscores[0:10]:
      print '%f\t%s' % (score,self.geturlname(urlid))
    return wordids,[r[1] for r in rankedscores[0:10]]

在上面的代码中我们可以看到有不同的评价函数,不同的评价函数可能评价方式不同,有的分数越高越好,有的越低越好,因此我们需要一个归一化的函数,将评分控制在相同的值域和变化方向。代码如下:

  def normalizescores(self,scores,smallIsBetter=0):
    vsmall=0.00001 # Avoid division by zero errors
    if smallIsBetter:
      minscore=min(scores.values())
      return dict([(u,float(minscore)/max(vsmall,l)) for (u,l) in scores.items()])
    else:
      maxscore=max(scores.values())
      if maxscore==0: maxscore=vsmall
      return dict([(u,float(c)/maxscore) for (u,c) in scores.items()])

单词频度评价方法

这种评价方法很简单,根据文档中出现的次数返回列表,次数越多,分数越高。代码如下:

  def frequencyscore(self,rows):
    counts=dict([(row[0],0) for row in rows])
    for row in rows: counts[row[0]]+=1
    return self.normalizescores(counts)

文档位置评价方法

wordlocation记录了单词在文档中出现的位置,单词出现的位置越靠前,得分越高。代码如下:

  def locationscore(self,rows):
    locations=dict([(row[0],1000000) for row in rows])
    for row in rows:
      loc=sum(row[1:])
      if loc<locations[row[0]]: locations[row[0]]=loc

    return self.normalizescores(locations,smallIsBetter=1)

单词距离

不解释了,看上面的解释吧。代码如下:

  def distancescore(self,rows):
    # If there's only one word, everyone wins!
    if len(rows[0])<=2: return dict([(row[0],1.0) for row in rows])

    # Initialize the dictionary with large values
    mindistance=dict([(row[0],1000000) for row in rows])

    for row in rows:
      dist=sum([abs(row[i]-row[i-1]) for i in range(2,len(row))])
      if dist<mindistance[row[0]]: mindistance[row[0]]=dist
    return self.normalizescores(mindistance,smallIsBetter=1)

外部回指链接排名和PageRank

Tips:利用本节方法进行排名的时候还应该利用上一节中任何一种度量方法。

本节采用外界就该网页提供的信息—尤其是谁链向了该网页,以及他们对该网页的评价,来对于网页进行排名。对于每个遇到的链接,links表中记录了与其源和目的相对应的URL ID,而且linkwords表还记录了单词与链接的关联。本节主要介绍以下三个方面内容:

  • 简单计数
  • PageRank
  • 利用链接文本

简单计数

统计每个网页上链接的数目,并将链接总数作为针对网页的度量。科研论文的评价经常采用这种方式。下面的代码中通过对查询link表所得到的行集中的每个唯一的URL ID进行计数,建立起了一个字典。随后,函数返回一个经过归一化处理的评价结果。

  def inboundlinkscore(self,rows):
    uniqueurls=dict([(row[0],1) for row in rows])
    inboundcount=dict([(u,self.con.execute('select count(*) from link where toid=%d' % u).fetchone()[0]) for u in uniqueurls])   
    return self.normalizescores(inboundcount)

PageRank

上一种方法中我们看到对于每一个链接,我们给予了相同的权重。在PageRank中,为每一个网页都赋予了一个指示网页重要程度的评价值。网页的重要程度是依据指向该网页的所有其他网页的重要性,以及这些网页中所包含的链接数求得的。

详细的PageRank算法可以自行查阅书籍或者网络。下面将在crawler类中添加一个计算PageRank的方法:

  def calculatepagerank(self,iterations=20):
    # clear out the current page rank tables
    self.con.execute('drop table if exists pagerank') self.con.execute('create table pagerank(urlid primary key,score)') # initialize every url with a page rank of 1 for (urlid,) in self.con.execute('select rowid from urllist'): self.con.execute('insert into pagerank(urlid,score) values (%d,1.0)' % urlid) 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 page rank 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()

利用链接文本

这个方法是根据指向某一网页的链接文本来决定网页的相关程度。

实现的代码如下所示:

  def linktextscore(self,rows,wordids):
    linkscores=dict([(row[0],0) for row in rows])
    for wordid in wordids:
      cur=self.con.execute('select link.fromid,link.toid from linkwords,link where wordid=%d and linkwords.linkid=link.rowid' % wordid)
      for (fromid,toid) in cur:
        if toid in linkscores:
          pr=self.con.execute('select score from pagerank where urlid=%d' % fromid).fetchone()[0]
          linkscores[toid]+=pr
    maxscore=max(linkscores.values())
    normalizedscores=dict([(u,float(l)/maxscore) for (u,l) in linkscores.items()])
    return normalizedscores

从点击行为中学习

在搜索引擎的应用过程中,会持续收到以用户行为为表现形式的反馈信息。比如每一位用户可以通过只点击某条搜索结果,而不选择点击其他内容,向引擎及时提供有关于他对搜索结果喜好程度的信息。

本节我们构造一个人工神经网络,记录用户点击查询结果的情况从而改进搜索结果的排名。因此我们需要提供:查询条件中的单词,返回给用户的搜索结果,以及用户的点击决策,然后再对其加以训练。一旦网络经过了许多不同的查询以后,我们就可以利用它来改进搜索结果的排序,以更好地反映用户在过去一段时间里的实际点击情况。

多层感知机是神经网络的一种,此类网络由多层神经元(一组节点)构造而成,其中第一层神经元接受输入,最后一层神经元给予输出。可以用多个中间层,外界无法直接与其交互,因此被称为隐藏层,职责是对输入进行组合。

在本例中,输入为用户输入的单词,输出为不同URL的权重列表。

(一)设置数据库

神经网络在不断的训练,因此需要将反映网络现状的信息存入数据库中,数据库中已经有了一张涉及单词和URL的数据表,另外还需要一张代表隐藏层的数据表(hiddennode),以及两张反映网络节点连接状况的表(一张从单词层到隐藏层,一张从隐藏层到输出层)。

在新的nn.py文件中,新建一个类,取名searchnet,代码如下:

from math import tanh
from pysqlite2 import dbapi2 as sqlite

def dtanh(y):
    return 1.0-y*y

class searchnet:
    def __init__(self,dbname):
      self.con=sqlite.connect(dbname)

    def __del__(self):
      self.con.close()

    def maketables(self):
      self.con.execute('create table hiddennode(create_key)')
      self.con.execute('create table wordhidden(fromid,toid,strength)')
      self.con.execute('create table hiddenurl(fromid,toid,strength)')
      self.con.commit()

我们需要不断的判断当前连接的强度,需要一个方法来做这件事(getstrength)。由于新连接只在必要的时候才会被创建,因此该方法在连接不存在时将会返回一个默认值。对于从单词层到隐藏层的默认值为-0.2,隐藏层到URL的连接默认值为0.代码如下:

    def getstrength(self,fromid,toid,layer):
      if layer==0: table='wordhidden'
      else: table='hiddenurl'
      res=self.con.execute('select strength from %s where fromid=%d and toid=%d' % (table,fromid,toid)).fetchone()
      if res==None: 
          if layer==0: return -0.2
          if layer==1: return 0
      return res[0]

还需要一个方法(setstrength),用以判断连接是否已经存在,并利用新的强度值更新连接或者创建连接。代码如下:

    def setstrength(self,fromid,toid,layer,strength):
      if layer==0: table='wordhidden'
      else: table='hiddenurl'
      res=self.con.execute('select rowid from %s where fromid=%d and toid=%d' % (table,fromid,toid)).fetchone()
      if res==None: 
        self.con.execute('insert into %s (fromid,toid,strength) values (%d,%d,%f)' % (table,fromid,toid,strength))
      else:
        rowid=res[0]
        self.con.execute('update %s set strength=%f where rowid=%d' % (table,strength,rowid))

每传入一组从未见过的单词组合,该函数就会在隐藏层中建立一个新的节点。随后会在各层之间建立默认权重的连接。代码如下:

    def generatehiddennode(self,wordids,urls):
      if len(wordids)>3: return None
      # Check if we already created a node for this set of words
      sorted_words=[str(id) for id in wordids]
      sorted_words.sort()
      createkey='_'.join(sorted_words)
      res=self.con.execute(
      "select rowid from hiddennode where create_key='%s'" % createkey).fetchone()

      # If not, create it
      if res==None:
        cur=self.con.execute(
        "insert into hiddennode (create_key) values ('%s')" % createkey)
        hiddenid=cur.lastrowid
        # Put in some default weights
        for wordid in wordids:
          self.setstrength(wordid,hiddenid,0,1.0/len(wordids))
        for urlid in urls:
          self.setstrength(hiddenid,urlid,1,0.1)
        self.con.commit()

未完待续。

你可能感兴趣的:(python)