FRAUDAR: Bounding Graph Fraud in the Face of Camouflage 论文理解及算法解析

来源:互联网 发布:js写计算器 编辑:程序博客网 时间:2024/06/08 08:20

FRAUDAR: Bounding Graph Fraud in the Face of Camouflage

1、一段话概括算法

FRAUDAR算法来源于2016年KDD会议,该论文获得了当年的最佳论文奖。该算法要解决的问题是找出站内最善于伪装的虚假账户簇。其原理是虚假账户会通过增加和正常用户的联系来进行伪装,而这些伪装(边)会形成一个很紧密的子网络,这样就可以通过定义一个全局的度量,再移除二部图结构中的边,使得剩余网络结构对应的度量的值最大,这样就找到了最紧密的子网络,而这个网络就是最可疑的。

2、论文主页

http://www.andrew.cmu.edu/user/bhooi/projects/fraudar/index.html

3、背景

在社交网络或者电商网站中,存在着用户关注其他用户或者用户浏览商品的二部图,其中的虚假用户会通过关注正常用户来伪装自己,而用户也会通过多浏览其它商品来伪装其真正要浏览的商品。更严重的情况是有一些正常的用户可能被盗,从而被利用来进行关注或者浏览等。在这样的情况下,虚假用户或者被盗用户与目标之间就会形成一个“dense”的子网络。算法的目的就是找到这样的子网络,从而完成虚假用户行为的识别。

4、计算过程

算法的核心计算过程可以简要描述如下,具体可以参考原论文中Algorithm1的伪代码:

  • a、建立优先树(一种用于快速移除图结构边的树结构);
  • b、对于二部图中的任意节点,贪心地移除优先级最高(由优先树得到)的节点,直至整个网络结构为空;
  • c、比较上述每一步得到的子网络结构对应的全局的度量,取该值最大的子网络结构,那么该子网络结构就是最紧密的子网络,也就是最可疑的团伙。

其中最关键的地方是定义了一个全局度量,该Metric的定义是(目标度量),可以理解成子网络结构中每个点的平均可疑程度

g(s)=f(s)|s|

其中,

f(s)=fv(s)+fe(s)

通过这样的定义,很容易可以得出4条性质:

  • 固定其他条件,包含更高可疑度节点的子网络比包含较低可疑度节点的子网络更可疑;
  • 固定其他条件,增加可疑的边使得子网络更可疑;
  • 如果节点和边的可疑程度固定,那么大的子网络比小的子网络更可疑;
  • 总的可疑程度相同,那么包含节点数少的子网络更可疑。

5、优先树

该算法的核心计算过程就是贪心地移除图中的点,使得每次变更f的变化最大,在移除一个节点的同时,只有与之相邻的节点会发生变化,那么这样最多产生O(|E|)次变更,如果找到合适的数据结构使得访问节点的时间复杂度为O(log|V|),那么总的时间复杂度就是O(NlogN)。

基于这样的考虑建立了优先树,这是一个二叉树,图中的每个节点都是树的一个叶子节点,其父节点为子节点中优先级较高的,树建完之后就可以按照O(log|V|)的速度获得优先级最高的节点。

6、通过Column-weighting来实现Camouflage Resistance

如何实现抵抗虚假行为呢?一个直观的方法是将图中每个边的嫌疑程度设为相等,但是为了实现抵抗虚假行为,我们不能这样做!如果节点i到一个度较大的节点j,那么将其边Cij的嫌疑降低,这是因为度较大的点本身可能就是很受欢迎的,例如大V和销售良好的商品。这使得我们不是仅仅关注度较大的节点,而是关注dense的子网络。

如果我们的邻接矩阵中,行代表用户,列代表目标。那么虚假用户向正常的目标增加边并不会使得子网络的嫌疑程度变低,因为虚假用户向正常的目标增加边只会使得正常目标对应的边的嫌疑程度下降,而嫌疑子网络内的权重却不会变化。

作者的实验指出Column-weighting函数选择类似于idf比较好,即

h(x)=1/log(x+c)

7、重要代码赏析

# 实际上这里只考虑了edge weight,没有考虑node weight,# 贪心算法只能保证是局部最优的,而不能保证是全局最优的def fastGreedyDecreasing(M, colWeights):    (m, n) = M.shape    Md = M.todok()    Ml = M.tolil()  # copy sparse matrix M    Mlt = M.transpose().tolil()    rowSet = set(range(0, m))    colSet = set(range(0, n))    curScore = c2Score(M, rowSet, colSet)    print "curScore", curScore    SSS = (len(rowSet) + len(colSet))    print "SSS", SSS    bestAveScore = curScore / SSS    print "bestAveScore", bestAveScore    bestSets = (rowSet, colSet)    print "finished setting up greedy"    # 横轴是1,纵轴是0    rowDeltas = np.squeeze(M.sum(axis=1).A)  # *decrease* in total weight when *removing* this row    print "rowDeltas", type(rowDeltas)    #print rowDeltas    colDeltas = np.squeeze(M.sum(axis=0).A)    print "colDeltas", type(colDeltas)    #print colDeltas    print "finished setting deltas"    rowTree = MinTree(rowDeltas)    colTree = MinTree(colDeltas)    print "finished building min trees"    numDeleted = 0    deleted = []    bestNumDeleted = 0    initLength = len(rowSet)    while rowSet and colSet:        if (len(colSet) + len(rowSet)) % 10000 == 0:            pass            # print "current set size = ", len(colSet) + len(rowSet)        (nextRow, rowDelt) = rowTree.getMin()        (nextCol, colDelt) = colTree.getMin()        if rowDelt <= colDelt:            # 如果行的权重变化小于列的,那么就减去行的,这样保留下来的较大            curScore -= rowDelt            # 减去行之后,计算这行对所有列的影响,与该行有联系的列            # 更新列树,列树的权重由每一列相加得到            # colWeights,代表列中object的权重            #print "Ml.rows[nextRow]"            #print Ml.rows[nextRow]            # 减去要去掉的行对所有列的影响            # ml            for j in Ml.rows[nextRow]:                delt = colWeights[j]                # 最核心的一句                colTree.changeVal(j, -colWeights[j])            rowSet -= {nextRow}            rowTree.changeVal(nextRow, float('inf'))            deleted.append((0, nextRow))        else:            curScore -= colDelt            # 计算这一列去掉后对每一行的影响,而这一列的权重就是colWeights[nextCol]            # mlt,按照列查找与其相邻的所有行            for i in Mlt.rows[nextCol]:                delt = colWeights[nextCol]                # 最核心的一句                rowTree.changeVal(i, -colWeights[nextCol])            colSet -= {nextCol}            colTree.changeVal(nextCol, float('inf'))            deleted.append((1, nextCol))        numDeleted += 1        #print "numDeleted", numDeleted        curAveScore = curScore / (len(colSet) + len(rowSet))        if curAveScore > bestAveScore:            # print "curAveScore", curAveScore            # print "bestAveScore", bestAveScore            bestAveScore = curAveScore            bestNumDeleted = numDeleted    # reconstruct the best row and column sets    finalRowSet = set(range(m))    finalColSet = set(range(n))    for i in range(bestNumDeleted):        if deleted[i][0] == 0:             finalRowSet.remove(deleted[i][1])        else:            finalColSet.remove(deleted[i][1])    return ((finalRowSet, finalColSet), bestAveScore, bestNumDeleted)
# this is edge weight constructdef logWeightedAveDegree(M):    print "begin computing weight matrix"    print type(M)    # print M, m is users in row, n is objects in column    (m, n) = M.shape    print "m, n", m, n    colSums = M.sum(axis=0)    print "colSums.shape", colSums.shape    asum = colSums.A    print "asum", type(asum)    # print asum    tempsqueeze = np.squeeze(asum)    # squeeze, Remove single-dimensional entries from the shape of an array.    print "tempsqueeze", type(tempsqueeze)    #print tempsqueeze    colWeights = 1.0 / np.log(tempsqueeze + 5)    #print "colWeights", colWeights    # row based sparse matrix    colDiag = sparse.lil_matrix((n, n))    """Fills the diagonal elements {a_ii} with the values from the            given sequence.  If k != 0, fills the off-diagonal elements            {a_{i,i+k}} instead.            values may have any length.  If the diagonal is longer than values,            then the remaining diagonal entries will not be set.  If values if            longer than the diagonal, then the remaining values are ignored.            """    colDiag.setdiag(colWeights)    # print "colDiag", type(colDiag)    # print colDiag    #print "M", M    W = M * colDiag    # print "W", W    print "finished computing weight matrix"    return fastGreedyDecreasing(W, colWeights)

8、一些缺点及改进(个人观点)

  • 因为是贪心计算,所以不能保证全局最优,文章中作者给出了理论上的“界”。
  • 只能找出最dense的网络,通过循环地执行FRAUDAR算法可以得到多个子网络。
  • 只考虑了edge的权重,没有考虑node的权重。
0 0
原创粉丝点击