HITS算法--从原理到实现

本文介绍HITS算法的相关内容。html

1.算法来源
2.算法原理
3.算法证实
4.算法实现
4.1 基于迭代法的简单实现
4.2 MapReduce实现
5.HITS算法的缺点
6.写在最后
参考资料node


1. 算法来源

1999年,Jon Kleinberg 提出了HITS算法。做为几乎是与PageRank同一时期被提出的算法,HITS一样以更精确的搜索为目的,并到今天仍然是一个优秀的算法。python

HITS算法的全称是Hyperlink-Induced Topic Search。在HITS算法中,每一个页面被赋予两个属性:hub属性和authority属性。同时,网页被分为两种:hub页面和authority页面。hub,中心的意思,因此hub页面指那些包含了不少指向authority页面的连接的网页,好比国内的一些门户网站;authority页面则指那些包含有实质性内容的网页。HITS算法的目的是:当用户查询时,返回给用户高质量的authority页面。算法


2. 算法原理

不少算法都是创建在一些假设之上的,HITS算法也不例外。HITS算法基于下面两个假设[^ref_1]:spring

  • 一个高质量的authority页面会被不少高质量的hub页面所指向。
  • 一个高质量的hub页面会指向不少高质量的authority页面。

什么叫“高质量”,这由每一个页面的hub值和authority值肯定。其肯定方法为:网络

  • 页面hub值等于全部它指向的页面的authority值之和。
  • 页面authority值等于全部指向它的页面的hub值之和。

为了让你们快速理解HITS算法,先举一个简单的例子[^ref_2]。app

hits1

图中共有3个网页,它们构成了一个有向图。咱们设每一个网页的初始hub值和authority值都为1。记\(h(p)\)为页面\(p\)的hub值,\(a(p)\)为页面\(p\)的authority值。则有\(h(1)=h(2)=h(3)=1\)\(a(1)=a(2)=a(3)=1\)框架

HITS算法的计算过程也是一个迭代的过程。在第一次迭代中,有:分布式

\[ a(1) = 0, a(2) = 0, a(3) = h(1) + h(2) = 2 (没有页面指向网页1和网页2)\\ h(1) = a(3) = 2, h(2) = a(3) = 2, h(3) = 0 (网页3没有指向任何页面) \]ide

这里就已经能够看出网页3是一个相对好的authority页面,而网页1和网页2是相对好的hub页面。其实到这里迭代也能够结束了,由于再迭代下去无非是\(a(3)\)\(h(1)\)\(h(2)\)的值不断增大,而哪一个是hub页面,哪一个是authority页面并不会改变。

上面的简单例子只是为了帮助理解,省略掉了不少步骤和细节。下面将详细地介绍HITS算法[^ref_3]:。

与PageRank算法不一样,HITS算法是在用户搜索后运行的,因此HITS算法的处理对象集合确定得小不少。

首先,咱们须要肯定这个集合。整个互联网中的网页之间的关系能够抽象为一个有向图\(G = (V,E)\),当有一个搜索请求产生时(不妨设关键字为\(\sigma\)),咱们能够取全部包含关键字\(\sigma\)的网页组成的集合\(Q_\sigma\)为初始集合,并在这个集合上运行咱们的HITS算法。然而,这个集合却有着明显的缺陷:这个集合可能很是之大,大到包含了数百万个网页,而这显然不是理想的集合大小。因而,咱们进而想找到一个更小的集合\(S_\sigma\),知足如下条件:

  1. \(S_\sigma\)确实足够小。
  2. \(S_\sigma\)包含不少与查询相关的页面。
  3. \(S_\sigma\)包含不少高质量的authority页面。

如何找到这个\(S_\sigma\)集合?咱们假设用户输入关键字搜索,搜索引擎使用一个基于文本的引擎进行搜索。而后咱们取排名(按照相关度排名)最靠前的t(t通常取200左右)个网页做为初始集合,记为根集合\(R_\sigma\)。这个集合知足咱们上面提到的前两个条件,可是还远远不能知足第三个条件。

因而,咱们须要扩展\(R_\sigma\)。通常认为,一个与关键字相关的高质量的网页即便不在\(R_\sigma\)中,那也极可能在\(R_\sigma\)中有某些网页指向它。基于此,咱们扩展\(R_\sigma\)的过程以下(摘自Jon Kleinberg 的论文):

Subgraph(\(\sigma\), \(\psi\), t, d)
  \(\sigma\): a query string.
  \(\psi\): a text-based search engine.
  t, d: natural numbers.

  Let \(R_\sigma\) denote the top t results of \(\psi\) on \(\sigma\).
  Set \(S_\sigma\) := \(R_\sigma\)

  For each page p \(\in\) \(R_\sigma\)
    Let \(\Gamma^+(p)\) denote the set of all pages p points to.
    Let \(\Gamma^-(p)\) denote the set of all pages pointing to p.
    Add all pages in \(\Gamma^+(p)\) to \(S_\sigma\).
    If \(|\Gamma^-(p)| \leq d\), then
      Add all pages in \(\Gamma^-(p)\) to \(S_\sigma\).
    Else
      Add an arbitrary set of d pages from \(\Gamma^-(p)\) to \(S_\sigma\).
  End
  Return \(S_\sigma\)

一开始咱们令\(S_\sigma\) = \(R_\sigma\)。而后经过上面的方法,咱们将全部被\(R_\sigma\)中网页所指向的网页加入到\(S_\sigma\)中,再把必定数量的指向\(R_\sigma\)集合中网页的那些网页(每一个\(R_\sigma\)中网页最多能添加d个指向它的网页)加入到\(S_\sigma\)中。为了保证\(S_\sigma\)集合的合适的大小,d不能太大,通常设置为50左右。一般状况下,扩展以后集合的大小为1000~5000个网页,知足上面的三个条件。

在计算hub值和authority值以前,咱们还须要对\(S_\sigma\)进行一下处理。咱们把同一个“域名”(域名指一个网站)下的网页之间的连接所有删除,由于一般这些连接只是为了让人能在这个网站下的不一样网页之间进行切换,例如网站内的导航连接。在HITS算法中,这些连接与不一样网站之间的连接相比,确定是后者更能体现hub值和authority值的传递关系。因此咱们在\(S_\sigma\)集合中删除这些连接,造成新集合\(G_\sigma\)

如今,就能够开始计算hub值和authority值了[^ref_4]。咱们用\(h(p)\)表示页面\(p\)的hub值,\(a(p)\)表示页面\(p\)的authority值。首先令每一个页面的初始hub值\(h(p)\)为1,初始authority值\(a(p)\)也为1。而后就开始迭代计算的过程(n为\(G_\sigma\)中总的网页数):

\[ \forall p, a(p) = \sum_{i = 1}^n h(i), \\ \forall p, h(p) = \sum_{i = 1}^n a(i) \]

每一轮迭代结束,都须要进行标准化,使\(\sum_{i = 1}^n h(i)^2 = \sum_{i = 1}^n a(i)^2 = 1\)。标准化的必要性将在算法证实部分解释。

何时迭代结束呢?咱们能够设置一个迭代次数上限k来控制,或者设定一个阈值,当变化小于阈值的时候迭代结束。而后只要返回给用户authority值靠前的十几个网页就好了。

好了,HITS算法的原理其实就这么点,十分通俗易懂。


3. 算法证实

上面说到如何控制迭代的终止,而这又有个前提条件,那就是通过不断的迭代,每一个网页的hub值和authority值最终会收敛。下面咱们就来证实HITS算法的收敛性。

为了证实的方便,咱们用矩阵的方式来表示HITS算法。

对于初始集合\(G_\sigma\),用一个矩阵\(M\)表示\(G_\sigma\)中网页之间的关系:\(m_{ij} = 1\)表示网页\(i\)指向网页\(j\),不然为0。用向量\(H\)表示全部页面的hub值,其中第i个份量表示网页i的hub值;用向量\(A\)表示全部页面的authority值,其中第i个份量表示网页i的authority值。全部页面的hub值和authority值初始都为1。例如上面算法原理的例子中的图就能够表示为:

\[ M = \left( \begin{array}{ccc} 0 & 0 & 1 \\ 0 & 0 & 1 \\ 0 & 0 & 0 \\ \end{array} \right) \]

而后能够计算:

\[ A = M^T H = \left( \begin{array}{ccc} 0 & 0 & 0 \\ 0 & 0 & 0 \\ 1 & 1 & 0 \\ \end{array} \right) \left( \begin{array}{ccc} 1 \\ 1 \\ 1 \\ \end{array} \right) = \left( \begin{array}{ccc} 0 \\ 0 \\ 2 \\ \end{array} \right), \\ H = M A = \left( \begin{array}{ccc} 0 & 0 & 1 \\ 0 & 0 & 1 \\ 0 & 0 & 0 \\ \end{array} \right) \left( \begin{array}{ccc} 0 \\ 0 \\ 2 \\ \end{array} \right) = \left( \begin{array}{ccc} 2 \\ 2 \\ 0 \\ \end{array} \right) \]

通常的,咱们有:

\[ A_k = M^T H_{k - 1}, \\ H_k = M A_k \\ (每一轮先计算A_k,而后根据A_k计算H_k) \]

更进一步,有:

\[ A_k = (M^T M)^{k - 1} M^T Z, \\ H_k = (M M^T)^k Z \\ (其中Z的全部份量都为1) \]

在这里咱们引用一些线性代数的知识:

定理1
: 一个矩阵与该矩阵的转置的乘积是对称矩阵。

定理2
: 实对称矩阵的特征值都是实数,且若矩阵大小为n * n,则其必有n个实特征值(包含重根)。

定理3
: 含有n个特征值的n阶矩阵,其主对角线元素之和等于其特征值之和。

定义1
: 对于实数矩阵,绝对值最大的特征值称为主特征值,对应的特征向量称为主特征向量。

定理4
: 若是一个实对称矩阵是非负矩阵,则其主特征向量也是非负的,而且是非0向量。

定理5
: **令\(W\)为一个n*n实对称矩阵,\(v\)是一个n维向量且与\(W\)的主特征向量\(\omega_w\)非正交,则一个n维单位向量将沿着\(W^k v\)的方向收敛至\(\omega_w\)。**

由定理1可知上面的\(M^T M\)\(M M^T\)都是对称矩阵,且由定理2可知都有n个实特征值。

\(H_k = (M M^T)^k Z\)中,\(Z\)\(M^T M\)的主特征向量非正交,因此\(H\)向量最终将收敛至\(M^T M\)的主特征向量。定理5中的单位向量指的就是\(H\)向量,为了保证其每轮迭代时都是一个单位向量,咱们在每次迭代以后都对其进行标准化

\(A_k = (M^T M)^{k - 1} M^T Z\)中,\(M^T Z\)\(M^T M\)的主特征向量非正交。证实以下(这部分证实在 Jon Kleinberg 的原论文中省略了,自行证实,仅供参考):

\[ 假设 M^T Z与M^T M的主特征向量\omega_w正交,则有: \\ (M^T Z)^T \omega_w = 0 \\ \Rightarrow (Z^T M) \omega_w = 0 \\ \Rightarrow Z^T (M \omega_w) = 0 \\ \because M是非负矩阵,\omega_w也是非负矩阵 \\ \therefore M \omega_w是非负矩阵 \]

咱们只要再证\(M \omega_w\)不是\(0\)矩阵,就能够推翻咱们的假设了。由于\(Z^T\)的全部份量都为1,因此\(Z^T\)与非负且非0矩阵的内积必定不为0。在进一步证实以前,咱们先证\(M^T M 的主特征值 \lambda_w \neq 0\)

\[ 令m_{ij}为矩阵M第i行第j列的元素 \\ 即M = \left( \begin{array}{ccc} m_{11} & \dots & m_{1n} \\ \vdots & m_{ij} & \vdots \\ m_{n1} & \dots & m_{nn} \\ \end{array} \right), M^T = \left( \begin{array}{ccc} m_{11} & \dots & m_{n1} \\ \vdots & m_{ji} & \vdots \\ m_{1n} & \dots & m_{nn} \\ \end{array} \right) \\ 则M^T M 的主对角线元素w_{ii} = \sum_{j = 1}^n m_{ji}^2 \\ \because M不是0矩阵 \\ \therefore w_{ii}不全为0 \\ \therefore M^T M 主对角线元素之和必不为0 \\ 又 \because 由定理2和定理3可知对称矩阵的主对角线元素之和为特征值之和 \\ \therefore M^T M 的特征值之和必不为0 \\ \therefore M^T M 的主特征值 \lambda_w \neq 0 \]

下面证实\(M \omega_w\)不是\(0\)矩阵:

\[ \because \omega_w是M^T M的主特征向量 \\ \therefore 有 M^T M \omega_w = \lambda_w \omega_w,其中 \lambda_w 为主特征值 \\ 假设M \omega_w 是0矩阵,则: \\ M^T M \omega_w = 0 \Rightarrow \lambda_w \omega_w = 0 \\ 但这显然不成立 \\ \because \lambda_w \neq 0 且 \omega_w是非负矩阵 \\ \therefore \lambda_w \omega_w \neq 0 \\ \therefore 假设不成立 \\ \therefore M \omega_w 不是0矩阵 \]

再结合上面的结论:\(M \omega_w是非负矩阵\),便可得出:\(M \omega_w\)是非负矩阵且不是\(0\)矩阵。

因此上面的假设:\(假设 M^T Z与M^T M的主特征向量\omega_w 正交\) 不成立。因此\(M^T Z\)\(M^T M\)的主特征向量非正交。也即\(A\)向量最终将收敛至\(M^T M\)的主特征向量。一样的,为了保证其每轮迭代时都是一个单位向量,咱们在每次迭代以后都对其进行标准化

至此,咱们便证实了HITS算法的收敛性。


4. 算法实现

下面的代码原理与个人另外一篇博客PageRank类似。

4.1 基于迭代法的简单实现

用python实现,须要先安装python-graph-core。

class HITSIterator:
    __doc__ = '''计算一张图中的hub,authority值'''

    def __init__(self, dg):
        self.max_iterations = 100  # 最大迭代次数
        self.min_delta = 0.0001  # 肯定迭代是否结束的参数
        self.graph = dg

        self.hub = {}
        self.authority = {}
        for node in self.graph.nodes():
            self.hub[node] = 1
            self.authority[node] = 1

    def hits(self):
        """
        计算每一个页面的hub,authority值
        :return:
        """
        if not self.graph:
            return

        flag = False
        for i in range(self.max_iterations):
            change = 0.0  # 记录每轮的变化值
            norm = 0  # 标准化系数
            tmp = {}
            # 计算每一个页面的authority值
            tmp = self.authority.copy()
            for node in self.graph.nodes():
                self.authority[node] = 0
                for incident_page in self.graph.incidents(node):  # 遍历全部“入射”的页面
                    self.authority[node] += self.hub[incident_page]
                norm += pow(self.authority[node], 2)
            # 标准化
            norm = sqrt(norm)
            for node in self.graph.nodes():
                self.authority[node] /= norm
                change += abs(tmp[node] - self.authority[node])

            # 计算每一个页面的hub值
            norm = 0
            tmp = self.hub.copy()
            for node in self.graph.nodes():
                self.hub[node] = 0
                for neighbor_page in self.graph.neighbors(node):  # 遍历全部“出射”的页面
                    self.hub[node] += self.authority[neighbor_page]
                norm += pow(self.hub[node], 2)
            # 标准化
            norm = sqrt(norm)
            for node in self.graph.nodes():
                self.hub[node] /= norm
                change += abs(tmp[node] - self.hub[node])

            print("This is NO.%s iteration" % (i + 1))
            print("authority", self.authority)
            print("hub", self.hub)

            if change < self.min_delta:
                flag = True
                break
        if flag:
            print("finished in %s iterations!" % (i + 1))
        else:
            print("finished out of 100 iterations!")

        print("The best authority page: ", max(self.authority.items(), key=lambda x: x[1]))
        print("The best hub page: ", max(self.hub.items(), key=lambda x: x[1]))


if __name__ == '__main__':
    dg = digraph()

    dg.add_nodes(["A", "B", "C", "D", "E"])

    dg.add_edge(("A", "C"))
    dg.add_edge(("A", "D"))
    dg.add_edge(("B", "D"))
    dg.add_edge(("C", "E"))
    dg.add_edge(("D", "E"))
    dg.add_edge(("B", "E"))
    dg.add_edge(("E", "A"))

    hits = HITSIterator(dg)
    hits.hits()

程序中给出的网页之间的关系以下:

hits2

运行结果以下:

This is NO.9 iteration
authority {'E': 0.7886751345855355, 'C': 0.2113248654398108, 'B': 0.0, 'A': 7.119870133749228e-06, 'D': 0.5773502691457247}
hub {'E': 3.6855159786102477e-06, 'C': 0.40824829046663563, 'B': 0.7071067811721405, 'A': 0.40824829046663563, 'D': 0.40824829046663563}
finished in 9 iterations!
The best authority page:  ('E', 0.7886751345855355)
The best hub page:  ('B', 0.7071067811721405)

4.2 MapReduce实现

MapReduce是一个高效的分布式计算框架,在这里就很少作介绍了(若还不怎么了解MapReduce能够参考我另外一篇博客PageRank,里面有简单的原理介绍和代码展现)。

下面是实现HITS算法的类,其中注释较为详细,就很少作解释了:

class HITSMapReduce:
    __doc__ = '''计算一张图中的hub,authority值'''

    def __init__(self, dg):
        self.max_iterations = 100  # 最大迭代次数
        self.min_delta = 0.0001  # 肯定迭代是否结束的参数

        # graph表示整个网络图。是字典类型。
        # graph[i][authority][0] 存放第i网页的authority值
        # graph[i][authority][1] 存放第i网页的入链网页,是一个列表
        # graph[i][hub][0] 存放第i网页的hub值
        # graph[i][hub][1] 存放第i网页的出链网页,是一个列表
        self.graph = {}
        for node in dg.nodes():
            self.graph[node] = {"authority": [1, dg.incidents(node)], "hub": [1, dg.neighbors(node)]}

    @staticmethod
    def normalize(ah_list):
        """
        标准化
        :param ah_list: 一个列表,其元素为(网页名,数值)
        :return: 返回一个标准化的列表,其元素为(网页名,标准化的数值)
        """
        norm = 0
        for ah in ah_list:
            norm += pow(ah[1], 2)

        norm = sqrt(norm)
        return [(ah[0], ah[1] / norm) for ah in ah_list]

    def hits_authority_mapper(self, input_key, input_value):
        """
        用于计算每一个页面能得到的hub值,这个hub值将传递给页面的authority值
        :param input_key: 网页名,如 A
        :param input_value: self.graph[input_key],即这个网页的相关信息,包含两个字典,{a...}和{h...}
        :return: [(网页名, 0.0), (出链网页1, A的hub值), (出链网页2, A的hub值)...]
        """
        return [(input_key, 0.0)] + \
               [(out_link, input_value["hub"][0]) for out_link in input_value["hub"][1]]

    def hits_hub_mapper(self, input_key, input_value):
        """
        用于计算每一个页面能得到的authority值,这个authority值将传递给页面的hub值
        :param input_key: 网页名,如 A
        :param input_value: self.graph[input_key],即这个网页的相关信息,包含两个字典,{a...}和{h...}
        :return: [(网页名, 0.0), (入链网页1, A的authority值), (入链网页2, A的authority值)...]
        """
        return [(input_key, 0.0)] + \
               [(in_link, input_value["authority"][0]) for in_link in input_value["authority"][1]]

    def hits_reducer(self, intermediate_key, intermediate_value_list):
        """
        统计每一个网页得到的authority或hub值
        :param intermediate_key: 网页名,如 A
        :param intermediate_value_list: A全部得到的authority值或hub值的列表:[0.0,得到的值,得到的值...]
        :return: (网页名,计算所得的authority值或hub值)
        """
        return intermediate_key, sum(intermediate_value_list)

    def hits(self):
        """
        计算authority值与hub值,各须要调用一次mapreduce模块
        :return: self.graph,其中的 authority值与hub值 已经计算好
        """
        iteration = 1  # 迭代次数
        change = 1  # 记录每轮迭代后的PR值变化状况,初始值为1保证至少有一次迭代
        while change > self.min_delta:
            print("Iteration: " + str(iteration))

            # 计算每一个页面的authority值并标准化
            # new_authority为一个列表,元素为:(网页名,此轮迭代所得的authority值)
            new_authority = HITSMapReduce.normalize(
                MapReduce.map_reduce(self.graph, self.hits_authority_mapper, self.hits_reducer))

            # 计算每一个页面的hub值并标准化
            # new_hub为一个列表,元素为:(网页名,此轮迭代所得的hub值)
            new_hub = HITSMapReduce.normalize(
                MapReduce.map_reduce(self.graph, self.hits_hub_mapper, self.hits_reducer))

            # 计算此轮 authority值+hub值 的变化状况
            change = sum(
                [abs(new_authority[i][1] - self.graph[new_authority[i][0]]["authority"][0]) for i in range(len(self.graph))])
            change += sum(
                [abs(new_hub[i][1] - self.graph[new_hub[i][0]]["hub"][0]) for i in range(len(self.graph))])
            print("Change: " + str(change))

            # 更新authority值与hub值
            for i in range(len(self.graph)):
                self.graph[new_authority[i][0]]["authority"][0] = new_authority[i][1]
                self.graph[new_hub[i][0]]["hub"][0] = new_hub[i][1]

            iteration += 1
        return self.graph

下面是一个测试用例:

if __name__ == '__main__':
    dg = digraph()

    dg.add_nodes(["A", "B", "C", "D", "E"])

    dg.add_edge(("A", "C"))
    dg.add_edge(("A", "D"))
    dg.add_edge(("B", "D"))
    dg.add_edge(("C", "E"))
    dg.add_edge(("D", "E"))
    dg.add_edge(("B", "E"))
    dg.add_edge(("E", "A"))

    h = HITSMapReduce(dg)
    hits_result = h.hits()

    print("The final iteration result is")
    for key, value in hits_result.items():
        print(key + " authority: ", value["authority"][0], " hub: ", value["hub"][0])

    max_authority_page = max(hits_result.items(), key=lambda x: x[1]["authority"][0])
    max_hub_page = max(hits_result.items(), key=lambda x: x[1]["hub"][0])
    print("The best authority page: ", (max_authority_page[0], max_authority_page[1]["authority"][0]))
    print("The best hub page: ", (max_hub_page[0], max_hub_page[1]["hub"][0]))

运行结果为:

The final iteration result is
E authority:  0.7886751345948128  hub:  8.64738646858812e-10
A authority:  7.060561487452559e-10  hub:  0.408267180858587
C authority:  0.2113248654051872  hub:  0.40823884510260666
B authority:  0.0  hub:  0.7071067809972986
D authority:  0.5773502691896258  hub:  0.40823884510260666
The best authority page:  ('E', 0.7886751345948128)
The best hub page:  ('B', 0.7071067809972986)

以上即是HITS算法的MapReduce实现。


5. HITS算法的缺点

  • 计算效率低

这里说的“效率低”是针对其实时计算的特色而提出的。HITS算法是在用户提出搜索请求以后才开始运行的,然而计算出结果又须要屡次迭代计算,因此就这点上来讲HITS算法效率仍然较低。

  • 主题漂移

在算法原理部分咱们介绍了HITS算法是如何生成初始集合\(G_\sigma\)。从根集合\(R_\sigma\)咱们经过连接添加网页的方法进行扩展,但这也极可能添加进与搜索主题无关的网页。如果这部分网页中又偏偏有着一些高质量的authority页面,则颇有可能返回给用户,下降用户的搜索体验。

  • 做弊网页

试想咱们弄一个页面指向不少高质量的authority页面,那么这个页面就成为了一个高质量的hub页面。而后再弄个连接指向本身的搓网页,按照HITS算法,将大大提高本身的搓网页的authority值。

  • 稳定性差

对于一个网页集合,如果删除其中的某条连接,就有可能形成一些网页的hub值和authority值发生巨大变化。


6. 写在最后

之后想到什么再写上来吧。


参考资料

1:《这就是搜索引擎:核心技术详解》,张俊林

2:The Mathematics of Web Search,这个网站上有HITS和PageRank的一些数学知识。

3:原论文:Authoritative Sources in a Hyperlinked Environment

4:“标准参考文献”:维基百科

相关文章
相关标签/搜索