基于Winnowing的代码相似度算法

前言

本文的设计思想以及算法的使用都是基于以下两篇文章。

《Winnowing: Local Algorithms for Document Fingerprinting》

《程序代码相似度度量算法研究_邓爱萍》

相关实现过程参考https://blog.csdn.net/chichoxian/article/details/53128303,文章幽默诙谐,深入浅出,建议大家去看一下。

但是这里提一下,他的文章没有实现相似度的计算问题,我在邓爱萍的文章挑选了一种算法,最后实验了相似度的计算问题。

算法的思路

《Winnowing: Local Algorithms for Document Fingerprinting》在这篇文章中介绍了一种算法,在下面简单的介绍一下。

首先是关键字的提取

无论是代码相似度的比较还是文本相似度的比较(我这里是代码相似度),我们都需要找到文章中的关键字。所以第一步是关键字的提取。

PS:这里只讲实现过程,关于论证这样做的准确性以及正确性的论证可以参见原文。

1、首先看这样一个字符串“yabbadabbadoo”,确定gram的大小,这里用K来表示,K,gram的具体含义参见下图(文章我都白嫖了,盗个图不过分吧/狗头保命)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WbTDpm4H-1618112577638)(C:\Users\60917\AppData\Roaming\Typora\typora-user-images\image-20210411103813025.png)]

这里的红色蓝色绿色的,就是gram,在这里的K值就是3,可以记作3-gram

2、于是乎,我们获得了以下的子串

“yab”,“abb”,“bba”,“bad”,“ada”,”dab“,”abb“,”bba“,”bad“,”ado“,”doo“

可以计算我们获得的 子串个数 = 原字符串长度 - K + 1

所以这里可以看出,这种选择方案其实对文档的压缩并不明显,甚至效果很差,所以我们决定进一步压缩。

PS:这里的压缩值得是关键字的提取。

3、所以在以上子串中我们决定选择一种比较常见的方法进行筛选,然后获得其中代表作为文章的关键字

(1)首先对每一个子串hash我们可以得到一列数.所以有必要说一下我们所采用的hash函数。下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dL8Am1V1-1618112577643)(C:\Users\60917\AppData\Roaming\Typora\typora-user-images\image-20210411105950384.png)]

看不懂?没关系,下面有解释

第一个红框代表我们所使用的hash函数,第二个红框就是优化。通过H(K)来求H(K+1),不用重复计算了。这里的ci表示字符串中的第i个字符的ASCII值,b表示一个基数(BASE)这里我们可以取其为2(不确定是不是可以随便取),然后就可以计算出数了。

对应后面的函数def generateHash(Base,kgram,K)【未优化】 和def generateHash1(Base,kgram,K)【优化】

这里贴以下计算结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MRYrT7is-1618112577646)(C:\Users\60917\AppData\Roaming\Typora\typora-user-images\image-20210411110546611.png)]

可以看出相同的值,hash也是相同的(第四个与倒数第三个),通过对比hash值我们来确定有没有相似。但是也可以看到第三个和第5个虽然字符串不一样,但是hash值却相同,主要原因是发生了hash碰撞,具体可以百度一下这个关键字,去理解一下hash碰撞。

(2)获得hash值之后,一种算法是通过0modp来进行选择如下:

”5“ ”10“ ”21“ ”41“ ”62“ ”26“ ”74“ ”21“

我们的p取5,所以这里选出的值就是”5“,”10“来代表这篇文章,这里我们可以容易的看出,这种选择方法有一定的缺陷,就是没办法考虑的全篇,一般文章的辨识度都在文中,只看开头一小部分是不行的(虽然上面的数据是我故意设计的)

另一种就是论文中提到的方法,通过一个滑动的窗口获得特征值我们,我们这是窗口的大小为4然后可以得到

原hashlist长度-winSize+1个小的窗口。(就是一个4个大小的窗口向右侧滑动,每次滑动一位,每滑动一次都要记录下来)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YnwDJgmS-1618112577649)(C:\Users\60917\AppData\Roaming\Typora\typora-user-images\image-20210411111852650.png)]

得到这些小的窗口之后,我们根据以下规则进行选择。(选择最小的值,如果有两个那么就选择右侧的)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EJiAtUOJ-1618112577653)(C:\Users\60917\AppData\Roaming\Typora\typora-user-images\image-20210411112136505.png)]

获得结果:

[776, 682, 685, 686]
[682, 685, 686, 685]
[685, 686, 685, 692]
[686, 685, 692, 682]
[685, 692, 682, 685]
[692, 682, 685, 686]
[682, 685, 686, 699]
[685, 686, 699, 733]

不难看出4-7行的的682其实是一个,不过他占了很多窗口的最小值。

于是我们选择除了这篇文章的特征值(计算机喜欢从0开始)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UcqmukYM-1618112577656)(C:\Users\60917\AppData\Roaming\Typora\typora-user-images\image-20210411113317782.png)]

至此我们文章的关键字的提取告一段落。

可能有人会问关于K以及winSize的值的选取,这两个值的选取,要根据实际情况来进行选择,K值的选取将小于K长度的值筛选掉了,如果在英文中比如K=4,”the“就不会在统计范围内,要想做的比别人的好,这种阈值的设置就要符合自己的实际情况,可以通过做一个图表来分析阈值的取值在什么情况下比较合适。

(PS:这样做法的正确性参见上述论文)

然后就是通过关键字进行相似度的分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UvOneCLk-1618112577659)(C:\Users\60917\AppData\Roaming\Typora\typora-user-images\image-20210411114119934.png)]

这个是基于邓爱萍的文章中提出的一个计算方法。比较简单。

代码部分

#先使用文本进行测试
#标点符合去掉
excludes = {',','。','/','《','》','?',';','‘',':','“','【','】','{','}',
            '、','|','!','@','#','¥','%','……','&','*','(',')','-','=',
            '——','+','·','~','”',
            ',','.','/','<','>','?',';','\'',':','"','[',']','{','}','\\','|',
            '~','`','!','@','#','$','%','^','&','*','(',')','_','-','+','=',
            ' ','\n'}

def getThetxt(str):
    d1 = open(str,encoding='utf-8').read()

    for word in excludes:
        d1 = d1.replace(word,'')
    # print(d1)
    return d1


#获得切片
#K值切片的大小
def generateKgram(fileLine,K):

    kgram = []
    for i in range(len(fileLine)):
        if(i+K > len(fileLine)):
            break
        shingle = fileLine[i:i+K]
        kgram.append(shingle)

    return kgram

#获得哈希值
# 通过公式cj*(b**(k-j))
def generateHash(Base,kgram,K):
    HashList = []
    for i in range(len(kgram)):
        hash = 0
        shingle = kgram[i]

        for j in range(K):
            hash += ord(shingle[j])*(Base**(K-1-j))

        HashList.append(hash)

    return HashList

'''    
    输入:kgram-->窗口中的内容 
    输出:哈希值
    改进算法 通过公式H(c2,c3,...,ck+1) = (H(c1...ck)-c1*(b**(k-1)))*b+ck+1
'''
def generateHash1(Base,kgram,K):
    HashList = []
    hash = 0

    firstShingle = kgram[0]

    for j in range(K):
        hash += ord(firstShingle[j]) * (Base ** (K - 1 - j))

    HashList.append(hash)

    for i in range(1,len(kgram)):
        preshingle = kgram[i-1]
        shingle = kgram[i]
        hash = hash*Base - ord(preshingle[0])*(Base**K)+ord(shingle[K-1])
        HashList.append(hash)

    return HashList

#获得最小值
def getmin(list):

    mindict={}
    min = list[0]
    mindict[0] = 0
    mindict[1] = min
    for i in range(len(list)):

        if(list[i] <= min):
            min = list[i]
            mindict[0] = i
            mindict[1] = min

    return mindict

# 获得特征值
def getCvalue(WinSize,hashValues):

    minHash = 0
    minpos = 0
    fingerPrint = {}
    for i in range(len(hashValues)):
        if(i+WinSize > len(hashValues)):
            break

        tmplist = hashValues[i:i+WinSize]
        print(tmplist)
        # minHash = tmplist[WinSize-1]
        # minpos = WinSize+i-1

        dict = getmin(tmplist)

        minpos = dict.get(0)+i
        minHash = dict.get(1)

        if minpos not in fingerPrint:
            fingerPrint[minpos] = minHash

    return fingerPrint

#相似度计算公式
# 严格模式
def sim(A,B):
    sum = len(A)+len(B)
    similar = round(2.0*getSimnumber(A,B,1)/sum,2)

    return similar

# 两个里面有重复的情况怎么办?
# mode = 1 严格 mode = 0 宽松
def getSimnumber(A,B,mode):
    list1 = A.values()
    list2 = B.values()
    number = 0
    for i in list1:
        if i in list2:
            number = number + 1

    number2 = 0
    for i in list2:
        if i in list1:
            number2 = number2 + 1

    if mode == 1:
        return max(number,number2)
    elif mode == 0:
        return min(number,number2)
    else:
        print("error")

if __name__ == '__main__':

    # hashValues = [77, 74, 42, 17, 98, 50, 17, 98, 8, 88, 67, 39, 77, 74, 42, 17, 98]
    #
    # print(getCvalue(4,hashValues))


    # txt1 = getThetxt("C:/Users/60917/Desktop/第一章.txt")
    # txt2 = getThetxt("C:/Users/60917/Desktop/第二章.txt")
    K = 3
    WindowsSize = 4
    # list1 = generateKgram(txt1, K)
    # list2 = generateKgram(txt2, K)
    # list_1 = generateHash1(2,list1,K)
    # list_2 = generateHash1(2,list2,K)
    # dict1 =  getCvalue(WindowsSize, list_1)
    # dict2 = getCvalue(WindowsSize, list_2)
    #
    # print(sim(dict1,dict2))
    list2 = generateKgram("yabbadabbadoo",K)
    print(list2)
    list3 = generateHash1(2,list2,K)
    print(list3)
    list4 = getCvalue(WindowsSize, list3)
    print(list4)



  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值