PageRank算法原理介绍
PageRank算法是google的网页排序算法,在《The Top Ten Algorithms in Data Mining》一书中第6章有介绍。大致原理是用户搜索出的多个网页需要按照一定的重要程度(即后面讲的权重)排序,每个网页的权重由所有链接到它的其他 网页的权重的加权和,加权系数为每个网页链出的网页数的倒数,也就是说每个网页的权重会平均分配到其链向的所有网页。
例如A链接到B和C,B链接到C,C链接到A,P(X)表示X的权重,如下图所示
则每个节点的权重关系为:
P(A) = P(C)
P(B) = P(A)/2
P(C) = P(A)/2 + P(B)
一般地,可以写成个线性方程组形式:
P = AP
如此通过迭代即可求出最终各个网页的权重值。
但是,当存在某些特殊情况时,如某个网页的链入或链出数为0,则迭代不会收敛。因此在上述算法中增加了个阻尼系数d,d表示用户会继续点击下一网页的概率,同时用户在1-d的概率下会随机访问到任意网页,那么上面的公式会修正为:
P = (1-d)/N * ones(N,1) + d*AP
其中N为所有网页数,ones(N,1)表示N行1列的全1矩阵。通过上述公式可以迭代计算出最终各网页权重。
详细介绍可以参考wiki百科。
算法实现
首先声明这个CPageRank类:
1 typedef unsigned char BYTE; 2 class CPageRank 3 { 4 public: 5 CPageRank(int nWebNum = 5, bool bLoadFromFile = false); 6 ~CPageRank; 7 8 int Process; 9 float *GetWeight; private: void InitGraph(bool bLoadFromFile = false); void GenerateP; BYTE *m_pu8Relation; //节点关系 i行j列表示j是否指向i float *m_pf32P; //转移矩阵 //缓存 float *m_pf32Weight0; float *m_pf32Weight1; int *m_pl32Out; int m_nNum; float m_f32DampFactor; //阻尼系数 int m_nMaxIterTime; float m_f32IterErr; float *m_pf32OutWeight;//输出的最终权重 };
下面就是具体各个函数的实现了。
首先构造函数主要是初始化一些迭代相关变量,分配空间等,这里把生成节点指向图的工作也放在这里,支持直接随机生成和读取二进制文件两种方式。
1 CPageRank::CPageRank(int nWebNum, bool bLoadFromFile) 2 { 3 m_f32DampFactor = ; 4 m_nMaxIterTime = ; 5 m_f32IterErr = 1e-6; 6 7 m_nNum = nWebNum; 8 m_pu8Relation = new BYTE[m_nNum * m_nNum]; 9 m_pf32P = new float[m_nNum * m_nNum]; m_pf32Weight0 = new float[m_nNum]; m_pf32Weight1 = new float[m_nNum]; m_pl32Out = new int[m_nNum];//每个节点指向的节点数 InitGraph(bLoadFromFile); }
析构函数自然就是释放内存了:
1 CPageRank::~CPageRank 2 { 3 delete m_pl32Out; 4 delete m_pf32P; 5 delete m_pf32Weight0; 6 delete m_pf32Weight1; 7 delete m_pu8Relation; 8 }
下面就是随机生成或读取文件产生节点指向关系,如果随机生成的话,会自动保存当前生成的图,便于遇到问题时可复现调试:
1 void CPageRank::InitGraph(bool bLoadFromFile) 2 { 3 FILE *pf = NULL; 4 if(bLoadFromFile) 5 { 6 pf = fopen("map.dat", "rb"); 7 if(pf) 8 { 9 fread(m_pu8Relation, sizeof(BYTE), m_nNum * m_nNum, pf); fclose(pf); return; } } //建立随机的节点指向图 int i, j; srand((unsigned)time(NULL)); for(i = 0; i < m_nNum; i++) { //指向第i个的节点 for(j = 0; j < m_nNum; j++) { m_pu8Relation[i * m_nNum + j] = rand & 1; } //自己不指向自己 m_pu8Relation[i * m_nNum + i] = 0; } pf = fopen("map.dat", "wb"); if(pf) { fwrite(m_pu8Relation, sizeof(BYTE), m_nNum * m_nNum, pf); fclose(pf); } }
既然已经产生了各个节点的关系了,那PageRank的核心思想就是根据关系,生成出上面的转移矩阵P:
1 void CPageRank::GenerateP 2 { 3 int i,j; 4 float *pf32P = NULL; 5 BYTE *pu8Relation = NULL; 6 7 //统计流入流出每个节点数 8 memset(m_pl32Out, 0, m_nNum * sizeof(int)); 9 pu8Relation = m_pu8Relation; for(i = 0; i < m_nNum; i++) { for(j = 0; j < m_nNum; j++) { m_pl32Out[j] += *pu8Relation; pu8Relation++; } } //生成转移矩阵,每个节点的权重平均流出 pu8Relation = m_pu8Relation; pf32P = m_pf32P; for(i = 0; i < m_nNum; i++) { for(j = 0; j < m_nNum; j++) { if(m_pl32Out[j] > { *pf32P = *pu8Relation * 1.0f / m_pl32Out[j]; } else { *pf32P = 0.0f; } pu8Relation++; pf32P++; } } //考虑阻尼系数,修正转移矩阵 pf32P = m_pf32P; for(i = 0; i < m_nNum; i++) { for(j = 0; j < m_nNum; j++) { *pf32P = *pf32P * m_f32DampFactor; pf32P++; } } }
接下来就需要求解出各个节点的权重,process函数里先调用GenerateP生成出P矩阵,然后采用迭代法求解,当时为了测试收敛速度,直接返回了迭代次数:
1 int CPageRank::Process 2 { 3 int i,j,k,t; 4 float f32MaxErr = 0.0f; 5 float *pf32Org = m_pf32Weight0; 6 float *pf32New = m_pf32Weight1; 7 float f32MinWeight = (1 - m_f32DampFactor) / m_nNum; 8 9 //设置初始值,全 for(i = 0; i < m_nNum; i++) { pf32Org[i] = 1.0f / m_nNum;//rand * 2.0f / RAND_MAX; } //生成P矩阵 GenerateP; //迭代 for(t = 0; t < m_nMaxIterTime; t++) { //开始迭代 f32MaxErr = 0.0f; for(i = 0; i < m_nNum; i++) { pf32New[i] = f32MinWeight; int l32Off = i * m_nNum; for(j = 0; j < m_nNum; j++) { pf32New[i] += m_pf32P[l32Off + j] * pf32Org[j]; } float f32Err = fabs(pf32New[i] - pf32Org[i]); if(f32Err > f32MaxErr) { f32MaxErr = f32Err; } } //迭代误差足够小,停止 if(f32MaxErr < m_f32IterErr) { break; } //交换2次迭代结果 float *pf32Temp = pf32Org; pf32Org = pf32New; pf32New = pf32Temp; } //迭代结果存在pf32New中 m_pf32OutWeight = pf32New; return t; }
最后的结果已经存在了m_pf32OutWeight中了,下面函数直接传出结果:
1 float * CPageRank::GetWeight 2 { 3 return m_pf32OutWeight; 4 }
这样,整个算法就算完成了,考虑到篇幅,贴上来的代码把opencv显示相关的代码去掉了,完整代码见
https://bitbucket.org/jcchen1987/mltest。
下面是结果图,即便节点数较多时,算法收敛也比较快。
分析总结
对于上面这个公式,看到网上有人假定P的总能量是1,则可以改写为P=BP 的形式来进行迭代,这种方法也实现了一下,问题仍然是当存在网页链入或者链出数为0时,每次迭代后不能保证能量守恒,那么下一次就会导致P=BP这个公式 不成立,从而出现迭代不收敛;一种有效的做法是每次迭代后就将P进行能量规一化,这样是可以保证结果的收敛性的。但是这种做法与原始算法的结果会有一点细 微的出入。因此建议按照原始的公式进行迭代求解。