相似性度量(距离度量)方法(二):字符串(文本)相似性度量

在本篇文章中,兔兔讲述字符串的相似性(距离)度量方法与算法实现。

(1)海明距离。

根据(一)篇当中的内容,我们已经知道了海明距离的度量方法。即判断字符串对应位置是否相等。

def Hamming_distance(list1,list2):
    n=len(list1) #n为列表长度,且list1与list2长度相等
    s=0
    for i in range(n):
        if list1[i]==list2[i]:
            s+=1
        else:
            continue
    return s
a=[1,2,3,4]
b=[1,3,2,4]
print(Hamming_distance(a,b))

如果距离越大,说明相似度越小。并且海明距离只适用于两个字符串长度相同的情况

(2)余弦相似性。

余弦相似性通常用于两组数据之间的距离或相似性。用它来计算字符串之间的相似性,通常可以用词袋模型(BOW)来向量化。例如,字符串string1='rabbit',string2=' lovely'。这两个字符串一共有的字母集合为{a,b,e,i,l,o,r,t,v,y},集合有10个元素,就可以把这两个字符串用长度为12的列表表示。如果字符串有字母集合中的元素,对应元素位置就是1,否则是0。

abeilortvy

string1

rabbit

1001001100

string2

lovely

0010110011

这样,根据余弦相似性的定义式,把两个向量化后的数组代入公式进行计算。例如[1,0]和[1,1]的相似性就是(1x1+0x1)/(sqrt(1^2+0^2)sqrt(1^2+1^2))。

import numpy as np
def cosine_similarity(string1,string2):
    s1=list(string1)
    s2=list(string2)
    set_s1s2=list(set(s1)|set(s2)) #两个字符串元素并集
    list1=[];list2=[]
    n=len(set_s1s2)
    for i in range(n):
        if set_s1s2[i] in s1:
            list1.append(1)
        else:
            list1.append(0)
        if set_s1s2[i] in s2:
            list2.append(1)
        else:
            list2.append(0)
    list1=np.array(list1) #转换成numpy数组形式便于向量乘法、模长运算
    list2=np.array(list2)
    similarity=np.dot(list1,list2)/(np.linalg.norm(list1)*np.linalg.norm(list2))
    return similarity
a='rabbit'
b='lovely'
print(cosine_similarity(a,b))

我们发现,两个字符串相似性为0,事实上,根据上面表格我们也可以发现,两个字符串对应元素始终是一个是1,另一个就是0,说明没有相同的元素,计算过程分子一项始终是0x1然后相加,所以分子那一项就是0,所以相似性自然就是0了。

(3)Dice距离。

事实上,在(一)篇中,凡是可以度量两个集合之间距离的,理论上都是可以用于计算两个字符串之间的距离,如杰卡德系数等。兔兔在这里就以Dice距离为例。

Dice距离实际上就是度量两个集合距离的一种方法。Dice系数如下:

QS=\frac{2|X\cap Y|}{|X|+|Y|}

我们还是以string1='rabbit'和string2=’lovey'为例子。string1的长度是6,string2为5,两个集合交集长度为0,所以就是2x0/(6+5)=0,也就是相似度为0。这里的集合与数学中的集合有一些区别,例如,数学中集合元素是不能有相同的,而这里是可以相同的。例如‘abcccd'与’ccc'的交集就是'ccc',交集长度为3。

def dice(string1,string2):
    s1=list(string1)
    s2=list(string2)
    s=0
    for i in s1:
        for j in s2:
            if i==j:
                s+=1 #计算两个字符串交集长度
            else:
                continue
    similarity=2*s/(len(s1)+len(s2))
    return similarity
a='rabbit'
b='lovely'
print(dice(a,b))

对于其它的用于度量集合之间距离或相似度的方法,我们也可以用于度量字符串的相似度,只要把字符串看出集合进行运算即可。

(4)J-W距离(Jaro-Winker distance)。

J-W距离是专门用于度量字符串相似度的方法,是Jaro distance的一个变种。而Jaro distance属于编辑距离的一类。这两种距离相互关联,并且都可以计算长度不同的字符串之间的距离。Jaro distance定义如下:

d_{j}=\begin{Bmatrix}0&if&m=0\\\frac{1}{3}(\frac{m}{|s_{1}|}+\frac{m}{|s_{2}|}+\frac{m-t}{m}) &otherwise\end{}

其中m是两个字符串匹配上的字符数目。t表示字符转换次数,是在匹配之后字符中换位数目的一半(准确来说是除以2的整数部分,如5//2=2);s1、s2代表两个字符串的长度。例如:匹配后得到的字符‘rabbit'与’rbabit',匹配个数m=6,s1,s2长度都是6,t=2/2=1。(换位表示两个字符串第i位出现a,b,在第j位又出现了b,a,则表示换位)。

关于两个字符的匹配,是相对比较复杂的。我们需要定义一个匹配窗口(matching window)。

Matching Window=[\frac{max(|s_{1}|,|s_{2}|)}{2}]-1

如果两个字符对应元素的索引距离小于这个匹配窗口数值,并且元素相同,就算作一个匹配。所有的匹配个数就是m。

兔兔在这里以‘CRT'与’RCCTB'为例。

CRT
RCCTB

我们计算窗口为[5/2]-1=[2.5]-1=1。那么对于第一个字符串中C,索引为0,那么它可以和第二个字符串中-1,0,1尝试匹配。-1不存在,0是R,与C不匹配,1是C,匹配;然后开始R,索引为1,可以和第二个字符串中索引为0,1,2尝试匹配,结果和R找到匹配;对于T,也能在窗口中找到相同的元素T。这样就完成了匹配。我们其实也可以从下面的字符串找上面字符串中的匹配,不过没有从短的字符串匹配长字符串直观,但结果是一样的。

匹配之后,就是‘CRT’与‘RCT’(即使匹配,第二个字符串中的元素还是要按照原来的相对位置排序)。所以C-R,R-C这两个位置就说明字符换位数目为2,t=2/2=1。m是匹配长度,这里也就是3。

那么,对应J-W距离,公式如下:

d_{w}=\begin{Bmatrix}d_{j}& if&d_{j}<b_{t} \\d_{j}+(lp(1-d_{j}))&otherwise\end{}

其中dj就是上面的Jaro distance。bt是boost threshold,如果dj大于bt值就计算第二行的式子,即J-W距离,如果小于bt就是Jard distance,bt一般设为0.7;p表示缩放因子,用于对l调整,避免dw超过1,一般设为0.1,最大不能超过0.25;l是字符串的起始最大公共前缀,最大不超过4。

class jaro_distance:
    def __init__(self,string1,string2):
        l1=len(list(string1))
        l2=len(list(string2))
        if l1<l2:
            self.list1=list(string1)
            self.list2=list(string2)
        else:
            self.list1=list(string2)
            self.list2=list(string1) #确保list1长度比list2短,之后用短的字符串匹配长的字符串
        self.s1=len(self.list1) #s1是字符串1长度(较短的那一个)
        self.s2=len(self.list2) #s2是字符串2长度(较长的那一个)
    def matching_window(self):
        '''计算匹配窗口'''
        a=max(self.s1,self.s2)/2
        mw=int(a)-1
        return mw
    def t(self,match):
        '''计算t'''
        match1=match.copy() #保存当前各个索引
        match.sort() #还原string匹配前的相对位置
        n=len(match)
        s=0
        for i in range(n):
            if match1[i] != match[i]:
                s+=1
        t=s//2
        return t
    def jaro_distance(self):
        '''计算jard主程序'''
        mw=self.matching_window() #匹配窗口长度
        match1=[] #储存匹配后字符串1中的元素
        match2=[] #储存字符串2被匹配的元素的索引位置
        for i in range(self.s1):
            if i-mw<0:
                for j in range(0,i+mw+1):
                    if self.list1[i]==self.list2[j]:
                        match1.append(self.list1[i])
                        match2.append(j)
            elif i-mw>=0 and i+mw+1<=self.s2:
                for j in range(i-mw,i+mw+1):
                    if self.list1[i]==self.list2[j]:
                        match1.append(self.list1[i])
                        match2.append(j)
            else:
                for j in range(i-mw,self.s2):
                    if self.list1[i] == self.list2[j]:
                        match1.append(self.list1[i])
                        match2.append(j)
        m=len(match1)
        t=self.t(match2)
        if m==0:
            return 0
        else:
            return (m/self.s1+m/self.s2+(m-t)/m)/3
x='CRT'
y='RCCTB'
a='rabbit'
b='lovely'
j=jaro_distance(x,y)
print(j.jaro_distance())

上面是计算两个字符串Jaro距离的代码,越相似,则数值越大。如果我们运行a,b的Jaro距离,结果为0,说明一点儿也不相似,直观上来看,两个字符串也的确没有相同元素。而x,y之间的相似度接近0.76,说明已经比较相似了。

class JW(jaro_distance):
    '''计算JW距离'''
    def __init__(self,string1,string2,bt=0.7,p=0.1):
        super(JW, self).__init__(string1,string2)
        self.bt=bt
        self.p=p
        self.dj=jaro_distance(string1,string2).jaro_distance()
    def l(self):
        '''计算两个字符串最大公共前缀,不超过4'''
        s=0
        for i in range(self.s1):
            if self.list1[i]==self.list2[i]:
                s+=1
        if s<=4:
            return s
        else:
            return 4
    def JW(self):
        l=self.l()
        dj=self.dj
        if dj<self.bt:
            return dj
        else:
            return dj+(l*self.p*(1-dj))

x='CRT'
y='RCCTB'
a='rabbit'
b='lovely'
j=jaro_distance(x,y)
jw=JW(a,b).JW()
print(jw)

上面代码用来计算J-W距离,用于它需要先计算jaro距离,并利用其中的数据,所以兔兔就采用调用父类的方法,效果也是可以的。

(5)莱文斯坦(levenshtein)距离。

levenshtein距离也叫做编辑距离,该算法体现的是动态规划的过程。它的思想是如何对一个字符串元素删除、增加、修改,从而变成另一个字符的最小操作次数(可以从前往后,也可以从后往前计算,下面演示是从后往前)。比如对于‘abc'与’abcd',我们只需要删除d,两个字符串就相等了,所以距离是1;如果是’abc'与‘abd',只需要更改一次,之后看倒数第2,1位置元素相同,不操作,所以距离也是1;如果是’abc','ascedc',那么就是先看最后一个c是相同的,不操作,也就是比较‘ab'与’asced'的距离,这时我们就要比较,是删除、增加、还是更改之后的两个字符串距离更近一些,对于相应三种操作之后的两个字符串,同样比较三种操作之后字符串距离。这个过程本质上是一个递归的过程,类似于汉诺塔结构。所以只需要写递归函数,里面包含终止条件与递归表达式即可。

class leven:
    def __init__(self,x,y):
        self.a=len(x)
        self.b=len(y)
        self.x=x
        self.y=y
    def lev(self,a,b):
        if min(a,b)==0: #递归终止条件
            return max(a,b)
        else:
            w=self.lev(a-1,b)+1
            y=self.lev(a,b-1)+1
            if self.x[a-1]==self.x[b-1]: #如果是更改,看这两个元素是否相同。
                z=self.lev(a-1,b-1) #如果相同,则不进行操作
            else:
                z=self.lev(a-1,b-1)+1 #如果不同,操作一次,加1
            return min(w,y,z) #选择这三种操作后距离最小的那一个
    def main(self):
        a=self.a
        b=self.b
        result=self.lev(a,b)
        return result
a='rabbit'
b='lovely'
d=leven(a,b).main()
print(d)

上面便是递归算法的实现。学习时尽量去考虑递归动态过程与终止条件,如果深入去想其中细节反而会越来越乱。

除了上面的方法,也有用莱文斯坦矩阵来进行计算的,结构如下:

rabbit
0123456
l1123456
o2223456
v3333456
e4444456
l5555556
y6666666

该矩阵从左上角到右上角进行填充,每次水平向右或竖直向下都对应着对字符串进行删除或插入,操作加1;而对于对角线,从左上格子走到右下格子,如果两个字符不匹配加1,匹配就是加0。操作之后,最终右下角的数值就是两个字符串的编辑距离了。

计算的时候是一直沿着对角线走,如果两个字符串不相同,当达到边界后再向下或向右即可,不要沿着其它的路径随意走。

如矩阵所示,从红色的0出发,每次往右或往下,格子数值加1,即黄色部分。

沿着对角线走,如果下一个对应两个元素不同(如r和l不同),那么就加1。沿着红色的对角线走,最终的6正好落在右下角,所以结果为6。

关于算法实现,也是比较容易实现的。

def leven(string1,string2):
    list1=list(string1)
    list2=list(string2)
    n1=len(list1)
    n2=len(list2)
    n=min(n1,n2)
    s=0
    for i in range(n):
        if list1[i]==list2[i]:
            s+=0
        else:
            s+=1
    s+=max(n1,n2)-n
    return s
a='abc'
b='ab'
print(leven(a,b))

总结:

关于序列的相似度分析,实际上是有非常多的方法的。对于长度相等的字符串,我们可以采用海明距离等方法,但是由于只能是相同的字符串,所以有局限性的;另一种的基于集合的距离定义来计算,不过我们需要去想如何形成两个序列的集合以及以何种方式来用新的方法表示两个字符串,从而能够用基于集合之间距离、余弦相似性甚至欧式距离等一系列方法来计算;最后是基于动态规划方法来计算,这类方法比较准确,能够用于长度不同的序列的相似度的计算,比如DNA序列相似性的计算,往往是比较准确的。这三大类方法往往在不同场合有着不同的应用,需要具体问题具体分析。

  • 4
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

生信小兔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值